Merge branch 'master' into master

This commit is contained in:
Niakr1s 2019-06-17 07:55:36 +03:00 committed by GitHub
commit 7b602393de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 277 additions and 190 deletions

View File

@ -118,7 +118,7 @@ bool ExtenWindow::eventFilter(QObject* target, QEvent* event)
void ExtenWindow::keyPressEvent(QKeyEvent* event) void ExtenWindow::keyPressEvent(QKeyEvent* event)
{ {
if (event->key() == Qt::Key_Delete && ui->extenList->currentItem() != nullptr) if (event->key() == Qt::Key_Delete && ui->extenList->currentItem())
{ {
Unload(ui->extenList->currentIndex().row()); Unload(ui->extenList->currentIndex().row());
Sync(); Sync();

View File

@ -29,7 +29,7 @@ namespace
TextHook GetHook(uint64_t addr) TextHook GetHook(uint64_t addr)
{ {
if (view == nullptr) return {}; if (!view) return {};
std::scoped_lock lock(viewMutex); std::scoped_lock lock(viewMutex);
for (auto hook : view) for (auto hook : view)
if (hook.address == addr) return hook; if (hook.address == addr) return hook;
@ -60,7 +60,7 @@ namespace
size_t HashThreadParam(ThreadParam tp) { return std::hash<int64_t>()(tp.processId + tp.addr) + std::hash<int64_t>()(tp.ctx + tp.ctx2); } 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>>, std::recursive_mutex> textThreadsByParams; Synchronized<std::unordered_map<ThreadParam, TextThread, Functor<HashThreadParam>>, std::recursive_mutex> textThreadsByParams;
Synchronized<std::unordered_map<DWORD, ProcessRecord>, std::recursive_mutex> processRecordsByIds; Synchronized<std::unordered_map<DWORD, ProcessRecord>> processRecordsByIds;
Host::ProcessEventHandler OnConnect, OnDisconnect; Host::ProcessEventHandler OnConnect, OnDisconnect;
Host::ThreadEventHandler OnCreate, OnDestroy; Host::ThreadEventHandler OnCreate, OnDestroy;
@ -102,10 +102,10 @@ namespace
case HOST_NOTIFICATION_FOUND_HOOK: case HOST_NOTIFICATION_FOUND_HOOK:
{ {
auto info = *(HookFoundNotif*)buffer; auto info = *(HookFoundNotif*)buffer;
auto& OnHookFound = processRecordsByIds->at(processId).OnHookFound; auto OnHookFound = processRecordsByIds->at(processId).OnHookFound;
std::wstring wide = info.text; std::wstring wide = info.text;
if (wide.size() > STRING) OnHookFound(info.hp, info.text); if (wide.size() > STRING) OnHookFound(info.hp, info.text);
info.hp.type = USING_STRING; info.hp.type &= ~USING_UNICODE;
if (auto converted = Util::StringToWideString((char*)info.text, Host::defaultCodepage)) if (auto converted = Util::StringToWideString((char*)info.text, Host::defaultCodepage))
if (converted->size() > STRING) OnHookFound(info.hp, converted.value()); if (converted->size() > STRING) OnHookFound(info.hp, converted.value());
info.hp.codepage = CP_UTF8; info.hp.codepage = CP_UTF8;
@ -132,7 +132,7 @@ namespace
auto textThread = textThreadsByParams->find(tp); auto textThread = textThreadsByParams->find(tp);
if (textThread == textThreadsByParams->end()) if (textThread == textThreadsByParams->end())
{ {
try { textThread = textThreadsByParams->try_emplace(tp, tp, Host::GetHookParam(tp)).first; } try { textThread = 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 catch (std::out_of_range) { continue; } // probably garbage data in pipe, try again
OnCreate(textThread->second); OnCreate(textThread->second);
} }
@ -158,8 +158,6 @@ namespace Host
OnDestroy = [Destroy](TextThread& thread) { thread.Stop(); Destroy(thread); }; OnDestroy = [Destroy](TextThread& thread) { thread.Stop(); Destroy(thread); };
TextThread::Output = Output; TextThread::Output = Output;
processRecordsByIds->try_emplace(console.processId, console.processId, INVALID_HANDLE_VALUE);
OnConnect(console.processId);
textThreadsByParams->try_emplace(console, console, HookParam{}, CONSOLE); textThreadsByParams->try_emplace(console, console, HookParam{}, CONSOLE);
OnCreate(GetThread(console)); OnCreate(GetThread(console));
textThreadsByParams->try_emplace(clipboard, clipboard, HookParam{}, CLIPBOARD); textThreadsByParams->try_emplace(clipboard, clipboard, HookParam{}, CLIPBOARD);
@ -233,16 +231,18 @@ namespace Host
processRecordsByIds->at(processId).Send(FindHookCmd(sp)); processRecordsByIds->at(processId).Send(FindHookCmd(sp));
} }
HookParam GetHookParam(ThreadParam tp)
{
return processRecordsByIds->at(tp.processId).GetHook(tp.addr).hp;
}
TextThread& GetThread(ThreadParam tp) TextThread& GetThread(ThreadParam tp)
{ {
return textThreadsByParams->at(tp); return textThreadsByParams->at(tp);
} }
TextThread* GetThread(int64_t handle)
{
auto textThreadsByParams = ::textThreadsByParams.Acquire();
auto thread = std::find_if(textThreadsByParams->begin(), textThreadsByParams->end(), [&](const auto& thread) { return thread.second.handle == handle; });
return thread != textThreadsByParams->end() ? &thread->second : nullptr;
}
void AddConsoleOutput(std::wstring text) void AddConsoleOutput(std::wstring text)
{ {
GetThread(console).AddSentence(std::move(text)); GetThread(console).AddSentence(std::move(text));

View File

@ -12,11 +12,12 @@ namespace Host
void InjectProcess(DWORD processId); void InjectProcess(DWORD processId);
void DetachProcess(DWORD processId); void DetachProcess(DWORD processId);
void InsertHook(DWORD processId, HookParam hp); void InsertHook(DWORD processId, HookParam hp);
void RemoveHook(DWORD processId, uint64_t address); void RemoveHook(DWORD processId, uint64_t address);
void FindHooks(DWORD processId, SearchParam sp, HookEventHandler HookFound = {}); void FindHooks(DWORD processId, SearchParam sp, HookEventHandler HookFound = {});
HookParam GetHookParam(ThreadParam tp); TextThread* GetThread(int64_t handle);
TextThread& GetThread(ThreadParam tp); TextThread& GetThread(ThreadParam tp);
void AddConsoleOutput(std::wstring text); void AddConsoleOutput(std::wstring text);

View File

@ -11,6 +11,7 @@
#include <QCheckBox> #include <QCheckBox>
#include <QSpinBox> #include <QSpinBox>
#include <QListWidget> #include <QListWidget>
#include <QDialogButtonBox>
#include <QMessageBox> #include <QMessageBox>
#include <QInputDialog> #include <QInputDialog>
#include <QFileDialog> #include <QFileDialog>
@ -21,7 +22,7 @@ extern const char* DETACH;
extern const char* ADD_HOOK; extern const char* ADD_HOOK;
extern const char* REMOVE_HOOKS; extern const char* REMOVE_HOOKS;
extern const char* SAVE_HOOKS; extern const char* SAVE_HOOKS;
extern const char* FIND_HOOKS; extern const char* SEARCH_FOR_HOOKS;
extern const char* SETTINGS; extern const char* SETTINGS;
extern const char* EXTENSIONS; extern const char* EXTENSIONS;
extern const char* SELECT_PROCESS; extern const char* SELECT_PROCESS;
@ -30,6 +31,7 @@ extern const char* SEARCH_GAME;
extern const char* PROCESSES; extern const char* PROCESSES;
extern const char* CODE_INFODUMP; extern const char* CODE_INFODUMP;
extern const char* HOOK_SEARCH_UNSTABLE_WARNING; extern const char* HOOK_SEARCH_UNSTABLE_WARNING;
extern const char* SEARCH_CJK;
extern const char* SEARCH_PATTERN; extern const char* SEARCH_PATTERN;
extern const char* SEARCH_DURATION; extern const char* SEARCH_DURATION;
extern const char* PATTERN_OFFSET; extern const char* PATTERN_OFFSET;
@ -66,7 +68,7 @@ MainWindow::MainWindow(QWidget *parent) :
{ ADD_HOOK, &MainWindow::AddHook }, { ADD_HOOK, &MainWindow::AddHook },
{ REMOVE_HOOKS, &MainWindow::RemoveHooks }, { REMOVE_HOOKS, &MainWindow::RemoveHooks },
{ SAVE_HOOKS, &MainWindow::SaveHooks }, { SAVE_HOOKS, &MainWindow::SaveHooks },
{ FIND_HOOKS, &MainWindow::FindHooks }, { SEARCH_FOR_HOOKS, &MainWindow::FindHooks },
{ SETTINGS, &MainWindow::Settings }, { SETTINGS, &MainWindow::Settings },
{ EXTENSIONS, &MainWindow::Extensions } { EXTENSIONS, &MainWindow::Extensions }
}) })
@ -232,6 +234,13 @@ DWORD MainWindow::GetSelectedProcessId()
std::array<InfoForExtension, 10> MainWindow::GetMiscInfo(TextThread& thread) std::array<InfoForExtension, 10> MainWindow::GetMiscInfo(TextThread& thread)
{ {
void(*AddSentence)(MainWindow*, int64_t, const wchar_t*) = [](MainWindow* This, int64_t number, const wchar_t* sentence)
{
std::wstring sentenceStr = sentence;
// pointer from Host::GetThread may not stay valid unless on main thread
QMetaObject::invokeMethod(This, [=]() mutable { if (TextThread* thread = Host::GetThread(number)) thread->AddSentence(std::move(sentenceStr)); });
};
return return
{ { { {
{ "current select", &thread == current }, { "current select", &thread == current },
@ -240,6 +249,8 @@ std::array<InfoForExtension, 10> MainWindow::GetMiscInfo(TextThread& thread)
{ "hook address", (int64_t)thread.tp.addr }, { "hook address", (int64_t)thread.tp.addr },
{ "text handle", thread.handle }, { "text handle", thread.handle },
{ "text name", (int64_t)thread.name.c_str() }, { "text name", (int64_t)thread.name.c_str() },
{ "this", (int64_t)this },
{ "void (*AddSentence)(void* this, int64_t number, const wchar_t* sentence)", (int64_t)AddSentence },
{ nullptr, 0 } // nullptr marks end of info array { nullptr, 0 } // nullptr marks end of info array
} }; } };
} }
@ -321,10 +332,11 @@ void MainWindow::RemoveHooks()
for (int i = 0; i < ui->ttCombo->count(); ++i) for (int i = 0; i < ui->ttCombo->count(); ++i)
{ {
ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i)); ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i));
if (tp.processId == GetSelectedProcessId()) hooks[tp.addr] = Host::GetHookParam(tp); if (tp.processId == GetSelectedProcessId()) hooks[tp.addr] = Host::GetThread(tp).hp;
} }
auto hookList = new QListWidget(this); auto hookList = new QListWidget(this);
hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint); hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint);
hookList->setAttribute(Qt::WA_DeleteOnClose);
hookList->setMinimumSize({ 300, 50 }); hookList->setMinimumSize({ 300, 50 });
hookList->setWindowTitle(DOUBLE_CLICK_TO_REMOVE_HOOK); hookList->setWindowTitle(DOUBLE_CLICK_TO_REMOVE_HOOK);
for (auto [address, hp] : hooks) for (auto [address, hp] : hooks)
@ -343,46 +355,63 @@ void MainWindow::RemoveHooks()
void MainWindow::SaveHooks() void MainWindow::SaveHooks()
{ {
if (auto processName = Util::GetModuleFilename(GetSelectedProcessId())) auto processName = Util::GetModuleFilename(GetSelectedProcessId());
{ if (!processName) return;
QHash<uint64_t, QString> hookCodes; QHash<uint64_t, QString> hookCodes;
for (int i = 0; i < ui->ttCombo->count(); ++i) for (int i = 0; i < ui->ttCombo->count(); ++i)
{ {
ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i)); ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i));
if (tp.processId == GetSelectedProcessId()) if (tp.processId == GetSelectedProcessId())
{ {
HookParam hp = Host::GetHookParam(tp); HookParam hp = Host::GetThread(tp).hp;
if (!(hp.type & HOOK_ENGINE)) hookCodes[tp.addr] = S(Util::GenerateCode(hp, tp.processId)); if (!(hp.type & HOOK_ENGINE)) hookCodes[tp.addr] = S(Util::GenerateCode(hp, tp.processId));
} }
} }
auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values(); auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values();
ThreadParam tp = current.load()->tp; ThreadParam tp = current.load()->tp;
if (tp.processId == GetSelectedProcessId()) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(Util::GenerateCode(Host::GetHookParam(tp), tp.processId))); if (tp.processId == GetSelectedProcessId()) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(Util::GenerateCode(Host::GetThread(tp).hp, tp.processId)));
QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8()); QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8());
} }
}
void MainWindow::FindHooks() void MainWindow::FindHooks()
{ {
QMessageBox::information(this, FIND_HOOKS, HOOK_SEARCH_UNSTABLE_WARNING); QMessageBox::information(this, SEARCH_FOR_HOOKS, HOOK_SEARCH_UNSTABLE_WARNING);
struct : QDialog
DWORD processId = GetSelectedProcessId();
SearchParam sp = {};
bool customSettings = false;
std::wregex filter(L".");
QDialog dialog(this, Qt::WindowCloseButtonHint);
QFormLayout layout(&dialog);
QCheckBox cjkCheckbox(&dialog);
layout.addRow(SEARCH_CJK, &cjkCheckbox);
QDialogButtonBox confirm(QDialogButtonBox::Ok | QDialogButtonBox::Help, &dialog);
layout.addRow(&confirm);
confirm.button(QDialogButtonBox::Ok)->setText(START_HOOK_SEARCH);
confirm.button(QDialogButtonBox::Help)->setText(SETTINGS);
connect(&confirm, &QDialogButtonBox::helpRequested, [&customSettings] { customSettings = true; });
connect(&confirm, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(&confirm, &QDialogButtonBox::helpRequested, &dialog, &QDialog::accept);
dialog.setWindowTitle(SEARCH_FOR_HOOKS);
if (dialog.exec() == QDialog::Rejected) return;
if (customSettings)
{ {
using QDialog::QDialog; QDialog dialog(this, Qt::WindowCloseButtonHint);
void launch() QFormLayout layout(&dialog);
{ QLineEdit patternInput(x64 ? "CC CC 48 89" : "CC CC 55 8B EC", &dialog);
auto layout = new QFormLayout(this); layout.addRow(SEARCH_PATTERN, &patternInput);
auto patternInput = new QLineEdit(x64 ? "CC CC 48 89" : "CC CC 55 8B EC", this);
layout->addRow(SEARCH_PATTERN, patternInput);
for (auto [value, label] : Array<std::tuple<int&, const char*>>{ for (auto [value, label] : Array<std::tuple<int&, const char*>>{
{ sp.searchTime = 20000, SEARCH_DURATION }, { sp.searchTime = 20000, SEARCH_DURATION },
{ sp.offset = 2, PATTERN_OFFSET }, { sp.offset = 2, PATTERN_OFFSET },
}) })
{ {
auto spinBox = new QSpinBox(this); auto spinBox = new QSpinBox(&dialog);
spinBox->setMaximum(INT_MAX); spinBox->setMaximum(INT_MAX);
spinBox->setValue(value); spinBox->setValue(value);
layout->addRow(label, spinBox); layout.addRow(label, spinBox);
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), [=, &value] { value = spinBox->value(); }); connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), [&value] (int newValue) { value = newValue; });
} }
for (auto [value, label] : Array<std::tuple<uintptr_t&, const char*>>{ for (auto [value, label] : Array<std::tuple<uintptr_t&, const char*>>{
{ sp.minAddress = 0, MIN_ADDRESS }, { sp.minAddress = 0, MIN_ADDRESS },
@ -390,94 +419,79 @@ void MainWindow::FindHooks()
{ sp.padding = 0, STRING_OFFSET } { sp.padding = 0, STRING_OFFSET }
}) })
{ {
auto input = new QLineEdit(QString::number(value, 16), this); auto input = new QLineEdit(QString::number(value, 16), &dialog);
layout->addRow(label, input); layout.addRow(label, input);
connect(input, &QLineEdit::textEdited, [&value](QString input) connect(input, &QLineEdit::textEdited, [&value](QString input)
{ {
bool ok; bool ok;
if (uintptr_t newValue = input.toULongLong(&ok, 16); ok) value = newValue; if (uintptr_t newValue = input.toULongLong(&ok, 16); ok) value = newValue;
}); });
} }
auto filterInput = new QLineEdit(this); QLineEdit filterInput(".", &dialog);
layout->addRow(HOOK_SEARCH_FILTER, filterInput); layout.addRow(HOOK_SEARCH_FILTER, &filterInput);
auto save = new QPushButton(START_HOOK_SEARCH, this); QPushButton startButton(START_HOOK_SEARCH, &dialog);
layout->addWidget(save); layout.addWidget(&startButton);
connect(save, &QPushButton::clicked, this, &QDialog::accept); connect(&startButton, &QPushButton::clicked, &dialog, &QDialog::accept);
connect(save, &QPushButton::clicked, [this, patternInput, filterInput] if (dialog.exec() == QDialog::Rejected) return;
{ QByteArray pattern = QByteArray::fromHex(patternInput.text().replace("??", QString::number(XX, 16)).toUtf8());
QByteArray pattern = QByteArray::fromHex(patternInput->text().replace("??", QString::number(XX, 16)).toUtf8());
if (pattern.size() < 3) return;
std::wregex filter(L".");
if (!filterInput->text().isEmpty()) try { filter = std::wregex(S(filterInput->text())); } catch (std::regex_error) {};
memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), 25)); memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), 25));
try { filter = std::wregex(S(filterInput.text())); } catch (std::regex_error) {};
}
else
{
// sp.length is 0 in this branch, so default will be used
filter = cjkCheckbox.isChecked() ? std::wregex(L"[\\u3000-\\ua000]{4,}") : std::wregex(L"[\\u0020-\\u1000]{4,}");
}
auto hooks = std::make_shared<QString>(); auto hooks = std::make_shared<QString>();
DWORD processId = this->processId;
try try
{ {
Host::FindHooks(processId, sp, [processId, hooks, filter](HookParam hp, const std::wstring& text) Host::FindHooks(processId, sp, [processId, hooks, filter](HookParam hp, const std::wstring& text)
{ {
if (std::regex_search(text, filter)) hooks->append(S(Util::GenerateCode(hp, processId)) + ": " + S(text) + "\n"); if (std::regex_search(text, filter)) hooks->append(S(Util::GenerateCode(hp, processId)) + ": " + S(text) + "\n");
}); });
} } catch (std::out_of_range) { return; }
catch (std::out_of_range) { return; } QString saveFile = QFileDialog::getSaveFileName(this, SAVE_SEARCH_RESULTS, "./Hooks.txt", TEXT_FILES);
QString fileName = QFileDialog::getSaveFileName(this, SAVE_SEARCH_RESULTS, "./Hooks.txt", TEXT_FILES); if (saveFile.isEmpty()) saveFile = "Hooks.txt";
if (fileName.isEmpty()) fileName = "Hooks.txt"; std::thread([hooks, saveFile]
std::thread([hooks, fileName]
{ {
for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) lastSize = hooks->size(); for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) lastSize = hooks->size();
QTextFile(fileName, QIODevice::WriteOnly | QIODevice::Truncate).write(hooks->toUtf8()); QTextFile(saveFile, QIODevice::WriteOnly | QIODevice::Truncate).write(hooks->toUtf8());
hooks->clear(); hooks->clear();
}).detach(); }).detach();
});
setWindowTitle(FIND_HOOKS);
exec();
}
SearchParam sp = {};
DWORD processId;
} searchDialog(this, Qt::WindowCloseButtonHint);
searchDialog.processId = GetSelectedProcessId();
searchDialog.launch();
} }
void MainWindow::Settings() void MainWindow::Settings()
{ {
struct : QDialog QDialog dialog(this, Qt::WindowCloseButtonHint);
{ QSettings settings(CONFIG_FILE, QSettings::IniFormat, &dialog);
using QDialog::QDialog; QFormLayout layout(&dialog);
void launch() QPushButton saveButton(SAVE_SETTINGS, &dialog);
{ layout.addWidget(&saveButton);
auto settings = new QSettings(CONFIG_FILE, QSettings::IniFormat, this);
auto layout = new QFormLayout(this);
auto save = new QPushButton(SAVE_SETTINGS, this);
layout->addWidget(save);
for (auto [value, label] : Array<std::tuple<int&, const char*>>{ for (auto [value, label] : Array<std::tuple<int&, const char*>>{
{ Host::defaultCodepage, DEFAULT_CODEPAGE }, { Host::defaultCodepage, DEFAULT_CODEPAGE },
{ TextThread::maxBufferSize, MAX_BUFFER_SIZE }, { TextThread::maxBufferSize, MAX_BUFFER_SIZE },
{ TextThread::flushDelay, FLUSH_DELAY }, { TextThread::flushDelay, FLUSH_DELAY },
}) })
{ {
auto spinBox = new QSpinBox(this); auto spinBox = new QSpinBox(&dialog);
spinBox->setMaximum(INT_MAX); spinBox->setMaximum(INT_MAX);
spinBox->setValue(value); spinBox->setValue(value);
layout->insertRow(0, label, spinBox); layout.insertRow(0, label, spinBox);
connect(save, &QPushButton::clicked, [=, &value] { settings->setValue(label, value = spinBox->value()); }); connect(&saveButton, &QPushButton::clicked, [spinBox, label, &settings, &value] { settings.setValue(label, value = spinBox->value()); });
} }
for (auto [value, label] : Array<std::tuple<bool&, const char*>>{ for (auto [value, label] : Array<std::tuple<bool&, const char*>>{
{ TextThread::filterRepetition, FILTER_REPETITION }, { TextThread::filterRepetition, FILTER_REPETITION },
}) })
{ {
auto checkBox = new QCheckBox(this); auto checkBox = new QCheckBox(&dialog);
checkBox->setChecked(value); checkBox->setChecked(value);
layout->insertRow(0, label, checkBox); layout.insertRow(0, label, checkBox);
connect(save, &QPushButton::clicked, [=, &value] { settings->setValue(label, value = checkBox->isChecked()); }); connect(&saveButton, &QPushButton::clicked, [checkBox, label, &settings, &value] { settings.setValue(label, value = checkBox->isChecked()); });
} }
connect(save, &QPushButton::clicked, this, &QDialog::accept); connect(&saveButton, &QPushButton::clicked, &dialog, &QDialog::accept);
setWindowTitle(SETTINGS); dialog.setWindowTitle(SETTINGS);
exec(); dialog.exec();
}
} settingsDialog(this, Qt::WindowCloseButtonHint);
settingsDialog.launch();
} }
void MainWindow::Extensions() void MainWindow::Extensions()

