/*************************************************************************** * Copyright (C) 2008-2013 by Heiko Koehn - KoehnHeiko@googlemail.com * * Copyright (C) 2014 by Ahmed Charles - acharles@outlook.com * * Copyright (C) 2016 by Chris Leacy - cleacy1972@gmail.com * * Copyright (C) 2017-2018, 2021, 2026 by Stephen Lyons * * - slysven@virginmedia.com * * * * This program 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 2 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, write to the * * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ #include "TTrigger.h" #include "Host.h" #include "TConsole.h" #include "TDebug.h" #include "TMatchState.h" #include "TMedia.h" #include "mudlet.h" #include #include #include // Some extraordinary numbers outside of the range (0-255) used for ANSI colors: // Changing them WILL modify the Lua API of TLuaInterpreter::tempColorTrigger // and the replacement TLuaInterpreter::tempAnsiColorTrigger const int TTrigger::scmDefault = -2; const int TTrigger::scmIgnored = -1; TTrigger::TTrigger(TTrigger* parent, Host* pHost) : Tree(parent) , mpHost(pHost) , mpLua(mpHost->getLuaInterpreter()) { } TTrigger::TTrigger(const QString& name, const QStringList& patterns, const QList& patternKinds, bool isMultiline, Host* pHost) : Tree(nullptr) , mpHost(pHost) , mName(name) , mPatterns(patterns) , mPatternKinds(patternKinds) , mIsMultiline(isMultiline) , mpLua(mpHost->getLuaInterpreter()) { setRegexCodeList(patterns, patternKinds); } TTrigger::~TTrigger() { mColorPatternList.clear(); mConditionMap.clear(); if (!mpHost) { return; } mpHost->getTriggerUnit()->unregisterTrigger(this); if (isTemporary()) { if (mScript.isEmpty()) { mpHost->mLuaInterpreter.delete_luafunction(this); } else { mpHost->mLuaInterpreter.delete_luafunction(mFuncName); } } } void TTrigger::setName(const QString& name) { if (!isTemporary()) { mpHost->getTriggerUnit()->mLookupTable.remove(mName, this); } mName = name; mpHost->getTriggerUnit()->mLookupTable.insert(name, this); } static void pcre2_code_deleter(pcre2_code* pointer) { pcre2_code_free(pointer); } static void pcre2_match_data_deleter(pcre2_match_data* pointer) { pcre2_match_data_free(pointer); } //FIXME: lock if code *OR* regex doesn't compile bool TTrigger::setRegexCodeList(QStringList patterns, QList patternKinds, bool existingTrigger) { patterns.replaceInStrings("\n", ""); mPatterns.clear(); mRegexMap.clear(); mMatchDataMap.clear(); mPatternKinds.clear(); mLuaConditionMap.clear(); mColorPatternList.clear(); mTriggerContainsPerlRegex = false; if (patternKinds.size() != patterns.size()) { //FIXME: ronny managed to trigger this somehow qDebug() << "[CRITICAL ERROR (plz report):] Trigger name=" << mName << " aborting reason: patternKinds.size() != patterns.size()"; } if (existingTrigger && (patternKinds.empty()) && (!isFolder()) && (!mColorTrigger)) { setError(tr("error: this trigger has no patterns defined")); mOK_init = false; return false; } bool state = true; if (existingTrigger) { for (int i = 0; i < patterns.size(); i++) { if (patterns.at(i).isEmpty() && patternKinds.at(i) != REGEX_PROMPT) { continue; } mPatterns.append(patterns.at(i)); mPatternKinds.append(patternKinds.at(i)); if (patternKinds.at(i) == REGEX_PERL) { const QByteArray& regexp = patterns.at(i).toUtf8(); int errorcode; PCRE2_SIZE erroffset; // PCRE2_UTF needed to run compile in UTF-8 mode // PCRE2_UCP needed for \d, \w etc. to use Unicode properties: QSharedPointer const re(pcre2_compile(reinterpret_cast(regexp.constData()), PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroffset, nullptr), pcre2_code_deleter); if (!re) { PCRE2_UCHAR errorBuffer[256]; pcre2_get_error_message(errorcode, errorBuffer, sizeof(errorBuffer)); const char* error = reinterpret_cast(errorBuffer); if (mudlet::smDebugMode) { TDebug(Qt::white, Qt::red) << "REGEX ERROR: failed to compile, reason:\n" << error << "\n" >> mpHost; TDebug(Qt::red, Qt::gray) << TDebug::csmContinue << R"(in: ")" << regexp.constData() << "\"\n" >> mpHost; } setError(qsl("%1") .arg(tr(R"(Error: in item %1, perl regex "%2" failed to compile, reason: "%3".)") .arg(QString::number(i + 1), QString(regexp.constData()).toHtmlEscaped(), QString(error).toHtmlEscaped()))); state = false; } else { pcre2_jit_compile(re.data(), PCRE2_JIT_COMPLETE); if (mudlet::smDebugMode) { TDebug(Qt::white, Qt::darkGreen) << "[OK]: REGEX_COMPILE OK\n" >> mpHost; } } mRegexMap[i] = re; mTriggerContainsPerlRegex = true; } if (patternKinds.at(i) == REGEX_LUA_CODE) { std::string funcName; std::stringstream func; func << "trigger" << mID << "condition" << i; funcName = func.str(); const QString code = qsl("function %1() %2\nend").arg(funcName.c_str(), patterns[i]); QString error; if (!mpLua->compile(code, error, QString::fromStdString(funcName))) { setError(qsl("%1") .arg(tr(R"(Error: in item %1, lua function "%2" failed to compile, reason: "%3".)").arg(QString::number(i + 1), patterns.at(i).toHtmlEscaped(), QString(error)))); state = false; if (mudlet::smDebugMode) { TDebug(Qt::white, Qt::red) << "LUA ERROR: failed to compile, reason:\n" << error << "\n" >> mpHost; TDebug(Qt::red, Qt::gray) << TDebug::csmContinue << R"(in lua condition function: ")" << patterns.at(i) << "\"\n" >> mpHost; } } else { mLuaConditionMap[i] = funcName; } } if (patternKinds[i] == REGEX_COLOR_PATTERN) { int textAnsiFg = scmIgnored; int textAnsiBg = scmIgnored; // Decode the pattern string to the colour codes wanted: TTrigger::decodeColorPatternText(patterns.at(i), textAnsiFg, textAnsiBg); if (textAnsiBg == scmIgnored && textAnsiFg == scmIgnored) { setError(qsl("%1") .arg(tr("Error: in item %1, no colors to match were set - at least one of the foreground or background must not be ignored.") .arg(QString::number(i + 1)))); state = false; continue; } // The setupColorTrigger(...) method will push_back the created // TColorTable instance if it is successful: if (!setupColorTrigger(textAnsiFg, textAnsiBg)) { mColorPatternList.emplace_back(nullptr); state = false; continue; } } else { mColorPatternList.emplace_back(nullptr); } } } mOK_init = state; return state; } bool TTrigger::match_perl(char* haystackC, const QString& haystack, int patternNumber, int posOffset, int lineNumber) { assert(mRegexMap.contains(patternNumber)); QSharedPointer const re = mRegexMap[patternNumber]; if (!re) { if (mudlet::smDebugMode) { TDebug(Qt::white, Qt::red) << "ERROR:" >> mpHost; TDebug(Qt::darkRed, Qt::darkGray) << TDebug::csmContinue << " the regex of trigger " << mName << " does not compile. Please correct the expression. This trigger will never match until it is fixed.\n" >> mpHost; } return false; //regex compile error } const int haystackCLength = strlen(haystackC); QSharedPointer& matchData = mMatchDataMap[patternNumber]; if (!matchData) { matchData.reset(pcre2_match_data_create_from_pattern(re.data(), nullptr), pcre2_match_data_deleter); if (!matchData) { return false; } } pcre2_match_data* match_data = matchData.data(); int rc = pcre2_match(re.data(), reinterpret_cast(haystackC), haystackCLength, 0, 0, match_data, nullptr); if (rc < 0) { return false; } processRegexMatch(haystackC, haystack, patternNumber, posOffset, re, haystackCLength, match_data, rc, lineNumber); return true; } void TTrigger::processRegexMatch(const char* haystackC, const QString& haystack, int patternNumber, int posOffset, const QSharedPointer& re, int haystackCLength, pcre2_match_data* match_data, int rc, int lineNumber) { PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data); if (mudlet::smDebugMode) { TDebug(Qt::blue, Qt::black) << "Trigger name=" << mName << "(" << mPatterns.value(patternNumber) << ") matched.\n" >> mpHost; } int i = 0; int numberOfCaptureGroups = 0; std::list captureList; std::list posList; QMap> namePositions; NameGroupMatches nameGroups; for (i = 0; i < rc; i++) { const char* substring_start = haystackC + ovector[2 * i]; const int substring_length = ovector[2 * i + 1] - ovector[2 * i]; std::string match; if (substring_length < 1) { captureList.push_back(match); posList.push_back(-1); continue; } const int utf16_pos = QString::fromUtf8(haystackC, ovector[2 * i]).length(); match.append(substring_start, substring_length); captureList.push_back(match); posList.push_back(utf16_pos + posOffset); if (mudlet::smDebugMode) { TDebug(Qt::darkCyan, Qt::black) << "capture group #" << (i + 1) << " = " >> mpHost; TDebug(Qt::darkMagenta, Qt::black) << TDebug::csmContinue << "<" << match.c_str() << ">\n" >> mpHost; } } uint32_t namecount = 0; uint32_t name_entry_size = 0; PCRE2_SPTR tabptr = nullptr; pcre2_pattern_info(re.data(), PCRE2_INFO_NAMECOUNT, &namecount); if (namecount > 0) { // Retrieves char table and entry size and extracts name of group and captures pcre2_pattern_info(re.data(), PCRE2_INFO_NAMETABLE, &tabptr); pcre2_pattern_info(re.data(), PCRE2_INFO_NAMEENTRYSIZE, &name_entry_size); for (uint32_t j = 0; j < namecount; ++j) { const int n = (tabptr[0] << 8) | tabptr[1]; auto name = QString::fromUtf8(reinterpret_cast(&tabptr[2])).trimmed(); //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic, cppcoreguidelines-pro-bounds-constant-array-index) tabptr += name_entry_size; if (ovector[2 * n] == PCRE2_UNSET) { //NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) continue; } auto* substring_start = haystackC + ovector[2 * n]; //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic, cppcoreguidelines-pro-bounds-constant-array-index) auto substring_length = ovector[2 * n + 1] - ovector[2 * n]; //NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) auto utf16_pos = QString::fromUtf8(haystackC, ovector[2 * n]).length(); //NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) auto capture = QString::fromUtf8(substring_start, substring_length); nameGroups << qMakePair(name, capture); namePositions.insert(name, qMakePair(utf16_pos + posOffset, static_cast(capture.length()))); } } if (mIsColorizerTrigger || mFilterTrigger) { numberOfCaptureGroups = captureList.size(); } for (; mPerlSlashGOption;) { uint32_t options = 0; const PCRE2_SIZE start_offset = ovector[1]; if (ovector[0] == ovector[1]) { if (ovector[0] >= static_cast(haystackCLength)) { goto END; } options = PCRE2_NOTEMPTY_ATSTART | PCRE2_ANCHORED; } rc = pcre2_match(re.data(), reinterpret_cast(haystackC), haystackCLength, start_offset, options, match_data, nullptr); if (rc == PCRE2_ERROR_NOMATCH) { if (options == 0) { break; } ovector[1] = start_offset + 1; continue; } else if (rc < 0) { // NOLINT(readability-else-after-return) goto END; } for (i = 0; i < rc; i++) { const char* substring_start = haystackC + ovector[2 * i]; const int substring_length = ovector[2 * i + 1] - ovector[2 * i]; std::string match; if (substring_length < 1) { captureList.push_back(match); posList.push_back(-1); continue; } const int utf16_pos = QString::fromUtf8(haystackC, ovector[2 * i]).length(); match.append(substring_start, substring_length); captureList.push_back(match); posList.push_back(utf16_pos + posOffset); if (mudlet::smDebugMode) { TDebug(Qt::darkCyan, Qt::black) << " capture group #" << (i + 1) << " = " >> mpHost; TDebug(Qt::darkMagenta, Qt::black) << "<" << match.c_str() << ">\n" >> mpHost; } } } END: { if (mIsColorizerTrigger) { const int r1 = mBgColor.red(); const int g1 = mBgColor.green(); const int b1 = mBgColor.blue(); const int r2 = mFgColor.red(); const int g2 = mFgColor.green(); const int b2 = mFgColor.blue(); const int total = captureList.size(); TConsole* pC = mpHost->mpConsole; if (Q_UNLIKELY(!pC)) { return; } pC->deselect(); auto its = captureList.begin(); auto iti = posList.begin(); for (int position = 1; iti != posList.end(); ++iti, ++its, position++) { const int begin = *iti; const std::string& s = *its; const int length = QString::fromStdString(s).size(); if (total > 1) { // skip complete match in Perl /g option type of triggers // to enable people to highlight capture groups if there are any // otherwise highlight complete expression match if (position % numberOfCaptureGroups != 1) { pC->selectSection(begin, length); if (mBgColor != QColorConstants::Transparent) { pC->setBgColor(r1, g1, b1, 255); } if (mFgColor != QColorConstants::Transparent) { pC->setFgColor(r2, g2, b2); } } } else { pC->selectSection(begin, length); if (mBgColor != QColorConstants::Transparent) { pC->setBgColor(r1, g1, b1, 255); } if (mFgColor != QColorConstants::Transparent) { pC->setFgColor(r2, g2, b2); } } } pC->reset(); } if (mIsMultiline) { updateMultistates(patternNumber, captureList, posList, &nameGroups); return; } TLuaInterpreter* pL = mpHost->getLuaInterpreter(); pL->setCaptureGroups(captureList, posList); pL->setCaptureNameGroups(nameGroups, namePositions); execute(); pL->clearCaptureGroups(); if (mFilterTrigger) { if (captureList.size() > 1) { const int total = captureList.size(); auto its = captureList.begin(); auto iti = posList.begin(); for (int filterPosition = 1; iti != posList.end(); ++iti, ++its, filterPosition++) { int begin = *iti; std::string& s = *its; if (total > 1 && numberOfCaptureGroups > 0) { // skip complete match in Perl /g option type of triggers // to enable people to highlight capture groups if there are any // otherwise highlight complete expression match if (filterPosition % numberOfCaptureGroups != 1) { filter(s, begin, lineNumber); } } else { filter(s, begin, lineNumber); } } } } return; } } bool TTrigger::match_begin_of_line_substring(const QString& haystack, const QString& needle, int patternNumber, int posOffset, int lineNumber) { if (haystack.startsWith(needle)) { processBeginOfLine(needle, patternNumber, posOffset, lineNumber); return true; } return false; } void TTrigger::processBeginOfLine(const QString& needle, int patternNumber, int posOffset, int lineNumber) { std::list captureList; std::list posList; captureList.emplace_back(needle.toUtf8().constData()); posList.push_back(0 + posOffset); if (mudlet::smDebugMode) { TDebug(Qt::darkCyan, Qt::black) << "Trigger name=" << mName << "(" << mPatterns.value(patternNumber) << ") matched.\n" >> mpHost; } if (mIsColorizerTrigger) { const int r1 = mBgColor.red(); const int g1 = mBgColor.green(); const int b1 = mBgColor.blue(); const int r2 = mFgColor.red(); const int g2 = mFgColor.green(); const int b2 = mFgColor.blue(); TConsole* pC = mpHost->mpConsole; if (Q_UNLIKELY(!pC)) { return; } auto its = captureList.begin(); for (auto iti = posList.begin(); iti != posList.end(); ++iti, ++its) { const int begin = *iti; const std::string& s = *its; const int length = QString::fromStdString(s).size(); pC->selectSection(begin, length); if (mBgColor != QColorConstants::Transparent) { pC->setBgColor(r1, g1, b1, 255); } if (mFgColor != QColorConstants::Transparent) { pC->setFgColor(r2, g2, b2); } } pC->reset(); } if (mIsMultiline) { updateMultistates(patternNumber, captureList, posList); return; } TLuaInterpreter* pL = mpHost->getLuaInterpreter(); pL->setCaptureGroups(captureList, posList); // call lua trigger function with number of matches and matches itselves as arguments execute(); pL->clearCaptureGroups(); if (mFilterTrigger) { if (!captureList.empty()) { filter(captureList.front(), posList.front(), lineNumber); } } } void TTrigger::updateMultistates(int regexNumber, std::list& captureList, std::list& posList, const NameGroupMatches* nameMatches) { if (regexNumber == 0) { // automatically set to #1 auto pCondition = std::make_unique(mPatterns.size(), mConditionLineDelta); auto* pConditionRaw = pCondition.get(); mConditionMap[pConditionRaw] = std::move(pCondition); pConditionRaw->multiCaptureList.push_back(captureList); pConditionRaw->multiCapturePosList.push_back(posList); if (nameMatches) { pConditionRaw->nameCaptures.push_back(*nameMatches); } else { pConditionRaw->nameCaptures.push_back(QVector>()); } if (mudlet::smDebugMode) { TDebug(Qt::darkYellow, Qt::black) << "match state " << mConditionMap.size() << "/" << mConditionMap.size() << " condition #" << regexNumber << "=true (" << regexNumber << "/" << mPatterns.size() << ") regex=" << mPatterns[regexNumber] << "\n" >> mpHost; } } else { int k = 0; for (auto& matchStatePair : mConditionMap) { k++; if (matchStatePair.second->nextCondition() == regexNumber) { if (mudlet::smDebugMode) { TDebug(Qt::darkYellow, Qt::black) << "match state " << k << "/" << mConditionMap.size() << " condition #" << regexNumber << "=true (" << regexNumber << "/" << mPatterns.size() << ") regex=" << mPatterns[regexNumber] << "\n" >> mpHost; } matchStatePair.second->conditionMatched(); matchStatePair.second->multiCaptureList.push_back(captureList); matchStatePair.second->multiCapturePosList.push_back(posList); if (nameMatches != nullptr) { matchStatePair.second->nameCaptures.push_back(*nameMatches); } else { matchStatePair.second->nameCaptures.push_back(QVector>()); } } } } } void TTrigger::filter(std::string& capture, int& posOffset, int lineNumber) { if (capture.empty()) { return; } const QString text = QString::fromStdString(capture); for (auto& trigger : *mpMyChildrenList) { trigger->match(capture.data(), text, lineNumber, posOffset); } } int TTrigger::getExpiryCount() const { return mExpiryCount; } void TTrigger::setExpiryCount(int expiryCount) { mExpiryCount = expiryCount; } bool TTrigger::match_substring(const QString& haystack, const QString& needle, int patternNumber, int posOffset, int lineNumber) { const int where = haystack.indexOf(needle); if (where != -1) { processSubstringMatch(haystack, needle, patternNumber, posOffset, where, lineNumber); return true; } return false; } void TTrigger::processSubstringMatch(const QString& haystack, const QString& needle, int regexNumber, int posOffset, int where, int lineNumber) { std::list captureList; std::list posList; captureList.emplace_back(needle.toUtf8().constData()); posList.push_back(where + posOffset); if (mPerlSlashGOption) { while ((where = haystack.indexOf(needle, where + 1)) != -1) { captureList.emplace_back(needle.toUtf8().constData()); posList.push_back(where + posOffset); } } if (mudlet::smDebugMode) { TDebug(Qt::cyan, Qt::black) << "Trigger name=" << mName << "(" << mPatterns.value(regexNumber) << ") matched.\n" >> mpHost; } if (mIsColorizerTrigger) { const int r1 = mBgColor.red(); const int g1 = mBgColor.green(); const int b1 = mBgColor.blue(); const int r2 = mFgColor.red(); const int g2 = mFgColor.green(); const int b2 = mFgColor.blue(); TConsole* pC = mpHost->mpConsole; if (Q_UNLIKELY(!pC)) { return; } pC->deselect(); auto its = captureList.begin(); for (auto iti = posList.begin(); iti != posList.end(); ++iti, ++its) { const int begin = *iti; const std::string& s = *its; const int length = QString::fromStdString(s).size(); pC->selectSection(begin, length); if (mBgColor != QColorConstants::Transparent) { pC->setBgColor(r1, g1, b1, 255); } if (mFgColor != QColorConstants::Transparent) { pC->setFgColor(r2, g2, b2); } } pC->reset(); } if (mIsMultiline) { updateMultistates(regexNumber, captureList, posList); return; } TLuaInterpreter* pL = mpHost->getLuaInterpreter(); pL->setCaptureGroups(captureList, posList); // call lua trigger function with number of matches and matches itselves as arguments execute(); pL->clearCaptureGroups(); if (mFilterTrigger) { if (!captureList.empty()) { filter(captureList.front(), posList.front(), lineNumber); } } } bool TTrigger::match_color_pattern(int line, int patternNumber, int posOffset, int length) { if (patternNumber >= mColorPatternList.size()) { return false; } if (line == -1) { return false; } bool canExecute = false; std::list captureList; std::list posList; if (line >= static_cast(mpHost->mpConsole->buffer.buffer.size())) { return false; } std::deque& bufferLine = mpHost->mpConsole->buffer.buffer[line]; const QString& lineBuffer = mpHost->mpConsole->buffer.lineBuffer[line]; // Match against the colors as they arrived from the game, not as already // recolored by other triggers or scripts earlier in this trigger pass; // text inserted mid-pass has no game original so it is read live: const std::deque* pPassLine = mpHost->mpConsole->buffer.preTriggerPassLine(line); // Filter ("only pass matches") parents hand children just the matched // capture, so restrict the scan to that window; for top-level triggers // the window covers the whole line: const int start = qBound(0, posOffset, static_cast(bufferLine.size())); const int end = qBound(start, posOffset + length, static_cast(bufferLine.size())); int pos = start; int matchBegin = -1; bool matching = false; TColorTable* pCT = mColorPatternList[patternNumber].get(); if (!pCT) { return false; // no color pattern created } if (pCT->ansiBg == scmIgnored && pCT->ansiFg == scmIgnored) { // BOTH the foreground AND the background colors are set to "ignore" // so this is not an active color trigger setup - so ignore it return false; // no color settings to match against } for (auto it = bufferLine.begin() + start; pos < end; ++it, ++pos) { const TChar& character = (pPassLine && pos < static_cast(pPassLine->size())) ? (*pPassLine)[pos] : *it; // This now allows matching against the current default colours (-1) and // allows ONE of the foreground or background to NOT be considered (-2) // Ideally we should base the matching on only the ANSI code but not // all parts of the text come from the Server and can be determined to // have come from a decoded ANSI code number: if (((pCT->ansiFg == scmIgnored) || ((pCT->ansiFg == scmDefault) && mpHost->mpConsole->mFgColor == character.foreground()) || (pCT->mFgColor == character.foreground())) && ((pCT->ansiBg == scmIgnored) || ((pCT->ansiBg == scmDefault) && mpHost->mpConsole->mBgColor == character.background()) || (pCT->mBgColor == character.background()))) { if (matchBegin == -1) { matchBegin = pos; } matching = true; } else { matching = false; } if ((!matching) || (matching && (pos + 1 >= end))) { if (matchBegin > -1) { std::string got; if (matching) { got = lineBuffer.mid(matchBegin, pos - matchBegin + 1).toUtf8().constData(); } else { got = lineBuffer.mid(matchBegin, pos - matchBegin).toUtf8().constData(); } captureList.push_back(got); posList.push_back(matchBegin); matchBegin = -1; canExecute = true; matching = false; } } } if (canExecute) { processColorPattern(patternNumber, captureList, posList, line); return true; } return false; } void TTrigger::processColorPattern(int patternNumber, std::list& captureList, std::list& posList, int lineNumber) { if (mIsColorizerTrigger) { const int r1 = mBgColor.red(); const int g1 = mBgColor.green(); const int b1 = mBgColor.blue(); const int r2 = mFgColor.red(); const int g2 = mFgColor.green(); const int b2 = mFgColor.blue(); TConsole* pC = mpHost->mpConsole; if (Q_UNLIKELY(!pC)) { return; } pC->deselect(); auto its = captureList.begin(); for (auto iti = posList.begin(); iti != posList.end(); ++iti, ++its) { const int begin = *iti; // qDebug() << "TTrigger::match_color_pattern(" << line << "," << patternNumber << ") INFO - match found: " << (*its).c_str() << " size is:" << (*its).size(); const std::string& s = *its; const int length = QString::fromStdString(s).size(); pC->selectSection(begin, length); if (mBgColor != QColorConstants::Transparent) { pC->setBgColor(r1, g1, b1, 255); } if (mFgColor != QColorConstants::Transparent) { pC->setFgColor(r2, g2, b2); } } pC->reset(); } if (mIsMultiline) { updateMultistates(patternNumber, captureList, posList); return; } TLuaInterpreter* pL = mpHost->getLuaInterpreter(); pL->setCaptureGroups(captureList, posList); // call lua trigger function with number of matches and matches itselves as arguments execute(); pL->clearCaptureGroups(); if (mFilterTrigger) { if (!captureList.empty()) { auto it1 = captureList.begin(); auto it2 = posList.begin(); for (; it1 != captureList.end(); it1++, it2++) { filter(*it1, *it2, lineNumber); } } } } bool TTrigger::match_line_spacer(int patternNumber) { if (mIsMultiline) { int k = 0; for (auto& matchStatePair : mConditionMap) { k++; if (matchStatePair.second->nextCondition() == patternNumber) { if (matchStatePair.second->lineSpacerMatch(mPatterns.value(patternNumber).toInt())) { if (mudlet::smDebugMode) { TDebug(Qt::yellow, Qt::black) << "Trigger name=" << mName << "(" << mPatterns.value(patternNumber) << ") condition #" << patternNumber << "=true " >> mpHost; TDebug(Qt::darkYellow, Qt::black) << TDebug::csmContinue << "match state " << k << "/" << mConditionMap.size() << " condition #" << patternNumber << "=true (" << patternNumber + 1 << "/" << mPatterns.size() << ") line spacer=" << mPatterns.value(patternNumber) << "lines\n" >> mpHost; } matchStatePair.second->conditionMatched(); std::list const captureList; std::list const posList; matchStatePair.second->multiCaptureList.push_back(captureList); matchStatePair.second->multiCapturePosList.push_back(posList); matchStatePair.second->nameCaptures.push_back(QVector>()); } } } } return true; //line spacers don't make sense outside of AND triggers -> ignore them } bool TTrigger::match_lua_code(int patternNumber) { if (mLuaConditionMap.find(patternNumber) == mLuaConditionMap.end()) { return false; } if (mpLua->callConditionFunction(mLuaConditionMap[patternNumber], mName)) { if (mudlet::smDebugMode) { TDebug(Qt::yellow, Qt::black) << "Trigger name=" << mName << "(" << mPatterns.value(patternNumber) << ") matched.\n" >> mpHost; } if (mIsMultiline) { std::list captureList; std::list posList; updateMultistates(patternNumber, captureList, posList); return true; } execute(); return true; } return false; } bool TTrigger::match_prompt(int patternNumber) { if (mpHost->mpConsole->mIsPromptLine) { processPromptMatch(patternNumber); return true; } return false; } void TTrigger::processPromptMatch(int patternNumber) { if (mudlet::smDebugMode) { TDebug(Qt::yellow, Qt::black) << "Trigger name=" << mName << "(" << mPatterns.value(patternNumber) << ") matched.\n" >> mpHost; } if (mIsMultiline) { std::list captureList; std::list posList; updateMultistates(patternNumber, captureList, posList); return; } execute(); } bool TTrigger::match_exact_match(const QString& haystack, const QString& needle, int patternNumber, int posOffset, int lineNumber) { QStringView text(haystack); if (text.endsWith(QChar('\n'))) { text.chop(1); } if (text == needle) { processExactMatch(needle, patternNumber, posOffset, lineNumber); return true; } return false; } void TTrigger::processExactMatch(const QString& needle, int patternNumber, int posOffset, int lineNumber) { std::list captureList; std::list posList; captureList.emplace_back(needle.toUtf8().constData()); posList.push_back(0 + posOffset); if (mudlet::smDebugMode) { TDebug(Qt::yellow, Qt::black) << "Trigger name=" << mName << "(" << mPatterns.value(patternNumber) << ") matched.\n" >> mpHost; } if (mIsColorizerTrigger) { const int r1 = mBgColor.red(); const int g1 = mBgColor.green(); const int b1 = mBgColor.blue(); const int r2 = mFgColor.red(); const int g2 = mFgColor.green(); const int b2 = mFgColor.blue(); TConsole* pC = mpHost->mpConsole; if (Q_UNLIKELY(!pC)) { return; } auto its = captureList.begin(); for (auto iti = posList.begin(); iti != posList.end(); ++iti, ++its) { const int begin = *iti; const std::string& s = *its; const int length = QString::fromStdString(s).size(); pC->selectSection(begin, length); if (mBgColor != QColorConstants::Transparent) { pC->setBgColor(r1, g1, b1, 255); } if (mFgColor != QColorConstants::Transparent) { pC->setFgColor(r2, g2, b2); } } pC->reset(); } if (mIsMultiline) { updateMultistates(patternNumber, captureList, posList); return; } TLuaInterpreter* pL = mpHost->getLuaInterpreter(); pL->setCaptureGroups(captureList, posList); // call lua trigger function with number of matches and matches themselves as arguments execute(); pL->clearCaptureGroups(); if (mFilterTrigger) { if (!captureList.empty()) { filter(captureList.front(), posList.front(), lineNumber); } } } // haystackC: string to match as a char* // haystack: string to match as a QString // line: line number in the buffer // posOffset: position in the line to start matching from; used by child triggers bool TTrigger::match(char* haystackC, const QString& haystack, int line, int posOffset) { // Guard against re-entrancy: cleanup may have deleted this trigger while // match() was still on the call stack if (!mpMyChildrenList) { qWarning() << "TTrigger::match() called on destroyed trigger - ID:" << mID << "Name:" << mName; return false; } bool ret = false; if (isActive()) { if (mIsLineTrigger) { if (--mStartOfLineDelta < 0) { execute(); if (--mLineDelta <= 0) { deactivate(); mpHost->getTriggerUnit()->markCleanup(this); } return true; } return false; } if (haystack.isEmpty()) { return false; } bool conditionMet = false; int highestCondition = 0; if (mIsMultiline) { for (auto& matchStatePair : mConditionMap) { matchStatePair.second->newLineArrived(); const int next = matchStatePair.second->nextCondition(); if (next > highestCondition) { highestCondition = next; } } } const int size = mPatternKinds.size(); for (int patternNumber = 0;; patternNumber++) { if (patternNumber >= size) { break; } ret = false; switch (mPatternKinds.value(patternNumber)) { case REGEX_SUBSTRING: ret = match_substring(haystack, mPatterns.at(patternNumber), patternNumber, posOffset, line); break; case REGEX_PERL: ret = match_perl(haystackC, haystack, patternNumber, posOffset, line); break; case REGEX_BEGIN_OF_LINE_SUBSTRING: ret = match_begin_of_line_substring(haystack, mPatterns.at(patternNumber), patternNumber, posOffset, line); break; case REGEX_EXACT_MATCH: ret = match_exact_match(haystack, mPatterns.at(patternNumber), patternNumber, posOffset, line); break; case REGEX_LUA_CODE: ret = match_lua_code(patternNumber); break; case REGEX_LINE_SPACER: ret = match_line_spacer(patternNumber); break; case REGEX_COLOR_PATTERN: // for a filter child the haystack is just the parent's capture, // so its length bounds the color scan window on that line ret = match_color_pattern(line, patternNumber, posOffset, static_cast(haystack.length())); break; case REGEX_PROMPT: ret = match_prompt(patternNumber); break; } // policy: one match is enough to fire on OR-trigger, but in the case of // an AND-trigger all conditions have to be met in order to fire the trigger if (!mIsMultiline) { if (ret) { conditionMet = true; mKeepFiring = mStayOpen; break; } } else { if ((!ret) && (patternNumber >= highestCondition)) { break; } } } // in the case of multiline triggers: check our state if (mIsMultiline) { int k = 0; conditionMet = false; //invalidate conditionMet as it has no meaning for multiline triggers std::list removeList; for (auto& matchStatePair : mConditionMap) { k++; if (matchStatePair.second->isComplete()) { mKeepFiring = mStayOpen; if (mudlet::smDebugMode) { TDebug(Qt::yellow, Qt::darkMagenta) << "multiline trigger name=" << mName << " *FIRES* all conditions are fulfilled. Executing script.\n" >> mpHost; } removeList.push_back(matchStatePair.first); conditionMet = true; TLuaInterpreter* pL = mpHost->getLuaInterpreter(); pL->setMultiCaptureGroups(matchStatePair.second->multiCaptureList, matchStatePair.second->multiCapturePosList, matchStatePair.second->nameCaptures); execute(); pL->clearCaptureGroups(); if (mFilterTrigger) { std::list> multiCaptureList; multiCaptureList = matchStatePair.second->multiCaptureList; if (!multiCaptureList.empty()) { for (auto mit = multiCaptureList.begin(); mit != multiCaptureList.end(); mit++, k++) { const int total = (*mit).size(); auto its = (*mit).begin(); for (int i = 1; its != (*mit).end(); ++its, i++) { std::string s = *its; int p = 0; // multiline captures may come from earlier lines, so no // single line number applies here if (total > 1) { if (i % total != 1) { filter(s, p, -1); } } else { filter(s, p, -1); } } } } } } if (!matchStatePair.second->newLine()) { removeList.push_back(matchStatePair.first); } } for (auto& matchState : removeList) { if (mConditionMap.find(matchState) != mConditionMap.end()) { if (mudlet::smDebugMode) { TDebug(Qt::darkBlue, Qt::black) << "removing condition from condition table.\n" >> mpHost; } mConditionMap.erase(matchState); } } } // definition trigger chain: a folder is part of a trigger chain if it has a regex defined // a trigger chain only lets data pass if the condition matches or in case of multiline all // all conditions are fulfilled // // a folder can also be a simple structural element in which case all data passes through // if at least one regex is defined a folder is considered a trigger chain otherwise a structural element if (!mFilterTrigger) { if (conditionMet || (mPatterns.empty())) { for (auto trigger : *mpMyChildrenList) { ret = trigger->match(haystackC, haystack, line, posOffset); if (ret) { conditionMet = true; } } } } if ((mKeepFiring > 0) && (!conditionMet)) { mKeepFiring--; if ((mKeepFiring == mStayOpen) || (mpMyChildrenList->empty())) { execute(); } for (auto trigger : *mpMyChildrenList) { ret = trigger->match(haystackC, haystack, line, posOffset); if (ret) { conditionMet = true; } } return true; } if (conditionMet && mExpiryCount > -1) { mExpiryCount--; if (mExpiryCount == 0) { // The delete is deferred to the end of the outermost pass, so an // expired trigger left active would fire again from any pass that // re-enters meanwhile. What stops enableTrigger() resurrecting it // in that window is the markCleanup() below, not the deactivation: // enableTrigger() skips anything in mCleanupSet. setIsActive(false); mpHost->getTriggerUnit()->markCleanup(this); if (mudlet::smDebugMode) { // FIXME: This message is translated - but most other TDebug ones are not! TDebug(Qt::yellow, Qt::darkMagenta) << qsl("%1\n").arg(tr("Trigger name=%1 expired.").arg(mName)) >> mpHost; } } else if (mudlet::smDebugMode) { // FIXME: This message is translated - but most other TDebug ones are not! TDebug(Qt::yellow, Qt::darkMagenta) << qsl("%1\n").arg(tr("Trigger name=%1 will fire %n more time(s).", nullptr, mExpiryCount).arg(mName)) >> mpHost; } } return conditionMet; } return false; } bool TTrigger::checkIfNew() { return mIsNew; } void TTrigger::unmarkAsNew() { mIsNew = false; } // This NOW uses proper ANSI numbers // A TColorTable is a simple struct that stores four values, the two given ANSI // colors for foreground and background (proper ANSI indexes) and what they look // like give the current Host settings (the first 16 ANSI ones and the default // fore and background colors can be changed by the user and since OSC P/R // support has been implemented - by the MUD Server!) std::unique_ptr TTrigger::createColorPattern(int ansiFg, int ansiBg) { /* * OLD Mudlet simplified ANSI color codes * -> proper ANSI numbers * --------------------------------------------- * 0 default text color -> (-1 special value!) * 1 light black -> 8 * 2 dark black -> 0 * 3 light red -> 9 * 4 dark red -> 1 * 5 light green -> 10 * 6 dark green -> 2 * 7 light yellow -> 11 * 8 dark yellow -> 3 * 9 light blue -> 12 * 10 dark blue -> 4 * 11 light magenta -> 13 * 12 dark magenta -> 5 * 13 light cyan -> 14 * 14 dark cyan -> 6 * 15 light white -> 15 * 16 dark white -> 7 */ const QColor fgColor = mpHost->getAnsiColor(ansiFg, false); const QColor bgColor = mpHost->getAnsiColor(ansiBg, true); // If BOTH ansiFg AND ansiBg are scmIgnored then the color pattern is // totally unset if (!(fgColor.isValid() || bgColor.isValid())) { return nullptr; } auto pCT = std::unique_ptr(new (std::nothrow) TColorTable); if (!pCT) { return nullptr; } pCT->ansiBg = ansiBg; pCT->ansiFg = ansiFg; pCT->mBgColor = bgColor; pCT->mFgColor = fgColor; return pCT; } bool TTrigger::setupColorTrigger(int ansiFg, int ansiBg) { auto pCT = createColorPattern(ansiFg, ansiBg); if (!pCT) { // This can be caused by both ansiFg and ansiBg being scmIgnored return false; } mColorPatternList.emplace_back(std::move(pCT)); return true; } // The numbers here are NOW the proper ANSI colour codes (0 to 255) // or scmDefault for default fore/background or scmIgnored for ignored bool TTrigger::setupTmpColorTrigger(int ansiFg, int ansiBg) { auto pCT = createColorPattern(ansiFg, ansiBg); if (!pCT) { return false; } // createColorPatternText(...) now returns an empty string if BOTH color // codes are the scmIgnored ones: mPatterns << createColorPatternText(ansiFg, ansiBg); mPatternKinds << REGEX_COLOR_PATTERN; mColorPatternList.emplace_back(std::move(pCT)); return true; } bool TTrigger::isFilterChain() { return (!mPatterns.empty()) && (hasChildren()); } bool TTrigger::registerTrigger() { if (!mpHost) { return false; } return mpHost->getTriggerUnit()->registerTrigger(this); } void TTrigger::compileAll() { mNeedsToBeCompiled = true; if (!compileScript()) { if (mudlet::smDebugMode) { TDebug(Qt::white, Qt::red) << "ERROR: Lua compile error. compiling script of Trigger:" << mName << "\n" >> mpHost; } mOK_code = false; } setRegexCodeList(mPatterns, mPatternKinds); for (auto trigger : *mpMyChildrenList) { trigger->compileAll(); } } void TTrigger::compile() { if (mNeedsToBeCompiled) { if (!compileScript()) { if (mudlet::smDebugMode) { TDebug(Qt::white, Qt::red) << "ERROR: Lua compile error. compiling script of Trigger:" << mName << "\n" >> mpHost; } mOK_code = false; } } for (auto trigger : *mpMyChildrenList) { trigger->compile(); } } bool TTrigger::setScript(const QString& script) { // Switching from a registered anonymous Lua function (set up by tempTrigger with a // function argument) to a script string: release the old function from the Lua // registry and leave callback mode. Otherwise execute() keeps calling the stale // function so the new script never runs, and the registry entry leaks - the // destructor would take its mScript-based branch and delete the compiled function. if (mRegisteredAnonymousLuaFunction) { if (mpHost) { mpHost->mLuaInterpreter.delete_luafunction(this); } mRegisteredAnonymousLuaFunction = false; } mScript = script; if (script.isEmpty()) { mNeedsToBeCompiled = false; mOK_code = true; } else { mNeedsToBeCompiled = true; mOK_code = compileScript(); } return mOK_code; } bool TTrigger::compileScript() { mFuncName = qsl("Trigger%1").arg(QString::number(mID)); const QString code = qsl("function %1() %2\nend").arg(mFuncName, mScript); QString error; if (mpLua->compile(code, error, qsl("Trigger: %1").arg(getName()))) { mNeedsToBeCompiled = false; mOK_code = true; return true; } mOK_code = false; setError(error); return false; } namespace { // Tracks the innermost trigger running its script, so feedTriggers() can name the // culprit when aborting an endless loop, and the same-line creation lineage of // that trigger's root, so anything the script creates joins it; RAII restores the // prior values on exit. class ExecutingTriggerGuard { public: ExecutingTriggerGuard(TriggerUnit* pUnit, const QString* pName, const int chainId, const int generation) : mpUnit(pUnit) , mpPreviousName(pUnit->currentExecutingTriggerName()) , mPreviousChainId(pUnit->currentSameLineChainId()) , mPreviousGeneration(pUnit->currentSameLineGeneration()) { mpUnit->setCurrentExecutingTriggerName(pName); mpUnit->setCurrentSameLineChain(chainId, generation); } ~ExecutingTriggerGuard() { mpUnit->setCurrentExecutingTriggerName(mpPreviousName); mpUnit->setCurrentSameLineChain(mPreviousChainId, mPreviousGeneration); } ExecutingTriggerGuard(const ExecutingTriggerGuard&) = delete; ExecutingTriggerGuard& operator=(const ExecutingTriggerGuard&) = delete; private: TriggerUnit* mpUnit; const QString* mpPreviousName; int mPreviousChainId; int mPreviousGeneration; }; } // namespace void TTrigger::execute() { // Only root triggers carry a lineage, so a trigger nested in a folder or a // filter chain reads the one on the root its subtree hangs from. Creations // that go under a parent rather than to the root list are outside this // accounting altogether, as they are outside the list processDataStream() // walks. const TTrigger* pRoot = this; while (pRoot->getParent()) { pRoot = pRoot->getParent(); } const ExecutingTriggerGuard executingTriggerGuard(mpHost->getTriggerUnit(), &mName, pRoot->sameLineChainId(), pRoot->sameLineGeneration()); if (mSoundTrigger) { /* eventually something should be added to the gui to change sound volumes. 100=full volume */ QString mediaFileName = mSoundFile; if (QDir::homePath().contains('\\')) { mediaFileName.replace('/', R"(\)"); } else { mediaFileName.replace('\\', "/"); } TMediaData mediaData{}; mediaData.setMediaProtocol(TMediaData::MediaProtocolAPI); mediaData.setMediaType(TMediaData::MediaTypeSound); mediaData.setMediaFileName(mediaFileName); mediaData.setMediaVolume(TMediaData::MediaVolumeMax); mpHost->mpMedia->playMedia(mediaData); } if (!mCommand.isEmpty()) { mpHost->send(mCommand); } if (mNeedsToBeCompiled) { if (!compileScript()) { return; } } if (mRegisteredAnonymousLuaFunction) { if (Q_LIKELY(mExpiryCount <= 0)) { mpLua->call_luafunction(this); } else { // if the trigger is a temporary expiring one, // don't expire if it returned true auto result = mpLua->callLuaFunctionReturnBool(this); // if the function ran okay and returned true, it wants to extend the expiry count if (result.first && result.second) { mExpiryCount++; } } return; } if (mScript.isEmpty()) { return; } if (mIsMultiline) { if (Q_LIKELY(mExpiryCount <= 0)) { mpLua->callMulti(mFuncName, mName); } else { // if the trigger is a temporary expiring one, // don't expire if it returned true auto result = mpLua->callMultiReturnBool(mFuncName, mName); if (result.second) { mExpiryCount++; } } } else { if (Q_LIKELY(mExpiryCount <= 0)) { mpLua->call(mFuncName, mName); } else { // if the trigger is a temporary expiring one, // don't expire if it returned true auto result = mpLua->callReturnBool(mFuncName, mName); if (result.second) { mExpiryCount++; } } } } void TTrigger::enableTrigger(const QString& name) { if (mName == name) { setIsActive(true); } for (auto trigger : *mpMyChildrenList) { trigger->enableTrigger(name); } } void TTrigger::disableTrigger(const QString& name) { if (mName == name) { setIsActive(false); } for (auto trigger : *mpMyChildrenList) { trigger->disableTrigger(name); } } TTrigger* TTrigger::killTrigger(const QString& name) { if (mName == name) { setIsActive(false); } for (auto trigger : *mpMyChildrenList) { trigger->killTrigger(name); } return nullptr; } // Provide a pair of helpers which means we can provide a slightly more readable // pattern text for the odd cases - and do it in one place: QString TTrigger::createColorPatternText(const int fgColorCode, const int bgColorCode) { QString fgText; QString bgText; if (fgColorCode == scmIgnored) { fgText = QLatin1String("IGNORE"); } else if (fgColorCode == scmDefault) { fgText = QLatin1String("DEFAULT"); } else { fgText = qsl("%1").arg(fgColorCode, 3, 10, QLatin1Char('0')); } if (bgColorCode == scmIgnored) { bgText = QLatin1String("IGNORE"); } else if (bgColorCode == scmDefault) { bgText = QLatin1String("DEFAULT"); } else { bgText = qsl("%1").arg(bgColorCode, 3, 10, QLatin1Char('0')); } // QString::compare(...) returns zero (boolean false) on a match, or a // non-zero (boolean true) on no match - and we want to detect when BOTH // texts are IGNORE so we return an empty string in that case only - so that // it is equivalent to an empty other trigger type: return (fgText.compare(QLatin1String("IGNORE")) || bgText.compare(QLatin1String("IGNORE"))) ? qsl("ANSI_COLORS_F{%1}_B{%2}").arg(fgText, bgText) : QString(); } void TTrigger::decodeColorPatternText(const QString& patternText, int& fgColorCode, int& bgColorCode) { // The numbers used for the text have changed - see table in: // TColorTable* TTrigger::createColorPattern(int ansiFg, int ansiBg) const QRegularExpression regex = QRegularExpression(qsl("^ANSI_COLORS_F{(\\d+|DEFAULT|IGNORE)}_B{(\\d+|DEFAULT|IGNORE)}$")); // Was QRegularExpression regex = QRegularExpression(qsl(R"(FG(\d+)BG(\d+))")); const QRegularExpressionMatch match = regex.match(patternText); // scmDefault is the new code for "default" colour (as 0 is a valid ANSI color number!) // scmIgnored is the new code for "reset" i.e. NOT set color trigger (i.e. don't // bother with checking this part of the colour) if (match.capturedStart() > -1) { // There *is* a match - now examine what was found: if (match.captured(1) == QLatin1String("DEFAULT")) { fgColorCode = scmDefault; } else if (match.captured(1) == QLatin1String("IGNORE")) { fgColorCode = scmIgnored; } else { fgColorCode = match.captured(1).toInt(); } if (match.captured(2) == QLatin1String("DEFAULT")) { bgColorCode = scmDefault; } else if (match.captured(2) == QLatin1String("IGNORE")) { bgColorCode = scmIgnored; } else { bgColorCode = match.captured(2).toInt(); } } else { fgColorCode = scmIgnored; bgColorCode = scmIgnored; } } QString TTrigger::packageName(TTrigger* pTrigger) { if (!pTrigger) { return QString(); } if (!pTrigger->mPackageName.isEmpty()) { return !mpHost->mInstalledModules.contains(pTrigger->mPackageName) ? pTrigger->mPackageName : QString(); } if (pTrigger->getParent()) { return packageName(pTrigger->getParent()); } return QString(); } QString TTrigger::moduleName(TTrigger* pTrigger) { if (!pTrigger) { return QString(); } if (!pTrigger->mPackageName.isEmpty()) { return mpHost->mInstalledModules.contains(pTrigger->mPackageName) ? pTrigger->mPackageName : QString(); } if (pTrigger->getParent()) { return moduleName(pTrigger->getParent()); } return QString(); }