diff --git a/PTA/PTA.vcxproj b/PTA/PTA.vcxproj index 6c4afc8..c73773b 100644 --- a/PTA/PTA.vcxproj +++ b/PTA/PTA.vcxproj @@ -106,6 +106,7 @@ + @@ -137,6 +138,7 @@ + diff --git a/PTA/PTA.vcxproj.filters b/PTA/PTA.vcxproj.filters index ab67f02..2d0a573 100644 --- a/PTA/PTA.vcxproj.filters +++ b/PTA/PTA.vcxproj.filters @@ -61,6 +61,9 @@ Source Files + + Source Files + @@ -87,6 +90,9 @@ Header Files + + Header Files + diff --git a/PTA/Resources/client.png b/PTA/Resources/client.png new file mode 100644 index 0000000..678046c Binary files /dev/null and b/PTA/Resources/client.png differ diff --git a/PTA/clientmonitor.cpp b/PTA/clientmonitor.cpp new file mode 100644 index 0000000..1e015af --- /dev/null +++ b/PTA/clientmonitor.cpp @@ -0,0 +1,179 @@ +#include "clientmonitor.h" + +#include "pta_types.h" + +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +const std::chrono::milliseconds ClientMonitor::polling_rate = 350ms; + +ClientMonitor::ClientMonitor(QObject* parent) : QObject(parent), m_enabled(false), m_last_whisper(QString()) +{ + QSettings settings; + + QString logpath = settings.value(PTA_CONFIG_CLIENTLOG_PATH, QString()).toString(); + + if (logpath.isEmpty()) + { + qInfo() << "Client.txt path not set. Client features disabled."; + } + else + { + setPath(logpath); + } +} + +ClientMonitor::~ClientMonitor() +{ + m_watcher.reset(); +} + +void ClientMonitor::setPath(QString logpath) +{ + if (logpath.isEmpty()) + { + // If empty path, disable monitor + m_enabled = false; + + // Clear file watcher + if (m_watcher) + { + m_watcher.reset(); + } + + m_logpath.clear(); + + return; + } + + if (!QFileInfo::exists(logpath)) + { + qWarning() << "Client log file not found at" << logpath; + return; + } + + if (m_logpath == logpath) + { + // Same file + return; + } + + m_logpath = logpath; + + // Pre-read size + QFile log(m_logpath); + + if (!log.open(QIODevice::ReadOnly | QIODevice::Text)) + { + qWarning() << "Cannot process client log file found at" << m_logpath; + return; + } + + m_lastpos = log.size(); + + log.close(); + + m_watcher.reset(new QTimer(this)); + + connect(m_watcher.get(), &QTimer::timeout, this, &ClientMonitor::processLogChange); + m_watcher->start(polling_rate); + + m_enabled = true; + + qInfo() << "Client.txt set to" << m_logpath; +} + +bool ClientMonitor::enabled() +{ + return m_enabled; +} + +QString ClientMonitor::getLastWhisperer() +{ + return m_last_whisper; +} + +void ClientMonitor::processLogLine(QString line) +{ + auto parts = line.splitRef("] ", QString::SkipEmptyParts); + + if (parts.size() < 2) + { + // If not a game info line, skip + return; + } + + // Get last part + auto ltxt = parts[parts.size() - 1].trimmed().toString(); + + if (ltxt.startsWith('@')) + { + // Whisper + + // Remove whisper tags + ltxt.remove(QRegularExpression("^@(From|To) (<\\S+> )?")); + + auto msgparts = ltxt.splitRef(": ", QString::SkipEmptyParts); + + if (msgparts.size() < 1) + { + qWarning() << "Error parsing whisper text:" << ltxt; + return; + } + + auto pname = msgparts[0].toString(); + + m_last_whisper = pname; + } +} + +void ClientMonitor::processLogChange() +{ + if (!enabled()) + { + // Shouldn't ever get here + return; + } + + QFile log(m_logpath); + + if (!log.open(QIODevice::ReadOnly | QIODevice::Text)) + { + qWarning() << "Cannot process client log file found at" << m_logpath; + return; + } + + qint64 lastpos = log.size(); + + if (lastpos < m_lastpos) + { + // File got reset, so reset our position too + m_lastpos = lastpos; + return; + } + + if (lastpos == m_lastpos) + { + // No change + return; + } + + // Start reading from + log.seek(m_lastpos); + + QTextStream in(&log); + while (!in.atEnd()) + { + QString line = in.readLine(); + processLogLine(line); + } + + m_lastpos = lastpos; + + log.close(); +} diff --git a/PTA/clientmonitor.h b/PTA/clientmonitor.h new file mode 100644 index 0000000..16808b0 --- /dev/null +++ b/PTA/clientmonitor.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include + +class QTimer; + +class ClientMonitor : public QObject +{ + Q_OBJECT + +public: + ClientMonitor(QObject* parent = nullptr); + ~ClientMonitor(); + + void setPath(QString logpath); + + bool enabled(); + + QString getLastWhisperer(); + +private slots: + void processLogChange(); + void processLogLine(QString line); + +private: + static const std::chrono::milliseconds polling_rate; + + // We are FORCED to use a polling technique for Client.txt because it doesn't flush AT ALL + // unless the file is accessed externally. This renders functions like QFileSystemWatcher and + // even the WinAPI ReadDirectoryChangesW UNUSABLE for our purposes :(. + std::unique_ptr m_watcher; + + QString m_logpath; + + bool m_enabled; + qint64 m_lastpos = -1; + + QString m_last_whisper; +}; \ No newline at end of file diff --git a/PTA/configdialog.cpp b/PTA/configdialog.cpp index e28b191..c0db4d9 100644 --- a/PTA/configdialog.cpp +++ b/PTA/configdialog.cpp @@ -25,6 +25,7 @@ ConfigDialog::ConfigDialog(ItemAPI* api) pagesWidget->addWidget(new HotkeyPage(results)); pagesWidget->addWidget(new PriceCheckPage(results, api)); pagesWidget->addWidget(new MacrosPage(results)); + pagesWidget->addWidget(new ClientPage(results)); QPushButton* saveButton = new QPushButton(tr("Save")); QPushButton* closeButton = new QPushButton(tr("Close")); @@ -86,6 +87,12 @@ void ConfigDialog::createIcons() macroButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); macroButton->setIcon(QIcon(":/Resources/macros.png")); + QListWidgetItem* clientButton = new QListWidgetItem(contentsWidget); + clientButton->setText(tr("Client")); + clientButton->setTextAlignment(Qt::AlignHCenter); + clientButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + clientButton->setIcon(QIcon(":/Resources/client.png")); + connect(contentsWidget, &QListWidget::currentItemChanged, this, &ConfigDialog::changePage); } diff --git a/PTA/configpages.cpp b/PTA/configpages.cpp index 2b9450a..afafe31 100644 --- a/PTA/configpages.cpp +++ b/PTA/configpages.cpp @@ -213,7 +213,7 @@ PriceCheckPage::PriceCheckPage(json& set, ItemAPI* api, QWidget* parent) : QWidg QStringList leagues; - for (auto& league : api->getLeagues()) + for (const auto& league : api->getLeagues()) { leagues << QString::fromStdString(league.get()); } @@ -611,7 +611,7 @@ MacrosPage::MacrosPage(json& set, QWidget* parent) : QWidget(parent) set[PTA_CONFIG_CUSTOM_MACROS] = macrolist; - for (auto& [k, v] : macrolist.items()) + for (const auto& [k, v] : macrolist.items()) { auto key = QString::fromStdString(k); auto seq = QString::fromStdString(v["sequence"].get()); @@ -747,4 +747,56 @@ MacrosPage::MacrosPage(json& set, QWidget* parent) : QWidget(parent) layout->addLayout(buttonLayout); layout->addStretch(1); setLayout(layout); +} + +ClientPage::ClientPage(json& set, QWidget* parent) : QWidget(parent) +{ + QSettings settings; + + QGroupBox* configGroup = new QGroupBox(tr("Game Client")); + + // ------------------Client Log + QLabel* cliLabel = new QLabel(tr("Client Log location:")); + + QLineEdit* fedit = new QLineEdit; + fedit->setText(settings.value(PTA_CONFIG_CLIENTLOG_PATH, QString()).toString()); + + connect(fedit, &QLineEdit::textChanged, [=, &set](const QString& text) { + if (!text.isEmpty()) + { + set[PTA_CONFIG_CLIENTLOG_PATH] = text.toStdString(); + } + }); + + QPushButton* browseButton = new QPushButton(tr("Browse")); + connect(browseButton, &QAbstractButton::clicked, [=]() { + QString defpath = QDir::currentPath(); + + QString fname = QFileDialog::getOpenFileName(this, tr("Load Client.txt"), defpath, tr("Client.txt (Client.txt)")); + + if (!fname.isNull()) + { + QFileInfo info(fname); + + if (info.exists()) + { + fedit->setText(info.absoluteFilePath()); + } + } + }); + + QHBoxLayout* cliLayout = new QHBoxLayout; + cliLayout->addWidget(cliLabel); + cliLayout->addWidget(fedit); + cliLayout->addWidget(browseButton); + + QVBoxLayout* configLayout = new QVBoxLayout; + configLayout->addLayout(cliLayout); + + configGroup->setLayout(configLayout); + + QVBoxLayout* mainLayout = new QVBoxLayout; + mainLayout->addWidget(configGroup); + mainLayout->addStretch(1); + setLayout(mainLayout); } \ No newline at end of file diff --git a/PTA/configpages.h b/PTA/configpages.h index 74e0f36..116e200 100644 --- a/PTA/configpages.h +++ b/PTA/configpages.h @@ -48,3 +48,11 @@ class MacrosPage : public QWidget public: MacrosPage(json& set, QWidget* parent = 0); }; + +class ClientPage : public QWidget +{ + Q_OBJECT + +public: + ClientPage(json& set, QWidget* parent = 0); +}; diff --git a/PTA/itemapi.cpp b/PTA/itemapi.cpp index d86e579..d91a936 100644 --- a/PTA/itemapi.cpp +++ b/PTA/itemapi.cpp @@ -99,14 +99,14 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) json excj = json::parse(edat.toStdString()); - for (auto& e : excj["excludes"]) + for (const auto& e : excj["excludes"]) { c_excludes.insert(e.get()); } } else if (synchronizedGetJSON(QNetworkRequest(u_pta_excludes), data)) { - for (auto& e : data["excludes"]) + for (const auto& e : data["excludes"]) { c_excludes.insert(e.get()); } @@ -127,11 +127,11 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) auto& stt = data["result"]; - for (auto& type : stt) + for (const auto& type : stt) { auto& el = type["entries"]; - for (auto& et : el) + for (const auto& et : el) { // Cut the key for multiline mods std::string::size_type nl; @@ -162,11 +162,11 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) auto& itm = data["result"]; - for (auto& type : itm) + for (const auto& type : itm) { - auto& el = type["entries"]; + const auto& el = type["entries"]; - for (auto& et : el) + for (const auto& et : el) { if (et.contains("name")) { @@ -213,7 +213,7 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) throw std::runtime_error("Failed to download base item data"); } - for (auto& [k, o] : data.items()) + for (const auto& [k, o] : data.items()) { std::string typeName = o["name"].get(); std::string itemClass = o["item_class"].get(); @@ -242,7 +242,7 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) throw std::runtime_error("Failed to download mod type data"); } - for (auto& [k, o] : data.items()) + for (const auto& [k, o] : data.items()) { std::string modname = o["name"].get(); std::string modtype = o["generation_type"].get(); @@ -329,14 +329,14 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) json wlr = json::parse(wdat.toStdString()); - for (auto& e : wlr["data"]) + for (const auto& e : wlr["data"]) { c_weaponLocals.insert(e.get()); } } else if (synchronizedGetJSON(QNetworkRequest(u_pta_weaponlocals), data)) { - for (auto& e : data["data"]) + for (const auto& e : data["data"]) { c_weaponLocals.insert(e.get()); } @@ -357,14 +357,14 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) json alr = json::parse(adat.toStdString()); - for (auto& e : alr["data"]) + for (const auto& e : alr["data"]) { c_armourLocals.insert(e.get()); } } else if (synchronizedGetJSON(QNetworkRequest(u_pta_armourlocals), data)) { - for (auto& e : data["data"]) + for (const auto& e : data["data"]) { c_armourLocals.insert(e.get()); } @@ -386,9 +386,9 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) json dcr = json::parse(ddat.toStdString()); - for (auto [entry, list] : dcr.items()) + for (const auto [entry, list] : dcr.items()) { - for (auto value : list["unused"]) + for (const auto value : list["unused"]) { c_discriminators[entry].insert(value.get()); } @@ -396,9 +396,9 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) } else if (synchronizedGetJSON(QNetworkRequest(u_pta_disc), data)) { - for (auto [entry, list] : data.items()) + for (const auto [entry, list] : data.items()) { - for (auto value : list["unused"]) + for (const auto value : list["unused"]) { c_discriminators[entry].insert(value.get()); } @@ -431,7 +431,7 @@ ItemAPI::ItemAPI(QObject* parent) : QObject(parent) throw std::runtime_error("Cannot open currency.json"); } - for (auto& [k, v] : c_currencyMap.items()) + for (const auto& [k, v] : c_currencyMap.items()) { c_currencyCodes.insert(v.get()); } @@ -527,7 +527,7 @@ socket_filters_t ItemAPI::readSockets(QString prop) ss.links = socks.length(); } - for (auto& s : socks) + for (const auto& s : socks) { if ("R" == s) { @@ -1643,13 +1643,13 @@ PItem* ItemAPI::parse(QString itemText) // Process special/pseudo rules if (item->filters.size()) { - for (auto [key, fil] : item->filters.items()) + for (const auto [key, fil] : item->filters.items()) { if (c_pseudoRules.contains(key)) { - auto& rules = c_pseudoRules[key]; + const auto& rules = c_pseudoRules[key]; - for (auto& r : rules) + for (const auto& r : rules) { std::string pid = r["id"].get(); @@ -1661,7 +1661,7 @@ PItem* ItemAPI::parse(QString itemText) ps_entry["value"] = json::array(); - for (auto v : fil["value"]) + for (const auto v : fil["value"]) { if (v.is_number_float()) { @@ -1749,7 +1749,7 @@ QString ItemAPI::toJson(PItem* item) { j["influences"] = json::array(); - for (auto i : item->f_misc.influences) + for (const auto i : item->f_misc.influences) { std::string inf = i; inf[0] = toupper(inf[0]); @@ -2347,7 +2347,7 @@ void ItemAPI::advancedPriceCheck(std::shared_ptr item) if (misc.contains("influences")) { - for (auto [key, value] : misc["influences"].items()) + for (const auto [key, value] : misc["influences"].items()) { if (!key.empty() && value.get()) { diff --git a/PTA/itemapi.h b/PTA/itemapi.h index d99ec89..e970cbf 100644 --- a/PTA/itemapi.h +++ b/PTA/itemapi.h @@ -23,8 +23,8 @@ class ItemAPI : public QObject public: ItemAPI(QObject* parent = nullptr); - json getLeagues() { return m_leagues; } - QString getLeague(); + const json getLeagues() { return m_leagues; } + QString getLeague(); PItem* parse(QString itemText); @@ -35,8 +35,8 @@ class ItemAPI : public QObject void openWiki(std::shared_ptr item); signals: - void humour(QString msg); - void priceCheckFinished(std::shared_ptr item, QString results); + void humour(const QString& msg); + void priceCheckFinished(std::shared_ptr item, const QString& results); private: int readPropInt(QString prop); diff --git a/PTA/macrohandler.cpp b/PTA/macrohandler.cpp index fddd581..e4f768a 100644 --- a/PTA/macrohandler.cpp +++ b/PTA/macrohandler.cpp @@ -1,16 +1,23 @@ #include "macrohandler.h" +#include "clientmonitor.h" #include "pta_types.h" #include "putil.h" #include #include #include +#include #include #include -MacroHandler::MacroHandler(QObject* parent) : QObject(parent) +using namespace std::placeholders; + +MacroHandler::MacroHandler(ClientMonitor* client, QObject* parent) : QObject(parent), m_client(client) { + // Setup variables + m_variables.insert("last_whisper", std::bind(&ClientMonitor::getLastWhisperer, m_client)); + QSettings settings; auto macstr = settings.value(PTA_CONFIG_CUSTOM_MACROS).toString().toStdString(); @@ -29,7 +36,7 @@ void MacroHandler::setMacros(json macrolist) m_macrolist = macrolist; - for (auto& [k, v] : macrolist.items()) + for (const auto& [k, v] : macrolist.items()) { auto key = QString::fromStdString(k); auto seq = QString::fromStdString(v["sequence"].get()); @@ -91,6 +98,50 @@ void MacroHandler::insertChatCommand(std::vector& keystrokes, std::string insertKeyPress(keystrokes, VK_RETURN); } +bool MacroHandler::processChatCommand(std::string& command) +{ + QString qcmd = QString::fromStdString(command); + + QRegularExpression re("!(\\w+)!"); + QRegularExpressionMatch match = re.match(qcmd); + + if (match.hasMatch()) + { + if (!m_client->enabled()) + { + qWarning() << "Client features unavailable. Please set Client.txt path in settings to enable."; + emit humour(tr("Client features unavailable. Please set Client.txt path in settings to enable.")); + + return false; + } + + QString var = match.captured(1); + + if (!m_variables.contains(var)) + { + qWarning() << "Command variable" << var << "not found."; + emit humour(tr("Command variable not found. See log for more details.")); + + return false; + } + + QString getvar = m_variables[var](); + + if (getvar.isEmpty()) + { + qWarning() << "Failed to retrieve variable" << var << ". Variable returned empty string."; + emit humour(tr("Failed to retrieve variable. See log for more details.")); + return false; + } + + qcmd.replace(re, getvar); + } + + command = qcmd.toStdString(); + + return true; +} + void MacroHandler::sendChatCommand(std::string command) { std::vector keystroke; @@ -132,7 +183,10 @@ void MacroHandler::handleMacro(QString key) { case MACRO_TYPE_CHAT: { - sendChatCommand(command); + if (processChatCommand(command)) + { + sendChatCommand(command); + } break; } diff --git a/PTA/macrohandler.h b/PTA/macrohandler.h index 212108a..591ff2f 100644 --- a/PTA/macrohandler.h +++ b/PTA/macrohandler.h @@ -2,33 +2,45 @@ #include +#include + #include #include using json = nlohmann::json; +class ClientMonitor; + class MacroHandler : public QObject { Q_OBJECT public: - MacroHandler(QObject* parent = nullptr); + MacroHandler(ClientMonitor* client, QObject* parent = nullptr); void setMacros(json macrolist); void clearMacros(); +signals: + void humour(const QString& msg); + public slots: void handleForegroundChange(bool isPoe); private: void insertKeyPress(std::vector& keystrokes, WORD key); void insertChatCommand(std::vector& keystrokes, std::string command); + bool processChatCommand(std::string& command); void sendChatCommand(std::string command); private slots: void handleMacro(QString key); private: + QMap> m_variables; + + ClientMonitor* m_client; + json m_macrolist; std::vector> m_macros; }; \ No newline at end of file diff --git a/PTA/pta.cpp b/PTA/pta.cpp index a94d504..d701b4f 100644 --- a/PTA/pta.cpp +++ b/PTA/pta.cpp @@ -25,7 +25,12 @@ namespace } PTA::PTA(LogWindow* log, QWidget* parent) : - QMainWindow(parent), m_logWindow(log), m_inputhandler(this), m_macrohandler(this), m_netmanager(new QNetworkAccessManager(this)) + QMainWindow(parent), + m_logWindow(log), + m_inputhandler(this), + m_clientmonitor(this), + m_macrohandler(&m_clientmonitor, this), + m_netmanager(new QNetworkAccessManager(this)) { if (nullptr == m_logWindow) { @@ -110,7 +115,7 @@ void PTA::showToolTip(QString message) QToolTip::showText(QCursor::pos() + QPoint(5, 20), message); } -void PTA::showPriceResults(std::shared_ptr item, QString results) +void PTA::showPriceResults(std::shared_ptr item, const QString& results) { #ifndef NDEBUG qDebug() << "Prices copied to clipboard"; @@ -189,9 +194,12 @@ void PTA::createActions() void PTA::setupFunctionality() { + // UI connect(m_api, &ItemAPI::humour, this, &PTA::showToolTip); connect(m_api, &ItemAPI::priceCheckFinished, this, &PTA::showPriceResults); + connect(&m_macrohandler, &MacroHandler::humour, this, &PTA::showToolTip); + // Hotkeys QSettings settings; @@ -322,7 +330,7 @@ void PTA::saveSettings(int result) QSettings settings; - for (auto& [k, v] : results.items()) + for (const auto& [k, v] : results.items()) { if (v.is_boolean()) { @@ -455,6 +463,13 @@ void PTA::saveSettings(int result) m_macrohandler.setMacros(v); } + + if (k == PTA_CONFIG_CLIENTLOG_PATH) + { + QString logpath = QString::fromStdString(v.get()); + + m_clientmonitor.setPath(logpath); + } } } diff --git a/PTA/pta.h b/PTA/pta.h index 99866bc..548a82a 100644 --- a/PTA/pta.h +++ b/PTA/pta.h @@ -2,6 +2,7 @@ #include "ui_pta.h" +#include "clientmonitor.h" #include "macrohandler.h" #include @@ -55,7 +56,7 @@ class PTA : public QMainWindow public slots: void showToolTip(QString message); - void showPriceResults(std::shared_ptr item, QString results); + void showPriceResults(std::shared_ptr item, const QString& results); protected: virtual void closeEvent(QCloseEvent* event) override; @@ -107,6 +108,9 @@ private slots: std::unique_ptr m_advancedKey; std::unique_ptr m_wikiKey; + // Client Monitor + ClientMonitor m_clientmonitor; + // Macros MacroHandler m_macrohandler; diff --git a/PTA/pta.qrc b/PTA/pta.qrc index 1f7354f..106f4e2 100644 --- a/PTA/pta.qrc +++ b/PTA/pta.qrc @@ -9,5 +9,6 @@ Resources/splash.png Resources/logo.ico Resources/macros.png + Resources/client.png diff --git a/PTA/pta_types.h b/PTA/pta_types.h index 4befbb7..acbbd2f 100644 --- a/PTA/pta_types.h +++ b/PTA/pta_types.h @@ -44,6 +44,8 @@ constexpr auto PTA_CONFIG_PREFILL_BASE = "pricecheck/prefillbase"; constexpr auto PTA_CONFIG_CUSTOM_MACROS = "macro/list"; +constexpr auto PTA_CONFIG_CLIENTLOG_PATH = "client/path"; + // defaults constexpr auto PTA_CONFIG_DEFAULT_PRICE_TEMPLATE = "templates/price/index.html"; constexpr auto PTA_CONFIG_DEFAULT_TEMPLATE_WIDTH = 600; diff --git a/PTA/statdialog.cpp b/PTA/statdialog.cpp index 7c08537..c3d52e7 100644 --- a/PTA/statdialog.cpp +++ b/PTA/statdialog.cpp @@ -255,7 +255,7 @@ StatDialog::StatDialog(PItem* item) if (!item->f_misc.influences.empty()) { - for (auto i : item->f_misc.influences) + for (const auto i : item->f_misc.influences) { QString influence = QString::fromStdString(i); QString influcap = influence;