Files
Sprinter-SDCC/applications/PoP/docs/status_line_text.md
T
snark13 ffb59b3470 Статус-строка: время, отсчёт ##:## в отладочной полосе, смерть как в оригинале
* Порт show_time() целиком: «N MINUTES LEFT», «N SECONDS LEFT» (у
  единственной секунды свой total=12, как у оригинала), «TIME HAS
  EXPIRED!».  Флаг pop_show_time — это is_show_time: взводит и само ядро
  таймера (круглые пятёрки, каждая секунда последней минуты), и старт
  уровня, и читы времени.  Значение 2 = «перебить текущую строку», как
  оригинал делает в последнюю минуту и после читов.
* Отладочная полоса показывает оставшееся время ##:## у правого края.
  На табло уходит minutes-1: rem_min у оригинала — НОМЕР идущей минуты, а
  не остаток целых (старт 60 при rem_tick 719 = «почти 60:00»).  Секунды
  считаются делением раз в 12 кадров, а не каждый кадр — это единственное
  деление на кадровом пути.
* Смерть Кида по образцу оригинала: строка «Press Button to Continue» с
  7-го кадра (Kid.alive > 6), 288 тиков = 24 секунды, последние 72 тика
  мигание с периодом 12 и звуком 38 на появлении.  Промолчал — start_game
  (title), нажал Enter/Shift — рестарт уровня.  Авто-респавна по таймеру
  больше нет.  Отпускания клавиш оригинал не ждёт, а мы ждём: Shift у нас
  клавиша действия — см. impl_diff.md.
* POP_APP_PLAYING принимает EV_RESTART_INTRO: start_game зовётся прямо из
  геймплея, раньше это событие принимал только pause menu.
* expired() загейчен уровнем 13 (seg000:910).  Время на уровне Джафара
  идёт до открытия двери, но КОНЧИТЬСЯ игра там уже не может — у нас
  этого гейта не было, и партия завершалась.

Бюджет: _CODE 23979, куча 263 Б.  Host-тесты: 15 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:06:17 +03:00

14 KiB
Raw Blame History

Текст в нижней статус-строке (строке HP) — полная инвентаризация SDLPoP

Разбор SDLPoP/src/ на 2026-08-25. Цель — знать ВЕСЬ набор сообщений, которые оригинал печатает в ту же полосу, где нарисованы деления HP, и правила их появления/исчезновения. Это входные данные для порта: у нас пока туда пишется только GAME PAUSED.


1. Геометрия: одна полоса на HP и на текст

rect_bottom_text = { top 193, left 70, bottom 202, right 250 }   // data.h:217
display_text_bottom: draw_rect(чёрным) + show_text(halign_center, valign_bottom)
  • Деления HP Кида — от x = 0 вправо, шаг 7, максимум 10 → занимают x 0..69.
  • Деления HP стража — от x = 314 влево, шаг 7, максимум 10 → занимают x 245..320.
  • Текст живёт РОВНО в промежутке x 70..250 и по X с делениями не пересекается.
  • По Y деления на y = 194..200, текст (valign_bottom к 202) — на y = 195..201, то есть на строку ниже. Именно поэтому в оригинале текст выглядит «сидящим» чуть ниже стрелок HP.

У нас: POP_HP_Y = 194 (pop_cdraw.h), экран сдвинут на POP_YOFF = 28, базовая линия крупного шрифта POP_YOFF + POP_HP_Y + 8 = 230 — силуэт ложится на 223..229, то есть та же картинка.

2. Два примитива и два таймера

Имя Что делает
display_text_bottom(text) (seg008:2644) стереть прямоугольник цветом 0 и напечатать текст по центру
erase_bottom_text(arg) (seg008:266D) стереть прямоугольник; при arg != 0 ещё и обнулить оба таймера
text_time_remaining сколько игровых тиков сообщение ещё висит; 0 — ничего не висит
text_time_total идентификатор сообщения, а не только его длительность

Обработка тика — в draw_game_frame/idle (seg000:956). Комментарий в оригинале прямой: «Note: texts are identified by their total time!» Значения text_time_total, у которых есть особое поведение:

