Skip to content

feat(dr): player-supplied custom prep/cast/invoke messages - #1528

Open
MahtraDR wants to merge 3 commits into
elanthia-online:mainfrom
MahtraDR:feature/dr-custom-spell-messages
Open

feat(dr): player-supplied custom prep/cast/invoke messages#1528
MahtraDR wants to merge 3 commits into
elanthia-online:mainfrom
MahtraDR:feature/dr-custom-spell-messages

Conversation

@MahtraDR

Copy link
Copy Markdown
Contributor

Problem

DRCA's prepare?, cast?, and invoke match game responses against the message lists in base-spells.yaml. Players routinely get spells altered to have unique-to-them flavor text (and use unusual cambrinth foci / runestones) whose lines aren't in the shared lists. When a message isn't recognized, bput waits out its full timeout on every prep/cast/invoke — slow and spammy — with no way for a player to fix it short of a Lich release.

Solution

Three optional player settings, merged on top of the built-in lists (they never replace them):

Key Scope Applies to
custom_invoke_message per-character (setting) and per-spell (waggle entry) cambrinth invoke; runestone prepare my <rune>
custom_prep_message per-spell (waggle entry) normal spell prep (falls back to the existing custom_spell_prep setting)
custom_cast_message per-spell (waggle entry) cast

Each entry is a single string, matched case-insensitively and regex-capable.

# per-character (base.yaml / <name>-setup.yaml)
custom_invoke_message: "Your artifact hums with a strange resonance"

# per-spell (inside a waggle_sets entry)
waggle_sets:
  buffs:
    Fire Shield:
      abbrev: fireshield
      runestone_name: ruby
      custom_invoke_message: "The ruby runestone flares"    # runestone prep
      custom_prep_message: "Flames wreathe your hands"       # normal prep
      custom_cast_message: "A shield of fire springs to life"

Safety

A shared guard (custom_message_pattern) trims and compiles each entry with Regexp::IGNORECASE, and drops it (rather than applying it) when it is:

  • not a string,
  • blank/whitespace-only (an empty pattern compiles to // and would match every line), or
  • an invalid regex (which would otherwise raise inside bput).

Patterns are unanchored, so they match as substrings the way the built-ins do.

Backward compatibility

  • prepare? gains an optional custom_invoke_message: keyword; cast? gains an optional trailing custom_cast_message positional. Both default to nil; existing internal and external callers are unaffected.
  • The existing custom_spell_prep setting still works and is the fallback for custom_prep_message.
  • No changes to base-spells.yaml or any core file — the whole change lives in common-arcana.rb.

Tests

Adversarial + DAMP specs covering the guard and merge helper (hostile inputs — non-string types, empty/whitespace, invalid regex, list-under-a-singular-key), the per-character/per-spell invoke merge, runestone vs non-runestone behavior, and end-to-end wiring through the real invoke/prepare?/cast?. Rubocop clean; full suite green.

Notes

  • Duplicate-message detection (warning a player their custom line is already recognized) is intentionally left to config validation (e.g. validate.lic); a duplicate here is merely redundant, not harmful.
  • A companion dr-scripts docs PR + a wiki article documenting waggle setup and all options will follow.

🤖 Generated with Claude Code

Let players teach Lich the flavor text of altered/unusual spells, cambrinth
foci, and runestones so DRCA's prepare?/cast?/invoke stop waiting out the bput
timeout on messages that aren't in base-spells.yaml.

Adds three optional, case-insensitive (regex-capable) settings, merged on top of
the built-in message lists (never replacing them):

- custom_invoke_message  per-character (ritual focus / cambrinth) and per-spell
                         in a waggle entry (runestone "prepare my <rune>")
- custom_prep_message    per-spell (falls back to the existing custom_spell_prep)
- custom_cast_message    per-spell

A shared guard (custom_message_pattern) trims and compiles each entry with
IGNORECASE, dropping non-strings, blanks (which would compile to // and match
every line), and invalid regexes (which would raise in bput). Merging flows
through a single with_custom_messages helper. Patterns are unanchored, matching
as substrings like the built-ins.

Backward compatible: prepare? gains an optional custom_invoke_message: keyword and
cast? an optional trailing custom_cast_message positional, both defaulted; all
existing callers and the custom_spell_prep setting are unchanged. Change is
contained to common-arcana.rb; no core files or base-spells.yaml touched.

Tests: adversarial + DAMP specs for the guard/merge (hostile inputs, whitespace,
invalid regex, list-under-singular-key) plus end-to-end wiring through the real
prepare?/cast?/invoke. Rubocop clean; full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 05224c31-0f1d-4928-8469-cc5870242d00

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MahtraDR

Copy link
Copy Markdown
Contributor Author

@coderabbitai review in full

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR metadata =='
gh pr view 1528 --repo elanthia-online/lich-5 --json number,title,author,baseRefName,headRefName,changedFiles,additions,deletions,mergeable,state,url

echo '== Changed files =='
git diff --name-status "origin/$(gh pr view 1528 --repo elanthia-online/lich-5 --json baseRefName --jq .baseRefName)"...HEAD

echo '== Diff (no context truncation) =='
git diff --find-renames --find-copies --unified=80 "origin/$(gh pr view 1528 --repo elanthia-online/lich-5 --json baseRefName --jq .baseRefName)"...HEAD

echo '== Ruby outlines for changed files =='
git diff --name-only "origin/$(gh pr view 1528 --repo elanthia-online/lich-5 --json baseRefName --jq .baseRefName)"...HEAD \
  | while IFS= read -r f; do
      case "$f" in
        *.rb)
          echo "--- $f ---"
          ast-grep outline "$f" --items all --type class,module,function,method || true
          ;;
      esac
    done

