This commit is contained in:
Akash Mozumdar 2018-09-29 16:05:08 -04:00
parent baa7923be2
commit 23736478c0
19 changed files with 47 additions and 47 deletions

View File

@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.5)
set(MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.cmake/Modules") set(MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.cmake/Modules")
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${MODULE_DIR}) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${MODULE_DIR})
project(NextHooker) project(Textractor)
add_compile_options( add_compile_options(
/std:c++17 /std:c++17

View File

@ -2,7 +2,7 @@ include(QtUtils)
msvc_registry_search() msvc_registry_search()
find_qt5(Core Widgets) find_qt5(Core Widgets)
set(RESOURCE_FILES NextHooker.rc NextHooker.ico) set(RESOURCE_FILES Textractor.rc Textractor.ico)
add_compile_options(/GL) add_compile_options(/GL)
# Populate a CMake variable with the sources # Populate a CMake variable with the sources
set(gui_SRCS set(gui_SRCS

View File

@ -1 +0,0 @@
IDI_ICON1 ICON DISCARDABLE "NextHooker.ico"

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

1
GUI/Textractor.rc Normal file
View File

@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "Textractor.ico"

View File

@ -148,7 +148,7 @@ namespace Host
void Close() void Close()
{ {
// Artikash 7/25/2018: This is only called when NextHooker is closed, at which point Windows should free everything itself...right? // Artikash 7/25/2018: This is only called when Textractor is closed, at which point Windows should free everything itself...right?
#ifdef _DEBUG // Check memory leaks #ifdef _DEBUG // Check memory leaks
LOCK(hostMutex); LOCK(hostMutex);
OnRemove = [](TextThread* textThread) { delete textThread; }; OnRemove = [](TextThread* textThread) { delete textThread; };
@ -180,7 +180,7 @@ namespace Host
IsWow64Process(processHandle, &invalidProcess); IsWow64Process(processHandle, &invalidProcess);
if (invalidProcess) if (invalidProcess)
{ {
AddConsoleOutput(L"architecture mismatch: try 32 bit NextHooker instead"); AddConsoleOutput(L"architecture mismatch: try 32 bit Textractor instead");
CloseHandle(processHandle); CloseHandle(processHandle);
return false; return false;
} }

View File

@ -16,7 +16,7 @@ LONG WINAPI ExceptionHandler(EXCEPTION_POINTERS* exception)
L"Error address: " << (DWORD)exception->ExceptionRecord->ExceptionAddress << std::endl << L"Error address: " << (DWORD)exception->ExceptionRecord->ExceptionAddress << std::endl <<
L"Error in module: " << moduleName << std::endl << L"Error in module: " << moduleName << std::endl <<
L"Additional info: " << exception->ExceptionRecord->ExceptionInformation[1]; L"Additional info: " << exception->ExceptionRecord->ExceptionInformation[1];
MessageBoxW(NULL, errorMsg.str().c_str(), L"NextHooker ERROR", MB_ICONERROR); MessageBoxW(NULL, errorMsg.str().c_str(), L"Textractor ERROR", MB_ICONERROR);
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
} }

View File

@ -34,7 +34,7 @@ MainWindow::MainWindow(QWidget *parent) :
); );
ReloadExtensions(); ReloadExtensions();
Host::AddConsoleOutput(L"NextHooker beta v3.2.1 by Artikash\r\nSource code and more information available under GPLv3 at https://github.com/Artikash/NextHooker"); Host::AddConsoleOutput(L"Textractor beta v3.2.1 by Artikash\r\nSource code and more information available under GPLv3 at https://github.com/Artikash/Textractor");
} }
MainWindow::~MainWindow() MainWindow::~MainWindow()

View File

@ -52,7 +52,7 @@ private:
QVector<HookParam> GetAllHooks(DWORD processId); QVector<HookParam> GetAllHooks(DWORD processId);
Ui::MainWindow* ui; Ui::MainWindow* ui;
QSettings settings = QSettings("NextHooker.ini", QSettings::IniFormat); QSettings settings = QSettings("Textractor.ini", QSettings::IniFormat);
QComboBox* processCombo; QComboBox* processCombo;
QComboBox* ttCombo; QComboBox* ttCombo;
QComboBox* extenCombo; QComboBox* extenCombo;

