forked from Public-Mirror/Textractor
Compare commits
1206 Commits
ithvnr-ver
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
df5b830d17 | ||
|
f3fbe04409 | ||
|
5adeb1fab7 | ||
|
5c7b41dd84 | ||
|
92e2e85191 | ||
|
0bc23ba2c7 | ||
|
fd8a090e28 | ||
|
aa00d9e789 | ||
|
cfa11c5646 | ||
|
446f6d44e6 | ||
|
ff1a7a358c | ||
|
60a5d74eda | ||
|
4cebe9462f | ||
|
d97ebd4fbb | ||
|
233e75225c | ||
|
72ad51f039 | ||
|
39fcfaf644 | ||
|
5eb0440f01 | ||
|
c4ca84d39b | ||
|
c7b818aa75 | ||
|
2cb45ab9e3 | ||
|
72132239a5 | ||
|
5eaafbe81c | ||
|
97f2bc6304 | ||
|
f732f488e6 | ||
|
3e283975c8 | ||
|
5b35f121d2 | ||
|
b28aa3d189 | ||
|
faeda02f2b | ||
|
ef55ef0c61 | ||
|
c9240d9e68 | ||
|
6ba4f9c909 | ||
|
7c9f861926 | ||
|
e158219266 | ||
|
3a9fdfb9ae | ||
|
dad036083a | ||
|
b09783d111 | ||
|
306b9f0525 | ||
|
823987e79f | ||
|
fdc8a13b36 | ||
|
90b58da867 | ||
|
2316f59415 | ||
|
06b93e3def | ||
|
fde588d922 | ||
|
8e0e17c59b | ||
|
e6ee5ecf3d | ||
|
49133974b3 | ||
|
4b67b8a5cf | ||
|
663bb97fff | ||
|
953a537cb8 | ||
|
8ac4a072a9 | ||
|
45279d26dc | ||
|
5df90e2c01 | ||
|
0346f95707 | ||
|
1f19a9e74d | ||
|
b236a50a8f | ||
|
2f08ab1e9b | ||
|
ad3f09d8ef | ||
|
44c8e4f94c | ||
|
a17d8d5993 | ||
|
bf49f0bca6 | ||
|
15db478e62 | ||
|
46bc6ec84b | ||
|
b3b665fb65 | ||
|
17b5884149 | ||
|
742b7cacf3 | ||
|
4ff150b674 | ||
|
e83579ed7c | ||
|
febff243d3 | ||
|
1a29f5670a | ||
|
6ed3d9c951 | ||
|
492c843e5c | ||
|
574eeec79e | ||
|
6c74df864a | ||
|
ca93120442 | ||
|
eddf1bf0ee | ||
|
c9ad8880fb | ||
|
fb9fb5d54a | ||
|
085bec6d5b | ||
|
695672ce9c | ||
|
bda08d500e | ||
|
5862bf50ac | ||
|
493e80e568 | ||
|
db31e19997 | ||
|
d64a2c05b4 | ||
|
f8874bf8a0 | ||
|
b28b68d218 | ||
|
50f5b183f1 | ||
|
a566818ad4 | ||
|
ac95d873f1 | ||
|
444974ee8a | ||
|
3b948c3359 | ||
|
1eaa054b33 | ||
|
647058e9aa | ||
|
918c877e04 | ||
|
2cf146652d | ||
|
89198fd4fe | ||
|
9e0c95be98 | ||
|
7425ae770f | ||
|
e0981f89e8 | ||
|
eb1421c143 | ||
|
a1d3abb080 | ||
|
dd4b8cfbb5 | ||
|
615d372eeb | ||
|
e4cea83ae4 | ||
|
9e26c462e7 | ||
|
0df2526560 | ||
|
de3cad37f8 | ||
|
8c9faf9947 | ||
|
76fea31fb3 | ||
|
795ecce45e | ||
|
675695cde1 | ||
|
9f8e523ce3 | ||
|
d74dcdc286 | ||
|
b4aa113fac | ||
|
1e03e69d00 | ||
|
76d716956d | ||
|
1782292662 | ||
|
3c33d11d80 | ||
|
a8ae2156d9 | ||
|
f26f30dce7 | ||
|
1fcacc7bc9 | ||
|
71fe1410c2 | ||
|
aa0c0e0047 | ||
|
74121d7484 | ||
|
8543d49192 | ||
|
5537679442 | ||
|
20f35a02ed | ||
|
1bab6956a8 | ||
|
acc85f3a86 | ||
|
54a285b53b | ||
|
bf97055155 | ||
|
ad83eb290a | ||
|
c7ae06c121 | ||
|
fdc53a393d | ||
|
68b382e5fa | ||
|
9e6f638488 | ||
|
fcd735283d | ||
|
a03c939043 | ||
|
faa14f8cb9 | ||
|
e00b831e3d | ||
|
084f26a72d | ||
|
4219115a40 | ||
|
43aa1d6711 | ||
|
662ca45321 | ||
|
dcff7e550c | ||
|
c62c586a59 | ||
|
fc81b17a3c | ||
|
3a34f989e5 | ||
|
2c9ac1da3c | ||
|
95b145bece | ||
|
457aed96c2 | ||
|
4783469a0a | ||
|
0680de413a | ||
|
29d23935ad | ||
|
3aee03a244 | ||
|
268f40771d | ||
|
7eb45899c0 | ||
|
ea6ddc7a7c | ||
|
6958b22e68 | ||
|
9bb7fbff06 | ||
|
1b71b3ee86 | ||
|
1634120669 | ||
|
f8b4bc1ae3 | ||
|
01b6b6de16 | ||
|
4b46057800 | ||
|
93b8711bf8 | ||
|
8ab1e576bd | ||
|
3af4217075 | ||
|
2ab780a491 | ||
|
73d1f21bc1 | ||
|
a3ebaf0023 | ||
|
f0a1690c92 | ||
|
6b54ec0733 | ||
|
fe1cdfc947 | ||
|
f6742de3d5 | ||
|
dc1f819952 | ||
|
8e67827ae6 | ||
|
93238e13ed | ||
|
ef9d05f10e | ||
|
b4303e4d4d | ||
|
39b0882bbf | ||
|
79058c6811 | ||
|
681fc5b628 | ||
|
cc7d02d8a1 | ||
|
409ad121ae | ||
|
ae35f82199 | ||
|
51b217169e | ||
|
261da66d74 | ||
|
fe3e152553 | ||
|
341b6915c8 | ||
|
6da63208c7 | ||
|
9c006bce17 | ||
|
c3c73d2ac2 | ||
|
ae5fa5fb9f | ||
|
d86f22877c | ||
|
287776903a | ||
|
166ec1456b | ||
|
5739af5a3f | ||
|
4b0fc12794 | ||
|
3e21a6b4ba | ||
|
a2d1374ad4 | ||
|
a129394fb5 | ||
|
e20a12862c | ||
|
9711ce94c9 | ||
|
535dac480a | ||
|
d1887db2a5 | ||
|
386e09a81a | ||
|
4a320a3659 | ||
|
26960cf099 | ||
|
86415fca10 | ||
|
ecab473482 | ||
|
accd231c64 | ||
|
8a710659dc | ||
|
87c056a5b3 | ||
|
19f35f743c | ||
|
bb0df71da2 | ||
|
af2ba72f39 | ||
|
402b685820 | ||
|
e4d9b70040 | ||
|
bc01179626 | ||
|
d1d453f359 | ||
|
ac4cec9708 | ||
|
df3d3a0355 | ||
|
39e81bb904 | ||
|
d6412ca0b0 | ||
|
b9081f2472 | ||
|
a0306fea8b | ||
|
2be920c50e | ||
|
3ff31e0ac6 | ||
|
aa4b980422 | ||
|
5fb78b87c7 | ||
|
99be5fd40f | ||
|
6bd0c74c83 | ||
|
b4b26d50ca | ||
|
b515a17767 | ||
|
6a61342d19 | ||
|
fcb525df36 | ||
|
49432f689d | ||
|
74b4ae9da5 | ||
|
47836f20f3 | ||
|
360254d5bf | ||
|
b7978ff512 | ||
|
0d4af5c140 | ||
|
ffff4f2eb9 | ||
|
0688daf1da | ||
|
a490ebc274 | ||
|
a40ae621af | ||
|
b79af59e3d | ||
|
c2725e8518 | ||
|
22e0d38a1e | ||
|
f6cf3c9c62 | ||
|
16540bfe69 | ||
|
200c01f4ab | ||
|
d6b0114f27 | ||
|
73ccb38641 | ||
|
c8853a1af8 | ||
|
565f99cced | ||
|
dc48f2a3c8 | ||
|
2e23d4016d | ||
|
2d2a3dedb9 | ||
|
b61272a5e6 | ||
|
b0eeca5b36 | ||
|
3d88f2b9ec | ||
|
e14c9b6a09 | ||
|
666b0a49c0 | ||
|
6f01d46315 | ||
|
1300f3c540 | ||
|
42e4973721 | ||
|
b1769b7cd1 | ||
|
0f7a648403 | ||
|
a8202893e3 | ||
|
0ace753199 | ||
|
81d5b745aa | ||
|
622a7214f3 | ||
|
2482c26316 | ||
|
8e719b2964 | ||
|
cbd5b01385 | ||
|
7b3136dcc7 | ||
|
34feb59e8c | ||
|
4409fb88b0 | ||
|
3414e14bc4 | ||
|
6eda5651a4 | ||
|
5c8edc63d6 | ||
|
aeb0cafb15 | ||
|
b7b9a315c4 | ||
|
3db37ac450 | ||
|
bf2838f3fc | ||
|
06d5242842 | ||
|
6a6c208c20 | ||
|
9e86ecb987 | ||
|
415ceb4694 | ||
|
822c2ae233 | ||
|
07bd54d726 | ||
|
61f67766a5 | ||
|
a5e5417791 | ||
|
2436571768 | ||
|
1a17043924 | ||
|
0f2bbaace3 | ||
|
b685bfc780 | ||
|
8b345a4377 | ||
|
17aa20109f | ||
|
cc543a682c | ||
|
54f60f7657 | ||
|
268da6e41a | ||
|
1aa381dff3 | ||
|
f9e1e61d51 | ||
|
46c4f163ba | ||
|
f7f6b18ad2 | ||
|
ab916126b9 | ||
|
90bed9c2db | ||
|
770f219078 | ||
|
0c9fc5c08a | ||
|
5d77350e3c | ||
|
b7c9f0bfce | ||
|
9415e83511 | ||
|
96f235732c | ||
|
3794afc7c4 | ||
|
3d7e1be0ae | ||
|
1cf60785e3 | ||
|
590c08e8d0 | ||
|
d3da9c06e3 | ||
|
562c6e1a3a | ||
|
3f004554f8 | ||
|
2333692448 | ||
|
c3274b8275 | ||
|
c5b1e6988e | ||
|
749f8fa596 | ||
|
58f58b02d8 | ||
|
4e3c1a93e0 | ||
|
e4c74a55fe | ||
|
a8c239498c | ||
|
594198c921 | ||
|
2e99d6b3f2 | ||
|
60372a6b8a | ||
|
488a19134a | ||
|
c38551905d | ||
|
f1e1257f21 | ||
|
8f6595e869 | ||
|
29367928c3 | ||
|
4ab197c374 | ||
|
e20f16da80 | ||
|
703fc47972 | ||
|
b4c7abd0a8 | ||
|
fe9c9435c5 | ||
|
e498442eb4 | ||
|
b7b263ba3b | ||
|
9df97804ed | ||
|
79cd50ade7 | ||
|
68e915192f | ||
|
c49c57dded | ||
|
25d36ff301 | ||
|
ad20ca360d | ||
|
e896cb033f | ||
|
b098a05a23 | ||
|
f85e3a3841 | ||
|
6a015f04bc | ||
|
f56e9ff07c | ||
|
5d83c9736b | ||
|
c074bbf506 | ||
|
011a5418e9 | ||
|
c5c115c5da | ||
|
a3bb8dcf27 | ||
|
ce33ca5732 | ||
|
447400980e | ||
|
d0dd8fe3ae | ||
|
7a5369365f | ||
|
4192bfc412 | ||
|
e7b5c1eb0c | ||
|
68c65bbea6 | ||
|
f87da8aedf | ||
|
50af685d96 | ||
|
fc2676e403 | ||
|
5f5c5d5a1a | ||
|
ad846b90d4 | ||
|
c4913fa897 | ||
|
c03b4303db | ||
|
881b5ea3a7 | ||
|
91f282e75e | ||
|
9982c0632d | ||
|
0b0416efdb | ||
|
f1d46e9f73 | ||
|
6bac544c89 | ||
|
38b6558ba7 | ||
|
0ca9e301a0 | ||
|
ea8e8fe3ed | ||
|
f7c2641d58 | ||
|
8db76f86e0 | ||
|
c915432763 | ||
|
d9ba1e5854 | ||
|
ddd2109399 | ||
|
68534619aa | ||
|
e14ada3957 | ||
|
5cc09d43ac | ||
|
7b602393de | ||
|
c85618cd0d | ||
|
566c0beb0a | ||
|
3a103890ad | ||
|
28d14bcc32 | ||
|
a9a9c0cea5 | ||
|
88b797cd33 | ||
|
ef90382bbb | ||
|
f86a776536 | ||
|
b4c5b31482 | ||
|
07d25ef25c | ||
|
0c61b0afcf | ||
|
94760703f9 | ||
|
5d2c1339d5 | ||
|
50a505404b | ||
|
67a5f42c51 | ||
|
b5c319ee46 | ||
|
140c9057ea | ||
|
76804dd0aa | ||
|
e7fff79f8f | ||
|
ce88341cce | ||
|
58c80c82ba | ||
|
fefa08c519 | ||
|
516881d409 | ||
|
64eee8f049 | ||
|
c5a6e16488 | ||
|
c808e7594d | ||
|
ae0398a243 | ||
|
0f44a0c6f8 | ||
|
d56a4196b2 | ||
|
d25a61e9e6 | ||
|
121606af14 | ||
|
e453b571ba | ||
|
440474d2ee | ||
|
907c43db4a | ||
|
5e27de842b | ||
|
7964623ec8 | ||
|
d4d6c6c8b6 | ||
|
958ce10327 | ||
|
b8bd602474 | ||
|
b18fe3ddd0 | ||
|
e107eff849 | ||
|
64bfb4596e | ||
|
cf90539d09 | ||
|
d16db4d319 | ||
|
e529046d7d | ||
|
94a8e2ce62 | ||
|
3a36dbca1d | ||
|
7cdbd93dcb | ||
|
494fcd24c3 | ||
|
02f127e80c | ||
|
9668b01192 | ||
|
125d160ef6 | ||
|
8d763e4b3d | ||
|
15dad81627 | ||
|
9a1f123611 | ||
|
44558a6059 | ||
|
0c05b73b7c | ||
|
e4431e2131 | ||
|
078778bebd | ||
|
2517393688 | ||
|
1af629b2d2 | ||
|
096a1b49a7 | ||
|
6975145db9 | ||
|
b7fe41f537 | ||
|
93e3e2f82f | ||
|
4d15ecf7c2 | ||
|
6264cd3577 | ||
|
e3c70a721f | ||
|
54e8cbcf82 | ||
|
bb7a36bcad | ||
|
23a6b116cd | ||
|
a38cbe2769 | ||
|
c7edfef9ad | ||
|
aa3b9ee14f | ||
|
0a50a9b70d | ||
|
fbe190f39c | ||
|
49e915c3b6 | ||
|
2f7365acbd | ||
|
a8100b4d8c | ||
|
e8607e576d | ||
|
c6b7c6ba22 | ||
|
1d4e1aabd5 | ||
|
184df2f781 | ||
|
ce2b413f5b | ||
|
be08c65799 | ||
|
a40f814843 | ||
|
e73dbed498 | ||
|
f53950581a | ||
|
21fd3e1d59 | ||
|
09552fe7c7 | ||
|
7e8dfc0332 | ||
|
0345181484 | ||
|
cf423fa525 | ||
|
0cf2af47a6 | ||
|
259ab2cb16 | ||
|
d26a384734 | ||
|
4c1cac7dbb | ||
|
f62f90a068 | ||
|
ec0b9c077c | ||
|
9900c09483 | ||
|
7b82aa62aa | ||
|
08cd3959fa | ||
|
121eddc290 | ||
|
107544df70 | ||
|
f6a82984c2 | ||
|
40e62a13d6 | ||
|
241d5906e4 | ||
|
a64fa9b3dd | ||
|
1f5edecbf6 | ||
|
25d2811c0f | ||
|
29f616ae9d | ||
|
8016ac178a | ||
|
3c4a8920fe | ||
|
bcc384ff53 | ||
|
aef011a713 | ||
|
146afdc926 | ||
|
e826801c87 | ||
|
48ea8f531b | ||
|
c9a7a606cb | ||
|
8e40c71563 | ||
|
ef8783d82d | ||
|
3c7b3d728c | ||
|
c7ff5f637a | ||
|
9ed3900512 | ||
|
95a988f19b | ||
|
3504f6cc4a | ||
|
913074e6af | ||
|
855f8ebfe5 | ||
|
4eefafeae9 | ||
|
a5bbf0bb62 | ||
|
510acdeca3 | ||
|
f1ab7cd208 | ||
|
a4133ce243 | ||
|
99d7d69b75 | ||
|
551d5ba49d | ||
|
e494ebc077 | ||
|
f21db0925c | ||
|
72b9a1f8ed | ||
|
6c47a27351 | ||
|
4a7b29de72 | ||
|
ceeeced02f | ||
|
cf34160bbe | ||
|
a07e10344f | ||
|
d6b39eb2c3 | ||
|
5e53e63e07 | ||
|
ac86323010 | ||
|
4b7452bef2 | ||
|
e6805a2be3 | ||
|
c78747c228 | ||
|
b9a4a94d61 | ||
|
1759b74fd3 | ||
|
a7a4a80913 | ||
|
dcfbeafd44 | ||
|
f74cd553c0 | ||
|
b7ec42ee4e | ||
|
67d88c48a7 | ||
|
4e8a8cce25 | ||
|
d9e33b9be2 | ||
|
b7e8b68838 | ||
|
3f1b2fcccb | ||
|
84bf8c659f | ||
|
c864425c36 | ||
|
29a3bb0a6f | ||
|
c89b05cf42 | ||
|
22d3e3f3a5 | ||
|
a05a4d41b1 | ||
|
b61cef5386 | ||
|
67c3e572d7 | ||
|
5aa4b982fd | ||
|
0d9976b0c4 | ||
|
4e9df97991 | ||
|
7469e9f115 | ||
|
4d68c45f24 | ||
|
e398fdf94e | ||
|
69f5522492 | ||
|
c596116a03 | ||
|
b873ecbc96 | ||
|
7e4cc13bcf | ||
|
f52104009e | ||
|
a4e7359d83 | ||
|
413c5b17cf | ||
|
c57e97bd83 | ||
|
b9590a1c36 | ||
|
7fcf994a00 | ||
|
632139dce2 | ||
|
ba83760157 | ||
|
ac6294f30e | ||
|
af8a069243 | ||
|
38d011271e | ||
|
4e65d29978 | ||
|
df3ba5dec8 | ||
|
126b48c51e | ||
|
cba353438d | ||
|
c0e8a638ba | ||
|
4e85b8b479 | ||
|
cb2415c460 | ||
|
027d5962f8 | ||
|
7e080b1b44 | ||
|
d1917ed9a6 | ||
|
3101147b6d | ||
|
c7df716df8 | ||
|
3cb8016b2c | ||
|
cea9edab55 | ||
|
332e5aecaf | ||
|
7c3ca92db3 | ||
|
dad7f23532 | ||
|
a7a19ef017 | ||
|
2e5ee9e547 | ||
|
25eb1f5956 | ||
|
01421fa88b | ||
|
50472a98ea | ||
|
64e3d901c8 | ||
|
650e2077ca | ||
|
69435f8a9f | ||
|
a84ea357ec | ||
|
6a357b5e32 | ||
|
75454b3fa5 | ||
|
8e80543e2e | ||
|
d8429851cb | ||
|
0f7fe46980 | ||
|
54afbb24a5 | ||
|
5903bbe2e4 | ||
|
cdfbd77d21 | ||
|
24d9244ad4 | ||
|
0afdafb3d1 | ||
|
84e9beea63 | ||
|
f1e7b4dc70 | ||
|
a9249111c0 | ||
|
6247a4ed35 | ||
|
8bb7a1ba5d | ||
|
103c805cfe | ||
|
49d9d7a0c4 | ||
|
f5d8d43149 | ||
|
b06dc8ff82 | ||
|
24491e1bc3 | ||
|
a53af3d0fd | ||
|
479c73d029 | ||
|
3dd7be65ff | ||
|
f080656e60 | ||
|
8880d27dc5 | ||
|
9216548848 | ||
|
b87b4c0b41 | ||
|
e8b378da75 | ||
|
54c1b508d5 | ||
|
bdc083a62a | ||
|
96e9b66e9f | ||
|
8a88ad3c95 | ||
|
bf4eb4df8b | ||
|
6c0f4be45b | ||
|
a5057d8671 | ||
|
807e081bee | ||
|
4ce7425034 | ||
|
978bf2a6b8 | ||
|
b191e511eb | ||
|
1817a3ac53 | ||
|
19e7df5631 | ||
|
1c391e3a4b | ||
|
507e0bc1e6 | ||
|
7015ef0c53 | ||
|
afea96e9d5 | ||
|
4ea5c9cb32 | ||
|
e0507b85d3 | ||
|
7cd464661b | ||
|
7ec7253ec3 | ||
|
4e4cf00884 | ||
|
6bef925920 | ||
|
c22a30400a | ||
|
e0b0fa10ad | ||
|
f184654883 | ||
|
240f5f235b | ||
|
5523644ef9 | ||
|
3c67f5a15d | ||
|
b82d8d5523 | ||
|
464c001773 | ||
|
29822c89c8 | ||
|
4079720307 | ||
|
465dde33d5 | ||
|
c747e68344 | ||
|
e7e81f09bf | ||
|
86b2014a12 | ||
|
606a36968b | ||
|
97f3f51758 | ||
|
dfb45a3699 | ||
|
a3ac850bf4 | ||
|
c92a67dbe3 | ||
|
651cd486eb | ||
|
36ad3aa05d | ||
|
5cf12dfc27 | ||
|
be185233d1 | ||
|
0a3c397b4e | ||
|
a0b8b78727 | ||
|
5f87dae972 | ||
|
4efaa9f34f | ||
|
aae330ebef | ||
|
9068228427 | ||
|
74383ee0d8 | ||
|
f409ee78ce | ||
|
dca006b28c | ||
|
de109d0840 | ||
|
cc1d2b0414 | ||
|
5ef1ff6352 | ||
|
0f903940bc | ||
|
385af259a9 | ||
|
2f544afaa5 | ||
|
bc644bbe4e | ||
|
2f43249aa8 | ||
|
90d32c011d | ||
|
7693f1373d | ||
|
0ddd4a3461 | ||
|
7310f9349b | ||
|
026912ca9c | ||
|
bb86bde8a5 | ||
|
943e53e9a2 | ||
|
966620429e | ||
|
efa8d26ada | ||
|
24e31247af | ||
|
f48355c096 | ||
|
7a2ceec29e | ||
|
c50e2992bf | ||
|
49c4af8c2c | ||
|
c105f6700f | ||
|
9f85edf704 | ||
|
b80f795143 | ||
|
902ded684d | ||
|
95ca747912 | ||
|
523008d7e0 | ||
|
59869dc45a | ||
|
0492cb9549 | ||
|
d9a3d5cefd | ||
|
1d17902c64 | ||
|
c39356473d | ||
|
6866e6bfe9 | ||
|
d0f48a67a4 | ||
|
f351148b3d | ||
|
97d0528550 | ||
|
303140dfbf | ||
|
e489c38990 | ||
|
22bb1420c1 | ||
|
ddc8313d37 | ||
|
219804cecc | ||
|
1edc6ab1a7 | ||
|
3b5ad6edfe | ||
|
0c7103964d | ||
|
3db253c790 | ||
|
46ffaf017c | ||
|
a48815e9f4 | ||
|
3cb65dba63 | ||
|
6b2dcaa957 | ||
|
43a0bac59f | ||
|
ff44cc5f0c | ||
|
b218e241da | ||
|
62541968aa | ||
|
5468e44929 | ||
|
e598ae731e | ||
|
49dc067492 | ||
|
d88900aa46 | ||
|
1d90787564 | ||
|
66d02a7bf0 | ||
|
1ca9b3c8e6 | ||
|
83e8c2ecde | ||
|
0160578c2d | ||
|
68cb554102 | ||
|
d42dcc1705 | ||
|
b74ca7e20b | ||
|
a99131fa23 | ||
|
71f7e9bf29 | ||
|
5167ec3a6b | ||
|
534ca4ec35 | ||
|
c1198aeefd | ||
|
7696d45475 | ||
|
5caf42a6b1 | ||
|
ee0a8c4887 | ||
|
4b82d545bf | ||
|
942cdd94ba | ||
|
6ec8e7c19e | ||
|
ce225fd900 | ||
|
e48adc14bb | ||
|
aa2d71a078 | ||
|
c877d9cd31 | ||
|
1915008d00 | ||
|
8e45b35ebe | ||
|
69e01dab7c | ||
|
dddbc00694 | ||
|
6bc8143980 | ||
|
3a3bdfd48e | ||
|
1e2f6e14ec | ||
|
580ab0a75c | ||
|
26dbab020d | ||
|
e921062684 | ||
|
0ea99ed732 | ||
|
60be482634 | ||
|
74f2bc1c4d | ||
|
16666ac699 | ||
|
8f60060e37 | ||
|
d71fe0f147 | ||
|
3fc5cb17bd | ||
|
5e6d67691c | ||
|
104f9a7b8c | ||
|
3ad311293f | ||
|
abba2d77e3 | ||
|
4adf820d58 | ||
|
d307b7af2e | ||
|
273411d22e | ||
|
e79bbe4c03 | ||
|
af34731880 | ||
|
6d35e7ea67 | ||
|
199844987b | ||
|
f7e3bbeb02 | ||
|
3b9ca65e39 | ||
|
effb005376 | ||
|
ec1bd5622e | ||
|
7304727c17 | ||
|
59effb32dd | ||
|
1325d41e35 | ||
|
39a0161989 | ||
|
586e22e4a5 | ||
|
69511ddeda | ||
|
16e17526c7 | ||
|
59be83eee8 | ||
|
967f75a5ec | ||
|
0166df7209 | ||
|
d07dd5c641 | ||
|
5a464fc083 | ||
|
9929c3fac1 | ||
|
a21ee5bba4 | ||
|
e126e4c4f3 | ||
|
0b7db9e5ca | ||
|
5f74684b46 | ||
|
68bafcdda7 | ||
|
effe03fd96 | ||
|
2947c5a497 | ||
|
98e27bec60 | ||
|
257e617eb6 | ||
|
750638e52d | ||
|
f4c068fa29 | ||
|
e244799a72 | ||
|
4f38287bea | ||
|
d506b3edd9 | ||
|
8a3a494a14 | ||
|
23736478c0 | ||
|
baa7923be2 | ||
|
8cd4ea4a29 | ||
|
923bb15f3d | ||
|
d2c9e5a6f5 | ||
|
e31c9563ef | ||
|
6b1bb33b66 | ||
|
1dfaa759b9 | ||
|
3df68a6c2c | ||
|
524a0f362d | ||
|
cd3cb280d5 | ||
|
310d12ea14 | ||
|
22c4f10c82 | ||
|
665c5b3eea | ||
|
38ea5dba36 | ||
|
e5dcd33680 | ||
|
4dc7b236c9 | ||
|
7a059cc2e0 | ||
|
a2cc3bab28 | ||
|
c5d5976b47 | ||
|
4bc0c834d7 | ||
|
e2f83d47b9 | ||
|
28172e2cea | ||
|
aa4e289cc8 | ||
|
a36c9b4f52 | ||
|
bb48b1e64b | ||
|
6ae86a5988 | ||
|
50c488f109 | ||
|
878e4b443f | ||
|
6d59ad763c | ||
|
329d56c969 | ||
|
20956f6412 | ||
|
6100134646 | ||
|
23b4cffbb3 | ||
|
0fe18a43ac | ||
|
2ad278255d | ||
|
a455869837 | ||
|
7d49166dc4 | ||
|
64cbe4e5d6 | ||
|
8e0f5697f8 | ||
|
bd63cf3994 | ||
|
29586a4f41 | ||
|
baebb238ad | ||
|
79b2782dab | ||
|
34082f31ed | ||
|
72c37c9f9e | ||
|
a59942fde2 | ||
|
c2c96746be | ||
|
5b31e9fed9 | ||
|
abfa150513 | ||
|
0c4bb85f50 | ||
|
6ca7eb3f54 | ||
|
a6151f4b24 | ||
|
0e9d9210a1 | ||
|
c0b294de0f | ||
|
ee14e73541 | ||
|
030cdd6a03 | ||
|
7c2bddcc98 | ||
|
072840e307 | ||
|
88d67c5378 | ||
|
50fc7e24c6 | ||
|
ee0a953848 | ||
|
d31748c105 | ||
|
ca749691f8 | ||
|
a978df2655 | ||
|
4854cd35f1 | ||
|
4f1bc5a9b2 | ||
|
a40222285a | ||
|
72c37dee88 | ||
|
8342850bb7 | ||
|
6a25a41d79 | ||
|
02c163f930 | ||
|
166c9681e4 | ||
|
53cf099bc6 | ||
|
728c7a798b | ||
|
0b8e2a217b | ||
|
383fe554c9 | ||
|
fcb3fd63d3 | ||
|
189691a562 | ||
|
d1007097ad | ||
|
a36ba41eb4 | ||
|
79bcc7d6e1 | ||
|
41f80622cb | ||
|
88b2d85a75 | ||
|
4979349a56 | ||
|
4660be2efb | ||
|
e92d260b31 | ||
|
39ef47d85a | ||
|
41e4b9fd9a | ||
|
0509bc13a2 | ||
|
0e0d44961c | ||
|
6d670324f5 | ||
|
33210a95ac | ||
|
72221106ac | ||
|
3f35c5c230 | ||
|
2be762b708 | ||
|
cb55ef9a86 | ||
|
f8ba6102e3 | ||
|
a93a7dc444 | ||
|
70ae03279c | ||
|
a8cebd1b86 | ||
|
61fb3248fe | ||
|
411e4b17dc | ||
|
13c2bfed2f | ||
|
4b3edeef07 | ||
|
db6b09a50e | ||
|
65bb4b35cf | ||
|
4310e97236 | ||
|
679317b5f5 | ||
|
389db72dd2 | ||
|
446ff71464 | ||
|
98649ffbcc | ||
|
fc00a3c1ed | ||
|
6a3c07ee63 | ||
|
17b057adc0 | ||
|
e621155e25 | ||
|
588b013392 | ||
|
cda2cefc28 | ||
|
29eb1dcfe6 | ||
|
5984ae514b | ||
|
4c5814d1a7 | ||
|
2a222c4446 | ||
|
af1512b7bf | ||
|
e35e26c2eb | ||
|
51dc229edc | ||
|
a89e33c68a | ||
|
911fb991be | ||
|
67e5ff31d1 | ||
|
fffb63edf4 | ||
|
1312a80ddd | ||
|
2c37a50f7b | ||
|
d03bd93bec | ||
|
52a4fb348b | ||
|
47ea350eaa | ||
|
19d7cdb180 | ||
|
0b3599cb84 | ||
|
4e10a597c9 | ||
|
2f09b534d2 | ||
|
dc2643ffa7 | ||
|
91ac0825f8 | ||
|
3f7a0fdd52 | ||
|
8711332f77 | ||
|
9733cca628 | ||
|
8a1c946abd | ||
|
c1005a9736 | ||
|
715bf9feaa | ||
|
73eedc4177 | ||
|
f1a2beef4f | ||
|
44a0367883 | ||
|
34acf4f300 | ||
|
b290392034 | ||
|
16cf2eb9b2 | ||
|
0f3195a81c | ||
|
2be7f72813 | ||
|
c431cda7d7 | ||
|
42546e5215 | ||
|
12c3262f95 | ||
|
751533bdc8 | ||
|
cf29840c1a | ||
|
0634f3dab5 | ||
|
0db5944c81 | ||
|
5e630f76f5 | ||
|
cb7649177f | ||
|
707bc2cdf4 | ||
|
d535ece5a3 | ||
|
2fcdf141c5 | ||
|
7353a95a7e | ||
|
e06b8f27c8 | ||
|
dd78e555f8 | ||
|
b8a76d79b7 | ||
|
1150d83474 | ||
|
143a2f091f | ||
|
4b70c62d64 | ||
|
c3707de5b5 | ||
|
cda2914a27 | ||
|
0d3597a91d | ||
|
a55f04d628 | ||
|
f09bac71d2 | ||
|
7a336af56c | ||
|
8e38b3a970 | ||
|
e86817fb15 | ||
|
584817744b | ||
|
d85ba3911b | ||
|
aba5dadc6a | ||
|
959d7c2dd5 | ||
|
7739c45458 | ||
|
ed4879ba5c | ||
|
9a299f357f | ||
|
c32066e43c | ||
|
bc6c1325ed | ||
|
f22ccbd909 | ||
|
3a0b0bb77e | ||
|
df9fb07395 | ||
|
1b9febb4a5 | ||
|
5edfb4de0d | ||
|
60fb17155e | ||
|
1e58842ca9 | ||
|
c7f119e0a2 | ||
|
6835c339d7 | ||
|
afe0464b56 | ||
|
31dafc4416 | ||
|
42585984c0 | ||
|
24407f8979 | ||
|
11d75c2987 | ||
|
971e35dc10 | ||
|
b1b4625a06 | ||
|
634bb04f48 | ||
|
bc8ba90838 | ||
|
f63bd97f71 | ||
|
e4d74fe6ab | ||
|
732cbce106 | ||
|
b5eef8b3fe | ||
|
7a39c40936 | ||
|
cf2a658ac7 | ||
|
ff2fc74c5a | ||
|
5d297404f6 | ||
|
dbe7bed0d0 | ||
|
4acd030e54 | ||
|
7808fd508c | ||
|
5fbf92bc07 | ||
|
41566e68c1 | ||
|
f3c4884660 | ||
|
d0b1efd033 | ||
|
9c5be4be80 | ||
|
11016d146a | ||
|
11d6a156dd | ||
|
a85c398f47 | ||
|
541e3cc4e7 | ||
|
97fe9800a6 | ||
|
fe30b77a44 | ||
|
ffeb4e2dad | ||
|
e12a35a609 | ||
|
e500c9ceed | ||
|
3d4ee3d698 | ||
|
89f8cb9d9b | ||
|
ff6dd7c7d8 | ||
|
a01e7c70a8 | ||
|
fd82471303 | ||
|
4543a86e48 | ||
|
2905ed4f9c | ||
|
3f6348e7d0 | ||
|
a4eede1b4e | ||
|
4aef775a6a | ||
|
a2924f4a2c | ||
|
a5232b605e | ||
|
af19b4e481 | ||
|
224af91806 | ||
|
dd6230c92d | ||
|
a055db4bf8 | ||
|
a8f2ca79a5 | ||
|
314c17b435 | ||
|
62b5f5fe8f | ||
|
a8cdee67e8 | ||
|
1380f64cec | ||
|
ebc1303e9a | ||
|
933a225a12 | ||
|
9407c96be3 | ||
|
b7bf5b91f0 | ||
|
61ca07d29b | ||
|
f132e01b7a | ||
|
9dfebf7eac | ||
|
a290c36b52 | ||
|
f4980d6eb6 | ||
|
379024afc0 | ||
|
5ea77e23a5 | ||
|
d6a90e5426 | ||
|
5ae1e811d7 | ||
|
7df8e40fc9 | ||
|
a7a3379050 | ||
|
55764bf8e2 | ||
|
097e274347 | ||
|
e4f5f3cfd5 | ||
|
05a14d201e | ||
|
775af2da54 | ||
|
7a6fe61edc | ||
|
6598b979b3 | ||
|
48450f9717 | ||
|
2065359a4e | ||
|
904804de28 | ||
|
4368826f2a | ||
|
a52bfd36f8 | ||
|
ffbb081bcc | ||
|
ae6656441b | ||
|
803a1cd4bf | ||
|
8a11cc9949 | ||
|
8e615704ca | ||
|
af2708217c | ||
|
c87729814a | ||
|
7802195193 | ||
|
18b9c7bd38 | ||
|
b903d77730 | ||
|
5ec35dcbae | ||
|
ef36cb9274 | ||
|
a05714fdae | ||
|
5e3230373e | ||
|
758e729f30 | ||
|
423ee9efac | ||
|
9da6875444 | ||
|
3d342503f0 | ||
|
1a2672604a | ||
|
9bc95191ff | ||
|
3a001e66b2 | ||
|
faa4eb55e0 | ||
|
37d29586d6 | ||
|
1e83b50fdf | ||
|
545b6f9492 | ||
|
29e6229326 | ||
|
393341a443 | ||
|
534838342a | ||
|
c6ca4696f1 | ||
|
3a0a24878f | ||
|
acf639dd45 | ||
|
6a199a8246 | ||
|
4456e1c35c | ||
|
e3f64131f6 | ||
|
8cfb3def97 | ||
|
39e439e4b1 | ||
|
a422fca50d | ||
|
73246f7eea | ||
|
5890f0022c | ||
|
b7764ad860 | ||
|
d89cc56d6f | ||
|
c393d29115 | ||
|
c5d847f310 | ||
|
47dccbf218 | ||
|
162b2cb79f | ||
|
1dbaff780c | ||
|
6d7a800a56 | ||
|
32bcee8f50 | ||
|
30f95423f1 | ||
|
3ac79fb9c5 | ||
|
b6a43e7283 | ||
|
27fa8c7db7 | ||
|
efd162ae40 | ||
|
2ea47a56cb | ||
|
5f24f1efd7 | ||
|
c80f36efff | ||
|
bb0133bc58 | ||
|
2cf987bef5 | ||
|
79d39ce135 | ||
|
4e373df13b | ||
|
a1962865f8 | ||
|
b59a962f4a | ||
|
337038d684 | ||
|
3a6b98b48b | ||
|
fb56457cdb | ||
|
aaf33094d1 | ||
|
97bca014e4 | ||
|
1a2156f502 | ||
|
ca6ec15ac8 | ||
|
f8cb81ec59 | ||
|
0fb2c3a9c1 | ||
|
9715612b83 | ||
|
0b79811cc3 | ||
|
553059ee0f | ||
|
2591a8c124 | ||
|
fe552f6e19 | ||
|
b3496222ec | ||
|
1fb66ddc04 | ||
|
dff4ae3463 | ||
|
60866c158a | ||
|
ef1884701f | ||
|
7aa4e51803 | ||
|
2ba72fcbfe | ||
|
50d7cdf8fb | ||
|
1ab516a1b8 | ||
|
6dab2871eb | ||
|
65ec2d5be0 | ||
|
05fa52f589 | ||
|
290ef57490 | ||
|
c7a79e6064 | ||
|
38a57eb480 |
39
.appveyor.yml
Normal file
39
.appveyor.yml
Normal file
@ -0,0 +1,39 @@
|
||||
version: '{branch}-{build}'
|
||||
configuration: RelWithDebInfo
|
||||
clone_folder: C:\Textractor
|
||||
skip_branch_with_pr: false
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||
arch: x64
|
||||
platform: x64
|
||||
qtbin: msvc2017_64
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||
arch: x86
|
||||
platform: Win32
|
||||
qtbin: msvc2017
|
||||
|
||||
before_build:
|
||||
- git submodule update --init
|
||||
- cd C:\
|
||||
- mkdir %arch%
|
||||
- cd %arch%
|
||||
- cmake -G "Visual Studio 15 2017" -A %platform% -DQt5_DIR="C:\Qt\5.13.2\%qtbin%\lib\cmake\Qt5" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -DVERSION="" ../Textractor
|
||||
|
||||
build:
|
||||
project: C:\%arch%\Textractor.sln
|
||||
parallel: false
|
||||
verbosity: normal
|
||||
|
||||
after_build:
|
||||
- 7z a Textractor-Alpha%APPVEYOR_BUILD_NUMBER%-%arch%.zip C:\Textractor\builds\RelWithDebInfo_%arch%\*
|
||||
- appveyor PushArtifact Textractor-Alpha%APPVEYOR_BUILD_NUMBER%-%arch%.zip
|
||||
|
||||
notifications:
|
||||
- provider: Email
|
||||
to:
|
||||
- akashmozumdar@gmail.com
|
||||
on_build_success: false
|
||||
on_build_failure: true
|
||||
on_build_status_changed: true
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,2 +1,8 @@
|
||||
gui/command.cpp
|
||||
.vs/
|
||||
/Textractor*
|
||||
/build/
|
||||
/builds/
|
||||
/out/
|
||||
*.vs/
|
||||
*.vscode/
|
||||
*.user
|
||||
*.aps
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "texthook/minhook"]
|
||||
path = texthook/minhook
|
||||
url = https://github.com/TsudaKageyu/minhook.git
|
137
CMakeLists.txt
137
CMakeLists.txt
@ -1,117 +1,50 @@
|
||||
cmake_minimum_required(VERSION 2.8)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(CMAKE_CONFIGURATION_TYPES Debug Release)
|
||||
project(Textractor)
|
||||
|
||||
project(ITHVNR)
|
||||
if (NOT MSVC)
|
||||
message(FATAL_ERROR "Textractor can only be built with Visual Studio")
|
||||
endif()
|
||||
|
||||
set(WDK_HOME "C:\\WinDDK\\7600.16385.1" CACHE FILEPATH "Windows Driver Kit path")
|
||||
set(CMAKE_INSTALL_PREFIX "" CACHE FILEPATH "installation path")
|
||||
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/Debug")
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/Release")
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/Debug")
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/Release")
|
||||
|
||||
set(CMAKE_WARN_ON_ABSOLUTE_INSTALL_DESTINATION ON)
|
||||
|
||||
|
||||
set(CPACK_GENERATOR "ZIP")
|
||||
set(CPACK_PACKAGE_VERSION_MAJOR 3)
|
||||
set(CPACK_PACKAGE_VERSION_MINOR 5640)
|
||||
set(CPACK_PACKAGE_VERSION_PATCH 1)
|
||||
set(CPACK_SOURCE_GENERATOR "ZIP")
|
||||
set(CPACK_SOURCE_IGNORE_FILES "/CVS/;/\\\\.svn/;/\\\\.bzr/;/\\\\.hg/;/\\\\.git/;\\\\.swp$;\\\\.#;/#" ".*\\\\.user$" "\\\\.gitignore$" "\\\\.gitmodules$" "\\\\.git$")
|
||||
include(CPack)
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
|
||||
|
||||
add_compile_options(
|
||||
#/Zc:auto # config.pri
|
||||
/wd4819 # config.pri
|
||||
/std:c++17
|
||||
/MP
|
||||
/GS-
|
||||
# $<$<CONFIG:Release>:/MT>
|
||||
# $<$<CONFIG:Debug>:/MTd>
|
||||
)
|
||||
|
||||
add_definitions(
|
||||
/D_SECURE_SCL=0 # config.pri
|
||||
/D_SCL_SECURE_NO_WARNINGS # config.pri
|
||||
/D_CRT_SECURE_NO_WARNINGS # config.pri
|
||||
/wd4018 # signed/unsigned mismatch
|
||||
/DVERSION="${VERSION}"
|
||||
/DUNICODE # config.pri
|
||||
/D_UNICODE
|
||||
/D_CRT_NON_CONFORMING_SWPRINTFS # common.pri
|
||||
/DITH_HAS_CRT
|
||||
)
|
||||
|
||||
include_directories(
|
||||
.
|
||||
vnr
|
||||
vnr/texthook
|
||||
${CMAKE_BINARY_DIR}/gui
|
||||
)
|
||||
if(${CMAKE_SIZEOF_VOID_P} EQUAL 8)
|
||||
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)
|
||||
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}>)
|
||||
|
||||
set(resource_src
|
||||
gui/ITHVNR.rc
|
||||
gui/icon1.ico
|
||||
)
|
||||
include_directories(include)
|
||||
add_library(pch text.cpp)
|
||||
target_precompile_headers(pch PUBLIC include/common.h)
|
||||
|
||||
set(ithvnr_src
|
||||
gui/command.cpp
|
||||
gui/CustomFilter.cpp
|
||||
gui/CustomFilter.h
|
||||
gui/ITH.h
|
||||
gui/language.cpp
|
||||
gui/language.h
|
||||
gui/main.cpp
|
||||
gui/ProcessWindow.cpp
|
||||
gui/ProcessWindow.h
|
||||
gui/ProfileManager.cpp
|
||||
gui/ProfileManager.h
|
||||
gui/resource.h
|
||||
gui/utility.cpp
|
||||
gui/utility.h
|
||||
${CMAKE_BINARY_DIR}/gui/version.h
|
||||
gui/version.h.in
|
||||
gui/window.cpp
|
||||
gui/window.h
|
||||
gui/TextBuffer.cpp
|
||||
gui/TextBuffer.h
|
||||
${resource_src}
|
||||
)
|
||||
file(GLOB ASSETS assets/*)
|
||||
file(COPY ${ASSETS} DESTINATION ${CMAKE_FINAL_OUTPUT_DIRECTORY})
|
||||
|
||||
source_group("Resource Files" FILES ${resource_src})
|
||||
include(QtUtils)
|
||||
msvc_registry_search()
|
||||
find_qt5(Core Widgets WinExtras WebSockets)
|
||||
|
||||
add_executable(${PROJECT_NAME} ${ithvnr_src})
|
||||
add_library(text text.cpp)
|
||||
target_compile_definitions(text PRIVATE ${TEXT_LANGUAGE})
|
||||
link_libraries(text)
|
||||
|
||||
add_subdirectory(vnr)
|
||||
# add_subdirectory(profile)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
LINK_FLAGS "/SUBSYSTEM:WINDOWS /MANIFESTDEPENDENCY:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""
|
||||
)
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
PSAPI_VERSION=1
|
||||
DEFAULT_MM
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
profile
|
||||
vnrhost
|
||||
ithsys
|
||||
${WDK_HOME}/lib/wxp/i386/ntdll.lib
|
||||
comctl32.lib
|
||||
psapi.lib
|
||||
)
|
||||
|
||||
target_compile_options(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
/EHsc
|
||||
)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME}
|
||||
DESTINATION .
|
||||
CONFIGURATIONS Release
|
||||
)
|
||||
|
||||
configure_file(gui/version.h.in gui/version.h)
|
||||
add_subdirectory(host)
|
||||
add_subdirectory(texthook)
|
||||
add_subdirectory(GUI)
|
||||
add_subdirectory(extensions)
|
||||
add_subdirectory(test)
|
||||
|
32
CMakeSettings.json
Normal file
32
CMakeSettings.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"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" ]
|
||||
}
|
||||
]
|
||||
}
|
20
GUI/CMakeLists.txt
Normal file
20
GUI/CMakeLists.txt
Normal file
@ -0,0 +1,20 @@
|
||||
add_executable(Textractor WIN32
|
||||
main.cpp
|
||||
exception.cpp
|
||||
mainwindow.cpp
|
||||
extenwindow.cpp
|
||||
attachprocessdialog.cpp
|
||||
Textractor.rc
|
||||
Textractor.ico
|
||||
)
|
||||
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 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()
|
BIN
GUI/Textractor.ico
Normal file
BIN
GUI/Textractor.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
1
GUI/Textractor.rc
Normal file
1
GUI/Textractor.rc
Normal file
@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON DISCARDABLE "Textractor.ico"
|
39
GUI/attachprocessdialog.cpp
Normal file
39
GUI/attachprocessdialog.cpp
Normal file
@ -0,0 +1,39 @@
|
||||
#include "attachprocessdialog.h"
|
||||
#include <QtWinExtras/QtWin>
|
||||
|
||||
extern const char* SELECT_PROCESS;
|
||||
extern const char* ATTACH_INFO;
|
||||
|
||||
AttachProcessDialog::AttachProcessDialog(QWidget* parent, std::vector<std::pair<QString, HICON>> processIcons) :
|
||||
QDialog(parent, Qt::WindowCloseButtonHint),
|
||||
model(this)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
setWindowTitle(SELECT_PROCESS);
|
||||
ui.label->setText(ATTACH_INFO);
|
||||
ui.processList->setModel(&model);
|
||||
|
||||
QPixmap transparent(100, 100);
|
||||
transparent.fill(QColor::fromRgba(0));
|
||||
for (const auto& [process, icon] : processIcons)
|
||||
{
|
||||
auto item = new QStandardItem(icon ? QIcon(QtWin::fromHICON(icon)) : transparent, process);
|
||||
item->setEditable(false);
|
||||
model.appendRow(item);
|
||||
}
|
||||
|
||||
connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(ui.processList, &QListView::clicked, [this](QModelIndex index) { ui.processEdit->setText(model.item(index.row())->text()); });
|
||||
connect(ui.processList, &QListView::doubleClicked, this, &QDialog::accept);
|
||||
connect(ui.processEdit, &QLineEdit::textEdited, [this](QString process)
|
||||
{
|
||||
for (int i = 0; i < model.rowCount(); ++i) ui.processList->setRowHidden(i, !model.item(i)->text().contains(process, Qt::CaseInsensitive));
|
||||
});
|
||||
connect(ui.processEdit, &QLineEdit::returnPressed, this, &QDialog::accept);
|
||||
}
|
||||
|
||||
QString AttachProcessDialog::SelectedProcess()
|
||||
{
|
||||
return ui.processEdit->text();
|
||||
}
|
17
GUI/attachprocessdialog.h
Normal file
17
GUI/attachprocessdialog.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "qtcommon.h"
|
||||
#include "ui_attachprocessdialog.h"
|
||||
#include <QStandardItemModel>
|
||||
|
||||
class AttachProcessDialog : public QDialog
|
||||
{
|
||||
public:
|
||||
explicit AttachProcessDialog(QWidget* parent, std::vector<std::pair<QString, HICON>> processIcons);
|
||||
QString SelectedProcess();
|
||||
|
||||
private:
|
||||
Ui::AttachProcessDialog ui;
|
||||
QStandardItemModel model;
|
||||
QString selectedProcess;
|
||||
};
|
37
GUI/attachprocessdialog.ui
Normal file
37
GUI/attachprocessdialog.ui
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AttachProcessDialog</class>
|
||||
<widget class="QWidget" name="AttachProcessDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::WindowModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="processEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListView" name="processList"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
59
GUI/exception.cpp
Normal file
59
GUI/exception.cpp
Normal file
@ -0,0 +1,59 @@
|
||||
#include "module.h"
|
||||
#include <sstream>
|
||||
|
||||
namespace
|
||||
{
|
||||
char* GetCppExceptionInfo(EXCEPTION_POINTERS* exception)
|
||||
{
|
||||
// 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"; }
|
||||
}
|
||||
|
||||
const char* GetCppExceptionMessage(EXCEPTION_POINTERS* exception)
|
||||
{
|
||||
__try { return ((std::exception*)exception->ExceptionRecord->ExceptionInformation[1])->what(); }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) { return "Could not find"; }
|
||||
}
|
||||
|
||||
thread_local std::wstring lastError = L"Unknown error";
|
||||
|
||||
__declspec(noreturn) void Terminate()
|
||||
{
|
||||
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)
|
||||
{
|
||||
thread_local static auto _ = set_terminate(Terminate);
|
||||
|
||||
MEMORY_BASIC_INFORMATION info = {};
|
||||
VirtualQuery(exception->ExceptionRecord->ExceptionAddress, &info, sizeof(info));
|
||||
|
||||
std::wstringstream errorMsg;
|
||||
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: " << GetModuleFilename((HMODULE)info.AllocationBase).value_or(L"Could not find") << std::endl <<
|
||||
L"Additional info: " << info.AllocationBase << std::endl;
|
||||
|
||||
if (exception->ExceptionRecord->ExceptionCode == 0xE06D7363)
|
||||
{
|
||||
if (char* info = GetCppExceptionInfo(exception)) errorMsg << L"Additional info: " << info << std::endl;
|
||||
if (const char* info = GetCppExceptionMessage(exception)) errorMsg << L"Additional info: " << info << std::endl;
|
||||
}
|
||||
|
||||
for (int i = 0; i < exception->ExceptionRecord->NumberParameters; ++i)
|
||||
errorMsg << L"Additional info: " << exception->ExceptionRecord->ExceptionInformation[i] << std::endl;
|
||||
|
||||
lastError = errorMsg.str();
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
auto _ = (AddVectoredExceptionHandler(FALSE, ExceptionLogger), SetUnhandledExceptionFilter([](auto) -> LONG { Terminate(); }));
|
||||
}
|
176
GUI/extenwindow.cpp
Normal file
176
GUI/extenwindow.cpp
Normal file
@ -0,0 +1,176 @@
|
||||
#include "extenwindow.h"
|
||||
#include "ui_extenwindow.h"
|
||||
#include <QMenu>
|
||||
#include <QFileDialog>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDropEvent>
|
||||
#include <QMimeData>
|
||||
#include <QUrl>
|
||||
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
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 + ".xdll").c_str()))
|
||||
{
|
||||
if (auto callback = (decltype(Extension::callback))GetProcAddress(module, "OnNewSentence"))
|
||||
{
|
||||
std::scoped_lock lock(extenMutex);
|
||||
extensions.push_back({ S(extenName), callback });
|
||||
return true;
|
||||
}
|
||||
FreeLibrary(module);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Unload(int index)
|
||||
{
|
||||
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 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)
|
||||
{
|
||||
wchar_t* sentenceBuffer = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, (sentence.size() + 1) * sizeof(wchar_t));
|
||||
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))) break;
|
||||
sentence = sentenceBuffer;
|
||||
HeapFree(GetProcessHeap(), 0, sentenceBuffer);
|
||||
return !sentence.empty();
|
||||
}
|
||||
|
||||
void CleanupExtensions()
|
||||
{
|
||||
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)
|
||||
{
|
||||
This = this;
|
||||
ui.setupUi(this);
|
||||
ui.vboxLayout->addWidget(new QLabel(EXTEN_WINDOW_INSTRUCTIONS, this));
|
||||
setWindowTitle(EXTENSIONS);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
bool ExtenWindow::eventFilter(QObject* target, QEvent* event)
|
||||
{
|
||||
// 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());
|
||||
Reorder(extenNames);
|
||||
Sync();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ExtenWindow::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Delete) Delete();
|
||||
}
|
||||
|
||||
void ExtenWindow::dragEnterEvent(QDragEnterEvent* event)
|
||||
{
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
void ExtenWindow::dropEvent(QDropEvent* event)
|
||||
{
|
||||
for (auto file : event->mimeData()->urls()) Add(file.toLocalFile());
|
||||
}
|
24
GUI/extenwindow.h
Normal file
24
GUI/extenwindow.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "qtcommon.h"
|
||||
|
||||
struct InfoForExtension
|
||||
{
|
||||
const char* name;
|
||||
int64_t value;
|
||||
};
|
||||
|
||||
bool DispatchSentenceToExtensions(std::wstring& sentence, const InfoForExtension* sentenceInfo);
|
||||
void CleanupExtensions(); // must call this before exiting the program, only way to uphold guarantee that DllMain and OnNewSentence won't be called concurrently
|
||||
|
||||
class ExtenWindow : public QMainWindow
|
||||
{
|
||||
public:
|
||||
explicit ExtenWindow(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject* target, QEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||
void dropEvent(QDropEvent* event) override;
|
||||
};
|
36
GUI/extenwindow.ui
Normal file
36
GUI/extenwindow.ui
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ExtenWindow</class>
|
||||
<widget class="QMainWindow" name="ExtenWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="extenList">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::InternalMove</enum>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
31
GUI/main.cpp
Normal file
31
GUI/main.cpp
Normal file
@ -0,0 +1,31 @@
|
||||
#include "mainwindow.h"
|
||||
#include "module.h"
|
||||
#include <winhttp.h>
|
||||
|
||||
extern const wchar_t* UPDATE_AVAILABLE;
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
std::thread([]
|
||||
{
|
||||
if (!*VERSION) return;
|
||||
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>;
|
||||
// Queries GitHub releases API https://developer.github.com/v3/repos/releases/ and checks the last release tag to check if it's the same
|
||||
if (InternetHandle internet = WinHttpOpen(L"Mozilla/5.0 Textractor", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0))
|
||||
if (InternetHandle connection = WinHttpConnect(internet, L"api.github.com", INTERNET_DEFAULT_HTTPS_PORT, 0))
|
||||
if (InternetHandle request = WinHttpOpenRequest(connection, L"GET", L"/repos/Artikash/Textractor/releases", NULL, NULL, NULL, WINHTTP_FLAG_SECURE))
|
||||
if (WinHttpSendRequest(request, NULL, 0, NULL, 0, 0, NULL))
|
||||
{
|
||||
char buffer[1000] = {};
|
||||
WinHttpReceiveResponse(request, NULL);
|
||||
WinHttpReadData(request, buffer, 1000, DUMMY);
|
||||
if (abs(strstr(buffer, "/tag/") - strstr(buffer, VERSION)) > 10) TEXTRACTOR_MESSAGE(UPDATE_AVAILABLE);
|
||||
}
|
||||
}).detach();
|
||||
|
||||
QDir::setCurrent(QFileInfo(S(GetModuleFilename().value())).absolutePath());
|
||||
|
||||
QApplication app(argc, argv);
|
||||
app.setFont(QFont("MS Shell Dlg 2", 10));
|
||||
return MainWindow().show(), app.exec();
|
||||
}
|
704
GUI/mainwindow.cpp
Normal file
704
GUI/mainwindow.cpp
Normal file
@ -0,0 +1,704 @@
|
||||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include "defs.h"
|
||||
#include "module.h"
|
||||
#include "extenwindow.h"
|
||||
#include "../host/host.h"
|
||||
#include "../host/hookcode.h"
|
||||
#include "attachprocessdialog.h"
|
||||
#include <shellapi.h>
|
||||
#include <process.h>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringListModel>
|
||||
#include <QScrollBar>
|
||||
#include <QMenu>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileDialog>
|
||||
#include <QFontDialog>
|
||||
#include <QHash>
|
||||
|
||||
extern const char* ATTACH;
|
||||
extern const char* LAUNCH;
|
||||
extern const char* CONFIG;
|
||||
extern const char* DETACH;
|
||||
extern const char* FORGET;
|
||||
extern const char* ADD_HOOK;
|
||||
extern const char* REMOVE_HOOKS;
|
||||
extern const char* SAVE_HOOKS;
|
||||
extern const char* SEARCH_FOR_HOOKS;
|
||||
extern const char* SETTINGS;
|
||||
extern const char* EXTENSIONS;
|
||||
extern const char* FONT;
|
||||
extern const char* SELECT_PROCESS;
|
||||
extern const char* SELECT_PROCESS_INFO;
|
||||
extern const char* FROM_COMPUTER;
|
||||
extern const char* PROCESSES;
|
||||
extern const char* CODE_INFODUMP;
|
||||
extern const char* FAILED_TO_CREATE_CONFIG_FILE;
|
||||
extern const char* HOOK_SEARCH_UNSTABLE_WARNING;
|
||||
extern const char* HOOK_SEARCH_STARTING_VIEW_CONSOLE;
|
||||
extern const char* SEARCH_CJK;
|
||||
extern const char* SEARCH_PATTERN;
|
||||
extern const char* SEARCH_DURATION;
|
||||
extern const char* SEARCH_MODULE;
|
||||
extern const char* PATTERN_OFFSET;
|
||||
extern const char* MIN_ADDRESS;
|
||||
extern const char* MAX_ADDRESS;
|
||||
extern const char* STRING_OFFSET;
|
||||
extern const char* MAX_HOOK_SEARCH_RECORDS;
|
||||
extern const char* HOOK_SEARCH_FILTER;
|
||||
extern const char* SEARCH_FOR_TEXT;
|
||||
extern const char* TEXT;
|
||||
extern const char* CODEPAGE;
|
||||
extern const char* START_HOOK_SEARCH;
|
||||
extern const char* SAVE_SEARCH_RESULTS;
|
||||
extern const char* TEXT_FILES;
|
||||
extern const char* DOUBLE_CLICK_TO_REMOVE_HOOK;
|
||||
extern const char* SAVE_SETTINGS;
|
||||
extern const char* USE_JP_LOCALE;
|
||||
extern const char* FILTER_REPETITION;
|
||||
extern const char* AUTO_ATTACH;
|
||||
extern const char* ATTACH_SAVED_ONLY;
|
||||
extern const char* SHOW_SYSTEM_PROCESSES;
|
||||
extern const char* DEFAULT_CODEPAGE;
|
||||
extern const char* FLUSH_DELAY;
|
||||
extern const char* MAX_BUFFER_SIZE;
|
||||
extern const char* MAX_HISTORY_SIZE;
|
||||
extern const char* CONFIG_JP_LOCALE;
|
||||
extern const wchar_t* ABOUT;
|
||||
extern const wchar_t* CL_OPTIONS;
|
||||
extern const wchar_t* LAUNCH_FAILED;
|
||||
extern const wchar_t* INVALID_CODE;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto HOOK_SAVE_FILE = u8"SavedHooks.txt";
|
||||
constexpr auto GAME_SAVE_FILE = u8"SavedGames.txt";
|
||||
|
||||
enum LaunchWithJapaneseLocale { PROMPT, ALWAYS, NEVER };
|
||||
|
||||
Ui::MainWindow ui;
|
||||
std::atomic<DWORD> selectedProcessId = 0;
|
||||
ExtenWindow* extenWindow = nullptr;
|
||||
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;
|
||||
MainWindow* This = nullptr;
|
||||
|
||||
void FindHooks();
|
||||
|
||||
QString TextThreadString(TextThread& thread)
|
||||
{
|
||||
return QString("%1:%2:%3:%4:%5: %6").arg(
|
||||
QString::number(thread.handle, 16),
|
||||
QString::number(thread.tp.processId, 16),
|
||||
QString::number(thread.tp.addr, 16),
|
||||
QString::number(thread.tp.ctx, 16),
|
||||
QString::number(thread.tp.ctx2, 16)
|
||||
).toUpper().arg(S(thread.name));
|
||||
}
|
||||
|
||||
ThreadParam ParseTextThreadString(QString ttString)
|
||||
{
|
||||
auto threadParam = ttString.splitRef(":");
|
||||
return { threadParam[1].toUInt(nullptr, 16), threadParam[2].toULongLong(nullptr, 16), threadParam[3].toULongLong(nullptr, 16), threadParam[4].toULongLong(nullptr, 16) };
|
||||
}
|
||||
|
||||
std::array<InfoForExtension, 20> GetSentenceInfo(TextThread& thread)
|
||||
{
|
||||
void (*AddText)(int64_t, const wchar_t*) = [](int64_t number, const wchar_t* text)
|
||||
{
|
||||
QMetaObject::invokeMethod(This, [number, text = std::wstring(text)] { if (TextThread* thread = Host::GetThread(number)) thread->Push(text.c_str()); });
|
||||
};
|
||||
void (*AddSentence)(int64_t, const wchar_t*) = [](int64_t number, const wchar_t* sentence)
|
||||
{
|
||||
// pointer from Host::GetThread may not stay valid unless on main thread
|
||||
QMetaObject::invokeMethod(This, [number, sentence = std::wstring(sentence)] { if (TextThread* thread = Host::GetThread(number)) thread->AddSentence(sentence); });
|
||||
};
|
||||
DWORD (*GetSelectedProcessId)() = [] { return selectedProcessId.load(); };
|
||||
|
||||
return
|
||||
{ {
|
||||
{ "current select", &thread == current },
|
||||
{ "text number", thread.handle },
|
||||
{ "process id", thread.tp.processId },
|
||||
{ "hook address", (int64_t)thread.tp.addr },
|
||||
{ "text handle", thread.handle },
|
||||
{ "text name", (int64_t)thread.name.c_str() },
|
||||
{ "add sentence", (int64_t)AddSentence },
|
||||
{ "add text", (int64_t)AddText },
|
||||
{ "get selected process id", (int64_t)GetSelectedProcessId },
|
||||
{ "void (*AddSentence)(int64_t number, const wchar_t* sentence)", (int64_t)AddSentence },
|
||||
{ "void (*AddText)(int64_t number, const wchar_t* text)", (int64_t)AddText },
|
||||
{ "DWORD (*GetSelectedProcessId)()", (int64_t)GetSelectedProcessId },
|
||||
{ nullptr, 0 } // nullptr marks end of info array
|
||||
} };
|
||||
}
|
||||
|
||||
void AttachSavedProcesses()
|
||||
{
|
||||
std::unordered_set<std::wstring> attachTargets;
|
||||
if (autoAttach)
|
||||
for (auto process : QString(QTextFile(GAME_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts))
|
||||
attachTargets.insert(S(process));
|
||||
if (autoAttachSavedOnly)
|
||||
for (auto process : QString(QTextFile(HOOK_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts))
|
||||
attachTargets.insert(S(process.split(" , ")[0]));
|
||||
|
||||
if (!attachTargets.empty())
|
||||
for (auto [processId, processName] : GetAllProcesses())
|
||||
if (processName && attachTargets.count(processName.value()) > 0 && alreadyAttached.count(processId) == 0) Host::InjectProcess(processId);
|
||||
}
|
||||
|
||||
std::optional<std::wstring> UserSelectedProcess()
|
||||
{
|
||||
QStringList savedProcesses = QString::fromUtf8(QTextFile(GAME_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts);
|
||||
std::reverse(savedProcesses.begin(), savedProcesses.end());
|
||||
savedProcesses.removeDuplicates();
|
||||
savedProcesses.insert(1, FROM_COMPUTER);
|
||||
QString process = QInputDialog::getItem(This, SELECT_PROCESS, SELECT_PROCESS_INFO, savedProcesses, 0, true, &ok, Qt::WindowCloseButtonHint);
|
||||
if (process == FROM_COMPUTER) process = QDir::toNativeSeparators(QFileDialog::getOpenFileName(This, SELECT_PROCESS, "/", PROCESSES));
|
||||
if (ok && process.contains('\\')) return S(process);
|
||||
return {};
|
||||
}
|
||||
|
||||
void ViewThread(int index)
|
||||
{
|
||||
ui.ttCombo->setCurrentIndex(index);
|
||||
ui.textOutput->setPlainText(sanitize(S((current = &Host::GetThread(ParseTextThreadString(ui.ttCombo->itemText(index))))->storage->c_str())));
|
||||
ui.textOutput->moveCursor(QTextCursor::End);
|
||||
}
|
||||
|
||||
void AttachProcess()
|
||||
{
|
||||
QMultiHash<QString, DWORD> processesMap;
|
||||
std::vector<std::pair<QString, HICON>> processIcons;
|
||||
for (auto [processId, processName] : GetAllProcesses())
|
||||
{
|
||||
if (processName && (showSystemProcesses || processName->find(L":\\Windows\\") == std::string::npos))
|
||||
{
|
||||
QString fileName = QFileInfo(S(processName.value())).fileName();
|
||||
if (!processesMap.contains(fileName))
|
||||
{
|
||||
HICON bigIcon, smallIcon;
|
||||
ExtractIconExW(processName->c_str(), 0, &bigIcon, &smallIcon, 1);
|
||||
processIcons.push_back({ fileName, bigIcon ? bigIcon : smallIcon });
|
||||
}
|
||||
processesMap.insert(fileName, processId);
|
||||
}
|
||||
}
|
||||
std::sort(processIcons.begin(), processIcons.end(), [](auto one, auto two) { return QString::compare(one.first, two.first, Qt::CaseInsensitive) < 0; });
|
||||
|
||||
AttachProcessDialog attachProcessDialog(This, processIcons);
|
||||
if (attachProcessDialog.exec())
|
||||
{
|
||||
QString process = attachProcessDialog.SelectedProcess();
|
||||
if (int processId = process.toInt(nullptr, 0)) Host::InjectProcess(processId);
|
||||
else for (int processId : processesMap.values(process)) Host::InjectProcess(processId);
|
||||
}
|
||||
}
|
||||
|
||||
void LaunchProcess()
|
||||
{
|
||||
std::wstring process;
|
||||
if (auto selected = UserSelectedProcess()) process = selected.value();
|
||||
else return;
|
||||
std::wstring path = std::wstring(process).erase(process.rfind(L'\\'));
|
||||
|
||||
PROCESS_INFORMATION info = {};
|
||||
auto useLocale = Settings().value(CONFIG_JP_LOCALE, PROMPT).toInt();
|
||||
if (!x64 && (useLocale == ALWAYS || (useLocale == PROMPT && QMessageBox::question(This, SELECT_PROCESS, USE_JP_LOCALE) == QMessageBox::Yes)))
|
||||
{
|
||||
if (HMODULE localeEmulator = LoadLibraryW(L"LoaderDll"))
|
||||
{
|
||||
// https://github.com/xupefei/Locale-Emulator/blob/aa99dec3b25708e676c90acf5fed9beaac319160/LEProc/LoaderWrapper.cs#L252
|
||||
struct
|
||||
{
|
||||
ULONG AnsiCodePage = SHIFT_JIS;
|
||||
ULONG OemCodePage = SHIFT_JIS;
|
||||
ULONG LocaleID = LANG_JAPANESE;
|
||||
ULONG DefaultCharset = SHIFTJIS_CHARSET;
|
||||
ULONG HookUiLanguageApi = FALSE;
|
||||
WCHAR DefaultFaceName[LF_FACESIZE] = {};
|
||||
TIME_ZONE_INFORMATION Timezone;
|
||||
ULONG64 Unused = 0;
|
||||
} LEB;
|
||||
GetTimeZoneInformation(&LEB.Timezone);
|
||||
((LONG(__stdcall*)(decltype(&LEB), LPCWSTR appName, LPWSTR commandLine, LPCWSTR currentDir, void*, void*, PROCESS_INFORMATION*, void*, void*, void*, void*))
|
||||
GetProcAddress(localeEmulator, "LeCreateProcess"))(&LEB, process.c_str(), NULL, path.c_str(), NULL, NULL, &info, NULL, NULL, NULL, NULL);
|
||||
}
|
||||
}
|
||||
if (info.hProcess == NULL)
|
||||
{
|
||||
STARTUPINFOW DUMMY = { sizeof(DUMMY) };
|
||||
CreateProcessW(process.c_str(), NULL, nullptr, nullptr, FALSE, 0, nullptr, path.c_str(), &DUMMY, &info);
|
||||
}
|
||||
if (info.hProcess == NULL) return Host::AddConsoleOutput(LAUNCH_FAILED);
|
||||
Host::InjectProcess(info.dwProcessId);
|
||||
CloseHandle(info.hProcess);
|
||||
CloseHandle(info.hThread);
|
||||
}
|
||||
|
||||
void ConfigureProcess()
|
||||
{
|
||||
if (auto processName = GetModuleFilename(selectedProcessId)) if (int last = processName->rfind(L'\\') + 1)
|
||||
{
|
||||
std::wstring configFile = std::wstring(processName.value()).replace(last, std::string::npos, GAME_CONFIG_FILE);
|
||||
if (!std::filesystem::exists(configFile)) QTextFile(S(configFile), QFile::WriteOnly).write("see https://github.com/Artikash/Textractor/wiki/Game-configuration-file");
|
||||
if (std::filesystem::exists(configFile)) _wspawnlp(_P_DETACH, L"notepad", L"notepad", configFile.c_str(), NULL);
|
||||
else QMessageBox::critical(This, CONFIG, QString(FAILED_TO_CREATE_CONFIG_FILE).arg(S(configFile)));
|
||||
}
|
||||
}
|
||||
|
||||
void DetachProcess()
|
||||
{
|
||||
try { Host::DetachProcess(selectedProcessId); }
|
||||
catch (std::out_of_range) {}
|
||||
}
|
||||
|
||||
void ForgetProcess()
|
||||
{
|
||||
auto processName = GetModuleFilename(selectedProcessId);
|
||||
if (!processName) processName = UserSelectedProcess();
|
||||
DetachProcess();
|
||||
if (!processName) return;
|
||||
for (auto file : { GAME_SAVE_FILE, HOOK_SAVE_FILE })
|
||||
{
|
||||
QStringList lines = QString::fromUtf8(QTextFile(file, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts);
|
||||
lines.erase(std::remove_if(lines.begin(), lines.end(), [&](const QString& line) { return line.contains(S(processName.value())); }), lines.end());
|
||||
QTextFile(file, QIODevice::WriteOnly | QIODevice::Truncate).write(lines.join("\n").append("\n").toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
void AddHook(QString hook)
|
||||
{
|
||||
if (QString hookCode = QInputDialog::getText(This, ADD_HOOK, CODE_INFODUMP, QLineEdit::Normal, hook, &ok, Qt::WindowCloseButtonHint); ok)
|
||||
if (hookCode.startsWith("S") || hookCode.startsWith("/S")) FindHooks(); // backwards compatibility for old hook search UX
|
||||
else if (auto hp = HookCode::Parse(S(hookCode))) try { Host::InsertHook(selectedProcessId, hp.value()); } catch (std::out_of_range) {}
|
||||
else Host::AddConsoleOutput(INVALID_CODE);
|
||||
}
|
||||
|
||||
void AddHook()
|
||||
{
|
||||
AddHook("");
|
||||
}
|
||||
|
||||
void RemoveHooks()
|
||||
{
|
||||
DWORD processId = selectedProcessId;
|
||||
std::unordered_map<uint64_t, HookParam> hooks;
|
||||
for (int i = 0; i < ui.ttCombo->count(); ++i)
|
||||
{
|
||||
ThreadParam tp = ParseTextThreadString(ui.ttCombo->itemText(i));
|
||||
if (tp.processId == selectedProcessId) hooks[tp.addr] = Host::GetThread(tp).hp;
|
||||
}
|
||||
auto hookList = new QListWidget(This);
|
||||
hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint);
|
||||
hookList->setAttribute(Qt::WA_DeleteOnClose);
|
||||
hookList->setMinimumSize({ 300, 50 });
|
||||
hookList->setWindowTitle(DOUBLE_CLICK_TO_REMOVE_HOOK);
|
||||
for (auto [address, hp] : hooks) new QListWidgetItem(QString(hp.name) + "@" + QString::number(address, 16), hookList);
|
||||
QObject::connect(hookList, &QListWidget::itemDoubleClicked, [processId, hookList](QListWidgetItem* item)
|
||||
{
|
||||
try
|
||||
{
|
||||
Host::RemoveHook(processId, item->text().split("@")[1].toULongLong(nullptr, 16));
|
||||
delete item;
|
||||
}
|
||||
catch (std::out_of_range) { hookList->close(); }
|
||||
});
|
||||
hookList->show();
|
||||
}
|
||||
|
||||
void SaveHooks()
|
||||
{
|
||||
auto processName = GetModuleFilename(selectedProcessId);
|
||||
if (!processName) return;
|
||||
QHash<uint64_t, QString> hookCodes;
|
||||
for (int i = 0; i < ui.ttCombo->count(); ++i)
|
||||
{
|
||||
ThreadParam tp = ParseTextThreadString(ui.ttCombo->itemText(i));
|
||||
if (tp.processId == selectedProcessId)
|
||||
{
|
||||
HookParam hp = Host::GetThread(tp).hp;
|
||||
if (!(hp.type & HOOK_ENGINE)) hookCodes[tp.addr] = S(HookCode::Generate(hp, tp.processId));
|
||||
}
|
||||
}
|
||||
auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values();
|
||||
ThreadParam tp = current->tp;
|
||||
if (tp.processId == selectedProcessId) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(HookCode::Generate(current->hp, tp.processId)));
|
||||
QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8());
|
||||
}
|
||||
|
||||
void FindHooks()
|
||||
{
|
||||
QMessageBox::information(This, SEARCH_FOR_HOOKS, HOOK_SEARCH_UNSTABLE_WARNING);
|
||||
|
||||
DWORD processId = selectedProcessId;
|
||||
SearchParam sp = {};
|
||||
sp.codepage = Host::defaultCodepage;
|
||||
bool searchForText = false, customSettings = false;
|
||||
QRegularExpression filter(".", QRegularExpression::UseUnicodePropertiesOption | QRegularExpression::DotMatchesEverythingOption);
|
||||
|
||||
QDialog dialog(This, Qt::WindowCloseButtonHint);
|
||||
QFormLayout layout(&dialog);
|
||||
QCheckBox asianCheck(&dialog);
|
||||
layout.addRow(SEARCH_CJK, &asianCheck);
|
||||
QDialogButtonBox confirm(QDialogButtonBox::Ok | QDialogButtonBox::Help | QDialogButtonBox::Retry, &dialog);
|
||||
layout.addRow(&confirm);
|
||||
confirm.button(QDialogButtonBox::Ok)->setText(START_HOOK_SEARCH);
|
||||
confirm.button(QDialogButtonBox::Retry)->setText(SEARCH_FOR_TEXT);
|
||||
confirm.button(QDialogButtonBox::Help)->setText(SETTINGS);
|
||||
QObject::connect(&confirm, &QDialogButtonBox::clicked, [&](QAbstractButton* button)
|
||||
{
|
||||
if (button == confirm.button(QDialogButtonBox::Retry)) searchForText = true;
|
||||
if (button == confirm.button(QDialogButtonBox::Help)) customSettings = true;
|
||||
dialog.accept();
|
||||
});
|
||||
dialog.setWindowTitle(SEARCH_FOR_HOOKS);
|
||||
if (!dialog.exec()) return;
|
||||
|
||||
if (searchForText)
|
||||
{
|
||||
QDialog dialog(This, Qt::WindowCloseButtonHint);
|
||||
QFormLayout layout(&dialog);
|
||||
QLineEdit textEdit(&dialog);
|
||||
layout.addRow(TEXT, &textEdit);
|
||||
QSpinBox codepageSpin(&dialog);
|
||||
codepageSpin.setMaximum(INT_MAX);
|
||||
codepageSpin.setValue(sp.codepage);
|
||||
layout.addRow(CODEPAGE, &codepageSpin);
|
||||
QDialogButtonBox confirm(QDialogButtonBox::Ok);
|
||||
QObject::connect(&confirm, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
layout.addRow(&confirm);
|
||||
if (!dialog.exec()) return;
|
||||
wcsncpy_s(sp.text, S(textEdit.text()).c_str(), PATTERN_SIZE - 1);
|
||||
try
|
||||
{
|
||||
Host::FindHooks(selectedProcessId, sp);
|
||||
ViewThread(0);
|
||||
} catch (std::out_of_range) {}
|
||||
return;
|
||||
}
|
||||
|
||||
filter.setPattern(asianCheck.isChecked() ? "[\\x{3000}-\\x{a000}]{4,}" : "[\\x{0020}-\\x{1000}]{4,}");
|
||||
if (customSettings)
|
||||
{
|
||||
QDialog dialog(This, Qt::WindowCloseButtonHint);
|
||||
QFormLayout layout(&dialog);
|
||||
QLineEdit patternEdit(x64 ? "CC CC 48 89" : "55 8B EC", &dialog);
|
||||
assert(QByteArray::fromHex(patternEdit.text().toUtf8()) == QByteArray((const char*)sp.pattern, sp.length));
|
||||
layout.addRow(SEARCH_PATTERN, &patternEdit);
|
||||
for (auto [value, label] : Array<int&, const char*>{
|
||||
{ sp.searchTime, SEARCH_DURATION },
|
||||
{ sp.offset, PATTERN_OFFSET },
|
||||
{ sp.maxRecords, MAX_HOOK_SEARCH_RECORDS },
|
||||
{ sp.codepage, CODEPAGE },
|
||||
})
|
||||
{
|
||||
auto spinBox = new QSpinBox(&dialog);
|
||||
spinBox->setMaximum(INT_MAX);
|
||||
spinBox->setValue(value);
|
||||
layout.addRow(label, spinBox);
|
||||
QObject::connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), [&value](int newValue) { value = newValue; });
|
||||
}
|
||||
QLineEdit boundEdit(QFileInfo(S(GetModuleFilename(selectedProcessId).value_or(L""))).fileName(), &dialog);
|
||||
layout.addRow(SEARCH_MODULE, &boundEdit);
|
||||
for (auto [value, label] : Array<uintptr_t&, const char*>{
|
||||
{ sp.minAddress, MIN_ADDRESS },
|
||||
{ sp.maxAddress, MAX_ADDRESS },
|
||||
{ sp.padding, STRING_OFFSET },
|
||||
})
|
||||
{
|
||||
auto edit = new QLineEdit(QString::number(value, 16), &dialog);
|
||||
layout.addRow(label, edit);
|
||||
QObject::connect(edit, &QLineEdit::textEdited, [&value](QString text) { if (uintptr_t newValue = text.toULongLong(&ok, 16); ok) value = newValue; });
|
||||
}
|
||||
QLineEdit filterEdit(filter.pattern(), &dialog);
|
||||
layout.addRow(HOOK_SEARCH_FILTER, &filterEdit);
|
||||
QPushButton startButton(START_HOOK_SEARCH, &dialog);
|
||||
layout.addWidget(&startButton);
|
||||
QObject::connect(&startButton, &QPushButton::clicked, &dialog, &QDialog::accept);
|
||||
if (!dialog.exec()) return;
|
||||
if (patternEdit.text().contains('.'))
|
||||
{
|
||||
wcsncpy_s(sp.exportModule, S(patternEdit.text()).c_str(), MAX_MODULE_SIZE - 1);
|
||||
sp.length = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
QByteArray pattern = QByteArray::fromHex(patternEdit.text().replace("??", QString::number(XX, 16)).toUtf8());
|
||||
memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), PATTERN_SIZE));
|
||||
}
|
||||
wcsncpy_s(sp.boundaryModule, S(boundEdit.text()).c_str(), MAX_MODULE_SIZE - 1);
|
||||
filter.setPattern(filterEdit.text());
|
||||
if (!filter.isValid()) filter.setPattern(".");
|
||||
}
|
||||
else
|
||||
{
|
||||
sp.length = 0; // use default
|
||||
}
|
||||
filter.optimize();
|
||||
|
||||
auto hooks = std::make_shared<QStringList>();
|
||||
try
|
||||
{
|
||||
Host::FindHooks(processId, sp,
|
||||
[hooks, filter](HookParam hp, std::wstring text) { if (filter.match(S(text)).hasMatch()) *hooks << sanitize(S(HookCode::Generate(hp) + L" => " + text)); });
|
||||
}
|
||||
catch (std::out_of_range) { return; }
|
||||
ViewThread(0);
|
||||
std::thread([hooks]
|
||||
{
|
||||
for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) lastSize = hooks->size();
|
||||
|
||||
QString saveFileName;
|
||||
QMetaObject::invokeMethod(This, [&]
|
||||
{
|
||||
auto hookList = new QListView(This);
|
||||
hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint);
|
||||
hookList->setAttribute(Qt::WA_DeleteOnClose);
|
||||
hookList->resize({ 750, 300 });
|
||||
hookList->setWindowTitle(SEARCH_FOR_HOOKS);
|
||||
if (hooks->size() > 5'000)
|
||||
{
|
||||
hookList->setUniformItemSizes(true); // they aren't actually uniform, but this improves performance
|
||||
hooks->push_back(QString(2000, '-')); // dumb hack: with uniform item sizes, the last item is assumed to be the largest
|
||||
}
|
||||
hookList->setModel(new QStringListModel(*hooks, hookList));
|
||||
QObject::connect(hookList, &QListView::clicked, [](QModelIndex i) { AddHook(i.data().toString().split(" => ")[0]); });
|
||||
hookList->show();
|
||||
|
||||
saveFileName = QFileDialog::getSaveFileName(This, SAVE_SEARCH_RESULTS, "./results.txt", TEXT_FILES);
|
||||
}, Qt::BlockingQueuedConnection);
|
||||
if (!saveFileName.isEmpty())
|
||||
{
|
||||
QTextFile saveFile(saveFileName, QIODevice::WriteOnly | QIODevice::Truncate);
|
||||
for (auto hook = hooks->cbegin(); hook != hooks->cend(); ++hook) saveFile.write(hook->toUtf8().append('\n')); // QStringList::begin() makes a copy
|
||||
}
|
||||
hooks->clear();
|
||||
}).detach();
|
||||
QMessageBox::information(This, SEARCH_FOR_HOOKS, HOOK_SEARCH_STARTING_VIEW_CONSOLE);
|
||||
}
|
||||
|
||||
void OpenSettings()
|
||||
{
|
||||
QDialog dialog(This, Qt::WindowCloseButtonHint);
|
||||
Settings settings(&dialog);
|
||||
QFormLayout layout(&dialog);
|
||||
QPushButton saveButton(SAVE_SETTINGS, &dialog);
|
||||
for (auto [value, label] : Array<bool&, const char*>{
|
||||
{ TextThread::filterRepetition, FILTER_REPETITION },
|
||||
{ autoAttach, AUTO_ATTACH },
|
||||
{ autoAttachSavedOnly, ATTACH_SAVED_ONLY },
|
||||
{ showSystemProcesses, SHOW_SYSTEM_PROCESSES },
|
||||
})
|
||||
{
|
||||
auto checkBox = new QCheckBox(&dialog);
|
||||
checkBox->setChecked(value);
|
||||
layout.addRow(label, checkBox);
|
||||
QObject::connect(&saveButton, &QPushButton::clicked, [checkBox, label, &settings, &value] { settings.setValue(label, value = checkBox->isChecked()); });
|
||||
}
|
||||
for (auto [value, label] : Array<int&, const char*>{
|
||||
{ TextThread::maxBufferSize, MAX_BUFFER_SIZE },
|
||||
{ TextThread::flushDelay, FLUSH_DELAY },
|
||||
{ TextThread::maxHistorySize, MAX_HISTORY_SIZE },
|
||||
{ Host::defaultCodepage, DEFAULT_CODEPAGE },
|
||||
})
|
||||
{
|
||||
auto spinBox = new QSpinBox(&dialog);
|
||||
spinBox->setMaximum(INT_MAX);
|
||||
spinBox->setValue(value);
|
||||
layout.addRow(label, spinBox);
|
||||
QObject::connect(&saveButton, &QPushButton::clicked, [spinBox, label, &settings, &value] { settings.setValue(label, value = spinBox->value()); });
|
||||
}
|
||||
QComboBox localeCombo(&dialog);
|
||||
assert(PROMPT == 0 && ALWAYS == 1 && NEVER == 2);
|
||||
localeCombo.addItems({ { "Prompt", "Always", "Never" } });
|
||||
localeCombo.setCurrentIndex(settings.value(CONFIG_JP_LOCALE, PROMPT).toInt());
|
||||
layout.addRow(CONFIG_JP_LOCALE, &localeCombo);
|
||||
QObject::connect(&localeCombo, qOverload<int>(&QComboBox::activated), [&settings](int i) { settings.setValue(CONFIG_JP_LOCALE, i); });
|
||||
layout.addWidget(&saveButton);
|
||||
QObject::connect(&saveButton, &QPushButton::clicked, &dialog, &QDialog::accept);
|
||||
dialog.setWindowTitle(SETTINGS);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void Extensions()
|
||||
{
|
||||
extenWindow->activateWindow();
|
||||
extenWindow->showNormal();
|
||||
}
|
||||
|
||||
void SetOutputFont(QString fontString)
|
||||
{
|
||||
QFont font = ui.textOutput->font();
|
||||
font.fromString(fontString);
|
||||
font.setStyleStrategy(QFont::NoFontMerging);
|
||||
ui.textOutput->setFont(font);
|
||||
Settings().setValue(FONT, font.toString());
|
||||
}
|
||||
|
||||
void ProcessConnected(DWORD processId)
|
||||
{
|
||||
alreadyAttached.insert(processId);
|
||||
|
||||
QString process = S(GetModuleFilename(processId).value_or(L"???"));
|
||||
QMetaObject::invokeMethod(This, [process, processId]
|
||||
{
|
||||
ui.processCombo->addItem(QString::number(processId, 16).toUpper() + ": " + QFileInfo(process).fileName());
|
||||
});
|
||||
if (process == "???") return;
|
||||
|
||||
// This does add (potentially tons of) duplicates to the file, but as long as I don't perform Ω(N^2) operations it shouldn't be an issue
|
||||
QTextFile(GAME_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((process + "\n").toUtf8());
|
||||
|
||||
QStringList allProcesses = QString(QTextFile(HOOK_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts);
|
||||
auto hookList = std::find_if(allProcesses.rbegin(), allProcesses.rend(), [&](QString hookList) { return hookList.contains(process); });
|
||||
if (hookList != allProcesses.rend())
|
||||
for (auto hookInfo : hookList->split(" , "))
|
||||
if (auto hp = HookCode::Parse(S(hookInfo))) Host::InsertHook(processId, hp.value());
|
||||
else swscanf_s(S(hookInfo).c_str(), L"|%I64d:%I64d:%[^\n]", &savedThreadCtx, &savedThreadCtx2, savedThreadCode, (unsigned)std::size(savedThreadCode));
|
||||
}
|
||||
|
||||
void ProcessDisconnected(DWORD processId)
|
||||
{
|
||||
QMetaObject::invokeMethod(This, [processId]
|
||||
{
|
||||
ui.processCombo->removeItem(ui.processCombo->findText(QString::number(processId, 16).toUpper() + ":", Qt::MatchStartsWith));
|
||||
}, Qt::BlockingQueuedConnection);
|
||||
}
|
||||
|
||||
void ThreadAdded(TextThread& thread)
|
||||
{
|
||||
std::wstring threadCode = HookCode::Generate(thread.hp, thread.tp.processId);
|
||||
bool savedMatch = (savedThreadCtx & 0xFFFF) == (thread.tp.ctx & 0xFFFF) && savedThreadCtx2 == thread.tp.ctx2 && savedThreadCode == threadCode;
|
||||
if (savedMatch)
|
||||
{
|
||||
savedThreadCtx = savedThreadCtx2 = savedThreadCode[0] = 0;
|
||||
current = &thread;
|
||||
}
|
||||
QMetaObject::invokeMethod(This, [savedMatch, ttString = TextThreadString(thread) + S(FormatString(L" (%s)", threadCode))]
|
||||
{
|
||||
ui.ttCombo->addItem(ttString);
|
||||
if (savedMatch) ViewThread(ui.ttCombo->count() - 1);
|
||||
});
|
||||
}
|
||||
|
||||
void ThreadRemoved(TextThread& thread)
|
||||
{
|
||||
QMetaObject::invokeMethod(This, [ttString = TextThreadString(thread)]
|
||||
{
|
||||
int threadIndex = ui.ttCombo->findText(ttString, Qt::MatchStartsWith);
|
||||
if (threadIndex == ui.ttCombo->currentIndex()) ViewThread(0);
|
||||
ui.ttCombo->removeItem(threadIndex);
|
||||
}, Qt::BlockingQueuedConnection);
|
||||
}
|
||||
|
||||
bool SentenceReceived(TextThread& thread, std::wstring& sentence)
|
||||
{
|
||||
for (int i = 0; i < sentence.size(); ++i) if (sentence[i] == '\r' && sentence[i + 1] == '\n') sentence[i] = 0x200b; // for some reason \r appears as newline - no need to double
|
||||
if (!DispatchSentenceToExtensions(sentence, GetSentenceInfo(thread).data())) return false;
|
||||
sentence += L'\n';
|
||||
if (&thread == current) QMetaObject::invokeMethod(This, [sentence = S(sentence)]() mutable
|
||||
{
|
||||
sanitize(sentence);
|
||||
auto scrollbar = ui.textOutput->verticalScrollBar();
|
||||
bool atBottom = scrollbar->value() + 3 > scrollbar->maximum() || (double)scrollbar->value() / scrollbar->maximum() > 0.975; // arbitrary
|
||||
QTextCursor cursor(ui.textOutput->document());
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
cursor.insertText(sentence);
|
||||
if (atBottom) scrollbar->setValue(scrollbar->maximum());
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void OutputContextMenu(QPoint point)
|
||||
{
|
||||
std::unique_ptr<QMenu> menu(ui.textOutput->createStandardContextMenu());
|
||||
menu->addAction(FONT, [] { if (QString font = QFontDialog::getFont(&ok, ui.textOutput->font(), This, FONT).toString(); ok) SetOutputFont(font); });
|
||||
menu->exec(ui.textOutput->mapToGlobal(point));
|
||||
}
|
||||
|
||||
void CopyUnlessMouseDown()
|
||||
{
|
||||
if (!(QApplication::mouseButtons() & Qt::LeftButton)) ui.textOutput->copy();
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
{
|
||||
This = this;
|
||||
ui.setupUi(this);
|
||||
extenWindow = new ExtenWindow(this);
|
||||
for (auto [text, slot] : Array<const char*, void(&)()>{
|
||||
{ ATTACH, AttachProcess },
|
||||
{ LAUNCH, LaunchProcess },
|
||||
{ CONFIG, ConfigureProcess },
|
||||
{ DETACH, DetachProcess },
|
||||
{ FORGET, ForgetProcess },
|
||||
{ ADD_HOOK, AddHook },
|
||||
{ REMOVE_HOOKS, RemoveHooks },
|
||||
{ SAVE_HOOKS, SaveHooks },
|
||||
{ SEARCH_FOR_HOOKS, FindHooks },
|
||||
{ SETTINGS, OpenSettings },
|
||||
{ EXTENSIONS, Extensions }
|
||||
})
|
||||
{
|
||||
auto button = new QPushButton(text, ui.processFrame);
|
||||
connect(button, &QPushButton::clicked, slot);
|
||||
ui.processLayout->addWidget(button);
|
||||
}
|
||||
ui.processLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding));
|
||||
|
||||
connect(ui.processCombo, qOverload<int>(&QComboBox::currentIndexChanged), [] { selectedProcessId = ui.processCombo->currentText().split(":")[0].toULong(nullptr, 16); });
|
||||
connect(ui.ttCombo, qOverload<int>(&QComboBox::activated), this, ViewThread);
|
||||
connect(ui.textOutput, &QPlainTextEdit::selectionChanged, this, CopyUnlessMouseDown);
|
||||
connect(ui.textOutput, &QPlainTextEdit::customContextMenuRequested, this, OutputContextMenu);
|
||||
|
||||
Settings settings;
|
||||
if (settings.contains(WINDOW) && QApplication::screenAt(settings.value(WINDOW).toRect().center())) setGeometry(settings.value(WINDOW).toRect());
|
||||
SetOutputFont(settings.value(FONT, ui.textOutput->font().toString()).toString());
|
||||
TextThread::filterRepetition = settings.value(FILTER_REPETITION, TextThread::filterRepetition).toBool();
|
||||
autoAttach = settings.value(AUTO_ATTACH, autoAttach).toBool();
|
||||
autoAttachSavedOnly = settings.value(ATTACH_SAVED_ONLY, autoAttachSavedOnly).toBool();
|
||||
showSystemProcesses = settings.value(SHOW_SYSTEM_PROCESSES, showSystemProcesses).toBool();
|
||||
TextThread::flushDelay = settings.value(FLUSH_DELAY, TextThread::flushDelay).toInt();
|
||||
TextThread::maxBufferSize = settings.value(MAX_BUFFER_SIZE, TextThread::maxBufferSize).toInt();
|
||||
TextThread::maxHistorySize = settings.value(MAX_HISTORY_SIZE, TextThread::maxHistorySize).toInt();
|
||||
Host::defaultCodepage = settings.value(DEFAULT_CODEPAGE, Host::defaultCodepage).toInt();
|
||||
|
||||
Host::Start(ProcessConnected, ProcessDisconnected, ThreadAdded, ThreadRemoved, SentenceReceived);
|
||||
current = &Host::GetThread(Host::console);
|
||||
Host::AddConsoleOutput(ABOUT);
|
||||
|
||||
AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), CL_OPTIONS, wcslen(CL_OPTIONS), DUMMY, NULL);
|
||||
auto processes = GetAllProcesses();
|
||||
int argc;
|
||||
std::unique_ptr<LPWSTR[], Functor<LocalFree>> argv(CommandLineToArgvW(GetCommandLineW(), &argc));
|
||||
for (int i = 0; i < argc; ++i)
|
||||
if (std::wstring arg = argv[i]; arg[0] == L'/' || arg[0] == L'-')
|
||||
if (arg[1] == L'p' || arg[1] == L'P')
|
||||
if (DWORD processId = wcstoul(arg.substr(2).c_str(), nullptr, 0)) Host::InjectProcess(processId);
|
||||
else for (auto [processId, processName] : processes)
|
||||
if (processName.value_or(L"").find(L"\\" + arg.substr(2)) != std::string::npos) Host::InjectProcess(processId);
|
||||
|
||||
std::thread([] { for (; ; Sleep(10000)) AttachSavedProcesses(); }).detach();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
Settings().setValue(WINDOW, geometry());
|
||||
CleanupExtensions();
|
||||
SetErrorMode(SEM_NOGPFAULTERRORBOX);
|
||||
ExitProcess(0);
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent*)
|
||||
{
|
||||
QApplication::quit(); // Need to do this to kill any windows that might've been made by extensions
|
||||
}
|
12
GUI/mainwindow.h
Normal file
12
GUI/mainwindow.h
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "qtcommon.h"
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
private:
|
||||
void closeEvent(QCloseEvent*);
|
||||
};
|
103
GUI/mainwindow.ui
Normal file
103
GUI/mainwindow.ui
Normal file
@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1000</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Textractor</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">
|
||||
QPushButton, QComboBox
|
||||
{
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralWidget">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QFrame" name="processFrame">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="processLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QComboBox" name="processCombo">
|
||||
<property name="editable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="insertPolicy">
|
||||
<enum>QComboBox::InsertAtBottom</enum>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToContents</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QComboBox" name="ttCombo">
|
||||
<property name="editable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="maxVisibleItems">
|
||||
<number>50</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="textOutput">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Arial Unicode MS</family>
|
||||
<pointsize>13</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="undoRedoEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="6"/>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
BIN
INSTALL_THIS_UNICODE_FONT.ttf
Normal file
BIN
INSTALL_THIS_UNICODE_FONT.ttf
Normal file
Binary file not shown.
67
README.md
67
README.md
@ -1,43 +1,52 @@
|
||||
# ITHVNR
|
||||
# Textractor
|
||||
|
||||
## Compiling
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
Before compiling *ITHVNR*, You should get CMake, [Windows Driver Kit 7.1](http://www.microsoft.com/en-us/download/details.aspx?id=11800), and Visual Studio.
|
||||
[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)
|
||||
|
||||
## Downloads
|
||||
**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.
|
||||
|
||||
Releases of *ITHVNR* can be found [here](https://github.com/mireado/ITHVNR/releases).
|
||||
## Download
|
||||
|
||||
~~mireado build server can be found [here](http://mireado.blog.me).~~
|
||||
|
||||
## Overview
|
||||
|
||||
*ITHVNR* is an open-source x32~~/x64~~ text hooker for Windows.
|
||||
|
||||
Basically, ITHVNR base on [Stomp](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine)'s version.
|
||||
|
||||
## Activity Graph
|
||||
|
||||
[![Throughput Graph](https://graphs.waffle.io/mireado/ITHVNR/throughput.svg)](https://waffle.io/mireado/ITHVNR/metrics/throughput)
|
||||
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>
|
||||
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
|
||||
|
||||
- Open-source
|
||||
- Hook text
|
||||
- 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)
|
||||
- Automatically search for possible hook codes
|
||||
|
||||
## License
|
||||
## Support
|
||||
|
||||
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/).
|
||||
|
||||
## Developers
|
||||
## Extensions
|
||||
|
||||
- Copyright (C) 2010-2012 [kaosu](qiupf2000@gmail.com)
|
||||
- Copyright (C) 2015 [zorkzero](zorkzero@hotmail.com)
|
||||
- VNR engine making by [jichi](http://sakuradite.com/topic)
|
||||
- ITH updating by [Andys](http://www.hongfire.com/forum/member/126633-andys)
|
||||
- ITHVNR new GUI & VNR engine migration by [Stomp](http://www.hongfire.com/forum/member/325894-stomp)
|
||||
See my [Example Extension project](https://github.com/Artikash/ExampleExtension) to see how to build an extension.<br>
|
||||
See the extensions folder for examples of what extensions can do.
|
||||
|
||||
## Special Thanks
|
||||
## Contributing
|
||||
|
||||
- Everybody adding issues!
|
||||
- [IJEMIN](https://github.com/IJEMIN)
|
||||
- [Eguni](https://github.com/Eguni)
|
||||
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: [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 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 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](docs/CREDITS.md)
|
||||
|
52
README_DE.md
Normal file
52
README_DE.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Textractor
|
||||
|
||||
![Wie es aussieht](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.b.a. NextHooker) ist ein open-source x86/x64 Video spiel Text hooker für Windows 7+ (und Wine) basierend auf [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Schau das [Tutorial Video](docs/TUTORIAL.md) (auf Englisch) an für einen schnellen Überblick wie du Textractor verwendest.
|
||||
## Download
|
||||
|
||||
Der offizielle Release ist [hier](https://github.com/Artikash/Textractor/releases) zu finden.<br>
|
||||
Der letzte Release von ITHVNR ist [hier](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).<br>
|
||||
Der experimentelle Release von Textractor (mit debug Informationen) ist [hier](https://ci.appveyor.com/project/Artikash/textractor/history) in der 'Artifacts'
|
||||
Kategorie des jeweiligen Jobs.
|
||||
|
||||
## Features
|
||||
|
||||
- Modular und Anpassbar
|
||||
- Automatischen 'hooken' von mehreren Engines (einige davon welche keine VNR Support haben!)
|
||||
- Text 'hooken' mithilfe von /H "hook" Codes (die meisten AGTH codes funktionieren)
|
||||
- Automatische suche nach funktionierenden Hook's
|
||||
|
||||
## Support
|
||||
|
||||
Wenn ihr irgendwelche Fehler, Spiele bei denen Textractor nicht funktioniert, oder Fragen/Anmerkungen habt lasst es mich bitte wissen.<br>
|
||||
Falls ihr Probleme mit einem Spiel habt, schickt mir einen kostenlosen download Link von dem Spiel oder schenkt es mir auf [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
## Erweiterungen
|
||||
|
||||
Siehe [Example Extension project](https://github.com/Artikash/ExampleExtension) für Anleitungen, wie man eine Erweiterung erstellt.<br>
|
||||
Im 'Extensions' Ordner sind Beispiele für Erweiterungen.
|
||||
|
||||
## Unterstützen
|
||||
|
||||
Ich bin dankbar für alle Unterstützungen! Schickt mir eine E-Mail an akashmozumdar@gmail.com falls ihr Fragen zur Codebasis habt.<br>
|
||||
Verwendet bitte als Standard eine pull Request (fork, branch, commit) zum Master Release.<br>
|
||||
Zu Übersetzungen beizutragen ist einfach: [text.cpp](text.cpp) enthält alle text Strings welche übersetzt werden sollen. Übersetzungen der README oder des Tutorial Video Transkripts sind ebenfalls willkommen.
|
||||
|
||||
## Compiling
|
||||
Zum Compilen braucht ihr Qt Version 5.13 und Visual Studio mit CMake Unterstützung.
|
||||
Erstellt einen Clone vom Quellcode und initialisiert die submodule mit 'git clone https://github.com/Artikash/Textractor.git' und 'git submodule update --init'.
|
||||
Ihr solltet danach in der Lage sein, den Quellordner in Visual Studio zu öffnen und anzufangen.
|
||||
|
||||
## Projekt Architektur
|
||||
|
||||
Der Host injiziert texthooks in den ziel Prozess und verbindet ihn mit 2 Pipe Dateien.
|
||||
Texthook wartet auf die Pipe und injiziert Instruktionen für den Text Output (z.b. TextOut, GetGlyphOutline) welche durch die Pipe gesendet werden.<br>
|
||||
Weitere Informationen werden durch geteilten Speicher ausgetauscht.<br>
|
||||
Der Text, welchen der Host durch die Pipe erhält, wird dann verarbeitet, bevor er wieder an die GUI gesendet wird.<br>
|
||||
Zu guter Letzt, sendet die GUI den Text an die Erweiterung, welche einen lesbaren Output anzeigt.
|
||||
|
||||
## [Entwickler](docs/CREDITS.md)
|
24
README_ES.md
Normal file
24
README_ES.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Textractor
|
||||
|
||||
[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](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](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)
|
||||
|
||||
## Descarga
|
||||
|
||||
Textractor se puede descargar [acá](https://github.com/Artikash/Textractor/releases), incluye versiones anteriores.
|
||||
|
||||
Las antiguas versiones de ITHVNR se pueden descargar [acá](https://github.com/mireado/ITHVNR/releases).
|
||||
|
||||
## Características
|
||||
|
||||
- Totalmente personalizable
|
||||
- Extrae y traduce el texto en muchos motores de juegos y novelas visuales (¡Incluye juegos que no funcionan en VNR/Visual Novel Reader!)
|
||||
- Extrae el texto usando /H "hook" codes o conocidos también como H-codes (los códigos AGTH también funcionan)
|
||||
- Extrae el texto directamente usando el /R "read" code
|
51
README_FR.md
Normal file
51
README_FR.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Textractor
|
||||
|
||||
![À quoi ça ressemble](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.k.a. NextHooker) est un traducteur de jeux-videos basé surtout sur du texte en open source x86/x64 pour Windows/Wine [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Regarde le [tutorial video](docs/TUTORIAL.md) pour un aperçu rapide de son utilisation.
|
||||
|
||||
## Téléchargement
|
||||
|
||||
Les versions de Textractor peuvent être trouvées[here](https://github.com/Artikash/Textractor/releases).<br>
|
||||
Le denière version de THVNR peut etre trouvé [here](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).
|
||||
|
||||
## Nouveautés
|
||||
- Hautement extensible et personnalisable
|
||||
- Accrochage automatique de nombreux moteurs de jeu (dont certains non pris en charge par VNR!)
|
||||
- Les textes de hook qui utilisent des /H "hook" codes (la plupart des codes AGTH pris en charge)
|
||||
- Extraire directement le texte à l'aide des codes /R "lire"
|
||||
|
||||
## Support
|
||||
|
||||
Veuillez me signaler les bugs, les jeux dont Textractor a du mal à accrocher, les demandes de fonctionnalités ou d'autres suggestions. <br>
|
||||
Si vous rencontrez des difficultés pour accrocher un jeu, veuillez m'envoyer un e-mail à un endroit où je peux le télécharger librement/gratuitement ou me le faire cadeau sur [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
## Extensions
|
||||
|
||||
Regarde mes [Example Extension project](https://github.com/Artikash/ExampleExtension) pour voir comment créer un extension.<br>
|
||||
Voir le dossier des extensions pour des exemples de ce que les extensions peuvent faire.
|
||||
|
||||
## Contribution
|
||||
|
||||
Toutes les contributions sont appréciées! Veuillez m'envoyer un e-mail À akashmozumdar@gmail.com si vous avez des questions sur la base de code. <br>
|
||||
Vous devez utiliser le processus standard de création d'une demande d'extraction (fork, branch, commit changes, make PR from your branch to my master). <br>
|
||||
Contribuer à une traduction est simple: il suffit de traduire les chaînes dans [text.cpp](text.cpp) ainsi que ce fichier README.
|
||||
|
||||
## Compilation
|
||||
|
||||
Avant de compiler *Textractor*, vous devriez obtenir Visual Studio avec prise en charge de CMake, ainsi que Qt version 5.13 <br>
|
||||
Vous devriez ensuite pouvoir simplement ouvrir le dossier dans Visual Studio et faire le build. Exécutez Textractor.exe.
|
||||
|
||||
## Architecture du projet
|
||||
|
||||
L'hôte (voir GUI / dossier hôte) injecte texthook.dll (créé à partir du dossier texthook) dans le processus cible et s'y connecte via 2 fichiers pipe. <br>
|
||||
L'hôte écrit dans hostPipe, texthook écrit dans hookPipe. <br>
|
||||
Texthook attend que le canal soit connecté, puis injecte quelques instructions dans toutes les fonctions de sortie de texte (par exemple TextOut, GetGlyphOutline) qui provoquent l'envoi de leur entrée via le canal. <br>
|
||||
Des informations supplémentaires sur les hooks sont échangées via la mémoire partagée. <br>
|
||||
Le texte que l'hôte reçoit via le canal est ensuite traité un peu avant d'être renvoyé à l'interface graphique. <br>
|
||||
Enfin, l'interface graphique envoie le texte aux extensions avant de l'afficher.
|
||||
|
||||
## [Développeurs](docs/CREDITS.md)
|
53
README_ID.md
Normal file
53
README_ID.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Textractor
|
||||
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [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 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).
|
||||
|
||||
## Fitur
|
||||
|
||||
- Sangat Ekstensibel
|
||||
- Tempel otomatis banyak engine game (termasuk beberapa yang tidak didukung oleh VNR)
|
||||
- Hook teks menggunakan "hook" /H (mendukung semua kode AGTH)
|
||||
- Mengutip teks secara langsung menggunakan kode /R "read"
|
||||
|
||||
## Dukungan
|
||||
|
||||
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 ekstensi folder untuk melihat sampel ekstensi.
|
||||
|
||||
## Kontribusi
|
||||
|
||||
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.
|
||||
|
||||
## Mengcompile
|
||||
|
||||
Sebelum melakukan proses compile *Textractor*, kamu harus memiliki Visual Studio dengan dukungan Cmake, juga dengan Qt version 5.13<br>
|
||||
Lalu kamu dapat membuka folder di Visual Studio, dan build. Jalankan Textractor.exe.
|
||||
|
||||
|
||||
## Arsitektur Project
|
||||
|
||||
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 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>
|
||||
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](docs/CREDITS.md)
|
52
README_IT.md
Normal file
52
README_IT.md
Normal 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)
|
17
README_JP.md
Normal file
17
README_JP.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Textractor
|
||||
|
||||
[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** はビジュアルノベル文字抽出プログラム。
|
||||
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
## ダウンロード
|
||||
|
||||
[こっち](https://github.com/Artikash/Textractor/releases)
|
||||
|
||||
## 投稿
|
||||
|
||||
日本語で不具合報告・機能リクエスト・プルリクエストも読むことが出来ますので、どうぞ日本語で投稿してください。私の方へメールして頂いても結構です。
|
||||
|
||||
ただしこちらからの返信に関しては、一言二言程度の短い文章しか書くことが出来ないと思われますので、その点はご了承下さい。
|
50
README_KR.md
Normal file
50
README_KR.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Textractor
|
||||
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [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](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)서 받을 수 있습니다.
|
||||
|
||||
## 특징
|
||||
|
||||
- 높은 확장성과 커스터마이즈
|
||||
- 많은 게임엔진의 자동 후킹이 가능 (몇몇의 VNR로 후킹 불가능한 경우도 포함)
|
||||
- /H "hook" 코드를 통한 후킹 (많은 AGTH 코드가 지원됨)
|
||||
- /R "read" 코드를 통해 직접적으로 텍스트 추출이 가능
|
||||
|
||||
## 지원
|
||||
|
||||
버그나, 후킹에 문제가 있는 게임이나, 기능요청 혹은 제안들을 알려주시기 바랍니다.<br>
|
||||
특정 게임의 문제해결을 위하여 해당게임을 무료로 받을 수 있는 링크나 [Steam](https://steamcommunity.com/profiles/76561198097566313/)을 통한 제공을 받고 있습니다.
|
||||
|
||||
## 확장기능
|
||||
|
||||
어떻게 확장기능을 만드는지 [Example Extension project](https://github.com/Artikash/ExampleExtension) 을 확인해 보시기 바랍니다.<br>
|
||||
확장기능 폴더를 확인해 확장기능들이 어떤 역할들을 하는지 알아보세요.
|
||||
|
||||
## 기여
|
||||
|
||||
모든 기여자들에게 감사하고 있습니다! 코드베이스에 궁금한 점이 있다면 akashmozumdar@gmail.com 에 이메일 해 주시기 바랍니다.
|
||||
|
||||
## 컴파일링
|
||||
|
||||
*Textractor*를 컴파일링 하기 전에, Qt version 5.13과 CMake를 포함한 Visual Studio가 있어야 합니다.<br>
|
||||
그 이후로는, 단순히 Visual Studio를 통해 폴더를 열고 빌드하는 것으로 실행이 가능합니다.
|
||||
|
||||
## 프로젝트 아키텍쳐
|
||||
|
||||
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.
|
||||
|
||||
## [개발자들](docs/CREDITS.md)
|
52
README_PT.md
Normal file
52
README_PT.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Textractor
|
||||
|
||||
![Como se Parece](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [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](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).
|
||||
|
||||
## Recursos e Funções
|
||||
|
||||
- Altamente extensível e personalizável.
|
||||
- Automaticamente extrai vários games engines (inclusive algumas não compatíveis com VNR!)
|
||||
- Extrai texto usando códigos "hook" /H (a maioria dos códigos utilizados pelo AGTH são compatíveis.)
|
||||
- Extrai texto diretamente utilizando códigos "read" /R
|
||||
|
||||
## Suporte Técnico
|
||||
|
||||
Por favor, deixe-me saber de quaisquer bugs, jogos que o Textractor tenha problema extraindo texto, pedido por recursos ou funções, ou quaisquer outras sugestões.<br>
|
||||
Se você tiver algum problema extraindo um jogo, favor me mandar um e-mail do lugar de onde eu possa livremente dar download do jogo, ou presenteie-me o jogo no [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
## Extensões
|
||||
|
||||
Veja o meu [Projeto de Extensão-Exemplo](https://github.com/Artikash/ExampleExtension) para como construir uma extensão.<br>
|
||||
Veja a pasta de extensões para mais exemplos do que as extensões são capazes de fazerem.
|
||||
|
||||
## Contribuindo
|
||||
|
||||
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](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.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 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](docs/CREDITS.md)
|
52
README_RU.md
Normal file
52
README_RU.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Textractor
|
||||
|
||||
![Как это выглядит](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) это проект 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).
|
||||
|
||||
## Возможности
|
||||
|
||||
- Высокая расширяемость
|
||||
- Автозахват текста из множества игровых движков (включая некоторые, неподдерживаемые в VNR!)
|
||||
- Захват текста с использованием /H "hook" кодов (поддерживается большинство AGTH кодов)
|
||||
- Прямое извлечение текста с использованием /R "read" кодов
|
||||
|
||||
## Поддержка
|
||||
|
||||
Сообщайте о любых ошибках, играх, с которыми у Textractor проблемы, запросах о новых функциях или другие предложения.<br>
|
||||
Если у вас возникают проблемы с захватом текста из какой либо игры, скиньте на электронную почту ссылку на её загрузку или киньте её подарком в [Steam](https://steamcommunity.com/profiles/76561198097566313/) , для тестирования.
|
||||
|
||||
## Расширения
|
||||
|
||||
Смотрите [Проект примера расширения](https://github.com/Artikash/ExampleExtension), чтобы узнать, как создать расширение.<br>
|
||||
Также для примера того, что делают расширения, смотрите папку extensions.
|
||||
|
||||
## Вклад
|
||||
|
||||
Любой вклад приветствуется! Пишите мне(автору) на akashmozumdar@gmail.com, если у вас есть любые вопросы о кодовой базе.<br>
|
||||
Используйте стандартные действия для создания pull request (fork, branch, commit changes, создайте PR из своей ветки branch на мой master).<br>
|
||||
Вклад в перевод совсем не сложен: просто переведите строки в [text.cpp](text.cpp), также, как и этот README.
|
||||
|
||||
## Компиляция
|
||||
|
||||
Перед компиляцией *Textractor*, установите Visual Studio с поддержкой CMake, а также Qt версии 5.13<br>
|
||||
Тогда вы сможете просто открыть и построить проект в Visual Studio. Запустите Textractor.exe.
|
||||
|
||||
## Архитектура проекта
|
||||
|
||||
Хост (смотрите папку host) внедряет texthook.dll (созданной из папки texthook) в целевой процесс и подключается к нему через два файла-канала (pipe).<br>
|
||||
Хост пишет в hostPipe, texthook пишет в hookPipe.<br>
|
||||
texthook ждет присоединения канала, тогда внедряет некоторые инструкции в любые выводящие текст функции (такие как TextOut, GetGlyphOutline), что вызывает пересылку поступающего в них текста через канал.<br>
|
||||
Дополнительная информация о хуках размещена через файл просмотра (a.k.a. section object), который сопоставлен с ссылкой на класс TextHook.<br>
|
||||
Текст, который хост получает через канал, затем немного обрабатывается перед отправкой обратно в графический интерфейс (GUI).<br>
|
||||
Наконец, GUI отправляет текст расширениям, перед его отображением.
|
||||
|
||||
## [Разработчики](docs/CREDITS.md)
|
49
README_SC.md
Normal file
49
README_SC.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Textractor
|
||||
|
||||
[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](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)
|
||||
|
||||
## 下载
|
||||
|
||||
Textractor 的发行版可以在[这里](https://github.com/Artikash/Textractor/releases)找到.
|
||||
|
||||
老版 ITHVNR 可以在[这里](https://github.com/mireado/ITHVNR/releases)找到.
|
||||
|
||||
## 特点
|
||||
|
||||
- 高度可扩展
|
||||
- 自动从很多游戏中提取 (包括一些没有被 VNR 支持的!)
|
||||
- 通过 /H "hook" 码提取文本 (支持大多数 AGTH 码)
|
||||
- 使用 /R "read" 码直接抽取文本
|
||||
|
||||
## 扩展
|
||||
|
||||
通过我的[扩展示例项目](https://github.com/Artikash/ExampleExtension)查看如何构建扩展.<br>
|
||||
通过 extensions 文件夹查看扩展能够做什么.
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎一切贡献!如有任何关于代码的疑问,请向 akashmozumdar@gmail.com 发邮件.<br>
|
||||
你应当使用创建 PR 的标准过程 (分岔 (fork), 分支 (branch), 提交变化, 创建从你的分支到我的 master 分支的 PR).<br>
|
||||
提供翻译贡献很简单: 只需翻译 [text.cpp](text.cpp) 中的字符串和这份 README 即可.
|
||||
|
||||
## 编译
|
||||
|
||||
编译 *Textractor* 前, 你应当获取支持 CMake 的 Visual Studio, 以及 Qt 5.13 版.<br>
|
||||
之后就可以使用 Visual Studio 打开文件夹, 然后构建. 运行 Textractor.exe.
|
||||
|
||||
## 项目架构
|
||||
|
||||
宿主 (位于 host 文件夹) 向目标进程注入 texthook.dll (由 texthook 文件夹创建) 并通过两个管道文件互联.<br>
|
||||
宿主向 hostPipe 写入, texthook 向 hookPipe 写入.<br>
|
||||
texthook 等待管道连接, 之后向一些文本输出函数 (如 TextOut, GetGlyphOutline) 注入一系列指令, 使得它们的输入被沿着管道发送.<br>
|
||||
其它关于钩子的信息通过一个被 TextHook 类保有引用的文件视图 (曾用名: 段对象) 共享.<br>
|
||||
之后, 宿主通过管道接收到的文本在传回 GUI 前被简单处理.<br>
|
||||
最后, GUI 在显示文本前将其分发给扩展.
|
||||
|
||||
## [开发者](docs/CREDITS.md)
|
48
README_TH.md
Normal file
48
README_TH.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Textractor
|
||||
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [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](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).
|
||||
|
||||
## คุณสมบัติ
|
||||
|
||||
- Highly extensible and customizable
|
||||
- ต่อยอดได้ไกล และ ปรับแต่งได้ง่าย
|
||||
- สามารถเชื่อม/ดึงคำแปลได้จากระบบเกมหลายเกม (รวมทั่งเกมที่ไม่ได้รองรับโดยโปรแกรม Visual Novel Reader)
|
||||
- สามารถเชื่อมตัวอักษรโดยการใช้ /H "hook" (รหัสเชื่อม) และยังรองรับการใช้รหัสของ AGTH
|
||||
- สามารถการดึงข้อมูลโดยใช้รหัส /R "read
|
||||
|
||||
## ความช่วยเหลือ
|
||||
|
||||
ในกรณีที่พบกับปัญหาระหว่างใช้งานโปรแกรม หรือ เกมที่ Textractor ไม่สามารถเชื่อมข้อมูล, หรือแนะนำต่างๆ สามารถแจ้งให้ทราบได้จากเว็บไซต์ที่ดาวน์โหลด Textractor.
|
||||
ถ้าหากมีเกมใดที่มีปัญหาการเชื่อมกรุณาส่งอีเมลสถานที่ที่สามารถดาวน์โหลดเกมได้ หรือ ส่งของขวัญเกมผ่านทาง [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
## ส่วนขยาย
|
||||
|
||||
กรุณาลองสำรวจ [ตัวอย่างของส่วนขยาย](https://github.com/Artikash/ExampleExtension) เพื่อที่จะเรียนรู้เกี่ยวกับการเขียนส่วนขยาย.<br>
|
||||
และลองดูโฟลเดอร์ extensions สำหรับตัวอย่างการทำงานของส่วนขยาย
|
||||
|
||||
## การสนับสนุน
|
||||
|
||||
การสนับสนุนใดๆนั่นยินดีเป็นอย่างยิ่ง! สามารถส่งอีเมลมาได้ตลอดเวลาที่ akashmozumdar@gmail.com ถ้าหากมีคำถามเกี่ยวกับโค้ด.<br>
|
||||
|
||||
## โครงสร้างโปรแกรม
|
||||
|
||||
ฐานของโปรแกรม (โฟลเดอร์ host) ส่งข้อมูลจาก texthook.dll (ที่ถูกสร้างจาก texthook โฟลเดอร์) ไปยังเกมเป้าหมาย และ เชื่อมทั่งสองอย่างเข้าด้วยกัน<br>
|
||||
ฐานโปรแกรมเขียนผ่านฝั่ง hostPipe(ท่อเชื่อมฝั่งฐานข้อมูล) ในขณะที่ตัวดึงตัวอักษรที่ทางฝั่ง hookPipe(ท่อเชื่อมฝั่งดึงข้อมูล).<br>
|
||||
ตัวดึงตัวอักษรรอการเชื่อมเข้ากับของทั่งสองท่อ หลังจากนั่นส่งคำสั่งไปยังข้อมูลนั่น (เช่น แสดงผลข้อมูล เป็นต้น) และทำให้ข้อมูลส่งผ่านต่อมาออกมาได้ถูกต้อง<br>
|
||||
ข้อมูลบางอย่างเกี่ยวกับการเชื่อมจะถูกแลกเปลี่ยนผ่านความทรงจำของระบบ (shared memory)
|
||||
<br>
|
||||
ตัวอักษรที่ฐานโปรแกรมรับผ่านท่อจะถูกแปลงเล็กน้อยก่อนที่จะแสดงผ่าน GUI <br>
|
||||
สุดท้ายแล้ว GUI จะส่งข้อมูลตัวอักษรไปยังส่วนขยายต่างๆก่อนที่จะแสดงให้เห็นในหน้าจอ
|
||||
|
||||
## [นักพัฒนา](docs/CREDITS.md)
|
3
README_TU.md
Normal file
3
README_TU.md
Normal file
@ -0,0 +1,3 @@
|
||||
# WIP
|
||||
|
||||
## README not translated to Turkish yet :(
|
BIN
assets/LoaderDll.dll
Normal file
BIN
assets/LoaderDll.dll
Normal file
Binary file not shown.
BIN
assets/LocaleEmulator.dll
Normal file
BIN
assets/LocaleEmulator.dll
Normal file
Binary file not shown.
BIN
assets/StressTest.txt
Normal file
BIN
assets/StressTest.txt
Normal file
Binary file not shown.
84
cmake/QtUtils.cmake
Normal file
84
cmake/QtUtils.cmake
Normal file
@ -0,0 +1,84 @@
|
||||
macro(msvc_registry_search)
|
||||
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
|
||||
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)
|
||||
|
||||
list(GET QT_VERSIONS 0 QT_VERSION)
|
||||
|
||||
# fix any double slashes which seem to be common
|
||||
string(REPLACE "//" "/" QT_VERSION "${QT_VERSION}")
|
||||
|
||||
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)
|
||||
if(CMAKE_CL_64)
|
||||
SET(QT_SUFFIX "_64")
|
||||
else()
|
||||
set(QT_SUFFIX "")
|
||||
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})
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
set(imported_location "${qt5_install_prefix}/bin/windeployqt.exe")
|
||||
|
||||
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()
|
||||
endmacro(find_qt5)
|
101
deploy.ps1
Normal file
101
deploy.ps1
Normal file
@ -0,0 +1,101 @@
|
||||
param([string]$version)
|
||||
|
||||
cd $PSScriptRoot;
|
||||
mkdir -Force -Verbose builds;
|
||||
cd builds;
|
||||
mkdir -Force -Verbose x86;
|
||||
mkdir -Force -Verbose x64;
|
||||
|
||||
foreach ($language in @{
|
||||
ENGLISH="";
|
||||
SPANISH="Spanish";
|
||||
SIMPLIFIED_CHINESE="Simplified-Chinese";
|
||||
RUSSIAN="Russian";
|
||||
TURKISH="Turkish";
|
||||
INDONESIAN="Indonesian";
|
||||
PORTUGUESE="Portuguese";
|
||||
THAI="Thai";
|
||||
KOREAN="Korean";
|
||||
ITALIAN="Italian";
|
||||
FRENCH="French"
|
||||
}.GetEnumerator())
|
||||
{
|
||||
$folder = "Textractor-$($language.Value)-$version";
|
||||
rm -Force -Recurse -Verbose $folder;
|
||||
mkdir -Force -Verbose $folder;
|
||||
|
||||
foreach ($arch in @("x86", "x64"))
|
||||
{
|
||||
cd $arch;
|
||||
$VS_arch = if ($arch -eq "x86") {"Win32"} else {"x64"};
|
||||
&"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 ..;
|
||||
mkdir -Force -Verbose "$folder/$arch";
|
||||
foreach ($file in @(
|
||||
"Textractor.exe",
|
||||
"TextractorCLI.exe",
|
||||
"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";
|
||||
mkdir -Force -Verbose "Runtime";
|
||||
foreach ($arch in @("x86", "x64"))
|
||||
{
|
||||
mkdir -Force -Verbose "Runtime/$arch";
|
||||
foreach ($file in @(
|
||||
"LoaderDll.dll",
|
||||
"LocaleEmulator.dll",
|
||||
"Qt5Core.dll",
|
||||
"Qt5Gui.dll",
|
||||
"Qt5Network.dll",
|
||||
"Qt5WebSockets.dll",
|
||||
"Qt5WinExtras.dll"
|
||||
"Qt5Widgets.dll",
|
||||
"platforms",
|
||||
"styles"
|
||||
))
|
||||
{
|
||||
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 ..;
|
||||
&"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
32
docs/CREDITS.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Developers
|
||||
|
||||
- Textractor mainly made by [Artikash](https://github.com/Artikash) with the contributions of
|
||||
- Build system improvements by [DoumanAsh](https://github.com/DoumanAsh)
|
||||
- Original text removal in Extra Window by [Niakr1s](https://github.com/Niakr1s)
|
||||
- CLI bugfixes by [tinyAdapter](https://github.com/tinyAdapter)
|
||||
- Hook codes for WAFFLE and WillPlus by [lgztx96](https://github.com/lgztx96)
|
||||
- Hook codes for TokyoNecro by [Jazzinghen](https://github.com/Jazzinghen)
|
||||
- Hook codes for ShinyDays and Artemis by [luojunyuan](https://github.com/luojunyuan)
|
||||
- Google Chrome DevTools integration by [zeheyler](https://github.com/zeheyler)
|
||||
- AppVeyor setup by [silas1037](https://github.com/silas1037)
|
||||
- Improved attach dialog by [tera8m4](https://github.com/tera8m4)
|
||||
- Regex Replacer, Papago, Systran, improvements to Extra Window, and bugfixes by [Blu3train](https://github.com/Blu3train)
|
||||
- French translation by [Racky](mailto:maitrenoah@gmail.com) and [Gratusfr](https://github.com/Gratusfr)
|
||||
- Spanish translation by [scese250](https://github.com/scese250)
|
||||
- Turkish translation by [niisokusu](https://reddit.com/u/niisokusu)
|
||||
- Simplified Chinese translation by [tinyAdapter](https://github.com/tinyAdapter), [lgztx96](https://github.com/lgztx96) and [chinanoahli](https://github.com/chinanoahli)
|
||||
- Russian translation by [TokcDK](https://github.com/TokcDK)
|
||||
- Indonesian translation by [Hawxone](https://github.com/Hawxone)
|
||||
- Portuguese translation by [TsumiHokiro](https://github.com/TsumiHokiro)
|
||||
- Thai translation by [azmadoppler](https://github.com/azmadoppler)
|
||||
- Korean translation by [O SK](mailto:afkl11@outlook.kr)
|
||||
- Italian translation by [StarFang208](https://github.com/StarFang208)
|
||||
- ITHVNR updated by [mireado](https://github.com/mireado), [Eguni](https://github.com/Eguni), and [IJEMIN](https://github.com/IJEMIN)
|
||||
- ITHVNR originally made by [Stomp](mailto:zorkzero@hotmail.com)
|
||||
- VNR engine made by [jichi](https://github.com/jichifly)
|
||||
- ITH updated by [Andys](https://github.com/AndyScull)
|
||||
- ITH originally made by [kaosu](https://code.google.com/archive/p/interactive-text-hooker)
|
||||
|
||||
If you're on this list and want your link changed let Artikash know.
|
||||
|
||||
# THANK YOU!!
|
7
docs/TUTORIAL.md
Normal file
7
docs/TUTORIAL.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Tutorial Video
|
||||
|
||||
https://www.youtube.com/watch?v=eecEOacF6mw
|
||||
|
||||
## Updates/Corrections
|
||||
|
||||
Automatically finding hooks is now done via the `Search for hooks` button, the method shown in the video is found in the `Search for specific text` option.
|
64
extensions/CMakeLists.txt
Normal file
64
extensions/CMakeLists.txt
Normal file
@ -0,0 +1,64 @@
|
||||
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()
|
241
extensions/bingtranslate.cpp
Normal file
241
extensions/bingtranslate.cpp
Normal file
@ -0,0 +1,241 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "network.h"
|
||||
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
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
|
||||
{
|
||||
"Afrikaans",
|
||||
"Albanian",
|
||||
"Amharic",
|
||||
"Arabic",
|
||||
"Armenian",
|
||||
"Assamese",
|
||||
"Azerbaijani",
|
||||
"Bangla",
|
||||
"Bosnian (Latin)",
|
||||
"Bulgarian",
|
||||
"Cantonese (Traditional)",
|
||||
"Catalan",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dari",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Fijian",
|
||||
"Filipino",
|
||||
"Finnish",
|
||||
"French",
|
||||
"French (Canada)",
|
||||
"German",
|
||||
"Greek",
|
||||
"Gujarati",
|
||||
"Haitian Creole",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hmong Daw",
|
||||
"Hungarian",
|
||||
"Icelandic",
|
||||
"Indonesian",
|
||||
"Inuktitut",
|
||||
"Irish",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Kannada",
|
||||
"Kazakh",
|
||||
"Khmer",
|
||||
"Klingon",
|
||||
"Korean",
|
||||
"Kurdish (Central)",
|
||||
"Kurdish (Northern)",
|
||||
"Lao",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Malagasy",
|
||||
"Malay",
|
||||
"Malayalam",
|
||||
"Maltese",
|
||||
"Maori",
|
||||
"Marathi",
|
||||
"Myanmar",
|
||||
"Nepali",
|
||||
"Norwegian",
|
||||
"Odia",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese (Brazil)",
|
||||
"Portuguese (Portugal)",
|
||||
"Punjabi",
|
||||
"Queretaro Otomi",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Samoan",
|
||||
"Serbian (Cyrillic)",
|
||||
"Serbian (Latin)",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swahili",
|
||||
"Swedish",
|
||||
"Tahitian",
|
||||
"Tamil",
|
||||
"Telugu",
|
||||
"Thai",
|
||||
"Tigrinya",
|
||||
"Tongan",
|
||||
"Turkish",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Vietnamese",
|
||||
"Welsh",
|
||||
"Yucatec Maya"
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Afrikaans" }, { L"af" } },
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Amharic" }, { L"am" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Armenian" }, { L"hy" } },
|
||||
{ { L"Assamese" }, { L"as" } },
|
||||
{ { L"Azerbaijani" }, { L"az" } },
|
||||
{ { L"Bangla" }, { L"bn" } },
|
||||
{ { L"Bosnian (Latin)" }, { L"bs" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Cantonese (Traditional)" }, { L"yue" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-Hans" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zh-Hant" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dari" }, { L"prs" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Fijian" }, { L"fj" } },
|
||||
{ { L"Filipino" }, { L"fil" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"French (Canada)" }, { L"fr-ca" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Gujarati" }, { L"gu" } },
|
||||
{ { L"Haitian Creole" }, { L"ht" } },
|
||||
{ { L"Hebrew" }, { L"he" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hmong Daw" }, { L"mww" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Icelandic" }, { L"is" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Inuktitut" }, { L"iu" } },
|
||||
{ { L"Irish" }, { L"ga" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Kannada" }, { L"kn" } },
|
||||
{ { L"Kazakh" }, { L"kk" } },
|
||||
{ { L"Khmer" }, { L"km" } },
|
||||
{ { L"Klingon" }, { L"tlh-Latn" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Kurdish (Central)" }, { L"ku" } },
|
||||
{ { L"Kurdish (Northern)" }, { L"kmr" } },
|
||||
{ { L"Lao" }, { L"lo" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Malagasy" }, { L"mg" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Malayalam" }, { L"ml" } },
|
||||
{ { L"Maltese" }, { L"mt" } },
|
||||
{ { L"Maori" }, { L"mi" } },
|
||||
{ { L"Marathi" }, { L"mr" } },
|
||||
{ { L"Myanmar" }, { L"my" } },
|
||||
{ { L"Nepali" }, { L"ne" } },
|
||||
{ { L"Norwegian" }, { L"nb" } },
|
||||
{ { L"Odia" }, { L"or" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese (Brazil)" }, { L"pt" } },
|
||||
{ { L"Portuguese (Portugal)" }, { L"pt-pt" } },
|
||||
{ { L"Punjabi" }, { L"pa" } },
|
||||
{ { L"Queretaro Otomi" }, { L"otq" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Samoan" }, { L"sm" } },
|
||||
{ { L"Serbian (Cyrillic)" }, { L"sr-Cyrl" } },
|
||||
{ { L"Serbian (Latin)" }, { L"sr-Latn" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Swahili" }, { L"sw" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tahitian" }, { L"ty" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Telugu" }, { L"te" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Tigrinya" }, { L"ti" } },
|
||||
{ { L"Tongan" }, { L"to" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"Welsh" }, { L"cy" } },
|
||||
{ { L"Yucatec Maya" }, { L"yua" } },
|
||||
{ { L"?" }, { L"auto-detect" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = false, 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=%s&to=%s&text=%s%s.%d", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text), token.Copy(), i++).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 (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
57
extensions/blockmarkup.h
Normal 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;
|
||||
};
|
16
extensions/copyclipboard.cpp
Normal file
16
extensions/copyclipboard.cpp
Normal file
@ -0,0 +1,16 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["current select"] && sentenceInfo["process id"] != 0)
|
||||
{
|
||||
if (!OpenClipboard(FindWindowW(NULL, L"Textractor"))) return false;
|
||||
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, (sentence.size() + 2) * sizeof(wchar_t));
|
||||
memcpy(GlobalLock(hMem), sentence.c_str(), (sentence.size() + 2) * sizeof(wchar_t));
|
||||
EmptyClipboard();
|
||||
SetClipboardData(CF_UNICODETEXT, hMem);
|
||||
GlobalUnlock(hMem);
|
||||
CloseClipboard();
|
||||
}
|
||||
return false;
|
||||
}
|
180
extensions/deepltranslate.cpp
Normal file
180
extensions/deepltranslate.cpp
Normal 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
174
extensions/devtools.cpp
Normal file
@ -0,0 +1,174 @@
|
||||
#include "devtools.h"
|
||||
#include "module.h"
|
||||
#include <ppltasks.h>
|
||||
#include <ShlObj.h>
|
||||
#include <QWebSocket>
|
||||
#include <QMetaEnum>
|
||||
#include <QFileDialog>
|
||||
#include <QMouseEvent>
|
||||
|
||||
extern const char* CHROME_LOCATION;
|
||||
extern const char* START_DEVTOOLS;
|
||||
extern const char* STOP_DEVTOOLS;
|
||||
extern const char* HIDE_CHROME;
|
||||
extern const char* DEVTOOLS_STATUS;
|
||||
extern const char* AUTO_START;
|
||||
|
||||
extern const char* TRANSLATION_PROVIDER;
|
||||
|
||||
extern QFormLayout* display;
|
||||
extern Settings settings;
|
||||
|
||||
namespace
|
||||
{
|
||||
QLabel* statusLabel;
|
||||
AutoHandle<> process = NULL;
|
||||
QWebSocket webSocket;
|
||||
std::atomic<int> idCounter = 0;
|
||||
Synchronized<std::unordered_map<int, concurrency::task_completion_event<JSON::Value<wchar_t>>>> mapQueue;
|
||||
|
||||
void StatusChanged(QString status)
|
||||
{
|
||||
QMetaObject::invokeMethod(statusLabel, std::bind(&QLabel::setText, statusLabel, status));
|
||||
}
|
||||
void Start(std::wstring chromePath, bool headless)
|
||||
{
|
||||
if (process) DevTools::Close();
|
||||
|
||||
auto args = FormatString(
|
||||
L"%s --proxy-server=direct:// --disable-extensions --disable-gpu --no-first-run --user-data-dir=\"%s\\devtoolscache\" --remote-debugging-port=9222",
|
||||
chromePath,
|
||||
std::filesystem::current_path().wstring()
|
||||
);
|
||||
args += headless ? L" --window-size=1920,1080 --headless" : L" --window-size=850,900";
|
||||
DWORD exitCode = 0;
|
||||
STARTUPINFOW DUMMY = { sizeof(DUMMY) };
|
||||
PROCESS_INFORMATION processInfo = {};
|
||||
if (!CreateProcessW(NULL, args.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &DUMMY, &processInfo)) return StatusChanged("StartupFailed");
|
||||
CloseHandle(processInfo.hThread);
|
||||
process = processInfo.hProcess;
|
||||
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"127.0.0.1",
|
||||
L"POST",
|
||||
L"/json/list",
|
||||
"",
|
||||
NULL,
|
||||
9222,
|
||||
NULL,
|
||||
WINHTTP_FLAG_ESCAPE_DISABLE
|
||||
})
|
||||
if (auto list = Copy(JSON::Parse(httpRequest.response).Array())) if (auto it = std::find_if(
|
||||
list->begin(),
|
||||
list->end(),
|
||||
[](const JSON::Value<wchar_t>& object) { return object[L"type"].String() && *object[L"type"].String() == L"page" && object[L"webSocketDebuggerUrl"].String(); }
|
||||
); it != list->end()) return webSocket.open(S(*(*it)[L"webSocketDebuggerUrl"].String()));
|
||||
|
||||
StatusChanged("ConnectingFailed");
|
||||
}
|
||||
|
||||
auto _ = ([]
|
||||
{
|
||||
QObject::connect(&webSocket, &QWebSocket::stateChanged,
|
||||
[](QAbstractSocket::SocketState state) { StatusChanged(QMetaEnum::fromType<QAbstractSocket::SocketState>().valueToKey(state)); });
|
||||
QObject::connect(&webSocket, &QWebSocket::textMessageReceived, [](QString message)
|
||||
{
|
||||
auto result = JSON::Parse(S(message));
|
||||
auto mapQueue = ::mapQueue.Acquire();
|
||||
if (auto id = result[L"id"].Number()) if (auto request = mapQueue->find((int)*id); request != mapQueue->end())
|
||||
{
|
||||
request->second.set(result);
|
||||
mapQueue->erase(request);
|
||||
}
|
||||
});
|
||||
}(), 0);
|
||||
}
|
||||
|
||||
namespace DevTools
|
||||
{
|
||||
void Initialize()
|
||||
{
|
||||
QString chromePath = settings.value(CHROME_LOCATION).toString();
|
||||
if (chromePath.isEmpty())
|
||||
{
|
||||
for (auto [_, process] : GetAllProcesses())
|
||||
if (process && (process->find(L"\\chrome.exe") != std::string::npos || process->find(L"\\msedge.exe") != std::string::npos)) chromePath = S(process.value());
|
||||
wchar_t programFiles[MAX_PATH + 100] = {};
|
||||
for (auto folder : { CSIDL_PROGRAM_FILESX86, CSIDL_PROGRAM_FILES, CSIDL_LOCAL_APPDATA })
|
||||
{
|
||||
SHGetFolderPathW(NULL, folder, NULL, SHGFP_TYPE_CURRENT, programFiles);
|
||||
wcscat_s(programFiles, L"/Google/Chrome/Application/chrome.exe");
|
||||
if (std::filesystem::exists(programFiles)) chromePath = S(programFiles);
|
||||
}
|
||||
}
|
||||
auto chromePathEdit = new QLineEdit(chromePath);
|
||||
static struct : QObject
|
||||
{
|
||||
bool eventFilter(QObject* object, QEvent* event)
|
||||
{
|
||||
if (auto mouseEvent = dynamic_cast<QMouseEvent*>(event))
|
||||
if (mouseEvent->button() == Qt::LeftButton)
|
||||
if (QString chromePath = QFileDialog::getOpenFileName(nullptr, TRANSLATION_PROVIDER, "/", "Google Chrome (*.exe)"); !chromePath.isEmpty())
|
||||
((QLineEdit*)object)->setText(chromePath);
|
||||
return false;
|
||||
}
|
||||
} chromeSelector;
|
||||
chromePathEdit->installEventFilter(&chromeSelector);
|
||||
QObject::connect(chromePathEdit, &QLineEdit::textChanged, [chromePathEdit](QString path) { settings.setValue(CHROME_LOCATION, path); });
|
||||
display->addRow(CHROME_LOCATION, chromePathEdit);
|
||||
auto headlessCheck = new QCheckBox();
|
||||
auto startButton = new QPushButton(START_DEVTOOLS), stopButton = new QPushButton(STOP_DEVTOOLS);
|
||||
headlessCheck->setChecked(settings.value(HIDE_CHROME, true).toBool());
|
||||
QObject::connect(headlessCheck, &QCheckBox::clicked, [](bool headless) { settings.setValue(HIDE_CHROME, headless); });
|
||||
QObject::connect(startButton, &QPushButton::clicked, [chromePathEdit, headlessCheck] { Start(S(chromePathEdit->text()), headlessCheck->isChecked()); });
|
||||
QObject::connect(stopButton, &QPushButton::clicked, &Close);
|
||||
auto buttons = new QHBoxLayout();
|
||||
buttons->addWidget(startButton);
|
||||
buttons->addWidget(stopButton);
|
||||
display->addRow(HIDE_CHROME, headlessCheck);
|
||||
auto autoStartCheck = new QCheckBox();
|
||||
autoStartCheck->setChecked(settings.value(AUTO_START, false).toBool());
|
||||
QObject::connect(autoStartCheck, &QCheckBox::clicked, [](bool autoStart) { settings.setValue(AUTO_START, autoStart); });
|
||||
display->addRow(AUTO_START, autoStartCheck);
|
||||
display->addRow(buttons);
|
||||
statusLabel = new QLabel("Stopped");
|
||||
statusLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);
|
||||
display->addRow(DEVTOOLS_STATUS, statusLabel);
|
||||
if (autoStartCheck->isChecked()) QMetaObject::invokeMethod(startButton, &QPushButton::click, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
webSocket.close();
|
||||
for (const auto& [_, task] : mapQueue.Acquire().contents) task.set_exception(std::runtime_error("closed"));
|
||||
mapQueue->clear();
|
||||
|
||||
if (process)
|
||||
{
|
||||
TerminateProcess(process, 0);
|
||||
WaitForSingleObject(process, 1000);
|
||||
for (int retry = 0; ++retry < 20; Sleep(100))
|
||||
try { std::filesystem::remove_all(L"devtoolscache"); break; }
|
||||
catch (std::filesystem::filesystem_error) { continue; }
|
||||
}
|
||||
process = NULL;
|
||||
StatusChanged("Stopped");
|
||||
}
|
||||
|
||||
bool Connected()
|
||||
{
|
||||
return webSocket.state() == QAbstractSocket::ConnectedState;
|
||||
}
|
||||
|
||||
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params)
|
||||
{
|
||||
concurrency::task_completion_event<JSON::Value<wchar_t>> response;
|
||||
int id = idCounter += 1;
|
||||
if (!Connected()) return {};
|
||||
mapQueue->try_emplace(id, response);
|
||||
QMetaObject::invokeMethod(&webSocket, std::bind(&QWebSocket::sendTextMessage, &webSocket, S(FormatString(LR"({"id":%d,"method":"%S","params":%s})", id, method, params))));
|
||||
try { if (auto result = create_task(response).get()[L"result"]) return result; } catch (...) {}
|
||||
return {};
|
||||
}
|
||||
}
|
10
extensions/devtools.h
Normal file
10
extensions/devtools.h
Normal file
@ -0,0 +1,10 @@
|
||||
#include "qtcommon.h"
|
||||
#include "network.h"
|
||||
|
||||
namespace DevTools
|
||||
{
|
||||
void Initialize();
|
||||
void Close();
|
||||
bool Connected();
|
||||
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params = L"{}");
|
||||
}
|
148
extensions/devtoolsdeepltranslate.cpp
Normal file
148
extensions/devtoolsdeepltranslate.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools DeepL Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese (Simplified)",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English (American)",
|
||||
"English (British)",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Portuguese (Brazilian)",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish"
|
||||
},
|
||||
languagesFrom =
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish"
|
||||
};
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Bulgarian" }, { L"Bulgarian" } },
|
||||
{ { L"Chinese" }, { L"Chinese" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"Chinese (simplified)" } },
|
||||
{ { L"Czech" }, { L"Czech" } },
|
||||
{ { L"Danish" }, { L"Danish" } },
|
||||
{ { L"Dutch" }, { L"Dutch" } },
|
||||
{ { L"English" }, { L"English" } },
|
||||
{ { L"English (American)" }, { L"English (American)" } },
|
||||
{ { L"English (British)" }, { L"English (British)" } },
|
||||
{ { L"Estonian" }, { L"Estonian" } },
|
||||
{ { L"Finnish" }, { L"Finnish" } },
|
||||
{ { L"French" }, { L"French" } },
|
||||
{ { L"German" }, { L"German" } },
|
||||
{ { L"Greek" }, { L"Greek" } },
|
||||
{ { L"Hungarian" }, { L"Hungarian" } },
|
||||
{ { L"Italian" }, { L"Italian" } },
|
||||
{ { L"Japanese" }, { L"Japanese" } },
|
||||
{ { L"Latvian" }, { L"Latvian" } },
|
||||
{ { L"Lithuanian" }, { L"Lithuanian" } },
|
||||
{ { L"Polish" }, { L"Polish" } },
|
||||
{ { L"Portuguese" }, { L"Portuguese" } },
|
||||
{ { L"Portuguese (Brazilian)" }, { L"Portuguese (Brazilian)" } },
|
||||
{ { L"Romanian" }, { L"Romanian" } },
|
||||
{ { L"Russian" }, { L"Russian" } },
|
||||
{ { L"Slovak" }, { L"Slovak" } },
|
||||
{ { L"Slovenian" }, { L"Slovenian" } },
|
||||
{ { L"Spanish" }, { L"Spanish" } },
|
||||
{ { L"Swedish" }, { L"Swedish" } },
|
||||
{ { L"?" }, { L"Detect language" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
|
||||
// DevTools can't handle concurrent translations yet
|
||||
static std::mutex translationMutex;
|
||||
std::scoped_lock lock(translationMutex);
|
||||
std::wstring escaped; // DeepL breaks with slash in input
|
||||
for (auto ch : text) ch == '/' ? escaped += L"\\/" : escaped += ch;
|
||||
DevTools::SendRequest("Page.navigate", FormatString(LR"({"url":"https://www.deepl.com/en/translator#en/en/%s"})", Escape(escaped)));
|
||||
for (int retry = 0; ++retry < 20; Sleep(100))
|
||||
if (Copy(DevTools::SendRequest("Runtime.evaluate", LR"({"expression":"document.readyState"})")[L"result"][L"value"].String()) == L"complete") break;
|
||||
|
||||
DevTools::SendRequest("Runtime.evaluate", FormatString(LR"({"expression":"
|
||||
document.querySelector('.lmt__language_select--source').querySelector('button').click();
|
||||
document.evaluate(`//*[text()='%s']`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click();
|
||||
document.querySelector('.lmt__language_select--target').querySelector('button').click();
|
||||
document.evaluate(`//*[text()='%s']`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click();
|
||||
"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo)));
|
||||
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('#target-dummydiv').innerHTML.trim() ","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
|
||||
if (auto errorMessage = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('div.lmt__system_notification').innerHTML","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, errorMessage.value()) };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
82
extensions/devtoolspapagotranslate.cpp
Normal file
82
extensions/devtoolspapagotranslate.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools Papago Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"English",
|
||||
"French",
|
||||
"German",
|
||||
"Hindi",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Portuguese",
|
||||
"Russian",
|
||||
"Spanish",
|
||||
"Thai",
|
||||
"Vietnamese",
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-CN" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zt-TW" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"?" }, { L"auto" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
|
||||
// DevTools can't handle concurrent translations yet
|
||||
static std::mutex translationMutex;
|
||||
std::scoped_lock lock(translationMutex);
|
||||
DevTools::SendRequest("Page.navigate", FormatString(LR"({"url":"https://papago.naver.com/?sk=%s&tk=%s&st=%s"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text)));
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('#txtTarget').textContent.trim() ","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
152
extensions/devtoolssystrantranslate.cpp
Normal file
152
extensions/devtoolssystrantranslate.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools Systran Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Albanian",
|
||||
"Arabic",
|
||||
"Bengali",
|
||||
"Bulgarian",
|
||||
"Burmese",
|
||||
"Catalan",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hungarian",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Malay",
|
||||
"Norwegian",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Serbian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Somali",
|
||||
"Spanish",
|
||||
"Swedish",
|
||||
"Tagalog",
|
||||
"Tamil",
|
||||
"Thai",
|
||||
"Turkish",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Vietnamese"
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Bengali" }, { L"bn" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Burmese" }, { L"my" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zt" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Hebrew" }, { L"he" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Norwegian" }, { L"no" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Serbian" }, { L"sr" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Somali" }, { L"so" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tagalog" }, { L"tl" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"?" }, { L"autodetect" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
|
||||
// DevTools can't handle concurrent translations yet
|
||||
static std::mutex translationMutex;
|
||||
std::scoped_lock lock(translationMutex);
|
||||
|
||||
DevTools::SendRequest(
|
||||
"Page.navigate",
|
||||
FormatString(LR"({"url":"https://translate.systran.net/?source=%s&target=%s&input=%s"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text))
|
||||
);
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('#outputEditor').textContent.trim() ","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
21
extensions/extension.h
Normal file
21
extensions/extension.h
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
struct InfoForExtension
|
||||
{
|
||||
const char* name;
|
||||
int64_t value;
|
||||
};
|
||||
|
||||
struct SentenceInfo
|
||||
{
|
||||
const InfoForExtension* infoArray;
|
||||
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;
|
||||
return *(int*)0xDEAD = 0; // gives better error message than alternatives
|
||||
}
|
||||
};
|
||||
|
||||
struct SKIP {};
|
||||
inline void Skip() { throw SKIP(); }
|
33
extensions/extensionimpl.cpp
Normal file
33
extensions/extensionimpl.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo);
|
||||
|
||||
/*
|
||||
You shouldn't mess with this or even look at it unless you're certain you know what you're doing.
|
||||
Param sentence: pointer to sentence received by Textractor (UTF-16).
|
||||
This can be modified. Textractor uses the modified sentence for future processing and display. If empty (starts with null terminator), Textractor will destroy it.
|
||||
Textractor will display the sentence after all extensions have had a chance to process and/or modify it.
|
||||
The buffer is allocated using HeapAlloc(). If you want to make it larger, please use HeapReAlloc().
|
||||
Param sentenceInfo: pointer to array containing misc info about the sentence. End of array is marked with name being nullptr.
|
||||
Return value: the buffer used for the sentence. Remember to return a new pointer if HeapReAlloc() gave you one.
|
||||
This function may be run concurrently with itself: please make sure it's thread safe.
|
||||
It will not be run concurrently with DllMain.
|
||||
*/
|
||||
extern "C" __declspec(dllexport) wchar_t* OnNewSentence(wchar_t* sentence, const InfoForExtension* sentenceInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::wstring sentenceCopy(sentence);
|
||||
int oldSize = sentenceCopy.size();
|
||||
if (ProcessSentence(sentenceCopy, SentenceInfo{ sentenceInfo }))
|
||||
{
|
||||
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)
|
||||
{
|
||||
*sentence = L'\0';
|
||||
}
|
||||
return sentence;
|
||||
}
|
8
extensions/extranewlines.cpp
Normal file
8
extensions/extranewlines.cpp
Normal file
@ -0,0 +1,8 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
sentence += L"\n";
|
||||
return true;
|
||||
}
|
604
extensions/extrawindow.cpp
Normal file
604
extensions/extrawindow.cpp
Normal file
@ -0,0 +1,604 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "ui_extrawindow.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <fstream>
|
||||
#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* 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;
|
||||
extern const char* TEXT_COLOR;
|
||||
extern const char* TEXT_OUTLINE;
|
||||
extern const char* OUTLINE_COLOR;
|
||||
extern const char* OUTLINE_SIZE;
|
||||
extern const char* OUTLINE_SIZE_INFO;
|
||||
extern const char* FONT;
|
||||
|
||||
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)
|
||||
{
|
||||
QColor color = QColorDialog::getColor(default, parent, title);
|
||||
if (customOpacity) color.setAlpha(255 * QInputDialog::getDouble(parent, title, OPACITY, default.alpha() / 255.0, 0, 1, 3, nullptr, Qt::WindowCloseButtonHint));
|
||||
return color;
|
||||
}
|
||||
|
||||
struct PrettyWindow : QDialog, Localizer
|
||||
{
|
||||
PrettyWindow(const char* name)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
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);
|
||||
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] { 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);
|
||||
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()
|
||||
{
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
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 };
|
||||
Settings settings{ this };
|
||||
|
||||
private:
|
||||
void RequestFont()
|
||||
{
|
||||
if (QFont font = QFontDialog::getFont(&ok, ui.display->font(), this, FONT); ok)
|
||||
{
|
||||
settings.setValue(FONT, font.toString());
|
||||
ui.display->setFont(font);
|
||||
}
|
||||
};
|
||||
|
||||
void SetBackgroundColor(QColor color)
|
||||
{
|
||||
if (!color.isValid()) return;
|
||||
if (color.alpha() == 0) color.setAlpha(1);
|
||||
backgroundColor = color;
|
||||
repaint();
|
||||
settings.setValue(BG_COLOR, color.name(QColor::HexArgb));
|
||||
};
|
||||
|
||||
QColor TextColor()
|
||||
{
|
||||
return ui.display->palette().color(QPalette::WindowText);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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, -outliner->size, 0, INT_MAX, 2, nullptr, Qt::WindowCloseButtonHint);
|
||||
}
|
||||
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(), backgroundColor);
|
||||
}
|
||||
|
||||
bool autoHide = false, hidden = false;
|
||||
QColor backgroundColor{ palette().window().color() };
|
||||
struct Outliner : QGraphicsEffect
|
||||
{
|
||||
void draw(QPainter* painter) override
|
||||
{
|
||||
if (size < 0) return drawSource(painter);
|
||||
QPoint offset;
|
||||
QPixmap pixmap = sourcePixmap(Qt::LogicalCoordinates, &offset);
|
||||
offset.setX(offset.x() + size);
|
||||
for (auto offset2 : Array<QPointF>{ { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 }, { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 } })
|
||||
{
|
||||
QImage outline = pixmap.toImage();
|
||||
QPainter outlinePainter(&outline);
|
||||
outlinePainter.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
outlinePainter.fillRect(outline.rect(), color);
|
||||
painter->drawImage(offset + offset2 * size, outline);
|
||||
}
|
||||
painter->drawPixmap(offset, pixmap);
|
||||
}
|
||||
QColor color{ Qt::black };
|
||||
double size = -0.5;
|
||||
}* outliner;
|
||||
};
|
||||
|
||||
class ExtraWindow : public PrettyWindow, QAbstractNativeEventFilter
|
||||
{
|
||||
public:
|
||||
ExtraWindow() : PrettyWindow("Extra Window")
|
||||
{
|
||||
ui.display->setTextFormat(Qt::PlainText);
|
||||
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::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
|
||||
QMetaObject::invokeMethod(this, std::bind(slot, this, default = settings.value(name, default).toBool()), Qt::QueuedConnection);
|
||||
auto action = menu.addAction(name, this, slot);
|
||||
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);
|
||||
}
|
||||
|
||||
~ExtraWindow()
|
||||
{
|
||||
settings.setValue(WINDOW, geometry());
|
||||
}
|
||||
|
||||
void AddSentence(QString sentence)
|
||||
{
|
||||
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;
|
||||
DisplaySentence();
|
||||
}
|
||||
|
||||
private:
|
||||
void DisplaySentence()
|
||||
{
|
||||
if (sentenceHistory.empty()) return;
|
||||
QString sentence = sentenceHistory[historyIndex];
|
||||
if (sentence.contains(u8"\x200b \n"))
|
||||
if (!showOriginal) sentence = sentence.split(u8"\x200b \n")[1];
|
||||
else if (showOriginalAfterTranslation) sentence = sentence.split(u8"\x200b \n")[1] + "\n" + sentence.split(u8"\x200b \n")[0];
|
||||
|
||||
if (sizeLock && !autoResize)
|
||||
{
|
||||
QFontMetrics fontMetrics(ui.display->font(), ui.display);
|
||||
int low = 0, high = sentence.size(), last = 0;
|
||||
while (low <= high)
|
||||
{
|
||||
int mid = (low + high) / 2;
|
||||
if (fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence.left(mid)).height() <= ui.display->height())
|
||||
{
|
||||
last = mid;
|
||||
low = mid + 1;
|
||||
}
|
||||
else high = mid - 1;
|
||||
}
|
||||
sentence = sentence.left(last);
|
||||
}
|
||||
|
||||
ui.display->setText(sentence);
|
||||
if (autoResize)
|
||||
resize(width(), height() - ui.display->height() +
|
||||
QFontMetrics(ui.display->font(), ui.display).boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence).height()
|
||||
);
|
||||
}
|
||||
|
||||
void SetTopmost(bool topmost)
|
||||
{
|
||||
for (auto window : { winId(), dictionaryWindow.winId() })
|
||||
SetWindowPos((HWND)window, topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
settings.setValue(TOPMOST, topmost);
|
||||
};
|
||||
|
||||
void SetPositionLock(bool locked)
|
||||
{
|
||||
settings.setValue(POSITION_LOCK, posLock = locked);
|
||||
};
|
||||
|
||||
void SetSizeLock(bool locked)
|
||||
{
|
||||
setSizeGripEnabled(!locked);
|
||||
settings.setValue(SIZE_LOCK, sizeLock = locked);
|
||||
};
|
||||
|
||||
void SetCenteredText(bool centeredText)
|
||||
{
|
||||
ui.display->setAlignment(centeredText ? Qt::AlignHCenter : Qt::AlignLeft);
|
||||
settings.setValue(CENTERED_TEXT, this->centeredText = centeredText);
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
dictionaryWindow.UpdateDictionary();
|
||||
if (dictionaryWindow.dictionary.empty())
|
||||
{
|
||||
std::ofstream(DICTIONARY_SAVE_FILE) << u8"\ufeff" << DICTIONARY_INSTRUCTIONS;
|
||||
_spawnlp(_P_DETACH, "notepad", "notepad", DICTIONARY_SAVE_FILE, NULL); // show file to user
|
||||
}
|
||||
}
|
||||
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 (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();
|
||||
oldPos = event->globalPos();
|
||||
}
|
||||
|
||||
void mouseMoveEvent(QMouseEvent* event) override
|
||||
{
|
||||
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) --historyIndex;
|
||||
if (scroll < 0 && historyIndex + 1 < sentenceHistory.size()) ++historyIndex;
|
||||
DisplaySentence();
|
||||
}
|
||||
|
||||
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")
|
||||
{
|
||||
ui.display->setSizePolicy({ QSizePolicy::Fixed, QSizePolicy::Minimum });
|
||||
}
|
||||
|
||||
void UpdateDictionary()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (dictionaryFileLastWrite == std::filesystem::last_write_time(DICTIONARY_SAVE_FILE)) return;
|
||||
dictionaryFileLastWrite = std::filesystem::last_write_time(DICTIONARY_SAVE_FILE);
|
||||
}
|
||||
catch (std::filesystem::filesystem_error) { return; }
|
||||
|
||||
dictionary.clear();
|
||||
charStorage.clear();
|
||||
|
||||
auto StoreCopy = [&](std::string_view string)
|
||||
{
|
||||
auto location = &*charStorage.insert(charStorage.end(), string.begin(), string.end());
|
||||
charStorage.push_back(0);
|
||||
return location;
|
||||
};
|
||||
|
||||
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())
|
||||
{
|
||||
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 });
|
||||
}
|
||||
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())
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
void SetTerm(QString term)
|
||||
{
|
||||
this->term = term;
|
||||
UpdateDictionary();
|
||||
definitions.clear();
|
||||
definitionIndex = 0;
|
||||
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 hide();
|
||||
ui.display->setText(definitions[definitionIndex]);
|
||||
adjustSize();
|
||||
resize(width(), 1);
|
||||
show();
|
||||
}
|
||||
|
||||
struct DictionaryEntry
|
||||
{
|
||||
const char* term;
|
||||
const char* definition;
|
||||
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();
|
||||
if (scroll > 0 && definitionIndex > 0) definitionIndex -= 1;
|
||||
if (scroll < 0 && definitionIndex + 1 < definitions.size()) definitionIndex += 1;
|
||||
int oldHeight = height();
|
||||
ShowDefinition();
|
||||
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> charStorage;
|
||||
std::vector<QString> definitions;
|
||||
int definitionIndex;
|
||||
} dictionaryWindow;
|
||||
} extraWindow;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["current select"] && sentenceInfo["text number"] != 0)
|
||||
QMetaObject::invokeMethod(&extraWindow, [sentence = S(sentence)] { extraWindow.AddSentence(sentence); });
|
||||
return false;
|
||||
}
|
51
extensions/extrawindow.ui
Normal file
51
extensions/extrawindow.ui
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ExtraWindow</class>
|
||||
<widget class="QDialog" name="ExtraWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<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">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>16</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
267
extensions/googletranslate.cpp
Normal file
267
extensions/googletranslate.cpp
Normal file
@ -0,0 +1,267 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "network.h"
|
||||
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
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
|
||||
{
|
||||
"Afrikaans",
|
||||
"Albanian",
|
||||
"Amharic",
|
||||
"Arabic",
|
||||
"Armenian",
|
||||
"Azerbaijani",
|
||||
"Basque",
|
||||
"Belarusian",
|
||||
"Bengali",
|
||||
"Bosnian",
|
||||
"Bulgarian",
|
||||
"Catalan",
|
||||
"Cebuano",
|
||||
"Chichewa",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Corsican",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Esperanto",
|
||||
"Estonian",
|
||||
"Filipino",
|
||||
"Finnish",
|
||||
"French",
|
||||
"Frisian",
|
||||
"Galician",
|
||||
"Georgian",
|
||||
"German",
|
||||
"Greek",
|
||||
"Gujarati",
|
||||
"Haitian Creole",
|
||||
"Hausa",
|
||||
"Hawaiian",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hmong",
|
||||
"Hungarian",
|
||||
"Icelandic",
|
||||
"Igbo",
|
||||
"Indonesian",
|
||||
"Irish",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Javanese",
|
||||
"Kannada",
|
||||
"Kazakh",
|
||||
"Khmer",
|
||||
"Kinyarwanda",
|
||||
"Korean",
|
||||
"Kurdish (Kurmanji)",
|
||||
"Kyrgyz",
|
||||
"Lao",
|
||||
"Latin",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Luxembourgish",
|
||||
"Macedonian",
|
||||
"Malagasy",
|
||||
"Malay",
|
||||
"Malayalam",
|
||||
"Maltese",
|
||||
"Maori",
|
||||
"Marathi",
|
||||
"Mongolian",
|
||||
"Myanmar (Burmese)",
|
||||
"Nepali",
|
||||
"Norwegian",
|
||||
"Odia (Oriya)",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Punjabi",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Samoan",
|
||||
"Scots Gaelic",
|
||||
"Serbian",
|
||||
"Sesotho",
|
||||
"Shona",
|
||||
"Sindhi",
|
||||
"Sinhala",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Somali",
|
||||
"Spanish",
|
||||
"Sundanese",
|
||||
"Swahili",
|
||||
"Swedish",
|
||||
"Tajik",
|
||||
"Tamil",
|
||||
"Tatar",
|
||||
"Telugu",
|
||||
"Thai",
|
||||
"Turkish",
|
||||
"Turkmen",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Uyghur",
|
||||
"Uzbek",
|
||||
"Vietnamese",
|
||||
"Welsh",
|
||||
"Xhosa",
|
||||
"Yiddish",
|
||||
"Yoruba",
|
||||
"Zulu",
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Afrikaans" }, { L"af" } },
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Amharic" }, { L"am" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Armenian" }, { L"hy" } },
|
||||
{ { L"Azerbaijani" }, { L"az" } },
|
||||
{ { L"Basque" }, { L"eu" } },
|
||||
{ { L"Belarusian" }, { L"be" } },
|
||||
{ { L"Bengali" }, { L"bn" } },
|
||||
{ { L"Bosnian" }, { L"bs" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Cebuano" }, { L"ceb" } },
|
||||
{ { L"Chichewa" }, { L"ny" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-CN" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zh-TW" } },
|
||||
{ { L"Corsican" }, { L"co" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Esperanto" }, { L"eo" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Filipino" }, { L"tl" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"Frisian" }, { L"fy" } },
|
||||
{ { L"Galician" }, { L"gl" } },
|
||||
{ { L"Georgian" }, { L"ka" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Gujarati" }, { L"gu" } },
|
||||
{ { L"Haitian Creole" }, { L"ht" } },
|
||||
{ { L"Hausa" }, { L"ha" } },
|
||||
{ { L"Hawaiian" }, { L"haw" } },
|
||||
{ { L"Hebrew" }, { L"iw" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hmong" }, { L"hmn" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Icelandic" }, { L"is" } },
|
||||
{ { L"Igbo" }, { L"ig" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Irish" }, { L"ga" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Javanese" }, { L"jw" } },
|
||||
{ { L"Kannada" }, { L"kn" } },
|
||||
{ { L"Kazakh" }, { L"kk" } },
|
||||
{ { L"Khmer" }, { L"km" } },
|
||||
{ { L"Kinyarwanda" }, { L"rw" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Kurdish (Kurmanji)" }, { L"ku" } },
|
||||
{ { L"Kyrgyz" }, { L"ky" } },
|
||||
{ { L"Lao" }, { L"lo" } },
|
||||
{ { L"Latin" }, { L"la" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Luxembourgish" }, { L"lb" } },
|
||||
{ { L"Macedonian" }, { L"mk" } },
|
||||
{ { L"Malagasy" }, { L"mg" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Malayalam" }, { L"ml" } },
|
||||
{ { L"Maltese" }, { L"mt" } },
|
||||
{ { L"Maori" }, { L"mi" } },
|
||||
{ { L"Marathi" }, { L"mr" } },
|
||||
{ { L"Mongolian" }, { L"mn" } },
|
||||
{ { L"Myanmar (Burmese)" }, { L"my" } },
|
||||
{ { L"Nepali" }, { L"ne" } },
|
||||
{ { L"Norwegian" }, { L"no" } },
|
||||
{ { L"Odia (Oriya)" }, { L"or" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Punjabi" }, { L"pa" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Samoan" }, { L"sm" } },
|
||||
{ { L"Scots Gaelic" }, { L"gd" } },
|
||||
{ { L"Serbian" }, { L"sr" } },
|
||||
{ { L"Sesotho" }, { L"st" } },
|
||||
{ { L"Shona" }, { L"sn" } },
|
||||
{ { L"Sindhi" }, { L"sd" } },
|
||||
{ { L"Sinhala" }, { L"si" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Somali" }, { L"so" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Sundanese" }, { L"su" } },
|
||||
{ { L"Swahili" }, { L"sw" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tajik" }, { L"tg" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Tatar" }, { L"tt" } },
|
||||
{ { L"Telugu" }, { L"te" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Turkmen" }, { L"tk" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Uyghur" }, { L"ug" } },
|
||||
{ { L"Uzbek" }, { L"uz" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"Welsh" }, { L"cy" } },
|
||||
{ { L"Xhosa" }, { L"xh" } },
|
||||
{ { L"Yiddish" }, { L"yi" } },
|
||||
{ { L"Yoruba" }, { L"yo" } },
|
||||
{ { L"Zulu" }, { L"zu" } },
|
||||
{ { L"?" }, { L"auto" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = false, 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"&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) };
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
{
|
||||
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) };
|
||||
}
|
130
extensions/lua.cpp
Normal file
130
extensions/lua.cpp
Normal file
@ -0,0 +1,130 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
extern const char* LUA_INTRO;
|
||||
extern const char* LOAD_SCRIPT;
|
||||
extern const wchar_t* LUA_ERROR;
|
||||
|
||||
constexpr auto LUA_SAVE_FILE = u8"Textractor.lua";
|
||||
|
||||
extern "C" // Lua library
|
||||
{
|
||||
enum LuaType { LUA_TNIL, LUA_TBOOLEAN, LUA_TLIGHTUSERDATA, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION, LUA_TUSERDATA, LUA_TTHREAD };
|
||||
|
||||
enum LuaStatus { LUA_OK, LUA_YIELD, LUA_ERRRUN, LUA_ERRSYNTAX, LUA_ERRMEM, LUA_ERRGCMM, LUA_ERRERR };
|
||||
|
||||
struct lua_State;
|
||||
lua_State* luaL_newstate();
|
||||
void luaL_openlibs(lua_State*);
|
||||
void lua_close(lua_State*);
|
||||
LuaStatus luaL_loadstring(lua_State*, const char* str);
|
||||
|
||||
const char* lua_tolstring(lua_State*, int index, size_t* size);
|
||||
const char* lua_pushstring(lua_State*, const char* str);
|
||||
void lua_pushinteger(lua_State*, int64_t n);
|
||||
void lua_createtable(lua_State*, int narr, int nrec);
|
||||
void lua_settable(lua_State*, int index);
|
||||
|
||||
void lua_settop(lua_State*, int index);
|
||||
LuaType lua_getglobal(lua_State*, const char* name);
|
||||
LuaStatus lua_pcallk(lua_State*, int nargs, int nresults, int msgh, void*, void*);
|
||||
}
|
||||
|
||||
bool luaL_dostring(lua_State* L, const char* str)
|
||||
{
|
||||
return luaL_loadstring(L, str) || lua_pcallk(L, 0, -1, 0, NULL, NULL);
|
||||
}
|
||||
|
||||
bool logErrors = true;
|
||||
Synchronized<std::string> script;
|
||||
std::atomic<int> revCount = 0;
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript);
|
||||
|
||||
if (scriptEditor.toPlainText().isEmpty()) scriptEditor.setPlainText(LUA_INTRO);
|
||||
layout.addWidget(&scriptEditor);
|
||||
layout.addWidget(&loadButton);
|
||||
|
||||
resize(800, 600);
|
||||
setWindowTitle("Lua");
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
|
||||
LoadScript();
|
||||
}
|
||||
|
||||
~Window()
|
||||
{
|
||||
Save();
|
||||
}
|
||||
|
||||
private:
|
||||
void LoadScript()
|
||||
{
|
||||
revCount += 1;
|
||||
script->assign(scriptEditor.toPlainText().toUtf8());
|
||||
Save();
|
||||
}
|
||||
|
||||
void Save()
|
||||
{
|
||||
QTextFile(LUA_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate).write(scriptEditor.toPlainText().toUtf8());
|
||||
}
|
||||
|
||||
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 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.Copy().c_str()) != LUA_OK)
|
||||
{
|
||||
sentence += L"\n" + FormatString(LUA_ERROR, StringToWideString(lua_tolstring(L, 1, nullptr)));
|
||||
lua_settop(L, 0);
|
||||
return logErrors;
|
||||
}
|
||||
}
|
||||
|
||||
if (lua_getglobal(L, "ProcessSentence") != LUA_TFUNCTION)
|
||||
{
|
||||
sentence += L"\n" + FormatString(LUA_ERROR, L"ProcessSentence is not a function");
|
||||
lua_settop(L, 0);
|
||||
return logErrors;
|
||||
}
|
||||
lua_pushstring(L, WideStringToString(sentence).c_str());
|
||||
lua_createtable(L, 0, 0);
|
||||
for (auto info = sentenceInfo.infoArray; info->name; ++info)
|
||||
{
|
||||
lua_pushstring(L, info->name);
|
||||
lua_pushinteger(L, info->value);
|
||||
lua_settable(L, 3);
|
||||
}
|
||||
if (lua_pcallk(L, 2, 1, 0, NULL, NULL) != LUA_OK)
|
||||
{
|
||||
sentence += L"\n" + FormatString(LUA_ERROR, StringToWideString(lua_tolstring(L, 1, nullptr)));
|
||||
lua_settop(L, 0);
|
||||
return logErrors;
|
||||
}
|
||||
if (const char* newSentence = lua_tolstring(L, 1, nullptr))
|
||||
{
|
||||
sentence = StringToWideString(newSentence);
|
||||
lua_settop(L, 0);
|
||||
return true;
|
||||
}
|
||||
lua_settop(L, 0);
|
||||
return false;
|
||||
}
|
65
extensions/network.cpp
Normal file
65
extensions/network.cpp
Normal file
@ -0,0 +1,65 @@
|
||||
#include "network.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** 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, port, 0))
|
||||
if (InternetHandle request = WinHttpOpenRequest(connection, action, objectName, httpVersion, referrer, acceptTypes, requestFlags))
|
||||
if (WinHttpSendRequest(request, headers, -1UL, body.empty() ? NULL : body.data(), body.size(), body.size(), NULL))
|
||||
{
|
||||
WinHttpReceiveResponse(request, NULL);
|
||||
|
||||
//DWORD size = 0;
|
||||
//WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &size, WINHTTP_NO_HEADER_INDEX);
|
||||
//this->headers.resize(size);
|
||||
//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
|
||||
{
|
||||
availableSize = 0;
|
||||
WinHttpQueryDataAvailable(request, &availableSize);
|
||||
if (!availableSize) break;
|
||||
std::vector<char> buffer(availableSize);
|
||||
WinHttpReadData(request, buffer.data(), availableSize, &downloadedSize);
|
||||
data.append(buffer.data(), downloadedSize);
|
||||
} while (availableSize > 0);
|
||||
response = StringToWideString(data);
|
||||
this->connection = std::move(connection);
|
||||
this->request = std::move(request);
|
||||
}
|
||||
else errorCode = GetLastError();
|
||||
else errorCode = GetLastError();
|
||||
else errorCode = GetLastError();
|
||||
else errorCode = GetLastError();
|
||||
}
|
||||
|
||||
std::wstring Escape(const std::wstring& text)
|
||||
{
|
||||
std::wstring escaped;
|
||||
for (unsigned char ch : WideStringToString(text)) escaped += FormatString(L"%%%02X", (int)ch);
|
||||
return escaped;
|
||||
}
|
||||
|
||||
std::string Escape(const std::string& text)
|
||||
{
|
||||
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"])")));
|
233
extensions/network.h
Normal file
233
extensions/network.h
Normal file
@ -0,0 +1,233 @@
|
||||
#pragma once
|
||||
|
||||
#include <winhttp.h>
|
||||
#include <variant>
|
||||
|
||||
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>;
|
||||
|
||||
struct 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 = 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** 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);
|
||||
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);
|
||||
}
|
||||
}
|
72
extensions/regexfilter.cpp
Normal file
72
extensions/regexfilter.cpp
Normal file
@ -0,0 +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;
|
||||
|
||||
const char* REGEX_SAVE_FILE = "SavedRegexFilters.txt";
|
||||
|
||||
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() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
|
||||
connect(ui.regexEdit, &QLineEdit::textEdited, this, &Window::SetRegex);
|
||||
connect(ui.saveButton, &QPushButton::clicked, this, &Window::Save);
|
||||
|
||||
setWindowTitle(REGEX_FILTER);
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void SetRegex(QString regex)
|
||||
{
|
||||
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;
|
||||
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;
|
||||
}
|
61
extensions/regexfilter.ui
Normal file
61
extensions/regexfilter.ui
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FilterWindow</class>
|
||||
<widget class="QDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>80</height>
|
||||
</rect>
|
||||
</property>
|
||||
<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><a href="https://regexr.com">regexr.com</a></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/>
|
||||
</ui>
|
72
extensions/regexreplacer.cpp
Normal file
72
extensions/regexreplacer.cpp
Normal file
@ -0,0 +1,72 @@
|
||||
#include "extension.h"
|
||||
#include "module.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <fstream>
|
||||
#include <process.h>
|
||||
|
||||
extern const wchar_t* REGEX_REPLACER_INSTRUCTIONS;
|
||||
|
||||
const char* REPLACE_SAVE_FILE = "SavedRegexReplacements.txt";
|
||||
|
||||
std::atomic<std::filesystem::file_time_type> replaceFileLastWrite = {};
|
||||
concurrency::reader_writer_lock m;
|
||||
std::vector<std::tuple<std::wregex, std::wstring, std::regex_constants::match_flag_type>> replacements;
|
||||
|
||||
void UpdateReplacements()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (replaceFileLastWrite.exchange(std::filesystem::last_write_time(REPLACE_SAVE_FILE)) == std::filesystem::last_write_time(REPLACE_SAVE_FILE)) return;
|
||||
std::scoped_lock lock(m);
|
||||
replacements.clear();
|
||||
std::ifstream stream(REPLACE_SAVE_FILE, std::ios::binary);
|
||||
BlockMarkupIterator savedFilters(stream, Array<std::wstring_view>{ L"|REGEX|", L"|BECOMES|", L"|MODIFIER|" });
|
||||
while (auto read = savedFilters.Next())
|
||||
{
|
||||
const auto& [regex, replacement, modifier] = read.value();
|
||||
try
|
||||
{
|
||||
replacements.emplace_back(
|
||||
std::wregex(regex, modifier.find(L'i') == std::string::npos ? std::regex::ECMAScript : std::regex::icase),
|
||||
replacement,
|
||||
modifier.find(L'g') == std::string::npos ? std::regex_constants::format_first_only : std::regex_constants::format_default
|
||||
);
|
||||
}
|
||||
catch (std::regex_error) {}
|
||||
}
|
||||
}
|
||||
catch (std::filesystem::filesystem_error) { replaceFileLastWrite.store({}); }
|
||||
}
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
UpdateReplacements();
|
||||
if (replacements.empty())
|
||||
{
|
||||
auto file = std::ofstream(REPLACE_SAVE_FILE, std::ios::binary) << "\xff\xfe";
|
||||
for (auto ch : std::wstring_view(REGEX_REPLACER_INSTRUCTIONS))
|
||||
file << (ch == L'\n' ? std::string_view("\r\0\n", 4) : std::string_view((char*)&ch, 2));
|
||||
SpawnThread([] { _spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); }); // show file to user
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
UpdateReplacements();
|
||||
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
for (const auto& [regex, replacement, flags] : replacements) sentence = std::regex_replace(sentence, regex, replacement, flags);
|
||||
return true;
|
||||
}
|
54
extensions/removerepeatchar.cpp
Normal file
54
extensions/removerepeatchar.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
std::vector<int> repeatNumbers(sentence.size() + 1, 0);
|
||||
for (int i = 0; i < sentence.size(); ++i)
|
||||
{
|
||||
if (sentence[i] != sentence[i + 1])
|
||||
{
|
||||
int j = i;
|
||||
while (sentence[j] == sentence[i] && --j >= 0);
|
||||
repeatNumbers[i - j] += 1;
|
||||
}
|
||||
}
|
||||
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[i]);
|
||||
for (int j = i; j <= sentence.size(); ++j)
|
||||
{
|
||||
if (j == sentence.size() || sentence[i] != sentence[j])
|
||||
{
|
||||
i += (j - i) % repeatNumber == 0 ? repeatNumber : 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sentence = newSentence;
|
||||
return true;
|
||||
}
|
||||
|
||||
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 });
|
||||
ProcessSentence(one, { nonConsole });
|
||||
ProcessSentence(normal, { nonConsole });
|
||||
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
|
||||
}
|
||||
);
|
97
extensions/removerepeatphrase.cpp
Normal file
97
extensions/removerepeatphrase.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
#include "extension.h"
|
||||
|
||||
std::vector<int> GenerateSuffixArray(const std::wstring& text)
|
||||
{
|
||||
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> 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> prevEqClasses = eqClasses;
|
||||
eqClasses[suffixArray[0]] = 0;
|
||||
for (int i = 1; i < text.size(); ++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)
|
||||
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[eqClasses[precedingSuffix]]++] = precedingSuffix;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i + 1 < text.size(); ++i)
|
||||
assert(wcscmp(text.c_str() + suffixArray[i], text.c_str() + suffixArray[i + 1]) > 0);
|
||||
return suffixArray;
|
||||
}
|
||||
|
||||
constexpr wchar_t ERASED = 0xf246; // inside Unicode private use area
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
// This algorithm looks for repeating substrings (in other words, common prefixes among the set of suffixes) of the sentence with length > 6
|
||||
// It then looks for any regions of characters at least twice as long as the substring made up only of characters in the substring, and erases them
|
||||
// If this results in the substring being completely erased from the string, the substring is copied to the last location where it was located in the original string
|
||||
auto timeout = GetTickCount64() + 30'000; // give up if taking over 30 seconds
|
||||
std::vector<int> suffixArray = GenerateSuffixArray(sentence);
|
||||
for (int i = 0; i + 1 < sentence.size() && GetTickCount64() < timeout; ++i)
|
||||
{
|
||||
int commonPrefixLength = 0;
|
||||
for (int j = suffixArray[i], k = suffixArray[i + 1]; j < sentence.size() && k < sentence.size(); ++j, ++k)
|
||||
if (sentence[j] != ERASED && sentence[j] == sentence[k]) commonPrefixLength += 1;
|
||||
else break;
|
||||
|
||||
if (commonPrefixLength > 6)
|
||||
{
|
||||
std::wstring substring(sentence, suffixArray[i], commonPrefixLength);
|
||||
bool substringCharMap[0x10000] = {};
|
||||
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;
|
||||
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]));
|
||||
}
|
||||
}
|
||||
sentence.erase(std::remove(sentence.begin(), sentence.end(), ERASED), sentence.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(
|
||||
{
|
||||
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
|
||||
|
||||
std::wstring cyclicRepeats = L"Name: '_abcdefg_abcdefg_abcdefg_abcdefg_abcdefg'";
|
||||
std::wstring buildupRepeats = L"Name: '__a_ab_abc_abcd_abcde_abcdef_abcdefg'";
|
||||
std::wstring breakdownRepeats = L"Name: '_abcdefg_abcdef_abcde_abcd_abc_ab_a_'";
|
||||
ProcessSentence(cyclicRepeats, { nonConsole });
|
||||
ProcessSentence(buildupRepeats, { nonConsole });
|
||||
ProcessSentence(breakdownRepeats, { nonConsole });
|
||||
assert(cyclicRepeats == L"Name: '_abcdefg'");
|
||||
assert(buildupRepeats == L"Name: '_abcdefg'");
|
||||
assert(breakdownRepeats == L"Name: '_abcdefg'");
|
||||
|
||||
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
|
||||
ProcessSentence(empty, { nonConsole });
|
||||
ProcessSentence(one, { nonConsole });
|
||||
ProcessSentence(normal, { nonConsole });
|
||||
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
|
||||
}
|
||||
);
|
52
extensions/removerepeatphrase2.cpp
Normal file
52
extensions/removerepeatphrase2.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#include "extension.h"
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
// This algorithm looks at all the prefixes of the sentence: if a prefix is found later in the sentence, it is removed from the beginning and the process is repeated
|
||||
auto timeout = GetTickCount64() + 30'000; // give up if taking over 30 seconds
|
||||
auto data = std::make_unique<wchar_t[]>(sentence.size() + 1);
|
||||
wcscpy_s(data.get(), sentence.size() + 1, sentence.c_str());
|
||||
wchar_t* dataEnd = data.get() + sentence.size();
|
||||
int skip = 0, count = 0;
|
||||
for (wchar_t* end = dataEnd; end - data.get() > skip && GetTickCount64() < timeout; --end)
|
||||
{
|
||||
std::swap(*end, *dataEnd);
|
||||
int junkLength = end - data.get() - skip;
|
||||
auto junkFound = wcsstr(sentence.c_str() + skip + junkLength, data.get() + skip);
|
||||
std::swap(*end, *dataEnd);
|
||||
if (junkFound)
|
||||
{
|
||||
if (count && junkLength < min(skip / count, 4)) break;
|
||||
skip += junkLength;
|
||||
count += 1;
|
||||
end = dataEnd;
|
||||
}
|
||||
}
|
||||
if (count && skip / count >= 3)
|
||||
{
|
||||
sentence = data.get() + skip;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
TEST(
|
||||
{
|
||||
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
|
||||
|
||||
std::wstring cyclicRepeats = L"_abcde_abcdef_abcdefg_abcdefg_abcdefg_abcdefg_abcdefg";
|
||||
std::wstring buildupRepeats = L"__a_ab_abc_abcd_abcde_abcdef_abcdefg";
|
||||
ProcessSentence(cyclicRepeats, { nonConsole });
|
||||
ProcessSentence(buildupRepeats, { nonConsole });
|
||||
assert(cyclicRepeats == L"_abcdefg");
|
||||
assert(buildupRepeats == L"_abcdefg");
|
||||
|
||||
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
|
||||
ProcessSentence(empty, { nonConsole });
|
||||
ProcessSentence(one, { nonConsole });
|
||||
ProcessSentence(normal, { nonConsole });
|
||||
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
|
||||
}
|
||||
);
|
44
extensions/removerepeatsentence.cpp
Normal file
44
extensions/removerepeatsentence.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
#include "extension.h"
|
||||
|
||||
int sentenceCacheSize = 30;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
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.xdll", &sentenceCacheSize);
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
uint64_t textNumber = sentenceInfo["text number"];
|
||||
if (textNumber == 0) return false;
|
||||
|
||||
static std::deque<Synchronized<std::vector<std::wstring>>> cache;
|
||||
static std::mutex m;
|
||||
m.lock();
|
||||
if (textNumber + 1 > cache.size()) cache.resize(textNumber + 1);
|
||||
auto prevSentences = cache[textNumber].Acquire();
|
||||
m.unlock();
|
||||
auto& inserted = prevSentences->emplace_back(sentence);
|
||||
auto firstLocation = std::find(prevSentences->begin(), prevSentences->end(), sentence);
|
||||
if (&*firstLocation != &inserted)
|
||||
{
|
||||
prevSentences->erase(firstLocation);
|
||||
sentence.clear();
|
||||
}
|
||||
if (prevSentences->size() > sentenceCacheSize) prevSentences->erase(prevSentences->begin());
|
||||
return sentence.empty();
|
||||
}
|
142
extensions/replacer.cpp
Normal file
142
extensions/replacer.cpp
Normal file
@ -0,0 +1,142 @@
|
||||
#include "extension.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <cwctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <process.h>
|
||||
|
||||
extern const wchar_t* REPLACER_INSTRUCTIONS;
|
||||
|
||||
constexpr auto REPLACE_SAVE_FILE = u8"SavedReplacements.txt";
|
||||
|
||||
std::atomic<std::filesystem::file_time_type> replaceFileLastWrite = {};
|
||||
concurrency::reader_writer_lock m;
|
||||
|
||||
class Trie
|
||||
{
|
||||
public:
|
||||
Trie(const std::istream& replacementScript)
|
||||
{
|
||||
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 = charStorage.insert(charStorage.end(), replacement.c_str(), replacement.c_str() + replacement.size() + 1) - charStorage.begin();
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring Replace(const std::wstring& sentence) const
|
||||
{
|
||||
std::wstring result;
|
||||
for (int i = 0; i < sentence.size();)
|
||||
{
|
||||
std::wstring_view replacement(sentence.c_str() + i, 1);
|
||||
int originalLength = 1;
|
||||
|
||||
const Node* current = &root;
|
||||
for (int j = i; current && j <= sentence.size(); ++j)
|
||||
{
|
||||
if (current->value >= 0)
|
||||
{
|
||||
replacement = charStorage.data() + current->value;
|
||||
originalLength = j - i;
|
||||
}
|
||||
if (!Ignore(sentence[j])) current = Next(current, sentence[j]) ? Next(current, sentence[j]) : Next(current, L'^');
|
||||
}
|
||||
|
||||
result += replacement;
|
||||
i += originalLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Empty()
|
||||
{
|
||||
return root.charMap.empty();
|
||||
}
|
||||
|
||||
private:
|
||||
static bool Ignore(wchar_t ch)
|
||||
{
|
||||
return ch <= 0x20 || iswspace(ch);
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
static Node* Next(Node* node, wchar_t ch)
|
||||
{
|
||||
auto it = std::lower_bound(node->charMap.begin(), node->charMap.end(), ch, [](const auto& one, auto two) { return one.first < two; });
|
||||
if (it != node->charMap.end() && it->first == ch) return it->second.get();
|
||||
if constexpr (!std::is_const_v<Node>) return node->charMap.insert(it, { ch, std::make_unique<Node>() })->second.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct Node
|
||||
{
|
||||
std::vector<std::pair<wchar_t, std::unique_ptr<Node>>> charMap;
|
||||
ptrdiff_t value = -1;
|
||||
} root;
|
||||
|
||||
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::scoped_lock lock(m);
|
||||
trie = Trie(std::ifstream(REPLACE_SAVE_FILE, std::ios::binary));
|
||||
}
|
||||
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 (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));
|
||||
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)
|
||||
{
|
||||
UpdateReplacements();
|
||||
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
sentence = trie.Replace(sentence);
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(
|
||||
{
|
||||
std::wstring replacementScript = LR"(
|
||||
|ORIG|さよなら|BECOMES|goodbye |END|Ignore this text
|
||||
And this text ツ
|
||||
|ORIG|バカ|BECOMES|idiot|END|
|
||||
|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);
|
||||
assert(replaced == L"Don't replace thisgoodbye idiot hello");
|
||||
}
|
||||
);
|
54
extensions/styler.cpp
Normal file
54
extensions/styler.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
extern const char* LOAD_SCRIPT;
|
||||
|
||||
constexpr auto STYLE_SAVE_FILE = u8"Textractor.qss";
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript);
|
||||
|
||||
if (scriptEditor.toPlainText().isEmpty())
|
||||
scriptEditor.setPlainText("/*\nhttps://www.google.com/search?q=Qt+stylesheet+gallery\nhttps://doc.qt.io/qt-5/stylesheet-syntax.html\n*/");
|
||||
layout.addWidget(&scriptEditor);
|
||||
layout.addWidget(&loadButton);
|
||||
|
||||
resize(800, 600);
|
||||
setWindowTitle("Styler");
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
|
||||
LoadScript();
|
||||
}
|
||||
|
||||
~Window()
|
||||
{
|
||||
qApp->setStyleSheet("");
|
||||
Save();
|
||||
}
|
||||
|
||||
private:
|
||||
void LoadScript()
|
||||
{
|
||||
qApp->setStyleSheet(scriptEditor.toPlainText());
|
||||
Save();
|
||||
}
|
||||
|
||||
void Save()
|
||||
{
|
||||
QTextFile(STYLE_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate).write(scriptEditor.toPlainText().toUtf8());
|
||||
}
|
||||
|
||||
QHBoxLayout layout{ this };
|
||||
QPlainTextEdit scriptEditor{ QTextFile(STYLE_SAVE_FILE, QIODevice::ReadOnly).readAll(), this };
|
||||
QPushButton loadButton{ LOAD_SCRIPT, this };
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
return false;
|
||||
}
|
79
extensions/threadlinker.cpp
Normal file
79
extensions/threadlinker.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
#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_set<int64_t>> links;
|
||||
std::unordered_set<int64_t> universalLinks, empty;
|
||||
bool separateSentences = false; // allow user to change?
|
||||
concurrency::reader_writer_lock m;
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
ui.linkButton->setText(LINK);
|
||||
ui.unlinkButton->setText(UNLINK);
|
||||
connect(ui.linkButton, &QPushButton::clicked, this, &Window::Link);
|
||||
connect(ui.unlinkButton, &QPushButton::clicked, this, &Window::Unlink);
|
||||
|
||||
setWindowTitle(THREAD_LINKER);
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
private:
|
||||
void Link()
|
||||
{
|
||||
bool 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))
|
||||
{
|
||||
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) Unlink();
|
||||
}
|
||||
|
||||
Ui::LinkWindow ui;
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
auto action = separateSentences ? sentenceInfo["add sentence"] : sentenceInfo["add text"];
|
||||
auto it = links.find(sentenceInfo["text number"]);
|
||||
for (const auto& linkSet : { it != links.end() ? it->second : empty, sentenceInfo["text number"] > 1 ? universalLinks : empty })
|
||||
for (auto link : linkSet)
|
||||
((void(*)(int64_t, const wchar_t*))action)(link, sentence.c_str());
|
||||
return false;
|
||||
}
|
47
extensions/threadlinker.ui
Normal file
47
extensions/threadlinker.ui
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>LinkWindow</class>
|
||||
<widget class="QDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="linkList"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="linkButton">
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="unlinkButton">
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
212
extensions/translatewrapper.cpp
Normal file
212
extensions/translatewrapper.cpp
Normal file
@ -0,0 +1,212 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <concurrent_priority_queue.h>
|
||||
#include <fstream>
|
||||
#include <QComboBox>
|
||||
|
||||
extern const char* NATIVE_LANGUAGE;
|
||||
extern const char* TRANSLATE_TO;
|
||||
extern const char* TRANSLATE_FROM;
|
||||
extern const char* TRANSLATE_SELECTED_THREAD_ONLY;
|
||||
extern const char* RATE_LIMIT_ALL_THREADS;
|
||||
extern const char* RATE_LIMIT_SELECTED_THREAD;
|
||||
extern const char* USE_TRANS_CACHE;
|
||||
extern const char* FILTER_GARBAGE;
|
||||
extern const char* MAX_TRANSLATIONS_IN_TIMESPAN;
|
||||
extern const char* TIMESPAN;
|
||||
extern const char* MAX_SENTENCE_SIZE;
|
||||
extern const char* API_KEY;
|
||||
extern const wchar_t* SENTENCE_TOO_LARGE_TO_TRANS;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
extern const wchar_t* TOO_MANY_TRANS_REQUESTS;
|
||||
|
||||
extern const char* TRANSLATION_PROVIDER;
|
||||
extern const char* GET_API_KEY_FROM;
|
||||
extern const QStringList languagesTo, languagesFrom;
|
||||
extern bool translateSelectedOnly, useRateLimiter, rateLimitSelected, useCache, useFilter;
|
||||
extern int tokenCount, rateLimitTimespan, maxSentenceSize;
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp);
|
||||
|
||||
QFormLayout* display;
|
||||
Settings settings;
|
||||
|
||||
namespace
|
||||
{
|
||||
Synchronized<TranslationParam> tlp;
|
||||
Synchronized<std::unordered_map<std::wstring, std::wstring>> translationCache;
|
||||
|
||||
std::string CacheFile()
|
||||
{
|
||||
return FormatString("%s Cache (%S).txt", TRANSLATION_PROVIDER, tlp->translateTo);
|
||||
}
|
||||
void SaveCache()
|
||||
{
|
||||
std::wstring allTranslations(L"\xfeff");
|
||||
for (const auto& [sentence, translation] : translationCache.Acquire().contents)
|
||||
allTranslations.append(L"|SENTENCE|").append(sentence).append(L"|TRANSLATION|").append(translation).append(L"|END|\r\n");
|
||||
std::ofstream(CacheFile(), std::ios::binary | std::ios::trunc).write((const char*)allTranslations.c_str(), allTranslations.size() * sizeof(wchar_t));
|
||||
}
|
||||
void LoadCache()
|
||||
{
|
||||
translationCache->clear();
|
||||
std::ifstream stream(CacheFile(), std::ios::binary);
|
||||
BlockMarkupIterator savedTranslations(stream, Array<std::wstring_view>{ L"|SENTENCE|", L"|TRANSLATION|" });
|
||||
auto translationCache = ::translationCache.Acquire();
|
||||
while (auto read = savedTranslations.Next())
|
||||
{
|
||||
auto& [sentence, translation] = read.value();
|
||||
translationCache->try_emplace(std::move(sentence), std::move(translation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
display = new QFormLayout(this);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
~Window()
|
||||
{
|
||||
SaveCache();
|
||||
}
|
||||
|
||||
private:
|
||||
void SaveTranslateTo(QString language)
|
||||
{
|
||||
SaveCache();
|
||||
settings.setValue(TRANSLATE_TO, S(tlp->translateTo = S(language)));
|
||||
LoadCache();
|
||||
}
|
||||
void SaveTranslateFrom(QString language)
|
||||
{
|
||||
settings.setValue(TRANSLATE_FROM, S(tlp->translateFrom = S(language)));
|
||||
}
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
static class
|
||||
{
|
||||
public:
|
||||
bool Request()
|
||||
{
|
||||
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:
|
||||
concurrency::concurrent_priority_queue<DWORD64, std::greater<DWORD64>> tokens;
|
||||
} rateLimiter;
|
||||
|
||||
bool cache = false;
|
||||
std::wstring translation;
|
||||
if (useFilter)
|
||||
{
|
||||
Trim(sentence);
|
||||
sentence.erase(std::remove_if(sentence.begin(), sentence.end(), [](wchar_t ch) { return ch < ' ' && ch != '\n'; }), sentence.end());
|
||||
}
|
||||
if (sentence.empty()) return true;
|
||||
if (sentence.size() > maxSentenceSize) translation = SENTENCE_TOO_LARGE_TO_TRANS;
|
||||
if (useCache)
|
||||
{
|
||||
auto translationCache = ::translationCache.Acquire();
|
||||
if (auto it = translationCache->find(sentence); it != translationCache->end()) translation = it->second;
|
||||
}
|
||||
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;
|
||||
|
||||
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"こんにちは", { L"English", L"?", L"" }).second.find(L"ello") == 1 || strstr(TRANSLATION_PROVIDER, "DevTools"));
|
||||
|
||||
for (auto languages : { languagesFrom, languagesTo }) for (auto language : languages)
|
||||
assert(codes.count(S(language)));
|
||||
assert(codes.count(L"?"));
|
||||
}
|
||||
);
|
6
extensions/translatewrapper.h
Normal file
6
extensions/translatewrapper.h
Normal file
@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
struct TranslationParam
|
||||
{
|
||||
std::wstring translateTo, translateFrom, authKey;
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "CustomFilter.h"
|
||||
|
||||
void CustomFilter::Insert(WORD number)
|
||||
{
|
||||
set.insert(number);
|
||||
}
|
||||
|
||||
void CustomFilter::Erase(WORD number)
|
||||
{
|
||||
set.erase(number);
|
||||
}
|
||||
|
||||
bool CustomFilter::Find(WORD number) const
|
||||
{
|
||||
return set.find(number) != set.end();
|
||||
}
|
||||
|
||||
void CustomFilter::Clear()
|
||||
{
|
||||
set.clear();
|
||||
}
|
||||
|
||||
void CustomFilter::Traverse(CustomFilterCallBack callback, PVOID param)
|
||||
{
|
||||
for (auto ch = set.begin(); ch != set.end(); ++ch)
|
||||
callback(*ch, param);
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ITH.h"
|
||||
|
||||
typedef void (*CustomFilterCallBack) (WORD, PVOID);
|
||||
|
||||
class CustomFilter
|
||||
{
|
||||
public:
|
||||
bool Find(WORD number) const;
|
||||
void Insert(WORD number);
|
||||
void Erase(WORD number);
|
||||
void Clear();
|
||||
void Traverse(CustomFilterCallBack callback, PVOID param);
|
||||
private:
|
||||
std::set<WORD> set;
|
||||
};
|
37
gui/ITH.h
37
gui/ITH.h
@ -1,37 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <ios>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <assert.h>
|
||||
#include <WindowsX.h>
|
||||
#include <algorithm>
|
||||
#include <Psapi.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <CommCtrl.h>
|
||||
#include <intrin.h>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include <set>
|
||||
#include "profile/pugixml.hpp"
|
||||
#pragma warning(disable: 4146)
|
@ -1,82 +0,0 @@
|
||||
// Generated by ResEdit 1.6.5
|
||||
// Copyright (C) 2006-2015
|
||||
// http://www.resedit.net
|
||||
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
#include <richedit.h>
|
||||
#include "resource.h"
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Dialog resources
|
||||
//
|
||||
LANGUAGE LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN
|
||||
IDD_DIALOG2 DIALOGEX 100, 100, 341, 210
|
||||
STYLE DS_MODALFRAME | DS_SHELLFONT | WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
|
||||
CAPTION "Process Explorer"
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 1
|
||||
{
|
||||
DEFPUSHBUTTON "OK", IDOK, 281, 189, 53, 14, 0, WS_EX_LEFT
|
||||
PUSHBUTTON "Remove Profile", IDC_BUTTON6, 226, 189, 53, 14, 0, WS_EX_LEFT
|
||||
CONTROL "", IDC_LIST1, WC_LISTVIEW, WS_TABSTOP | WS_BORDER | LVS_ALIGNLEFT | LVS_SHOWSELALWAYS | LVS_SINGLESEL | LVS_REPORT, 7, 20, 327, 164, WS_EX_LEFT
|
||||
LTEXT "Process", IDC_STATIC, 7, 7, 65, 13, SS_LEFT | SS_CENTERIMAGE, WS_EX_LEFT
|
||||
PUSHBUTTON "Attach", IDC_BUTTON2, 61, 189, 53, 14, 0, WS_EX_LEFT
|
||||
PUSHBUTTON "Detach", IDC_BUTTON3, 116, 189, 53, 14, 0, WS_EX_LEFT
|
||||
PUSHBUTTON "Add Profile", IDC_BUTTON5, 171, 189, 53, 14, 0, WS_EX_LEFT
|
||||
PUSHBUTTON "Refresh", IDC_BUTTON1, 7, 189, 53, 14, 0, WS_EX_LEFT
|
||||
}
|
||||
|
||||
|
||||
|
||||
LANGUAGE LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN
|
||||
IDD_DIALOG4 DIALOGEX 150, 100, 123, 185
|
||||
STYLE DS_MODALFRAME | DS_SHELLFONT | WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
|
||||
CAPTION "Option"
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 1
|
||||
{
|
||||
DEFPUSHBUTTON "OK", IDOK, 8, 164, 50, 14, 0, WS_EX_LEFT
|
||||
PUSHBUTTON "Cancel", IDCANCEL, 65, 164, 50, 14, 0, WS_EX_LEFT
|
||||
EDITTEXT IDC_EDIT1, 60, 7, 55, 14, ES_AUTOHSCROLL, WS_EX_LEFT
|
||||
LTEXT "Split time", IDC_STATIC, 7, 7, 47, 13, SS_LEFT | SS_CENTERIMAGE, WS_EX_LEFT
|
||||
EDITTEXT IDC_EDIT2, 60, 25, 55, 14, ES_AUTOHSCROLL, WS_EX_LEFT
|
||||
LTEXT "Process delay", IDC_STATIC, 7, 26, 47, 13, SS_LEFT | SS_CENTERIMAGE, WS_EX_LEFT
|
||||
EDITTEXT IDC_EDIT3, 60, 45, 55, 14, ES_AUTOHSCROLL, WS_EX_LEFT
|
||||
LTEXT "Inject delay", IDC_STATIC, 7, 45, 47, 13, SS_LEFT | SS_CENTERIMAGE, WS_EX_LEFT
|
||||
EDITTEXT IDC_EDIT4, 60, 65, 55, 14, ES_AUTOHSCROLL, WS_EX_LEFT
|
||||
LTEXT "Insert delay", IDC_STATIC, 7, 65, 47, 13, SS_LEFT | SS_CENTERIMAGE, WS_EX_LEFT
|
||||
AUTOCHECKBOX "Auto attach", IDC_CHECK1, 7, 87, 54, 10, 0, WS_EX_LEFT
|
||||
AUTOCHECKBOX "Auto insert", IDC_CHECK2, 62, 87, 50, 10, 0, WS_EX_LEFT
|
||||
AUTOCHECKBOX "Auto copy to clipboard", IDC_CHECK3, 7, 103, 88, 10, 0, WS_EX_LEFT
|
||||
AUTOCHECKBOX "Auto suppress repetition", IDC_CHECK4, 7, 119, 95, 10, 0, WS_EX_LEFT
|
||||
AUTOCHECKBOX "Reset character filter", IDC_CHECK6, 7, 149, 81, 8, 0, WS_EX_LEFT
|
||||
AUTOCHECKBOX "Enable global filter", IDC_CHECK5, 7, 134, 75, 10, 0, WS_EX_LEFT
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Icon resources
|
||||
//
|
||||
LANGUAGE LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN
|
||||
IDI_ICON1 ICON "icon1.ico"
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Version Information resources
|
||||
//
|
||||
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,0,0,0
|
||||
PRODUCTVERSION 0,0,0,0
|
||||
FILEOS VOS_UNKNOWN
|
||||
FILETYPE VFT_UNKNOWN
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
FILEFLAGSMASK 0
|
||||
FILEFLAGS 0
|
||||
{
|
||||
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
#include "ProcessWindow.h"
|
||||
#include "resource.h"
|
||||
#include "host/host.h"
|
||||
#include "host/hookman.h"
|
||||
#include "ProfileManager.h"
|
||||
#include "profile/Profile.h"
|
||||
|
||||
extern HookManager* man; // main.cpp
|
||||
extern ProfileManager* pfman; // ProfileManager.cpp
|
||||
|
||||
ProcessWindow::ProcessWindow(HWND hDialog) : hDlg(hDialog)
|
||||
{
|
||||
hbRefresh = GetDlgItem(hDlg, IDC_BUTTON1);
|
||||
hbAttach = GetDlgItem(hDlg, IDC_BUTTON2);
|
||||
hbDetach = GetDlgItem(hDlg, IDC_BUTTON3);
|
||||
hbAddProfile = GetDlgItem(hDlg, IDC_BUTTON5);
|
||||
hbRemoveProfile = GetDlgItem(hDlg, IDC_BUTTON6);
|
||||
EnableWindow(hbAddProfile, FALSE);
|
||||
EnableWindow(hbRemoveProfile, FALSE);
|
||||
hlProcess = GetDlgItem(hDlg, IDC_LIST1);
|
||||
heOutput = GetDlgItem(hDlg, IDC_EDIT1);
|
||||
ListView_SetExtendedListViewStyleEx(hlProcess, LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT);
|
||||
InitProcessDlg();
|
||||
RefreshProcess();
|
||||
EnableWindow(hbDetach, FALSE);
|
||||
EnableWindow(hbAttach, FALSE);
|
||||
}
|
||||
|
||||
void ProcessWindow::InitProcessDlg()
|
||||
{
|
||||
LVCOLUMN lvc = {};
|
||||
lvc.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
|
||||
lvc.fmt = LVCFMT_RIGHT; // left-aligned column
|
||||
lvc.cx = 40;
|
||||
lvc.pszText = L"PID";
|
||||
ListView_InsertColumn(hlProcess, 0, &lvc);
|
||||
lvc.cx = 100;
|
||||
lvc.fmt = LVCFMT_LEFT; // left-aligned column
|
||||
lvc.pszText = L"Name";
|
||||
ListView_InsertColumn(hlProcess, 1, &lvc);
|
||||
}
|
||||
|
||||
void ProcessWindow::RefreshProcess()
|
||||
{
|
||||
ListView_DeleteAllItems(hlProcess);
|
||||
LVITEM item = {};
|
||||
item.mask = LVIF_TEXT | LVIF_PARAM | LVIF_STATE;
|
||||
DWORD idProcess[1024], cbNeeded;
|
||||
WCHAR path[MAX_PATH];
|
||||
|
||||
if (EnumProcesses(idProcess, sizeof(idProcess), &cbNeeded))
|
||||
{
|
||||
DWORD len = cbNeeded / sizeof(DWORD);
|
||||
for (DWORD i = 0; i < len; ++i)
|
||||
{
|
||||
DWORD pid = idProcess[i];
|
||||
UniqueHandle hProcess(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid));
|
||||
if (hProcess)
|
||||
{
|
||||
if (GetProcessImageFileName(hProcess.get(), path, MAX_PATH))
|
||||
{
|
||||
WCHAR buffer[256];
|
||||
std::swprintf(buffer, L"%d", pid);
|
||||
PWCHAR name = wcsrchr(path, L'\\') + 1;
|
||||
item.pszText = buffer;
|
||||
item.lParam = pid;
|
||||
ListView_InsertItem(hlProcess, &item);
|
||||
ListView_SetItemText(hlProcess, item.iItem, 1, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessWindow::AttachProcess()
|
||||
{
|
||||
DWORD pid = GetSelectedPID();
|
||||
if (Host_InjectByPID(pid))
|
||||
{
|
||||
Host_HijackProcess(pid);
|
||||
RefreshThreadWithPID(pid, true);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessWindow::DetachProcess()
|
||||
{
|
||||
DWORD pid = GetSelectedPID();
|
||||
if (Host_ActiveDetachProcess(pid) == 0)
|
||||
RefreshThreadWithPID(pid, false);
|
||||
}
|
||||
|
||||
void ProcessWindow::CreateProfileForSelectedProcess()
|
||||
{
|
||||
DWORD pid = GetSelectedPID();
|
||||
auto path = GetProcessPath(pid);
|
||||
if (!path.empty())
|
||||
{
|
||||
Profile* pf = pfman->CreateProfile(pid);
|
||||
pfman->SaveProfiles();
|
||||
RefreshThread(ListView_GetSelectionMark(hlProcess));
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessWindow::DeleteProfileForSelectedProcess()
|
||||
{
|
||||
DWORD pid = GetSelectedPID();
|
||||
auto path = GetProcessPath(pid);
|
||||
if (!path.empty())
|
||||
{
|
||||
pfman->DeleteProfile(path);
|
||||
RefreshThread(ListView_GetSelectionMark(hlProcess));
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessWindow::RefreshThread(int index)
|
||||
{
|
||||
LVITEM item = {};
|
||||
item.mask = LVIF_PARAM;
|
||||
item.iItem = index;
|
||||
ListView_GetItem(hlProcess, &item);
|
||||
DWORD pid = item.lParam;
|
||||
bool isAttached = man->GetProcessRecord(pid) != NULL;
|
||||
RefreshThreadWithPID(pid, isAttached);
|
||||
}
|
||||
|
||||
void ProcessWindow::RefreshThreadWithPID(DWORD pid, bool isAttached)
|
||||
{
|
||||
EnableWindow(hbDetach, isAttached);
|
||||
EnableWindow(hbAttach, !isAttached);
|
||||
auto path = GetProcessPath(pid);
|
||||
bool hasProfile = !path.empty() && pfman->HasProfile(path);
|
||||
EnableWindow(hbAddProfile, isAttached && !hasProfile);
|
||||
EnableWindow(hbRemoveProfile, hasProfile);
|
||||
if (pid == GetCurrentProcessId())
|
||||
EnableWindow(hbAttach, FALSE);
|
||||
}
|
||||
|
||||
DWORD ProcessWindow::GetSelectedPID()
|
||||
{
|
||||
LVITEM item = {};
|
||||
item.mask = LVIF_PARAM;
|
||||
item.iItem = ListView_GetSelectionMark(hlProcess);
|
||||
ListView_GetItem(hlProcess, &item);
|
||||
return item.lParam;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ITH.h"
|
||||
|
||||
class ProcessWindow
|
||||
{
|
||||
public:
|
||||
ProcessWindow(HWND hDialog);
|
||||
void InitProcessDlg();
|
||||
void RefreshProcess();
|
||||
void AttachProcess();
|
||||
void DetachProcess();
|
||||
void CreateProfileForSelectedProcess();
|
||||
void DeleteProfileForSelectedProcess();
|
||||
void RefreshThread(int index);
|
||||
private:
|
||||
void RefreshThreadWithPID(DWORD pid, bool isAttached);
|
||||
DWORD GetSelectedPID();
|
||||
HWND hDlg;
|
||||
HWND hlProcess;
|
||||
HWND hbRefresh,hbAttach,hbDetach,hbAddProfile,hbRemoveProfile;
|
||||
HWND heOutput;
|
||||
};
|
@ -1,218 +0,0 @@
|
||||
#include "ProfileManager.h"
|
||||
#include "profile/Profile.h"
|
||||
#include "host/host.h"
|
||||
#include "host/hookman.h"
|
||||
#include "vnrhook/include/types.h"
|
||||
#include "vnrhook/include/const.h"
|
||||
#include "utility.h"
|
||||
#include "profile/misc.h"
|
||||
|
||||
extern HookManager* man; // main.cpp
|
||||
extern LONG auto_inject, auto_insert, inject_delay; // main.cpp
|
||||
extern LONG insert_delay, process_time; // main.cpp
|
||||
bool MonitorFlag;
|
||||
ProfileManager* pfman;
|
||||
|
||||
DWORD WINAPI MonitorThread(LPVOID lpThreadParameter);
|
||||
|
||||
ProfileManager::ProfileManager() :
|
||||
hMonitorThread(IthCreateThread(MonitorThread, 0))
|
||||
{
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
ProfileManager::~ProfileManager()
|
||||
{
|
||||
SaveProfiles();
|
||||
WaitForSingleObject(hMonitorThread.get(), 0);
|
||||
}
|
||||
|
||||
Profile* ProfileManager::GetProfile(DWORD pid)
|
||||
{
|
||||
std::wstring path = GetProcessPath(pid);
|
||||
if (!path.empty())
|
||||
{
|
||||
auto node = profile_tree.find(path);
|
||||
if (node != profile_tree.end())
|
||||
return node->second.get();
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool ProfileManager::CreateProfile(pugi::xml_node game)
|
||||
{
|
||||
auto file = game.child(L"File");
|
||||
auto profile = game.child(L"Profile");
|
||||
if (!file || !profile)
|
||||
return false;
|
||||
auto path = file.attribute(L"Path");
|
||||
if (!path)
|
||||
return false;
|
||||
auto profile_title = game.attribute(L"Title");
|
||||
auto title = profile_title ? profile_title.value() : L"";
|
||||
auto pf = new Profile(title);
|
||||
if (!pf->XmlReadProfile(profile))
|
||||
return false;
|
||||
CSLock lock(cs);
|
||||
auto& oldProfile = profile_tree[path.value()];
|
||||
if (!oldProfile)
|
||||
oldProfile.swap(profile_ptr(pf));
|
||||
return true;
|
||||
}
|
||||
|
||||
Profile* ProfileManager::CreateProfile(DWORD pid)
|
||||
{
|
||||
CSLock lock(cs);
|
||||
auto path = GetProcessPath(pid);
|
||||
auto& pf = profile_tree[path];
|
||||
if (!pf)
|
||||
{
|
||||
std::wstring title = GetProcessTitle(pid);
|
||||
pf.reset(new Profile(title));
|
||||
}
|
||||
return pf.get();
|
||||
}
|
||||
|
||||
void ProfileManager::WriteProfileXml(const std::wstring& path, Profile& pf, pugi::xml_node root)
|
||||
{
|
||||
auto game = root.append_child(L"Game");
|
||||
auto file_node = game.append_child(L"File");
|
||||
file_node.append_attribute(L"Path") = path.c_str();
|
||||
auto profile_node = game.append_child(L"Profile");
|
||||
pf.XmlWriteProfile(profile_node);
|
||||
if (!pf.Title().empty())
|
||||
{
|
||||
if (!game.attribute(L"Title"))
|
||||
game.append_attribute(L"Title");
|
||||
game.attribute(L"Title") = pf.Title().c_str();
|
||||
}
|
||||
}
|
||||
|
||||
void ProfileManager::LoadProfiles()
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
UniqueHandle hFile(IthCreateFile(L"ITH_Profile.xml", GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING));
|
||||
if (hFile.get() == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
DWORD size = GetFileSize(hFile.get(), NULL);
|
||||
std::unique_ptr<char[]> buffer(new char[size]);
|
||||
ReadFile(hFile.get(), buffer.get(), size, &size, NULL);
|
||||
auto result = doc.load_buffer(buffer.get(), size);
|
||||
if (!result)
|
||||
return;
|
||||
auto root = doc.root().child(L"ITH_Profile");
|
||||
if (!root)
|
||||
return;
|
||||
for (auto game = root.begin(); game != root.end(); ++game)
|
||||
CreateProfile(*game);
|
||||
}
|
||||
|
||||
void ProfileManager::SaveProfiles()
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
auto root = doc.append_child(L"ITH_Profile");
|
||||
for (auto it = profile_tree.begin(); it != profile_tree.end(); ++it) {
|
||||
auto& path = it->first;
|
||||
auto& profile = it->second;
|
||||
WriteProfileXml(path, *profile, root);
|
||||
}
|
||||
UniqueHandle hFile(IthCreateFile(L"ITH_Profile.xml", GENERIC_WRITE, 0, CREATE_ALWAYS));
|
||||
if (hFile.get() != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
FileWriter fw(hFile.get());
|
||||
doc.save(fw);
|
||||
}
|
||||
}
|
||||
|
||||
void ProfileManager::DeleteProfile(const std::wstring& path)
|
||||
{
|
||||
CSLock lock(cs);
|
||||
profile_tree.erase(profile_tree.find(path));
|
||||
}
|
||||
|
||||
Profile* ProfileManager::GetProfile(const std::wstring& path)
|
||||
{
|
||||
if (path.empty())
|
||||
return nullptr;
|
||||
auto it = profile_tree.find(path);
|
||||
if (it == profile_tree.end())
|
||||
return nullptr;
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
bool ProfileManager::HasProfile(const std::wstring& path)
|
||||
{
|
||||
return profile_tree.find(path) != profile_tree.end();
|
||||
}
|
||||
|
||||
DWORD ProfileManager::CountProfiles()
|
||||
{
|
||||
return profile_tree.size();
|
||||
}
|
||||
|
||||
DWORD WINAPI InjectThread(LPVOID lpThreadParameter)
|
||||
{
|
||||
DWORD pid = (DWORD)lpThreadParameter;
|
||||
Sleep(inject_delay);
|
||||
if (man == NULL)
|
||||
return 0;
|
||||
DWORD status = Host_InjectByPID(pid);
|
||||
if (!auto_insert)
|
||||
return status;
|
||||
if (status == -1)
|
||||
return status;
|
||||
Sleep(insert_delay);
|
||||
const Profile* pf = pfman->GetProfile(pid);
|
||||
if (pf)
|
||||
{
|
||||
SendParam sp;
|
||||
sp.type = 0;
|
||||
for (auto hp = pf->Hooks().begin(); hp != pf->Hooks().end(); ++hp)
|
||||
{
|
||||
std::string name = toMultiByteString((*hp)->Name());
|
||||
Host_InsertHook(pid, const_cast<HookParam*>(&(*hp)->HP()), name.c_str());
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
DWORD WINAPI MonitorThread(LPVOID lpThreadParameter)
|
||||
{
|
||||
while (MonitorFlag)
|
||||
{
|
||||
DWORD aProcesses[1024], cbNeeded, cProcesses;
|
||||
if (!EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded))
|
||||
break;
|
||||
cProcesses = cbNeeded / sizeof(DWORD);
|
||||
for (size_t i = 0; i < cProcesses; ++i)
|
||||
{
|
||||
Sleep(process_time);
|
||||
if (!auto_inject || man == NULL || man->GetProcessRecord(aProcesses[i]))
|
||||
continue;
|
||||
std::wstring process_path = GetProcessPath(aProcesses[i]);
|
||||
if (!process_path.empty() && pfman->HasProfile(process_path))
|
||||
{
|
||||
UniqueHandle hThread(IthCreateThread(InjectThread, aProcesses[i]));
|
||||
WaitForSingleObject(hThread.get(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD SaveProcessProfile(DWORD pid)
|
||||
{
|
||||
std::wstring path = GetProcessPath(pid);
|
||||
if (path.empty())
|
||||
return 0;
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_node profile_node = doc.append_child(L"Profile");
|
||||
man->GetProfile(pid, profile_node);
|
||||
Profile* pf = pfman->GetProfile(pid);
|
||||
if (pf != NULL)
|
||||
pf->Clear();
|
||||
else
|
||||
pf = pfman->CreateProfile(pid);
|
||||
pf->XmlReadProfile(profile_node);
|
||||
return 0;
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ITH.h"
|
||||
#include "utility.h" // UniqueHandle, CriticalSection
|
||||
|
||||
class Profile;
|
||||
|
||||
class ProfileManager
|
||||
{
|
||||
public:
|
||||
ProfileManager();
|
||||
~ProfileManager();
|
||||
Profile* CreateProfile(DWORD pid);
|
||||
Profile* GetProfile(DWORD pid);
|
||||
Profile* GetProfile(const std::wstring& path);
|
||||
void LoadProfiles();
|
||||
void SaveProfiles();
|
||||
void DeleteProfile(const std::wstring& path);
|
||||
void UpdateHookAddresses(DWORD pid);
|
||||
bool HasProfile(const std::wstring& path);
|
||||
private:
|
||||
typedef std::unique_ptr<Profile> profile_ptr;
|
||||
typedef std::map<std::wstring, profile_ptr> profile_map;
|
||||
|
||||
ProfileManager(const ProfileManager&);
|
||||
ProfileManager operator=(const ProfileManager&);
|
||||
|
||||
DWORD CountProfiles();
|
||||
bool CreateProfile(pugi::xml_node game);
|
||||
void WriteProfileXml(const std::wstring& path, Profile& pf, pugi::xml_node doc);
|
||||
// locate profile with executable path
|
||||
profile_map profile_tree;
|
||||
CriticalSection cs;
|
||||
UniqueHandle hMonitorThread;
|
||||
};
|
@ -1,41 +0,0 @@
|
||||
#include "TextBuffer.h"
|
||||
|
||||
DWORD WINAPI FlushThread(LPVOID lParam); // window.cpp
|
||||
|
||||
TextBuffer::TextBuffer(HWND edit) : hThread(IthCreateThread(FlushThread, (DWORD)this)),
|
||||
hEdit(edit),
|
||||
running(true)
|
||||
{
|
||||
}
|
||||
|
||||
TextBuffer::~TextBuffer()
|
||||
{
|
||||
running = false;
|
||||
WaitForSingleObject(hThread.get(), 0);
|
||||
}
|
||||
|
||||
void TextBuffer::AddText(LPCWSTR str, int len, bool line)
|
||||
{
|
||||
CSLock lock(cs);
|
||||
if (len > 0)
|
||||
this->str.append(str, len);
|
||||
line_break = line;
|
||||
}
|
||||
|
||||
void TextBuffer::Flush()
|
||||
{
|
||||
CSLock lock(cs);
|
||||
if (line_break || str.empty())
|
||||
return;
|
||||
DWORD t = Edit_GetTextLength(hEdit);
|
||||
Edit_SetSel(hEdit, t, -1);
|
||||
Edit_ReplaceSel(hEdit, str.c_str());
|
||||
str.clear();
|
||||
}
|
||||
|
||||
void TextBuffer::ClearBuffer()
|
||||
{
|
||||
CSLock lock(cs);
|
||||
str.clear();
|
||||
line_break = false;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ITH.h"
|
||||
#include "utility.h" // UniqueHandle, CriticalSection
|
||||
|
||||
class TextBuffer
|
||||
{
|
||||
public:
|
||||
TextBuffer(HWND edit);
|
||||
~TextBuffer();
|
||||
void Flush();
|
||||
void AddText(LPCWSTR str, int len, bool line);
|
||||
void ClearBuffer();
|
||||
bool Running() { return running; }
|
||||
private:
|
||||
CriticalSection cs;
|
||||
bool line_break, running;
|
||||
UniqueHandle hThread;
|
||||
HWND hEdit;
|
||||
std::wstring str;
|
||||
};
|
@ -1,73 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ITH.h"
|
||||
#include "host/host.h"
|
||||
#include "vnrhook/include/const.h"
|
||||
#include "vnrhook/include/types.h"
|
||||
#include "language.h"
|
||||
#include "utility.h"
|
||||
#include "profile/misc.h"
|
||||
|
||||
extern HookManager* man;
|
||||
extern HWND hwndProcessComboBox;
|
||||
|
||||
DWORD ProcessCommand(const std::wstring& cmd, DWORD pid)
|
||||
{
|
||||
using std::wregex;
|
||||
using std::regex_match;
|
||||
std::match_results<std::wstring::const_iterator> m;
|
||||
|
||||
if (regex_match(cmd, m, wregex(L"/pn(.+)", wregex::icase)))
|
||||
{
|
||||
pid = Host_GetPIDByName(m[1].str().c_str());
|
||||
if (pid == 0)
|
||||
return 0;
|
||||
Host_InjectByPID(pid);
|
||||
}
|
||||
else if (regex_match(cmd, m, wregex(L"/p(\\d+)", wregex::icase)))
|
||||
{
|
||||
pid = std::stoul(m[1].str());
|
||||
Host_InjectByPID(pid);
|
||||
}
|
||||
else if (regex_match(cmd, m, wregex(L"/h(.+)", wregex::icase)))
|
||||
{
|
||||
HookParam hp = {};
|
||||
if (Parse(m[1].str(), hp))
|
||||
Host_InsertHook(pid, &hp);
|
||||
}
|
||||
else if (regex_match(cmd, m, wregex(L":l([[:xdigit:]]+)-([[:xdigit:]]+)", wregex::icase)))
|
||||
{
|
||||
DWORD from = std::stoul(m[1].str(), NULL, 16);
|
||||
DWORD to = std::stoul(m[2].str(), NULL, 16);
|
||||
Host_AddLink(from, to);
|
||||
}
|
||||
else if (regex_match(cmd, m, wregex(L":u([[:xdigit:]]+)", wregex::icase)))
|
||||
{
|
||||
DWORD from = std::stoul(m[1].str(), NULL, 16);
|
||||
Host_UnLink(from);
|
||||
}
|
||||
else if (regex_match(cmd, m, wregex(L":(?:h|help)", wregex::icase)))
|
||||
{
|
||||
ConsoleOutput(Usage);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConsoleOutput(L"Unknown command. Type :h or :help for help.");
|
||||
}
|
||||
return 0;
|
||||
}
|
BIN
gui/icon1.ico
BIN
gui/icon1.ico
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
132
gui/language.cpp
132
gui/language.cpp
@ -1,132 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const wchar_t* Warning=L"Warning!";
|
||||
//command.cpp
|
||||
const wchar_t* ErrorSyntax=L"Syntax error";
|
||||
const wchar_t* Usage = L"Syntax:\r\n\
|
||||
\r\n\
|
||||
:H[ELP] - print help\r\n\
|
||||
:Lfrom-to - link from thread 'from' to thread 'to'\r\n\
|
||||
:Ufrom - unlink link from thread 'from'\r\n\
|
||||
\r\n\
|
||||
'from' and 'to' and hexadecimal thread numbers. The thread number is the first number in the combo box.\r\n\
|
||||
\r\n\
|
||||
Loader options:\r\n\
|
||||
/P[{process_id|Nprocess_name}] - attach to process\r\n\
|
||||
\r\n\
|
||||
Hook options:\r\n\
|
||||
/H[X]{A|B|W|S|Q}[N][data_offset[*drdo]][:sub_offset[*drso]]@addr[:module[:{name|#ordinal}]]\r\n\
|
||||
\r\n\
|
||||
All numbers in /H (except ordinal) are hexadecimal without any prefixes";
|
||||
|
||||
const wchar_t* ExtendedUsage = L"/H[X]{A|B|W|S|Q}[N][data_offset[*drdo]][:sub_offset[*drso]]@addr[:[module[:{name|#ordinal}]]]\r\n\
|
||||
\r\n\
|
||||
Set additional custom hook\r\n\
|
||||
\r\n\
|
||||
Hook types :\r\n\
|
||||
A - DBCS char\r\n\
|
||||
B - DBCS char(big-endian)\r\n\
|
||||
W - UCS2 char\r\n\
|
||||
S - MBCS string\r\n\
|
||||
Q - UTF-16 string\r\n\
|
||||
\r\n\
|
||||
Parameters:\r\n\
|
||||
X - use hardware breakpoints\r\n\
|
||||
N - don't use contexts\r\n\
|
||||
data_offset - stack offset to char / string pointer\r\n\
|
||||
drdo - add a level of indirection to data_offset\r\n\
|
||||
sub_offset - stack offset to subcontext\r\n\
|
||||
drso - add a level of indirection to sub_offset\r\n\
|
||||
addr - address of the hook\r\n\
|
||||
module - name of the module to use as base for 'addr'\r\n\
|
||||
name - name of the 'module' export to use as base for 'addr'\r\n\
|
||||
ordinal - number of the 'module' export ordinal to use as base for 'addr'\r\n\
|
||||
\r\n\
|
||||
Negative values of 'data_offset' and 'sub_offset' refer to registers: \r\n\
|
||||
- 4 for EAX, -8 for ECX, -C for EDX, -10 for EBX, -14 for ESP, -18 for EBP, -1C for ESI, -20 for EDI\r\n\
|
||||
\r\n\
|
||||
\"Add a level of indirection\" means in C/C++ style: (*(ESP+data_offset)+drdo) instead of (ESP+data_offset)\r\n\
|
||||
\r\n\
|
||||
All numbers except ordinal are hexadecimal without any prefixes";
|
||||
|
||||
//inject.cpp
|
||||
const wchar_t* ErrorRemoteThread=L"Can't create remote thread.";
|
||||
const wchar_t* ErrorOpenProcess=L"Can't open process.";
|
||||
const wchar_t* ErrorNoProcess=L"Process not found";
|
||||
const wchar_t* SelfAttach=L"Please do not attach to ITH.exe";
|
||||
const wchar_t* AlreadyAttach=L"Process already attached.";
|
||||
const wchar_t* FormatInject=L"Inject process %d. Module base %.8X";
|
||||
//main.cpp
|
||||
const wchar_t* NotAdmin=L"Can't enable SeDebugPrevilege. ITH might malfunction.\r\n\
|
||||
Please run ITH as administrator or turn off UAC.";
|
||||
//pipe.cpp
|
||||
const wchar_t* ErrorCreatePipe=L"Can't create text pipe or too many instance.";
|
||||
const wchar_t* FormatDetach=L"Process %d detached.";
|
||||
const wchar_t* ErrorCmdQueueFull=L"Command queue full.";
|
||||
const wchar_t* ErrorNoAttach=L"No process attached.";
|
||||
|
||||
//profile.cpp
|
||||
const wchar_t* ErrorMonitor=L"Can't monitor process.";
|
||||
//utility.cpp
|
||||
const wchar_t* InitMessage=L"Copyright (C) 2010-2012 kaosu <qiupf2000@gmail.com>\r\n\
|
||||
Copyright (C) 2015 zorkzero <zorkzero@hotmail.com>\r\n\
|
||||
Source code <https://code.google.com/p/interactive-text-hooker/>\r\n\
|
||||
General discussion <https://groups.google.com/forum/?fromgroups#!forum/interactive-text-hooker>";
|
||||
const wchar_t* BackgroundMsg=L"Type \":h\" or \":help\" for help.";
|
||||
const wchar_t* ErrorLinkExist=L"Link exist.";
|
||||
const wchar_t* ErrorCylicLink=L"Link failed. No cyclic link allowed.";
|
||||
const wchar_t* FormatLink=L"Link from thread%.4x to thread%.4x.";
|
||||
const wchar_t* ErrorLink=L"Link failed. Source or/and destination thread not found.";
|
||||
const wchar_t* ErrorDeleteCombo=L"Error delete from combo.";
|
||||
|
||||
//window.cpp
|
||||
const wchar_t* ClassName=L"ITH";
|
||||
const wchar_t* ClassNameAdmin=L"ITH (Administrator)";
|
||||
const wchar_t* ErrorNotSplit=L"Need to enable split first!";
|
||||
const wchar_t* ErrorNotModule=L"Need to enable module first!";
|
||||
//Main window buttons
|
||||
const wchar_t* ButtonTitleProcess=L"Process";
|
||||
const wchar_t* ButtonTitleThread=L"Thread";
|
||||
const wchar_t* ButtonTitleHook=L"Hook";
|
||||
const wchar_t* ButtonTitleProfile=L"Profile";
|
||||
const wchar_t* ButtonTitleOption=L"Option";
|
||||
const wchar_t* ButtonTitleClear=L"Clear";
|
||||
const wchar_t* ButtonTitleSave=L"Save";
|
||||
const wchar_t* ButtonTitleTop=L"Top";
|
||||
//Hook window
|
||||
const wchar_t* SpecialHook=L"Special hook, no AGTH equivalent.";
|
||||
//Process window
|
||||
const wchar_t* TabTitlePID=L"PID";
|
||||
const wchar_t* TabTitleMemory=L"Memory";
|
||||
const wchar_t* TabTitleName=L"Name";
|
||||
const wchar_t* TabTitleTID=L"TID";
|
||||
const wchar_t* TabTitleStart=L"Start";
|
||||
const wchar_t* TabTitleModule=L"Module";
|
||||
const wchar_t* TabTitleState=L"State";
|
||||
const wchar_t* SuccessAttach=L"Attach ITH to process successfully.";
|
||||
const wchar_t* FailAttach=L"Failed to attach ITH to process.";
|
||||
const wchar_t* SuccessDetach=L"ITH detach from process.";
|
||||
const wchar_t* FailDetach=L"Detach failed.";
|
||||
//Profile window
|
||||
const wchar_t* ProfileExist=L"Profile already exists.";
|
||||
const wchar_t* SuccessAddProfile=L"Profile added.";
|
||||
const wchar_t* FailAddProfile=L"Fail to add profile";
|
||||
const wchar_t* TabTitleNumber=L"No.";
|
||||
const wchar_t* NoFile=L"Can't find file.";
|
||||
const wchar_t* PathDismatch=L"Process name dismatch, continue?";
|
||||
const wchar_t* SuccessImportProfile=L"Import profile success";
|
||||
//const wchar_t* SuccessAddProfile=L"Profile added.";
|
@ -1,86 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
extern const wchar_t* Warning;
|
||||
//command.cpp
|
||||
extern const wchar_t* ErrorSyntax;
|
||||
extern const wchar_t* Usage;
|
||||
extern const wchar_t* ExtendedUsage;
|
||||
//inject.cpp
|
||||
extern const wchar_t* ErrorRemoteThread;
|
||||
extern const wchar_t* ErrorOpenProcess;
|
||||
extern const wchar_t* ErrorNoProcess;
|
||||
extern const wchar_t* SelfAttach;
|
||||
extern const wchar_t* AlreadyAttach;
|
||||
extern const wchar_t* FormatInject;
|
||||
//main.cpp
|
||||
extern const wchar_t* NotAdmin;
|
||||
//pipe.cpp
|
||||
extern const wchar_t* ErrorCreatePipe;
|
||||
extern const wchar_t* FormatDetach;
|
||||
extern const wchar_t* ErrorCmdQueueFull;
|
||||
extern const wchar_t* ErrorNoAttach;
|
||||
|
||||
//profile.cpp
|
||||
extern const wchar_t* ErrorMonitor;
|
||||
|
||||
//utility.cpp
|
||||
extern const wchar_t* InitMessage;
|
||||
extern const wchar_t* BackgroundMsg;
|
||||
extern const wchar_t* ErrorLinkExist;
|
||||
extern const wchar_t* ErrorCylicLink;
|
||||
extern const wchar_t* FormatLink;
|
||||
extern const wchar_t* ErrorLink;
|
||||
extern const wchar_t* ErrorDeleteCombo;
|
||||
|
||||
//window.cpp
|
||||
extern const wchar_t* ClassName;
|
||||
extern const wchar_t* ClassNameAdmin;
|
||||
extern const wchar_t* ErrorNotSplit;
|
||||
extern const wchar_t* ErrorNotModule;
|
||||
//Main window buttons
|
||||
extern const wchar_t* ButtonTitleProcess;
|
||||
extern const wchar_t* ButtonTitleThread;
|
||||
extern const wchar_t* ButtonTitleHook;
|
||||
extern const wchar_t* ButtonTitleProfile;
|
||||
extern const wchar_t* ButtonTitleOption;
|
||||
extern const wchar_t* ButtonTitleClear;
|
||||
extern const wchar_t* ButtonTitleSave;
|
||||
extern const wchar_t* ButtonTitleTop;
|
||||
//Hook window
|
||||
extern const wchar_t* SpecialHook;
|
||||
//Process window
|
||||
extern const wchar_t* TabTitlePID;
|
||||
extern const wchar_t* TabTitleMemory;
|
||||
extern const wchar_t* TabTitleName;
|
||||
extern const wchar_t* TabTitleTID;
|
||||
extern const wchar_t* TabTitleStart;
|
||||
extern const wchar_t* TabTitleModule;
|
||||
extern const wchar_t* TabTitleState;
|
||||
extern const wchar_t* SuccessAttach;
|
||||
extern const wchar_t* FailAttach;
|
||||
extern const wchar_t* SuccessDetach;
|
||||
extern const wchar_t* FailDetach;
|
||||
//Profile window
|
||||
extern const wchar_t* ProfileExist;
|
||||
extern const wchar_t* SuccessAddProfile;
|
||||
extern const wchar_t* FailAddProfile;
|
||||
extern const wchar_t* TabTitleNumber;
|
||||
extern const wchar_t* NoFile;
|
||||
extern const wchar_t* PathDismatch;
|
||||
extern const wchar_t* SuccessImportProfile;
|
286
gui/main.cpp
286
gui/main.cpp
@ -1,286 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ITH.h"
|
||||
#include "host/host.h"
|
||||
#include "host/hookman.h"
|
||||
#include "host/settings.h"
|
||||
#include "CustomFilter.h"
|
||||
#include "profile/Profile.h"
|
||||
#include "ProfileManager.h"
|
||||
|
||||
HINSTANCE hIns;
|
||||
ATOM MyRegisterClass(HINSTANCE hInstance);
|
||||
BOOL InitInstance(HINSTANCE hInstance, DWORD nCmdShow, RECT *rc);
|
||||
RECT window;
|
||||
extern HWND hMainWnd; // windows.cpp
|
||||
extern bool MonitorFlag; // ProfileManager.cpp
|
||||
extern ProfileManager* pfman; // ProfileManager.cpp
|
||||
|
||||
extern "C" {
|
||||
BOOL IthInitSystemService();
|
||||
void IthCloseSystemService();
|
||||
}
|
||||
|
||||
CustomFilter* uni_filter;
|
||||
CustomFilter* mb_filter;
|
||||
HookManager* man;
|
||||
Settings* setman;
|
||||
LONG split_time, cyclic_remove, global_filter;
|
||||
LONG process_time, inject_delay, insert_delay,
|
||||
auto_inject, auto_insert, clipboard_flag;
|
||||
|
||||
std::map<std::wstring, long> setting;
|
||||
|
||||
void RecordMBChar(WORD mb, PVOID f)
|
||||
{
|
||||
auto filter = (pugi::xml_node*)f;
|
||||
DWORD m = mb;
|
||||
WCHAR buffer[16];
|
||||
std::swprintf(buffer, L"m%04X", m);
|
||||
filter->append_attribute(buffer) = L"0";
|
||||
}
|
||||
|
||||
void RecordUniChar(WORD uni, PVOID f)
|
||||
{
|
||||
auto filter = (pugi::xml_node*)f;
|
||||
DWORD m = uni;
|
||||
WCHAR buffer[16];
|
||||
std::swprintf(buffer, L"u%04X", m);
|
||||
filter->append_attribute(buffer) = L"0";
|
||||
std::wstring text = filter->text().get();
|
||||
text += (wchar_t)m;
|
||||
filter->text().set(text.c_str());
|
||||
}
|
||||
|
||||
void SaveSettings()
|
||||
{
|
||||
WINDOWPLACEMENT wndpl;
|
||||
wndpl.length = sizeof(WINDOWPLACEMENT);
|
||||
GetWindowPlacement(hMainWnd, &wndpl);
|
||||
setting[L"window_left"] = wndpl.rcNormalPosition.left;
|
||||
setting[L"window_right"] = wndpl.rcNormalPosition.right;
|
||||
setting[L"window_top"] = wndpl.rcNormalPosition.top;
|
||||
setting[L"window_bottom"] = wndpl.rcNormalPosition.bottom;
|
||||
setting[L"split_time"] = split_time;
|
||||
setting[L"process_time"] = process_time;
|
||||
setting[L"inject_delay"] = inject_delay;
|
||||
setting[L"insert_delay"] = insert_delay;
|
||||
setting[L"auto_inject"] = auto_inject;
|
||||
setting[L"auto_insert"] = auto_insert;
|
||||
setting[L"auto_copy"] = clipboard_flag;
|
||||
setting[L"auto_suppress"] = cyclic_remove;
|
||||
setting[L"global_filter"] = global_filter;
|
||||
|
||||
UniqueHandle hFile(IthCreateFile(L"ITH.xml", GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS));
|
||||
if (hFile.get() != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
FileWriter fw(hFile.get());
|
||||
pugi::xml_document doc;
|
||||
auto root = doc.root().append_child(L"ITH_Setting");
|
||||
for (auto it = setting.begin(); it != setting.end(); ++it)
|
||||
root.append_attribute(it->first.c_str()).set_value(it->second);
|
||||
auto filter = root.append_child(L"SingleCharFilter");
|
||||
filter.append_child(pugi::xml_node_type::node_pcdata);
|
||||
mb_filter->Traverse(RecordMBChar, &filter);
|
||||
uni_filter->Traverse(RecordUniChar, &filter);
|
||||
doc.save(fw);
|
||||
}
|
||||
}
|
||||
|
||||
void DefaultSettings()
|
||||
{
|
||||
setting[L"split_time"] = 200;
|
||||
setting[L"process_time"] = 50;
|
||||
setting[L"inject_delay"] = 3000;
|
||||
setting[L"insert_delay"] = 500;
|
||||
setting[L"auto_inject"] = 1;
|
||||
setting[L"auto_insert"] = 1;
|
||||
setting[L"auto_copy"] = 0;
|
||||
setting[L"auto_suppress"] = 0;
|
||||
setting[L"global_filter"] = 0;
|
||||
setting[L"window_left"] = 100;
|
||||
setting[L"window_right"] = 800;
|
||||
setting[L"window_top"] = 100;
|
||||
setting[L"window_bottom"] = 600;
|
||||
}
|
||||
|
||||
void InitializeSettings()
|
||||
{
|
||||
split_time = setting[L"split_time"];
|
||||
process_time = setting[L"process_time"];
|
||||
inject_delay = setting[L"inject_delay"];
|
||||
insert_delay = setting[L"insert_delay"];
|
||||
auto_inject = setting[L"auto_inject"];
|
||||
auto_insert = setting[L"auto_insert"];
|
||||
clipboard_flag = setting[L"auto_copy"];
|
||||
cyclic_remove = setting[L"auto_suppress"];
|
||||
global_filter = setting[L"global_filter"];
|
||||
window.left = setting[L"window_left"];
|
||||
window.right = setting[L"window_right"];
|
||||
window.top = setting[L"window_top"];
|
||||
window.bottom = setting[L"window_bottom"];
|
||||
|
||||
if (auto_inject > 1)
|
||||
auto_inject = 1;
|
||||
if (auto_insert > 1)
|
||||
auto_insert = 1;
|
||||
if (clipboard_flag > 1)
|
||||
clipboard_flag = 1;
|
||||
if (cyclic_remove > 1)
|
||||
cyclic_remove = 1;
|
||||
|
||||
if (window.right < window.left || window.right - window.left < 600)
|
||||
window.right = window.left + 600;
|
||||
if (window.bottom < window.top || window.bottom - window.top < 200)
|
||||
window.bottom = window.top + 200;
|
||||
}
|
||||
|
||||
void LoadSettings()
|
||||
{
|
||||
UniqueHandle hFile(IthCreateFile(L"ITH.xml", GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING));
|
||||
if (hFile.get() != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
DWORD size = GetFileSize(hFile.get(), NULL);
|
||||
std::unique_ptr<char[]> buffer(new char[size]);
|
||||
ReadFile(hFile.get(), buffer.get(), size, &size, NULL);
|
||||
pugi::xml_document doc;
|
||||
auto result = doc.load_buffer_inplace(buffer.get(), size);
|
||||
if (!result)
|
||||
return;
|
||||
auto root = doc.root().child(L"ITH_Setting");
|
||||
for (auto attr = root.attributes_begin(); attr != root.attributes_end(); ++attr)
|
||||
{
|
||||
auto it = setting.find(attr->name());
|
||||
if (it != setting.end())
|
||||
it->second = std::stoul(attr->value());
|
||||
}
|
||||
auto filter = root.child(L"SingleCharFilter");
|
||||
if (filter)
|
||||
{
|
||||
for (auto attr = filter.attributes_begin(); attr != filter.attributes_end(); ++attr)
|
||||
{
|
||||
if (attr->name()[0] == L'm')
|
||||
{
|
||||
DWORD c = std::stoul(attr->name() + 1, NULL, 16);
|
||||
mb_filter->Insert(c & 0xFFFF);
|
||||
}
|
||||
else if (attr->name()[0] == L'u')
|
||||
{
|
||||
DWORD c = std::stoul(attr->name() + 1, NULL, 16);
|
||||
uni_filter->Insert(c & 0xFFFF);
|
||||
}
|
||||
}
|
||||
std::wstring filter_value = filter.text().get();
|
||||
for (auto it = filter_value.begin(); it != filter_value.end(); ++it)
|
||||
{
|
||||
WCHAR filter_unichar[2] = { *it, L'\0' };
|
||||
char filter_mbchar[4];
|
||||
WC_MB(filter_unichar, filter_mbchar, 4);
|
||||
mb_filter->Insert(*(WORD*)filter_mbchar);
|
||||
uni_filter->Insert(*it);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern LPCWSTR ClassName, ClassNameAdmin;
|
||||
static WCHAR mutex[] = L"ITH_RUNNING";
|
||||
DWORD FindITH()
|
||||
{
|
||||
HWND hwnd = FindWindow(ClassName, ClassName);
|
||||
if (hwnd == NULL)
|
||||
hwnd = FindWindow(ClassName, ClassNameAdmin);
|
||||
if (hwnd)
|
||||
{
|
||||
ShowWindow(hwnd, SW_SHOWNORMAL);
|
||||
SetForegroundWindow(hwnd);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
LONG WINAPI UnhandledExcept(_EXCEPTION_POINTERS *ExceptionInfo)
|
||||
{
|
||||
wchar_t path_name[512]; // fully qualified path name
|
||||
WCHAR code[16];
|
||||
EXCEPTION_RECORD* rec = ExceptionInfo->ExceptionRecord;
|
||||
std::swprintf(code, L"%08X", rec->ExceptionCode);
|
||||
MEMORY_BASIC_INFORMATION info;
|
||||
if (VirtualQuery(rec->ExceptionAddress, &info, sizeof(info)))
|
||||
{
|
||||
if (GetModuleFileName((HMODULE)info.AllocationBase, path_name, 512))
|
||||
{
|
||||
LPWSTR name = wcsrchr(path_name, L'\\');
|
||||
if (name)
|
||||
{
|
||||
DWORD addr = (DWORD)rec->ExceptionAddress;
|
||||
std::swprintf(name, L"%s:%08X", name + 1, addr - (DWORD)info.AllocationBase);
|
||||
MessageBox(NULL, name, code, MB_OK);
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::swprintf(path_name, L"%08X", rec->ExceptionAddress);
|
||||
MessageBox(NULL, path_name, code, MB_OK);
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
|
||||
{
|
||||
InitCommonControls();
|
||||
if (!IthInitSystemService())
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
CreateMutex(NULL, TRUE, L"ITH_MAIN_RUNNING");
|
||||
if (Host_Open())
|
||||
{
|
||||
SetUnhandledExceptionFilter(UnhandledExcept);
|
||||
Host_GetHookManager(&man);
|
||||
Host_GetSettings(&setman);
|
||||
setman->splittingInterval = 200;
|
||||
MonitorFlag = true;
|
||||
pfman = new ProfileManager();
|
||||
mb_filter = new CustomFilter();
|
||||
uni_filter = new CustomFilter();
|
||||
DefaultSettings();
|
||||
LoadSettings();
|
||||
InitializeSettings();
|
||||
setman->splittingInterval = split_time;
|
||||
setman->clipboardFlag = clipboard_flag > 0;
|
||||
hIns = hInstance;
|
||||
MyRegisterClass(hIns);
|
||||
InitInstance(hIns, FALSE, &window);
|
||||
MSG msg;
|
||||
while (GetMessage(&msg, NULL, 0, 0))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
//delete mb_filter;
|
||||
//delete uni_filter;
|
||||
delete pfman;
|
||||
MonitorFlag = false;
|
||||
man = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
FindITH();
|
||||
}
|
||||
Host_Close();
|
||||
IthCloseSystemService();
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
#ifndef IDC_STATIC
|
||||
#define IDC_STATIC (-1)
|
||||
#endif
|
||||
|
||||
#define IDD_DIALOG2 102
|
||||
#define IDD_DIALOG4 104
|
||||
#define IDI_ICON1 110
|
||||
#define IDC_CHECK1 1000
|
||||
#define IDC_CHECK2 1001
|
||||
#define IDC_CHECK3 1002
|
||||
#define IDC_CHECK4 1003
|
||||
#define IDC_CHECK5 1004
|
||||
#define IDC_EDIT1 1011
|
||||
#define IDC_EDIT2 1012
|
||||
#define IDC_EDIT3 1013
|
||||
#define IDC_EDIT4 1014
|
||||
#define IDC_BUTTON1 1020
|
||||
#define IDC_BUTTON2 1021
|
||||
#define IDC_BUTTON3 1022
|
||||
#define IDC_BUTTON5 1024
|
||||
#define IDC_LIST1 1028
|
||||
#define IDC_BUTTON6 40000
|
||||
#define IDC_CHECK6 40001
|
303
gui/utility.cpp
303
gui/utility.cpp
@ -1,303 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "utility.h"
|
||||
#include "host/host.h"
|
||||
#include "host/hookman.h"
|
||||
#include "vnrhook/include/types.h"
|
||||
#include "vnrhook/include/const.h"
|
||||
#include "profile/misc.h"
|
||||
|
||||
extern HookManager* man; // main.cpp
|
||||
|
||||
std::wstring GetDriveLetter(const std::wstring& devicePath);
|
||||
std::wstring GetWindowsPath(const std::wstring& fileObjectPath);
|
||||
PVOID GetAllocationBase(DWORD pid, LPCVOID);
|
||||
std::wstring GetModuleFileNameAsString(DWORD pid, PVOID allocationBase);
|
||||
std::wstring GetModuleFileNameAsString();
|
||||
std::wstring GetProcessPath(HANDLE hProc);
|
||||
|
||||
void ConsoleOutput(LPCWSTR text)
|
||||
{
|
||||
man->AddConsoleOutput(text);
|
||||
}
|
||||
|
||||
void ConsoleOutput(LPCSTR text)
|
||||
{
|
||||
int wc_length = MB_WC_count(text, -1);
|
||||
LPWSTR wc = new WCHAR[wc_length];
|
||||
MB_WC(text, wc, wc_length);
|
||||
man->AddConsoleOutput(wc);
|
||||
delete wc;
|
||||
}
|
||||
|
||||
std::wstring GetProcessPath(DWORD pid)
|
||||
{
|
||||
UniqueHandle hProc(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid));
|
||||
if (hProc)
|
||||
return GetProcessPath(hProc.get());
|
||||
else
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring GetProcessPath(HANDLE hProc)
|
||||
{
|
||||
wchar_t path[MAX_PATH];
|
||||
GetProcessImageFileName(hProc, path, MAX_PATH);
|
||||
return GetWindowsPath(path);
|
||||
}
|
||||
|
||||
std::wstring GetWindowsPath(const std::wstring& path)
|
||||
{
|
||||
// path is in device form
|
||||
// \Device\HarddiskVolume2\Windows\System32\taskhost.exe
|
||||
auto pathOffset = path.find(L'\\', 1) + 1;
|
||||
pathOffset = path.find(L'\\', pathOffset);
|
||||
std::wstring devicePath = path.substr(0, pathOffset); // \Device\HarddiskVolume2
|
||||
std::wstring dosDrive = GetDriveLetter(devicePath); // C:
|
||||
if (dosDrive.empty())
|
||||
return path;
|
||||
std::wstring dosPath = dosDrive; // C:
|
||||
dosPath += path.substr(pathOffset); // C:\Windows\System32\taskhost.exe
|
||||
return dosPath;
|
||||
}
|
||||
|
||||
std::wstring GetDriveLetter(const std::wstring& devicePath)
|
||||
{
|
||||
for (wchar_t drive = L'A'; drive <= L'Z'; drive++)
|
||||
{
|
||||
wchar_t szDriveName[3] = { drive, L':', L'\0' };
|
||||
wchar_t szTarget[512];
|
||||
if (QueryDosDevice(szDriveName, szTarget, 512))
|
||||
if (devicePath.compare(szTarget) == 0)
|
||||
return szDriveName;
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring GetCode(const HookParam& hp, DWORD pid)
|
||||
{
|
||||
std::wstring code(L"/H");
|
||||
WCHAR c;
|
||||
if (hp.type & PRINT_DWORD)
|
||||
c = L'H';
|
||||
else if (hp.type & USING_UNICODE)
|
||||
{
|
||||
if (hp.type & USING_STRING)
|
||||
c = L'Q';
|
||||
else if (hp.type & STRING_LAST_CHAR)
|
||||
c = L'L';
|
||||
else
|
||||
c = L'W';
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hp.type & USING_STRING)
|
||||
c = L'S';
|
||||
else if (hp.type & BIG_ENDIAN)
|
||||
c = L'A';
|
||||
else if (hp.type & STRING_LAST_CHAR)
|
||||
c = L'E';
|
||||
else
|
||||
c = L'B';
|
||||
}
|
||||
code += c;
|
||||
if (hp.type & NO_CONTEXT)
|
||||
code += L'N';
|
||||
if (hp.offset >> 31)
|
||||
code += L"-" + ToHexString(-(hp.offset + 4));
|
||||
else
|
||||
code += ToHexString(hp.offset);
|
||||
if (hp.type & DATA_INDIRECT)
|
||||
{
|
||||
if (hp.index >> 31)
|
||||
code += L"*-" + ToHexString(-hp.index);
|
||||
else
|
||||
code += L"*" + ToHexString(hp.index);
|
||||
}
|
||||
if (hp.type & USING_SPLIT)
|
||||
{
|
||||
if (hp.split >> 31)
|
||||
code += L":-" + ToHexString(-(4 + hp.split));
|
||||
else
|
||||
code += L":" + ToHexString(hp.split);
|
||||
}
|
||||
if (hp.type & SPLIT_INDIRECT)
|
||||
{
|
||||
if (hp.split_index >> 31)
|
||||
code += L"*-" + ToHexString(-hp.split_index);
|
||||
else
|
||||
code += L"*" + ToHexString(hp.split_index);
|
||||
}
|
||||
if (pid)
|
||||
{
|
||||
PVOID allocationBase = GetAllocationBase(pid, (LPCVOID)hp.address);
|
||||
if (allocationBase)
|
||||
{
|
||||
std::wstring path = GetModuleFileNameAsString(pid, allocationBase);
|
||||
if (!path.empty())
|
||||
{
|
||||
auto fileName = path.substr(path.rfind(L'\\') + 1);
|
||||
DWORD relativeHookAddress = hp.address - (DWORD)allocationBase;
|
||||
code += L"@" + ToHexString(relativeHookAddress) + L":" + fileName;
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hp.module)
|
||||
{
|
||||
code += L"@" + ToHexString(hp.address) + L"!" + ToHexString(hp.module);
|
||||
if (hp.function)
|
||||
code += L"!" + ToHexString(hp.function);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hack. The original address is stored in the function field
|
||||
// if (module == NULL && function != NULL).
|
||||
// MODULE_OFFSET and FUNCTION_OFFSET are removed from HookParam.type in
|
||||
// TextHook::UnsafeInsertHookCode() and can not be used here.
|
||||
if (hp.function)
|
||||
code += L"@" + ToHexString(hp.function);
|
||||
else
|
||||
code += L"@" + ToHexString(hp.address) + L":";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
std::wstring GetModuleFileNameAsString(DWORD pid, PVOID allocationBase)
|
||||
{
|
||||
const ProcessRecord* pr = man->GetProcessRecord(pid);
|
||||
if (pr)
|
||||
{
|
||||
HANDLE hProc = pr->process_handle;
|
||||
WCHAR path[MAX_PATH];
|
||||
if (GetModuleFileNameEx(hProc, (HMODULE)allocationBase, path, MAX_PATH))
|
||||
return path;
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
PVOID GetAllocationBase(DWORD pid, LPCVOID addr)
|
||||
{
|
||||
const ProcessRecord *pr = man->GetProcessRecord(pid);
|
||||
if (pr)
|
||||
{
|
||||
MEMORY_BASIC_INFORMATION info;
|
||||
HANDLE hProc = pr->process_handle;
|
||||
if (VirtualQueryEx(hProc, addr, &info, sizeof(info)))
|
||||
{
|
||||
if (info.Type & MEM_IMAGE)
|
||||
return info.AllocationBase;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct TitleParam
|
||||
{
|
||||
DWORD pid, buffer_len, retn_len;
|
||||
std::wstring buffer;
|
||||
};
|
||||
|
||||
BOOL CALLBACK EnumProc(HWND hwnd, LPARAM lParam)
|
||||
{
|
||||
TitleParam* p = (TitleParam*)lParam;
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
if (pid == p->pid)
|
||||
{
|
||||
if (GetWindowLong(hwnd, GWL_STYLE) & WS_VISIBLE)
|
||||
{
|
||||
int len = GetWindowTextLength(hwnd);
|
||||
std::unique_ptr<wchar_t[]> result(new wchar_t[len + 1]);
|
||||
GetWindowText(hwnd, result.get(), len + 1);
|
||||
p->buffer = result.get();
|
||||
p->retn_len = p->buffer.size();
|
||||
if (!p->buffer.empty())
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::wstring GetProcessTitle(DWORD pid)
|
||||
{
|
||||
TitleParam p;
|
||||
p.pid = pid;
|
||||
p.buffer_len = 0;
|
||||
p.retn_len = 0;
|
||||
EnumWindows(EnumProc, (LPARAM)&p);
|
||||
return p.buffer;
|
||||
}
|
||||
|
||||
WindowsError::WindowsError(DWORD error_code) : error_code(error_code), msg("")
|
||||
{
|
||||
CHAR str[512];
|
||||
std::sprintf(str, "error code 0x%8x", error_code);
|
||||
msg = str;
|
||||
}
|
||||
|
||||
const char *WindowsError::what() const
|
||||
{
|
||||
return msg.c_str();
|
||||
}
|
||||
|
||||
HANDLE IthCreateThread(LPVOID start_addr, DWORD param)
|
||||
{
|
||||
return CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)start_addr, (LPVOID)param, 0, NULL);
|
||||
}
|
||||
|
||||
std::wstring GetModuleFileNameAsString()
|
||||
{
|
||||
WCHAR path[MAX_PATH];
|
||||
GetModuleFileName(NULL, path, MAX_PATH);
|
||||
return path;
|
||||
}
|
||||
|
||||
bool IthCreateDirectory(LPCWSTR name)
|
||||
{
|
||||
std::wstring path = GetModuleFileNameAsString();
|
||||
path = path.substr(0, path.rfind(L'\\') + 1) + name;
|
||||
BOOL error_code = CreateDirectory(path.c_str(), NULL);
|
||||
return error_code != 0 || GetLastError() == ERROR_ALREADY_EXISTS;
|
||||
}
|
||||
|
||||
HANDLE IthCreateFile(LPCWSTR name, DWORD option, DWORD share, DWORD disposition)
|
||||
{
|
||||
std::wstring path = GetModuleFileNameAsString();
|
||||
path = path.substr(0, path.rfind(L'\\') + 1) + name;
|
||||
return CreateFile(path.c_str(), option, share, NULL, disposition, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
}
|
||||
|
||||
//SJIS->Unicode. mb must be null-terminated. wc_length is the length of wc in characters.
|
||||
int MB_WC(const char* mb, wchar_t* wc, int wc_length)
|
||||
{
|
||||
return MultiByteToWideChar(932, 0, mb, -1, wc, wc_length);
|
||||
}
|
||||
|
||||
// Count characters in wide string. mb_length is the number of bytes from mb to convert or
|
||||
// -1 if the string is null terminated.
|
||||
int MB_WC_count(const char* mb, int mb_length)
|
||||
{
|
||||
return MultiByteToWideChar(932, 0, mb, mb_length, NULL, 0);
|
||||
}
|
||||
|
||||
// Unicode->SJIS. Analogous to MB_WC.
|
||||
int WC_MB(const wchar_t *wc, char* mb, int mb_length)
|
||||
{
|
||||
return WideCharToMultiByte(932, 0, wc, -1, mb, mb_length, NULL, NULL);
|
||||
}
|
105
gui/utility.h
105
gui/utility.h
@ -1,105 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ITH.h"
|
||||
|
||||
struct HookParam;
|
||||
struct ProcessRecord;
|
||||
|
||||
DWORD ProcessCommand(const std::wstring& cmd, DWORD pid);
|
||||
std::wstring GetProcessPath(DWORD pid);
|
||||
void ConsoleOutput(LPCWSTR);
|
||||
void ConsoleOutput(LPCSTR text);
|
||||
std::wstring GetProcessTitle(DWORD pid);
|
||||
std::wstring GetCode(const HookParam& hp, DWORD pid = 0);
|
||||
|
||||
// http://codesequoia.wordpress.com/2012/08/26/stdunique_ptr-for-windows-handles/
|
||||
struct HandleDeleter
|
||||
{
|
||||
typedef HANDLE pointer;
|
||||
void operator() (HANDLE h)
|
||||
{
|
||||
if (h != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(h);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::unique_ptr<HANDLE, HandleDeleter> UniqueHandle;
|
||||
|
||||
class FileWriter : public pugi::xml_writer
|
||||
{
|
||||
HANDLE hFile;
|
||||
public:
|
||||
FileWriter(HANDLE hFile) : hFile(hFile) {};
|
||||
~FileWriter() {};
|
||||
|
||||
virtual void write(const void* data, size_t size)
|
||||
{
|
||||
DWORD dwNumberOfBytesWritten;
|
||||
WriteFile(hFile, data, size, &dwNumberOfBytesWritten, NULL);
|
||||
}
|
||||
};
|
||||
|
||||
class WindowsError : public std::exception
|
||||
{
|
||||
private:
|
||||
std::string msg;
|
||||
DWORD error_code;
|
||||
public:
|
||||
WindowsError(DWORD error_code);
|
||||
virtual const char *what() const;
|
||||
};
|
||||
|
||||
HANDLE IthCreateThread(LPVOID start_addr, DWORD param);
|
||||
bool IthCreateDirectory(LPCWSTR name);
|
||||
HANDLE IthCreateFile(LPCWSTR name, DWORD option, DWORD share, DWORD disposition);
|
||||
int MB_WC(const char* mb, wchar_t* wc, int wc_length);
|
||||
int MB_WC_count(const char* mb, int mb_length);
|
||||
int WC_MB(const wchar_t *wc, char* mb, int mb_length);
|
||||
bool Parse(const std::wstring& cmd, HookParam& hp);
|
||||
|
||||
// http://jrdodds.blogs.com/blog/2004/08/raii_in_c.html
|
||||
class CriticalSection
|
||||
{
|
||||
public:
|
||||
CriticalSection()
|
||||
{
|
||||
::InitializeCriticalSection(&m_rep);
|
||||
}
|
||||
~CriticalSection()
|
||||
{
|
||||
::DeleteCriticalSection(&m_rep);
|
||||
}
|
||||
void Enter()
|
||||
{
|
||||
::EnterCriticalSection(&m_rep);
|
||||
}
|
||||
void Leave()
|
||||
{
|
||||
::LeaveCriticalSection(&m_rep);
|
||||
}
|
||||
private:
|
||||
CriticalSection(const CriticalSection&);
|
||||
CriticalSection& operator=(const CriticalSection&);
|
||||
|
||||
CRITICAL_SECTION m_rep;
|
||||
};
|
||||
|
||||
class CSLock
|
||||
{
|
||||
public:
|
||||
CSLock(CriticalSection& a_section)
|
||||
: m_section(a_section)
|
||||
{
|
||||
m_section.Enter();
|
||||
}
|
||||
~CSLock()
|
||||
{
|
||||
m_section.Leave();
|
||||
}
|
||||
private:
|
||||
CSLock(const CSLock&);
|
||||
CSLock& operator=(const CSLock&);
|
||||
|
||||
CriticalSection& m_section;
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
const wchar_t* build_date=L"27.01.2013";
|
||||
const WCHAR program_version[] = L"@CPACK_PACKAGE_VERSION_MAJOR@.@CPACK_PACKAGE_VERSION_MINOR@.@CPACK_PACKAGE_VERSION_PATCH@";
|
866
gui/window.cpp
866
gui/window.cpp
@ -1,866 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "window.h"
|
||||
#include "ProcessWindow.h"
|
||||
#include "resource.h"
|
||||
#include "language.h"
|
||||
#include "host/host.h"
|
||||
#include "host/hookman.h"
|
||||
#include "vnrhook/include/const.h"
|
||||
#include "version.h"
|
||||
#include "ProfileManager.h"
|
||||
#include "host/settings.h"
|
||||
#include "CustomFilter.h"
|
||||
#include "profile/Profile.h"
|
||||
#include "TextBuffer.h"
|
||||
#include "profile/misc.h"
|
||||
|
||||
#define CMD_SIZE 512
|
||||
|
||||
static WNDPROC proc, proccmd, procChar;
|
||||
static WCHAR last_cmd[CMD_SIZE];
|
||||
extern HINSTANCE hIns; // main.cpp
|
||||
|
||||
HWND hMainWnd, hwndCombo, hwndProcessComboBox, hwndEdit, hwndCmd;
|
||||
HWND hwndProcess;
|
||||
HWND hwndOption, hwndTop, hwndClear, hwndSave, hwndRemoveLink, hwndRemoveHook;
|
||||
HWND hProcDlg, hOptionDlg;
|
||||
HBRUSH hWhiteBrush;
|
||||
DWORD background;
|
||||
ProcessWindow* pswnd;
|
||||
TextBuffer* texts;
|
||||
extern ProfileManager* pfman; // ProfileManager.cpp
|
||||
extern HookManager* man; // main.cpp
|
||||
extern CustomFilter* mb_filter; // main.cpp
|
||||
extern CustomFilter* uni_filter; // main.cpp
|
||||
extern Settings* setman; // main.cpp
|
||||
#define COMMENT_BUFFER_LENGTH 512
|
||||
static WCHAR comment_buffer[COMMENT_BUFFER_LENGTH];
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
void SaveSettings(); // main.cpp
|
||||
extern LONG split_time, process_time, inject_delay, insert_delay,
|
||||
auto_inject, auto_insert, clipboard_flag, cyclic_remove, global_filter; //main.cpp
|
||||
static int last_select, last_edit;
|
||||
|
||||
ATOM MyRegisterClass(HINSTANCE hInstance)
|
||||
{
|
||||
WNDCLASSEX wcex;
|
||||
wcex.cbSize = sizeof(WNDCLASSEX);
|
||||
wcex.style = CS_HREDRAW | CS_VREDRAW;
|
||||
wcex.lpfnWndProc = WndProc;
|
||||
wcex.cbClsExtra = 0;
|
||||
wcex.cbWndExtra = 0;
|
||||
wcex.hInstance = hInstance;
|
||||
wcex.hIcon = NULL;
|
||||
wcex.hCursor = NULL;
|
||||
wcex.hbrBackground = GetStockBrush(WHITE_BRUSH);
|
||||
wcex.lpszMenuName = NULL;
|
||||
wcex.lpszClassName = ClassName;
|
||||
wcex.hIconSm = LoadIcon(hInstance, (LPWSTR)IDI_ICON1);
|
||||
return RegisterClassEx(&wcex);
|
||||
}
|
||||
|
||||
BOOL InitInstance(HINSTANCE hInstance, DWORD nAdmin, RECT* rc)
|
||||
{
|
||||
hIns = hInstance;
|
||||
LPCWSTR name = (nAdmin) ? ClassNameAdmin : ClassName;
|
||||
hMainWnd = CreateWindow(ClassName, name, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
|
||||
rc->left, rc->top, rc->right - rc->left, rc->bottom - rc->top, NULL, NULL, hInstance, 0);
|
||||
if (!hMainWnd)
|
||||
return FALSE;
|
||||
ShowWindow(hMainWnd, SW_SHOWNORMAL);
|
||||
UpdateWindow(hMainWnd);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
DWORD SaveProcessProfile(DWORD pid); // ProfileManager.cpp
|
||||
|
||||
BOOL CALLBACK OptionDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (uMsg)
|
||||
{
|
||||
case WM_INITDIALOG:
|
||||
{
|
||||
SetWindowText(GetDlgItem(hDlg, IDC_EDIT1), std::to_wstring((long long)split_time).c_str());
|
||||
SetWindowText(GetDlgItem(hDlg, IDC_EDIT2), std::to_wstring((long long)process_time).c_str());
|
||||
SetWindowText(GetDlgItem(hDlg, IDC_EDIT3), std::to_wstring((long long)inject_delay).c_str());
|
||||
SetWindowText(GetDlgItem(hDlg, IDC_EDIT4), std::to_wstring((long long)insert_delay).c_str());
|
||||
CheckDlgButton(hDlg, IDC_CHECK1, auto_inject);
|
||||
CheckDlgButton(hDlg, IDC_CHECK2, auto_insert);
|
||||
CheckDlgButton(hDlg, IDC_CHECK3, clipboard_flag);
|
||||
CheckDlgButton(hDlg, IDC_CHECK4, cyclic_remove);
|
||||
CheckDlgButton(hDlg, IDC_CHECK5, global_filter);
|
||||
}
|
||||
return TRUE;
|
||||
case WM_COMMAND:
|
||||
{
|
||||
DWORD wmId = LOWORD(wParam);
|
||||
DWORD wmEvent = HIWORD(wParam);
|
||||
switch (wmId)
|
||||
{
|
||||
case IDOK:
|
||||
{
|
||||
WCHAR str[128];
|
||||
GetWindowText(GetDlgItem(hDlg, IDC_EDIT1), str, 0x80);
|
||||
DWORD st = std::stoul(str);
|
||||
split_time = st > 100 ? st : 100;
|
||||
GetWindowText(GetDlgItem(hDlg, IDC_EDIT2), str, 0x80);
|
||||
DWORD pt = std::stoul(str);
|
||||
process_time = pt > 50 ? pt : 50;
|
||||
GetWindowText(GetDlgItem(hDlg, IDC_EDIT3), str, 0x80);
|
||||
DWORD jd = std::stoul(str);
|
||||
inject_delay = jd > 1000 ? jd : 1000;
|
||||
GetWindowText(GetDlgItem(hDlg, IDC_EDIT4), str, 0x80);
|
||||
DWORD sd = std::stoul(str);
|
||||
insert_delay = sd > 200 ? sd : 200;
|
||||
if (IsDlgButtonChecked(hDlg, IDC_CHECK6))
|
||||
{
|
||||
man->ResetRepeatStatus();
|
||||
}
|
||||
auto_inject = IsDlgButtonChecked(hDlg, IDC_CHECK1);
|
||||
auto_insert = IsDlgButtonChecked(hDlg, IDC_CHECK2);
|
||||
clipboard_flag = IsDlgButtonChecked(hDlg, IDC_CHECK3);
|
||||
cyclic_remove = IsDlgButtonChecked(hDlg, IDC_CHECK4);
|
||||
global_filter = IsDlgButtonChecked(hDlg, IDC_CHECK5);
|
||||
setman->clipboardFlag = clipboard_flag;
|
||||
setman->splittingInterval = split_time;
|
||||
if (auto_inject == 0) auto_insert = 0;
|
||||
}
|
||||
case IDCANCEL:
|
||||
EndDialog(hDlg, 0);
|
||||
hOptionDlg = NULL;
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
BOOL CALLBACK ProcessDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (uMsg)
|
||||
{
|
||||
case WM_INITDIALOG:
|
||||
{
|
||||
pswnd = new ProcessWindow(hDlg);
|
||||
return TRUE;
|
||||
}
|
||||
case WM_COMMAND:
|
||||
{
|
||||
DWORD wmId, wmEvent;
|
||||
wmId = LOWORD(wParam);
|
||||
wmEvent = HIWORD(wParam);
|
||||
switch (wmId)
|
||||
{
|
||||
case WM_DESTROY:
|
||||
case IDOK:
|
||||
EndDialog(hDlg, NULL);
|
||||
hProcDlg = NULL;
|
||||
delete pswnd;
|
||||
pswnd = NULL;
|
||||
break;
|
||||
case IDC_BUTTON1:
|
||||
pswnd->RefreshProcess();
|
||||
break;
|
||||
case IDC_BUTTON2:
|
||||
pswnd->AttachProcess();
|
||||
break;
|
||||
case IDC_BUTTON3:
|
||||
pswnd->DetachProcess();
|
||||
break;
|
||||
case IDC_BUTTON5:
|
||||
pswnd->CreateProfileForSelectedProcess();
|
||||
break;
|
||||
case IDC_BUTTON6:
|
||||
pswnd->DeleteProfileForSelectedProcess();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
|
||||
case WM_NOTIFY:
|
||||
{
|
||||
LPNMHDR dr = (LPNMHDR)lParam;
|
||||
switch (dr->code)
|
||||
{
|
||||
case LVN_ITEMCHANGED:
|
||||
if (dr->idFrom == IDC_LIST1)
|
||||
{
|
||||
NMLISTVIEW *nmlv = (LPNMLISTVIEW)lParam;
|
||||
if (nmlv->uNewState & LVIS_SELECTED)
|
||||
pswnd->RefreshThread(nmlv->iItem);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK EditProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case WM_CHAR: //Filter user input.
|
||||
if (GetKeyState(VK_CONTROL) & 0x8000)
|
||||
{
|
||||
if (wParam == 1)
|
||||
{
|
||||
Edit_SetSel(hwndEdit, 0, -1);
|
||||
SendMessage(hwndEdit, WM_COPY, 0, 0);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
case WM_LBUTTONUP:
|
||||
if (hwndEdit)
|
||||
SendMessage(hwndEdit, WM_COPY, 0, 0);
|
||||
default:
|
||||
{
|
||||
return proc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK EditCmdProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_KEYDOWN:
|
||||
if (wParam == VK_UP)
|
||||
{
|
||||
SetWindowText(hWnd, last_cmd);
|
||||
SetFocus(hWnd);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_CHAR:
|
||||
if (wParam == VK_RETURN)
|
||||
{
|
||||
DWORD s = 0, pid = 0;
|
||||
WCHAR str[32];
|
||||
if (GetWindowTextLength(hWnd) == 0)
|
||||
break;
|
||||
GetWindowText(hWnd, last_cmd, CMD_SIZE);
|
||||
//IthBreak();
|
||||
if (GetWindowText(hwndProcessComboBox, str, 32))
|
||||
pid = std::stoul(str);
|
||||
ProcessCommand(last_cmd, pid);
|
||||
Edit_SetSel(hWnd, 0, -1);
|
||||
Edit_ReplaceSel(hWnd, &s);
|
||||
SetFocus(hWnd);
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return CallWindowProc(proccmd, hWnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void CreateButtons(HWND hWnd)
|
||||
{
|
||||
hwndProcess = CreateWindow(L"Button", L"Process", WS_CHILD | WS_VISIBLE,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndOption = CreateWindow(L"Button", L"Option", WS_CHILD | WS_VISIBLE,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndClear = CreateWindow(L"Button", L"Clear", WS_CHILD | WS_VISIBLE,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndSave = CreateWindow(L"Button", L"Save", WS_CHILD | WS_VISIBLE,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndRemoveLink = CreateWindow(L"Button", L"Unlink", WS_CHILD | WS_VISIBLE,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndRemoveHook = CreateWindow(L"Button", L"Unhook", WS_CHILD | WS_VISIBLE,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndTop = CreateWindow(L"Button", L"Top", WS_CHILD | WS_VISIBLE | BS_PUSHLIKE | BS_CHECKBOX,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndProcessComboBox = CreateWindow(L"ComboBox", NULL,
|
||||
WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST |
|
||||
CBS_SORT | WS_VSCROLL | WS_TABSTOP,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndCmd = CreateWindowEx(WS_EX_CLIENTEDGE, L"Edit", NULL,
|
||||
WS_CHILD | WS_VISIBLE | ES_NOHIDESEL | ES_LEFT | ES_AUTOHSCROLL,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
hwndEdit = CreateWindowEx(WS_EX_CLIENTEDGE, L"Edit", NULL,
|
||||
WS_CHILD | WS_VISIBLE | ES_NOHIDESEL | WS_VSCROLL |
|
||||
ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
}
|
||||
|
||||
void ClickButton(HWND hWnd, HWND h)
|
||||
{
|
||||
if (h == hwndProcess)
|
||||
{
|
||||
if (hProcDlg)
|
||||
SetForegroundWindow(hProcDlg);
|
||||
else
|
||||
hProcDlg = CreateDialog(hIns, (LPWSTR)IDD_DIALOG2, 0, ProcessDlgProc);
|
||||
}
|
||||
else if (h == hwndOption)
|
||||
{
|
||||
if (hOptionDlg)
|
||||
SetForegroundWindow(hOptionDlg);
|
||||
else
|
||||
hOptionDlg = CreateDialog(hIns, (LPWSTR)IDD_DIALOG4, 0, OptionDlgProc);
|
||||
}
|
||||
else if (h == hwndClear)
|
||||
{
|
||||
WCHAR pwcEntry[128] = {};
|
||||
DWORD dwId = ComboBox_GetCurSel(hwndCombo);
|
||||
int len = ComboBox_GetLBText(hwndCombo, dwId, pwcEntry);
|
||||
dwId = std::stoul(pwcEntry, NULL, 16);
|
||||
if (dwId == 0)
|
||||
man->ClearCurrent();
|
||||
else
|
||||
man->RemoveSingleThread(dwId);
|
||||
}
|
||||
else if (h == hwndTop)
|
||||
{
|
||||
if (Button_GetCheck(h) == BST_CHECKED)
|
||||
{
|
||||
Button_SetCheck(h, BST_UNCHECKED);
|
||||
SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
|
||||
if (hProcDlg)
|
||||
SetWindowPos(hProcDlg, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
|
||||
if (hOptionDlg)
|
||||
SetWindowPos(hOptionDlg, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
|
||||
}
|
||||
else
|
||||
{
|
||||
Button_SetCheck(h, BST_CHECKED);
|
||||
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
|
||||
if (hProcDlg)
|
||||
SetWindowPos(hProcDlg, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
|
||||
if (hOptionDlg)
|
||||
SetWindowPos(hOptionDlg, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
|
||||
}
|
||||
}
|
||||
else if (h == hwndSave)
|
||||
{
|
||||
WCHAR str[32];
|
||||
if (GetWindowText(hwndProcessComboBox, str, 32))
|
||||
{
|
||||
DWORD pid = std::stoul(str);
|
||||
SaveProcessProfile(pid);
|
||||
}
|
||||
pfman->SaveProfiles();
|
||||
}
|
||||
else if (h == hwndRemoveLink)
|
||||
{
|
||||
WCHAR str[32];
|
||||
if (GetWindowText(hwndCombo, str, 32))
|
||||
{
|
||||
DWORD from = std::stoul(str, NULL, 16);
|
||||
if (from != 0)
|
||||
Host_UnLink(from);
|
||||
}
|
||||
}
|
||||
else if (h == hwndRemoveHook)
|
||||
{
|
||||
WCHAR str[32];
|
||||
if (GetWindowText(hwndCombo, str, 32))
|
||||
{
|
||||
std::wstring entry(str);
|
||||
std::size_t i;
|
||||
DWORD threadNumber = std::stoul(entry, &i, 16);
|
||||
entry = entry.substr(i + 1);
|
||||
DWORD pid = std::stoul(entry, &i);
|
||||
entry = entry.substr(i + 1);
|
||||
DWORD addr = std::stoul(entry, NULL, 16);
|
||||
if (threadNumber != 0)
|
||||
Host_RemoveHook(pid, addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DWORD ThreadFilter(TextThread* thread, BYTE* out, DWORD len, DWORD new_line, PVOID data, bool space)
|
||||
{
|
||||
DWORD status = thread->Status();
|
||||
if (global_filter && !new_line && thread->Number() != 0)
|
||||
{
|
||||
if (status & USING_UNICODE)
|
||||
{
|
||||
DWORD i, j;
|
||||
len /= 2;
|
||||
LPWSTR str = (LPWSTR)out;
|
||||
for (i = 0, j = 0; i < len; i++)
|
||||
{
|
||||
WCHAR c = str[i];
|
||||
if (!uni_filter->Find(c))
|
||||
str[j++] = c;
|
||||
}
|
||||
memset(str + j, 0, (len - j) * 2);
|
||||
len = j * 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD i, j;
|
||||
for (i = 0, j = 0; i < len; i++)
|
||||
{
|
||||
WORD c = out[i];
|
||||
if (!IsDBCSLeadByte(c & 0xFF))
|
||||
{
|
||||
if (!mb_filter->Find(c))
|
||||
out[j++] = c & 0xFF;
|
||||
}
|
||||
else if (i + 1 < len)
|
||||
{
|
||||
|
||||
c = out[i + 1];
|
||||
c <<= 8;
|
||||
c |= out[i];
|
||||
if (!mb_filter->Find(c))
|
||||
{
|
||||
out[j++] = c & 0xFF;
|
||||
out[j++] = c >> 8;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
memset(out + j, 0, len - j);
|
||||
len = j;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
DWORD ThreadOutput(TextThread* thread, BYTE* out, DWORD len, DWORD new_line, PVOID data, bool space)
|
||||
{
|
||||
if (len == 0)
|
||||
return len;
|
||||
DWORD status = thread->Status();
|
||||
if (status & CURRENT_SELECT)
|
||||
{
|
||||
if (new_line)
|
||||
{
|
||||
if (thread->Number() == 0)
|
||||
texts->AddText(L"\r\n", 2, true);
|
||||
else
|
||||
texts->AddText(L"\r\n\r\n", 4, true);
|
||||
}
|
||||
else if (status & USING_UNICODE)
|
||||
{
|
||||
texts->AddText((LPWSTR)out, len / 2, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
int uni_len = MB_WC_count((char*)out, len);
|
||||
LPWSTR str = new WCHAR[uni_len + 1];
|
||||
MB_WC((char*)out, str, uni_len + 1);
|
||||
str[uni_len] = L'\0';
|
||||
texts->AddText(str, uni_len, false);
|
||||
delete str;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
bool GetHookParam(DWORD pid, DWORD hook_addr, HookParam& hp)
|
||||
{
|
||||
if (!pid)
|
||||
return false;
|
||||
ProcessRecord *pr = ::man->GetProcessRecord(pid);
|
||||
if (!pr)
|
||||
return false;
|
||||
bool result = false;
|
||||
WaitForSingleObject(pr->hookman_mutex, 0);
|
||||
const Hook *hks = (Hook *)pr->hookman_map;
|
||||
for (int i = 0; i < MAX_HOOK; i++)
|
||||
{
|
||||
if (hks[i].Address() == hook_addr)
|
||||
{
|
||||
hp = hks[i].hp;
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ReleaseMutex(pr->hookman_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring GetEntryString(TextThread& thread)
|
||||
{
|
||||
CHAR entry[512];
|
||||
thread.GetEntryString(entry, 512);
|
||||
return toUnicodeString(entry);
|
||||
}
|
||||
|
||||
std::wstring CreateEntryWithLink(TextThread& thread, std::wstring& entry)
|
||||
{
|
||||
std::wstring entryWithLink = entry;
|
||||
if (thread.Link())
|
||||
entryWithLink += L"->" + ToHexString(thread.LinkNumber());
|
||||
if (thread.PID() == 0)
|
||||
entryWithLink += L"ConsoleOutput";
|
||||
HookParam hp = {};
|
||||
if (GetHookParam(thread.PID(), thread.Addr(), hp))
|
||||
entryWithLink += L" (" + GetCode(hp, thread.PID()) + L")";
|
||||
return entryWithLink;
|
||||
}
|
||||
|
||||
void AddToCombo(TextThread& thread, bool replace)
|
||||
{
|
||||
std::wstring entry = GetEntryString(thread);
|
||||
std::wstring entryWithLink = CreateEntryWithLink(thread, entry);
|
||||
int i = ComboBox_FindString(hwndCombo, -1, entry.c_str());
|
||||
if (replace)
|
||||
{
|
||||
int sel = ComboBox_GetCurSel(hwndCombo);
|
||||
if (i != CB_ERR)
|
||||
ComboBox_DeleteString(hwndCombo, i);
|
||||
ComboBox_AddString(hwndCombo, entryWithLink.c_str());
|
||||
ComboBox_SetCurSel(hwndCombo, sel);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (i == CB_ERR)
|
||||
ComboBox_AddString(hwndCombo, entryWithLink.c_str());
|
||||
// Why set current selection to 0 when the new thread is selected?
|
||||
if (thread.Status() & CURRENT_SELECT)
|
||||
ComboBox_SetCurSel(hwndCombo, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveFromCombo(TextThread* thread)
|
||||
{
|
||||
CHAR entry[512];
|
||||
thread->GetEntryString(entry, 512);
|
||||
std::wstring unicodeEntry = toUnicodeString(entry);
|
||||
if (thread->PID() == 0)
|
||||
unicodeEntry += L"ConsoleOutput";
|
||||
int i = ComboBox_FindString(hwndCombo, 0, unicodeEntry.c_str());
|
||||
if (i != CB_ERR)
|
||||
{
|
||||
if (ComboBox_DeleteString(hwndCombo, i) == CB_ERR)
|
||||
ConsoleOutput(ErrorDeleteCombo);
|
||||
}
|
||||
}
|
||||
|
||||
void ComboSelectCurrent(TextThread* thread)
|
||||
{
|
||||
ComboBox_SetCurSel(hwndCombo, thread->Number());
|
||||
}
|
||||
|
||||
DWORD SetEditText(LPWSTR wc)
|
||||
{
|
||||
DWORD line;
|
||||
Edit_SetText(hwndEdit, wc);
|
||||
line = Edit_GetLineCount(hwndEdit);
|
||||
SendMessage(hwndEdit, EM_LINESCROLL, 0, line);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD ThreadReset(TextThread* thread)
|
||||
{
|
||||
texts->ClearBuffer();
|
||||
man->SetCurrent(thread);
|
||||
thread->LockVector();
|
||||
DWORD uni = thread->Status() & USING_UNICODE;
|
||||
if (uni)
|
||||
{
|
||||
DWORD len = 0;
|
||||
LPWSTR wc = (LPWSTR)thread->GetStore(&len);
|
||||
len /= 2;
|
||||
wc[len] = L'\0';
|
||||
SetEditText(wc);
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD len = MB_WC_count((char*)thread->Storage(), thread->Used());
|
||||
LPWSTR wc = new WCHAR[len + 1];
|
||||
MB_WC((char*)thread->Storage(), wc, len + 1);
|
||||
wc[len] = L'\0';
|
||||
SetEditText(wc);
|
||||
delete wc;
|
||||
}
|
||||
WCHAR buffer[16];
|
||||
std::swprintf(buffer, L"%04X", thread->Number());
|
||||
DWORD tmp = ComboBox_FindString(hwndCombo, 0, buffer);
|
||||
if (tmp != CB_ERR)
|
||||
ComboBox_SetCurSel(hwndCombo, tmp);
|
||||
thread->UnlockVector();
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD AddRemoveLink(TextThread* thread)
|
||||
{
|
||||
AddToCombo(*thread, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool IsUnicodeHook(const ProcessRecord& pr, DWORD hook);
|
||||
void AddLinksToHookManager(const Profile* pf, size_t thread_index, const TextThread* thread);
|
||||
|
||||
DWORD ThreadCreate(TextThread* thread)
|
||||
{
|
||||
thread->RegisterOutputCallBack(ThreadOutput, 0);
|
||||
thread->RegisterFilterCallBack(ThreadFilter, 0);
|
||||
AddToCombo(*thread, false);
|
||||
const auto& tp = thread->GetThreadParameter();
|
||||
auto pr = man->GetProcessRecord(tp->pid);
|
||||
if (pr == NULL)
|
||||
return 0;
|
||||
if (IsUnicodeHook(*pr, tp->hook))
|
||||
thread->Status() |= USING_UNICODE;
|
||||
auto pf = pfman->GetProfile(tp->pid);
|
||||
if (!pf)
|
||||
return 0;
|
||||
const std::wstring& hook_name = GetHookNameByAddress(*pr, thread->GetThreadParameter()->hook);
|
||||
auto thread_profile = pf->FindThread(thread->GetThreadParameter(), hook_name);
|
||||
if (thread_profile != pf->Threads().end())
|
||||
{
|
||||
(*thread_profile)->HookManagerIndex() = thread->Number();
|
||||
auto thread_index = thread_profile - pf->Threads().begin();
|
||||
AddLinksToHookManager(pf, thread_index, thread);
|
||||
if (pf->IsThreadSelected(thread_profile))
|
||||
ThreadReset(thread);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool IsUnicodeHook(const ProcessRecord& pr, DWORD hook)
|
||||
{
|
||||
bool res = false;
|
||||
WaitForSingleObject(pr.hookman_mutex, 0);
|
||||
auto hooks = (const Hook*)pr.hookman_map;
|
||||
for (DWORD i = 0; i < MAX_HOOK; i++)
|
||||
{
|
||||
if (hooks[i].Address() == hook)
|
||||
{
|
||||
res = hooks[i].Type() & USING_UNICODE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ReleaseMutex(pr.hookman_mutex);
|
||||
return res;
|
||||
}
|
||||
|
||||
void AddLinksToHookManager(const Profile* pf, size_t thread_index, const TextThread* thread)
|
||||
{
|
||||
for (auto lp = pf->Links().begin(); lp != pf->Links().end(); ++lp)
|
||||
{
|
||||
if ((*lp)->FromIndex() == thread_index)
|
||||
{
|
||||
WORD to_index = pf->Threads()[(*lp)->ToIndex()]->HookManagerIndex();
|
||||
if (to_index != 0)
|
||||
man->AddLink(thread->Number(), to_index);
|
||||
}
|
||||
if ((*lp)->ToIndex() == thread_index)
|
||||
{
|
||||
WORD from_index = pf->Threads()[(*lp)->FromIndex()]->HookManagerIndex();
|
||||
if (from_index != 0)
|
||||
man->AddLink(from_index, thread->Number());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DWORD ThreadRemove(TextThread* thread)
|
||||
{
|
||||
RemoveFromCombo(thread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD RegisterProcessList(DWORD pid)
|
||||
{
|
||||
auto path = GetProcessPath(pid);
|
||||
if (!path.empty())
|
||||
{
|
||||
WCHAR str[MAX_PATH];
|
||||
std::swprintf(str, L"%04d:%s", pid, path.substr(path.rfind(L'\\') + 1).c_str());
|
||||
ComboBox_AddString(hwndProcessComboBox, str);
|
||||
if (ComboBox_GetCount(hwndProcessComboBox) == 1)
|
||||
ComboBox_SetCurSel(hwndProcessComboBox, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD RemoveProcessList(DWORD pid)
|
||||
{
|
||||
WCHAR str[MAX_PATH];
|
||||
std::swprintf(str, L"%04d", pid);
|
||||
DWORD i = ComboBox_FindString(hwndProcessComboBox, 0, str);
|
||||
DWORD j = ComboBox_GetCurSel(hwndProcessComboBox);
|
||||
if (i != CB_ERR)
|
||||
{
|
||||
DWORD k = ComboBox_DeleteString(hwndProcessComboBox, i);
|
||||
if (i == j)
|
||||
ComboBox_SetCurSel(hwndProcessComboBox, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD RefreshProfileOnNewHook(DWORD pid)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_CREATE:
|
||||
CreateButtons(hWnd);
|
||||
// Add text to the window.
|
||||
Edit_LimitText(hwndEdit, -1);
|
||||
SendMessage(hwndEdit, WM_INPUTLANGCHANGEREQUEST, 0, 0x411);
|
||||
proc = (WNDPROC)SetWindowLong(hwndEdit, GWL_WNDPROC, (LONG)EditProc);
|
||||
proccmd = (WNDPROC)SetWindowLong(hwndCmd, GWL_WNDPROC, (LONG)EditCmdProc);
|
||||
hwndCombo = CreateWindow(L"ComboBox", NULL,
|
||||
WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST |
|
||||
CBS_SORT | WS_VSCROLL | WS_TABSTOP,
|
||||
0, 0, 0, 0, hWnd, 0, hIns, NULL);
|
||||
{
|
||||
HDC hDC = GetDC(hWnd);
|
||||
int nHeight = -MulDiv(12, GetDeviceCaps(hDC, LOGPIXELSY), 72);
|
||||
ReleaseDC(hWnd, hDC);
|
||||
HFONT hf = CreateFont(nHeight, 0, 0, 0, FW_LIGHT, FALSE, FALSE, FALSE, SHIFTJIS_CHARSET,
|
||||
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
|
||||
L"MS Gothic");
|
||||
hWhiteBrush = GetStockBrush(WHITE_BRUSH);
|
||||
SendMessage(hwndCmd, WM_SETFONT, (WPARAM)hf, 0);
|
||||
SendMessage(hwndEdit, WM_SETFONT, (WPARAM)hf, 0);
|
||||
SendMessage(hwndCombo, WM_SETFONT, (WPARAM)hf, 0);
|
||||
SendMessage(hwndProcessComboBox, WM_SETFONT, (WPARAM)hf, 0);
|
||||
texts = new TextBuffer(hwndEdit);
|
||||
man->RegisterThreadCreateCallback(ThreadCreate);
|
||||
man->RegisterThreadRemoveCallback(ThreadRemove);
|
||||
man->RegisterThreadResetCallback(ThreadReset);
|
||||
TextThread* console = man->FindSingle(0);
|
||||
console->RegisterOutputCallBack(ThreadOutput, NULL);
|
||||
AddToCombo(*console, false);
|
||||
man->RegisterProcessAttachCallback(RegisterProcessList);
|
||||
man->RegisterProcessDetachCallback(RemoveProcessList);
|
||||
man->RegisterProcessNewHookCallback(RefreshProfileOnNewHook);
|
||||
man->RegisterAddRemoveLinkCallback(AddRemoveLink);
|
||||
man->RegisterConsoleCallback(ConsoleOutput);
|
||||
Host_Start();
|
||||
{
|
||||
static const WCHAR program_name[] = L"Interactive Text Hooker";
|
||||
//static const WCHAR program_version[] = L"3.0";
|
||||
static WCHAR version_info[256];
|
||||
std::swprintf(version_info, L"%s %s (%s)", program_name, program_version, build_date);
|
||||
man->AddConsoleOutput(version_info);
|
||||
man->AddConsoleOutput(InitMessage);
|
||||
}
|
||||
|
||||
if (background == 0)
|
||||
man->AddConsoleOutput(BackgroundMsg);
|
||||
}
|
||||
|
||||
return 0;
|
||||
case WM_COMMAND:
|
||||
{
|
||||
DWORD wmId, wmEvent, dwId;
|
||||
wmId = LOWORD(wParam);
|
||||
wmEvent = HIWORD(wParam);
|
||||
switch (wmEvent)
|
||||
{
|
||||
case EN_VSCROLL:
|
||||
{
|
||||
SCROLLBARINFO info = { sizeof(info) };
|
||||
GetScrollBarInfo(hwndEdit, OBJID_VSCROLL, &info);
|
||||
InvalidateRect(hwndEdit, 0, 1);
|
||||
ValidateRect(hwndEdit, &info.rcScrollBar);
|
||||
RedrawWindow(hwndEdit, 0, 0, RDW_ERASE);
|
||||
}
|
||||
break;
|
||||
case CBN_SELENDOK:
|
||||
{
|
||||
if ((HWND)lParam == hwndProcessComboBox)
|
||||
return 0;
|
||||
dwId = ComboBox_GetCurSel(hwndCombo);
|
||||
int len = ComboBox_GetLBTextLen(hwndCombo, dwId);
|
||||
if (len > 0)
|
||||
{
|
||||
LPWSTR pwcEntry = new WCHAR[len + 1];
|
||||
len = ComboBox_GetLBText(hwndCombo, dwId, pwcEntry);
|
||||
DWORD num = std::stoul(pwcEntry, NULL, 16);
|
||||
man->SelectCurrent(num);
|
||||
delete[] pwcEntry;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
case BN_CLICKED:
|
||||
ClickButton(hWnd, (HWND)lParam);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case WM_SETFOCUS:
|
||||
SetFocus(hwndEdit);
|
||||
return 0;
|
||||
case WM_SIZE:
|
||||
{
|
||||
WORD width = LOWORD(lParam);
|
||||
WORD height = HIWORD(lParam);
|
||||
DWORD l = width / 7;
|
||||
WORD h = HIWORD(GetDialogBaseUnits()); // height of the system font
|
||||
h = h + (h / 2);
|
||||
HDC hDC = GetDC(hWnd);
|
||||
RECT rc;
|
||||
GetClientRect(hWnd, &rc);
|
||||
FillRect(hDC, &rc, hWhiteBrush);
|
||||
ReleaseDC(hWnd, hDC);
|
||||
MoveWindow(hwndProcess, 0, 0, l, h, TRUE);
|
||||
MoveWindow(hwndOption, l * 1, 0, l, h, TRUE);
|
||||
MoveWindow(hwndTop, l * 2, 0, l, h, TRUE);
|
||||
MoveWindow(hwndClear, l * 3, 0, l, h, TRUE);
|
||||
MoveWindow(hwndRemoveLink, l * 4, 0, l, h, TRUE);
|
||||
MoveWindow(hwndRemoveHook, l * 5, 0, l, h, TRUE);
|
||||
MoveWindow(hwndSave, l * 6, 0, width - 6 * l, h, TRUE);
|
||||
l *= 2;
|
||||
MoveWindow(hwndProcessComboBox, 0, h, l, 200, TRUE);
|
||||
MoveWindow(hwndCmd, l, h, width - l, h, TRUE);
|
||||
MoveWindow(hwndCombo, 0, h * 2, width, 200, TRUE);
|
||||
h *= 3;
|
||||
MoveWindow(hwndEdit, 0, h, width, height - h, TRUE);
|
||||
}
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
man->RegisterThreadCreateCallback(0);
|
||||
man->RegisterThreadRemoveCallback(0);
|
||||
man->RegisterThreadResetCallback(0);
|
||||
man->RegisterProcessAttachCallback(0);
|
||||
man->RegisterProcessDetachCallback(0);
|
||||
//delete texts;
|
||||
SaveSettings();
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
default:
|
||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI FlushThread(LPVOID lParam)
|
||||
{
|
||||
TextBuffer* t = (TextBuffer*)lParam;
|
||||
while (t->Running())
|
||||
{
|
||||
t->Flush();
|
||||
Sleep(10);
|
||||
}
|
||||
return 0;
|
||||
}
|
20
gui/window.h
20
gui/window.h
@ -1,20 +0,0 @@
|
||||
/* Copyright (C) 2010-2012 kaosu (qiupf2000@gmail.com)
|
||||
* This file is part of the Interactive Text Hooker.
|
||||
|
||||
* Interactive Text Hooker is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ITH.h"
|
4
host/CLI/CMakeLists.txt
Normal file
4
host/CLI/CMakeLists.txt
Normal file
@ -0,0 +1,4 @@
|
||||
# The CLI isn't used by Textractor itself, but is here for other people that want to build projects on top of Textractor
|
||||
add_executable(TextractorCLI main.cpp)
|
||||
target_precompile_headers(TextractorCLI REUSE_FROM pch)
|
||||
target_link_libraries(TextractorCLI host)
|
40
host/CLI/main.cpp
Normal file
40
host/CLI/main.cpp
Normal file
@ -0,0 +1,40 @@
|
||||
#include "../host.h"
|
||||
#include "../hookcode.h"
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
_setmode(_fileno(stdout), _O_U16TEXT);
|
||||
_setmode(_fileno(stdin), _O_U16TEXT);
|
||||
wprintf_s(L"Usage: {'attach'|'detach'|hookcode} -Pprocessid\n");
|
||||
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(),
|
||||
HookCode::Generate(thread.hp, thread.tp.processId).c_str(),
|
||||
output.c_str()
|
||||
);
|
||||
fflush(stdout);
|
||||
return false;
|
||||
});
|
||||
wchar_t input[500] = {};
|
||||
while (fgetws(input, 500, stdin))
|
||||
{
|
||||
wchar_t command[500] = {};
|
||||
DWORD processId = 0;
|
||||
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 = HookCode::Parse(command)) Host::InsertHook(processId, hp.value());
|
||||
else ExitProcess(0);
|
||||
}
|
||||
ExitProcess(0);
|
||||
}
|
8
host/CMakeLists.txt
Normal file
8
host/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
||||
add_library(host
|
||||
host.cpp
|
||||
textthread.cpp
|
||||
hookcode.cpp
|
||||
)
|
||||
target_precompile_headers(host REUSE_FROM pch)
|
||||
|
||||
add_subdirectory(CLI)
|
304
host/hookcode.cpp
Normal file
304
host/hookcode.cpp
Normal file
@ -0,0 +1,304 @@
|
||||
#include "hookcode.h"
|
||||
#include "module.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::optional<HookParam> ParseRCode(std::wstring RCode)
|
||||
{
|
||||
std::wsmatch match;
|
||||
HookParam hp = {};
|
||||
hp.type |= DIRECT_READ;
|
||||
|
||||
// {S|Q|V|M}
|
||||
switch (RCode[0])
|
||||
{
|
||||
case L'S':
|
||||
break;
|
||||
case L'Q':
|
||||
hp.type |= USING_UNICODE;
|
||||
break;
|
||||
case L'V':
|
||||
hp.type |= USING_UTF8;
|
||||
break;
|
||||
case L'M':
|
||||
hp.type |= USING_UNICODE | HEX_DUMP;
|
||||
break;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
RCode.erase(0, 1);
|
||||
|
||||
// [null_length<]
|
||||
if (std::regex_search(RCode, match, std::wregex(L"^([0-9]+)<")))
|
||||
{
|
||||
hp.null_length = std::stoi(match[1]);
|
||||
RCode.erase(0, match[0].length());
|
||||
}
|
||||
|
||||
// [codepage#]
|
||||
if (std::regex_search(RCode, match, std::wregex(L"^([0-9]+)#")))
|
||||
{
|
||||
hp.codepage = std::stoi(match[1]);
|
||||
RCode.erase(0, match[0].length());
|
||||
}
|
||||
|
||||
// @addr
|
||||
if (!std::regex_match(RCode, match, std::wregex(L"@([[:xdigit:]]+)"))) return {};
|
||||
hp.address = std::stoull(match[1], nullptr, 16);
|
||||
return hp;
|
||||
}
|
||||
|
||||
std::optional<HookParam> ParseHCode(std::wstring HCode)
|
||||
{
|
||||
std::wsmatch match;
|
||||
HookParam hp = {};
|
||||
|
||||
// {A|B|W|H|S|Q|V|M}
|
||||
switch (HCode[0])
|
||||
{
|
||||
case L'A':
|
||||
hp.type |= BIG_ENDIAN;
|
||||
hp.length_offset = 1;
|
||||
break;
|
||||
case L'B':
|
||||
hp.length_offset = 1;
|
||||
break;
|
||||
case L'W':
|
||||
hp.type |= USING_UNICODE;
|
||||
hp.length_offset = 1;
|
||||
break;
|
||||
case L'H':
|
||||
hp.type |= USING_UNICODE | HEX_DUMP;
|
||||
hp.length_offset = 1;
|
||||
break;
|
||||
case L'S':
|
||||
hp.type |= USING_STRING;
|
||||
break;
|
||||
case L'Q':
|
||||
hp.type |= USING_STRING | USING_UNICODE;
|
||||
break;
|
||||
case L'V':
|
||||
hp.type |= USING_STRING | USING_UTF8;
|
||||
break;
|
||||
case L'M':
|
||||
hp.type |= USING_STRING | USING_UNICODE | HEX_DUMP;
|
||||
break;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
HCode.erase(0, 1);
|
||||
|
||||
if (hp.type & USING_STRING)
|
||||
{
|
||||
if (HCode[0] == L'F')
|
||||
{
|
||||
hp.type |= FULL_STRING;
|
||||
HCode.erase(0, 1);
|
||||
}
|
||||
|
||||
// [null_length<]
|
||||
if (std::regex_search(HCode, match, std::wregex(L"^([0-9]+)<")))
|
||||
{
|
||||
hp.null_length = std::stoi(match[1]);
|
||||
HCode.erase(0, match[0].length());
|
||||
}
|
||||
}
|
||||
|
||||
// [N]
|
||||
if (HCode[0] == L'N')
|
||||
{
|
||||
hp.type |= NO_CONTEXT;
|
||||
HCode.erase(0, 1);
|
||||
}
|
||||
|
||||
// [codepage#]
|
||||
if (std::regex_search(HCode, match, std::wregex(L"^([0-9]+)#")))
|
||||
{
|
||||
hp.codepage = std::stoi(match[1]);
|
||||
HCode.erase(0, match[0].length());
|
||||
}
|
||||
|
||||
// [padding+]
|
||||
if (std::regex_search(HCode, match, std::wregex(L"^([[:xdigit:]]+)\\+")))
|
||||
{
|
||||
hp.padding = std::stoull(match[1], nullptr, 16);
|
||||
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
|
||||
hp.offset = ConsumeHexInt();
|
||||
|
||||
// [*deref_offset1]
|
||||
if (HCode[0] == L'*')
|
||||
{
|
||||
hp.type |= DATA_INDIRECT;
|
||||
HCode.erase(0, 1);
|
||||
hp.index = ConsumeHexInt();
|
||||
}
|
||||
|
||||
// [:split_offset[*deref_offset2]]
|
||||
if (HCode[0] == L':')
|
||||
{
|
||||
hp.type |= USING_SPLIT;
|
||||
HCode.erase(0, 1);
|
||||
hp.split = ConsumeHexInt();
|
||||
|
||||
if (HCode[0] == L'*')
|
||||
{
|
||||
hp.type |= SPLIT_INDIRECT;
|
||||
HCode.erase(0, 1);
|
||||
hp.split_index = ConsumeHexInt();
|
||||
}
|
||||
}
|
||||
|
||||
// @addr[:module[:func]]
|
||||
if (!std::regex_match(HCode, match, std::wregex(L"^@([[:xdigit:]]+)(:.+?)?(:.+)?"))) return {};
|
||||
hp.address = std::stoull(match[1], nullptr, 16);
|
||||
if (match[2].matched)
|
||||
{
|
||||
hp.type |= MODULE_OFFSET;
|
||||
wcsncpy_s(hp.module, match[2].str().erase(0, 1).c_str(), MAX_MODULE_SIZE - 1);
|
||||
}
|
||||
if (match[3].matched)
|
||||
{
|
||||
hp.type |= FUNCTION_OFFSET;
|
||||
std::wstring func = match[3];
|
||||
strncpy_s(hp.function, std::string(func.begin(), func.end()).erase(0, 1).c_str(), MAX_MODULE_SIZE - 1);
|
||||
}
|
||||
|
||||
// ITH has registers offset by 4 vs AGTH: need this to correct
|
||||
if (hp.offset < 0) hp.offset -= 4;
|
||||
if (hp.split < 0) hp.split -= 4;
|
||||
|
||||
return hp;
|
||||
}
|
||||
|
||||
std::wstring HexString(int64_t num)
|
||||
{
|
||||
if (num < 0) return FormatString(L"-%I64X", -num);
|
||||
return FormatString(L"%I64X", num);
|
||||
}
|
||||
|
||||
std::wstring GenerateRCode(HookParam hp)
|
||||
{
|
||||
std::wstring RCode = L"R";
|
||||
|
||||
if (hp.type & USING_UNICODE)
|
||||
{
|
||||
if (hp.type & HEX_DUMP) RCode += L'M';
|
||||
else RCode += L'Q';
|
||||
if (hp.null_length != 0) RCode += std::to_wstring(hp.null_length) + L'<';
|
||||
}
|
||||
else
|
||||
{
|
||||
RCode += L'S';
|
||||
if (hp.null_length != 0) RCode += std::to_wstring(hp.null_length) + L'<';
|
||||
if (hp.codepage != 0) RCode += std::to_wstring(hp.codepage) + L'#';
|
||||
}
|
||||
|
||||
RCode += L'@' + HexString(hp.address);
|
||||
|
||||
return RCode;
|
||||
}
|
||||
|
||||
std::wstring GenerateHCode(HookParam hp, DWORD processId)
|
||||
{
|
||||
std::wstring HCode = L"H";
|
||||
|
||||
if (hp.type & USING_UNICODE)
|
||||
{
|
||||
if (hp.type & HEX_DUMP)
|
||||
{
|
||||
if (hp.type & USING_STRING) HCode += L'M';
|
||||
else HCode += L'H';
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hp.type & USING_STRING) HCode += L'Q';
|
||||
else HCode += L'W';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hp.type & USING_STRING) HCode += L'S';
|
||||
else if (hp.type & BIG_ENDIAN) HCode += L'A';
|
||||
else HCode += L'B';
|
||||
}
|
||||
|
||||
if (hp.type & FULL_STRING) HCode += L'F';
|
||||
|
||||
if (hp.null_length != 0) HCode += std::to_wstring(hp.null_length) + L'<';
|
||||
|
||||
if (hp.type & NO_CONTEXT) HCode += L'N';
|
||||
if (hp.text_fun || hp.filter_fun || hp.hook_fun || hp.length_fun) HCode += L'X'; // no AGTH equivalent
|
||||
|
||||
if (hp.codepage != 0 && !(hp.type & USING_UNICODE)) HCode += std::to_wstring(hp.codepage) + L'#';
|
||||
|
||||
if (hp.padding) HCode += HexString(hp.padding) + L'+';
|
||||
|
||||
if (hp.offset < 0) hp.offset += 4;
|
||||
if (hp.split < 0) hp.split += 4;
|
||||
|
||||
HCode += HexString(hp.offset);
|
||||
if (hp.type & DATA_INDIRECT) HCode += L'*' + HexString(hp.index);
|
||||
if (hp.type & USING_SPLIT) HCode += L':' + HexString(hp.split);
|
||||
if (hp.type & SPLIT_INDIRECT) HCode += L'*' + HexString(hp.split_index);
|
||||
|
||||
// Attempt to make the address relative
|
||||
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 = GetModuleFilename(processId, (HMODULE)info.AllocationBase))
|
||||
{
|
||||
hp.type |= MODULE_OFFSET;
|
||||
hp.address -= (uint64_t)info.AllocationBase;
|
||||
wcsncpy_s(hp.module, moduleName->c_str() + moduleName->rfind(L'\\') + 1, MAX_MODULE_SIZE - 1);
|
||||
}
|
||||
|
||||
HCode += L'@' + HexString(hp.address);
|
||||
if (hp.type & MODULE_OFFSET) HCode += L':' + std::wstring(hp.module);
|
||||
if (hp.type & FUNCTION_OFFSET) HCode += L':' + StringToWideString(hp.function);
|
||||
|
||||
return HCode;
|
||||
}
|
||||
}
|
||||
|
||||
namespace HookCode
|
||||
{
|
||||
std::optional<HookParam> Parse(std::wstring code)
|
||||
{
|
||||
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 Generate(HookParam hp, DWORD processId)
|
||||
{
|
||||
return hp.type & DIRECT_READ ? GenerateRCode(hp) : GenerateHCode(hp, processId);
|
||||
}
|
||||
|
||||
TEST(
|
||||
assert(StringToWideString(u8"こんにちは") == L"こんにちは"),
|
||||
assert(HexString(-12) == L"-C"),
|
||||
assert(HexString(12) == L"C"),
|
||||
assert(Parse(L"/HQN936#-c*C:C*1C@4AA:gdi.dll:GetTextOutA")),
|
||||
assert(Parse(L"/HQN936#-c*C:C*1C@4AA:gdi.dll:GetTextOutA /KF")),
|
||||
assert(Parse(L"HB4@0")),
|
||||
assert(Parse(L"/RS65001#@44")),
|
||||
assert(Parse(L"HQ@4")),
|
||||
assert(!Parse(L"/RW@44")),
|
||||
assert(!Parse(L"/HWG@33"))
|
||||
);
|
||||
}
|
9
host/hookcode.h
Normal file
9
host/hookcode.h
Normal 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);
|
||||
}
|
259
host/host.cpp
Normal file
259
host/host.cpp
Normal file
@ -0,0 +1,259 @@
|
||||
#include "host.h"
|
||||
#include "defs.h"
|
||||
#include "module.h"
|
||||
#include "hookcode.h"
|
||||
#include "../texthook/texthook.h"
|
||||
|
||||
extern const wchar_t* ALREADY_INJECTED;
|
||||
extern const wchar_t* NEED_32_BIT;
|
||||
extern const wchar_t* NEED_64_BIT;
|
||||
extern const wchar_t* INJECT_FAILED;
|
||||
extern const wchar_t* CONSOLE;
|
||||
extern const wchar_t* CLIPBOARD;
|
||||
|
||||
namespace
|
||||
{
|
||||
class ProcessRecord
|
||||
{
|
||||
public:
|
||||
ProcessRecord(DWORD processId, HANDLE pipe) :
|
||||
pipe(pipe),
|
||||
mappedFile(OpenFileMappingW(FILE_MAP_READ, FALSE, (ITH_SECTION_ + std::to_wstring(processId)).c_str())),
|
||||
view(*(const TextHook(*)[MAX_HOOK])MapViewOfFile(mappedFile, FILE_MAP_READ, 0, 0, HOOK_SECTION_SIZE / 2)), // jichi 1/16/2015: Changed to half to hook section size
|
||||
viewMutex(ITH_HOOKMAN_MUTEX_ + std::to_wstring(processId))
|
||||
{}
|
||||
|
||||
~ProcessRecord()
|
||||
{
|
||||
UnmapViewOfFile(view);
|
||||
}
|
||||
|
||||
TextHook GetHook(uint64_t addr)
|
||||
{
|
||||
if (!view) return {};
|
||||
std::scoped_lock lock(viewMutex);
|
||||
for (auto hook : view) if (hook.address == addr) return hook;
|
||||
return {};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void Send(T data)
|
||||
{
|
||||
static_assert(sizeof(data) < PIPE_BUFFER_SIZE);
|
||||
std::thread([=]
|
||||
{
|
||||
WriteFile(pipe, &data, sizeof(data), DUMMY, nullptr);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
Host::HookEventHandler OnHookFound = [](HookParam hp, std::wstring text)
|
||||
{
|
||||
Host::AddConsoleOutput(HookCode::Generate(hp) + L": " + text);
|
||||
};
|
||||
|
||||
private:
|
||||
HANDLE pipe;
|
||||
AutoHandle<> mappedFile;
|
||||
const TextHook(&view)[MAX_HOOK];
|
||||
WinMutex viewMutex;
|
||||
};
|
||||
|
||||
size_t HashThreadParam(ThreadParam tp) { return std::hash<int64_t>()(tp.processId + tp.addr) + std::hash<int64_t>()(tp.ctx + tp.ctx2); }
|
||||
Synchronized<std::unordered_map<ThreadParam, TextThread, Functor<HashThreadParam>>> textThreadsByParams;
|
||||
Synchronized<std::unordered_map<DWORD, ProcessRecord>> processRecordsByIds;
|
||||
|
||||
Host::ProcessEventHandler OnConnect, OnDisconnect;
|
||||
Host::ThreadEventHandler OnCreate, OnDestroy;
|
||||
|
||||
void RemoveThreads(std::function<bool(ThreadParam)> removeIf)
|
||||
{
|
||||
std::vector<TextThread*> threadsToRemove;
|
||||
for (auto& [tp, thread] : textThreadsByParams.Acquire().contents) if (removeIf(tp)) threadsToRemove.push_back(&thread);
|
||||
for (auto thread : threadsToRemove)
|
||||
{
|
||||
OnDestroy(*thread);
|
||||
textThreadsByParams->erase(thread->tp);
|
||||
}
|
||||
}
|
||||
|
||||
void CreatePipe()
|
||||
{
|
||||
std::thread([]
|
||||
{
|
||||
struct PipeCloser { void operator()(HANDLE h) { DisconnectNamedPipe(h); CloseHandle(h); } };
|
||||
AutoHandle<PipeCloser>
|
||||
hookPipe = CreateNamedPipeW(HOOK_PIPE, PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, PIPE_UNLIMITED_INSTANCES, 0, PIPE_BUFFER_SIZE, MAXDWORD, &allAccess),
|
||||
hostPipe = CreateNamedPipeW(HOST_PIPE, PIPE_ACCESS_OUTBOUND, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, PIPE_UNLIMITED_INSTANCES, PIPE_BUFFER_SIZE, 0, MAXDWORD, &allAccess);
|
||||
static AutoHandle<> pipeAvailableEvent = CreateEventW(&allAccess, FALSE, FALSE, PIPE_AVAILABLE_EVENT);
|
||||
SetEvent(pipeAvailableEvent);
|
||||
ConnectNamedPipe(hookPipe, nullptr);
|
||||
|
||||
BYTE buffer[PIPE_BUFFER_SIZE] = {};
|
||||
DWORD bytesRead, processId;
|
||||
ReadFile(hookPipe, &processId, sizeof(processId), &bytesRead, nullptr);
|
||||
processRecordsByIds->try_emplace(processId, processId, hostPipe);
|
||||
OnConnect(processId);
|
||||
|
||||
CreatePipe();
|
||||
|
||||
while (ReadFile(hookPipe, buffer, PIPE_BUFFER_SIZE, &bytesRead, nullptr))
|
||||
switch (*(HostNotificationType*)buffer)
|
||||
{
|
||||
case HOST_NOTIFICATION_FOUND_HOOK:
|
||||
{
|
||||
auto info = *(HookFoundNotif*)buffer;
|
||||
auto OnHookFound = processRecordsByIds->at(processId).OnHookFound;
|
||||
std::wstring wide = info.text;
|
||||
if (wide.size() > STRING) OnHookFound(info.hp, std::move(info.text));
|
||||
info.hp.type &= ~USING_UNICODE;
|
||||
if (auto converted = StringToWideString((char*)info.text, info.hp.codepage))
|
||||
if (converted->size() > STRING) OnHookFound(info.hp, std::move(converted.value()));
|
||||
if (auto converted = StringToWideString((char*)info.text, info.hp.codepage = CP_UTF8))
|
||||
if (converted->size() > STRING) OnHookFound(info.hp, std::move(converted.value()));
|
||||
}
|
||||
break;
|
||||
case HOST_NOTIFICATION_RMVHOOK:
|
||||
{
|
||||
auto info = *(HookRemovedNotif*)buffer;
|
||||
RemoveThreads([&](ThreadParam tp) { return tp.processId == processId && tp.addr == info.address; });
|
||||
}
|
||||
break;
|
||||
case HOST_NOTIFICATION_TEXT:
|
||||
{
|
||||
auto info = *(ConsoleOutputNotif*)buffer;
|
||||
Host::AddConsoleOutput(StringToWideString(info.message));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
auto tp = *(ThreadParam*)buffer;
|
||||
auto textThreadsByParams = ::textThreadsByParams.Acquire();
|
||||
auto thread = textThreadsByParams->find(tp);
|
||||
if (thread == textThreadsByParams->end())
|
||||
{
|
||||
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(thread->second);
|
||||
}
|
||||
thread->second.Push(buffer + sizeof(tp), bytesRead - sizeof(tp));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
RemoveThreads([&](ThreadParam tp) { return tp.processId == processId; });
|
||||
OnDisconnect(processId);
|
||||
processRecordsByIds->erase(processId);
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
namespace Host
|
||||
{
|
||||
void Start(ProcessEventHandler Connect, ProcessEventHandler Disconnect, ThreadEventHandler Create, ThreadEventHandler Destroy, TextThread::OutputCallback Output)
|
||||
{
|
||||
OnConnect = Connect;
|
||||
OnDisconnect = Disconnect;
|
||||
OnCreate = [Create](TextThread& thread) { Create(thread); thread.Start(); };
|
||||
OnDestroy = [Destroy](TextThread& thread) { thread.Stop(); Destroy(thread); };
|
||||
TextThread::Output = Output;
|
||||
|
||||
textThreadsByParams->try_emplace(console, console, HookParam{}, CONSOLE);
|
||||
OnCreate(GetThread(console));
|
||||
textThreadsByParams->try_emplace(clipboard, clipboard, HookParam{}, CLIPBOARD);
|
||||
OnCreate(GetThread(clipboard));
|
||||
|
||||
CreatePipe();
|
||||
|
||||
static AutoHandle<> clipboardUpdate = CreateEventW(nullptr, FALSE, TRUE, NULL);
|
||||
SetWindowsHookExW(WH_GETMESSAGE, [](int statusCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (statusCode == HC_ACTION && wParam == PM_REMOVE && ((MSG*)lParam)->message == WM_CLIPBOARDUPDATE) SetEvent(clipboardUpdate);
|
||||
return CallNextHookEx(NULL, statusCode, wParam, lParam);
|
||||
}, NULL, GetCurrentThreadId());
|
||||
std::thread([]
|
||||
{
|
||||
while (WaitForSingleObject(clipboardUpdate, INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
std::optional<std::wstring> clipboardText;
|
||||
for (int retry = 0; !clipboardText && retry < 3; ++retry) // retry loop in case something else is using the clipboard
|
||||
{
|
||||
Sleep(10);
|
||||
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();
|
||||
}
|
||||
|
||||
void InjectProcess(DWORD processId)
|
||||
{
|
||||
std::thread([processId]
|
||||
{
|
||||
if (processId == GetCurrentProcessId()) return;
|
||||
|
||||
WinMutex(ITH_HOOKMAN_MUTEX_ + std::to_wstring(processId));
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) return AddConsoleOutput(ALREADY_INJECTED);
|
||||
|
||||
if (AutoHandle<> process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId))
|
||||
{
|
||||
#ifdef _WIN64
|
||||
BOOL invalidProcess = FALSE;
|
||||
IsWow64Process(process, &invalidProcess);
|
||||
if (invalidProcess) return AddConsoleOutput(NEED_32_BIT);
|
||||
#endif
|
||||
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);
|
||||
if (AutoHandle<> thread = CreateRemoteThread(process, nullptr, 0, (LPTHREAD_START_ROUTINE)LoadLibraryW, remoteData, 0, nullptr)) WaitForSingleObject(thread, INFINITE);
|
||||
else if (GetLastError() == ERROR_ACCESS_DENIED) AddConsoleOutput(NEED_64_BIT); // https://stackoverflow.com/questions/16091141/createremotethread-access-denied
|
||||
VirtualFreeEx(process, remoteData, 0, MEM_RELEASE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AddConsoleOutput(INJECT_FAILED);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void DetachProcess(DWORD processId)
|
||||
{
|
||||
processRecordsByIds->at(processId).Send(HOST_COMMAND_DETACH);
|
||||
}
|
||||
|
||||
void InsertHook(DWORD processId, HookParam hp)
|
||||
{
|
||||
processRecordsByIds->at(processId).Send(InsertHookCmd(hp));
|
||||
}
|
||||
|
||||
void RemoveHook(DWORD processId, uint64_t address)
|
||||
{
|
||||
processRecordsByIds->at(processId).Send(RemoveHookCmd(address));
|
||||
}
|
||||
|
||||
void FindHooks(DWORD processId, SearchParam sp, HookEventHandler HookFound)
|
||||
{
|
||||
if (HookFound) processRecordsByIds->at(processId).OnHookFound = HookFound;
|
||||
processRecordsByIds->at(processId).Send(FindHookCmd(sp));
|
||||
}
|
||||
|
||||
TextThread& GetThread(ThreadParam tp)
|
||||
{
|
||||
return textThreadsByParams->at(tp);
|
||||
}
|
||||
|
||||
TextThread* GetThread(int64_t handle)
|
||||
{
|
||||
for (auto& [tp, thread] : textThreadsByParams.Acquire().contents) if (thread.handle == handle) return &thread;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AddConsoleOutput(std::wstring text)
|
||||
{
|
||||
GetThread(console).AddSentence(std::move(text));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user