Length of output: 67191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== prepare? call sites =='
rg -n -P '\bprepare\?\s*\(' -g '*.rb' -C 3 .

echo '== cast? call sites =='
rg -n -P '\bcast\?\s*\(' -g '*.rb' -C 3 .

echo '== invoke_messages/custom_invoke_message/custom_spell_prep usages =='
rg -n -P '\b(invoke_messages|custom_invoke_message|custom_prep_message|custom_cast_message|custom_spell_prep)\b' -g '*.rb' -g '*.yaml' -C 3 .

echo '== bput definitions and pattern normalization =='
rg -n -P '^\s*def\s+bput\b|\bbput\s*=' -g '*.rb' -C 30 .

echo '== Settings retrieval and settings object definitions =='
rg -n -P '^\s*def\s+get_settings\b|^\s*(class|struct)\s+\w*Settings|def\s+method_missing\b' -g '*.rb' -C 15 .

echo '== Relevant source and spec setup =='
sed -n '1,130p' lib/dragonrealms/commons/common-arcana.rb
sed -n '1,180p' spec/lib/dragonrealms/commons/common_arcana_spec.rb

Length of output: 120725


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Ruby version declarations =='
for f in .ruby-version Gemfile gemspec *.gemspec; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n -P 'ruby|required_ruby_version|filter_map' "$f" -C 2 || true
  fi
done

echo '== SetupFiles settings loading and OpenStruct conversion =='
ast-grep outline lib/common/setup_files.rb --items all --type method,function
rg -n -P 'OpenStruct|transform|deep_merge|merge|to_h|settings' lib/common/setup_files.rb -C 10

echo '== Exact changed regions and line numbers =='
nl -ba lib/dragonrealms/commons/common-arcana.rb | sed -n '340,415p;435,470p;505,555p;860,910p;990,1025p'

echo '== Existing use of filter_map =='
rg -n -P '\.filter_map\b' -g '*.rb' -C 2 .

Length of output: 8638


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Ruby version declarations =='
if [ -f .ruby-version ]; then
  echo '--- .ruby-version ---'
  cat .ruby-version
fi
if [ -f Gemfile ]; then
  echo '--- Gemfile Ruby declarations ---'
  rg -n -i -P '^\s*ruby\s+|required_ruby_version|ruby_version' Gemfile -C 2 || true
fi
find . -maxdepth 2 -type f \( -name '*.gemspec' -o -name 'gemspec' \) -print | while IFS= read -r f; do
  echo "--- $f ---"
  rg -n -i -P 'required_ruby_version|ruby_version' "$f" -C 2 || true
done

echo '== Existing filter_map usage =='
rg -n -P '\.filter_map\b' -g '*.rb' -C 2 . || true

echo '== DragonRealms settings transform configuration =='
fd -i 'settings*config*.rb' lib . || true
rg -n -P 'TRANSFORM_CONFIG|custom_spell_prep|SettingsTransformer' -g '*.rb' -C 12 lib