View File

@ -14,7 +14,7 @@ struct SentenceInfo
// nullptr marks end of info array // nullptr marks end of info array
int64_t operator[](std::string propertyName) int64_t operator[](std::string propertyName)
{ {
for (auto info = infoArray; info->name != nullptr; ++info) if (propertyName == info->name) return info->value; for (auto info = infoArray; info->name; ++info) if (propertyName == info->name) return info->value;
throw; throw;
} }

View File

@ -7,6 +7,10 @@
#include <QMenu> #include <QMenu>
#include <QLayout> #include <QLayout>
#include <QLabel> #include <QLabel>
#include <QFormLayout>
#include <QLineEdit>
#include <QSpinBox>
#include <QPushButton>
#include <QPainter> #include <QPainter>
#include <QMouseEvent> #include <QMouseEvent>
#include <QSettings> #include <QSettings>
@ -19,7 +23,11 @@ extern const char* SHOW_ORIGINAL_INFO;
extern const char* SIZE_LOCK; extern const char* SIZE_LOCK;
extern const char* BG_COLOR; extern const char* BG_COLOR;
extern const char* TEXT_COLOR; extern const char* TEXT_COLOR;
extern const char* FONT;
extern const char* FONT_SIZE; extern const char* FONT_SIZE;
extern const char* FONT_FAMILY;
extern const char* FONT_WEIGHT;
extern const char* SAVE_SETTINGS;
std::mutex m; std::mutex m;
@ -57,12 +65,32 @@ public:
display->setPalette(newPalette); display->setPalette(newPalette);
settings->setValue(TEXT_COLOR, color); settings->setValue(TEXT_COLOR, color);
}; };
auto setFontSize = [=](int pt) auto requestFont = [=]
{ {
QFont newFont = display->font(); QFont font = display->font();
newFont.setPointSize(pt); auto fontDialog = new QDialog(this, Qt::WindowCloseButtonHint);
display->setFont(newFont); fontDialog->setAttribute(Qt::WA_DeleteOnClose);
settings->setValue(FONT_SIZE, pt); fontDialog->setWindowTitle(FONT);
auto layout = new QFormLayout(fontDialog);
fontDialog->setLayout(layout);
auto fontFamily = new QLineEdit(font.family(), fontDialog);
layout->addRow(FONT_FAMILY, fontFamily);
auto fontSize = new QSpinBox(fontDialog);
fontSize->setValue(font.pointSize());
layout->addRow(FONT_SIZE, fontSize);
auto fontWeight = new QSpinBox(fontDialog);
fontWeight->setValue(font.weight());
layout->addRow(FONT_WEIGHT, fontWeight);
auto save = new QPushButton(SAVE_SETTINGS, fontDialog);
layout->addWidget(save);
connect(save, &QPushButton::clicked, fontDialog, &QDialog::accept);
fontDialog->open();
connect(fontDialog, &QDialog::accepted, [=]
{
QFont font(fontFamily->text(), fontSize->value(), fontWeight->value());
settings->setValue(FONT, font.toString());
display->setFont(font);
});
}; };
auto setTopmost = [=](bool topmost) auto setTopmost = [=](bool topmost)
{ {
@ -83,7 +111,10 @@ public:
setGeometry(settings->value(WINDOW, geometry()).toRect()); setGeometry(settings->value(WINDOW, geometry()).toRect());
setLock(settings->value(SIZE_LOCK, false).toBool()); setLock(settings->value(SIZE_LOCK, false).toBool());
setTopmost(settings->value(TOPMOST, false).toBool()); setTopmost(settings->value(TOPMOST, false).toBool());
setFontSize(settings->value(FONT_SIZE, 16).toInt()); QFont font = display->font();
font.setPointSize(16);
font.fromString(settings->value(FONT, font.toString()).toString());
display->setFont(font);
setBackgroundColor(settings->value(BG_COLOR, palette().window().color()).value<QColor>()); setBackgroundColor(settings->value(BG_COLOR, palette().window().color()).value<QColor>());
setTextColor(settings->value(TEXT_COLOR, display->palette().windowText().color()).value<QColor>()); setTextColor(settings->value(TEXT_COLOR, display->palette().windowText().color()).value<QColor>());
@ -99,7 +130,7 @@ public:
showOriginal->setChecked(settings->value(SHOW_ORIGINAL, true).toBool()); showOriginal->setChecked(settings->value(SHOW_ORIGINAL, true).toBool());
menu->addAction(BG_COLOR, [=] { setBackgroundColor(QColorDialog::getColor(bgColor, this, BG_COLOR, QColorDialog::ShowAlphaChannel)); }); menu->addAction(BG_COLOR, [=] { setBackgroundColor(QColorDialog::getColor(bgColor, this, BG_COLOR, QColorDialog::ShowAlphaChannel)); });
menu->addAction(TEXT_COLOR, [=] { setTextColor(QColorDialog::getColor(display->palette().windowText().color(), this, TEXT_COLOR, QColorDialog::ShowAlphaChannel)); }); menu->addAction(TEXT_COLOR, [=] { setTextColor(QColorDialog::getColor(display->palette().windowText().color(), this, TEXT_COLOR, QColorDialog::ShowAlphaChannel)); });
menu->addAction(FONT_SIZE, [=] { setFontSize(QInputDialog::getInt(this, FONT_SIZE, "", display->font().pointSize(), 0, INT_MAX, 1, nullptr, Qt::WindowCloseButtonHint)); }); menu->addAction(FONT, requestFont);
display->setContextMenuPolicy(Qt::CustomContextMenu); display->setContextMenuPolicy(Qt::CustomContextMenu);
connect(display, &QLabel::customContextMenuRequested, [=](QPoint point) { menu->exec(mapToGlobal(point)); }); connect(display, &QLabel::customContextMenuRequested, [=](QPoint point) { menu->exec(mapToGlobal(point)); });
connect(this, &QDialog::destroyed, [=] { settings->setValue(WINDOW, geometry()); }); connect(this, &QDialog::destroyed, [=] { settings->setValue(WINDOW, geometry()); });
@ -147,7 +178,7 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
case DLL_PROCESS_DETACH: case DLL_PROCESS_DETACH:
{ {
std::lock_guard l(m); std::lock_guard l(m);
if (window != nullptr) if (window)
{ {
window->settings->setValue(WINDOW, window->geometry()); window->settings->setValue(WINDOW, window->geometry());
window->settings->sync(); window->settings->sync();

View File

@ -127,7 +127,7 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
} }
lua_pushstring(L, WideStringToString(sentence).c_str()); lua_pushstring(L, WideStringToString(sentence).c_str());
lua_createtable(L, 0, 0); lua_createtable(L, 0, 0);
for (auto info = sentenceInfo.infoArray; info->name != nullptr; ++info) for (auto info = sentenceInfo.infoArray; info->name; ++info)
{ {
lua_pushstring(L, info->name); lua_pushstring(L, info->name);
lua_pushinteger(L, info->value); lua_pushinteger(L, info->value);

View File

@ -93,13 +93,11 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{ {
std::lock_guard l(m); std::lock_guard l(m);
static std::unordered_map<int64_t, std::wstring> queuedWritesByHandle; int64_t textHandle = sentenceInfo["text number"];
int64_t textHandle = sentenceInfo["text handle"];
for (auto linkedHandle : linkedTextHandles[textHandle]) queuedWritesByHandle[linkedHandle] += L"\n" + sentence; for (auto linkedHandle : linkedTextHandles[textHandle])
((void(*)(void*, int64_t, const wchar_t*))sentenceInfo["void (*AddSentence)(void* this, int64_t number, const wchar_t* sentence)"])
((void*)sentenceInfo["this"], linkedHandle, sentence.c_str());
if (queuedWritesByHandle[textHandle].empty()) return false; return false;
sentence += queuedWritesByHandle[textHandle];
queuedWritesByHandle[textHandle].clear();
return true;
} }

View File

@ -2,6 +2,7 @@
#include "network.h" #include "network.h"
#include <QTimer> #include <QTimer>
#include <QInputDialog> #include <QInputDialog>
#include <QFile>
extern const char* SELECT_LANGUAGE; extern const char* SELECT_LANGUAGE;
extern const char* SELECT_LANGUAGE_MESSAGE; extern const char* SELECT_LANGUAGE_MESSAGE;
@ -12,6 +13,8 @@ extern QStringList languages;
extern Synchronized<std::wstring> translateTo; extern Synchronized<std::wstring> translateTo;
std::pair<bool, std::wstring> Translate(const std::wstring& text); std::pair<bool, std::wstring> Translate(const std::wstring& text);
Synchronized<std::unordered_map<std::wstring, std::wstring>> translationCache;
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{ {
switch (ul_reason_for_call) switch (ul_reason_for_call)
@ -33,10 +36,21 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
.toStdWString() .toStdWString()
); );
}); });
QFile file(QString("%1 Cache.txt").arg(TRANSLATION_PROVIDER));
file.open(QIODevice::ReadOnly | QIODevice::Text);
QStringList savedCache = QString(file.readAll()).split("|T|\n", QString::SkipEmptyParts);
for (int i = 0; i < savedCache.size() - 1; i += 2)
translationCache->insert({ savedCache[i].toStdWString(), savedCache[i + 1].toStdWString() });
} }
break; break;
case DLL_PROCESS_DETACH: case DLL_PROCESS_DETACH:
{ {
QFile file(QString("%1 Cache.txt").arg(TRANSLATION_PROVIDER));
file.open(QIODevice::WriteOnly | QIODevice::Text);
auto translationCache = ::translationCache.Acquire();
for (const auto& [original, translation] : translationCache.contents)
file.write(QString::fromStdWString(FormatString(L"%s|T|\n%s|T|\n", original, translation)).toUtf8());
} }
break; break;
} }
@ -63,7 +77,6 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
const int tokenCount = 30, delay = 60 * 1000; const int tokenCount = 30, delay = 60 * 1000;
Synchronized<std::vector<DWORD>> tokens; Synchronized<std::vector<DWORD>> tokens;
} rateLimiter; } rateLimiter;
static Synchronized<std::unordered_map<std::wstring, std::wstring>> translationCache;
bool cache = false; bool cache = false;
std::wstring translation; std::wstring translation;

