Звук PoP: приоритеты и перебиваемость вместо «всегда перебивать»
Пользователь услышал расхождение с SDLPoP: у нас решётка обрывалась приземлением Кида, в оригинале доигрывает до конца, а приземления не слышно. Оказалось, упущен целый механизм. play_sound (seg000:12C5) НЕ играет, а только номинирует кандидата на кадр — из нескольких выживает важнейший (меньше prio = важнее, при равенстве последний). play_next_sound (seg000:1304) раз в кадр решает, запускать ли: можно, только если ничего не играет ЛИБО текущий помечен перебиваемым и новый не менее важен. Иначе номинант выбрасывается — очереди в оригинале нет. Отсюда всё, что слышно: gate_closing_fast неперебиваем и доигрывает целиком; челюсти (prio 0x10) всегда важнее решётки (0x32), поэтому решётка звучит только в паузах между укусами. Таблицы из SDLPoP с учётом fix_sound_priorities (в его config.h он определён безусловно). Створка двери уровня — единственная запись, правимая на ходу, вынесена в отдельный байт. Добавлен пропущенный stop_sounds на завершении открытия двери (seg007:455). Проверено записью MAME: старт уровня 1 был 135+210 мс (решётка, обрезанная на 80 мс), стал один всплеск 455 мс с корреляцией огибающей +0,889 со звуком 6. Заведён BUG-SND-FIRSTRUN: искажение первого эффекта при первом запуске после загрузки системы — вероятно, лечится _cbl_prime, но проверить можно только на железе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -514,3 +514,68 @@ ESTEX на каждый атлас) ПРИ ОТКРЫТОМ звуке, насо
|
|||||||
Лечение: `pop_sfx_close()` первым действием `pop_shutdown`. Проверено
|
Лечение: `pop_sfx_close()` первым действием `pop_shutdown`. Проверено
|
||||||
второй записью — всплеска на выходе больше нет. Механизм тот же, из-за
|
второй записью — всплеска на выходе больше нет. Механизм тот же, из-за
|
||||||
которого звук глушится на время загрузки уровня.
|
которого звук глушится на время загрузки уровня.
|
||||||
|
|
||||||
|
## 11. Приоритеты и перебиваемость: звук у оригинала НЕ «всегда перебивать»
|
||||||
|
|
||||||
|
Пользователь услышал расхождение: у нас решётка обрывалась приземлением
|
||||||
|
Кида, в SDLPoP — доигрывала до звонкого конца, а приземления не было
|
||||||
|
слышно вовсе. Разбор исходника показал, что мы упустили ЦЕЛЫЙ МЕХАНИЗМ.
|
||||||
|
|
||||||
|
### 11.1 Модель оригинала
|
||||||
|
|
||||||
|
```
|
||||||
|
play_sound(id) seg000:12C5 — только НОМИНИРУЕТ кандидата на кадр:
|
||||||
|
if next < 0 || prio[id] <= prio[next]: next = id
|
||||||
|
|
||||||
|
play_next_sound() seg000:1304 — раз в кадр решает, запускать ли:
|
||||||
|
if next >= 0:
|
||||||
|
if !играет_что_то ||
|
||||||
|
(перебиваем[текущий] && prio[next] <= prio[текущий]):
|
||||||
|
текущий = next; запустить
|
||||||
|
next = -1 // НЕ запустили -> номинант ВЫБРОШЕН, очереди нет
|
||||||
|
```
|
||||||
|
|
||||||
|
Три следствия, каждое слышно:
|
||||||
|
|
||||||
|
- **Неперебиваемый звук доигрывает целиком.** У `gate_closing_fast` (6)
|
||||||
|
`interruptible = 0`, поэтому приземление Кида (17) в этот момент
|
||||||
|
пропадает совсем — не откладывается, а именно теряется.
|
||||||
|
- **Внутри кадра выживает важнейший.** Меньше `prio` — важнее; при
|
||||||
|
равенстве побеждает ПОСЛЕДНИЙ (сравнение `<=`).
|
||||||
|
- **Два источника не «чередуются как получится».** Челюсти (47, prio
|
||||||
|
0x10) всегда важнее решётки (4, prio 0x32): решётка не может перебить
|
||||||
|
укус, а укус решётку — может. Отсюда и картина на ур. 9 к. 9, где
|
||||||
|
решётка звучит только в паузах между укусами.
|
||||||
|
|
||||||
|
### 11.2 Что сделано у нас
|
||||||
|
|
||||||
|
`pop_sfx_play` теперь только номинирует; запуск — в `pop_sfx_tick`,
|
||||||
|
который зовётся раз в кадр в конце отрисовки (там же, где оригинал зовёт
|
||||||
|
`play_next_sound`, seg000:954). Таблицы `snd_prio` (57 байт) и битовая
|
||||||
|
карта `snd_intr` (8 байт) — в резиденте, значения из SDLPoP С УЧЁТОМ
|
||||||
|
`fix_sound_priorities()`: в `config.h` SDLPoP `FIX_SOUND_PRIORITIES`
|
||||||
|
определён безусловно, значит сравниваемся мы с исправленным вариантом
|
||||||
|
(звук 10 → 0x0D, 48 → 0x15, 49 перебиваем).
|
||||||
|
|
||||||
|
Створка двери уровня (15) — единственная запись, которую оригинал правит
|
||||||
|
на ходу: перебиваема при закрытии, нет при открытии (seg007:442/464).
|
||||||
|
Держим отдельным байтом `pop_sfx_slide_intr`, чтобы таблица осталась в
|
||||||
|
ПЗУ. Там же добавлен пропущенный `stop_sounds()` на завершении открытия
|
||||||
|
двери (seg007:455) — без него неперебиваемый съезд (1,6 с) блокировал бы
|
||||||
|
очередь.
|
||||||
|
|
||||||
|
Звуки без оцифровки (музыка, длина 0) не номинируются вовсе — порт
|
||||||
|
проверки `if (NULL == sound_pointers[id]) return;`. Раньше такой id
|
||||||
|
глушил живой эффект.
|
||||||
|
|
||||||
|
### 11.3 Проверка
|
||||||
|
|
||||||
|
Записью MAME, старт уровня 1:
|
||||||
|
|
||||||
|
| | всплески | что это |
|
||||||
|
|---|---|---|
|
||||||
|
| до | 135 мс + 210 мс | решётка, обрезанная приземлением на 80 мс |
|
||||||
|
| после | **один, 455 мс** | решётка целиком, корреляция огибающей со звуком 6 **+0,889** |
|
||||||
|
|
||||||
|
Цена: резидент +~250 Б (таблицы + логика), куча ужалась с 347 до 134 Б —
|
||||||
|
довод в пользу давно назревшей реорганизации базовой памяти.
|
||||||
|
|||||||
@@ -893,3 +893,30 @@ Makefile правила нет.
|
|||||||
|
|
||||||
**Приоритет низкий:** ситуация возникает только когда труп остаётся на
|
**Приоритет низкий:** ситуация возникает только когда труп остаётся на
|
||||||
проваливающемся полу, играбельности не мешает.
|
проваливающемся полу, играбельности не мешает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BUG-SND-FIRSTRUN — при ПЕРВОМ запуске первый эффект слегка искажён
|
||||||
|
|
||||||
|
**Симптом** (пользователь, 2026-08-20): после загрузки системы, при первом
|
||||||
|
запуске программы, самый первый звуковой эффект идёт с искажением; при
|
||||||
|
повторном запуске программы искажения нет. Наблюдалось ещё на тестовых
|
||||||
|
примерах CBL, до PoP.
|
||||||
|
|
||||||
|
**Причина, скорее всего, уже устранена.** Аппаратный буфер CBL (256
|
||||||
|
слотов) железо не чистит, а запись в порт управления сразу пускает
|
||||||
|
воспроизведение с нулевого слота — то есть первые 23,4 мс играет то, что
|
||||||
|
лежало в буфере раньше. При первом запуске это остатки после загрузки
|
||||||
|
системы, при втором — наша же тишина, поэтому и слышно только один раз.
|
||||||
|
Разбор — `../docs/sound_plan.md` §10.2.
|
||||||
|
|
||||||
|
Лечение сделано в libc: `_cbl_prime` заливает буфер тишиной сразу после
|
||||||
|
включения (`cbl_open`). **В MAME проверить нельзя**: эмулируемый буфер
|
||||||
|
стартует нулями при двухдополнительном ЦАП, то есть там этот дефект нем
|
||||||
|
изначально — записи до и после фикса совпали побитово по огибающей.
|
||||||
|
|
||||||
|
**Что нужно:** проверка на РЕАЛЬНОМ железе, первый запуск после холодной
|
||||||
|
загрузки. Если искажение осталось — значит мусор приходит не из буфера
|
||||||
|
CBL, и копать надо в системных буферах DSS.
|
||||||
|
|
||||||
|
**Приоритет низкий:** один раз за сеанс, на играбельность не влияет.
|
||||||
|
|||||||
@@ -87,13 +87,98 @@ int pop_sfx_fill(uint16_t n)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- ОЧЕРЕДЬ И ПРИОРИТЕТЫ (порт seg000:12C5 + seg000:1304) ------------ *
|
||||||
|
*
|
||||||
|
* Оригинал НЕ перебивает играющий звук чем попало, и это слышно. Модель
|
||||||
|
* ровно такая:
|
||||||
|
*
|
||||||
|
* play_sound(id) — только НОМИНИРУЕТ кандидата на кадр; из нескольких
|
||||||
|
* за кадр остаётся ВАЖНЕЙШИЙ (меньше prio = важнее,
|
||||||
|
* при равенстве побеждает последний — сравнение <=);
|
||||||
|
* play_next_sound() — раз в кадр решает, запускать ли: можно, если ничего
|
||||||
|
* не играет, ЛИБО текущий звук помечен «перебиваемым»
|
||||||
|
* И новый не менее важен. Иначе номинант ВЫБРАСЫВАЕТСЯ
|
||||||
|
* (next_sound = -1 безусловно), а не встаёт в очередь.
|
||||||
|
*
|
||||||
|
* Отсюда то, что слышно в игре: `gate_closing_fast` (6) неперебиваем и
|
||||||
|
* доигрывает до звонкого конца, а приземление Кида в этот момент пропадает
|
||||||
|
* совсем; челюсти (47, prio 0x10) всегда важнее решётки (4, prio 0x32), и
|
||||||
|
* решётка звучит только в паузах между укусами.
|
||||||
|
*
|
||||||
|
* Таблицы — из SDLPoP (sound_prio_table seg000:1528, sound_interruptible
|
||||||
|
* data.h:433) с правками fix_sound_priorities(): в config.h SDLPoP
|
||||||
|
* FIX_SOUND_PRIORITIES определён безусловно, то есть сравниваем мы себя
|
||||||
|
* именно с исправленным вариантом (10 -> 0x0D, 48 -> 0x15, 49 перебиваем).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Приоритет: МЕНЬШЕ значит ВАЖНЕЕ. Музыкальные id тут тоже есть — они
|
||||||
|
* никогда не номинируются (нет оцифровки), но индексация должна совпадать
|
||||||
|
* с оригиналом, иначе таблицу не сверить глазами. */
|
||||||
|
static const uint8_t snd_prio[POP_SND_COUNT] = {
|
||||||
|
0x14, 0x1E, 0x23, 0x66, 0x32, 0x37, 0x30, 0x30, 0x4B, 0x50, /* 0.. 9 */
|
||||||
|
0x0D, 0x12, 0x0C, 0x0B, 0x69, 0x6E, 0x73, 0x78, 0x7D, 0x82, /* 10..19 */
|
||||||
|
0x91, 0x96, 0x9B, 0xA0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x13, /* 20..29 */
|
||||||
|
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, /* 30..39 */
|
||||||
|
0x01, 0x01, 0x01, 0x01, 0x87, 0x8C, 0x0F, 0x10, 0x15, 0x16, /* 40..49 */
|
||||||
|
0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01 /* 50..56 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* «Играющий звук можно перебить» — битовая карта (бит id&7 байта id>>3):
|
||||||
|
* 57 флагов уложены в 8 байт, потому что в резиденте W1/W2 каждая сотня
|
||||||
|
* байт на счету, а читаем мы их раз в кадр. */
|
||||||
|
static const uint8_t snd_intr[8] = {
|
||||||
|
/* 0.. 7 */ (uint8_t)(0<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 1<<7),
|
||||||
|
/* 8..15 */ (uint8_t)(1<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 0<<7),
|
||||||
|
/*16..23 */ (uint8_t)(1<<0 | 1<<1 | 0<<2 | 1<<3 | 1<<4 | 1<<5 | 1<<6 | 1<<7),
|
||||||
|
/*24..31 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 1<<5 | 0<<6 | 0<<7),
|
||||||
|
/*32..39 */ 0,
|
||||||
|
/*40..47 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 0<<5 | 0<<6 | 1<<7),
|
||||||
|
/*48..55 */ 0,
|
||||||
|
/*56.. */ 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Створка двери уровня (15) — единственная запись, которую оригинал правит
|
||||||
|
* НА ХОДУ (seg007:442/464): при закрытии её перебивать можно, при открытии
|
||||||
|
* нельзя (иначе съезд створки обрывается и «конца» не слышно). Держим
|
||||||
|
* отдельным байтом, чтобы таблица осталась в ПЗУ. */
|
||||||
|
uint8_t pop_sfx_slide_intr;
|
||||||
|
|
||||||
|
/* Номинант кадра, ЗАПИСАННЫЙ КАК id+1: ноль значит «никого». Так сделано
|
||||||
|
* ради crt0 — он зануляет _DATA, а «нет номинанта» у оригинала это -1;
|
||||||
|
* со смещением на единицу начальное состояние получается даром. */
|
||||||
|
static uint8_t snd_next1;
|
||||||
|
static uint8_t snd_curr; /* что играет — для prio/intr */
|
||||||
|
|
||||||
|
static uint8_t snd_interruptible(uint8_t id)
|
||||||
|
{
|
||||||
|
if (id == 15) return pop_sfx_slide_intr;
|
||||||
|
return (uint8_t)(snd_intr[id >> 3] & (uint8_t)(1 << (id & 7)));
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- запуск / останов ------------------------------------------------ */
|
/* ---- запуск / останов ------------------------------------------------ */
|
||||||
|
|
||||||
void pop_sfx_play(uint8_t id)
|
void pop_sfx_play(uint8_t id)
|
||||||
{
|
{
|
||||||
uint8_t pg;
|
|
||||||
uint16_t off, len;
|
|
||||||
if (!pop_snd_ok || id >= POP_SND_COUNT) return;
|
if (!pop_snd_ok || id >= POP_SND_COUNT) return;
|
||||||
|
/* Нет оцифровки (музыка) — не номинируем вовсе: порт проверки
|
||||||
|
* `if (NULL == sound_pointers[id]) return;`. Иначе музыкальный id
|
||||||
|
* выигрывал бы конкурс и глушил живой эффект. */
|
||||||
|
if (pop_snd_tbl[id].len == 0) return;
|
||||||
|
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
|
||||||
|
snd_next1 = (uint8_t)(id + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_sfx_tick(void)
|
||||||
|
{
|
||||||
|
uint8_t id, pg;
|
||||||
|
uint16_t off, len;
|
||||||
|
if (!snd_next1) return;
|
||||||
|
id = (uint8_t)(snd_next1 - 1);
|
||||||
|
snd_next1 = 0; /* номинант живёт ровно кадр */
|
||||||
|
if (sfx_left != 0 && /* что-то играет... */
|
||||||
|
(!snd_interruptible(snd_curr) || snd_prio[id] > snd_prio[snd_curr]))
|
||||||
|
return; /* ...и уступать не обязано */
|
||||||
|
snd_curr = id;
|
||||||
pg = pop_snd_tbl[id].page;
|
pg = pop_snd_tbl[id].page;
|
||||||
off = pop_snd_tbl[id].off;
|
off = pop_snd_tbl[id].off;
|
||||||
len = pop_snd_tbl[id].len;
|
len = pop_snd_tbl[id].len;
|
||||||
@@ -103,7 +188,7 @@ void pop_sfx_play(uint8_t id)
|
|||||||
IRQ_DISABLE();
|
IRQ_DISABLE();
|
||||||
sfx_pg = pg;
|
sfx_pg = pg;
|
||||||
sfx_ptr = off;
|
sfx_ptr = off;
|
||||||
sfx_left = len; /* len == 0 — просто глушим */
|
sfx_left = len;
|
||||||
IRQ_ENABLE();
|
IRQ_ENABLE();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,27 @@ void pop_sfx_toggle(void) __banked;
|
|||||||
* честно means «звука не будет». */
|
* честно means «звука не будет». */
|
||||||
extern uint8_t pop_snd_want;
|
extern uint8_t pop_snd_want;
|
||||||
|
|
||||||
/* Начать эффект с номером id (0..56 — нумерация оригинала, см.
|
/* Заявить эффект с номером id (0..56 — нумерация оригинала, см.
|
||||||
* sound_plan.md). Перебивает играющий. У звуков без оцифровки (это
|
* sound_plan.md). НЕ запускает его немедленно и НЕ перебивает играющий:
|
||||||
* музыка) длина 0 — вызов просто глушит текущий.
|
* это порт play_sound (seg000:12C5), который лишь НОМИНИРУЕТ кандидата на
|
||||||
|
* текущий кадр. Из нескольких заявок за кадр выживает важнейшая, а решение
|
||||||
|
* «запускать или нет» принимает pop_sfx_tick. Звуки без оцифровки (музыка)
|
||||||
|
* игнорируются.
|
||||||
*
|
*
|
||||||
* НЕ банковая и НЕ __banked: зовётся из горячих мест (play_seq в
|
* НЕ банковая и НЕ __banked: зовётся из горячих мест (play_seq в
|
||||||
* pop_kid.c, физика, боёвка), и трамплин на каждый шаг Кида не нужен. */
|
* pop_kid.c, физика, боёвка), и трамплин на каждый шаг Кида не нужен. */
|
||||||
void pop_sfx_play(uint8_t id);
|
void pop_sfx_play(uint8_t id);
|
||||||
|
|
||||||
|
/* Раз в кадр: запустить номинанта, если можно (порт play_next_sound,
|
||||||
|
* seg000:1304). Оригинал зовёт его в конце отрисовки кадра — зовём там же.
|
||||||
|
* Не запустили — номинант ВЫБРАСЫВАЕТСЯ, очереди у оригинала нет. */
|
||||||
|
void pop_sfx_tick(void);
|
||||||
|
|
||||||
|
/* Перебиваемость съезжающей створки двери уровня (звук 15) — единственная
|
||||||
|
* запись таблицы, которую оригинал правит на ходу (seg007:442/464):
|
||||||
|
* 1 при закрытии двери, 0 при открытии. */
|
||||||
|
extern uint8_t pop_sfx_slide_intr;
|
||||||
|
|
||||||
/* Оборвать текущий эффект (порт stop_sounds). */
|
/* Оборвать текущий эффект (порт stop_sounds). */
|
||||||
void pop_sfx_stop(void);
|
void pop_sfx_stop(void);
|
||||||
|
|
||||||
|
|||||||
@@ -529,12 +529,18 @@ static void animate_leveldoor(uint8_t *m, int8_t *type, uint8_t cur_room)
|
|||||||
if (sp >= *m) { *m = 0; *type = -1; pop_sfx_play(14); } /* seg007:437 */
|
if (sp >= *m) { *m = 0; *type = -1; pop_sfx_play(14); } /* seg007:437 */
|
||||||
else {
|
else {
|
||||||
*m = (uint8_t)(*m - sp);
|
*m = (uint8_t)(*m - sp);
|
||||||
if (*type == 4) pop_sfx_play(15); /* seg007:443 */
|
/* seg007:442: створку НА ЗАКРЫТИИ перебивать можно. */
|
||||||
|
if (*type == 4) { pop_sfx_slide_intr = 1; pop_sfx_play(15); }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (*m < 43) { (*m)++; pop_sfx_play(15); } /* seg007:465 */
|
/* seg007:464: на ОТКРЫТИИ створка неперебиваема — иначе съезд
|
||||||
|
* обрывается посреди и «конца» не слышно. */
|
||||||
|
if (*m < 43) { (*m)++; pop_sfx_slide_intr = 0; pop_sfx_play(15); }
|
||||||
if (*m >= 43) {
|
if (*m >= 43) {
|
||||||
*m = 43; *type = -1;
|
*m = 43; *type = -1;
|
||||||
|
/* seg007:455: дверь открылась — оригинал глушит всё. Без этого
|
||||||
|
* неперебиваемый съезд (1,6 с) продолжал бы блокировать очередь. */
|
||||||
|
pop_sfx_stop();
|
||||||
/* Условие оригинала — переход ИЗ 0 или 2, то есть ровно один раз
|
/* Условие оригинала — переход ИЗ 0 или 2, то есть ровно один раз
|
||||||
* за открытие; иначе зеркало ставилось бы заново каждый кадр,
|
* за открытие; иначе зеркало ставилось бы заново каждый кадр,
|
||||||
* пока дверь стоит открытой. */
|
* пока дверь стоит открытой. */
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "pop_trob.h"
|
#include "pop_trob.h"
|
||||||
#include "pop_redraw.h"
|
#include "pop_redraw.h"
|
||||||
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
|
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
|
||||||
|
#include "pop_sfx.h" /* pop_sfx_tick — решение о запуске звука за кадр */
|
||||||
#include "pop_guard.h"
|
#include "pop_guard.h"
|
||||||
#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */
|
#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */
|
||||||
#include "pop_cheat.h"
|
#include "pop_cheat.h"
|
||||||
@@ -704,6 +705,12 @@ int main(void)
|
|||||||
|
|
||||||
PROF(0); /* чёрный: ждём vsync */
|
PROF(0); /* чёрный: ждём vsync */
|
||||||
|
|
||||||
|
/* Звук: решить, запускать ли заявленный за кадр эффект (порт
|
||||||
|
* play_next_sound). Место то же, что у оригинала — конец отрисовки
|
||||||
|
* кадра, seg000:954: все заявки кадра уже поданы, и из них выбирается
|
||||||
|
* важнейшая. Раньше по кадру нельзя — потеряются поздние заявки. */
|
||||||
|
pop_sfx_tick();
|
||||||
|
|
||||||
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
|
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
|
||||||
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
|
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
|
||||||
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
|
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
|
||||||
|
|||||||
Reference in New Issue
Block a user