echo '== SettingsTransformer unknown-key behavior =='
fd -i 'settings_transformer.rb' lib . -x sh -c 'echo "--- $1 ---"; sed -n "1,280p" "$1"' sh {} || true

echo '== Changed implementation regions =='
sed -n '350,415p;440,470p;510,555p;865,905p;1008,1025p' lib/dragonrealms/commons/common-arcana.rb

Length of output: 57907


@MahtraDR I completed a full review. I used the repository preference to provide one consolidated result.

Findings

  • P2 — Validate the per-spell prep message before it overrides the global fallback.
    At Line 450, the expression data['custom_prep_message'] || settings['custom_spell_prep'] selects any truthy per-spell value before custom_message_pattern validates it. The same issue exists at Line 878 and Line 1021.

    If a spell has custom_prep_message: " ", custom_prep_message: "bad(", or a non-string value, the new guard correctly drops that value. However, it has already suppressed the existing settings['custom_spell_prep'] fallback. A player with a valid global prep message then waits for the normal bput timeout.

    Select the fallback after validation, or pass both values to the merge helper in per-spell-first order. Add coverage for a malformed or blank per-spell value together with a valid custom_spell_prep setting.

I found no other correctness issues in the changed behavior.


🧠 Learnings used
Learnt from: OSXLich-Doug
Repo: elanthia-online/lich-5 PR: 0
File: :0-0
Timestamp: 2026-03-11T23:17:06.572Z
Learning: For the elanthia-online/lich-5 repository, the PR author (OSXLich-Doug) requires that all CodeRabbit review feedback be delivered as one single coherent consolidated comment/review — not as multiple separate inline comments or review threads "dribbled into the stream." Always consolidate all observations into a single review pass with a unified list of findings.

You are interacting with an AI system.

…fallback

Addresses CodeRabbit's P2 review on PR elanthia-online#1528. The prepare? call sites used
`data['custom_prep_message'] || settings['custom_spell_prep']`, which selects by
truthiness: a blank/invalid/non-string per-spell value (e.g. "   " or "bad(") is
truthy, so || picked it and shadowed the valid global custom_spell_prep; the
guard then dropped the bad per-spell value, leaving no prep message and a full
bput timeout for a player who had a valid global set.

Pass both the per-spell and the global value to with_custom_messages via a new
custom_spell_prep: keyword on prepare?; each is validated independently, so a bad
per-spell value no longer suppresses a valid global. This also restores the
pre-PR behavior where the global custom_spell_prep applied to every spell.

Tests: regression coverage for a blank and an invalid-regex per-spell message
alongside a valid global custom_spell_prep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MahtraDR

Copy link
Copy Markdown
Contributor Author

@coderabbitai Valid finding — confirmed and fixed in 2bd6dbc.

The three prepare? call sites used data['custom_prep_message'] || settings['custom_spell_prep'], which selects by truthiness: a blank/invalid/non-string per-spell value (" ", "bad(", a number) is truthy, so || picked it and shadowed the valid global custom_spell_prep. The guard then dropped the bad per-spell value, leaving no prep message and a full bput timeout for a player who had a valid global set.

Fix: both the per-spell and the global value are now passed to with_custom_messages (via a new optional custom_spell_prep: keyword on prepare?), where each is validated independently by custom_message_pattern. A blank/invalid per-spell value is dropped without suppressing a valid global. This also restores the pre-PR behavior where the global custom_spell_prep applied to every spell (previously the || silently stopped applying it to any spell that had a per-spell message).

I took your "pass both to the merge helper" option (additive) rather than validity-based replace, since these are additive bput match patterns — an extra recognized line is harmless — and it's the more backward-compatible behavior.

Added regression specs: a blank per-spell message, and an invalid-regex per-spell message, each alongside a valid global custom_spell_prep, asserting the global still reaches the prep bput patterns. Rubocop clean; full suite green.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@MahtraDR Confirmed. The additive merge is correct for bput match patterns. It preserves custom_spell_prep when custom_prep_message is invalid, and it keeps both valid patterns available.

The regression coverage addresses the reported failure mode.


✏️ Learnings added
Learnt from: MahtraDR
URL: https://github.com/elanthia-online/lich-5/pull/1528