View File

@ -61,11 +61,12 @@ struct ThreadParam
struct SearchParam struct SearchParam
{ {
BYTE pattern[25] = {}; // pattern in memory to search for BYTE pattern[25]; // pattern in memory to search for
int length, // length of pattern int length, // length of pattern (zero means this SearchParam is invalid and the default should be used)
offset, // offset from start of pattern to add hook offset, // offset from start of pattern to add hook
searchTime; // ms searchTime; // ms
uintptr_t padding, minAddress, maxAddress; uintptr_t padding, minAddress, maxAddress;
void(*hookPostProcesser)(HookParam&);
}; };
struct InsertHookCmd // From host struct InsertHookCmd // From host

View File

@ -43,7 +43,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)
return FALSE; return FALSE;
}, 0), SW_SHOW); }, 0), SW_SHOW);
std::thread([] { while (true) Sleep(vars.at(0)), lstrlenW(L"こんにちは"); }).detach(); std::thread([] { while (true) Sleep(vars.at(0)), lstrlenW(L"こんにちは\n (Hello)"); }).detach();
STARTUPINFOW info = { sizeof(info) }; STARTUPINFOW info = { sizeof(info) };
wchar_t commandLine[] = { L"Textractor -p\"Test.exe\"" }; wchar_t commandLine[] = { L"Textractor -p\"Test.exe\"" };

