You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Полный разбор по текущему мастеру. Заменяет две предыдущие выкладки: комментарий в #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 проходило снова, и каждая следующая команда триггера опять слала тени в комнату и строку в лог. Починено локально в 2ec3624 — RemoveAffectFromChar плюс 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.
Кто поднимает бит мимо списка сейчас
Проверено по коду и конфигам, список полный:
прототип моба — для NPC affect_total берёт mob_proto[rnum].affected_by за основу (affect_data.cpp:926);
char_saved_aff — ровно два флага, kGroup и kHorse (affect_data.cpp:182);
apply_natural_affects — kNoobRegen, реморт ≤ 3 и не бессмертный (affect_data.cpp:174);
фиты — Applies::Impose зовёт affect_modify(..., apply.affect, ...) (talents_effects.cpp:237). В feats.xml записей с флагом пять: kInfravision (две), kVampirism, kDetectLife, kBlink;
финальное подавление — при 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(...)
Два из них ложные и в счёт не идут: 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. После переезда экипировки в список аффектов он покрывает практически всё и стоит копейки.
План работ
Реализовать гашение бита в RemoveAffectFromChar и RemoveAffectFromCharExceptEquipment.
Вычистить 18 ручных AFF_FLAGS(ch).unset(...) рядом с вызовами — они станут лишними.
Пройти по девяти настоящим дырам из списка и убедиться, что каждая закрылась.
Тест на регрессию MTRIG_HITPRCNT #3658: снять аффект, проверить AFF_FLAGGED до affect_total.
Полный разбор по текущему мастеру. Заменяет две предыдущие выкладки: комментарий в #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проходило снова, и каждая следующая команда триггера опять слала тени в комнату и строку в лог. Починено локально в 2ec3624 —RemoveAffectFromCharплюс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, а поведение стало другим. Разбор конфига:timer="-1"— материализуются в настоящиеAffectвch->affectedс меткойkAfFromEquipmentkBlindness,kPoison,kSleep,kHoldВсе четыре бестаймерные записи имеют
flag="kUndefined": они не поднимают никакого флага, а кастуют заклинание при надевании. А цикл re-derive вaffect_totalтребует одновременноtimer == kEquipmentAffectNoTimerиaff_affect != kUndefined— то есть на текущем конфиге не срабатывает никогда.Вывод: ни одна вещь не поднимает бит
affected_byмимо списка аффектов. Все предметные флаги лежат вch->affected.Побочное подтверждение — появление
RemoveAffectFromCharExceptEquipment(affect_data.cpp:749): раз предметный аффект теперь в списке, обычныйRemoveAffectFromCharего сносит, и дляAppear()понадобился вариант, пропускающийkAfFromEquipment.Кто поднимает бит мимо списка сейчас
Проверено по коду и конфигам, список полный:
affect_totalберётmob_proto[rnum].affected_byза основу (affect_data.cpp:926);char_saved_aff— ровно два флага,kGroupиkHorse(affect_data.cpp:182);apply_natural_affects—kNoobRegen, реморт ≤ 3 и не бессмертный (affect_data.cpp:174);Applies::Imposeзовётaffect_modify(..., apply.affect, ...)(talents_effects.cpp:237). Вfeats.xmlзаписей с флагом пять:kInfravision(две),kVampirism,kDetectLife,kBlink;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(...)MakeVisible(...)affect_totalОдиннадцать непокрытых:
Два из них ложные и в счёт не идут:
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. После переезда экипировки в список аффектов он покрывает практически всё и стоит копейки.
План работ
RemoveAffectFromCharиRemoveAffectFromCharExceptEquipment.AFF_FLAGS(ch).unset(...)рядом с вызовами — они станут лишними.AFF_FLAGGEDдоaffect_total.-- Дедал