View File

@ -17,7 +17,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>NextHooker</string> <string>Textractor</string>
</property> </property>
<property name="styleSheet"> <property name="styleSheet">
<string notr="true">QObject <string notr="true">QObject

View File

@ -1,10 +1,10 @@
# NextHooker # Textractor
## Overview ## Overview
*NextHooker* is an open-source x86/x64 text hooker for Windows. *Textractor* is an open-source x86/x64 text hooker for Windows.
![How it looks](https://media.discordapp.net/attachments/330538905072041994/486629608456847360/unknown.png?width=1083&height=353) ![How it looks](https://media.discordapp.net/attachments/330538905072041994/486629608456847360/unknown.png?width=1083&height=353)
@ -12,7 +12,7 @@ Basically, GUI text hooker based on [Stomp](http://www.hongfire.com/forum/showth
## Downloads ## Downloads
Releases of NextHooker can be found [here](https://github.com/Artikash/NextHooker/releases) Releases of Textractor can be found [here](https://github.com/Artikash/Textractor/releases)
Previous releases of ITHVNR can be found [here](https://github.com/mireado/ITHVNR/releases). Previous releases of ITHVNR can be found [here](https://github.com/mireado/ITHVNR/releases).
@ -32,8 +32,8 @@ See the extensions folder and my [Extensions project](https://github.com/Artikas
## Compiling ## Compiling
Before compiling *NextHooker*, you should get Visual Studio with CMake and ATL support, as well as Qt version 5.11<br> Before compiling *Textractor*, you should get Visual Studio with CMake and ATL support, as well as Qt version 5.11<br>
You should then be able to simply open the folder in Visual Studio, and build. Run Build/NextHooker.exe You should then be able to simply open the folder in Visual Studio, and build. Run Build/Textractor.exe
## Project Architecture ## Project Architecture
@ -54,7 +54,7 @@ GPL v3
## Developers ## Developers
- NextHooker creation/updating by [Me](https://github.com/Artikash) and [DoumanAsh](https://github.com/DoumanAsh) - Textractor creation/updating by [Me](https://github.com/Artikash) and [DoumanAsh](https://github.com/DoumanAsh)
- ITHVNR updating by [mireado](https://github.com/mireado) and [Eguni](https://github.com/Eguni) - ITHVNR updating by [mireado](https://github.com/mireado) and [Eguni](https://github.com/Eguni)
- ITHVNR new GUI & VNR engine migration by [Stomp](http://www.hongfire.com/forum/member/325894-stomp) - ITHVNR new GUI & VNR engine migration by [Stomp](http://www.hongfire.com/forum/member/325894-stomp)
- VNR engine making by [jichi](https://archive.is/prJwr) - VNR engine making by [jichi](https://archive.is/prJwr)

View File

@ -1,4 +1,4 @@
cd Builds/x86-Release/Build; cd Builds/x86-Release/Build;
Compress-Archive -Force -Path "NextHooker.exe","styles","platforms","Qt5Core.dll","Qt5Gui.dll","Qt5Widgets.dll","vnrhook.dll","1_Remove Repetition.dll","2_Copy to Clipboard.dll","3_Google Translate.dll","4_Extra Newlines.dll" -DestinationPath NextHooker; Compress-Archive -Force -Path "Textractor.exe","styles","platforms","Qt5Core.dll","Qt5Gui.dll","Qt5Widgets.dll","vnrhook.dll","1_Remove Repetition.dll","2_Copy to Clipboard.dll","3_Google Translate.dll","4_Extra Newlines.dll" -DestinationPath Textractor;
cd ../../x64-Release/Build; cd ../../x64-Release/Build;
Compress-Archive -Force -Path "NextHooker.exe","styles","platforms","Qt5Core.dll","Qt5Gui.dll","Qt5Widgets.dll","vnrhook.dll","1_Remove Repetition.dll","2_Copy to Clipboard.dll","3_Google Translate.dll","4_Extra Newlines.dll" -DestinationPath NextHooker; Compress-Archive -Force -Path "Textractor.exe","styles","platforms","Qt5Core.dll","Qt5Gui.dll","Qt5Widgets.dll","vnrhook.dll","1_Remove Repetition.dll","2_Copy to Clipboard.dll","3_Google Translate.dll","4_Extra Newlines.dll" -DestinationPath Textractor;

View File

@ -27,13 +27,13 @@ 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. * 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 NextHooker (UTF-16). * Param sentence: pointer to sentence received by Textractor (UTF-16).
* You should not modify this sentence. If you want NextHooker to receive a modified sentence, copy it into your own buffer and return that. * You should not modify this sentence. If you want Textractor to receive a modified sentence, copy it into your own buffer and return that.
* Please allocate the buffer using malloc() and not new[] or something else: NextHooker uses free() to free it. * Please allocate the buffer using malloc() and not new[] or something else: Textractor uses free() to free it.
* Param miscInfo: pointer to start of singly linked list containing misc info about the sentence. * Param miscInfo: pointer to start of singly linked list containing misc info about the sentence.
* Return value: pointer to sentence NextHooker takes for future processing and display. * Return value: pointer to sentence Textractor takes for future processing and display.
* Return 'sentence' unless you created a new sentence/buffer as mentioned above. * Return 'sentence' unless you created a new sentence/buffer as mentioned above.
* NextHooker will display the sentence after all extensions have had a chance to process and/or modify it. * Textractor will display the sentence after all extensions have had a chance to process and/or modify it.
* THIS FUNCTION MAY BE RUN SEVERAL TIMES CONCURRENTLY: PLEASE ENSURE THAT IT IS THREAD SAFE! * THIS FUNCTION MAY BE RUN SEVERAL TIMES CONCURRENTLY: PLEASE ENSURE THAT IT IS THREAD SAFE!
*/ */
extern "C" __declspec(dllexport) const wchar_t* OnNewSentence(const wchar_t* sentenceArr, const InfoForExtension* miscInfo) extern "C" __declspec(dllexport) const wchar_t* OnNewSentence(const wchar_t* sentenceArr, const InfoForExtension* miscInfo)
@ -41,7 +41,7 @@ extern "C" __declspec(dllexport) const wchar_t* OnNewSentence(const wchar_t* sen
std::wstring sentence(sentenceArr); std::wstring sentence(sentenceArr);
if (ProcessSentence(sentence, SentenceInfo{ miscInfo })) if (ProcessSentence(sentence, SentenceInfo{ miscInfo }))
{ {
// No need to worry about freeing this: NextHooker does it for you. // No need to worry about freeing this: Textractor does it for you.
wchar_t* newSentence = (wchar_t*)malloc((sentence.size() + 1) * sizeof(wchar_t*)); wchar_t* newSentence = (wchar_t*)malloc((sentence.size() + 1) * sizeof(wchar_t*));
wcscpy_s(newSentence, sentence.size() + 1, sentence.c_str()); wcscpy_s(newSentence, sentence.size() + 1, sentence.c_str());
return newSentence; return newSentence;

View File

@ -41,7 +41,7 @@ std::wstring GetTranslationUri(const wchar_t* text, unsigned int TKK)
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{ {
static HINTERNET internet = NULL; static HINTERNET internet = NULL;
if (!internet) internet = WinHttpOpen(L"Mozilla/5.0 NextHooker", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0); if (!internet) internet = WinHttpOpen(L"Mozilla/5.0 Textractor", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0);
static unsigned int TKK = 0; static unsigned int TKK = 0;
std::wstring translation; std::wstring translation;

View File

@ -56,7 +56,7 @@ namespace { // unnamed helpers
hp.type = DIRECT_READ; hp.type = DIRECT_READ;
if (funcAddr == funcW) hp.type |= USING_UNICODE; if (funcAddr == funcW) hp.type |= USING_UNICODE;
hp.address = addr; hp.address = addr;
ConsoleOutput("NextHooker: triggered: adding dynamic reader"); ConsoleOutput("Textractor: triggered: adding dynamic reader");
NewHook(hp, "READ"); NewHook(hp, "READ");
ret = true; ret = true;
} }
@ -2125,7 +2125,7 @@ bool InsertBaldrHook()
const BYTE ins[] = { 0x90,0xff,0x50,0x3c,0x83,0xc4,0x20,0x8b,0x45,0xec }; const BYTE ins[] = { 0x90,0xff,0x50,0x3c,0x83,0xc4,0x20,0x8b,0x45,0xec };
DWORD addr = Util::SearchMemory(ins, sizeof(ins)); DWORD addr = Util::SearchMemory(ins, sizeof(ins));
if (!addr) { if (!addr) {
ConsoleOutput("NextHooker: BALDR failed: could not find instructions"); ConsoleOutput("Textractor: BALDR failed: could not find instructions");
return false; return false;
} }
@ -2133,7 +2133,7 @@ bool InsertBaldrHook()
hp.address = addr; hp.address = addr;
hp.offset = 4; hp.offset = 4;
hp.type = NO_CONTEXT | USING_STRING | USING_UNICODE; // 0x403 hp.type = NO_CONTEXT | USING_STRING | USING_UNICODE; // 0x403
ConsoleOutput("NextHooker: INSERT BALDR"); ConsoleOutput("Textractor: INSERT BALDR");
NewHook(hp, "BALDR"); NewHook(hp, "BALDR");
return true; return true;
@ -5785,7 +5785,7 @@ bool InsertShinaHook()
if (ver >= 50) { if (ver >= 50) {
SwitchTrigger(true); SwitchTrigger(true);
trigger_fun_ = StackSearchingTrigger<GetGlyphOutlineA, NULL>; trigger_fun_ = StackSearchingTrigger<GetGlyphOutlineA, NULL>;
ConsoleOutput("NextHooker: ShinaRio 2.50+: adding trigger"); ConsoleOutput("Textractor: ShinaRio 2.50+: adding trigger");
return true; return true;
} }
else if (ver >= 48) { // v2.48, v2.49 else if (ver >= 48) { // v2.48, v2.49
@ -7768,7 +7768,7 @@ bool InsertQLIE3Hook()
}; };
ULONG addr = MemDbg::findBytes(bytes, sizeof(bytes), processStartAddress, processStopAddress); ULONG addr = MemDbg::findBytes(bytes, sizeof(bytes), processStartAddress, processStopAddress);
if (!addr) { if (!addr) {
ConsoleOutput("NextHooker:QLIE3: pattern not found"); ConsoleOutput("Textractor:QLIE3: pattern not found");
//ConsoleOutput("Not QLIE2"); //ConsoleOutput("Not QLIE2");
return false; return false;
} }
@ -7780,7 +7780,7 @@ bool InsertQLIE3Hook()
hp.split = pusha_edi_off - 4; hp.split = pusha_edi_off - 4;
hp.address = addr; hp.address = addr;
ConsoleOutput("NextHooker: INSERT QLIE3"); ConsoleOutput("Textractor: INSERT QLIE3");
NewHook(hp, "QLiE3"); NewHook(hp, "QLiE3");
//ConsoleOutput("QLIE2"); //ConsoleOutput("QLIE2");
return true; return true;
@ -9342,7 +9342,7 @@ static bool InsertNewWillPlusHook()
hp.type = USING_STRING | USING_UNICODE | DATA_INDIRECT; hp.type = USING_STRING | USING_UNICODE | DATA_INDIRECT;
hp.offset = pusha_ecx_off - 4; hp.offset = pusha_ecx_off - 4;
hp.index = 0; hp.index = 0;
ConsoleOutput("NextHooker: INSERT New WillPlus (ADVHD) hook"); ConsoleOutput("Textractor: INSERT New WillPlus (ADVHD) hook");
NewHook(hp, "WillPlus2"); NewHook(hp, "WillPlus2");
return true; return true;
} }
@ -16130,7 +16130,7 @@ bool InsertAIRNovelHook()
DWORD addr = MemDbg::findBytes(bytes, sizeof(bytes), base, base + 0x200000); // Artikash 7/14/2018: Probably big enough DWORD addr = MemDbg::findBytes(bytes, sizeof(bytes), base, base + 0x200000); // Artikash 7/14/2018: Probably big enough
if (!addr) if (!addr)
{ {
ConsoleOutput("NextHooker: AIRNovel: pattern not found"); ConsoleOutput("Textractor: AIRNovel: pattern not found");
return false; return false;
} }
HookParam hp = {}; HookParam hp = {};
@ -16153,7 +16153,7 @@ bool InsertAIRNovelHook()
// memcmp((char*)str, "app:/", 5); // memcmp((char*)str, "app:/", 5);
//}; //};
ConsoleOutput("NextHooker: INSERT AIRNovel"); ConsoleOutput("Textractor: INSERT AIRNovel");
NewHook(hp, "AIRNovel"); NewHook(hp, "AIRNovel");
return true; return true;
} }
@ -16434,7 +16434,7 @@ bool InsertRenpyHook()
hp.address = (DWORD)GetProcAddress(GetModuleHandleW(L"python27"), "PyUnicodeUCS2_Format"); hp.address = (DWORD)GetProcAddress(GetModuleHandleW(L"python27"), "PyUnicodeUCS2_Format");
if (!hp.address) if (!hp.address)
{ {
ConsoleOutput("NextHooker: Ren'py failed: failed to find python27.dll or PyUnicodeUCS2_Format"); ConsoleOutput("Textractor: Ren'py failed: failed to find python27.dll or PyUnicodeUCS2_Format");
return false; return false;
} }
hp.offset = 4; hp.offset = 4;

View File

@ -257,7 +257,7 @@ bool TextHook::UnsafeInsertHookCode()
if (hp.module && (hp.type & MODULE_OFFSET)) // Map hook offset to real address. if (hp.module && (hp.type & MODULE_OFFSET)) // Map hook offset to real address.
{ {
if (DWORD base = GetModuleBase(hp.module)) hp.address += base; if (DWORD base = GetModuleBase(hp.module)) hp.address += base;
else return ConsoleOutput("NextHooker: UnsafeInsertHookCode: FAILED: module not present"), false; else return ConsoleOutput("Textractor: UnsafeInsertHookCode: FAILED: module not present"), false;
hp.type &= ~MODULE_OFFSET; hp.type &= ~MODULE_OFFSET;
} }
@ -271,7 +271,7 @@ bool TextHook::UnsafeInsertHookCode()
} }
else else
{ {
ConsoleOutput(("NextHooker: UnsafeInsertHookCode: FAILED: error " + std::string(MH_StatusToString(err))).c_str()); ConsoleOutput(("Textractor: UnsafeInsertHookCode: FAILED: error " + std::string(MH_StatusToString(err))).c_str());
return false; return false;
} }
@ -310,13 +310,13 @@ DWORD WINAPI ReaderThread(LPVOID hookPtr)
{ {
if (!IthGetMemoryRange((void*)hook->hp.address, nullptr, nullptr)) if (!IthGetMemoryRange((void*)hook->hp.address, nullptr, nullptr))
{ {
ConsoleOutput("NextHooker: can't read desired address"); ConsoleOutput("Textractor: can't read desired address");
break; break;
} }
if (hook->hp.type & DATA_INDIRECT) currentAddress = *((char**)hook->hp.address + hook->hp.index); if (hook->hp.type & DATA_INDIRECT) currentAddress = *((char**)hook->hp.address + hook->hp.index);
if (!IthGetMemoryRange(currentAddress, nullptr, nullptr)) if (!IthGetMemoryRange(currentAddress, nullptr, nullptr))
{ {
ConsoleOutput("NextHooker: can't read desired address"); ConsoleOutput("Textractor: can't read desired address");
break; break;
} }
Sleep(500); Sleep(500);
@ -327,7 +327,7 @@ DWORD WINAPI ReaderThread(LPVOID hookPtr)
} }
if (++changeCount > 10) if (++changeCount > 10)
{ {
ConsoleOutput("NextHooker: memory constantly changing, useless to read"); ConsoleOutput("Textractor: memory constantly changing, useless to read");
break; break;
} }
@ -341,7 +341,7 @@ DWORD WINAPI ReaderThread(LPVOID hookPtr)
DWORD unused; DWORD unused;
WriteFile(::hookPipe, buffer, dataLen + sizeof(ThreadParam), &unused, nullptr); WriteFile(::hookPipe, buffer, dataLen + sizeof(ThreadParam), &unused, nullptr);
} }
ConsoleOutput("NextHooker: remove read code"); ConsoleOutput("Textractor: remove read code");
hook->ClearHook(); hook->ClearHook();
return 0; return 0;
} }
@ -377,7 +377,7 @@ void TextHook::RemoveReadCode()
void TextHook::ClearHook() void TextHook::ClearHook()
{ {
WaitForSingleObject(hmMutex, 0); WaitForSingleObject(hmMutex, 0);
if (hook_name) ConsoleOutput(("NextHooker: removing hook: " + std::string(hook_name)).c_str()); if (hook_name) ConsoleOutput(("Textractor: removing hook: " + std::string(hook_name)).c_str());
if (hp.type & DIRECT_READ) RemoveReadCode(); if (hp.type & DIRECT_READ) RemoveReadCode();
else RemoveHookCode(); else RemoveHookCode();
NotifyHookRemove(hp.address); NotifyHookRemove(hp.address);

View File

@ -70,14 +70,14 @@ void NewHook(const HookParam &hp, LPCSTR lpname, DWORD flag)
if (++currentHook < MAX_HOOK) if (++currentHook < MAX_HOOK)
{ {
if (name[0] == '\0') name = "UserHook" + std::to_string(userhookCount++); if (name[0] == '\0') name = "UserHook" + std::to_string(userhookCount++);
ConsoleOutput(("NextHooker: try inserting hook: " + name).c_str()); ConsoleOutput(("Textractor: try inserting hook: " + name).c_str());
// jichi 7/13/2014: This function would raise when too many hooks added // jichi 7/13/2014: This function would raise when too many hooks added
::hookman[currentHook].InitHook(hp, name.c_str(), flag); ::hookman[currentHook].InitHook(hp, name.c_str(), flag);
if (::hookman[currentHook].InsertHook()) ConsoleOutput(("NextHooker: inserted hook: " + name).c_str()); if (::hookman[currentHook].InsertHook()) ConsoleOutput(("Textractor: inserted hook: " + name).c_str());
else ConsoleOutput("NextHooker:WARNING: failed to insert hook"); else ConsoleOutput("Textractor:WARNING: failed to insert hook");
} }
else ConsoleOutput("NextHooker: too many hooks: can't insert"); else ConsoleOutput("Textractor: too many hooks: can't insert");
} }
void RemoveHook(uint64_t addr) void RemoveHook(uint64_t addr)

View File

@ -48,7 +48,7 @@ void CreatePipe()
*(DWORD*)buffer = GetCurrentProcessId(); *(DWORD*)buffer = GetCurrentProcessId();
WriteFile(::hookPipe, buffer, sizeof(DWORD), &count, nullptr); WriteFile(::hookPipe, buffer, sizeof(DWORD), &count, nullptr);
ConsoleOutput("NextHooker: pipe connected"); ConsoleOutput("Textractor: pipe connected");
#ifdef _WIN64 #ifdef _WIN64
ConsoleOutput("Hooks don't work on x64, only read codes work. Engine disabled."); ConsoleOutput("Hooks don't work on x64, only read codes work. Engine disabled.");
#else #else

View File

@ -297,7 +297,7 @@ namespace
} }
__except (1) __except (1)
{ {
ConsoleOutput("NextHooker: SearchMemory ERROR (NextHooker will likely still work fine, but please let Artikash know if this happens a lot!)"); ConsoleOutput("Textractor: SearchMemory ERROR (Textractor will likely still work fine, but please let Artikash know if this happens a lot!)");
return 0; return 0;
} }
return 0; return 0;