View File

@ -12,7 +12,7 @@ const char* DETACH = u8"Detach from game";
const char* ADD_HOOK = u8"Add hook"; const char* ADD_HOOK = u8"Add hook";
const char* REMOVE_HOOKS = u8"Remove hook(s)"; const char* REMOVE_HOOKS = u8"Remove hook(s)";
const char* SAVE_HOOKS = u8"Save hook(s)"; const char* SAVE_HOOKS = u8"Save hook(s)";
const char* FIND_HOOKS = u8"Find hooks"; const char* SEARCH_FOR_HOOKS = u8"Search for hooks";
const char* SETTINGS = u8"Settings"; const char* SETTINGS = u8"Settings";
const char* EXTENSIONS = u8"Extensions"; const char* EXTENSIONS = u8"Extensions";
const char* SELECT_PROCESS = u8"Select process"; const char* SELECT_PROCESS = u8"Select process";
@ -51,6 +51,7 @@ const char* CONFIRM_EXTENSION_OVERWRITE = u8"Another version of this extension a
const char* EXTENSION_WRITE_ERROR = u8"Failed to save extension"; const char* EXTENSION_WRITE_ERROR = u8"Failed to save extension";
const char* USE_JP_LOCALE = u8"Emulate japanese locale?"; const char* USE_JP_LOCALE = u8"Emulate japanese locale?";
const char* HOOK_SEARCH_UNSTABLE_WARNING = u8"Searching for hooks is unstable! Be prepared for your game to crash!"; const char* HOOK_SEARCH_UNSTABLE_WARNING = u8"Searching for hooks is unstable! Be prepared for your game to crash!";
const char* SEARCH_CJK = u8"Search for Chinese/Japanese/Korean";
const char* SEARCH_PATTERN = u8"Search pattern (hex byte array)"; const char* SEARCH_PATTERN = u8"Search pattern (hex byte array)";
const char* SEARCH_DURATION = u8"Search duration (ms)"; const char* SEARCH_DURATION = u8"Search duration (ms)";
const char* PATTERN_OFFSET = u8"Offset from pattern start"; const char* PATTERN_OFFSET = u8"Offset from pattern start";
@ -115,7 +116,10 @@ Only works if this extension is used directly after a translation extension)";
const char* SIZE_LOCK = u8"Size Locked"; const char* SIZE_LOCK = u8"Size Locked";
const char* BG_COLOR = u8"Background Color"; const char* BG_COLOR = u8"Background Color";
const char* TEXT_COLOR = u8"Text Color"; const char* TEXT_COLOR = u8"Text Color";
const char* FONT = u8"Font";
const char* FONT_FAMILY = u8"Font Family";
const char* FONT_SIZE = u8"Font Size"; const char* FONT_SIZE = u8"Font Size";
const char* FONT_WEIGHT = u8"Font Weight";
const char* LUA_INTRO = u8R"(--[[ const char* LUA_INTRO = u8R"(--[[
ProcessSentence is called each time Textractor receives a sentence of text. ProcessSentence is called each time Textractor receives a sentence of text.

View File

@ -16818,7 +16818,7 @@ bool InsertVanillawareGCHook()
/** Artikash 6/7/2019 /** Artikash 6/7/2019
* PPSSPP JIT code has pointers, but they are all added to an offset before being used. * PPSSPP JIT code has pointers, but they are all added to an offset before being used.
Find that offset and report it to user so they can search for hooks properly. Find that offset so that hook searching works properly.
To find the offset, find a page of mapped memory with size 0x1f00000, read and write permissions, take its address and subtract 0x8000000. To find the offset, find a page of mapped memory with size 0x1f00000, read and write permissions, take its address and subtract 0x8000000.
The above is useful for emulating PSP hardware, so unlikely to change between versions. The above is useful for emulating PSP hardware, so unlikely to change between versions.
*/ */
@ -16839,7 +16839,13 @@ bool FindPPSSPP()
if (info.RegionSize == 0x1f00000 && info.Protect == PAGE_READWRITE && info.Type == MEM_MAPPED) if (info.RegionSize == 0x1f00000 && info.Protect == PAGE_READWRITE && info.Type == MEM_MAPPED)
{ {
found = true; found = true;
ConsoleOutput("Textractor: PPSSPP memory found: use pattern 79 0F C7 85 and pattern offset 0 and string offset 0x%p to search for hooks", probe - 0x8000000); ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes");
memcpy(spDefault.pattern, Array<BYTE>{ 0x79, 0x0f, 0xc7, 0x85 }, spDefault.length = 4);
spDefault.offset = 0;
spDefault.minAddress = 0;
spDefault.maxAddress = -1ULL;
spDefault.padding = (uintptr_t)probe - 0x8000000;
spDefault.hookPostProcesser = [](HookParam& hp) { hp.type |= NO_CONTEXT; };
} }
probe += info.RegionSize; probe += info.RegionSize;
} }

View File

@ -37,8 +37,8 @@ namespace Engine
void Hijack() void Hijack()
{ {
static bool hijacked = false; static auto _ = []
if (hijacked) return; {
GetModuleFileNameW(nullptr, processPath, MAX_PATH); GetModuleFileNameW(nullptr, processPath, MAX_PATH);
processName = wcsrchr(processPath, L'\\') + 1; processName = wcsrchr(processPath, L'\\') + 1;
@ -50,9 +50,12 @@ namespace Engine
processStopAddress = (uintptr_t)info.BaseAddress + info.RegionSize; processStopAddress = (uintptr_t)info.BaseAddress + info.RegionSize;
} while (info.Protect > PAGE_NOACCESS); } while (info.Protect > PAGE_NOACCESS);
processStopAddress -= info.RegionSize; processStopAddress -= info.RegionSize;
spDefault.minAddress = processStartAddress;
spDefault.maxAddress = processStopAddress;
ConsoleOutput("Textractor: hijacking process located from 0x%p to 0x%p", processStartAddress, processStopAddress);
DetermineEngineType(); DetermineEngineType();
hijacked = true; return NULL;
ConsoleOutput("Textractor: finished hijacking process located from 0x%p to 0x%p", processStartAddress, processStopAddress); }();
} }
} }

