Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e389c03f8 | |||
| 05bcd8197e | |||
| 25abf8ea36 | |||
| f206a4cca6 | |||
| aaa480d0f2 | |||
| 8548aa9132 | |||
| 400f5cba63 | |||
| cb995bf0fc | |||
| 38fb3c03bb | |||
| 879f2bae31 | |||
| b3f9a7430c | |||
| fee3bdb354 | |||
| 6a97124e0d | |||
| 8610a8c178 | |||
| 81e8ed4676 | |||
| 8490288d79 | |||
| 31d0075090 | |||
| 6176f6da31 | |||
| 7377bf6a2c | |||
| b34997073e |
@@ -92,11 +92,19 @@ RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s)
|
|||||||
# bankN_*.asm; обычным небанковым программам ничего не стоит.
|
# bankN_*.asm; обычным небанковым программам ничего не стоит.
|
||||||
BANK_CHECK := $(PROJ_ROOT)/toolchain/check_bank_calls.py
|
BANK_CHECK := $(PROJ_ROOT)/toolchain/check_bank_calls.py
|
||||||
|
|
||||||
|
# ПРОВЕРКА ISR-СТАБА W0-СТРАНИЦ — там же, по свежей карте. Ловит стаб
|
||||||
|
# _gfx_w0_isr, оставшийся в W1: программа, кладущая свои страницы в W0
|
||||||
|
# (атласы спрайтов, gfx_w0_map), получает недетерминированные зависания,
|
||||||
|
# когда прерывание приходит во время вызова DSS и W1 перемаплен. Только
|
||||||
|
# предупреждение: страницы в W0 кладут не все. Разбор — в шапке скрипта.
|
||||||
|
W0ISR_CHECK := $(PROJ_ROOT)/toolchain/check_w0_isr.py
|
||||||
|
|
||||||
$(EXE): $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
|
$(EXE): $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
|
||||||
$(if $(strip $(BUILD_DIR)),@mkdir -p $(dir $@))
|
$(if $(strip $(BUILD_DIR)),@mkdir -p $(dir $@))
|
||||||
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
|
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
|
||||||
@d=$(dir $@).sprinter-cc-$(EXAMPLE); \
|
@d=$(dir $@).sprinter-cc-$(EXAMPLE); \
|
||||||
if ls $$d/bank*_*.asm >/dev/null 2>&1; then python3 $(BANK_CHECK) $$d; fi
|
if ls $$d/bank*_*.asm >/dev/null 2>&1; then python3 $(BANK_CHECK) $$d; fi; \
|
||||||
|
if ls $$d/*.map >/dev/null 2>&1; then python3 $(W0ISR_CHECK) $$d; fi
|
||||||
|
|
||||||
$(MKEXE):
|
$(MKEXE):
|
||||||
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
|
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# SprPoP — CHANGELOG
|
||||||
|
|
||||||
|
Заметные изменения по релизам. **Версия, дата и коммит каждой записи
|
||||||
|
берутся из git** — из аннотированного тега, а не проставляются руками,
|
||||||
|
поэтому расходиться с историей они не могут:
|
||||||
|
|
||||||
|
git for-each-ref \
|
||||||
|
--format='%(refname:short) | %(creatordate:short) | %(*objectname:short)' \
|
||||||
|
refs/tags/<тег>
|
||||||
|
|
||||||
|
Заголовок записи = `<имя тега> — <дата тега> — <коммит тега>`. Коммит —
|
||||||
|
дереференс аннотированного тега (`*objectname`), то есть та ревизия, на
|
||||||
|
которой релиз собран: по ней воспроизводится ровно этот билд.
|
||||||
|
|
||||||
|
Здесь только то, что видно ИГРОКУ или меняет способ сборки/запуска.
|
||||||
|
Разбор причин живёт в сообщениях коммитов, открытые баги — в
|
||||||
|
`docs/BUGS_OPEN.md`, закрытые — в архиве roomtest.
|
||||||
|
|
||||||
|
Файл начат с релиза v0.9.6; более ранняя история — в `git log` и в
|
||||||
|
досках задач.
|
||||||
|
|
||||||
|
## v0.9.6-hang-sound-button — 2026-09-02 — `aaa480d`
|
||||||
|
|
||||||
|
Три бага, найденные в живой игре и проверенные в MAME после исправления.
|
||||||
|
|
||||||
|
- **Зацеп срывался на следующем кадре** (регресс, тянулся с 4e12aa5).
|
||||||
|
Кид цеплялся за кромку, поза зацепа рисовалась — и он тут же падал.
|
||||||
|
Ловилось на длинном прыжке уровня 3 (комната 7) и в attract-демо, где
|
||||||
|
Кид вместо подъёма на кромку (0,2) улетал на пики. Причина: страж
|
||||||
|
«падения сквозь стену» принимал кладку под кромкой за стену, в которую
|
||||||
|
персонаж влетел, и отменял только что состоявшийся захват. Теперь
|
||||||
|
удачный зацеп отменяет этого стража. `38fb3c0`
|
||||||
|
|
||||||
|
- **Непрерывный взмах клинка у безоружного Кида.** На подъёме шёл
|
||||||
|
нескончаемый свист меча, которого у Кида нет. Причина: в порт не
|
||||||
|
доехали два условия оригинала, включая гард, который автор SDLPoP
|
||||||
|
подписал прямым текстом как «фикс зацикленного звука взмаха». `cb995bf`
|
||||||
|
|
||||||
|
- **Кнопка под Кидом заливалась кладкой** на дворцовых уровнях. Кид
|
||||||
|
подтягивается на открывающую кнопку, и кромка, которая его прячет,
|
||||||
|
нарисована кирпичом (уровень 4, комната 18). Причина: подстановка
|
||||||
|
спрайта «кнопка без пола слева» действует только в подземелье, и это
|
||||||
|
условие было потеряно в перерисовке поверх персонажа — при том что в
|
||||||
|
статической отрисовке комнаты оно есть. `8548aa9`
|
||||||
|
|
||||||
|
Сопутствующее, для разработки:
|
||||||
|
|
||||||
|
- В доске исправлено толкование звука 11: это индикатор ПРОМАХА, а не
|
||||||
|
«любого укола». Прежняя запись описывала наш собственный код вместо
|
||||||
|
оригинала, и из-за неё в CLIMB-VS-GUARD зря отодвинули версию про порог
|
||||||
|
дистанции. `400f5cb`
|
||||||
|
- Новый набор host-тестов `t_hang`: мерит не «вис наступил», а «вис
|
||||||
|
ДЕРЖИТСЯ» — прежние наборы регресс зацепа не видели именно поэтому.
|
||||||
|
Входит в `38fb3c0`.
|
||||||
|
- В `CLAUDE.md` записан путь запуска на HDD-образе: игра лежит в
|
||||||
|
`D:\GAMES\SPRPOP`, а не в корне диска. `aaa480d`
|
||||||
@@ -60,6 +60,24 @@ seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрис
|
|||||||
нового бага грепни архив по симптому: корень мог уже разбираться.
|
нового бага грепни архив по симптому: корень мог уже разбираться.
|
||||||
Открытые файлы обязаны читаться целиком за раз.
|
Открытые файлы обязаны читаться целиком за раз.
|
||||||
|
|
||||||
|
**CHANGELOG. Ставишь релизный тег — добавь запись в `CHANGELOG.md`.**
|
||||||
|
Версия, дата И КОММИТ в заголовке берутся ИЗ GIT, руками не
|
||||||
|
проставляются:
|
||||||
|
|
||||||
|
git for-each-ref \
|
||||||
|
--format='%(refname:short) | %(creatordate:short) | %(*objectname:short)' \
|
||||||
|
refs/tags/<тег>
|
||||||
|
|
||||||
|
Заголовок = `<имя тега> — <дата> — <коммит>`. Коммит обязателен: тег
|
||||||
|
можно передвинуть или переименовать, а ревизия, на которой релиз собран,
|
||||||
|
должна оставаться в записи однозначно. Берётся именно `*objectname`
|
||||||
|
(дереференс аннотированного тега), а не `objectname` — последний вернёт
|
||||||
|
хеш самого объекта тега, а не коммита.
|
||||||
|
|
||||||
|
В запись идёт только то, что видно ИГРОКУ или меняет сборку/запуск, — по
|
||||||
|
одному пункту «симптом -> причина одной фразой -> хеш коммита». Полный
|
||||||
|
разбор живёт в сообщении коммита, дублировать его сюда не надо.
|
||||||
|
|
||||||
Индекс документации с отметками актуальности — **`docs/README.md`**,
|
Индекс документации с отметками актуальности — **`docs/README.md`**,
|
||||||
начинать чтение оттуда.
|
начинать чтение оттуда.
|
||||||
|
|
||||||
@@ -133,9 +151,15 @@ Makefile склеивает архивы прямо в `assets/packed/<КАТА
|
|||||||
(memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
|
(memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
|
||||||
Пересобрал образ → MAME ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
|
Пересобрал образ → MAME ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
|
||||||
|
|
||||||
|
**Игра лежит на образе в `D:\GAMES\SPRPOP\`**, а НЕ в корне диска (так с
|
||||||
|
обобщения HDD-сборки, коммит ea8efdb — один образ рассчитан на несколько
|
||||||
|
приложений). `dir D:\` показывает только каталог `GAMES`; запуск из корня
|
||||||
|
отвечает `Bad command or file name`.
|
||||||
|
|
||||||
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
|
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
|
||||||
старт `run_bridge.sh` → 6 с → `go` → 8 с → `keyseq d:{ENTER}` +
|
старт `run_bridge.sh` → 6 с → `go` → 8 с → `keyseq d:{ENTER}` +
|
||||||
`keyseq sprpop{ENTER}` → 5 с → программа работает.
|
`keyseq cd games\sprpop{ENTER}` + `keyseq sprpop{ENTER}` → 5 с →
|
||||||
|
программа работает.
|
||||||
|
|
||||||
Отладочные тумблеры в живой сессии (`src/sprpop.c`): **1** — заморозить
|
Отладочные тумблеры в живой сессии (`src/sprpop.c`): **1** — заморозить
|
||||||
кадр, **2** — продолжить (разбор позы/окклюзии); **ESC** — выход. Читы
|
кадр, **2** — продолжить (разбор позы/окклюзии); **ESC** — выход. Читы
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ MEMORY ?= huge
|
|||||||
#
|
#
|
||||||
# ВАЖНО: сравнивать занятость банков можно только при ОДНОМ ALLOCS — иначе
|
# ВАЖНО: сравнивать занятость банков можно только при ОДНОМ ALLOCS — иначе
|
||||||
# сравниваются не правки, а уровни оптимизации.
|
# сравниваются не правки, а уровни оптимизации.
|
||||||
ALLOCS ?= 6000
|
ALLOCS ?= 10000
|
||||||
# Build info не хранится в C: ID и дата извлекаются из текущего commit.
|
# Build info не хранится в C: ID и дата извлекаются из текущего commit.
|
||||||
BUILD_ID ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
|
BUILD_ID ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
|
||||||
BUILD_DATE ?= $(shell git log -1 --format=%cs 2>/dev/null || echo unknown)
|
BUILD_DATE ?= $(shell git log -1 --format=%cs 2>/dev/null || echo unknown)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
|
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
|
||||||
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
||||||
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
||||||
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные (2026-08-31) |
|
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные; толкование звука ИСПРАВЛЕНО 2026-09-02 (звук 11 = промах, а не «любой укол») — главный подозреваемый снова ПОРОГ ДИСТАНЦИИ |
|
||||||
| [HP-BAR-RESTART](#hp-bar-restart) | после гибели и Ctrl+A на ОДНОЙ из двух страниц остаётся полоса HP по результатам боя | дабл-буфер/UI | КОРЕНЬ НАЙДЕН, фикс есть, ждёт проверки (2026-08-31) |
|
| [HP-BAR-RESTART](#hp-bar-restart) | после гибели и Ctrl+A на ОДНОЙ из двух страниц остаётся полоса HP по результатам боя | дабл-буфер/UI | КОРЕНЬ НАЙДЕН, фикс есть, ждёт проверки (2026-08-31) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1374,23 +1374,63 @@ uint16_t pop_music_left(void) { uint16_t a, b;
|
|||||||
В той же сцене оригинал играет ВЗМАХ (звук 11, «клинок движется»), а у нас
|
В той же сцене оригинал играет ВЗМАХ (звук 11, «клинок движется»), а у нас
|
||||||
слышен звук, похожий на упор Кида в стену (звук 8, `bumped`).
|
слышен звук, похожий на упор Кида в стену (звук 8, `bumped`).
|
||||||
|
|
||||||
Почему это важно. Звук 11 оригинал играет в `check_hurting` СРАЗУ на
|
|
||||||
кадре укола — ДО проверки расстояния и до отметки «ранен». То есть он
|
|
||||||
звучит при ЛЮБОМ уколе, попал тот или нет. Значит его отсутствие
|
|
||||||
означает не «промахнулись», а «до укола дело вообще не дошло»: страж не в
|
|
||||||
кадре укола, либо разбор вышел раньше (меч не вынут / ряды не совпали).
|
|
||||||
А звучащий вместо него упор в стену говорит, что у нас сработало
|
|
||||||
СТОЛКНОВЕНИЕ, а не атака.
|
|
||||||
|
|
||||||
Набор звуков проверен и НЕ виноват: в `assets/packed/SND/snd.idx` слот 11
|
Набор звуков проверен и НЕ виноват: в `assets/packed/SND/snd.idx` слот 11
|
||||||
на месте и содержит собственный короткий сэмпл (1280 Б), слот 8 — другой
|
на месте и содержит собственный короткий сэмпл (1280 Б), слот 8 — другой
|
||||||
(1664 Б). Раскладка не сдвинута.
|
(1664 Б). Раскладка не сдвинута.
|
||||||
|
|
||||||
Отсюда рабочая версия: в этой связке оригинал ведёт стража по ветке
|
**ПОПРАВКА 2026-09-02 — прежнее толкование звука было ОШИБОЧНЫМ.**
|
||||||
«атака с промахом», а мы — по ветке «столкновение с персонажем»
|
|
||||||
(`bump_into_opponent` / `check_bumped`). Это же объясняет и провалившегося
|
Здесь стояло, будто оригинал играет звук 11 «сразу на кадре укола, ДО
|
||||||
сквозь пол стража: столкновение двигает его координату, а не атака.
|
проверки расстояния», то есть при ЛЮБОМ уколе, попал тот или нет. Это
|
||||||
Проверять надо ветку выбора действия стража, а не только дистанцию.
|
описание НАШЕГО кода, а не оригинала: ровно так звук стоял у нас, внутри
|
||||||
|
ветки «не парировано» и до присвоения `Opp.action = 99_hurt`. В SDLPoP
|
||||||
|
(seg002:1039..1044) он стоит в ХВОСТЕ `check_hurting` и защищён условием
|
||||||
|
`Opp.action != actions_99_hurt`:
|
||||||
|
|
||||||
|
if (Char.direction == dir_56_none) return; // Fix looping "sword moving" sound.
|
||||||
|
if (Char.frame == frame_154_poking && Opp.frame != frame_161_parry &&
|
||||||
|
Opp.action != actions_99_hurt)
|
||||||
|
play_sound(sound_11_sword_moving);
|
||||||
|
|
||||||
|
То есть звук 11 в оригинале — это индикатор **ПРОМАХА**, а не укола: на
|
||||||
|
попадании его нет (играет боль), на парировании нет (`Opp.frame` уже 161).
|
||||||
|
Расхождение найдено и исправлено 2026-09-02 по симптому «непрерывный взмах
|
||||||
|
клинка у безоружного Кида на подъёме» (звук опознан в живой сессии по
|
||||||
|
`snd_curr == 11`; указатели насоса при этом сошлись с `snd.idx` байт в
|
||||||
|
байт, то есть тракт звука был исправен). Заодно приехал и пропущенный
|
||||||
|
ранний выход по `dir_56_none` — тот самый, что автор SDLPoP подписал
|
||||||
|
«Fix looping sword moving sound».
|
||||||
|
|
||||||
|
**Что из прежних выводов уцелело, а что нет.**
|
||||||
|
|
||||||
|
* УЦЕЛЕЛО: наблюдение «в оригинале слышен 11, у нас 8» — это данные, и они
|
||||||
|
остаются. Арифметика приоритетов их подкрепляет: `snd_prio[11] = 0x12`
|
||||||
|
(18) против `snd_prio[8] = 0x4B` (75), меньше значит важнее, поэтому
|
||||||
|
взмах не мог быть заглушён упором в стену. При ТОГДАШНЕМ коде звук 11
|
||||||
|
звучал на любом уколе, значит его отсутствие действительно означало, что
|
||||||
|
до кадра укола дело не дошло.
|
||||||
|
* ОТПАЛО: вывод «оригинал ведёт стража атакой с промахом, а мы —
|
||||||
|
столкновением» опирался на неверную посылку и больше не следует из звука
|
||||||
|
сам по себе.
|
||||||
|
* УСИЛИЛОСЬ: то, что в оригинале в этой сцене слышен ИМЕННО 11, теперь
|
||||||
|
доказывает, что укол у стража СОСТОЯЛСЯ и ПРОМАЗАЛ. А это ровно версия
|
||||||
|
про ПОРОГ ДИСТАНЦИИ (см. выше про пару пикселей и нижний порог 8), а не
|
||||||
|
про потерянную ветку. Прежняя «версия про столкновение» её вытеснила
|
||||||
|
зря — возвращаем дистанцию в главные подозреваемые.
|
||||||
|
|
||||||
|
**ВНИМАНИЕ тому, кто вернётся к этому багу: диагностика по звуку с
|
||||||
|
2026-09-02 ЗНАЧИТ ДРУГОЕ.** Старые заметки прогона будут вводить в
|
||||||
|
заблуждение — переснимать сцену заново. Новая таблица:
|
||||||
|
|
||||||
|
| слышно | что это значит |
|
||||||
|
|---|---|
|
||||||
|
| 11 (взмах) | укол состоялся и ПРОМАЗАЛ — поведение оригинала в этой сцене |
|
||||||
|
| 13 (боль Кида) | укол ПОПАЛ, `Opp.action = 99_hurt` |
|
||||||
|
| 8 (упор в стену) | укола не было вовсе, сработало столкновение |
|
||||||
|
| тишина | `check_hurting` вышел раньше: меч не вынут / ряды не совпали / кадр не 153-154 |
|
||||||
|
|
||||||
|
На саму механику урона правка 2026-09-02 НЕ влияет (тронут только звук),
|
||||||
|
поэтому расхождение «удар убивает» остаётся открытым как было.
|
||||||
|
|
||||||
**Решение пользователя:** отложено на будущее (2026-08-31) — поведение в
|
**Решение пользователя:** отложено на будущее (2026-08-31) — поведение в
|
||||||
этой связке расходится широко, чинить нужно целиком, а не по одному
|
этой связке расходится широко, чинить нужно целиком, а не по одному
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|----------|-------|
|
|----------|-------|
|
||||||
| [`TASKS_OPEN.md`](TASKS_OPEN.md) | **Что берётся в работу сейчас** — начинать отсюда |
|
| [`TASKS_OPEN.md`](TASKS_OPEN.md) | **Что берётся в работу сейчас** — начинать отсюда |
|
||||||
| [`BUGS_OPEN.md`](BUGS_OPEN.md) | Открытые баги и незакрытые оптимизации |
|
| [`BUGS_OPEN.md`](BUGS_OPEN.md) | Открытые баги и незакрытые оптимизации |
|
||||||
|
| [`../CHANGELOG.md`](../CHANGELOG.md) | **Что менялось по релизам** глазами игрока; версия и дата берутся из git-тега |
|
||||||
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где сделано не дословно и почему. Новое расхождение — записью сюда, а не только комментарием в коде |
|
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где сделано не дословно и почему. Новое расхождение — записью сюда, а не только комментарием в коде |
|
||||||
| [`keys.txt`](keys.txt) | **Целевая раскладка управления**, к которой подгоняем SprPoP |
|
| [`keys.txt`](keys.txt) | **Целевая раскладка управления**, к которой подгоняем SprPoP |
|
||||||
| [`keys_plan.md`](keys_plan.md) | План перевода на неё: что совпадает, что переназначить, каких функций нет вовсе |
|
| [`keys_plan.md`](keys_plan.md) | План перевода на неё: что совпадает, что переназначить, каких функций нет вовсе |
|
||||||
|
|||||||
@@ -1102,8 +1102,6 @@ static void check_hurting(void)
|
|||||||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
||||||
/* Соперник НЕ парирует. */
|
/* Соперник НЕ парирует. */
|
||||||
if (cf == FRAME_154_POKING) {
|
if (cf == FRAME_154_POKING) {
|
||||||
/* seg002:0DAE — свист клинка мимо цели. */
|
|
||||||
pop_sfx_play(11);
|
|
||||||
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
||||||
distance = pop_char_opp_dist();
|
distance = pop_char_opp_dist();
|
||||||
if (distance >= (int16_t)min_range && distance < 29)
|
if (distance >= (int16_t)min_range && distance < 29)
|
||||||
@@ -1116,6 +1114,29 @@ static void check_hurting(void)
|
|||||||
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
|
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
|
||||||
play_seq();
|
play_seq();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* СВИСТ КЛИНКА МИМО ЦЕЛИ (seg002:1039..1044) — в оригинале он стоит в
|
||||||
|
* ХВОСТЕ функции, после ОБЕИХ веток, а не внутри ветки «не парировано».
|
||||||
|
* Мы его туда и переносим; раньше он звучал раньше времени и без двух
|
||||||
|
* условий оригинала, отчего на подъёме Кида шёл непрерывный взмах
|
||||||
|
* клинка у безоружного (наблюдение пользователя 2026-09-02, звук
|
||||||
|
* опознан по snd_curr == 11 в живой сессии).
|
||||||
|
*
|
||||||
|
* Три отличия от прежнего кода, все из оригинала:
|
||||||
|
*
|
||||||
|
* 1. Ранний выход по dir_56_none — это ИМЕННО анти-зацикливание, автор
|
||||||
|
* SDLPoP так его и подписал («Fix looping sword moving sound»).
|
||||||
|
* Направление dir_56_none означает «персонаж выключен» (clear_char),
|
||||||
|
* и махать клинком ему нечем.
|
||||||
|
* 2. Условие `Opp.action != ACTION_99_HURT`: попали — играет звук боли,
|
||||||
|
* а не свист мимо. Поэтому звук обязан идти ПОСЛЕ присвоения выше.
|
||||||
|
* 3. Кадры перечитываются у персонажей, а не берутся из cf/of: ветка
|
||||||
|
* парирования только что записала Opp.frame, а play_seq в ней —
|
||||||
|
* Char.frame, и снимки выше уже устарели. */
|
||||||
|
if (Char.direction == DIR_56_NONE) return;
|
||||||
|
if (Char.frame == FRAME_154_POKING && Opp.frame != FRAME_161_PARRY &&
|
||||||
|
Opp.action != ACTION_99_HURT)
|
||||||
|
pop_sfx_play(11);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
|
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
|
||||||
|
|||||||
@@ -771,15 +771,26 @@ static void overlay_mid_tile(int row, int col)
|
|||||||
pop_env_b(42, x, pop_tile_table[1].right_y + dmy);
|
pop_env_b(42, x, pop_tile_table[1].right_y + dmy);
|
||||||
{ /* draw_tile_base (seg008:0A8E) — ЦЕЛИКОМ, вместе с подстановками id:
|
{ /* draw_tile_base (seg008:0A8E) — ЦЕЛИКОМ, вместе с подстановками id:
|
||||||
* у loose верх плиты берётся из loose_fram_left (в pop_tile_table base_id=0),
|
* у loose верх плиты берётся из loose_fram_left (в pop_tile_table base_id=0),
|
||||||
* у opener'а без пола слева — 148. Раньше здесь стоял голый base_id, и
|
* у opener'а без пола слева — 148, НО только в ПОДЗЕМЕЛЬЕ. Условие
|
||||||
|
* `tbl_level_type[current_level] == 0` в оригинале стоит третьим
|
||||||
|
* (seg008:628) и у нас было потеряно ИМЕННО ЗДЕСЬ: статическая
|
||||||
|
* отрисовка (pop_room.c, draw_tile_base) его имеет, а оверлей — нет.
|
||||||
|
* Отсюда симптом «кнопка, на которую лезет Кид, залита кирпичом»:
|
||||||
|
* пока тайл рисует статика, кнопка правильная, но как только Kid
|
||||||
|
* встаёт под кромку, оверлей перерисовывает её поверх него уже
|
||||||
|
* спрайтом 148, а во дворце это другая картинка (кладку палас
|
||||||
|
* вообще рисует заливками, а не спрайтами). Уровень 4, комната 18,
|
||||||
|
* кнопка (0,4) — найдено пользователем 2026-09-02.
|
||||||
|
*
|
||||||
|
* Раньше здесь стоял голый base_id, и
|
||||||
* верх loose-плиты в оверлей не попадал: передняя грань ложилась поверх
|
* верх loose-плиты в оверлей не попадал: передняя грань ложилась поверх
|
||||||
* Kid (foretable), а сама плита — нет, и подтягивающийся Kid рисовался
|
* Kid (foretable), а сама плита — нет, и подтягивающийся Kid рисовался
|
||||||
* поверх её верхней плоскости (комната 12). */
|
* поверх её верхней плоскости (комната 12). */
|
||||||
uint8_t base_id = t->base_id;
|
uint8_t base_id = t->base_id;
|
||||||
if (code == 11 && row >= 0)
|
if (code == 11 && row >= 0)
|
||||||
base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])];
|
base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])];
|
||||||
else if (code == 0x0F && lcode == 0)
|
else if (code == 0x0F && lcode == 0 && !pop_palace)
|
||||||
base_id = 148;
|
base_id = 148; /* ТОЛЬКО ПОДЗЕМЕЛЬЕ (seg008:628) */
|
||||||
if (base_id)
|
if (base_id)
|
||||||
pop_env_b(base_id, x, t->base_y + dmy);
|
pop_env_b(base_id, x, t->base_y + dmy);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -555,11 +555,6 @@ void pop_determine_col(void) __banked { determine_col(); }
|
|||||||
* Обе давно работают от Char, а не от Kid; не хватало только объявлений
|
* Обе давно работают от Char, а не от Kid; не хватало только объявлений
|
||||||
* (docs/sdlpop_audit.md, находка 12). */
|
* (docs/sdlpop_audit.md, находка 12). */
|
||||||
uint8_t pop_tile_behind_char(void) __banked { return get_tile_behind_char(); }
|
uint8_t pop_tile_behind_char(void) __banked { return get_tile_behind_char(); }
|
||||||
int8_t pop_dist_to_edge_weight(void) __banked
|
|
||||||
{
|
|
||||||
int d = distance_to_edge_weight();
|
|
||||||
return (int8_t)(d > 127 ? 127 : (d < -128 ? -128 : d));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* расстояние до края тайла (для in_wall). */
|
/* расстояние до края тайла (для in_wall). */
|
||||||
static int distance_to_edge(int xpos)
|
static int distance_to_edge(int xpos)
|
||||||
@@ -570,8 +565,15 @@ static int distance_to_edge(int xpos)
|
|||||||
if (Char.direction == 0) d = TILE_RIGHTX - d; /* dir_0_right */
|
if (Char.direction == 0) d = TILE_RIGHTX - d; /* dir_0_right */
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); }
|
static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); }
|
||||||
|
|
||||||
|
int8_t pop_dist_to_edge_weight(void) __banked
|
||||||
|
{
|
||||||
|
int d = distance_to_edge_weight();
|
||||||
|
return (int8_t)(d > 127 ? 127 : (d < -128 ? -128 : d));
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- *
|
/* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- *
|
||||||
* Передний край персонажа = char_x_right (взгляд вправо) / char_x_left
|
* Передний край персонажа = char_x_right (взгляд вправо) / char_x_left
|
||||||
* (влево) из set_char_collision (seg006:0723): obj_x/2 + 58, где obj_x =
|
* (влево) из set_char_collision (seg006:0723): obj_x/2 + 58, где obj_x =
|
||||||
@@ -837,20 +839,25 @@ static uint8_t can_grab_front_above(void)
|
|||||||
/* check_grab (seg006:0A28): в падении при зажатом Shift — зацепиться за
|
/* check_grab (seg006:0A28): в падении при зажатом Shift — зацепиться за
|
||||||
* уступ спереди-сверху (seq_15), если скорость падения ещё мала и высота
|
* уступ спереди-сверху (seq_15), если скорость падения ещё мала и высота
|
||||||
* подходящая. Выравнивает передний край по кромке, гасит fall_y, взводит
|
* подходящая. Выравнивает передний край по кромке, гасит fall_y, взводит
|
||||||
* grab_timer (блок climb-up на несколько кадров). */
|
* grab_timer (блок climb-up на несколько кадров).
|
||||||
static void check_grab(void)
|
*
|
||||||
|
* ВОЗВРАТ: 1 — зацеп СОСТОЯЛСЯ на этом кадре. Нужен вызывающему (do_fall):
|
||||||
|
* дальше по кадру идёт glide_through_wall_guard, и ему нельзя трогать
|
||||||
|
* персонажа, которого мы только что повесили на кромку (см. там же). */
|
||||||
|
static uint8_t check_grab(void)
|
||||||
{
|
{
|
||||||
uint8_t old_x;
|
uint8_t old_x;
|
||||||
if (!pop_ctrl_shift_held()) return; /* Shift не зажат */
|
if (!pop_ctrl_shift_held()) return 0; /* Shift не зажат */
|
||||||
if ((uint8_t)Char.fall_y >= 32) return; /* падает слишком быстро */
|
if ((uint8_t)Char.fall_y >= 32) return 0; /* падает слишком быстро */
|
||||||
if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return;
|
if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return 0;
|
||||||
old_x = Char.x;
|
old_x = Char.x;
|
||||||
Char.x = (uint8_t)char_dx_forward(-8);
|
Char.x = (uint8_t)char_dx_forward(-8);
|
||||||
determine_col();
|
determine_col();
|
||||||
if (!can_grab_front_above()) {
|
if (!can_grab_front_above()) {
|
||||||
Char.x = old_x; /* не за что — назад */
|
Char.x = old_x; /* не за что — назад */
|
||||||
determine_col();
|
determine_col();
|
||||||
} else {
|
return 0;
|
||||||
|
}
|
||||||
Char.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight());
|
Char.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight());
|
||||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||||
Char.fall_y = 0;
|
Char.fall_y = 0;
|
||||||
@@ -860,7 +867,7 @@ static void check_grab(void)
|
|||||||
grab_timer = 12;
|
grab_timer = 12;
|
||||||
pop_sfx_play(9); /* seg006 check_grab */
|
pop_sfx_play(9); /* seg006 check_grab */
|
||||||
is_screaming = 0; /* seg006:1219 */
|
is_screaming = 0; /* seg006:1219 */
|
||||||
}
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void make_loose_fall(int pos, uint8_t modifier);
|
static void make_loose_fall(int pos, uint8_t modifier);
|
||||||
@@ -970,7 +977,15 @@ static void do_fall(void)
|
|||||||
}
|
}
|
||||||
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
|
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
|
||||||
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
|
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
|
||||||
check_grab(); /* ещё летит — попытка зацепа */
|
/* ЗАЦЕП ОТМЕНЯЕТ GUARD. seq_15 — «grab ledge MIDAIR», то есть после
|
||||||
|
* удачного зацепа Char.action == 3, персонаж уже подтянут вплотную к
|
||||||
|
* кромке, а под кромкой в оригинальных комнатах стоит КЛАДКА. Guard,
|
||||||
|
* позванный следом, видел ровно её (t == TILE_WALL, d == 10), считал
|
||||||
|
* это «пролётом сквозь стену», отбрасывал Кида на 5 пикселей назад и
|
||||||
|
* гасил fall_x — зацеп рисовался и тут же срывался. Ловилось на
|
||||||
|
* длинном прыжке уровня 3 (комната 7 -> кромка комнаты 2) и в
|
||||||
|
* attract-демо; регресс-набор — tests/host/t_glide.c. */
|
||||||
|
if (!check_grab()) /* ещё летит — попытка зацепа */
|
||||||
glide_through_wall_guard(); /* и не сквозь кладку (см. выше) */
|
glide_through_wall_guard(); /* и не сквозь кладку (см. выше) */
|
||||||
} else if (Char.curr_row <= 2) {
|
} else if (Char.curr_row <= 2) {
|
||||||
if (get_tile_at_char() == TILE_WALL)
|
if (get_tile_at_char() == TILE_WALL)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include <sprite.h> /* gfx_w0_map/unmap — доступ к странице индекса */
|
#include <sprite.h> /* gfx_w0_map/unmap — доступ к странице индекса */
|
||||||
#include "pop_music.h"
|
#include "pop_music.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
|
#include "_pop_sfx.h" /* pop_snd_ok — открыт ли вывод (см. pop_music_busy) */
|
||||||
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
||||||
#include "_pop_file.h"
|
#include "_pop_file.h"
|
||||||
|
|
||||||
@@ -454,7 +455,15 @@ void pop_music_service(void) __banked
|
|||||||
|
|
||||||
uint8_t pop_music_busy(void) __banked
|
uint8_t pop_music_busy(void) __banked
|
||||||
{
|
{
|
||||||
return (uint8_t)(pop_mus_left != 0);
|
/* «ЗВУЧИТ», А НЕ «ЕСТЬ КУРСОР». Курсор трека двигает насос CBL, и пока
|
||||||
|
* вывод закрыт (pop_sfx_pause на загрузке уровня, Ctrl+S, показ заставки
|
||||||
|
* после Restart Game) pop_mus_left не убывает НИКОГДА. Вызывающий,
|
||||||
|
* который ждёт конца мелодии, в этом состоянии висит вечно — ровно так
|
||||||
|
* заставка после Restart Game вставала на экране Prince of Persia
|
||||||
|
* (поймано пользователем 2026-09-02). Корень того случая был в другом
|
||||||
|
* (CBL не открывался заново, см. pop_boot_sound), но ждать неиграющую
|
||||||
|
* музыку нельзя в принципе, поэтому признак спрашивает и про вывод. */
|
||||||
|
return (uint8_t)(pop_snd_ok && pop_mus_left != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит.
|
/* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит.
|
||||||
|
|||||||
@@ -263,6 +263,13 @@ void pop_sfx_play(uint8_t id)
|
|||||||
if (pop_mus_want && id >= 24 && id <= 43) pop_mus_req = id;
|
if (pop_mus_want && id >= 24 && id <= 43) pop_mus_req = id;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* НАБОР ЕЩЁ ДОЧИТЫВАЕТСЯ. На старте программы звук открывается после
|
||||||
|
* ОДНОЙ страницы (в ней блок тишины), чтобы музыка заставки заиграла
|
||||||
|
* почти сразу; остальные приезжают позже (pop_sfx_init_begin/finish).
|
||||||
|
* Страницы под них уже выделены, но не прочитаны — эффект из такой
|
||||||
|
* страницы был бы шумом из чужих данных. Заявку на МУЗЫКУ выше это
|
||||||
|
* не трогает: её данные лежат отдельно. */
|
||||||
|
if (!pop_snd_pages) return;
|
||||||
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
|
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
|
||||||
snd_next1 = (uint8_t)(id + 1);
|
snd_next1 = (uint8_t)(id + 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,28 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/* Поднять набор: занять 8 EMM-страниц и вычитать в них данные. ВЫВОД НЕ
|
/* ПОДЪЁМ НАБОРА РАЗБИТ НАДВОЕ — ради быстрого старта программы.
|
||||||
* ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 — не получилось (тогда всё
|
*
|
||||||
* дальнейшее молча ничего не делает, игра идёт без звука). */
|
* Насосу, чтобы открыть CBL и заиграть МУЗЫКУ, нужен из всего набора
|
||||||
int pop_sfx_init(void) __banked;
|
* ровно один блок — тишина, и упаковщик обязан класть её первым блоком
|
||||||
|
* СТРАНИЦЫ 0 (tools/pop_idx.py, проверка `_check_entry`). Поэтому:
|
||||||
|
*
|
||||||
|
* pop_sfx_init_begin() — индекс (snd.idx) + ОДНА страница (33 мс).
|
||||||
|
* После него можно звать pop_sfx_start() и
|
||||||
|
* играть музыку; эффектов ещё нет.
|
||||||
|
* pop_sfx_init_finish() — остальные страницы (~230 мс) и публикация
|
||||||
|
* pop_snd_pages: с этого момента есть эффекты.
|
||||||
|
*
|
||||||
|
* Пока набор неполон, pop_sfx_play() молча игнорирует эффекты (заявку на
|
||||||
|
* музыку принимает по-прежнему): страницы под них выделены, но ещё не
|
||||||
|
* прочитаны, и играть из них — это шум из чужих данных.
|
||||||
|
*
|
||||||
|
* ВЫВОД НИ ТА, НИ ДРУГАЯ НЕ ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 —
|
||||||
|
* не получилось (тогда всё дальнейшее молча ничего не делает, игра идёт
|
||||||
|
* без звука; begin при отсутствии индекса поднимает набор из одной
|
||||||
|
* страницы тишины, чтобы не встала музыка). */
|
||||||
|
int pop_sfx_init_begin(void) __banked;
|
||||||
|
int pop_sfx_init_finish(void) __banked;
|
||||||
|
|
||||||
/* Включить вывод (открыть CBL). Звать ПОСЛЕ всей загрузки: пока идёт
|
/* Включить вывод (открыть CBL). Звать ПОСЛЕ всей загрузки: пока идёт
|
||||||
* чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить
|
* чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
#include <sprite.h> /* gfx_w0_map — залить блок тишины */
|
#include <sprite.h> /* gfx_w0_map — залить блок тишины */
|
||||||
|
|
||||||
static uint8_t snd_blk; /* блок EMM под весь набор */
|
static uint8_t snd_blk; /* блок EMM под весь набор */
|
||||||
|
/* Раздельная загрузка (см. шапку pop_sfx.h): сколько страниц обещал индекс
|
||||||
|
* и сколько уже прочитано. 0 в want — begin не проходил либо поднялась
|
||||||
|
* только страница тишины (набора нет), и дочитывать нечего. */
|
||||||
|
static uint8_t snd_want_pages;
|
||||||
|
static uint8_t snd_done_pages;
|
||||||
|
|
||||||
/* --- ИНДЕКС НАБОРА (SND/snd.idx, формат PSI1) ----------------------- *
|
/* --- ИНДЕКС НАБОРА (SND/snd.idx, формат PSI1) ----------------------- *
|
||||||
*
|
*
|
||||||
@@ -142,11 +147,13 @@ static int8_t snd_silence_only(void)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int pop_sfx_init(void) __banked
|
/* ПЕРВАЯ ПОЛОВИНА: индекс + страница 0 (в ней блок тишины). После неё
|
||||||
|
* звук можно открывать, и музыка играет; эффекты — после finish. */
|
||||||
|
int pop_sfx_init_begin(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t i, blk, pages;
|
uint8_t blk, pages;
|
||||||
|
|
||||||
if (pop_snd_ok) return 0;
|
if (snd_blk) return 0; /* набор уже поднимали */
|
||||||
|
|
||||||
/* СНАЧАЛА ИНДЕКС, ПОТОМ ДАННЫЕ: сколько страниц читать и куда какой
|
/* СНАЧАЛА ИНДЕКС, ПОТОМ ДАННЫЕ: сколько страниц читать и куда какой
|
||||||
* эффект лёг — знает только он. Набора без индекса не бывает:
|
* эффект лёг — знает только он. Набора без индекса не бывает:
|
||||||
@@ -179,24 +186,58 @@ int pop_sfx_init(void) __banked
|
|||||||
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) !=
|
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) !=
|
||||||
(int8_t)pages)
|
(int8_t)pages)
|
||||||
return -1;
|
return -1;
|
||||||
|
/* Блок выделяем СРАЗУ на весь набор: номера страниц отдаёт
|
||||||
|
* mem_get_page по индексу, и finish дочитает в те же самые. */
|
||||||
blk = mem_alloc_pages(pages);
|
blk = mem_alloc_pages(pages);
|
||||||
if (!blk) { pop_arc_close(&arc); return -1; }
|
if (!blk) { pop_arc_close(&arc); return -1; }
|
||||||
for (i = 0; i < pages; i++) {
|
pop_snd_page[POP_SND_SILENCE_PAGE] = mem_get_page(blk, 0);
|
||||||
pop_snd_page[i] = mem_get_page(blk, i);
|
if (!pop_snd_page[POP_SND_SILENCE_PAGE] ||
|
||||||
if (!pop_snd_page[i] ||
|
pop_arc_read(&arc, tbl, 0, 0,
|
||||||
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) {
|
pop_snd_page[POP_SND_SILENCE_PAGE], 0, 0) != 16384) {
|
||||||
pop_arc_close(&arc);
|
pop_arc_close(&arc);
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
pop_arc_close(&arc);
|
pop_arc_close(&arc);
|
||||||
}
|
}
|
||||||
snd_blk = blk;
|
snd_blk = blk;
|
||||||
|
snd_want_pages = pages;
|
||||||
|
snd_done_pages = 1;
|
||||||
|
/* pop_snd_pages НЕ ставим: набор ещё неполон, и до finish эффекты
|
||||||
|
* запускать нельзя (страницы выделены, но не прочитаны). */
|
||||||
|
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ВТОРАЯ ПОЛОВИНА: дочитать остальные страницы и разрешить эффекты.
|
||||||
|
* Идемпотентна; архив открывается второй раз (51 мс) — это дешевле, чем
|
||||||
|
* держать открытый манипулятор через всю заставку при лимите в восемь. */
|
||||||
|
int pop_sfx_init_finish(void) __banked
|
||||||
|
{
|
||||||
|
pop_arc_t arc;
|
||||||
|
uint8_t tbl[POP_SND_MAX_PAGES * POP_ARC_REC];
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
if (pop_snd_pages) return 0; /* набор уже полный */
|
||||||
|
if (!snd_want_pages) return -1; /* begin не проходил либо только тишина */
|
||||||
|
if (snd_done_pages < snd_want_pages) {
|
||||||
|
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) !=
|
||||||
|
(int8_t)snd_want_pages)
|
||||||
|
return -1;
|
||||||
|
for (i = snd_done_pages; i < snd_want_pages; i++) {
|
||||||
|
pop_snd_page[i] = mem_get_page(snd_blk, i);
|
||||||
|
if (!pop_snd_page[i] ||
|
||||||
|
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) {
|
||||||
|
pop_arc_close(&arc);
|
||||||
|
return -1; /* страницы не отдаём: тишина в стр.0 жива */
|
||||||
|
}
|
||||||
|
snd_done_pages = (uint8_t)(i + 1);
|
||||||
|
}
|
||||||
|
pop_arc_close(&arc);
|
||||||
|
}
|
||||||
/* Число страниц ставим ПОСЛЕДНИМ: до этого момента насос обязан
|
/* Число страниц ставим ПОСЛЕДНИМ: до этого момента насос обязан
|
||||||
* видеть 0 и не пытаться ходить по полузаполненной таблице. */
|
* видеть 0 и не пытаться ходить по полузаполненной таблице. */
|
||||||
pop_snd_pages = pages;
|
pop_snd_pages = snd_want_pages;
|
||||||
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "pop_pal.h"
|
#include "pop_pal.h"
|
||||||
#include "pop_title.h"
|
#include "pop_title.h"
|
||||||
#include "pop_ui.h"
|
#include "pop_ui.h"
|
||||||
|
#include "sprpop_cold.h" /* pop_boot_sound/pop_boot_rest — фазы старта */
|
||||||
|
|
||||||
#define TITLE_W 320
|
#define TITLE_W 320
|
||||||
#define TITLE_H 200
|
#define TITLE_H 200
|
||||||
@@ -110,10 +111,14 @@ int pop_title_logo_ltr(void) __banked
|
|||||||
return pop_screen_present_ltr() ? 1 : 0;
|
return pop_screen_present_ltr() ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ждать ticks кадров луча, доливая трек. Тема заставки играет с ПЕРВОЙ
|
||||||
|
* страницы (pop_music_stream), остальные обязаны приезжать по ходу — шаг
|
||||||
|
* стоит 16 мс и укладывается в кадровый интервал. */
|
||||||
static uint8_t title_wait(uint16_t ticks)
|
static uint8_t title_wait(uint16_t ticks)
|
||||||
{
|
{
|
||||||
while (ticks--) {
|
while (ticks--) {
|
||||||
if (title_skip_pressed()) return 1;
|
if (title_skip_pressed()) return 1;
|
||||||
|
if (pop_music_loading()) (void)pop_music_load_step();
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync();
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -130,7 +135,8 @@ static void title_load_palette_dimmed(void)
|
|||||||
uint8_t pop_title_show(void) __banked
|
uint8_t pop_title_show(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t step;
|
uint8_t step;
|
||||||
uint8_t skipped = 0, music_ok = 0;
|
uint8_t skipped = 0;
|
||||||
|
uint16_t first_wait = 0; /* такт 0 за вычетом времени загрузки */
|
||||||
int prepared;
|
int prepared;
|
||||||
uint8_t cold_start = (uint8_t)(!title_shown &&
|
uint8_t cold_start = (uint8_t)(!title_shown &&
|
||||||
!pop_settings.show_build_info);
|
!pop_settings.show_build_info);
|
||||||
@@ -154,22 +160,50 @@ uint8_t pop_title_show(void) __banked
|
|||||||
pop_screen_present((uint8_t)prepared);
|
pop_screen_present((uint8_t)prepared);
|
||||||
title_shown = 1;
|
title_shown = 1;
|
||||||
|
|
||||||
/* Тему читаем с диска ЗАРАНЕЕ (пока экран ещё тёмный), а пускаем ниже —
|
/* ЗДЕСЬ ЖЕ ДОГРУЖАЕТСЯ ВСЯ ИГРА — и это не случайное место.
|
||||||
* после fade_in, ровно как show_title: fade_in_2, затем
|
*
|
||||||
* play_sound(sound_54). */
|
* До 2026-09-02 pop_boot читал ВСЕ ресурсы перед первым пикселем: от
|
||||||
music_ok = (uint8_t)(pop_music_load(POP_MUS_INTRO) == 0);
|
* запуска до проявления титула проходило несколько секунд чёрного
|
||||||
|
* экрана в тишине, потому что перед fade_in ещё и вычитывался целиком
|
||||||
|
* трек заставки (248 КБ). Теперь порядок обратный и совпадает с
|
||||||
|
* оригиналом (init_game_main грузит до заставки только мелочь,
|
||||||
|
* seg000.c:156):
|
||||||
|
* 1) картинка титула и fade_in — первое, что происходит;
|
||||||
|
* 2) звук: индекс + ОДНА страница набора, CBL (pop_boot_sound);
|
||||||
|
* 3) тема — с первой же страницы (pop_music_stream), не дожидаясь
|
||||||
|
* всех шестнадцати;
|
||||||
|
* 4) остальные ресурсы — под стоящим титулом и звучащей темой.
|
||||||
|
*
|
||||||
|
* Порядок «fade_in, потом тема» сохранён как у show_title (fade_in_2,
|
||||||
|
* затем play_sound(sound_54)). */
|
||||||
for (step = 0; step < sizeof(title_scene); step++) {
|
for (step = 0; step < sizeof(title_scene); step++) {
|
||||||
if (step && title_draw(title_scene[step], 1) < 0)
|
if (step && title_draw(title_scene[step], 1) < 0)
|
||||||
break;
|
break;
|
||||||
if (step == 0) {
|
if (step == 0) {
|
||||||
|
uint8_t t0;
|
||||||
|
|
||||||
if (pop_pal_fade_in_skip(TITLE_FADE)) { skipped = 1; break; }
|
if (pop_pal_fade_in_skip(TITLE_FADE)) { skipped = 1; break; }
|
||||||
if (music_ok) {
|
(void)pop_boot_sound();
|
||||||
(void)pop_sfx_start();
|
(void)pop_music_stream(POP_MUS_INTRO);
|
||||||
pop_music_play();
|
/* ЧЕМ ЗАПЛАЧЕН ПЕРВЫЙ ТАКТ. Загрузка занимает около секунды, и
|
||||||
|
* без поправки титул простоял бы на первом кадре дольше
|
||||||
|
* оригинала, а сцена уехала бы относительно темы. Считаем
|
||||||
|
* потраченное часами насоса (85,4 Гц, идут от расхода буфера
|
||||||
|
* CBL — единственные, кому не мешает диск) и переводим в кадры
|
||||||
|
* луча сдвигами: 48,83/85,4 = 0,572 ~ 1/2 + 1/16. Без звука
|
||||||
|
* часы стоят, поправка выходит нулевой — и правильно, тогда и
|
||||||
|
* синхронизировать не с чем. */
|
||||||
|
t0 = pop_snd_tick;
|
||||||
|
(void)pop_boot_rest();
|
||||||
|
{
|
||||||
|
uint16_t spent = (uint8_t)(pop_snd_tick - t0);
|
||||||
|
|
||||||
|
spent = (uint16_t)((spent >> 1) + (spent >> 4));
|
||||||
|
first_wait = (spent >= title_wait_ticks[0])
|
||||||
|
? 0 : (uint16_t)(title_wait_ticks[0] - spent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (title_wait(title_wait_ticks[step])) {
|
if (title_wait(step ? title_wait_ticks[step] : first_wait)) {
|
||||||
skipped = 1;
|
skipped = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
||||||
#include "pop_shadow.h" /* запечённый набор Тени */
|
#include "pop_shadow.h" /* запечённый набор Тени */
|
||||||
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
||||||
|
#include <cbl.h> /* cbl_shutdown — погасить железо звука на выходе */
|
||||||
#include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */
|
#include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */
|
||||||
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
||||||
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
||||||
@@ -1114,6 +1115,36 @@ void pop_flip_screen(void) __banked
|
|||||||
* уровень). В резиденте от них оставались только вызовы — сам код здесь.
|
* уровень). В резиденте от них оставались только вызовы — сам код здесь.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Фазы старта (см. шапку pop_boot): обе половины идемпотентны, а
|
||||||
|
* boot_rest_fail нужен, чтобы повторный вызов не пытался читать заново
|
||||||
|
* то, что уже не прочиталось. */
|
||||||
|
static uint8_t boot_sound_done;
|
||||||
|
static uint8_t boot_rest_done;
|
||||||
|
static uint8_t boot_rest_fail;
|
||||||
|
|
||||||
|
/* СТАРТ РАЗБИТ НА ТРИ ФАЗЫ — чтобы игрок увидел заставку и услышал музыку
|
||||||
|
* сразу, а не через несколько секунд чёрного экрана в тишине.
|
||||||
|
*
|
||||||
|
* pop_boot() — только настройки и графика: чёрный экран за ~0,1 с.
|
||||||
|
* pop_boot_sound() — индекс+первая страница звука, индекс музыки, CBL:
|
||||||
|
* после него титул уже показан, и тема заставки может
|
||||||
|
* начать играть, не дожидаясь остальных ресурсов.
|
||||||
|
* pop_boot_rest() — всё тяжёлое (эффекты, Кид, Тень, шрифт) ПОД стоящим
|
||||||
|
* титулом и звучащей темой; идемпотентно.
|
||||||
|
*
|
||||||
|
* ЧЕТЫРЕ ЗАГРУЗКИ ОТСЮДА ПРОСТО УБРАНЫ, а не отложены: атлас фона, страж,
|
||||||
|
* уровень 1 и игровая палитра — это дубликаты того, что и так делает
|
||||||
|
* pop_level_switch() на входе в ЛЮБОЙ уровень (а pop_new_game_load ещё и
|
||||||
|
* pop_pal_game_load перед ним). Оба маршрута в игру, LEVEL_LOAD и DEMO,
|
||||||
|
* упираются в pop_level_switch, так что второй раз читать их до заставки
|
||||||
|
* незачем — это ~0,77 с на ровном месте. Так же устроен и оригинал: у
|
||||||
|
* SDLPoP chtab_5/6/7 грузит load_lev_spr() на старте уровня, а init_game_main
|
||||||
|
* до заставки читает только меч, пламя, звуки и палитры (seg000.c:156).
|
||||||
|
*
|
||||||
|
* ПАЛИТРУ ТРОГАТЬ НЕЛЬЗЯ, пока на экране титул: и kid.pal (все 256 записей),
|
||||||
|
* и палитра тайлсета пишутся прямо в железо и перекрасили бы заставку.
|
||||||
|
* Поэтому в отложенной части нет ни одной палитровой операции — они все
|
||||||
|
* живут на маршруте входа в уровень, за чёрным экраном. */
|
||||||
int pop_boot(void) __banked
|
int pop_boot(void) __banked
|
||||||
{
|
{
|
||||||
/* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */
|
/* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */
|
||||||
@@ -1141,77 +1172,114 @@ int pop_boot(void) __banked
|
|||||||
gfx_set_visible_page(0);
|
gfx_set_visible_page(0);
|
||||||
pop_pal_black();
|
pop_pal_black();
|
||||||
|
|
||||||
/* Набор фона — по типу СТАРТОВОГО уровня; atlas_load безопасен и после
|
/* FONT.ATL — В МИНИМАЛЬНОЙ ФАЗЕ, И ОТКЛАДЫВАТЬ ЕГО НЕЛЬЗЯ.
|
||||||
* initgraph: при первом вызове он сохраняет страницу DSS W0. Уровень к
|
*
|
||||||
* этому моменту ещё не прочитан, поэтому тип берём напрямую от номера. */
|
* Дело не в шрифте: СНИМОК ПАЛИТРЫ физически живёт в хвосте страницы
|
||||||
|
* шрифта (pop_ui.c, POP_FONT_SNAPSHOT_OFF — в W2 килобайта нет), и вся
|
||||||
|
* машинерия яркости начинается с `if (!font_ready) return`. Без него
|
||||||
|
* pop_pal_apply(DIM_BLACK) не гасит, а fade_in нечего проявлять —
|
||||||
|
* заставка возникает разом на полной яркости (поймано пользователем
|
||||||
|
* сразу после первой версии этой перестановки, 2026-09-02).
|
||||||
|
* Цена вопроса — 12 КБ, около 75 мс. */
|
||||||
|
if (pop_menu_init() != 0) {
|
||||||
|
puts("menu font atlas load failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
pop_current_level = FIRST_LEVEL;
|
pop_current_level = FIRST_LEVEL;
|
||||||
if (pop_bg_load(pop_level_type()) != 0) {
|
|
||||||
puts("pop_bg_load failed");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
/* pop_bg_load установил только свои слоты палитры. До конца загрузки
|
|
||||||
* всё равно держим экран чёрным. */
|
|
||||||
pop_pal_black();
|
|
||||||
pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */
|
pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */
|
||||||
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
|
|
||||||
pop_guard_reset();
|
pop_guard_reset();
|
||||||
if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */
|
pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */
|
||||||
puts("kid.ani load failed\n");
|
pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */
|
||||||
return -1;
|
pop_checkpoint = 0; /* init_game (seg003:37) */
|
||||||
|
if (!pop_settings.show_build_info) pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
|
||||||
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
|
/* ЗВУК ВПЕРЁД ОСТАЛЬНЫХ РЕСУРСОВ. Насосу для тишины (а значит и для того,
|
||||||
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
|
* чтобы CBL вообще открылся) нужен ровно ОДИН блок набора, и упаковщик
|
||||||
|
* обязан класть его первым блоком страницы 0 — поэтому здесь читается одна
|
||||||
|
* страница вместо восьми (33 мс против 440). Остальные приезжают в
|
||||||
|
* pop_boot_rest, а до тех пор pop_sfx_play молча пропускает эффекты.
|
||||||
|
*
|
||||||
|
* Звать ПОСЛЕ того, как титул уже проявлен: чтение всё равно занимает
|
||||||
|
* полсекунды, и лучше, чтобы она прошла под картинкой. */
|
||||||
|
int pop_boot_sound(void) __banked
|
||||||
|
{
|
||||||
|
if (!boot_sound_done) {
|
||||||
|
boot_sound_done = 1;
|
||||||
|
(void)pop_sfx_init_begin(); /* эффектов может и не быть — не фатально */
|
||||||
/* Индекс музыкального набора — одна страница EMM. Без него музыки не
|
/* Индекс музыкального набора — одна страница EMM. Без него музыки не
|
||||||
* будет (pop_music_info вернёт -1 на любой трек), но игра работает:
|
* будет (pop_music_info вернёт -1 на любой трек), но игра работает:
|
||||||
* это необязательный ресурс, как и сами треки. */
|
* это необязательный ресурс, как и сами треки. */
|
||||||
(void)pop_music_index_load();
|
(void)pop_music_index_load();
|
||||||
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */
|
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */
|
||||||
|
}
|
||||||
|
/* ОТКРЫТИЕ CBL — ВНЕ РАЗОВОГО ГАРДА, и это не мелочь. Загрузка нужна
|
||||||
|
* один раз за запуск, а вот вывод к моменту следующего показа заставки
|
||||||
|
* почти наверняка закрыт: pop_title_show начинается с pop_sfx_pause, да
|
||||||
|
* и уровень оставляет CBL закрытым. Пока start стоял под флагом,
|
||||||
|
* Restart Game приводил к заставке БЕЗ музыки (поймано пользователем
|
||||||
|
* 2026-09-02), а заодно подвешивал ожидания «дослушать тему»: с
|
||||||
|
* выключенным насосом pop_mus_left не убывает, и pop_music_busy()
|
||||||
|
* истинен вечно. Идемпотентно и уважает Ctrl+S (pop_snd_want). */
|
||||||
|
(void)pop_sfx_start();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Шаг подкачки трека между кусками загрузки: заставочная тема играет уже
|
||||||
|
* с первой страницы (pop_music_stream), остальные обязаны приезжать по
|
||||||
|
* ходу — иначе насос уйдёт в ещё не прочитанную страницу. Запас после
|
||||||
|
* шага — 0,75 с звучания на каждые 8 КБ, куски загрузки короче. */
|
||||||
|
static void boot_music_step(void)
|
||||||
|
{
|
||||||
|
if (pop_music_loading()) (void)pop_music_load_step();
|
||||||
|
}
|
||||||
|
|
||||||
|
int pop_boot_rest(void) __banked
|
||||||
|
{
|
||||||
|
if (boot_rest_done) return boot_rest_fail ? -1 : 0;
|
||||||
|
boot_rest_done = 1;
|
||||||
|
/* Заставку могли пропустить в первую же секунду — тогда звук ещё не
|
||||||
|
* поднимался. Идемпотентно. */
|
||||||
|
(void)pop_boot_sound();
|
||||||
|
|
||||||
|
(void)pop_sfx_init_finish(); /* дочитать эффекты (музыка уже играет) */
|
||||||
|
boot_music_step();
|
||||||
|
if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */
|
||||||
|
puts("kid.ani load failed\n");
|
||||||
|
boot_rest_fail = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
boot_music_step();
|
||||||
|
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
|
||||||
|
boot_music_step();
|
||||||
if (pop_kid_load(KID_PAGES) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
if (pop_kid_load(KID_PAGES) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
||||||
puts("kid atlas load failed");
|
puts("kid atlas load failed");
|
||||||
|
boot_rest_fail = 1;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */
|
boot_music_step();
|
||||||
puts("level load failed");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */
|
|
||||||
pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */
|
|
||||||
pop_checkpoint = 0; /* init_game (seg003:37) */
|
|
||||||
|
|
||||||
/* FONT.ATL нужен palette snapshot/dim; загрузка идёт ещё на чёрной
|
/* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно выполняет
|
||||||
* палитре и поэтому не открывает игроку старое содержимое VRAM. */
|
* pop_level_switch. Единственное исключение — явно включённый
|
||||||
if (pop_menu_init() != 0) {
|
* build-info: его фон по контракту является игровой сценой, поэтому
|
||||||
puts("menu font atlas load failed");
|
* только здесь читаются фон с уровнем и применяется игровая палитра. */
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
/* kid.pal + динамические slots tileset/Shadow — один логический
|
|
||||||
* источник. pop_pal_game_load делает снимок ПОСЛЕ всех доплат. */
|
|
||||||
pop_pal_game_load();
|
|
||||||
/* Сохраняем ИМЕННО игровую палитру: после стартового title/intro она
|
|
||||||
* понадобится как источник обычного fade-in. Для build-info экран
|
|
||||||
* остаётся штатный яркий игровой фон. */
|
|
||||||
if (!pop_settings.show_build_info) {
|
|
||||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно заново
|
|
||||||
* выполняет pop_level_switch. Если нарисовать её сейчас в обе страницы,
|
|
||||||
* она становится сохранённой VRAM и может мелькнуть перед первым title
|
|
||||||
* при следующем запуске приложения. Единственное исключение — явно
|
|
||||||
* включённый build-info: его фон по контракту является игровой сценой. */
|
|
||||||
if (pop_settings.show_build_info) {
|
if (pop_settings.show_build_info) {
|
||||||
|
if (pop_bg_load(pop_level_type()) != 0 ||
|
||||||
|
pop_level_load_num(FIRST_LEVEL) != 0) {
|
||||||
|
puts("build-info scene load failed");
|
||||||
|
boot_rest_fail = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
pop_guard_load();
|
||||||
|
pop_pal_game_load();
|
||||||
pop_start_level();
|
pop_start_level();
|
||||||
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
|
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
|
||||||
gfx_set_draw_page(0);
|
gfx_set_draw_page(0);
|
||||||
pop_char_draw(POP_CH_KID);
|
pop_char_draw(POP_CH_KID);
|
||||||
pop_char_fore(POP_CH_KID);
|
pop_char_fore(POP_CH_KID);
|
||||||
}
|
}
|
||||||
/* Звук включаем ПОСЛЕДНИМ действием инициализации — когда все атласы,
|
|
||||||
* уровень и палитры уже прочитаны. Раньше нельзя: ESTEX уходит в диск
|
|
||||||
* надолго, насос не успевает долить блок, и на слух это скрежет
|
|
||||||
* (pop_sfx.h). Парная к pop_sfx_pause в pop_level_switch. */
|
|
||||||
pop_sfx_start();
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1224,6 +1292,11 @@ int pop_level_switch(void) __banked
|
|||||||
uint8_t old_page;
|
uint8_t old_page;
|
||||||
gfx_rect_t full;
|
gfx_rect_t full;
|
||||||
|
|
||||||
|
/* РЕСУРСЫ МОГУТ БЫТЬ ЕЩЁ НЕ ПРОЧИТАНЫ. Кид, Тень и шрифт грузятся под
|
||||||
|
* заставкой (см. шапку pop_boot), а её можно пропустить клавишей в
|
||||||
|
* первую же секунду. Идемпотентно: со второго раза это проверка байта. */
|
||||||
|
if (pop_boot_rest() != 0) return -1;
|
||||||
|
|
||||||
/* Граница FG0: повреждённый snapshot или старый внешний вызов не должен
|
/* Граница FG0: повреждённый snapshot или старый внешний вызов не должен
|
||||||
* даже пытаться открыть res2015.bin. Финальный маршрут 14 -> ENDING
|
* даже пытаться открыть res2015.bin. Финальный маршрут 14 -> ENDING
|
||||||
* добавит автомат приложения (FG11); пока просто оставляем уровень. */
|
* добавит автомат приложения (FG11); пока просто оставляем уровень. */
|
||||||
@@ -1368,6 +1441,10 @@ int pop_new_game_load(void) __banked
|
|||||||
gfx_wait_vsync();
|
gfx_wait_vsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ДО pop_pal_game_load: она доплачивает слоты палитры по загруженным
|
||||||
|
* наборам (Тень, страж, тайлсет), и звать её раньше их чтения нельзя. */
|
||||||
|
if (pop_boot_rest() != 0) return -1;
|
||||||
|
|
||||||
pop_kid_hp_newgame();
|
pop_kid_hp_newgame();
|
||||||
hitp_max = POP_START_HITP;
|
hitp_max = POP_START_HITP;
|
||||||
pop_timer_new_game();
|
pop_timer_new_game();
|
||||||
@@ -1414,4 +1491,14 @@ void pop_shutdown(void) __banked
|
|||||||
pop_guard_free();
|
pop_guard_free();
|
||||||
pop_kid_data_free();
|
pop_kid_data_free();
|
||||||
pop_level_free();
|
pop_level_free();
|
||||||
|
/* ЖЕЛЕЗО ЗВУКА ГАСИМ ПОСЛЕДНИМ ДЕЙСТВИЕМ, и pop_sfx_close выше для этого
|
||||||
|
* НЕ ДОСТАТОЧНО: он закрывает сессию, а bit7 порта CBL остаётся включён,
|
||||||
|
* пока его держит gfx_wait_vsync ради бита луча. Дальше main просто
|
||||||
|
* возвращается, а crt0 при возврате уходит прямо в ESTEX EXIT, минуя
|
||||||
|
* цепочку atexit (runtime/crt0_banked.s) — то есть vsync-ссылку снять
|
||||||
|
* уже некому. В результате мы уходили в шелл с включённым CBL, и он
|
||||||
|
* крутил свои 256 слотов: тихо до первой чужой записи в порт данных, а
|
||||||
|
* дальше она зацикливалась (поймано пользователем на железе 2026-09-02;
|
||||||
|
* в MAME не воспроизводится). */
|
||||||
|
cbl_shutdown();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,9 +83,21 @@ void pop_flip_screen(void) __banked;
|
|||||||
* Три куска главного цикла с частотой «раз за игру» или «раз на уровень».
|
* Три куска главного цикла с частотой «раз за игру» или «раз на уровень».
|
||||||
* В резиденте от них остались только вызовы. */
|
* В резиденте от них остались только вызовы. */
|
||||||
|
|
||||||
/* Загрузить ресурсы, поднять графику и палитру, поставить Кида на старт и
|
/* СТАРТ В ТРИ ФАЗЫ — ради быстрого появления заставки; подробности и
|
||||||
* нарисовать первый экран. 0 — OK, -1 — фатально (сообщение уже выведено). */
|
* причины (в том числе почему часть загрузок убрана совсем) — в шапке
|
||||||
|
* pop_boot в sprpop_cold.c. 0 — OK, -1 — фатально (сообщение выведено).
|
||||||
|
*
|
||||||
|
* pop_boot() настройки + графика, чёрный экран;
|
||||||
|
* pop_boot_sound() индекс+первая страница звука, индекс музыки, CBL —
|
||||||
|
* звать, когда титул уже на экране: после него можно
|
||||||
|
* пускать тему заставки;
|
||||||
|
* pop_boot_rest() всё тяжёлое (эффекты, Кид, Тень, шрифт) под стоящим
|
||||||
|
* титулом; идемпотентно, страхуется вызовом из
|
||||||
|
* pop_new_game_load/pop_level_switch на случай, если
|
||||||
|
* заставку пропустили сразу. */
|
||||||
int pop_boot(void) __banked;
|
int pop_boot(void) __banked;
|
||||||
|
int pop_boot_sound(void) __banked;
|
||||||
|
int pop_boot_rest(void) __banked;
|
||||||
|
|
||||||
/* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет,
|
/* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет,
|
||||||
* старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан
|
* старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
|
|||||||
OBJS_grab := $(OBJS_phys)
|
OBJS_grab := $(OBJS_phys)
|
||||||
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
|
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
|
||||||
OBJS_wall := $(OBJS_phys)
|
OBJS_wall := $(OBJS_phys)
|
||||||
|
# t_hang — зацеп обязан ДЕРЖАТЬСЯ (регресс glide_through_wall_guard).
|
||||||
|
OBJS_hang := $(OBJS_phys)
|
||||||
# t_gate — открытая решётка не должна считаться стеной у осторожного шага
|
# t_gate — открытая решётка не должна считаться стеной у осторожного шага
|
||||||
# (нужны pop_map + физика, то есть тот же состав).
|
# (нужны pop_map + физика, то есть тот же состав).
|
||||||
OBJS_gate := $(OBJS_phys)
|
OBJS_gate := $(OBJS_phys)
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* t_hang.c — ЗАЦЕП ОБЯЗАН ДЕРЖАТЬСЯ, а не срываться на следующем кадре.
|
||||||
|
*
|
||||||
|
* Регресс 2026-09-02. glide_through_wall_guard() (порт fix_glide_through_wall,
|
||||||
|
* взят в билд 2026-08-31) звался в do_fall СРАЗУ ПОСЛЕ check_grab. Беда в
|
||||||
|
* том, что seq_15 называется grab_ledge_MIDAIR: после удачного зацепа
|
||||||
|
* Char.action == 3 (в воздухе), персонаж уже подтянут вплотную к кромке — а
|
||||||
|
* под кромкой в комнатах оригинала стоит КЛАДКА. Guard видел ровно её
|
||||||
|
* (t == TILE_WALL, d == 10), считал это «пролётом сквозь стену», отбрасывал
|
||||||
|
* персонажа на 5 пикселей назад и гасил fall_x. Зацеп РИСОВАЛСЯ и тут же
|
||||||
|
* срывался — уровень 3 (комната 7, длинный прыжок через четыре пролёта) и
|
||||||
|
* attract-демо (комната 2, прыжок с места).
|
||||||
|
*
|
||||||
|
* ПОЧЕМУ ПРЕЖНИЕ НАБОРЫ ЭТОГО НЕ ПОЙМАЛИ. t_grab судит по «действие стало
|
||||||
|
* вис» — а вис-то наступал, он просто не жил. Отсюда мера здесь другая:
|
||||||
|
* вис обязан ДЕРЖАТЬСЯ подряд несколько кадров. Разница между «зацепился»
|
||||||
|
* и «зацепился и держится» — это и есть разница между багом и нормой.
|
||||||
|
*
|
||||||
|
* СЦЕНА (геометрия уровня 3, комната 7, приведённая внутрь одной комнаты,
|
||||||
|
* чтобы шов не примешивался): кромка на (0,0), под ней кладка (1,0), четыре
|
||||||
|
* пролёта (0,1..4), толчок с плиты (0,5), разбег справа.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "tcheck.h"
|
||||||
|
#include "scene.h"
|
||||||
|
#include "stubs.h"
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
|
||||||
|
#define E 0x00
|
||||||
|
#define F 0x01
|
||||||
|
#define G 0x04
|
||||||
|
#define L 0x0B
|
||||||
|
#define T 0x13
|
||||||
|
#define W 0x14
|
||||||
|
|
||||||
|
static uint8_t room[30] = {
|
||||||
|
F, E, E, E, E, L, F, T, F, T,
|
||||||
|
W, E, E, E, E, W, W, W, W, W,
|
||||||
|
W, E, E, E, E, W, W, W, W, W,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define ACT_HANG_CLIMB 2
|
||||||
|
#define ACT_HANG_STRAIGHT 6
|
||||||
|
#define HOLD_FRAMES 3
|
||||||
|
|
||||||
|
/* Вис ДЕРЖИТСЯ: действие «вис» стоит HOLD_FRAMES кадров подряд. */
|
||||||
|
static uint8_t hang_holds(uint8_t ledge, uint8_t nrun)
|
||||||
|
{
|
||||||
|
uint8_t i, run = 0;
|
||||||
|
room[0] = ledge;
|
||||||
|
sc_room(room, 7);
|
||||||
|
sc_kid_at(9, 0, -1); /* лицом влево, у правого края */
|
||||||
|
sc_run(SC_L, nrun); /* разбег */
|
||||||
|
sc_run(SC_L | SC_U, 6); /* толчок в прыжок */
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_SHIFT, 30); /* полёт, зацеп и жизнь после него */
|
||||||
|
for (i = 0; i < sc_len; i++) {
|
||||||
|
uint8_t a = sc_trace[i].action;
|
||||||
|
if (a == ACT_HANG_CLIMB || a == ACT_HANG_STRAIGHT) {
|
||||||
|
if (++run >= HOLD_FRAMES) return 1;
|
||||||
|
} else run = 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Сколько фаз разбега из 41 дают ЖИВОЙ вис. До регресса — все, кроме
|
||||||
|
* первой (слишком близкий толчок), то есть 34; при сорванном зацепе — ноль. */
|
||||||
|
static uint8_t window(uint8_t ledge)
|
||||||
|
{
|
||||||
|
uint8_t k, n = 0;
|
||||||
|
for (k = 6; k <= 46; k++) if (hang_holds(ledge, k)) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(long_jump_hang_survives_wall_under_ledge)
|
||||||
|
{
|
||||||
|
/* Кромка-пол: под ней кладка — та самая, на которую срабатывал guard. */
|
||||||
|
TC_TRUE(window(F) >= 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(long_jump_hang_survives_on_gate)
|
||||||
|
{
|
||||||
|
/* Кромка-решётка: тайл другой, кладка под ним та же. Проверяется
|
||||||
|
* отдельно, потому что зацеп за решётку идёт своей веткой can_grab. */
|
||||||
|
TC_TRUE(window(G) >= 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
sc_init();
|
||||||
|
TC_RUN(long_jump_hang_survives_wall_under_ledge);
|
||||||
|
TC_RUN(long_jump_hang_survives_on_gate);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -125,6 +125,8 @@ def find_ci(directory, name):
|
|||||||
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
|
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
|
||||||
PACKED = APP / "assets" / "packed" # раскладка диска игры
|
PACKED = APP / "assets" / "packed" # раскладка диска игры
|
||||||
GEN = APP / "gen" # генерируемые заголовки
|
GEN = APP / "gen" # генерируемые заголовки
|
||||||
|
DIST = APP / "dist" # ручное содержимое дистрибутива (README и
|
||||||
|
# производные наборы, напр. тихий звук)
|
||||||
|
|
||||||
|
|
||||||
def atl(name):
|
def atl(name):
|
||||||
@@ -139,3 +141,10 @@ def packed(name):
|
|||||||
p = PACKED / name
|
p = PACKED / name
|
||||||
p.mkdir(parents=True, exist_ok=True)
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def dist(name):
|
||||||
|
"""Каталог внутри dist/ в раскладке диска; создаётся при обращении."""
|
||||||
|
p = DIST / name
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
return p
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""pop_quiet.py — тише в N раз: пережать ГОТОВЫЕ наборы звука и музыки.
|
||||||
|
|
||||||
|
Берёт то, что уже лежит в assets/packed (SND/snd.arc + MUS/m*.bin), делает
|
||||||
|
тише и кладёт в dist/ в той же раскладке диска, ПЕРЕЗАПИСЫВАЯ то, что там
|
||||||
|
было. Оригиналы в assets/packed не трогаются никогда — значит скрипт
|
||||||
|
идемпотентен: сколько раз ни запусти, громкость упадёт ровно один раз.
|
||||||
|
|
||||||
|
ФОРМУЛА. PCM у нас 8 бит БЕЗ ЗНАКА, тишина = 0x80 (см. cbl.h и
|
||||||
|
docs/sound_plan.md), то есть байт — это знаковое отклонение от 0x80.
|
||||||
|
Тише вдвое:
|
||||||
|
|
||||||
|
out = 0x80 + (in - 0x80) / 2 = in / 2 + 0x40
|
||||||
|
|
||||||
|
Правая форма — та самая, что делается на Z80 двумя командами
|
||||||
|
(`srl a` / `add a,#0x40`), поэтому таблицу строим floor-делением: тогда
|
||||||
|
результат совпадает с ней БАЙТ В БАЙТ и его можно сверять с железной
|
||||||
|
реализацией. Тишина остаётся тишиной (0x80 -> 0x80) при любом gain, а
|
||||||
|
клиппинга не бывает по построению: |отклонение| только уменьшается.
|
||||||
|
|
||||||
|
ЧТО ИМЕННО ПЕРЕСЧИТЫВАЕТСЯ.
|
||||||
|
|
||||||
|
MUS/m*.bin — файл целиком: это чистый PCM без заголовка, загрузчик
|
||||||
|
читает его страницами с нулевого смещения (pop_music.c).
|
||||||
|
SND/snd.arc — ТОЛЬКО тела записей архива PBA1. Заголовок (512 Б) и
|
||||||
|
выравнивающий хвост между записями остаются как есть:
|
||||||
|
там нули упаковщика, а не звук, и пересчёт превратил бы
|
||||||
|
их в 0x40 — мусор на месте служебных байт.
|
||||||
|
*.idx — копируются без изменений: длины и смещения не поменялись,
|
||||||
|
меняются только сами отсчёты. Магию всё же проверяем,
|
||||||
|
чтобы молча не скопировать файл чужого формата.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
|
||||||
|
python3 tools/pop_quiet.py # вдвое тише, packed -> dist
|
||||||
|
python3 tools/pop_quiet.py --gain 0.25 # вчетверо тише
|
||||||
|
python3 tools/pop_quiet.py --src DIR --out DIR
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import paths
|
||||||
|
|
||||||
|
SILENCE = 0x80 # центр 8-битного беззнакового PCM
|
||||||
|
ARC_MAGIC = b"PBA1" # архив ресурсов, см. src/pop_arc.h
|
||||||
|
ARC_HEADER = 512
|
||||||
|
ARC_SECTOR = 512
|
||||||
|
SND_IDX_MAGIC = b"PSI1"
|
||||||
|
MUS_IDX_MAGIC = b"PMI1"
|
||||||
|
|
||||||
|
|
||||||
|
def gain_table(gain: float) -> bytes:
|
||||||
|
"""Таблица перекодировки байта: 256 значений, готовые для bytes.translate.
|
||||||
|
|
||||||
|
floor, а не округление к ближайшему: при gain = 1/2 это ровно
|
||||||
|
`in >> 1 + 0x40`, то есть побайтово то же, что сделал бы Z80.
|
||||||
|
"""
|
||||||
|
if not 0.0 <= gain <= 1.0:
|
||||||
|
raise SystemExit("gain обязан лежать в [0, 1]; громче делать нечем — "
|
||||||
|
"запас по амплитуде в исходнике уже выбран")
|
||||||
|
out = bytearray(256)
|
||||||
|
for v in range(256):
|
||||||
|
dev = v - SILENCE # знаковое отклонение
|
||||||
|
out[v] = max(0, min(255, SILENCE + _floor_div(dev, gain)))
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _floor_div(dev: int, gain: float) -> int:
|
||||||
|
"""dev * gain, округлённое ВНИЗ (к минус бесконечности)."""
|
||||||
|
scaled = dev * gain
|
||||||
|
i = int(scaled)
|
||||||
|
return i - 1 if scaled < i else i # int() режет к нулю
|
||||||
|
|
||||||
|
|
||||||
|
def quiet_raw(src: Path, dst: Path, table: bytes) -> int:
|
||||||
|
"""Файл целиком — PCM (музыкальный трек). Возвращает размер."""
|
||||||
|
data = src.read_bytes()
|
||||||
|
dst.write_bytes(data.translate(table))
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
|
||||||
|
def quiet_arc(src: Path, dst: Path, table: bytes) -> int:
|
||||||
|
"""Архив PBA1: пересчитываем только тела записей."""
|
||||||
|
raw = bytearray(src.read_bytes())
|
||||||
|
if raw[0:4] != ARC_MAGIC:
|
||||||
|
raise SystemExit(f"{src}: не {ARC_MAGIC.decode()} — это не наш архив")
|
||||||
|
count = raw[4]
|
||||||
|
if not 1 <= count <= (ARC_HEADER - 8) // 4:
|
||||||
|
raise SystemExit(f"{src}: count = {count}, вне 1..126")
|
||||||
|
for i in range(count):
|
||||||
|
off_sec, size = struct.unpack_from("<HH", raw, 8 + i * 4)
|
||||||
|
off = off_sec * ARC_SECTOR
|
||||||
|
if off + size > len(raw):
|
||||||
|
raise SystemExit(f"{src}: запись {i} выходит за файл "
|
||||||
|
f"({off} + {size} > {len(raw)})")
|
||||||
|
raw[off:off + size] = bytes(raw[off:off + size]).translate(table)
|
||||||
|
dst.write_bytes(bytes(raw))
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def copy_idx(src: Path, dst: Path, magic: bytes) -> None:
|
||||||
|
"""Индекс переносим как есть — но убеждаемся, что он наш."""
|
||||||
|
head = src.read_bytes()[:4]
|
||||||
|
if head != magic:
|
||||||
|
raise SystemExit(f"{src}: магия {head!r}, ожидалась {magic!r}")
|
||||||
|
shutil.copyfile(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def peak(data: bytes) -> int:
|
||||||
|
"""Максимальное отклонение от тишины — для отчёта «стало тише»."""
|
||||||
|
return max(abs(b - SILENCE) for b in data) if data else 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
ap.add_argument("--gain", type=float, default=0.5,
|
||||||
|
help="множитель амплитуды (по умолчанию 0.5 — вдвое тише)")
|
||||||
|
ap.add_argument("--src", type=Path, default=paths.PACKED,
|
||||||
|
help="откуда брать готовые наборы (умолчание assets/packed)")
|
||||||
|
ap.add_argument("--out", type=Path, default=paths.DIST,
|
||||||
|
help="куда класть (умолчание dist/)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
table = gain_table(args.gain)
|
||||||
|
src_snd, src_mus = args.src / "SND", args.src / "MUS"
|
||||||
|
out_snd, out_mus = args.out / "SND", args.out / "MUS"
|
||||||
|
if not src_snd.is_dir() or not src_mus.is_dir():
|
||||||
|
raise SystemExit(f"{args.src}: нет SND/ или MUS/ — сначала "
|
||||||
|
"`make resources`")
|
||||||
|
out_snd.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_mus.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"тише в {1 / args.gain:g} раз(а): {args.src} -> {args.out}")
|
||||||
|
|
||||||
|
arc = src_snd / "snd.arc"
|
||||||
|
before = peak(arc.read_bytes()[ARC_HEADER:])
|
||||||
|
n = quiet_arc(arc, out_snd / "snd.arc", table)
|
||||||
|
after = peak((out_snd / "snd.arc").read_bytes()[ARC_HEADER:])
|
||||||
|
copy_idx(src_snd / "snd.idx", out_snd / "snd.idx", SND_IDX_MAGIC)
|
||||||
|
print(f" SND/snd.arc {n} записей, пик {before} -> {after}")
|
||||||
|
|
||||||
|
tracks = sorted(src_mus.glob("m*.bin"))
|
||||||
|
if not tracks:
|
||||||
|
raise SystemExit(f"{src_mus}: нет ни одного m*.bin")
|
||||||
|
total = 0
|
||||||
|
worst_before = worst_after = 0
|
||||||
|
for t in tracks:
|
||||||
|
total += quiet_raw(t, out_mus / t.name, table)
|
||||||
|
worst_before = max(worst_before, peak(t.read_bytes()))
|
||||||
|
worst_after = max(worst_after, peak((out_mus / t.name).read_bytes()))
|
||||||
|
copy_idx(src_mus / "mus.idx", out_mus / "mus.idx", MUS_IDX_MAGIC)
|
||||||
|
print(f" MUS/ {len(tracks)} треков, {total // 1024} КБ, "
|
||||||
|
f"пик {worst_before} -> {worst_after}")
|
||||||
|
print("индексы (snd.idx, mus.idx) скопированы без изменений — длины те же")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""test_pop_quiet — правило громкости и неприкосновенность служебных байт.
|
||||||
|
|
||||||
|
Скрипт трогает данные, которые потом играет насос на Z80, поэтому тест
|
||||||
|
закрепляет ровно две вещи: формулу (она обязана совпадать с беззнаковым
|
||||||
|
`A/2 + 0x40`, чтобы результат можно было сверять с железной реализацией) и
|
||||||
|
то, что заголовок архива с выравнивающим хвостом остаются как были.
|
||||||
|
|
||||||
|
Запуск: make test-tools (либо python3 -m unittest discover tools/tests)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import pop_quiet as X
|
||||||
|
|
||||||
|
|
||||||
|
class Formula(unittest.TestCase):
|
||||||
|
"""Таблица громкости: то же, что сделал бы Z80."""
|
||||||
|
|
||||||
|
def test_half_is_shift_plus_40(self):
|
||||||
|
t = X.gain_table(0.5)
|
||||||
|
for v in range(256):
|
||||||
|
self.assertEqual(t[v], (v >> 1) + 0x40, f"байт {v:#04x}")
|
||||||
|
|
||||||
|
def test_silence_stays_silence(self):
|
||||||
|
for gain in (0.5, 0.25, 0.1, 1.0):
|
||||||
|
self.assertEqual(X.gain_table(gain)[X.SILENCE], X.SILENCE)
|
||||||
|
|
||||||
|
def test_no_clipping_and_range_shrinks(self):
|
||||||
|
t = X.gain_table(0.5)
|
||||||
|
self.assertEqual(t[0x00], 0x40)
|
||||||
|
self.assertEqual(t[0xFF], 0xBF)
|
||||||
|
for v in range(256):
|
||||||
|
self.assertLessEqual(abs(t[v] - X.SILENCE), abs(v - X.SILENCE))
|
||||||
|
|
||||||
|
def test_gain_one_is_identity(self):
|
||||||
|
self.assertEqual(X.gain_table(1.0), bytes(range(256)))
|
||||||
|
|
||||||
|
def test_gain_out_of_range_rejected(self):
|
||||||
|
for bad in (-0.1, 1.5):
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
X.gain_table(bad)
|
||||||
|
|
||||||
|
|
||||||
|
def make_arc(bodies):
|
||||||
|
"""Собрать PBA1 ровно так, как это делает tools/pop_pack_arc.py."""
|
||||||
|
raw = bytearray(X.ARC_HEADER)
|
||||||
|
raw[0:4] = X.ARC_MAGIC
|
||||||
|
raw[4] = len(bodies)
|
||||||
|
blob = bytearray()
|
||||||
|
for i, body in enumerate(bodies):
|
||||||
|
off = X.ARC_HEADER + len(blob)
|
||||||
|
struct.pack_into("<HH", raw, 8 + i * 4, off // X.ARC_SECTOR, len(body))
|
||||||
|
blob += body
|
||||||
|
blob += bytes(-len(blob) % X.ARC_SECTOR) # хвост выравнивания — нули
|
||||||
|
return bytes(raw + blob)
|
||||||
|
|
||||||
|
|
||||||
|
class Archive(unittest.TestCase):
|
||||||
|
"""PBA1: тела пересчитаны, служебные байты — нет."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.dir = Path(tempfile.mkdtemp())
|
||||||
|
self.bodies = [bytes([0x80, 0xFF, 0x00, 0x81, 0x7F]), bytes([0xC0] * 3)]
|
||||||
|
self.src = self.dir / "in.arc"
|
||||||
|
self.dst = self.dir / "out.arc"
|
||||||
|
self.src.write_bytes(make_arc(self.bodies))
|
||||||
|
|
||||||
|
def test_bodies_scaled(self):
|
||||||
|
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||||
|
raw = self.dst.read_bytes()
|
||||||
|
off = X.ARC_HEADER
|
||||||
|
got = raw[off:off + len(self.bodies[0])]
|
||||||
|
self.assertEqual(got, bytes([0x80, 0xBF, 0x40, 0x80, 0x7F]))
|
||||||
|
|
||||||
|
def test_header_and_padding_untouched(self):
|
||||||
|
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||||
|
src, dst = self.src.read_bytes(), self.dst.read_bytes()
|
||||||
|
self.assertEqual(len(src), len(dst))
|
||||||
|
self.assertEqual(src[:X.ARC_HEADER], dst[:X.ARC_HEADER])
|
||||||
|
pad_at = X.ARC_HEADER + len(self.bodies[0])
|
||||||
|
pad_len = -len(self.bodies[0]) % X.ARC_SECTOR
|
||||||
|
self.assertEqual(dst[pad_at:pad_at + pad_len], bytes(pad_len))
|
||||||
|
|
||||||
|
def test_foreign_magic_rejected(self):
|
||||||
|
self.src.write_bytes(b"NOPE" + bytes(X.ARC_HEADER))
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||||
|
|
||||||
|
|
||||||
|
class Track(unittest.TestCase):
|
||||||
|
"""Музыкальный трек — чистый PCM, пересчитывается целиком."""
|
||||||
|
|
||||||
|
def test_whole_file(self):
|
||||||
|
d = Path(tempfile.mkdtemp())
|
||||||
|
src, dst = d / "m54.bin", d / "out.bin"
|
||||||
|
src.write_bytes(bytes([0x00, 0x80, 0xFF] * 4))
|
||||||
|
n = X.quiet_raw(src, dst, X.gain_table(0.5))
|
||||||
|
self.assertEqual(n, 12)
|
||||||
|
self.assertEqual(dst.read_bytes(), bytes([0x40, 0x80, 0xBF] * 4))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Локальные исполняемые файлы платформенных проб собираются рядом с
|
||||||
|
# исходниками. Общий шаблон репозитория не покрывает этот дополнительный
|
||||||
|
# уровень вложенности applications/Volkov/tests/<probe>/.
|
||||||
|
/tests/*/*.exe
|
||||||
|
|
||||||
|
# Клон исторических исходников VC служит локальным справочным материалом и
|
||||||
|
# сохраняет собственный .git; во внешний репозиторий его как gitlink не кладём.
|
||||||
|
/VolkovCommander/
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Sprinter Commander — двухпанельный файловый менеджер.
|
||||||
|
|
||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := sprcmd
|
||||||
|
SRC_DIR := src
|
||||||
|
BUILD_DIR := build
|
||||||
|
MEMORY := big
|
||||||
|
ALLOCS ?= 3000
|
||||||
|
HDD_IMG := $(CURDIR)/build/hdd/sprcmd.chd
|
||||||
|
MDVIEW2_DIR := $(PROJ_ROOT)/examples/mdview2
|
||||||
|
MDVIEW2_SRCS := $(wildcard $(MDVIEW2_DIR)/*.c)
|
||||||
|
MDVIEW2_HEADERS := $(wildcard $(MDVIEW2_DIR)/*.h)
|
||||||
|
MDVIEW2_TEST_EXE := $(CURDIR)/build/external/mdview2.exe
|
||||||
|
P5_COPY_FAULT_EXE := $(CURDIR)/build/tests/cperr.exe
|
||||||
|
P6_SORT_TEST_EXE := $(CURDIR)/build/tests/sortrt.exe
|
||||||
|
P7_SELECT_TEST_EXE := $(CURDIR)/build/tests/selrt.exe
|
||||||
|
P9_MKDIR_TEST_EXE := $(CURDIR)/build/tests/mkrt.exe
|
||||||
|
P10_RENAME_TEST_EXE := $(CURDIR)/build/tests/renrt.exe
|
||||||
|
P11_DELETE_TEST_EXE := $(CURDIR)/build/tests/delrt.exe
|
||||||
|
P19_OVERWRITE_TEST_EXE := $(CURDIR)/build/tests/p19/delrt.exe
|
||||||
|
BANK1_SRCS := src/sc_source_fs.c src/sc_sort.c src/sc_select.c \
|
||||||
|
src/sc_copy_job.c src/sc_metadata.c src/sc_disk.c src/sc_tree.c src/sc_exec_ui.c
|
||||||
|
BANK2_SRCS := src/sc_draw.c src/sc_dialog.c src/sc_copy_ui.c \
|
||||||
|
src/sc_file_ui.c src/sc_name.c src/sc_rename.c
|
||||||
|
EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) -I include \
|
||||||
|
$(foreach f,$(BANK1_SRCS),--bank 1=$(f)) \
|
||||||
|
$(foreach f,$(BANK2_SRCS),--bank 2=$(f))
|
||||||
|
EXTRA_SRCS := \
|
||||||
|
src/sc_app.c \
|
||||||
|
src/sc_command.c \
|
||||||
|
src/sc_input.c \
|
||||||
|
src/sc_path.c \
|
||||||
|
src/sc_panel.c \
|
||||||
|
src/sc_platform_screen.c \
|
||||||
|
src/sc_platform_exec.c \
|
||||||
|
src/sc_screen.c \
|
||||||
|
src/sc_store.c \
|
||||||
|
src/sc_theme.c \
|
||||||
|
src/sc_video_system.c
|
||||||
|
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
|
|
||||||
|
HEADERS := $(wildcard include/*.h)
|
||||||
|
|
||||||
|
$(EXE): Makefile $(HEADERS) $(BANK1_SRCS) $(BANK2_SRCS)
|
||||||
|
|
||||||
|
hdd-stress: $(EXE)
|
||||||
|
sh tests/make_p3_stress_hdd.sh $(CURDIR)/build/hdd/p3_stress.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-copy: $(EXE)
|
||||||
|
sh tests/make_p4_copy_hdd.sh $(CURDIR)/build/hdd/p4_copy.chd $(EXE)
|
||||||
|
|
||||||
|
tests/p4_exec_child/p4child.exe: tests/p4_exec_child/p4child.c \
|
||||||
|
tests/p4_exec_child/Makefile
|
||||||
|
$(MAKE) -C tests/p4_exec_child ALLOCS=$(ALLOCS)
|
||||||
|
|
||||||
|
hdd-exec: $(EXE) tests/p4_exec_child/p4child.exe
|
||||||
|
sh tests/make_p4_exec_hdd.sh $(CURDIR)/build/hdd/p4_exec.chd \
|
||||||
|
$(EXE) tests/p4_exec_child/p4child.exe
|
||||||
|
|
||||||
|
hdd-p5: $(EXE)
|
||||||
|
sh tests/make_p5_hdd.sh $(CURDIR)/build/hdd/p5_stability.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p5-enospc: $(EXE)
|
||||||
|
sh tests/make_p5_enospc_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p5_enospc.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p5-panels: $(EXE)
|
||||||
|
sh tests/make_p5_panel_edges_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p5_panel_edges.chd $(EXE)
|
||||||
|
|
||||||
|
$(MDVIEW2_TEST_EXE): $(MDVIEW2_SRCS) $(MDVIEW2_HEADERS) $(LIB)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(PROJ_ROOT)/bin/sprinter-cc --memory small --max-allocs $(ALLOCS) \
|
||||||
|
-o $@ $(MDVIEW2_SRCS)
|
||||||
|
|
||||||
|
hdd-p5-viewer: $(EXE) $(MDVIEW2_TEST_EXE)
|
||||||
|
sh tests/make_p5_viewer_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p5_viewer.chd $(EXE) \
|
||||||
|
$(MDVIEW2_TEST_EXE) $(MDVIEW2_DIR)/README.md
|
||||||
|
|
||||||
|
$(P5_COPY_FAULT_EXE): tests/p5_copy_faults.c src/sc_copy_job.c \
|
||||||
|
src/sc_metadata.c src/sc_disk.c \
|
||||||
|
src/sc_path.c src/sc_platform_exec.c include/sc_copy.h \
|
||||||
|
include/sc_metadata.h include/sc_disk.h include/sc_path.h \
|
||||||
|
include/sc_exec.h $(LIB)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(PROJ_ROOT)/bin/sprinter-cc --memory big --safe --max-allocs $(ALLOCS) \
|
||||||
|
-I include --bank 1=src/sc_copy_job.c --bank 1=src/sc_metadata.c \
|
||||||
|
--bank 1=src/sc_disk.c -o $@ \
|
||||||
|
tests/p5_copy_faults.c src/sc_path.c src/sc_platform_exec.c
|
||||||
|
|
||||||
|
hdd-p5-copy-faults: $(P5_COPY_FAULT_EXE)
|
||||||
|
sh tests/make_p5_copy_faults_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p5_copy_faults.chd $(P5_COPY_FAULT_EXE)
|
||||||
|
|
||||||
|
hdd-p5-media: $(EXE)
|
||||||
|
sh tests/make_p5_media_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p5_media.chd $(EXE)
|
||||||
|
|
||||||
|
$(P6_SORT_TEST_EXE): tests/p6_sort_probe.c src/sc_sort.c src/sc_store.c \
|
||||||
|
include/sc_entry.h include/sc_panel.h include/sc_sort.h \
|
||||||
|
include/sc_store.h $(LIB)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(PROJ_ROOT)/bin/sprinter-cc --memory big --safe --max-allocs $(ALLOCS) \
|
||||||
|
-I include --bank 1=src/sc_sort.c -o $@ \
|
||||||
|
tests/p6_sort_probe.c src/sc_store.c
|
||||||
|
|
||||||
|
hdd-p6-sort: $(EXE) $(P6_SORT_TEST_EXE)
|
||||||
|
sh tests/make_p6_sort_hdd.sh $(CURDIR)/build/hdd/p6_sort.chd \
|
||||||
|
$(EXE) $(P6_SORT_TEST_EXE)
|
||||||
|
|
||||||
|
$(P7_SELECT_TEST_EXE): tests/p7_select_probe.c src/sc_select.c \
|
||||||
|
src/sc_store.c include/sc_entry.h include/sc_panel.h \
|
||||||
|
include/sc_select.h include/sc_store.h $(LIB)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(PROJ_ROOT)/bin/sprinter-cc --memory big --safe --max-allocs $(ALLOCS) \
|
||||||
|
-I include --bank 1=src/sc_select.c -o $@ \
|
||||||
|
tests/p7_select_probe.c src/sc_store.c
|
||||||
|
|
||||||
|
hdd-p7-select: $(EXE) $(P7_SELECT_TEST_EXE)
|
||||||
|
sh tests/make_p7_select_hdd.sh $(CURDIR)/build/hdd/p7_select.chd \
|
||||||
|
$(EXE) $(P7_SELECT_TEST_EXE)
|
||||||
|
|
||||||
|
hdd-p8-masks: hdd-p7-select
|
||||||
|
|
||||||
|
$(P9_MKDIR_TEST_EXE): tests/p9_mkdir_probe.c src/sc_name.c src/sc_path.c \
|
||||||
|
include/sc_path.h $(LIB)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(PROJ_ROOT)/bin/sprinter-cc --memory big --safe --max-allocs $(ALLOCS) \
|
||||||
|
-I include --bank 1=src/sc_name.c -o $@ \
|
||||||
|
tests/p9_mkdir_probe.c src/sc_path.c
|
||||||
|
|
||||||
|
hdd-p9-mkdir: $(EXE) $(P9_MKDIR_TEST_EXE)
|
||||||
|
sh tests/make_p9_mkdir_hdd.sh $(CURDIR)/build/hdd/p9_mkdir.chd \
|
||||||
|
$(EXE) $(P9_MKDIR_TEST_EXE)
|
||||||
|
|
||||||
|
$(P10_RENAME_TEST_EXE): tests/p10_rename_probe.c src/sc_rename.c \
|
||||||
|
include/sc_file_ops.h $(LIB)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(PROJ_ROOT)/bin/sprinter-cc --memory big --safe --max-allocs $(ALLOCS) \
|
||||||
|
-I include --bank 1=src/sc_rename.c -o $@ \
|
||||||
|
tests/p10_rename_probe.c
|
||||||
|
|
||||||
|
hdd-p10-rename: $(EXE) $(P10_RENAME_TEST_EXE)
|
||||||
|
sh tests/make_p10_rename_hdd.sh $(CURDIR)/build/hdd/p10_rename.chd \
|
||||||
|
$(EXE) $(P10_RENAME_TEST_EXE)
|
||||||
|
|
||||||
|
$(P11_DELETE_TEST_EXE): tests/p11_delete_probe.c $(LIB)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(PROJ_ROOT)/bin/sprinter-cc --memory big --safe --max-allocs $(ALLOCS) \
|
||||||
|
-o $@ tests/p11_delete_probe.c
|
||||||
|
|
||||||
|
hdd-p11-delete: $(EXE) $(P11_DELETE_TEST_EXE)
|
||||||
|
sh tests/make_p11_delete_hdd.sh $(CURDIR)/build/hdd/p11_delete.chd \
|
||||||
|
$(EXE) $(P11_DELETE_TEST_EXE)
|
||||||
|
|
||||||
|
hdd-p12-group-copy: $(EXE)
|
||||||
|
sh tests/make_p12_group_copy_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p12_group_copy.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p13-group-delete: $(EXE)
|
||||||
|
sh tests/make_p13_group_delete_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p13_group_delete.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p14-default-colors: $(EXE)
|
||||||
|
sh tests/make_p14_default_colors_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p14_default_colors.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p15-tree-copy: $(EXE)
|
||||||
|
sh tests/make_p15_tree_copy_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p15_tree_copy.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p15-tree-cancel: $(EXE)
|
||||||
|
sh tests/make_p15_tree_cancel_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p15_tree_cancel.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p15-tree-self: $(EXE)
|
||||||
|
sh tests/make_p15_tree_self_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p15_tree_self.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p16-group-tree-copy: $(EXE)
|
||||||
|
sh tests/make_p16_group_tree_copy_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p16_group_tree_copy.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p17-tree-delete: $(EXE)
|
||||||
|
sh tests/make_p17_tree_delete_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p17_tree_delete.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p17-tree-delete-cancel: $(EXE)
|
||||||
|
sh tests/make_p17_tree_delete_cancel_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p17_tree_delete_cancel.chd $(EXE)
|
||||||
|
|
||||||
|
hdd-p18-metadata: $(EXE)
|
||||||
|
sh tests/make_p18_metadata_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p18_metadata.chd $(EXE)
|
||||||
|
|
||||||
|
$(P19_OVERWRITE_TEST_EXE): tests/p19_overwrite_probe.c \
|
||||||
|
src/sc_copy_job.c src/sc_metadata.c src/sc_disk.c src/sc_path.c \
|
||||||
|
include/sc_copy.h include/sc_metadata.h include/sc_disk.h \
|
||||||
|
include/sc_path.h $(LIB)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(PROJ_ROOT)/bin/sprinter-cc --memory big --safe --max-allocs $(ALLOCS) \
|
||||||
|
-I include --bank 1=src/sc_copy_job.c --bank 1=src/sc_metadata.c \
|
||||||
|
--bank 1=src/sc_disk.c \
|
||||||
|
-o $@ tests/p19_overwrite_probe.c src/sc_path.c
|
||||||
|
|
||||||
|
hdd-p19-overwrite: $(P19_OVERWRITE_TEST_EXE)
|
||||||
|
sh tests/make_p19_overwrite_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p19_overwrite.chd $(P19_OVERWRITE_TEST_EXE)
|
||||||
|
|
||||||
|
hdd-p19-policy: $(EXE)
|
||||||
|
sh tests/make_p19_policy_hdd.sh \
|
||||||
|
$(CURDIR)/build/hdd/p19_policy.chd $(EXE)
|
||||||
|
|
||||||
|
.PHONY: hdd-stress hdd-copy hdd-exec hdd-p5 hdd-p5-enospc hdd-p5-panels \
|
||||||
|
hdd-p5-viewer hdd-p5-copy-faults hdd-p5-media hdd-p6-sort hdd-p7-select \
|
||||||
|
hdd-p8-masks hdd-p9-mkdir hdd-p10-rename hdd-p11-delete \
|
||||||
|
hdd-p12-group-copy hdd-p13-group-delete hdd-p14-default-colors \
|
||||||
|
hdd-p15-tree-copy hdd-p15-tree-cancel hdd-p15-tree-self \
|
||||||
|
hdd-p16-group-tree-copy hdd-p17-tree-delete hdd-p17-tree-delete-cancel \
|
||||||
|
hdd-p18-metadata hdd-p19-overwrite hdd-p19-policy
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Sprinter Commander
|
||||||
|
|
||||||
|
Функциональный аналог классического двухпанельного Commander для Sprinter
|
||||||
|
Sp2000. Volkov Commander 4.05/4.99 и Dos Navigator используются как
|
||||||
|
поведенческие и архитектурные референсы; проект не является переносом их
|
||||||
|
DOS-кода.
|
||||||
|
|
||||||
|
Текущее состояние: PoC 0.1 (этапы P1–P5) пройден в MAME / BIOS 3.06 /
|
||||||
|
DSS 1.71.57; аппаратный smoke-тест остаётся внешним контрольным пунктом.
|
||||||
|
Начат этап 0.2: готовы независимые для каждой панели сортировки по имени,
|
||||||
|
расширению, размеру и дате/времени в обоих направлениях, выбор записей через
|
||||||
|
`Insert`, `*` и маски `+`/`-`, создание каталога по `F7` и локальное
|
||||||
|
переименование файла/каталога по `F6` без overwrite, однообъектный F8 и
|
||||||
|
групповой рекурсивный F8 для выбранных файлов и каталогов, а также рекурсивный
|
||||||
|
F5 для выбранных файлов и каталогов. F5 сохраняет FAT date/time и атрибуты
|
||||||
|
`R/H/S/A` обычных файлов и поддерживает conflict-policy
|
||||||
|
`overwrite/skip/rename`, включая `overwrite all`/`skip all`, отдельную
|
||||||
|
защиту readonly и backup/rollback. Обход использует динамическую EMM-очередь.
|
||||||
|
Встроенный профиль имён уже различает белые каталоги, жёлтые `.EXE` и
|
||||||
|
светло-серые остальные файлы; загрузка пользовательских цветовых правил
|
||||||
|
остаётся задачей 1.0.
|
||||||
|
Командер
|
||||||
|
сканирует, хранит и сортирует каталоги, показывает явную виртуальную запись
|
||||||
|
предела DSS 1.71, безопасно копирует один файл через выделенную EMM-страницу
|
||||||
|
W3 и запускает `.EXE` с восстановлением режима, палитры, CWD и обеих панелей.
|
||||||
|
|
||||||
|
## Сборка
|
||||||
|
|
||||||
|
Из этого каталога:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат: `build/sprcmd.exe`, конфигурация
|
||||||
|
`--memory big --safe --max-allocs 3000`. Значение `ALLOCS=3000` принято как
|
||||||
|
исходное для Commander и всех его проб; при диагностике его можно переопределить
|
||||||
|
явно, например `make ALLOCS=4000`.
|
||||||
|
|
||||||
|
Основные тесты используют быстрый HDD `D:`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make hdd # только Commander
|
||||||
|
make hdd-stress # большой каталог P3
|
||||||
|
make hdd-copy # набор успех/overwrite/cancel/zero для P4.1
|
||||||
|
make hdd-exec # запуск child/restore/error для P4.2
|
||||||
|
make hdd-p5 # 20 copy, граничные размеры, 100 refresh, lifecycle
|
||||||
|
make hdd-p5-enospc # реальный disk-full и cleanup temp
|
||||||
|
make hdd-p5-panels # точные границы панели 27/28 записей
|
||||||
|
make hdd-p5-copy-faults # детерминированная матрица ошибок copy/EXEC
|
||||||
|
make hdd-p5-viewer # запуск реального mdview2 и восстановление Commander
|
||||||
|
make hdd-p5-media # снятие/возврат hard2 во время F5
|
||||||
|
make hdd-p6-sort # target-матрица и UI-сценарий сортировок 0.2
|
||||||
|
make hdd-p7-select # выбор Insert/*, статистика и EMM cleanup
|
||||||
|
make hdd-p8-masks # тот же расширенный образ: редактор и маски +/−
|
||||||
|
make hdd-p9-mkdir # validator DOS 8.3, mkdir/error/CWD и UI F7
|
||||||
|
make hdd-p10-rename # file/dir rename, no-overwrite, CWD и UI F6
|
||||||
|
make hdd-p11-delete # confirm/cancel, file/empty-dir, guards и UI F8
|
||||||
|
make hdd-p12-group-copy # legacy group fixture; current F5 also copies selected dirs
|
||||||
|
make hdd-p13-group-delete # legacy file-only group F8 regression
|
||||||
|
make hdd-p14-default-colors # built-in dir/EXE/file colours и restore палитры
|
||||||
|
make hdd-p15-tree-copy # recursive current directory, depth=16 и repeat guard
|
||||||
|
make hdd-p15-tree-cancel # Esc в 4-МБ файле и cleanup temp/traversal page
|
||||||
|
make hdd-p15-tree-self # target-inside-source guard до mutation
|
||||||
|
make hdd-p16-group-tree-copy # selected files + multiple directory roots
|
||||||
|
make hdd-p17-tree-delete # recursive group F8, depth=12, cancel confirmation
|
||||||
|
make hdd-p17-tree-delete-cancel # Esc после начала recursive group F8
|
||||||
|
make hdd-p18-metadata # exact FAT date/time + R/H/S/A после F5
|
||||||
|
make hdd-p19-overwrite # target backup/commit/rollback matrix
|
||||||
|
make hdd-p19-policy # UI overwrite/skip/rename/all/readonly/cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
`make floppy` сохранён для smoke-проверки совместимости с FDD и не запускает
|
||||||
|
эмулятор. Подробнее: [политика тестовых носителей](docs/test-media.md).
|
||||||
|
|
||||||
|
## Документы
|
||||||
|
|
||||||
|
- [требования PoC](docs/commander-requirements.md);
|
||||||
|
- [архитектура](docs/commander-architecture.md);
|
||||||
|
- [roadmap](docs/commander-roadmap.md);
|
||||||
|
- [клавиши и команды](docs/commander-keymap.md);
|
||||||
|
- [результаты платформенных проб P1](docs/p1-platform-probes.md);
|
||||||
|
- [экранный backend и границы BIOS API](docs/p2-screen-backend.md);
|
||||||
|
- [результаты coordinate-screen probe P2.1](docs/p2-screen-probe-results.md);
|
||||||
|
- [результаты системной палитры P2.2](docs/p2-palette-results.md);
|
||||||
|
- [результаты двухпанельного skeleton P2](docs/p2-skeleton-results.md);
|
||||||
|
- [результаты файловых панелей P3](docs/p3-panel-results.md);
|
||||||
|
- [результаты копирования P4.1](docs/p4-copy-results.md);
|
||||||
|
- [результаты запуска EXE P4.2](docs/p4-exec-results.md);
|
||||||
|
- [результаты стабилизации P5](docs/p5-stability-results.md);
|
||||||
|
- [результаты сортировок 0.2/P6](docs/p6-sort-results.md);
|
||||||
|
- [результаты выбора записей 0.2/P7](docs/p7-selection-results.md);
|
||||||
|
- [результаты выбора по маске 0.2/P8](docs/p8-mask-results.md);
|
||||||
|
- [результаты создания каталога 0.2/P9](docs/p9-mkdir-results.md);
|
||||||
|
- [результаты локального rename 0.2/P10](docs/p10-rename-results.md);
|
||||||
|
- [результаты однообъектного delete 0.2/P11](docs/p11-delete-results.md);
|
||||||
|
- [результаты группового F5 0.2/P12](docs/p12-group-copy-results.md);
|
||||||
|
- [результаты группового F8 0.2/P13](docs/p13-group-delete-results.md);
|
||||||
|
- [результаты стандартных цветов P14](docs/p14-default-colors-results.md);
|
||||||
|
- [результаты рекурсивного F5/P15](docs/p15-tree-copy-results.md);
|
||||||
|
- [результаты рекурсивного group F5/P16](docs/p16-group-tree-copy-results.md);
|
||||||
|
- [результаты рекурсивного group F8/P17](docs/p17-tree-delete-results.md);
|
||||||
|
- [результаты сохранения метаданных F5/P18](docs/p18-metadata-results.md);
|
||||||
|
- [результаты conflict-policy F5/P19](docs/p19-copy-policy-results.md);
|
||||||
|
- [направление развития UI по образцу VC](docs/ui-reference.md);
|
||||||
|
- [инструкция и ограничения PoC 0.1](docs/poc-0.1-release.md);
|
||||||
|
- [носители интеграционных тестов](docs/test-media.md);
|
||||||
|
- [предел больших каталогов DSS и стратегия Commander](docs/dss-large-directories.md).
|
||||||
|
|
||||||
|
Экран выводится системными координатными функциями ESTEX/BIOS без оконных
|
||||||
|
дескрипторов и без прямой адресации VRAM. Системные вызовы палитры разрешены;
|
||||||
|
их save/set/readback/restore и многоцветная сцена подтверждены пробой P2.2.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Артефакты MAME-прогона P1
|
||||||
|
|
||||||
|
Дата: 6 сентября 2026 года.
|
||||||
|
|
||||||
|
Окружение: MAME 0.287 (`b0c4527c`), машина `sprinter`, BIOS `v3.06`, текущие системные
|
||||||
|
образы проекта. `p1probe.exe` и `p1child.exe` запускались с дискеты A:.
|
||||||
|
|
||||||
|
Каталоги:
|
||||||
|
|
||||||
|
- `directory/` — CWD, `*.*`, `*`, завершение F_NEXT с `ENOENT=3`;
|
||||||
|
`strict-enoent.png` снят после удаления временного допуска `EISDIR`;
|
||||||
|
- `emm/` — три страницы, граничные patterns и возврат free pages;
|
||||||
|
- `dskinfo/` — геометрия и свободное место текущего диска;
|
||||||
|
- `winrest/` — полный экран 80x32 и частичный вывод из offset `0x2000`;
|
||||||
|
`partial-phase-hidden.png`/`partial-stable.png` показывают неудачные raw-
|
||||||
|
атрибуты, а `partial-flash-phase.png`/`partial-contrast-stable.png` —
|
||||||
|
именованный цветной pattern. Повторная проверка декодированных RGB показала,
|
||||||
|
что два последних кадра пиксельно идентичны; P2.2 отдельно подтвердил
|
||||||
|
стабильность четырёх палитровых планов;
|
||||||
|
- `exec/` — оба вызова child и итоговые значения EXEC/WAIT;
|
||||||
|
- `exec-then-emm/` — повторный EMM-тест после двух EXEC в том же процессе;
|
||||||
|
- `keyboard-commander/` — навигация, F1–F10, Ctrl+R, Ctrl+PgUp, Alt+F1;
|
||||||
|
- `keyboard/` — дополнительная проба Ctrl+A, Alt+X и Shift+Tab.
|
||||||
|
|
||||||
|
Нумерованные клавиатурные кадры соответствуют времени сценария:
|
||||||
|
|
||||||
|
- `0000.png` — первые навигационные события;
|
||||||
|
- `0001.png` — продолжение навигации и начало F-клавиш;
|
||||||
|
- `0002.png` — полная базовая таблица до сочетаний;
|
||||||
|
- `0003.png` — сочетания, Esc и общий `RESULT: PASS`;
|
||||||
|
- `0004.png` — возврат в главное меню.
|
||||||
|
|
||||||
|
Интерпретация результатов и ссылки на основные кадры находятся в
|
||||||
|
`docs/p1-platform-probes.md` и `docs/commander-keymap.md`.
|
||||||
|
Актуальная интерпретация палитры находится в
|
||||||
|
`docs/p2-palette-results.md`.
|
||||||
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 8.7 KiB |