План QuickSave/QuickLoad: разбор SDLPoP + инвентаризация нашего состояния
Только изучение и план, кода нет. Первое, что выяснилось: в оригинале 1989 года быстрого сохранения НЕТ вовсе — это enhancement SDLPoP (seg000.c, USE_QUICKSAVE, F6/F9). Значит искать в Apple II / MSDOS нечего, и повторяем мы не букву, а устройство. Что берём у SDLPoP: плоский снимок с ОДНИМ обходом на запись и на чтение (#define process(x)); совместимость держится строкой версии и ничем больше; клавиша только взводит флаг, работа идёт между кадрами; состояние отрисовки не сохраняется вовсе — комната перерисовывается с нуля. Чем наш случай тяжелее: уровень в EMM-странице, room_modif/trobs — static в банковом pop_trob.c, ГСЧ у нас ТРИ (pop_t_seed, trob_seed, pop_fight_seed), и дабл-буфер требует перерисовать после загрузки ОБЕ страницы. Снимок ≈1,9 КБ, поэтому основной носитель — EMM-страница (мгновенно, мимо DSS и его лимита манипуляторов), файл вынесен в необязательный шаг QS6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
| [`perf_green_phase.md`](perf_green_phase.md) | **ЗЕЛЁНАЯ фаза (слой фона)**: раскладка тактов, способы ускорения (G1..G6), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
||||
| [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
||||
| [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше |
|
||||
| [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad**: разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка, шаги QS1..QS6. План, код не начат. 2026-08-17 |
|
||||
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
|
||||
| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 |
|
||||
| [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 |
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
# QuickSave / QuickLoad — разбор оригинала и план реализации
|
||||
|
||||
Статус: **план, код не начат** (2026-08-17). Задача на доске —
|
||||
[`../roomtest/TASKS_OPEN.md#qsave`](../roomtest/TASKS_OPEN.md#qsave).
|
||||
|
||||
---
|
||||
|
||||
## 0. Важная оговорка об «оригинале»
|
||||
|
||||
**В оригинальном PoP 1989 года (DOS/Apple II) QuickSave/QuickLoad НЕТ.**
|
||||
Там вообще нет сохранения посреди уровня: игра рассчитана на один заход в
|
||||
60 минут, а «продолжение» — это только пароль/чекпоинт уровня 7. Поэтому
|
||||
`Prince-of-Persia-Apple-II/` и `MSDOS/` тут не источники — искать в них
|
||||
нечего.
|
||||
|
||||
Источник истины — **SDLPoP**, где быстрое сохранение добавлено как
|
||||
enhancement: `seg000.c`, блок `#ifdef USE_QUICKSAVE`, клавиши **F6** (save)
|
||||
и **F9** (load). Ниже разобран именно он. Это значит, что правило
|
||||
«расхождение с SDLPoP = баг у нас» здесь работает мягче: мы не обязаны
|
||||
повторять его байт-в-байт, но обязаны повторить его **устройство**, потому
|
||||
что оно решает ровно те проблемы, которые возникнут и у нас.
|
||||
|
||||
---
|
||||
|
||||
## 1. Как это устроено в SDLPoP
|
||||
|
||||
### 1.1 Точка вызова — отдельная фаза кадра, не обработчик клавиши
|
||||
|
||||
Клавиша только взводит флаг (`need_quick_save` / `need_quick_load`,
|
||||
`seg000.c:558`), а вся работа делается в `check_quick_op()` — она вызывается
|
||||
из главного цикла **между кадрами**, когда движок в согласованном состоянии.
|
||||
Это принципиально: загрузка посреди тика переписала бы `Char` под ногами у
|
||||
`play_seq`.
|
||||
|
||||
Отказ штатный, не фатальный: `quick_save()`/`quick_load()` возвращают
|
||||
успех/неуспех, и игра печатает `QUICKSAVE` / `NO QUICKLOAD` внизу экрана и
|
||||
продолжается.
|
||||
|
||||
### 1.2 Формат — плоская последовательность переменных, без структуры
|
||||
|
||||
```c
|
||||
#define process(x) ok = ok && process_func(&(x), sizeof(x))
|
||||
```
|
||||
|
||||
Один макрос и один и тот же список обходится **и на запись, и на чтение**
|
||||
(`quick_process(process_save)` / `quick_process(process_load)`). Поля
|
||||
пишутся встык, без имён и тегов; совместимость держится ровно одним
|
||||
средством — **строкой версии в начале файла**:
|
||||
|
||||
```c
|
||||
const char quick_version[] = "V1.16b4 ";
|
||||
```
|
||||
|
||||
При загрузке она сравнивается, и при несовпадении файл просто отвергается
|
||||
(`quick_load`, возврат 0). То есть формат нарочно хрупкий и нарочно
|
||||
одноразовый — это снимок конкретной сборки, а не сейв-формат.
|
||||
|
||||
**Это стоит перенять целиком.** Мы платим за версионирование одним байтом
|
||||
и получаем право менять состав снимка при каждой правке движка.
|
||||
|
||||
### 1.3 Что именно сохраняется
|
||||
|
||||
Полный список — `quick_process`, `seg000.c:257-366`. По смыслу он делится
|
||||
на пять групп:
|
||||
|
||||
| группа | поля |
|
||||
|---|---|
|
||||
| уровень | `level` (2305 Б целиком), `checkpoint`, `upside_down`, `drawn_room`, `current_level`, `next_level`, `leveldoor_open` |
|
||||
| анимируемые объекты | `mobs_count`, `mobs[14]`, `trobs_count`, `trobs[30]` |
|
||||
| Кид | `Kid`, `hitp_curr/max/beg_lev`, `grab_timer`, `holding_sword`, `united_with_shadow`, `have_sword`, `kid_sword_strike`, `pickup_obj_type`, `offguard` |
|
||||
| соперник | `Guard`, `Char`, `Opp`, `guardhp_curr/max`, `demo_index`, `demo_time`, `curr_guard_color`, `guard_notice_timer`, `guard_skill`, `shadow_initialized`, `guard_refrac`, `justblocked`, `droppedout`, `is_guard_notice`, `can_guard_see_kid` |
|
||||
| прочее | кэш коллизии (`*_row_coll_room/flags`, `prev_collision_row`), вспышка (`flash_color/time`), звук (`is_screaming`, `is_feather_fall`, …), **`random_seed`**, время (`rem_min`, `rem_tick`), весь блок управления (`control_*`, `ctrl1_*`) |
|
||||
|
||||
Два наблюдения, важные для нас:
|
||||
|
||||
1. **Состояние ОТРИСОВКИ не сохраняется вообще.** Ни экранных буферов, ни
|
||||
пометок перерисовки, ни того, что уже нарисовано. Вместо этого при
|
||||
загрузке комната перерисовывается с нуля. Это резко упрощает задачу и
|
||||
ровно то, что нам нужно при дабл-буфере.
|
||||
2. **`random_seed` сохраняется.** Без него загрузка не воспроизводима:
|
||||
после неё факелы, чомперы и `prandom` в боёвке пойдут иначе.
|
||||
|
||||
### 1.4 Что делается при загрузке
|
||||
|
||||
`restore_room_after_quick_load()` (`seg000.c:395`) — это и есть вся
|
||||
«сложность» операции:
|
||||
|
||||
- `load_lev_spr(current_level)` — **перезагрузка графики уровня** (тайлсет
|
||||
мог смениться: подземелье/дворец);
|
||||
- `different_room = 1`, `next_room = drawn_room = Kid.room` — принудительно
|
||||
«мы в другой комнате», чтобы движок перерисовал всё;
|
||||
- `load_room_links()` — связи комнат заново;
|
||||
- `draw_game_frame()` — отрисовать кадр (важно для состояния падения);
|
||||
- `hitp_delta = guardhp_delta = 1` — принудительный редрой полос HP;
|
||||
- если `Guard.room != drawn_room` — стража «выключить» (`direction =
|
||||
dir_56_none`, `guardhp_curr = 0`), как в `clear_char()`;
|
||||
- `loadkid_and_opp()` — восстановить окно `Char`/`Opp`;
|
||||
- сбросить таймеры текста и `exit_room_timer`.
|
||||
|
||||
Плюс визуальный приём: перед загрузкой экран заливается чёрным на 5 тиков —
|
||||
чтобы переход читался глазом и не выглядел «дёрганием».
|
||||
|
||||
### 1.5 Чего в SDLPoP решили НЕ восстанавливать
|
||||
|
||||
- звуки — просто `stop_sounds()`;
|
||||
- перо (`is_feather_fall`) — без фикса `fix_quicksave_during_feather`
|
||||
сохранение под пером запрещено вовсе, а при загрузке эффект гасится;
|
||||
- есть опциональный **штраф**: `USE_QUICKLOAD_PENALTY` отнимает минуту
|
||||
игрового времени за квиклоад. Нам не нужен (у нас пока нет игрового
|
||||
таймера).
|
||||
|
||||
---
|
||||
|
||||
## 2. Чем наша архитектура отличается
|
||||
|
||||
| | SDLPoP | у нас | следствие для задачи |
|
||||
|---|---|---|---|
|
||||
| уровень в памяти | `level_type` в ОЗУ, 2305 Б, мутабельный | EMM-страница (`pop_lvl_page`), плюс рабочая копия комнаты в W2 | снимок читает страницу через W0-маппинг, а не `memcpy` |
|
||||
| модификаторы тайлов | внутри `level.bg` | отдельный `room_modif[24][30]` в `pop_trob.c` (**static**) | нужен экспортируемый сериализатор из банка 6 |
|
||||
| код | один бинарник | 8 банков + резидент | сериализатор обязан жить там же, где данные, и зваться через трамплин |
|
||||
| экран | один буфер | **дабл-буфер**, у каждой страницы своя теневая копия | после загрузки перерисовать ОБЕ страницы, иначе через кадр мелькнёт старое |
|
||||
| ОЗУ | сколько угодно | куча 2969 Б, стек 1279 Б | буфер снимка целиком в ОЗУ не положить — писать потоком |
|
||||
| диск | `fopen` | DSS: 8 манипуляторов, 9-й ВЕШАЕТ систему ([[dss_fd_limit]]) | закрывать файл гарантированно, гард уже есть в libc |
|
||||
| ГСЧ | один `random_seed` | **три** независимых: `pop_t_seed`, `trob_seed`, `pop_fight_seed` | сохранять все три, иначе загрузка невоспроизводима |
|
||||
|
||||
---
|
||||
|
||||
## 3. Инвентаризация нашего состояния
|
||||
|
||||
Собрано по `.sprinter-cc-roomtest/roomtest.map` (данные всех модулей, включая
|
||||
банковые, лежат в W2 — банк влияет только на код). Отмечено, что глобально
|
||||
(видно снаружи), а что `static` и требует аксессора.
|
||||
|
||||
### 3.1 Мутабельные данные уровня
|
||||
|
||||
| что | где | размер | доступ |
|
||||
|---|---|---|---|
|
||||
| тайлы `fg` (провалившиеся плиты, открытые двери, съеденные предметы) | EMM-страница уровня | 720 Б | `pop_level_set_tile` пишет; чтения наружу нет — **нужен аксессор** |
|
||||
| `room_modif[24][30]` | `pop_trob.c`, static | 720 Б | **нужен сериализатор** (банк 6) |
|
||||
| `room_seen[24]` | `pop_trob.c`, static | 24 Б | там же |
|
||||
| `trobs[30]` + `trobs_count` | `pop_trob.c`, static | 91 Б | там же |
|
||||
| `trob_seed` | `pop_trob.c`, static | 4 Б | там же |
|
||||
| `mobs[14]` | `pop_room.c`, **глобален** | 210 Б | напрямую |
|
||||
| `mobs_live` | `pop_room.c`, static | 1 Б | аксессор |
|
||||
|
||||
### 3.2 Персонажи и бой
|
||||
|
||||
`Kid`, `Char`, `Opp` (`pop_kid.c`), `Guard` (`pop_guard.c`) — по 16 Б,
|
||||
все глобальные. Рядом: `hitp_curr/max/beg_lev/delta`, `guardhp_curr/max/delta`,
|
||||
`guard_skill`, `guard_refrac`, `justblocked`, `kid_sword_strike`, `offguard`,
|
||||
`holding_sword`, `can_guard_see_kid`, `is_guard_notice`,
|
||||
`pop_guard_notice_timer`, `pop_guard_hurt`, `pop_united_shadow`,
|
||||
`pop_shadow_init`, `pop_fight_seed`, `knock`.
|
||||
|
||||
### 3.3 Прогресс и физика
|
||||
|
||||
`pop_current_level`, `pop_next_level`, `pop_checkpoint`, `pop_have_sword`,
|
||||
`pop_item_taken`, `pop_leveldoor_open`, `pop_leveldoor_right`,
|
||||
`pop_leveldoor_ybottom`, `pop_kid_dead`, `pop_kid_hurt`, `pop_feather`,
|
||||
`pop_upside` / `pop_upside_want`, `pop_flash_time` / `pop_flash_color`,
|
||||
`pop_droppedout`, `pop_fell_out`, `pop_leave_dir`, `pop_leave_timer`,
|
||||
`pop_loose_*`, `pop_ceil_modif`, `pop_ceil_fell`, `pop_debris_at`,
|
||||
`pop_seamless`, `pop_jumped_mirror`.
|
||||
|
||||
### 3.4 Ввод
|
||||
|
||||
`control_x/y/shift/forward/backward/up/down/shift2` (`pop_state.c`) — как в
|
||||
SDLPoP, сохраняются.
|
||||
|
||||
### 3.5 Что НЕ сохранять (восстанавливается перерисовкой)
|
||||
|
||||
`room_fg/room_bg`, `lcol_*`/`rcol_*`/`below_fg`/`above_*`, `seam_*`,
|
||||
`cur_room`, `pop_t_*` (весь кэш слоя фона, окна клипа, `pop_cd_*`),
|
||||
`trob_drawn`, `mob_spr`, слоты `pop_cd`, метки `pop_redraw`, запечки
|
||||
(`bake_pg`). Всё это — производное; после загрузки оно обязано быть
|
||||
сброшено и пересчитано, а не восстановлено.
|
||||
|
||||
**Оценка объёма снимка: ≈ 1,9 КБ** (720 + 720 + 210 + 91 + 64 + ~60
|
||||
скаляров + запас).
|
||||
|
||||
---
|
||||
|
||||
## 4. Куда писать снимок: EMM-страница, а не файл
|
||||
|
||||
Рекомендация: **основной путь — EMM-страница, файл опционален.**
|
||||
|
||||
Мотивы:
|
||||
|
||||
- снимок 1,9 КБ, страница 16 КБ — влезает целиком, с запасом на рост;
|
||||
- свободно 215 страниц / 3440 КБ на старте ([[sprinter_emm_budget]]) — одна
|
||||
страница не заметна;
|
||||
- сохранение/загрузка становятся **мгновенными** (копия через W0), без
|
||||
обращения к DSS и без риска упереться в лимит манипуляторов;
|
||||
- не нужен путь к файлу и права на запись; на дискете запись ещё и медленная.
|
||||
|
||||
Цена: снимок не переживает выход из программы. Для отладочного инструмента
|
||||
(а QuickSave у нас в первую очередь именно он — быстро вернуться к месту
|
||||
бага) это ровно то, что нужно.
|
||||
|
||||
Файловый вариант (`QUICKSAVE.SAV` рядом с exe) делается тем же
|
||||
сериализатором и добавляется вторым шагом, если понадобится переживать
|
||||
рестарт. Общий обход состояния писать сразу так, чтобы «куда» было
|
||||
параметром — как у SDLPoP через `process_func`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Формат снимка
|
||||
|
||||
```
|
||||
+0 "PQS1" 4 Б магия
|
||||
+4 версия сборки 1 Б (инкремент при ЛЮБОМ изменении состава)
|
||||
+5 pop_current_level 1 Б
|
||||
+6 длина полезной части 2 Б (контроль, что обход совпал)
|
||||
+8 ... поля встык, ОДИН порядок на запись и на чтение ...
|
||||
```
|
||||
|
||||
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
|
||||
тегов и выравнивания: снимок одноразовый и живёт ровно одну сборку.
|
||||
|
||||
Обход — один список и один макрос, как `process(x)`:
|
||||
|
||||
```c
|
||||
static void qs_walk(qs_io_t io) /* io = запись или чтение */
|
||||
{
|
||||
QS(pop_current_level); QS(pop_checkpoint); ...
|
||||
}
|
||||
```
|
||||
|
||||
Так состав нельзя рассинхронизировать между сохранением и загрузкой —
|
||||
единственная реальная опасность плоского формата.
|
||||
|
||||
---
|
||||
|
||||
## 6. Что делать при загрузке (наш аналог `restore_room_after_quick_load`)
|
||||
|
||||
Порядок важен, каждый пункт закрывает конкретный отказ:
|
||||
|
||||
1. **Сменился уровень?** → `pop_level_load_num()`, `pop_bg_load(tileset)`,
|
||||
атласы стража по типу. Это дорого, но ровно тот же путь, что при
|
||||
переходе уровня (`pop_level_switch`), — переиспользовать его, а не писать
|
||||
заново.
|
||||
2. Залить экран чёрным (приём SDLPoP: переход должен читаться глазом).
|
||||
3. Восстановить состояние обходом `qs_walk`.
|
||||
4. **Сбросить всё производное:** `pop_trob_reset` (но НЕ трогая
|
||||
восстановленные `room_modif`/`trobs` — нужен отдельный «мягкий» сброс,
|
||||
только `trob_drawn` + метки), `pop_redraw_reset`, слоты `pop_cd`,
|
||||
`pop_bake_forget`, `pop_cd_clear`, сигнатуры пропуска перерисовки.
|
||||
5. `pop_room_load(Kid.room)` — рабочая копия комнаты и срезы соседей.
|
||||
6. **Полная отрисовка комнаты в ОБЕ страницы дабл-буфера.** Это наше
|
||||
главное отличие от SDLPoP: одной перерисовки мало, вторая страница
|
||||
останется со старой картинкой и мигнёт через кадр.
|
||||
7. Принудительный редрой полос HP (`hitp_delta = guardhp_delta = 1`).
|
||||
8. Если `Guard.room != Kid.room` — выключить стража
|
||||
(`Guard.direction = DIR_56_NONE`, `guardhp_curr = 0`), как `clear_char`.
|
||||
9. `pop_loadkid_and_opp()` — согласовать окно `Char`/`Opp`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Разбиение на шаги
|
||||
|
||||
| шаг | что | критерий готовности |
|
||||
|---|---|---|
|
||||
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
|
||||
| **QS2** | Ядро: `qs_walk` + запись/чтение в EMM-страницу, магия и версия, отказ при несовпадении | сохранение и загрузка **в той же комнате, без движения** — картинка и состояние не изменились |
|
||||
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
|
||||
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
|
||||
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
|
||||
| **QS6** | Опционально: файл `QUICKSAVE.SAV` тем же сериализатором | снимок переживает рестарт программы |
|
||||
|
||||
Порядок не переставлять: QS3 без QS2 нечего проверять, а QS5 обязан идти
|
||||
после QS3 — иначе смена тайлсета замаскирует ошибки восстановления.
|
||||
|
||||
---
|
||||
|
||||
## 8. Риски и открытые вопросы
|
||||
|
||||
1. **`static` в банковых модулях.** Их нет в карте символов, то есть
|
||||
отладчиком снимок не проверить. Возможно, стоит сделать `room_modif` и
|
||||
`trobs` НЕ-static — так же, как уже сделано с `mobs` в `pop_room.c` и
|
||||
ровно по той же мотивации (там это записано прямым комментарием).
|
||||
2. **Место в банке 6.** `pop_trob` занимает 3802/16384 — запас есть, но
|
||||
сериализатор лучше писать компактным обходом, а не 30 отдельными
|
||||
вызовами.
|
||||
3. **Три ГСЧ.** Проверить, что сохранены ВСЕ: пропуск любого даст
|
||||
«загрузилось, но играется иначе» — самый неприятный класс бага, потому
|
||||
что выглядит как случайность.
|
||||
4. **Согласованность `Char` и `Kid`.** У нас окно `Char` — отдельная копия;
|
||||
если сохранить их рассогласованными (снимок посреди тика), загрузка
|
||||
воскресит рассогласование. Отсюда требование QS4: только между кадрами.
|
||||
Урок свежий — ровно на этом стыке жил
|
||||
[BUG-CHEAT-IMM-1](../roomtest/BUGS_CLOSED.md#bug-cheat-imm-1).
|
||||
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
|
||||
страницу. Симптом — мерцание через кадр
|
||||
(см. `roomtest/CLAUDE.md`, раздел про дабл-буфер).
|
||||
6. **Открытый вопрос:** нужен ли снимок в файле вообще, или EMM-страницы
|
||||
достаточно. Решать после QS3, по факту использования.
|
||||
@@ -864,6 +864,34 @@ tp/10 у факелов таблицей, пустой слот соперник
|
||||
|
||||
## P1 — берётся в любой момент
|
||||
|
||||
### <a id="qsave"></a>QSAVE. QuickSave / QuickLoad
|
||||
|
||||
> **План целиком — [`../docs/quicksave_plan.md`](../docs/quicksave_plan.md)**
|
||||
> (разбор SDLPoP, инвентаризация нашего состояния, формат снимка, порядок
|
||||
> восстановления, разбиение на шаги QS1…QS6, риски). Изучено 2026-08-17,
|
||||
> **код не начат.**
|
||||
|
||||
Оговорка, с которой начинается план: **в оригинале 1989 года этого нет
|
||||
вовсе**, QuickSave — enhancement SDLPoP (`seg000.c`, `USE_QUICKSAVE`,
|
||||
F6/F9). Значит повторяем не букву, а устройство.
|
||||
|
||||
Что взять у SDLPoP без изменений: снимок — плоский список полей, один обход
|
||||
на запись и на чтение (`#define process(x)`), совместимость держится
|
||||
строкой версии и ничем больше; клавиша только взводит флаг, работа идёт
|
||||
**между кадрами**; состояние ОТРИСОВКИ не сохраняется вовсе — комната
|
||||
перерисовывается с нуля.
|
||||
|
||||
Чем наш случай тяжелее: уровень лежит в EMM-странице, а не в ОЗУ;
|
||||
`room_modif`/`trobs` — `static` в банковом `pop_trob.c` (нужны
|
||||
сериализаторы); ГСЧ у нас **три** (`pop_t_seed`, `trob_seed`,
|
||||
`pop_fight_seed`), и пропуск любого даст «загрузилось, но играется иначе»;
|
||||
и главное — **дабл-буфер**: перерисовать после загрузки надо ОБЕ страницы,
|
||||
иначе старая картинка мигнёт через кадр.
|
||||
|
||||
Размер снимка ≈ 1,9 КБ, поэтому основной носитель — **EMM-страница**
|
||||
(мгновенно, без DSS и его лимита манипуляторов), файл — опциональный
|
||||
шаг QS6.
|
||||
|
||||
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
|
||||
|
||||
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
|
||||
|
||||
Reference in New Issue
Block a user