total Смысл Что происходит по истечении
12 «1 SECOND LEFT» обычное стирание
24 обычное короткое сообщение обычное стирание
36 смерть на демо-уровне (0) или на уровне зелий (15) — текста нет start_game() — рестарт игры
288 «Press Button to Continue» start_game() — рестарт игры
1188 защита от копирования (уровень 15) не убывает и не исчезает

Мигание: при total == 288 и remaining < 72 сообщение мигает с периодом 12 тиков — 4 тика видно (blink_frame <= 3), 8 нет; в кадре blink_frame == 3 заново печатается текст и играет звук 38 (sound_38_blink).

Сброс: init_game() (seg003:32) обнуляет оба таймера и is_show_time — то есть любое сообщение умирает на старте уровня.


3. Полный список сообщений

3.1. Состояние программы

Текст Где Таймер
GAME PAUSED seg000:1769, пока is_paused без таймера: печатается на входе в паузу, erase_bottom_text(1) на выходе (seg000:1784)

3.2. Уровень и оставшееся время (show_level / show_time, seg008)

Текст Условие total
LEVEL %d show_level() при старте уровня; только 1..12 (hide_level_number_from_level = 14), не при seamless; уровень 13 показывается как 12 (level_13_level_number) 24, дальше сразу is_show_time = 1
%d MINUTES LEFT каждая минута, кратная 5, и каждая из последних 5 24
%d SECONDS LEFT последняя минута, раз в 12 тиков 24
1 SECOND LEFT остался 1 с 12
TIME HAS EXPIRED! rem_min == 0 24
%d MINUTES PASSED / 1 MINUTE PASSED только SDLPoP (ALLOW_INFINITE_TIME), при отрицательном таймере 24

Что взводит is_show_time (все → следующий кадр печатает время):

  • Space — seg000:612, штатная клавиша оригинала «сколько осталось»;
  • читы -/+ numpad (изменение времени) — seg000:762 / 777, при этом таймеры сообщения обнуляются, чтобы новое напечаталось немедленно;
  • смерть Джафараon_guard_killed() seg006:1936, уровень 13 (jaffar_victory_level): вспышка + показать время;
  • истечение очередной минуты — seg008:1796;
  • сразу после show_level().

Обнуляет is_show_time: play_kid() при смерти (seg006:1365) и show_copyprot(1) (seg000:2385).

3.3. Смерть Кида

Текст Где total
Press Button to Continue play_kid() seg006:1383 — умер на обычном уровне 288 (мигает, затем рестарт игры)
(без текста) тот же код, но уровень 0 (демо) или 15 (зелья) 36 (тихая пауза, затем рестарт игры)

Стирается: fell_out() (seg006:1342, упал из комнаты 0) и чит R (воскрешение, seg000:783) — оба зовут erase_bottom_text(1).

3.4. Сохранение и загрузка

Текст Клавиша total
GAME SAVED / UNABLE TO SAVE GAME Ctrl+G (save_game, seg000:2211) total не ставится, remaining = 24
QUICKSAVE / NO QUICKSAVE F6 (расширение SDLPoP, seg000:497) 24
QUICKLOAD / NO QUICKLOAD F9 (расширение SDLPoP, seg000:514) 24

3.5. Ответы на клавиши (answer_textneed_show_text, все total = 24)

Текст Клавиша
SOUND ON / SOUND OFF Ctrl+S
KEYBOARD MODE Ctrl+K
JOYSTICK MODE / JOYSTICK NOT FOUND / JOYSTICK UNAVAILABLE Ctrl+J
PRINCE OF PERSIA V1.0 (в SDLPoP заменено на SDLPoP v%s) Ctrl+V
SDL COMP v… LINK v… Ctrl+C — только SDLPoP

3.6. Отладочные читы (cheats_enabled, total = 24)

Текст Клавиша Смысл
S%d L%d R%d A%d B%d C номер отрисованной комнаты и её соседей L/R/A/B
AL%d AR%d BL%d BR%d Shift+C диагональные соседи

3.7. Защита от копирования (только уровень 15)

Текст Где total
WORD %d LINE %d PAGE %d show_copyprot(1) seg000:2389 1188 — висит, пока не сменится уровень

3.8. Только SDLPoP, в оригинале 1989 отсутствует

Текст Где
RECORDING, REPLAY SAVED, REPLAY CANCELED replay.c:599/626/628
имя файла скриншота screenshot.c:62

4. Что из этого касается нашего порта

