Skip to content

RemoveAffectFromChar не гасит бит affected_by: разбор по текущему коду и план #3731

Description

@bylins

Полный разбор по текущему мастеру. Заменяет две предыдущие выкладки: комментарий в #3705 от 6 августа (писался по коду с WeaponAffect, ключевой аргумент там уже неверен) и #3726 (правился по частям). Здесь всё целиком и в одном месте.

Суть

RemoveAffectFromChar (src/gameplay/affects/affect_data.cpp:729) убирает аффекты из списка ch->affected через RemoveAffect (affect_handler.cpp:8), который делает ровно erase и больше ничего. Сводный битвектор char_specials.saved.affected_by при этом не трогается, а AFF_FLAGGED читает именно его (char_data.h:791).

Между снятием аффекта и ближайшим affect_total флаг висит поднятым, хотя аффекта уже нет. Всё, что проверяет AFF_FLAGGED, в этом окне отвечает неправильно.

Само оно не рассасывается: периодический апдейт зовёт affect_total только при need_recalc, то есть когда в этом же такте что-то истекло. Оставленный посторонним кодом флаг может висеть сколько угодно.

Как это выстрелило

#3658: моб Эней #97131, триггер 97132 стряхивал холд и сон, флаг оставался поднятым, условие блока в mob_script_command_interpreter проходило снова, и каждая следующая команда триггера опять слала тени в комнату и строку в лог. Починено локально в 2ec3624RemoveAffectFromChar плюс ch->remove_affect() рядом.

Почему не лечится готовой RemoveAffectFromCharAndRecalculate

Она лежит тут же (affect_data.cpp:766) и делает ровно что нужно: снятие плюс affect_total. Но affect_total тяжёлая — полный проход по всем слотам экипировки, equipment_affect, фитам, природным аффектам, пересчёт всех apply, CheckLight, update_pos. Звать её на каждое снятие аффекта — на каждом шаге, каждой попытке спрятаться, каждом раунде боя — нельзя.

Что изменилось: экипировка ушла из внесписочных источников

Это главное, и именно это делает старый разбор неактуальным.

Раньше таблица weapon_affect была зашита в affect_contants.cpp и раздавала флаги мимо списка аффектов — в том числе kSneak, kHide, kInvisibility, kFly. Отсюда и следовал вывод «гасить бит нельзя, его может держать шмот».

После issue.equipment-affects-improve таблица переехала в конфиг lib/cfg/affects/equipment_affects.xml, а поведение стало другим. Разбор конфига:

записей
всего 45
с timer="-1" — материализуются в настоящие Affect в ch->affected с меткой kAfFromEquipment 41
без таймера — kBlindness, kPoison, kSleep, kHold 4
без таймера, но с флагом 0

Все четыре бестаймерные записи имеют flag="kUndefined": они не поднимают никакого флага, а кастуют заклинание при надевании. А цикл re-derive в affect_total требует одновременно timer == kEquipmentAffectNoTimer и aff_affect != kUndefined — то есть на текущем конфиге не срабатывает никогда.

Вывод: ни одна вещь не поднимает бит affected_by мимо списка аффектов. Все предметные флаги лежат в ch->affected.

Побочное подтверждение — появление RemoveAffectFromCharExceptEquipment (affect_data.cpp:749): раз предметный аффект теперь в списке, обычный RemoveAffectFromChar его сносит, и для Appear() понадобился вариант, пропускающий kAfFromEquipment.

Кто поднимает бит мимо списка сейчас

Проверено по коду и конфигам, список полный:

  1. прототип моба — для NPC affect_total берёт mob_proto[rnum].affected_by за основу (affect_data.cpp:926);
  2. char_saved_aff — ровно два флага, kGroup и kHorse (affect_data.cpp:182);
  3. apply_natural_affectskNoobRegen, реморт ≤ 3 и не бессмертный (affect_data.cpp:174);
  4. фитыApplies::Impose зовёт affect_modify(..., apply.affect, ...) (talents_effects.cpp:237). В feats.xml записей с флагом пять: kInfravision (две), kVampirism, kDetectLife, kBlink;
  5. финальное подавление — при GET_ENEMY или kGlitterDust гасятся kHide, kSneak, kDisguise, kInvisible (affect_data.cpp:1135).

Ни один из этих флагов руками не снимают: kGroup и kHorse живут своей жизнью, kNoobRegen — производная реморта, четыре фитовых флага постоянные. Пересечение с тем, что реально проходит через RemoveAffectFromChar, практически пустое.

Состояние вызовов на сегодня

36 вызовов RemoveAffectFromChar (без учёта AndRecalculate и ExceptEquipment):

что делают рядом сколько
руками гасят бит: AFF_FLAGS(ch).unset(...) или ch->remove_affect(...) 18
зовут MakeVisible(...) 6
зовут affect_total 1
ничего 11

Одиннадцать непокрытых:

src/gameplay/fight/fight.cpp:219
src/gameplay/skills/death_rage.cpp:11
src/gameplay/skills/lightwalk.cpp:28
src/gameplay/magic/magic.cpp:1936
src/gameplay/magic/magic.cpp:2233
src/engine/ui/cmd_god/do_restore.cpp:49
src/engine/ui/cmd_god/do_restore.cpp:51
src/engine/ui/cmd_god/do_arena_restore.cpp:44
src/engine/ui/cmd_god/do_arena_restore.cpp:46
src/engine/ui/cmd/do_sneak.cpp:28
src/engine/ui/cmd/do_camouflage.cpp:41

Два из них ложные и в счёт не идут: magic.cpp:2233 снимает все экземпляры и тут же добавляет их обратно через affect_to_char, который пересчитывает сам; do_sneak.cpp:28 следом накладывает новый аффект скрытности. Остальные девять — настоящие дыры.

Та же особенность у RemoveAffectFromCharExceptEquipment: снимает структуры, бит не гасит.

Варианты

1. Локальное гашение прямо в RemoveAffectFromChar. После erase пройти по ch->affected и, если аффектов этого типа не осталось, дёрнуть ch->remove_affect(). Стоимость — O(числа аффектов на персонаже), это единицы или десятки элементов. Раньше вариант не годился именно из-за экипировки; теперь она в списке, и портить остаётся нечего, кроме пяти источников выше, которые с ручным снятием не пересекаются. Для полной строгости — явный список исключений по типу аффекта.

2. Отдельный дешёвый пересчёт только флагов, RecalculateAffectFlags. Та же логика, что в affect_total, но без apply, статов, света и позиции: база плюс список плюс фиты плюс подавление. Всегда корректно, но всё равно требует прохода по фитам и, значит, заметно дороже первого варианта. Разумен как запасной, если исключения окажутся неудобными.

3. Отложенный пересчёт. Помечать персонажа грязным и звать affect_total один раз в конце пульса. Дёшево по амортизации, но внутри пульса флаг остаётся неверным — то есть #3658 этим вариантом не чинится, там несколько команд триггера отрабатывают подряд в одном пульсе. Не годится.

Рекомендация

Вариант 1. После переезда экипировки в список аффектов он покрывает практически всё и стоит копейки.

План работ

  1. Реализовать гашение бита в RemoveAffectFromChar и RemoveAffectFromCharExceptEquipment.
  2. Вычистить 18 ручных AFF_FLAGS(ch).unset(...) рядом с вызовами — они станут лишними.
  3. Пройти по девяти настоящим дырам из списка и убедиться, что каждая закрылась.
  4. Тест на регрессию MTRIG_HITPRCNT #3658: снять аффект, проверить AFF_FLAGGED до affect_total.

-- Дедал

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions