#### Brief overview of PR changes/additions
- One budget of 100 covered every root trigger created while a line was
processed, and tripping it deactivated all of them, so a script arming
101 unrelated triggers lost all 101.
- Triggers created mid-line now carry the creation lineage they belong
to and how many generations deep they sit in it. A batch is one
generation however big it is; only a trigger that re-creates itself
keeps adding generations, so that is the only shape the budget counts.
The limit is 1000 generations, and only the runaway lineage is stopped
and named.
- Generations do not bound a lineage that widens as it deepens, so past
20000 creations on one line new triggers stop being offered that line.
Nothing is disowned there - they are all still armed for the lines that
follow.
#### Motivation for adding to Mudlet
#9697 fixed a real freeze, but its counter had no lineage, so it
destroyed legitimate triggers along with the runaway. Any routine arming
more than 100 triggers from a trigger loses them, permanent ones
included, which is a regression against 4.22.0.
#### Other info (issues closed, discussion etc)
Fixes a regression introduced by merged #9697; release-blocking for 5.0.
Test case: `lua fired=0; tempTrigger("GATE", function() for i=1,200 do
tempTrigger("PAY", function() fired=fired+1 end) end end);
feedTriggers("\nGATE\n"); feedTriggers("\nPAY\n"); print(fired)` -
prints 0 before, 200 after.
A/B against a shipped 4.22.0 binary (4.22.0 / 5.0 RC / this PR), counted
on the line after the one that armed them: 1001 unrelated temp triggers
1001 / 0 / 1001; two scripts of 600 each 600+600 / 0+0 / 600+600; 1001
permanent triggers 1001 / 0 / 1001. #9697's freeze is still stopped and
bounded (601 runaway lines in 8 s, RSS flat at ~820 MB) and its own
tests still pass. Eleven new tests parameterise the creation count, and
cover nested passes and folder/filter-chain children, which none of
#9697's did - that is why this shipped.
Assisted-by: Claude:claude-opus-5
#### Brief overview of PR changes/additions
- Comment-only. `git diff origin/development...HEAD` changes no
statement, expression or declaration - every added and removed line is a
comment. 238 comment lines become 98.
- Applies the house standard to the comments added by "fix: a trigger
that re-creates itself freezes Mudlet" (#9697) and "Fix user key
bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical
passages, and the rest cut to what a reader cannot derive from the code.
- Corrects four claims that were wrong, two of them inherited from those
PRs: a fires-per-line measurement taken with a smaller budget than the
one that shipped, an over-general note on `shortcutInstalledFor()`, a
`KeyUnit::disableKey()` note that had the mechanism backwards, and a
test comment crediting the `isEmpty()` guard for a result it does not
produce.
#### Motivation for adding to Mudlet
Both PRs merged while their comment-reduction pass was still in flight,
so the trim never landed with them.
#### Other info (issues closed, discussion etc)
The gotchas worth keeping survive in shorter form: why the same-line
creation budget is counted per pass rather than sharing the
`feedTriggers()` depth counter, why permanent triggers get
`deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather
than the deactivation is what stops `enableTrigger()` resurrecting a
spent trigger, that `QShortcutMap` retries with consumed modifiers
stripped, and the `Key_Backtab` versus `Shift+Tab` spelling.
The matching trim for "fix: stop treating long-time Mudlet users as
brand new players" (#9695) already landed separately as #9707, so it is
not repeated here.
No demo video: a comment-only change is not observable on screen.
**Test case:** `ctest` in the build directory - 79/80, with
`TelnetBenchmark` timing out only under parallel load (31s standalone
against a 60s limit) on a path this PR does not touch.
`TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`,
`ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass.
Assisted-by: Claude:claude-opus-5
#### Brief overview of PR changes/additions
- A trigger whose script creates another trigger matching the same line
kept extending the list `TriggerUnit::processDataStream()` walks, so the
line never finished: 100% CPU and RSS climbing 1.7 GB to 5.9 GB in 44
seconds, from one ordinary line of game text. Same-line matching for
triggers created mid-pass now has a budget (100 per line); when it runs
out the offending trigger is named in an error and what the loop created
during that line is stopped - temporary ones removed, permanent ones
switched off for the session only, so nothing is saved to the profile.
- `enableTrigger()` could resurrect a killed or expired temporary
trigger during the window before its deferred delete runs, so a one-shot
fired twice and a `killTrigger()`ed trigger fired 49 more times. It now
skips anything queued for cleanup, which is what makes the guarantee
`TTrigger::match()` states actually true.
- The behaviour restored by #9458 ("fix: triggers created by other
triggers react to the current line again") is kept: triggers created
while a line is being processed still match that line, chained creation
included. 10 new tests, and the 6 that pin that behaviour still pass.
#### Motivation for adding to Mudlet
Release blocker for 5.0 - the freeze is reachable from ordinary server
text with the standard "one-shot trigger that re-arms itself" idiom, and
4.22.0 was not affected.
#### Other info (issues closed, discussion etc)
5.0 QA findings C11 (hang) and C12. C11 was introduced by eb2627383
(#9458), which deliberately restored pre-#9267 same-line semantics
without bounding them; #9368's depth guard cannot see it, because
nothing recurses. The budget is deliberately its own constant rather
than the `feedTriggers()` recursion depth: the two measure different
resources, and sharing one made a pass entered deep in nested
`feedTriggers()` abort before running anything.
**Test case:** run `function arm() tempRegexTrigger("^HP: 100/100$",
[[arm()]], 1) end arm()` then `feedTriggers("HP: 100/100\n")` - on
development Mudlet freezes for good; here it reports the trigger and
carries on.
Assisted-by: Claude:claude-opus-5
#### Brief overview of PR changes/additions
The 12 functional-test sites that dereferenced the result of
`getActiveHost()`, `getArea()`, `getRoom()` or `getHost()` without first
checking it for null now guard the pointer with `QVERIFY(ptr)` before
use. The other lookup sites this PR touched already had a `QVERIFY` and
are left as they were.
#### Motivation for adding to Mudlet
CodeQL's `cpp/inconsistent-null-check` rule flagged these lookups
because some call sites checked the returned pointer for null while
others in the same files dereferenced it directly. Adding `QVERIFY(ptr)`
at the previously-unchecked sites makes the handling consistent: every
lookup result is verified before it is dereferenced, so a null result
fails the test loudly instead of crashing.
`QVERIFY` is the project's accepted idiom for guarding a pointer in a
test, so it is used here rather than an `if (!ptr) { QFAIL(...); }`
block. CodeQL does not recognise `QVERIFY(ptr)` as a null check (the
branch it generates tests `qVerify()`'s return value, not the pointer
itself), so it will keep reporting `cpp/inconsistent-null-check` on
these sites and on the pre-existing `QVERIFY` sites. Those alerts will
be dismissed as false positives rather than changing the test style to
satisfy the checker.
#### Other info (issues closed, discussion etc)
Test files only - no production code is touched. The 12 newly-guarded
sites are:
- `MapRoundTripTest.cpp` - 2 (`pAreaA`, `pAreaB`)
- `TriggerSameLineMatchTest.cpp` - 6 (`host`)
- `TFeedTriggersRecursionTest.cpp` - 4 (`host`)
**Test case:**
Build and run the five affected binaries under the functional-test flock
- all pass:
- `TFeedTriggersRecursionTest` - passed
- `TriggerSameLineMatchTest` - passed
- `MapRoundTripTest` - 6 passed, 0 failed
- `UndoServerWrapTest` - passed
- `UndoServerWrapReplay` - 2 passed, 1 skipped (manual replay tool;
skips without `REPLAY_CAPTURE`/`REPLAY_OUT`)
#### Brief overview of PR changes/additions
- A `tempTrigger()`/`tempRegexTrigger()`/`tempLineTrigger()` created
from another trigger's script matches the line being processed again,
instead of only starting on the next line
- Keeps #9267's snapshot iteration (its use-after-free fix stays
intact); root triggers registered during a pass are matched against the
current line afterwards
- New `TriggerSameLineMatchTest` with 6 cases - all fail without the fix
#### Motivation for adding to Mudlet
Long-standing behaviour that room-capture scripts rely on ("start
capture on the room title line, grab it and the following lines")
silently regressed in 4.21.0; reported on Discord with ~6 years of
scripts affected.
#### Other info (issues closed, discussion etc)
Regression from #9267, shipped in 4.21.0/4.21.1/4.22.0. Nested
`feedTriggers()` passes, chained trigger creation, and single-shot
(`expireAfter=1`) semantics all restored to match the old live-list
iteration.
**Test case:** Make a trigger matching some line that runs
`tempRegexTrigger("^(.*)$", [[echo("got: " .. matches[2] .. "\n")]],
2)`, then feed that line: the temp trigger must capture the creating
line itself, not start one line late. Or run `TriggerSameLineMatchTest`.
**Demo** (before: capture starts a line late; after: it includes the
creating line):
https://github.com/user-attachments/assets/be8d95aa-0691-4a9b-954e-e85f39e18a3d
---------
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>