Timestamp: 2026-08-18T10:20:58.693Z
Learning: In `lib/dragonrealms/commons/common-arcana.rb`, custom prep message patterns are additive `bput` match patterns. `prepare?` must pass both the per-spell `custom_prep_message` and the global `custom_spell_prep` to `with_custom_messages` so that `custom_message_pattern` validates each value independently. An invalid, blank, or non-string per-spell pattern must not suppress a valid global `custom_spell_prep`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@mrhoribu mrhoribu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: feat(dr): player-supplied custom prep/cast/invoke messages (lich-5#1528)

Reviewed: 2 files, +379/-15, at d127e21f (head of feature/dr-custom-spell-messages, merged with main @ 4ed650ba) on 2026-08-21. Based on: full clone (history, blame, working tree, test suite, rubocop).
Verdict: Approve
Summary: Adds three new optional, regex-capable, case-insensitive player settings (custom_invoke_message, custom_prep_message, custom_cast_message) so DRCA's prepare?/cast?/invoke stop timing out on altered/flavor-text game responses that aren't in base-spells.yaml. The new messages are always additive to the built-in lists via a shared, well-guarded merge helper, and every one of the four internal call sites was updated consistently. 142 specs and rubocop pass clean.

Verification performed

  • Traced all four internal call sites of prepare?/cast? (ritual, cast_spell, crafting_cast_spell, crafting_prepare_spell) — all pass the new params consistently; confirmed via grep that no caller of prepare?/cast?/invoke exists outside common-arcana.rb, so there's no external blast radius to check.
  • Confirmed the new per-spell keys (custom_prep_message, custom_cast_message, custom_invoke_message) actually survive the settings pipeline: SettingsTransformer.enrich_spells (lib/common/settings_transformer.rb:60) merges base spell_data under the player's spell_setting for every waggle_set_keys entry (waggle_sets, per lib/dragonrealms/dependency/settings_config.rb:21), with the player's hash winning the merge — so these new keys aren't stripped or overwritten by base-spell enrichment.
  • Confirmed get_settings returns an OpenStruct (default) or a SettingsTransformer-built OpenStruct (game-specific), so get_settings.custom_invoke_message returns nil, not an exception, when unset — matching the test doubles in the spec (OpenStruct.new(...)).
  • Ran the full spec file: 142 examples, 0 failures. Ran rubocop on both changed files: no offenses.
  • Manually traced the regression the second commit fixes: with custom_prep_message: ' ' and custom_spell_prep: 'Valid global prep', with_custom_messages now validates each independently via filter_map, so the blank per-spell value is dropped and the valid global still gets appended — confirmed by the spec at spec/lib/dragonrealms/commons/common_arcana_spec.rb:422.

What this PR gets right

  • The guard is genuinely defensive and tested adversarially: custom_message_pattern (lib/dragonrealms/commons/common-arcana.rb:349) rejects non-strings, blank/whitespace-only strings (which would otherwise compile to // and match every line — a real footgun caught before it could ship), and invalid regex (caught via rescue RegexpError rather than raising inside bput). All three failure modes have dedicated specs.
  • Independent validation of per-spell vs. global prep message (the second commit) is a real fix for a real bug that would have silently broken existing custom_spell_prep users the moment they set any (even blank) per-spell value — good catch, and it shipped with regression tests.
  • Non-mutation of the base list is explicitly tested (with_custom_messages (merge helper)does not mutate the base list it was given), which matters since get_data('spells').prep_messages etc. are shared, cached data.
  • Consistent parameter threading across all four call sites, verified by direct inspection rather than trust.

Coverage notes

Full clone with history (git blame, git log -S), working tree at head, full test suite run, and rubocop run — no gaps in access. Not independently verified: in-game behavior of bput with a mixed string/Regexp pattern array (matches existing behavior for other Regexp entries already used elsewhere in base-spells.yaml, e.g. cast message list, so this isn't a new code path for bput itself) — this is a runtime confirmation the PR author is better placed to do than a static review.

@OSXLich-Doug OSXLich-Doug added the test The pull request is ready for all testing phases, unit, functional, integration and playability label Aug 21, 2026
@OSXLich-Doug
OSXLich-Doug self-requested a review August 22, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test The pull request is ready for all testing phases, unit, functional, integration and playability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants