* Порт 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>
14 KiB
Текст в нижней статус-строке (строке 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_text → need_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):
- Растянуть вывод на несколько кадров, не показывая полуготовую строку.
Печатать по нескольку букв за кадр, держа цвет шрифта чёрным (отдельный
индекс палитры), а по готовности подменить этот индекс на белый — строка
появится целиком и мгновенно. Стоит ноль байт памяти и укладывается в
нашу же технику «два разных чёрных» (
POP_COL_OUTSIDE). - Собирать строку в один спрайт в свободном хвосте страницы шрифта и блитить одним вызовом. Дороже по подготовке (~35 тыс. тактов на копирование), но выгодно там, где строка ЦЕЛИКОМ меняется каждый раз. Для меню этот путь уже рассматривался и был отвергнут; для статус-строк он имеет смысл только вместе с п.1.
Про QuickSave/QuickLoad оптимизация не нужна вовсе: там игра и так стоит на время дисковой операции.
6. Ловушка: свисающие глифы
Зона стирания текста обязана захватывать строку НИЖЕ базовой линии. В малом
шрифте 'p' имеет высоту 7 при ascent 5, ',' — 6: они свисают под базовую
линию. Стирание ровно до неё оставляло от хвоста «p» в «Speed:» одинокую
точку (поймано в MAME 2026-08-25).