View File

@ -8,7 +8,7 @@ namespace Engine
{ {
/** Artikash 6/7/2019 /** Artikash 6/7/2019
* PPSSPP JIT code has pointers, but they are all added to an offset before being used. * PPSSPP JIT code has pointers, but they are all added to an offset before being used.
Find that offset and report it to user so they can search for hooks properly. Find that offset so that hook searching works properly.
To find the offset, find a page of mapped memory with size 0x1f00000, read and write permissions, take its address and subtract 0x8000000. To find the offset, find a page of mapped memory with size 0x1f00000, read and write permissions, take its address and subtract 0x8000000.
The above is useful for emulating PSP hardware, so unlikely to change between versions. The above is useful for emulating PSP hardware, so unlikely to change between versions.
*/ */
@ -29,7 +29,13 @@ namespace Engine
if (info.RegionSize == 0x1f00000 && info.Protect == PAGE_READWRITE && info.Type == MEM_MAPPED) if (info.RegionSize == 0x1f00000 && info.Protect == PAGE_READWRITE && info.Type == MEM_MAPPED)
{ {
found = true; found = true;
ConsoleOutput("Textractor: PPSSPP memory found: use pattern 79 10 41 C7 and pattern offset 0 and string offset 0x%p to search for hooks", probe - 0x8000000); ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes");
memcpy(spDefault.pattern, Array<BYTE>{ 0x79, 0x10, 0x41, 0xc7 }, spDefault.length = 4);
spDefault.offset = 0;
spDefault.minAddress = 0;
spDefault.maxAddress = -1ULL;
spDefault.padding = (uintptr_t)probe - 0x8000000;
spDefault.hookPostProcesser = [](HookParam& hp) { hp.type |= NO_CONTEXT; };
} }
probe += info.RegionSize; probe += info.RegionSize;
} }

