Conversation
…ion msgs
CalcGeneralSaving: remove the two ` killer->get_name_str() == "Верий" ||
"Кудояр" || "Рогоза"` blocks that bypassed send_to_TC for those three
immortals. send_to_TC already gates the saving-throw debug trace on
EPrf::kTester / kCoderinfo / IsImpl, which is the correct opt-in. The
three immortals now see the line iff they're flagged like any other
tester.
CastCreation: three inline strings moved to the kDefault spell_msg.xml
sheaf, keyed on the cast spell so per-spell overrides can flavour them:
kItemNoPrototype "Что-то не видно образа для создания."
kItemCreatedToChar "Вы создали $o3."
kItemCreatedToRoom "$n создал$g $o3."
Files (4, +36 / -17):
src/gameplay/magic/spell_messages.{h,cpp} -- 3 enum entries + name tables
src/gameplay/magic/magic.cpp -- CalcGeneralSaving + CastCreation refactored
lib/cfg/spell_msg.xml -- 3 kDefault rows
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Migrate the 13 inline narration strings in SaySpell to kDefault sheaf
keys. Verbal-voice keys carry two variants each (one PC-flavoured, one
humanoid-NPC-flavoured); the container picks at random per emission so
both styles still appear in play without duplicating strings on every
spell. Sound voice collapses all six narration slots to one line for
non-humanoid NPC casters.
7 new ESpellMsg keys (kDefault sheaf):
kCastSayToSelf (2 verbal variants)
kCastSayToOther (2 verbal variants)
kCastSayToObj (2 verbal variants)
kCastSayToSomething (1 verbal variant -- PC and NPC styles coincided)
kCastSayDamageeToVict (2 verbal variants)
kCastSayHelpeeToVict (2 verbal variants)
kCastSaySound ("$n издал$g непонятный звук.")
SaySpell selector extracted to IsCasterVerbal(): non-humanoid NPC race
(default branch of the 5-race switch) returns false and the cast uses
kCastSaySound; everyone else uses the six per-situation keys. PC religion
and NPC random religion still pick between heathen/christian cast
phrases; sound voice skips phrase resolution entirely. PC-only caster
banner (kCastIncantToChar) and kNoRepeat opt-out preserved.
Behavior change: PC and humanoid-NPC verbal-narration styles are now
mixed at random rather than locked to caster role. A boggart may "прошептал"
and a player may "пробормотал" -- both lines exist for every verbal
caster.
The %s placeholder convention from the original sprintf-based code is
kept: viewers who Know the spell see the canonical name, others see the
cast phrase. kCastSaySound has no %s and the sprintf argument is silently
ignored, as is well-defined in standard C.
Files (4, +115 / -63):
src/gameplay/magic/spell_messages.{h,cpp} -- 7 enum entries + name tables
src/gameplay/magic/magic_utils.cpp -- SaySpell + IsCasterVerbal helper
lib/cfg/spell_msg.xml -- 12 verbal-variant rows + 1 sound row in kDefault
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… removal
Three new ESpellMsg keys in the kDefault sheaf:
kCastInterruptedToChar "Вы прервали заклинание !{name}!."
kCastPreparedToChar "Вы приготовились применить !{name}!."
kAfDispelledToOwner "Ваша магия была развеяна."
CallMagicToRoom: the single "Вы прервали ... и приготовились ..." line
(which combined the OLD and NEW spell names with two %s slots) split
into two separate sends -- kCastInterruptedToChar keyed on the OLD spell
with its name in {name}, kCastPreparedToChar keyed on the NEW spell with
the NEW name. Each side can be per-spell-overridden independently.
RemoveSingleAffectFromWorld (kRuneLabel cleanup on idle-quit etc.): the
hardcoded "Ваша рунная метка удалена." line replaced by a kAfDispelledToOwner
sheaf lookup. kRuneLabel sheaf overrides the default with its specialised
line so behavior is preserved; other spells using this path now also
notify the owner via the generic default.
New helper SendSpellNameMsg(ch, spell, key) factored into the file's
anonymous namespace -- standard sheaf lookup with optional {name}
substitution; reused by all three call sites.
Files (4, +54 / -3):
src/gameplay/magic/spell_messages.{h,cpp} -- 3 enum entries + name tables
src/gameplay/magic/magic_rooms.cpp -- SendSpellNameMsg helper + 2 call-site rewrites
lib/cfg/spell_msg.xml -- 3 kDefault rows + 1 kRuneLabel override
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two new ESpellMsg keys with generic kDefault placeholders + per-spell
overrides on the existing relevant sheaves:
kItemCreationFailToChar kDefault: "Создание не удалось."
kCreateWater: "Прекратите, ради бога, химичить."
kWrongTarget kDefault: "Это не подходящая цель для заклинания."
kIdentify / kFullIdentify: "С помощью магии нельзя
опознать другое существо."
Existing keys reused:
kItemCreatedToChar kCreateWater overrides the generic "Вы создали $o3."
with "Вы наполнили $o3 водой." (issue.point-bugs +
issue.spell-msg-improve flows now share the key).
kThirstToVict kCreateWater overrides with a literal "Вы полностью
утолили жажду." -- the manual SpellCreateWater path
bypasses CastToPoints, so no {intensity} expansion.
kSummonNoProto SpellMentalShadow's "Вы точно не помните..." line
replaced by the existing kDefault lookup (the
kDefault text already matches verbatim).
kSummonToRoom kMentalShadow overrides the 9 kClone-style random
variants with its single "Мимолётное наваждение
воплотилось в призрачную тень." success line.
Two existing kSheaves added: kIdentify, kFullIdentify (each holding just a
kWrongTarget override).
One inline line REMOVED outright: "Вы напоили $N3." -- the caster has no
way to gauge the target's prior thirst level, so the act() conveyed no
information beyond what kThirstToVict already tells the target.
Files (4, +72 / -11):
src/gameplay/magic/spell_messages.{h,cpp} -- 2 enum entries + name tables
src/gameplay/magic/spells.cpp -- SpellCreateWater (3 replaced + 1 deleted),
SpellFullIdentify, SpellIdentify,
SpellMentalShadow (2 replaced)
lib/cfg/spell_msg.xml -- 2 kDefault rows, 3 kCreateWater overrides,
kIdentify + kFullIdentify sheaves created,
kMentalShadow kSummonToRoom override
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six of SpellCharm's rejection paths share semantics with existing
kSummon* / kResurrect* keys. Migrate them to per-spell overrides on the
kCharm sheaf -- no new ESpellMsg keys, no kDefault changes (the kDefault
texts stay phrased for resurrect/summon contexts).
AFF kSanctuary / kProtect mob -> kResurrectConsecrated
AFF kGodsShield / kProtect mob -> kResurrectProtected
kNoCharm mob flag -> kResurrectNoPower
caster AFF kCharmed -> kSummonCharmed
aggressive victim -> kSummonFail
Will save failed -> kSummonFail (same key, second call site)
IS_HORSE victim -> kSummonWarhorse
The kCharm sheaf overrides preserve the original charm-specific wording
("Ваша жертва ..." / "Это боевой скакун ..." present tense / etc.) so
the resurrect/summon kDefault lines ("Оживляемый ...", "был боевой
скакун") stay correct for their original callers.
Four messages without a clean semantic match stay inline:
- "Вы просто очарованы своим внешним видом!" (self-cast humor)
- "Вы не можете очаровать реального игрока!" (PC target)
- "$M сейчас, похоже, не до вас." (act-style, target busy)
- "Следование по кругу запрещено." (circle_follow)
A small SendCharmMsg(ESpellMsg) lambda factored at the top of the
function shortens the seven rewritten else-if branches.
Files (2, +26 / -7):
src/gameplay/magic/spells.cpp -- SendCharmMsg lambda + 6 lookup rewrites
lib/cfg/spell_msg.xml -- 6 kCharm sheaf overrides
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five generic one-off slots in ESpellMsg for manual spells with quirky
single-use messages destined for migration to a Lua/Python scripting
system. The keys carry NO fixed semantic -- each per-spell sheaf
assigns its own meaning, documented by a one-line XML comment.
Policy (recorded as a doc comment in spell_messages.h):
- Use ONLY for messages unique to a single manual spell.
- NEVER use when a clean semantically-named key fits (SpellCharm's
earlier migration that reused kSummonWarhorse / kResurrectConsecrated
etc. is the right pattern; reaching for kCustomMsgOne would have
hidden the semantic match).
- Hitting the kCustomMsgFive cap on a single spell is a signal to
move that spell to scripts rather than extend the cap.
First usage: SpellCharm's self-cast humor line "Вы просто очарованы
своим внешним видом!" migrates to kCharm's kCustomMsgOne override. The
XML comment on the row records what the message represents.
Files (4, +33 / -1):
src/gameplay/magic/spell_messages.{h,cpp} -- 5 enum entries + policy
doc + 5 forward + 5
reverse name-table rows
src/gameplay/magic/spells.cpp -- self-cast line uses SendCharmMsg lambda
lib/cfg/spell_msg.xml -- kCharm sheaf gains kCustomMsgOne override
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Migrate ~32 inline narrative messages across 10 manual spell functions
to spell_msg.xml. Add two semantically-named keys for the shared
teleport-class pattern; route the rest through per-spell kCustomMsg
slots per the policy in spell_messages.h.
Two new ESpellMsg keys (kDefault placeholders + per-spell overrides):
kCastDisappearToRoom -- shared "actor leaves room" narration
kCastAppearToRoom -- shared "actor enters room" narration
Per-spell sheaves gain content:
kWorldOfRecall appear/disappear overrides
kTeleport appear/disappear overrides
kRelocate appear/disappear + kCustomMsgOne (azure-flash banner)
kSummon appear/disappear + kCustomMsgOne (block) +
kCustomMsgTwo (you-were-summoned) + kCustomMsgThree
(charmice follow-master arrival)
kPortal kCustomMsgOne (pentagram) + kCustomMsgTwo (pk variant) +
kCustomMsgThree (pentagram destroyed -- used by both
SpellPortal and RemovePortalGate)
kLocateObject kCustomMsgOne ("nothing felt")
kMentalShadow kCustomMsgOne (low-Int rejection)
kIdentify kCustomMsgOne (low-level self-target rejection)
kCharm kCustomMsgTwo/Three/Four (PC target / busy / circle)
spells.cpp call-site rewrites (32 sites):
SpellRecall 2 (disappear, appear)
SpellTeleport 2 (disappear, appear)
SpellRelocate 3 (disappear, appear, azure flash)
SpellSummon 7 (block + main triple + charmice triple)
SpellPortal 8 (pentagram per pk/normal x dest/source x char/room)
RemovePortalGate 4 (pentagram destroyed x 2 endpoints x char/room)
SpellLocateObject 1
SpellMentalShadow 1
SpellIdentify 1
SpellCharm leftover 3 (PC target, busy, circle)
Deliberately left inline / out of scope:
- mort_show_obj_values output rendering (UI labels, not narrative)
- extract_item recipe / book validation (skill domain)
- SpellCreateWeapon's 5 rejection lines (next round)
- SpellCharm's kAnimalMaster shape-change block (Lua territory)
- CheckAutoNosummon player setting toggle
- send_to_TC tester debug lines (per task brief)
Files (4, +133 / -46):
src/gameplay/magic/spell_messages.{h,cpp} -- 2 enum entries + name tables
src/gameplay/magic/spells.cpp -- 32 inline sites converted to sheaf lookups
lib/cfg/spell_msg.xml -- kDefault disappear/appear pair + 9 sheaf updates
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The ~390-line block inside SpellCharm that handles the kAnimalMaster
feat moves out into src/gameplay/skills/animal_master.{h,cpp} behind a
single ApplyAnimalMaster() entry point. The hardcoded race id 104 in
the gate is replaced by the named ENpcRace::kAnimal constant.
Logic is byte-for-byte identical to the prior inline block; this is a
pure mechanical extraction. SpellCharm now reads:
if (CanUseFeat(ch, EFeat::kAnimalMaster) && GET_RACE(victim) == ENpcRace::kAnimal) {
ApplyAnimalMaster(ch, victim, af, k_skills, skill_id);
}
The function takes the in-progress kCharm Affect, the k_skills and
skill_id outputs SpellCharm passes to create_charmice_stuff later, and
mutates `victim` in place (stats, flags, name, per-type skills/affects).
Files (4, +401 / -387):
src/gameplay/skills/animal_master.h -- public declaration
src/gameplay/skills/animal_master.cpp -- extracted body
src/gameplay/magic/spells.cpp -- 390-line block becomes 4-line call
meson.build -- new source listed
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage 2A: sweep the four magic files plus animal_master.cpp for noise
left behind by past refactors. The bulk of the changes drop trailing
"(issue.foo-bar)" tags from comments that still carry useful substance,
and remove pure-history narration ("X was retired in issue.Y, body
folded into Z") that no longer helps anyone reading the current code.
Dropped outright:
- Pure refactor-history one-liners with no current-state value
(e.g. "// CalcHeal retired in issue.mag-points -- body folded
into CastToPoints" -- the function is gone, no reader benefits
from remembering it once existed).
- The 3 dead `// SendMsgToChar(ch, "Найден в последователях...")`
debug prints in animal_master.cpp's charmice-type loop.
- A 6-line obsolete narration in CastToAlterObjs about a retired
ProcessMatComponents overload that was replaced ages ago.
- SYSERR log text "(issue.spellcomponents)." -- tracker
breadcrumbs help nobody at runtime.
Kept (with the issue tag stripped) where the comment explains WHY
the code looks the way it does or what a non-obvious choice was for.
Reworded the material-component matching comment to talk about the
rule itself rather than "the user laid out in issue.X follow-up".
No logic changes. Files (5, +67 / -84):
src/gameplay/magic/magic.cpp
src/gameplay/magic/spells.cpp
src/gameplay/magic/magic_utils.cpp
src/gameplay/magic/magic_rooms.cpp
src/gameplay/skills/animal_master.cpp
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ctions
Stage 2B targets the five biggest spells.cpp functions per the 200-line
ceiling rule. This commit handles the two cleanest decompositions; the
remaining three (SpellLocateObject 157 / SpellCharm 136 / CheckRecipeItems
178) stay as-is for now -- their bodies are essentially linear and pulling
a helper out would feel forced.
mort_show_obj_values 390 → 181 lines (under the ceiling)
Pulled the per-type detail switch into a new static helper
`ShowObjTypeSpecificValues(obj, ch)`. The 200+ line switch covers
scrolls/potions/wands/staves/weapons/armor/containers/books and
is self-contained -- the parent's pre-amble and post-amble (header,
accessibility, affects, set, enchants) are unaffected. Also dropped
the now-unused `j` / `li` / `drndice` / `drsdice` locals from the
parent.
SpellSummon 158 → 137 lines
Pulled the charmice follow-up loop into a new static helper
`SummonFollowingCharmices(ch, victim, vic_room, ch_room)`. Each
charmice gets relocated to the caster's room and emits the same
appear/disappear narration the main victim does, so the loop reads
as a single logical follow-on step.
Logic is byte-for-byte identical to before; this is pure extraction.
File: spells.cpp, +250 / -232.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage 2D targets the bigger functions in the two smaller magic files.
Only the clean wins ship here:
SaySpell 122 → 104 lines
Pulled the ~20-line caster-side banner (kCastIncantToChar lookup
with {color}/{name}/{nrm} placeholder substitution) into a new
static helper EmitCastIncantBanner(ch, spell_id). The PC/!NPC and
!kNoRepeat guards stay in the parent; the helper body is just the
sheaf lookup + sprintf-style substitution.
HandleRoomAffect 136 → 38 lines
The two largest per-spell cases (kDeadlyFog 53 lines and kThunderstorm
51 lines) each carry an inner switch on aff->duration with ~8 tick
branches. Each was pulled into its own helper:
HandleDeadlyFogTick(ch, duration)
HandleThunderstormTick(ch, aff)
The parent's outer switch on spell_id is now compact -- each case
is either a one-line cascade emit or a one-line helper call.
Skipped (under 200, naturally linear / dispatch-shaped):
- CastSpell (118), FindCastTarget (104), CallMagic (77) -- guard
chains where extraction would just shuffle lines around.
- CallMagicToRoom (156), UpdateRoomsAffects (74) -- linear pipelines.
No logic changes. Files (2, +132 / -119):
src/gameplay/magic/magic_utils.cpp -- EmitCastIncantBanner
src/gameplay/magic/magic_rooms.cpp -- HandleDeadlyFogTick,
HandleThunderstormTick
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six snake_case functions in the magic + spells files renamed to PascalCase, including one typo fix. \b word-boundary matching kept similarly-named symbols (show_weapon_affects_olc) untouched. The existing PascalCase / snake_case convention split is preserved: do_*(player commands), affect_to_char / affect_to_room / act / pk_* (legacy CircleMUD-wide API, would cascade across hundreds of call sites) all stay as-is. show_weapon → ShowWeapon (1 file) print_book_uprgd_skill → PrintBookUpgradeSkill (1 file, typo fix) add_rune_stats → AddRuneStats (1 file) mort_show_char_values → MortShowCharValues (1 file) is_room_forbidden → IsRoomForbidden (3 files) mort_show_obj_values → MortShowObjValues (7 files) Files (10, +27 / -27): src/gameplay/ai/mobact.cpp src/gameplay/clans/house.cpp src/gameplay/economics/auction.cpp src/gameplay/economics/exchange.cpp src/gameplay/economics/shop_ext.cpp src/gameplay/economics/shops_implementation.cpp src/gameplay/magic/magic.cpp src/gameplay/magic/magic.h src/gameplay/magic/spells.cpp src/gameplay/magic/spells.h Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…msg.xml
Retire the four SUMMON_FAIL / SUMMON_FAIL2 / SUMMON_FAIL3 / SUMMON_FAIL4
preprocessor defines in spells.cpp. The 30 call sites that fanned out
across SpellRecall / SpellTeleport / SpellRelocate / SpellPortal /
SpellSummon are now data-driven through per-spell sheaves.
Per-spell overrides added to spell_msg.xml:
kWorldOfRecall kSummonFail "Попытка перемещения не удалась."
kTeleport kSummonFail (same)
kRelocate kSummonFail (same)
kPortal kSummonFail (same)
kSummon kSummonFail (same)
kResurrectNoPower "Ваша жертва устойчива к этому."
(was SUMMON_FAIL2: target-resistant
branch, mirrors SpellCharm's
kResurrectNoPower override)
kResurrectProtected
"Магический кокон, окружающий вас,
помешал заклинанию сработать
правильно." (was SUMMON_FAIL3:
kGodsShield-blocked branch)
kCustomMsgFour "Ваша жертва в бою, подождите
немного." (was SUMMON_FAIL4: victim
fighting / below kRest branch)
The 27 SUMMON_FAIL (variant 1) call sites are routed through a small
static helper that hides the verbose sheaf lookup:
static void SendSummonFail(CharData *ch, ESpell spell_id) {
SendMsgToChar(MUD::SpellMessages().GetMessage(spell_id,
ESpellMsg::kSummonFail) + "\r\n", ch);
}
Each call site becomes a one-liner `SendSummonFail(ch, ESpell::kX)`
with the spell's identity made explicit. The kSummon-specific 2/3/4
variants are inlined with their respective sheaf keys (only 1-3 uses
each, helper would obscure the per-key intent).
The kCharm sheaf's kSummonFail override (set up earlier in the SpellCharm
rejection migration) is preserved -- SpellCharm's "Ваша магия потерпела
неудачу." continues to come from that sheaf with no behavior change.
Files (2, +54 / -34):
src/gameplay/magic/spells.cpp -- helper + 30 replacements + define
block removal
lib/cfg/spell_msg.xml -- 8 new sheaf overrides across 5 spells
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a new First Aid implementation alongside the legacy one. Switch by
flipping `#define BYLINS_FIRSTAID_NEW` at the top of firstaid.cpp:
the toggle ships at 0 (legacy behaviour); set to 1 to test the new one
in place. The legacy branch stays under `#else` for instant fallback
during debug. Delete it once the new version is verified in play.
The new mechanics, per the issue brief:
* `kAfCurable` is the source of truth for "what First Aid can cure".
The hardcoded GetRemovableSpellId list is bypassed; any affect carrying
the flag is eligible.
* Cure potency uses the same dice + skill + stat math the data-driven
spells use:
dice = roll(3d6)
skill_coeff = (min(skill, kNoviceSkillThreshold) * 3.0 +
max(0, skill - kNoviceSkillThreshold) * 1.0) / 100
stat_coeff = max(0, INT - 22) * 1.5 / 100
potency = (dice + skill_coeff + stat_coeff) * 1.0
Constants are hardcoded for now -- when spells.xml grows to cover skills
per kvirund's universal-ability-config direction, they migrate there.
* Contest gate mirrors DispelSucceeds: flat 5% luck floor, else
computed potency must exceed affect.potency. Tester/immortal trace
line shows the contest values.
* Cooldown applies only on failure (failed HP-heal roll, or lost
affect contest). Successful cures incur no cooldown.
* Argument syntax mirrors do_cast:
firstaid -> auto-pick easiest curable on self
firstaid <target> -> auto-pick easiest curable on target (legacy)
firstaid 'name' -> named affect on self
firstaid 'name' <target> -> named affect on target
Quote characters are ', *, ! (matching do_cast). Bare-target form is
preserved for backward compatibility.
* HP heal preserved verbatim from the legacy version -- low-level
fighters rely on it as their main healing route.
* "Triage easy stuff first": no-arg auto-pick chooses the kAfCurable
affect on the target with the LOWEST potency. Strong curses still
need a real dispel spell.
Both toggle states compile clean. 419 tests pass, boot smoke 0 SYSERR
under both. File: src/gameplay/skills/firstaid.cpp (+260 / -2).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Set BYLINS_FIRSTAID_NEW = 1 so the reworked First Aid runs in production for testing. The legacy implementation stays under `#else` as the immediate fallback if anything misbehaves -- flip the define back to 0 to revert without code surgery. Once the new version is verified in play, the legacy branch can be removed. 419 tests pass; boot smoke 0 SYSERR with the toggle ON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sync the spell-author manual with the post-issue.point-bugs /
post-issue.spell-msg-improve / post-issue.first-aid head and document
the kTestOne..kTestFive prototyping slots.
Per-section changes:
* Section 6 (<points>): rename <cond> -> <full> in the example, bullet
list, sign-convention header, and formula prose; explain the rename
(kept "cond" for the DRUNK/FULL/THIRST triplet, freed "full" for the
hunger slot to match the engine field name). Add a new
"Per-category narration" subsection covering the four per-category
message keys (kHealToVict / kMovesToVict / kThirstToVict /
kFullToVict), the unified descending-threshold Resolve, and the
empty <heal><degrade> + negative-threshold thirst/full <degrade>
intent.
* Section 11 (CastToPoints overview): drop the kPointsToVict mention,
reflect the per-category narration model and the issue.point-bugs
retirement of the single key.
* Section 12 (message keys table): retire kPointsToVict row; add the
four per-category restoration keys (kHealToVict / kMovesToVict /
kThirstToVict / kFullToVict); add kAffDispelledToOwner;
document the freshly-migrated fly-by lines from
issue.spell-msg-improve (kCantCastNotAlly,
kCastHereForbiddenToChar/Room, kCastIncantToChar,
kCastDisappear/AppearToRoom, kCastSay{ToSelf,ToOther,ToObj,
ToSomething,DamageeToVict,HelpeeToVict,Sound},
kCastInterruptedToChar, kCastPreparedToChar, kNoTarget,
kWrongTarget, kItemNoPrototype / kItemCreated* /
kItemCreationFailToChar, kResurrectNoPower / kResurrectProtected);
document the generic kCustomMsgOne..Five family.
* Section 15 step 1: point designers at the test-spell slots instead
of an engineer round-trip for prototyping.
* Section 16: remove the "Room spells" bullet — those are covered
by Section 11.5 since the issue.affect-flags room-affect-migration.
* New Section 17: "Test spell constants for prototyping" —
document kTestOne..kTestFive (ids 359-363), the
mode="kTesting" stub in spells.xml, the iteration workflow
(no rebuild beyond snapshot refresh), and the one-pass
graduation rename.
* Last-updated footer: rewritten to reflect the post-first-aid head
(points renames, message migration sweep, First Aid kAfCurable
rework, test-spell slots).
No code touched; pure docs.
The local RoomData *from_room = world[ch->in_room]; at townportal.cpp:110 was the leftover backing of the pkPenterUnique dead-write that issue.affect-flags removed. With that consumer gone, the local is orphaned and the compiler warns: warning: unused variable 'from_room' [-Wunused-variable] Delete the line. No behavioural change — the immediately-following comment in OpenTownportal already documents the cleanup.
Adds a third value to the <misc violent> attribute: "A" alongside "Y"
and "N". An A spell is helpful when cast on an ally (self, charmed-pet
chain rooted at the same PC, groupmate, NPC-vs-NPC mob casts) and fully
aggressive (PK liability, NPC retaliation, AR resistance, magic-mirror
reflection, dispel potency contest, etc.) when cast on an outsider.
Design (decided with kvirund):
* Storage: SpellInfo::violent_ is now spells::EViolent { kNo, kYes,
kAmbiguous } instead of a bare bool. SpellInfo::IsViolent() keeps
its bool signature but reports true only for kYes -- the conservative
back-compat behaviour for any call site that hasn't been migrated
yet (an A spell looks like a non-violent spell to those sites, so
it never accidentally fires PK-style logic on an outsider).
* Resolution helper: SpellInfo::IsViolentAgainst(caster, target)
returns the per-cast answer. For kAmbiguous it walks both caster
and target up their charm-chain to their "ally root", then declares
the cast non-violent when (self / shared root / NPC<->NPC root pair /
same group), and violent otherwise. The charm-chain walk is bounded
to depth 4 against pathological cycles.
* Parser: <misc violent> accepts "Y"/"1", "N"/"0"/empty, and "A"/"a"
via ParseViolent. Unknown values log CMP-level and fall back to kNo
so a typo doesn't silently turn the spell aggressive.
* Cross-check: kAmbiguous on a kMagRoom spell is a config error --
room paints have no per-target relationship to resolve. Loader
SYSERRs and clamps to kYes (conservative).
Call sites migrated to IsViolentAgainst (~20 spots) -- every site
that had concrete caster + target in scope:
* magic_utils.cpp: MayCastHere (4), FindCastTarget room/world PK
checks (2), CastSpell Peaceful per-victim check, SaySpell vict
narration key, EmitCastIncantBanner banner colour (now takes
optional tch; yellow for A when target unresolved),
CalcSpellSuccess god-flag prob mods (4 collapsed onto one helper
local).
* magic.cpp: TryReflect{ByMagicGlass, BySonicBarrier},
TryBlockByMagicalShield, OnCharAction AR check / immortal guard
/ AR-blanket / cast_debuff bit, DispelSucceeds char overload,
ReactToCast.
* dg_triggers.cpp: %violent% script var on MTRIG_CAST now reflects
the resolved sign of the cast against the trigger owner.
Call sites deliberately left on the existing IsViolent() -- semantics
are correct because A reports as "not always violent":
* do_cast.cpp / do_mixture.cpp self-target block: A self-cast allowed.
* dg_misc.cpp self-target fallback: same.
* magic_utils.cpp FindCastTarget self-fallback: A allows defaulting
to self when no target argument.
* magic_utils.cpp CastSpell NPC-alignment gate: NPC-vs-NPC casts of A
resolve to non-violent under the helper anyway, so the static
predicate matches.
* magic_utils.cpp CastSpell Peaceful no-target path: tightened to
GetViolent() != kNo (treats A as "could be violent" since there's
no target to resolve against).
* spell_capable.cpp Spell Capabler skill: requires kYes-strict damage,
A spells aren't damage spells.
* magic.cpp DispelSucceeds room overload: kMagRoom + A is a config
error caught at load, so this site never sees A.
* magic_rooms.cpp af[0].debuff: same.
* weather.cpp CalcComplexSpellMod: doesn't have a victim in scope;
leaving as IsViolent() means A spells skip weather modifiers. For
kDispellMagic (the only A spell today) this is moot -- it isn't an
element-based damage spell. A future issue can thread victim through
if/when an element-based A spell shows up.
UI: warcry list grew a third colour -- kColorBoldYel for kAmbiguous --
since the catalog row has no concrete target.
XML migration:
* kDispellMagic flips to violent="A" and drops kTarAllyOnly from
<targets> (the A semantics replace the old self/groupmate-only
restriction). The spell still uses the existing potency-gated
DispelSucceeds path, which now contests against outsiders' affects
and lets ally dispels through without a roll.
Verification: build clean (zero warnings), 419/419 gtests pass, boot
smoke reaches "Game started" with zero new SYSERRs (3 pre-existing
kNoTeleportOut <room_flags> entries unchanged).
…lback
The kDefault sheaf in lib/cfg/spell_msg.xml has carried generic
kAffDispelledToChar / kAffDispelledToRoom entries since the issue.3335
migration ("Волна тепла прошла по вашему телу." / "Быстрые тени
метнулись вокруг $n3."), but both RemoveAffectAndAnnounce overloads
in magic.cpp were looking those keys up via the sheaf-direct `[]`
operator, which bypasses kDefault entirely. The practical effect: any
affect whose own sheaf didn't author kAffDispelledTo* entries was
stripped silently (kSanctuary / kStability / kBless / kArmor / kFly /
kInvisible / kProtectFrom* / ~80 more), which made kDispellMagic feel
broken because the player saw no narration for most successful
dispels.
Switch both overloads to the fallback-aware
MUD::SpellMessages().GetMessage(removed, key) lookup so the per-affect
sheaf still wins where authored (10 affects today: kCurse, kPoison,
kBlindness, kHold, kSilence, kFever, kDeafness, four poison variants)
and kDefault carries every other affect. No XML changes needed -- the
entries were already there, just unreachable.
The room overload had an additional comment noting that "existing
keys were authored for char dispel and may read awkwardly until
designers adapt them"; that caveat still applies and is preserved.
Verification: build zero-warning clean, 419/419 gtests pass, boot
smoke reaches "Game started" on port 7778 with no new SYSERRs (same
3 pre-existing kNoTeleportOut <room_flags> + pre-existing missing-
trigger entries, both predate this branch).
Adds a presence-only <weave/> child to <components> alongside the
existing <verbal/>. A weave spell is "actual magic" and cannot be cast
in a room with the kNoMagic flag -- the CallMagic-side gate emits the
same kCastForbiddenTo{Char,Room} narration the legacy data-driven
<blocking><room_flags val="kNoMagic"/></blocking> did. Together with
removing that block from every weave-tagged spell, weave is now the
single source of truth for "is this magic", eliminating the repeating
247-times-duplicated blocking pattern.
Component class (talents_actions.{h,cpp}):
* Components::has_weave_ mirrors has_verbal_ -- presence-only parse,
no attributes.
* Components::HasWeave() accessor.
* Components::empty() and Print() updated.
CallMagic gate (magic_utils.cpp:472):
* New local weave_blocked = HasWeave() + ROOM_FLAGGED(kNoMagic).
* Combined with the existing data_blocked = IsRoomBlocked(...) into
one conditional. MayCastInForbiddenRoom() (greater gods, uncharmed
NPCs) bypass applies symmetrically.
* Fizzle narration sheaves unchanged -- kCastForbiddenToChar
(kDefault fallback) and kCastForbiddenToRoom continue to apply,
with per-spell overrides (kRuneLabel) still winning.
spells.xml migration:
* 209 spells now carry <weave/>:
- 196 already had a <components> block -- <weave/> appended after
the existing <verbal/> entry (or at the start of <components> for
the rare spells without <verbal/>).
- 13 clear magic spells that the spellcomponents migration missed
got a fresh <components><weave/></components> block inserted
after <name>: kDazzle, kChillTouch, kShockingGasp, kSonicWave,
kRoomLight, kGlitterDust, kRuneLabel, kPaladineInspiration,
kArrowsFire/Water/Earth/Air/Death.
* 38 doubtful entries left alone per kvirund's "actually magic" rule
-- they carry kNoMagic blocking but aren't user-cast magic in the
weave sense: 4 poisons (kAconitumPoison etc.), 5 mob breath attacks
(kFireBreath etc.), 2 affect-only entries (kTimerRestore, kCombatLuck),
27 kService skills/state-affects/internal machinery (kHide, kSneak,
kCamouflage, kDrunked, kAbstinent, kBattle, kCourage, kFrenzy,
kBerserk, kReligion, kHaemorrhage, kMagicBattle, kStrangle,
kRecallSpells, kSolobonus, kExpedient, kExpedientFail,
kLowerEffectiveness, kNoInjure, kNoCharge, kConfuse, kQUest,
kClanPray, kLightWalk, kBandage, kNoBandage, kCapable). Their
pre-existing kNoMagic blocking stays as-is; cleanup of those is a
separate refactor not in scope.
* 19 spells had no kNoMagic blocking to begin with and stay untouched:
13 warcries (kWarcry*) + 5 test slots (kTestOne..kTestFive) +
kUndefined.
* kNoMagic stripped from the 5 multi-flag <room_flags> entries
(kTeleport / kWorldOfRecall / kRelocate / kRuneLabel /
kBlackTentacles) so their other room blocks stay intact.
* 26 spells whose only <action> content was the kNoMagic blocking
now have their empty <action></action> and <talent_actions>
</talent_actions> stripped too -- pure XML hygiene, loader treats
empty as no-op anyway.
XML stats: 247 kNoMagic mentions -> 38 (the 38 skipped doubtful);
209 <weave/> added; 175 <blocking> blocks removed entirely + 34
trimmed; -395 net XML lines.
Verification: build zero-warning clean, 419/419 gtests pass, boot
smoke reaches "Game started" on port 7779 with no new SYSERRs (same
3 pre-existing kNoTeleportOut <room_flags> + pre-existing trigger
SYSERRs, both predate this branch).
…ffect_flags=>
Rework the <caster_blocking> schema:
Before:
<caster_blocking>
<align val="kEvil"/>
</caster_blocking>
After:
<caster_blocking>
<caster align="kEvil" affect_flags="kHold|kCharmed"/>
</caster_blocking>
The inner tag is renamed <align> -> <caster>, the val= attribute
becomes the align= attribute, and a new optional affect_flags=
attribute (|-separated EAffect list) lets the gate fire when the
caster carries any of the listed affect flags.
Both attributes are optional and accumulate onto the existing shared
FlagCondition storage:
* align="..." -> cond.align (EAlign::kGood / kEvil / kNeutral)
* affect_flags="..." -> cond.affect_flags (vector<EAffect>)
The check itself doesn't change: TargetIsBlocked(caster, cond) already
iterated cond.affect_flags + matched cond.align since the
issue.cast-dmg-migration days. So the new affect_flags attribute is
wired end-to-end the moment the parser populates it.
Schema-wise <caster_blocking> now diverges from <blocking>/<required>
(which still use the multi-child-tag form mob_flags/affect_flags/
room_flags/align). The caster gate is conceptually a single
descriptive entry, not a multi-axis AND of loose conditions -- making
it an attribute-bundle on a single <caster> child fits better.
Parser changes (src/gameplay/abilities/talents_actions.{h,cpp}):
* New ParseCasterBlocking(FlagCondition&, DataNode&) reads only the
new <caster .../> shape.
* ParseAction dispatches <caster_blocking> to ParseCasterBlocking
instead of the shared ParseFlagCondition.
* ParseFlagCondition stays untouched -- still serves <blocking>/
<required>.
XML migration (lib/cfg/spells.xml):
* kDispelEvil (line 601) and kDispelGood (line 1197) -- the only two
spells using <caster_blocking> today -- updated to the new
<caster align=.../> shape. No spell uses affect_flags yet; the
attribute lands as a forward-shaped extension point.
Verification: build zero-warning clean, 419/419 gtests pass, boot
smoke reaches "Game started" on port 7780 with no new SYSERRs (same
3 pre-existing kNoTeleportOut <room_flags> + trigger-vnum entries
that predate this branch).
Mirror of <unaffect potency_weight=> on the affects side. Default 1.0
(no change -- full back-compat with every spell that doesn't set it).
Smaller values (e.g. 0.5) record a deliberately weaker affect, which
the future dispel contest in DispelSucceeds finds easier to beat. Use
case: a big-modifier spell where the same potency roll feeds both
modifier and stored potency can make the affect undispellable just
because the modifier needs to be big. potency_weight lets the author
decouple the two -- keep the modifier strong, scale the stored
potency down to a sane range.
Storage (talents_actions.h):
* float potency_weight_{1.0f} on TalentAffect.
* GetPotencyWeight() accessor with a comment cross-referencing
TalentUnaffect::potency_weight_ for symmetry.
Parser (talents_actions.cpp TalentAffect ctor):
* Reads "potency_weight" attribute of the <affects> tag with the
(p && *p) optional-attribute guard. Default 1.0f. Mirrors the
TalentUnaffect parser line-for-line.
Print (talents_actions.cpp):
* Appended " potency_weight=<n>" to the Affect: header line so
mort_show_spell traces include the scaling factor.
Impose sites (2):
* magic.cpp TryApplyAffectTalent (line 946):
taf.potency = cast_potency * talent.GetPotencyWeight();
* magic_rooms.cpp CallMagicToRoom (line 495):
af[0].potency = CalcCastPotency(roll.potency) * talent.GetPotencyWeight();
Both sites already had `talent` in scope, so the multiplication is a
single-token addition. The dispel-side scale (TalentUnaffect's
potency_weight) is unchanged -- the two now compose independently:
stored_strength = cast_potency * affects.potency_weight;
dispel_strength = (dispel_roll) * unaffect.potency_weight;
dispel_succeeds = dispel_strength > stored_strength.
No spell.xml entries use the new attribute yet -- it lands as a
forward-shaped tuning knob. The 1.0 default keeps every existing
spell's behaviour byte-for-byte.
Verification: build zero-warning clean, 419/419 gtests pass, boot
smoke reaches "Game started" on port 7785 with the same pre-existing
SYSERR set as before (3 kNoTeleportOut + the inherited trigger/
object/zone/mob warnings -- all predate this branch). The
kNoTeleportOut SYSERRs are still present because fix.noteleportout-
mapping has not yet been merged into sventovit.work.
ERoomFlag::kNoTeleportOut exists in entities_constants.h:556 and was
already being referenced by 3 spells.xml entries (kWorldOfRecall,
kTeleport, kRelocate) via <blocking><room_flags val=...>, but the
flag name was missing from kBlockingRoomFlagByName in talents_actions.cpp.
ParseFlagCondition therefore logged
SYSERROR: Actions: unknown ERoomFlag 'kNoTeleportOut' in <room_flags>.
three times on every boot/reload and silently dropped the flag from
the parsed condition.
Pre-issue.weave-component this was a latent bug -- the room_flags val
strings on those spells were "kNoMagic|kNoTeleportOut", so kNoMagic
carried the gate work and the silent skip of kNoTeleportOut went
unnoticed. issue.weave-component then stripped kNoMagic (since weave
now does that job), leaving the val as just "kNoTeleportOut" -- which
the parser then drops entirely, regressing the kNoTeleportOut gate
that issue.no-teleport-out had data-driven.
The fix is a single new entry in kBlockingRoomFlagByName. No XML
changes needed -- the 3 spells already reference the correct flag
name. From this commit on, kTeleport / kWorldOfRecall / kRelocate
correctly fizzle in kNoTeleportOut rooms via the data-driven
<blocking> path.
Verification: build zero-warning clean, 419/419 gtests pass, boot
smoke reaches "Game started" with ZERO SYSERRs in the syslog window
(versus the 3 kNoTeleportOut + 2 trigger-vnum entries every prior
boot had). The pre-existing trigger-vnum entries appear to be tied
to small-world content state, not loader bugs -- they were absent
this run.
End of phase 1 of the data-driven spell system. From this point on, unstable is the working branch for feature work and its derivatives. Future syncs of master and sventovit.work into unstable continue via the same merge pattern.
Engine-side zone-type registry migrated from the hand-rolled
KOI8-R-marker text parser (InitZoneTypes() in db.cpp + struct
ZoneCategory in entities/zone.h + global `zone_types[]` array) to the
info_container partial specialization used by guilds.
New schema (lib.template/cfg/zone_types.xml):
<zone_types>
<type text_id="kNone" vnum="0" name="no_ingredients"/>
<type text_id="kTown" vnum="1" name="town"/>
<type text_id="kLightForest" vnum="2" name="light_forest">
<ingredients val="0|2|11|12|17|104"/>
</type>
...
</zone_types>
vnum= preserves the historical 0..24 slot ordering, so every existing
.zon file's `zone.type` integer keeps resolving to the same category
without a world-data sweep. text_id is a stable C++-style identifier
for documentation/OLC; name is the display label (current English
transliterations preserved verbatim, content pass for Russian names
can land separately). <ingredients> is an optional pipe-separated list
of integer im_type ids consumed by im.cpp's ImForageRoom; one example
non-empty list seeded on kLightForest per the issue brief.
New files:
* src/engine/entities/zone_types.{h,cpp}
- ZoneTypeInfo (BaseItem<int> partial specialization with
text_id + name + ingredients_)
- ZoneTypeInfoBuilder (parses <type> + optional <ingredients>)
- ZoneTypesLoader (ICfgLoader Load/Reload via cfg_manager)
- using ZoneTypesInfo = info_container::InfoContainer<int, ...>
* lib.template/cfg/zone_types.xml (25 entries)
Wiring:
* cfg_manager.cpp registers "zone_types" -> cfg/zone_types.xml.
* db.cpp: InitZoneTypes() body deleted; the line-826 boot step
replaced with MUD::CfgManager().LoadCfg("zone_types") so the
pre-world load ordering is preserved (zones still reference
type vnums after this point).
* do_reload.cpp: two InitZoneTypes() call sites swapped for
MUD::CfgManager().ReloadCfg("zone_types"); the `reload ztypes`
immortal command now hot-reloads via the standard path.
* global_objects.{h,cpp}: MUD::ZoneTypes() / MUD::ZoneType(int)
accessors added next to Guilds() mirrors.
Consumers migrated to MUD::ZoneTypes()[type] (no signature changes):
* im.cpp ImForageRoom: ingredient walk now reads GetIngredients()
(a const std::vector<int>&), empty-list short-circuit preserves
the legacy ingr_qty==0 semantic.
* do_show.cpp do_show zone: GetName().c_str() formatted into the
`stat zone` row.
* zedit.cpp: 3 sites + the `extern struct ZoneCategory *zone_types`
decl removed; the type-list menu now iterates the registry
directly (vnums no longer required to be 0..N-1 contiguous), the
type-parse branch uses IsKnown(vnum) instead of the sentinel walk.
* yaml_world_data_source.cpp GetZoneTypeName: registry IsKnown +
GetName().
Tooling:
* tools/converter/convert_to_yaml.py: added a load_zone_type_names_xml
parser; the main() candidate loop now prefers the new XML and
falls back to the legacy ztypes.lst for older world snapshots.
Old artifacts removed:
* struct ZoneCategory + global zone_types[] in entities/zone.{h,cpp}
(replaced by docstring redirecting to zone_types.h).
* InitZoneTypes() declaration in db.h + body in db.cpp.
* lib.template/misc/ztypes.lst (lib/misc/ztypes.lst and
build/small/misc/ztypes.lst are gitignored working copies and
were removed locally too).
Verification: build zero-warning clean, 419/419 gtests pass, boot
smoke reached "Game started" on port 7786 with cfg_manager loading
zone_types via the new XML, then a follow-up boot on port 7788 after
deleting ztypes.lst confirmed the engine is self-sufficient on the
new registry. No new SYSERRs introduced (still the same pre-existing
trigger-vnum / object / zone-cmd warnings unrelated to this branch).
Migrate the mob-race system to an info_container with per-race flags and
boolean traits, and replace scattered GET_RACE(ch)==kX checks with data lookups.
Infrastructure:
- Move ENpcRace (+ kNpcBoss/kNpcUnique) from entities_constants.h to
mob_races.h; entities_constants.h re-includes it at the bottom (compat shim,
placed after every enum so the heavy info_container/cfg_manager includes that
transitively re-enter it always see the full constant set).
- Add meta_enum name tables (ITEM_BY_NAME / NAME_BY_ITEM / NAMES_OF) for
ENpcRace (mob_races.cpp) and EMobFlag / ENpcFlag (entities_constants.cpp).
- Rebuild mob_races.{h,cpp} as an int-keyed InfoContainer<MobRace>: MobRace
gains mob_flags / npc_flags FlagData + vocal/respiration/skinnable traits and
keeps the ingredient drop list. New MobRacesLoader (IEditableCfgLoader) is
registered in cfg_manager as "mob_races" (Vedun editing + hot reload via
`reload mobraces`); MUD::MobRaces() accessor added. im.cpp / do_reload.cpp
repointed off the old global map.
- mob_races.xml: ids are ENpcRace tokens (kHuman, ...); <prob> values moved to a
`val=` attribute (parser_wrapper has no inner-text); per-race <mob_flags> /
<tzs_flags val="kA|kB"> + <traits> tags added.
Behaviour wiring:
- ENpcFlag::kUsingMagicItems added; race flags are OR'd onto each loaded mob
INSTANCE in ReadMobile (never onto prototypes). Mob-race load moved ahead of
the first zone reset so boot-spawned mobs get stamped.
- IsCasterVerbal + IsAbleToSay collapsed into one <vocal/>-driven IsAbleToSay.
- New CanBreathe(ch): strangle (mobact.cpp, strangle.cpp) now gates on
"breathes (race <respiration/>) and not undead".
- mob_casting magic-item use gated by kUsingMagicItems (human-only, preserved);
summon_mob_helpers shout now fires for any vocal race.
- DoSkinning gated by the <skinnable/> trait.
Trait mapping chosen to preserve current behaviour exactly. Build clean,
470 tests pass, boots clean (SYSERR 222 baseline, races parse without error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… editor scheme
Follow-up to the main issue.npc-races commit, per kvirund's addendum.
1+2. Undead detection via a flag instead of IS_UNDEAD (option B: a new flag,
distinct from kCorpse which keeps its "leaves no corpse / dust on death"
meaning -- it is also set on non-undead summons like the guardian-angel
tutelar, so it must not double as the undead marker):
- New EMobFlag::kUndead (kIntOne | 1<<27); added to the meta_enum name table,
the action_bits display array ("нежить"), and the sqlite world flag map.
- Removed the inline IS_UNDEAD(CharData*); its 5 call sites (strangle,
turnundead, expedientcut, mobact, warcry-in-magic) now test
IsFlagged(EMobFlag::kUndead). IsFlagged is NPC-gated, matching IS_UNDEAD's
old !IsNpc()->false behaviour.
- Behaviour preserved: the two kResurrected code sites (raise-corpse,
animate-dead) now also set kUndead; the zombie & ghost races carry
<mob_flags val="kUndead"/> so every instance of those races is flagged on
load (item-8 mechanism) -- reproducing the old race-based detection without
per-mob data migration.
- ExtractCharFromWorld logs a SYSERR when a mob has kResurrected but not
kUndead (designer set kResurrected by mistake), per the addendum.
3. Moved lib/cfg/mob_races.xml -> lib/cfg/mechanics/mob_races.xml (mob races are
mechanics); cfg_manager path updated.
4. Added lib/cfg/mechanics/mob_races.scheme (Vedun editing scheme) and registered
ENpcRace / EMobFlag / ENpcFlag in the editor enum registry.
Build clean, 470 tests pass, boots clean (SYSERR 222 baseline; mob races load
from the new path, kUndead parses, no designer-error logs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the mob-race ENpcFlag tag from tzs_flags to npc_flags across the loader, the editor scheme, and mob_races.xml (the MobRace member was already npc_flags_). Behaviour-preserving. Build clean, 470 tests, boots clean (SYSERR 222 baseline). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Vedun editable-set keyword (EditableWhat) for mob races used an underscore, unlike every other set (spell / spellmsg / skillmsg / hitmsg). Rename to "mobrace" for consistency; the command is now `vedun mobrace <kRace>`. The XML <mob_race> element tag (the data format) is unchanged -- the keyword is independent of it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onjurations The old EMobFlag::kSummoned actually marked "magic ally" instances (population accounting, combat-AI assist/heal, charm, follow): enchanted beasts (умки), animated corpses / necro minions, and keeper/firekeeper/clone. Rename it to kCompanion to match that meaning -- a companion need not be strictly "summoned". Pure rename: same bit (kIntOne|1<<26), same membership, no behaviour change. Display label "*призванный" -> "*спутник". Add a new EMobFlag::kSummoned (kIntOne|1<<28, display "*призванный") meaning the creature was conjured "from other realms" -- a banishable subset of kCompanion, so future spells can dispel specifically-summoned entities. Set at the five true- conjuration handler sites: SummonTutelar, SpellMentalShadow, SetupKeeperStats, SetupFirekeeperStats, CloneCascade (both the clone and each extra double). Deliberately NOT set in the shared FinalizeSummonedMob, since that path also creates animated corpses (CorpseSummon), which are companions but not "summoned". Per kvirund: tutelar & mental shadow (loaded via positive ReadMobile) keep their current accounting and get ONLY kSummoned, not kCompanion (flipping them would need a load-path change to keep mob_index.total_online correct). Renamed all 18 usage sites; meta_enum table + sqlite flag map updated (legacy "kSummoner" alias now -> kCompanion; added kCompanion + kSummoned names). World data uses neither name. Build clean, 470 tests, boots clean (SYSERR 222). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y spells Reshape the per-spell <success_roll> to match the planned success mechanic and the abilities roll system (for later unification), and remove the dead #3333 plumbing. Format (talents_actions::SuccessRoll, new): - NO <dices> -- success depends on randomness differently (planned: d100 vs a skill/stat-derived target with crit thresholds). - <base_skill> / <base_stat>: same shape as <potency_roll> (Roll). - <bonus> (optional): roll / pvp / pve / evp / eve integer attributes (flat + per caster-vs-target matchup, P=player E=env/NPC). - <thresholds> (optional): critsuccess (default 6) / critfail (default 95), matching the abilities defaults. Plumbing: the #3333 success RollResult was computed into CastContext but never consumed and its dice shape no longer fits. Removed CastContext.success()/the ctor param and the Compute/BuildCastContext eval; SpellInfo now just parses & stores the SuccessRoll (GetSuccessRoll) for the future mechanic. Updated the two direct CastContext constructions (handler.cpp, spell_holystrike.cpp). Data: added <success_roll> (base_skill + base_stat copied from <potency_roll>) to all 188 remaining spells that have a potency_roll (189 total now); stripped the stray <dices> from kChillTouch's success_roll. Spells without a potency_roll are left untouched (per decision). No <bonus>/<thresholds> in data yet. Editor: spells.scheme success_roll updated (drop dices, add bonus/thresholds child defs + tag defs); boot scheme-lint clean for the new tags. Build clean, 470 tests, boots clean (SYSERR 222), all success_roll blocks parse. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gicSkillId GetMagicSkillId(spell_id) mapped a spell's element -> magic-school skill via a switch. The skill is now authoritatively the success_roll's base_skill, so the function is redundant. Replaced all 8 call sites with MUD::Spell(spell_id).GetSuccessRoll().GetBaseSkill() (fight_hit, magic_item, animal_master, mem_queue, magic_utils x2, magic.cpp x2), and removed the declaration (spells.h) + definition (spells.cpp). animal_master.cpp gained the spells_info.h / global_objects.h includes. Behaviour-preserving: every spell with a <success_roll> carries the same magic- school base_skill the element switch returned (kCharm->kMindMagic, kCloudOfArrows->kFireMagic, ...); the only spells without a success_roll are kPowerDeafness (element kTypeMind, which the switch already mapped to kUndefined) and the disabled kUndefined sentinel -- both yield kUndefined either way. Build clean, 470 tests, boots clean (SYSERR 222). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ild)
Before deciding whether to offload XML parsing to a worker thread, measure where
the hot-reload time actually goes. CfgManager::LoadCfg/ReloadCfg now time the two
phases separately -- DOM parse (file read + pugixml) vs build+swap (constructing
the new register) -- and log per-file: "Cfg load/reload [id]: parse X ms + build
Y ms = Z ms (file)".
Finding (release build, full lib/cfg/spells.xml = 332 KB / 264 spells):
spells: parse 1.04 ms + build 2.65 ms = 3.69 ms
classes: parse 0.02 ms + build 5.14 ms = 5.17 ms
(all other cfg files are well under 1.5 ms)
do_reload "spells" runs ONLY ReloadCfg("spells"), so its whole cost is ~3.7 ms --
not a noticeable lag today. Any perceived "reload" lag is the broader `reload all`
reinit work (HelpSystem::reload_all, ReloadSpecProcs, GlobalDrop::init, ...), not
the spell XML parse. The instrumentation stays in so we can watch spells.xml grow
as abilities migrate in and revisit threading only if it nears a 40 ms pulse.
Build clean, 470 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
radix_trie.{h,cpp} was dead code -- no references anywhere in src/ (the engine's
prefix tree is CompactTrie, used by the command interpreter). It was also an
incomplete, set-only implementation with a node-split terminality bug and a
non-functional DfsIterator stub, plus a 256-pointer-per-node layout that defeats
a radix trie's space advantage. Future command/spell/ability name search will
reuse CompactTrie instead.
Removed src/engine/structs/radix_trie.{h,cpp}, tests/radix.trie.cpp, and their
meson entries. Build clean; tests 470 -> 463 (the 7 RadixTrie tests removed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…earch Pilot of separating display text from gameplay config (toward localization). Spells are the right pilot: a spell name is a single string (no cases). Container (msg_container.h): MsgSheaf gains an optional <name val="..."/> (parsed by MsgSheafBuilder, stored as name_); MsgContainer gains GetName(id) and FindByName(name)->id (the "search the string set, return the id" entry point, matching by utils::IsEquivalent). Forward-compatible: cased entities will later add <case> children to <name>. Spells: - spells.xml <name> keeps only eng="" (admin label); the Russian name moved to spell_msg.xml as <name val="...">. - Load order reversed: spell_messages now loads BEFORE spells, so SpellInfoBuilder::ParseName reads the display name from the container (SpellInfo::name_ = SpellMessages().GetName(id)); name_eng_ stays from eng=. - FindSpellIdWithName now delegates to SpellMessages().FindByName (search runs against the string container, not the gameplay records). Localization layout (option d): the base file IS the current language; future locales go in per-language directories. No locale dirs created yet (none exist). Data: migrated 263 spell names spells.xml -> spell_msg.xml (251 into existing sheaves, 12 new sheaves); stripped rus= from spells.xml. Schemes updated (spell_msg.scheme declares <name>; spells.scheme <name> drops rus). Caveat: reloading spell_messages alone leaves SpellInfo::name_ stale until spells is also reloaded (the name is copied at spell-build); reload both / reload all. Build clean, 463 tests, boots clean (SYSERR 222; spell_messages loads before spells; scheme lint clean for the new <name>). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…schemes 1. spells.xml <name eng="..."/> -> <name val="..."/> (consistent with spell_msg.xml's <name val=...>). ParseName reads "val"; spells.scheme attr renamed eng->val. (The attribute still holds the English admin label; only the attr name changed.) 2. do_reload: reloading a string file that another container caches at build time now prints a reminder to reload the dependent file. Driven by an extensible map (kReloadReminders); first entry: reload spell_messages -> reminder to also 'reload spells' (spell names are cached at spell load). 3. git-track the remaining Vedun editor schemes (hit_msg.scheme, skill_msg.scheme); all *.scheme are now tracked (lib/ is gitignored, so they need -f). spells/ spell_msg/mob_races were already tracked. Build clean, 463 tests, boots clean (SYSERR 222; spells load via val). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…le> + parent scheme lookup Group the message string files and set up the per-locale layout (option d): - cfg/messages/<type>_msg.scheme -- one scheme per message TYPE, shared across all its locales (the schemes differ by enum binding -- ESpell/ESpellMsg vs ESkill/ESkillMsg vs EDamageSource/EFightMsg -- so they stay per-type, NOT one scheme for everything). - cfg/messages/ru/<type>_msg.xml -- the Russian (base) data; future locales add only data files under cfg/messages/<lang>/, never another scheme copy. Moved spell_msg / skill_msg / hit_msg (.xml -> messages/ru/, .scheme -> messages/); updated the three cfg_manager paths. Vedun (scheme.cpp SchemePathFor): resolve "<stem>.scheme" by searching the data file's own directory first (unchanged for spells.xml/spells.scheme, mechanics/mob_races.scheme) then walking up to a few parents -- so a per-type scheme one level above the per-locale data dir is found. Benefits LintSchemes too. setup_world copies cfg recursively, so the snapshot mirrors the new tree. Build clean, 463 tests, boots clean (SYSERR 222; spellmsg/skillmsg/hitmsg lint report the schemes as found+typed from the parent dir). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… names 'reload' with no argument now lists the available targets (grouped). Renamed the underscore-bearing options -- the client renders '_' as a space, which is ambiguous -- to: spell_messages->spellmsg, skill_messages->skillmsg, fight_messages->hitmsg (matching the Vedun 'what' names), mob_classes->mobclasses. Internal ReloadCfg ids are unchanged; only the user-typed option strings changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…urrency_msg.xml
Add a case-separated name format and apply it to currencies (the first cased
entity).
Format (grammar::ItemName::Build extended): a declension number group now accepts
<singular><case id="kNom" val="куна"/><case id="kGen" .../>...</singular>
in addition to the legacy <singular nom=".." gen=".."/> (still used by pc_classes).
A language lists only the cases it has; unknown ids are logged. ItemName made
copyable so a name can be cached and handed to entities.
Currencies: display name (search string + gender + singular/plural declensions)
moved from currencies.xml into cfg/messages/ru/currency_msg.xml, keyed by currency
text_id (msg_container/msg_sheaf vocabulary). New CurrencyNamesLoader
("currency_messages") loads it BEFORE currencies.xml; CurrencyInfoBuilder pulls the
name from that registry by text_id (FindCurrencyName). currencies.xml keeps only
gameplay params (vnum/mode/permits/flags); the <name> block and name= attr are
gone (header-comment example updated). 5 currencies migrated; kZlatnik stays the
commented-out example.
Build clean, 463 tests, boots clean (SYSERR 222; currency_messages loads before
currencies, no missing-name errors).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the message container with a short abbreviation alongside the display name (MsgSheaf::abbr_ / GetAbbr; parsed from <name val= abbr=>), and move every skill's Russian name and status-bar abbreviation out of skills.xml into cfg/messages/ru/skill_msg.xml. SkillInfo now reads both from MUD::SkillMessages() at build time, so skill_messages loads before skills (db.cpp boot + scheck paths). skill_msg.scheme gains the <name> tag (val + abbr). Mirrors the spell/currency name migrations. 96 skill names+abbrs migrated (39 into existing sheaves, 57 new); name= /abbr= stripped from skills.xml. Build clean, 463 tests, boot SYSERR 222. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move every PC/service class's cased display name and short abbreviation
out of the per-class cfg/classes/pc_*.xml files into one shared
cfg/messages/ru/class_msg.xml (the user's "common class_msg.xml" over
per-class files). Mirrors the currency-names migration: a bespoke
ClassNamesLoader ("class_messages", loaded before "classes") fills a
g_class_names registry of ClassName{abbr, grammar::ItemName}; the new
loader uses the same <name abbr=> + <case id= val=> structured format as
currency_msg.xml. CharClassInfoBuilder::ParseName now pulls name+abbr
from the registry by class id token instead of the <name> child.
Since <name> was also the descent point into the class node's children,
ParseClass now descends per-section via GoToChild/GoToParent (robust to
the service classes that have no <stats>). 17 classes migrated (14 PC +
kUndefined/kMob/kNpcBase); <name> stripped from all class files. Build
clean, 463 tests, boot SYSERR 222 (class_messages loads before classes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give feats a dedicated msg_container (MsgContainer<EFeat, EFeatMsg>) like skills, since feats are expected to gain activation/deactivation/ambience messages later. For now feat_msg.xml carries only each feat's display name (<name val="..."/>); FeatInfoBuilder::ParseHeader reads it from MUD::FeatMessages().GetName(id) instead of feats.xml, so feat_messages loads before feats (db.cpp + cfg_manager). Adds the FeatMessagesLoader (editable, Vedun-aware), the MUD::FeatMessages() accessor, EFeatMsg, a "kDefault"->kUndefined EFeat alias for the default sheaf, NAMES_OF<EFeat> + EFeat/EFeatMsg registration in the Vedun enum registry, and feat_msg.scheme. 152 feat names migrated; name= stripped from feats.xml (the commented-out example feat keeps its name as documentation). Build clean, 463 tests, boot SYSERR 222, featmsg scheme lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…names.xml
These three entity types have only plain (non-declined) display names and
are unlikely to grow their own message types, so per kvirund they share a
single file, cfg/messages/ru/entity_names.xml, rather than a message
container each. A new entity_names::EntityNamesLoader ("entity_names",
loaded before all three in db.cpp + cfg_manager) parses one section per
type into a string-keyed registry; FindMobClassName / FindMobRaceName /
FindZoneTypeName are consulted by each builder at load time.
mob_classes keeps its English admin name (eng) in mob_classes.xml; only
the Russian name moves out (rus= stripped). zone_types name= and
mob_races name= stripped (the latter tolerating "name =" spacing and
leaving the <im name=> ingredient names intact). 10 mob classes + 15 mob
races + 25 zone types migrated. Build clean, 463 tests, boot SYSERR 222,
entity_names loads before zone_types/mob_classes/mob_races.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guilds are identified by an integer vnum (no enum, no `id` attribute) plus
a free-form text_id and a name, so the Vedun editor's default
single-enum-id element model couldn't locate them. The editor core already
drives editing through IEditableCfgLoader's virtual hooks, so no core
change is needed -- GuildsLoader now implements them with a vnum identity:
- EditableWhat() = "guild"; ListElements() reports each guild as
{vnum-string, "text_id name"} so a guild resolves by vnum (exact id) or
by text_id/name (do_vedun's label substring search).
- FindElementNode() matches the <guild> child by its `vnum` attribute
(the base impl matches `id`, which guilds lack).
- CanonicalElementId() accepts only a non-negative integer (the create-by
-vnum gate); existing guilds resolve via ListElements beforehand.
- CreateElementNode() stamps the new vnum (+ placeholder text_id/name).
- GuildInfo::GetName() moved to the public section for ListElements.
Adds lib/cfg/guilds.scheme for typed editing (vnum/text_id/name/mode +
trainers + skill/spell/feat talents; enum= only for registered enums).
New tests/guilds.loader.cpp covers the three vnum overrides. Build clean,
466 tests (+3), boot SYSERR 222, guild scheme lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olders
Extend msg_container to the int specialization (via `if constexpr`, the
enum path unchanged) so a vnum-keyed container is possible: the
MsgSheafBuilder parses the sheaf id as "kDefault" -> kUndefinedVnum or an
integer vnum, and GetMessage uses kUndefinedVnum as the default-sheaf key.
Guild messages move out of the hardcoded GuildInfo::GetMsg table into
lib/cfg/messages/ru/guild_msg.xml, a MsgContainer<int, guilds::EMsg>; all
22 messages live in the default sheaf today, with per-guild overrides by
vnum possible later. New guild_messages.{h,cpp} (GuildMessagesLoader,
editable; MUD::GuildMessages(); guilds::EMsg meta-enum), loaded before
"guilds"; guild_msg.scheme; guilds::EMsg registered as "EGuildMsg" in the
Vedun enum registry. GetMsg now reads from the container.
The three messages with unnamed {} placeholders are now named so a game
designer can tell what each holds: kTalentEarned -> {talent_type} {color}
{name} {nocolor}; kYouGiveMoney / kSomeoneGivesMoney -> {money} (call
sites switched to fmt::arg). New tests cover the int container (default
sheaf / vnum override / fallback). Build clean, 467 tests (+1), boot
SYSERR 222, guildmsg scheme lint clean, guild_messages before guilds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (talents)
DataNode is its own iterator with a `filter_name`. Children("guild") sets
filter_name on the iterator node; GuildsLoader::FindElementNode returned
that node, leaking the filter. The editor then called the no-arg
Children() on it, which did NOT reset filter_name, so operator++ used
next_sibling("guild") -- after the first child (<trainers>) it found no
sibling named "guild" and stopped, hiding <talents> (and any later child).
Two fixes: the no-arg DataNode::Children() now clears filter_name (a
no-arg Children iterates ALL children, regardless of a filter inherited
from a node copied out of a Children(key) range), and FindElementNode
iterates un-keyed with a name check (matching the default loader pattern)
so the returned node carries no leaked filter. Adds a regression test
that the found guild exposes both trainers and talents.
Build clean, 468 tests (+1), boot SYSERR 222.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CastToPoints intensity-grade vocabulary (percent-threshold <grade text=> rows for heal/moves/thirst/full) is localized narration text, so it belongs under cfg/messages/ru/. It is NOT a msg_container/sheaf structure, so points_msg.xml would mislead; renamed to points_intensity.xml, matching its <points_intensity> root element and the "points_intensity" loader id. Per-locale word choices (tier count/wording) are language-specific, so the whole file is per-locale rather than a shared scheme + locale overlays. Updated the loader path (cfg_manager.cpp) and two doc-comment references. Build clean, boot SYSERR 222, points_intensity loads from the new path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Russian doc-comments in src/engine/core/handler.h were destroyed (every Cyrillic char collapsed to an identical "О©╫" mojibake triple, unrecoverable) by commit 848dcd1 "issue.3375 stage 8" -- an editor saved the file with broken encoding. The C++ declarations were unaffected, so it still compiled, but the documentation was lost. master still had the correct comments. Restored the 7 comment lines from origin/master's clean handler.h (transplanted onto our current code structure -- issue.3375 only deleted legacy target-lookup API declarations, never touched the comments). Only comment lines change; no code lines are modified, so the build is unaffected. The blob is now valid UTF-8 / the working tree valid KOI8-R. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.