mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### Brief overview of PR changes/additions - `match_exact_match()` did `QString text = haystack;` then chopped a trailing newline. The assignment is copy-on-write and cheap, but `chop()` mutates, forcing the detach: a heap allocation plus a full character copy of the line. - The newline is always present - `TMainConsole::runTriggers()` appends one to every line before dispatch (`TMainConsole.cpp:1570`) - so the chop always fires and the copy always happens, once per exact-match pattern, per reachable trigger, per line of game text. - Use a `QStringView`. Chopping a view moves only its own end pointer, so nothing is allocated, and the comparison against the needle is unchanged. Both chop one UTF-16 code unit, so behaviour is identical for every input. #### Motivation for adding to Mudlet Removes a per-line heap allocation and line copy from the trigger matching path, which runs for every line of game text. #### Other info (issues closed, discussion etc) Measured with `PipelineBenchmark` on macOS/arm64, Release, `-DUSE_SANITIZER=""`, two binaries from one toolchain run in 24 interleaved ABBA pairs so run-order effects cancel: trigger throughput **+3.28%** (t=6.41), trigger overhead **-6.37%** (t=-4.94). The text-only control moved +0.23% (t=-0.56, 12/24 paired wins), i.e. no effect, which is the check that the trigger deltas are real. Worth stating plainly: the stock benchmark corpus contains **no** exact-match patterns, so `match_exact_match()` is never entered by it. Twelve were added locally purely to measure this. The gain therefore scales with how many exact-match patterns a profile actually has, and is zero for a profile with none. **Test case:** behaviour-neutral, so the checks are for regressions around where the chop lands. Exact-match triggers fire correctly on a plain ASCII line, on a line with an accented character, on one with an em dash, and on one containing an emoji (a surrogate pair, i.e. two UTF-16 code units - the case most likely to expose a code-unit-based chop). A line with trailing whitespace before the newline correctly does *not* match. Verified against before/after builds on macOS, plus 5 trigger functional test suites. --------- Signed-off-by: Jay Howard <jay.patrick.howard@gmail.com>
1549 lines
58 KiB
C++
1549 lines
58 KiB
C++
/***************************************************************************
|
|
* 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 <QRegularExpression>
|
|
|
|
#include <cassert>
|
|
#include <sstream>
|
|
|
|
// 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<TTrigger>(parent)
|
|
, mpHost(pHost)
|
|
, mpLua(mpHost->getLuaInterpreter())
|
|
{
|
|
}
|
|
|
|
TTrigger::TTrigger(const QString& name, const QStringList& patterns, const QList<int>& patternKinds, bool isMultiline, Host* pHost)
|
|
: Tree<TTrigger>(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<int> 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<pcre2_code> const re(pcre2_compile(reinterpret_cast<PCRE2_SPTR>(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<const char*>(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("<b>%1</b>")
|
|
.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("<b>%1</b>")
|
|
.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("<b>%1</b>")
|
|
.arg(tr("Error: in item %1, no colors to match were set - at least <i>one</i> of the foreground or background must not be <i>ignored</i>.")
|
|
.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<pcre2_code> 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<pcre2_match_data>& 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<PCRE2_SPTR>(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<pcre2_code>& 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<std::string> captureList;
|
|
std::list<int> posList;
|
|
QMap<QString, QPair<int, int>> 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<const char*>(&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<int>(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<PCRE2_SIZE>(haystackCLength)) {
|
|
goto END;
|
|
}
|
|
options = PCRE2_NOTEMPTY_ATSTART | PCRE2_ANCHORED;
|
|
}
|
|
|
|
rc = pcre2_match(re.data(), reinterpret_cast<PCRE2_SPTR>(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) << "<regex mode: match all> 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<std::string> captureList;
|
|
std::list<int> 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<std::string>& captureList, std::list<int>& posList, const NameGroupMatches* nameMatches)
|
|
{
|
|
if (regexNumber == 0) {
|
|
// automatically set to #1
|
|
auto pCondition = std::make_unique<TMatchState>(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<QPair<QString, QString>>());
|
|
}
|
|
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<QPair<QString, QString>>());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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<std::string> captureList;
|
|
std::list<int> 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<std::string> captureList;
|
|
std::list<int> posList;
|
|
if (line >= static_cast<int>(mpHost->mpConsole->buffer.buffer.size())) {
|
|
return false;
|
|
}
|
|
std::deque<TChar>& 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<TChar>* 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<int>(bufferLine.size()));
|
|
const int end = qBound(start, posOffset + length, static_cast<int>(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<int>(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<std::string>& captureList, std::list<int>& 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<std::string> const captureList;
|
|
std::list<int> const posList;
|
|
matchStatePair.second->multiCaptureList.push_back(captureList);
|
|
matchStatePair.second->multiCapturePosList.push_back(posList);
|
|
matchStatePair.second->nameCaptures.push_back(QVector<QPair<QString, QString>>());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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<std::string> captureList;
|
|
std::list<int> 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<std::string> captureList;
|
|
std::list<int> 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<std::string> captureList;
|
|
std::list<int> 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<int>(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<TMatchState*> 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<std::list<std::string>> 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<TColorTable> 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<TColorTable>(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();
|
|
}
|