View File

@ -11,7 +11,7 @@ extern WinMutex viewMutex;
namespace namespace
{ {
SearchParam current; SearchParam sp;
constexpr int CACHE_SIZE = 500'000; constexpr int CACHE_SIZE = 500'000;
struct HookRecord struct HookRecord
@ -23,7 +23,8 @@ namespace
hp.offset = offset; hp.offset = offset;
hp.type = USING_UNICODE | USING_STRING; hp.type = USING_UNICODE | USING_STRING;
hp.address = address; hp.address = address;
hp.padding = current.padding; hp.padding = sp.padding;
if (sp.hookPostProcesser) sp.hookPostProcesser(hp);
NotifyHookFound(hp, (wchar_t*)text); NotifyHookFound(hp, (wchar_t*)text);
} }
uint64_t address = 0; uint64_t address = 0;
@ -118,7 +119,7 @@ void Send(char** stack, uintptr_t address)
for (int i = -registers; i < 6; ++i) for (int i = -registers; i < 6; ++i)
{ {
int length = 0, sum = 0; int length = 0, sum = 0;
char* str = stack[i] + current.padding; char* str = stack[i] + sp.padding;
__try { for (; (str[length] || str[length + 1]) && length < 500; length += 2) sum += str[length] + str[length + 1]; } __try { for (; (str[length] || str[length + 1]) && length < 500; length += 2) sum += str[length] + str[length + 1]; }
__except (EXCEPTION_EXECUTE_HANDLER) {} __except (EXCEPTION_EXECUTE_HANDLER) {}
if (length > STRING && length < 499) if (length > STRING && length < 499)
@ -152,7 +153,7 @@ void Send(char** stack, uintptr_t address)
} }
} }
void SearchForHooks(SearchParam sp) void SearchForHooks(SearchParam spUser)
{ {
std::thread([=] std::thread([=]
{ {
@ -162,7 +163,7 @@ void SearchForHooks(SearchParam sp)
try { records = std::make_unique<HookRecord[]>(recordsAvailable = CACHE_SIZE); } try { records = std::make_unique<HookRecord[]>(recordsAvailable = CACHE_SIZE); }
catch (std::bad_alloc) { return ConsoleOutput("Textractor: SearchForHooks ERROR (out of memory)"); } catch (std::bad_alloc) { return ConsoleOutput("Textractor: SearchForHooks ERROR (out of memory)"); }
current = sp; sp = spUser.length == 0 ? spDefault : spUser;
uintptr_t moduleStartAddress = (uintptr_t)GetModuleHandleW(ITH_DLL); uintptr_t moduleStartAddress = (uintptr_t)GetModuleHandleW(ITH_DLL);
uintptr_t moduleStopAddress = moduleStartAddress; uintptr_t moduleStopAddress = moduleStartAddress;

View File

@ -14,6 +14,15 @@ void NotifyHookRemove(uint64_t addr, LPCSTR name);
void NewHook(HookParam hp, LPCSTR name, DWORD flag = HOOK_ENGINE); void NewHook(HookParam hp, LPCSTR name, DWORD flag = HOOK_ENGINE);
void RemoveHook(uint64_t addr, int maxOffset = 9); void RemoveHook(uint64_t addr, int maxOffset = 9);
inline SearchParam spDefault = []
{
SearchParam sp = {};
memcpy(sp.pattern, x64 ? Array<BYTE>{ 0xcc, 0xcc, 0x48, 0x89 } : Array<BYTE>{ 0xcc, 0xcc, 0x55, 0x8b, 0xec }, sp.length = x64 ? 4 : 5);
sp.offset = 2;
sp.searchTime = 20000;
return sp;
}();
extern "C" // minhook library extern "C" // minhook library
{ {
enum MH_STATUS enum MH_STATUS

View File

@ -327,11 +327,11 @@ int TextHook::GetLength(uintptr_t base, uintptr_t in)
int TextHook::HookStrlen(BYTE* data) int TextHook::HookStrlen(BYTE* data)
{ {
if (!hp.null_length) return hp.type & USING_UNICODE ? wcslen((wchar_t*)data) * 2 : strlen((char*)data);
BYTE* orig = data; BYTE* orig = data;
int nulls = hp.null_length ? hp.null_length : hp.type & USING_UNICODE ? 2 : 1; for (int nullsRemaining = hp.null_length; nullsRemaining > 0; ++data)
for (int nullsRemaining = nulls; nullsRemaining > 0; ++data)
if (*data == 0) nullsRemaining -= 1; if (*data == 0) nullsRemaining -= 1;
else nullsRemaining = nulls; else nullsRemaining = hp.null_length;
return data - orig; return data - orig;
} }