Реализовано (pop_status.c, таймер 24 тика как у оригинала):

  • GAME PAUSED — без таймера, рисует само меню (pop_menu.c);

  • QUICKSAVE / NO QUICKSAVE, QUICKLOAD / NO QUICKLOAD — заявка стоит в pop_qsave_process, то есть в единственном месте, где известно, что именно делали. Лейбл печатается ДО дисковой операции — осознанное расхождение, см. impl_diff.md;

  • SOUND ON / SOUND OFF — Ctrl+S;

  • LEVEL %d — порт show_level() целиком: демо-уровень 0 и номера от 14 молчат, тринадцатый показывается двенадцатым, бесшовный переход 12→13 пропускается и гасит флаг за собой.

  • вся группа времени — N MINUTES LEFT, N SECONDS LEFT, 1 SECOND LEFT, TIME HAS EXPIRED!. Флаг pop_show_time (порт is_show_time) взводит само ядро таймера на круглых пятёрках и каждую секунду последней минуты, а также старт уровня и читы времени; значение 2 означает «перебить текущую строку», как оригинал делает в последнюю минуту;

  • Press Button to Continue — висит бессрочно (MSG_HOLD), уровень перезапускает кнопка. Расхождение с оригиналом, см. impl_diff.md.

Пока НЕ печатается:

  • номера комнат (C/Shift+C) — у нас отдельная отладочная строка;
  • copy protection и SDLPoP-расширения (replay, скриншоты) — не нужны.

Отладочная строка вдобавок показывает оставшееся время ##:## у правого края. На табло уходит minutes-1: у оригинала rem_min — это НОМЕР идущей минуты, а не остаток целых (старт 60 при rem_tick 719 = «почти 60:00»). Секунды считаются делением раз в 12 кадров, а не каждый кадр.

Нам не нужно: copy protection (уровень 15 исключён из порта — см. full_game_plan.md), joystick-режимы, replay, скриншоты.

Механика, которую придётся портировать целиком, если брать группу времени: пара таймеров text_time_total/text_time_remaining с семантикой «идентификатор сообщения» — иначе не воспроизвести ни мигание, ни рестарт по истечении 36/288.


5. Цена вывода и что делать, если упрёмся

Блит одного глифа стоит ~4,6 тыс. тактов почти независимо от размера — это цена вызова, а не пикселей (memory blit_cost_model). Полсотни символов = полкадра. Что уже сделано в pop_status.c / pop_ui.c:

  • change-driven: пока показанное не изменилось, не рисуем вовсе;
  • по полям: смена комнаты — две цифры (~9 тыс. тактов, 2% кадра), а не вся строка; подписи рисуются только при полной инвалидации;
  • пробелы не блитятся: их глиф целиком прозрачен, а стоит как буква — на полной отладочной строке это девять сэкономленных блитов;
  • заливка только поля при входе в комнату (pop_screen_fill_field): борта от комнаты к комнате не меняются, это и четверть заливки, и то, что обе полосы вход переживают.

Запас, если бюджета всё же не хватит (идеи пользователя, 2026-08-25):

  1. Растянуть вывод на несколько кадров, не показывая полуготовую строку. Печатать по нескольку букв за кадр, держа цвет шрифта чёрным (отдельный индекс палитры), а по готовности подменить этот индекс на белый — строка появится целиком и мгновенно. Стоит ноль байт памяти и укладывается в нашу же технику «два разных чёрных» (POP_COL_OUTSIDE).
  2. Собирать строку в один спрайт в свободном хвосте страницы шрифта и блитить одним вызовом. Дороже по подготовке (~35 тыс. тактов на копирование), но выгодно там, где строка ЦЕЛИКОМ меняется каждый раз. Для меню этот путь уже рассматривался и был отвергнут; для статус-строк он имеет смысл только вместе с п.1.

Про QuickSave/QuickLoad оптимизация не нужна вовсе: там игра и так стоит на время дисковой операции.

6. Ловушка: свисающие глифы

Зона стирания текста обязана захватывать строку НИЖЕ базовой линии. В малом шрифте 'p' имеет высоту 7 при ascent 5, ',' — 6: они свисают под базовую линию. Стирание ровно до неё оставляло от хвоста «p» в «Speed:» одинокую точку (поймано в MAME 2026-08-25).