Compare commits

...

9 Commits

Author SHA1 Message Date
snark13 8e389c03f8 Volkov: добавить Sprinter Commander
Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места.

Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
2026-09-10 10:45:30 +03:00
snark13 05bcd8197e check_bank_calls: ловить адрес ПОЛЯ структуры в аргументах банкового вызова
Проверка 2 искала форму «адрес метки своего банка положен в аргументы вызова в
чужой банк», но регулярка принимала только `ld hl,#_метка` и `#_метка+6`. Адрес
поля структуры SDCC пишет в скобках:

	ld	bc, #(_mstream + 6)
	push	bc
	...
	call	___sdcc_bcall_ehl

Из-за этого целый класс ошибок собирался «чисто». Поймано вживую 2026-09-03 в
порту Loom: mask.c (банк 5, --bank-data 5) отдавал res_copy (банк 11) адрес поля
своей структуры — поток маски читался в чужую страницу, маска выходила «всё
заполнено», тайлов в комнате стало 30 вместо 17. Фон при этом оставался
побайтово верным, потому что его путь идёт из резидентного кода, так что
попиксельная сверка экрана молчала.

Класс проверки не меняется: адрес метки ДАННЫХ своего банка, положенный в стек
как аргумент вызова в ЧУЖОЙ банк, неверен всегда — это по-прежнему
доказательный случай, а не эвристика.

Проверено в трёх режимах: с ошибкой срабатывает, на исправленном коде молчит, и
на всех банковых сборках самого тулчейна (banktest, w3probe, banked, bankedbg,
banklocl, w3bankgfx, roomtest, сборка SprPoP) ложных срабатываний нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qxu9rwsmyDyYPxG198nzSR
2026-09-03 11:04:27 +03:00
snark13 25abf8ea36 SprPoP: в CHANGELOG к версии и дате добавлен коммит тега
Заголовок записи стал `<имя тега> — <дата> — <коммит>`.

Зачем коммит, если есть имя тега: тег можно передвинуть или
переименовать, а ревизия, на которой релиз собран, должна оставаться в
записи однозначно — по ней билд воспроизводится точно.  Заодно это тот же
хеш, что зашит в EXE как BUILD_ID и виден на экране About, так что
принесённый пользователем скриншот сразу сопоставляется с записью
changelog.

Берётся именно `*objectname` (дереференс аннотированного тега), а не
`objectname`: последний вернул бы хеш самого объекта тега, а не коммита.
Команда целиком выписана и в шапке CHANGELOG, и в правиле в CLAUDE.md,
чтобы не восстанавливать её каждый раз.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
2026-09-02 18:33:05 +03:00
snark13 f206a4cca6 SprPoP: завести CHANGELOG, версия и дата — из git-тега
До сих пор «что изменилось» восстанавливалось только из git log, а он
устроен по ПРИЧИНАМ правок, а не по тому, что заметит игрок.  Заводим
отдельный файл — как это уже сделано у mdview2 (changelog в репозитории
ведётся на приложение, а корневой RELEASE_NOTES.md описывает тулчейн и
живёт своей жизнью).

Начат с v0.9.6: зацеп, зацикленный взмах клинка, кнопка в паласе.  Более
ранняя история остаётся в git log и досках — переписывать её задним
числом смысла нет.

ВЕРСИЯ И ДАТА БЕРУТСЯ ИЗ GIT, а не проставляются руками: заголовок записи
= имя аннотированного тега, дата = дата его создания
(`git for-each-ref refs/tags/<тег>`).  Так запись не может разойтись с
историей.  Правило записано в CLAUDE.md рядом с дисциплиной досок, иначе
файл заведётся один раз и умрёт.

В запись идёт только то, что видно ИГРОКУ или меняет сборку/запуск, по
схеме «симптом -> причина одной фразой -> хеш»; полный разбор остаётся в
сообщении коммита и не дублируется.

Файл в UTF-8, как остальные доки SprPoP.  CP866 у mdview2 — вынужденная
мера (его changelog читает сам MDView), здесь такого требования нет.

Ссылка добавлена во входные точки docs/README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
2026-09-02 18:32:11 +03:00
snark13 aaa480d0f2 SprPoP: в CLAUDE.md — игра на образе лежит в D:\GAMES\SPRPOP
С обобщения HDD-сборки (ea8efdb) один образ рассчитан на несколько
приложений, и EXE переехал в подкаталог.  В инструкции по запуску это не
отразили, поэтому старая последовательность (`d:` + `sprpop`) отвечает
`Bad command or file name`, а `dir D:\` показывает только каталог GAMES —
на это можно потратить время впустую.

Записаны и сам путь, и исправленная цепочка клавиш для моста.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
2026-09-02 17:41:40 +03:00
snark13 8548aa9132 SprPoP: кнопка под Кидом больше не заливается кладкой (палас)
Симптом (пользователь, 2026-09-02, уровень 4 комната 18): Кид
подтягивается на открывающую кнопку (0,4), и кромка, которая его
прячет, нарисована кирпичом вместо кнопки.

РАЗБОР ПО ЖИВОМУ КАДРУ (заморозка): Kid frame 139, x 123, col 4, row 1,
room 18 — то есть персонаж действительно под кромкой кнопки (0,4).
Отсюда путь: кадр подъёма -> climb_overlay_tile; тайл не floor-подобный
(кнопка) -> other_overlay_tile; сосед слева (0,3) пуст -> overlay_mid_tile,
полная перерисовка тайла ПОВЕРХ персонажа.

ПРИЧИНА.  draw_tile_base оригинала подменяет базу кнопки на «левую
половину без пола слева» (id 148) по ТРЁМ условиям (seg008:628): тайл —
opener, слева пусто И tbl_level_type[current_level] == 0, то есть только
в ПОДЗЕМЕЛЬЕ.  Третье условие у нас было потеряно ИМЕННО В ОВЕРЛЕЕ:
статическая отрисовка (pop_room.c, draw_tile_base) его имеет и даже
ссылается на ту же строку оригинала, а pop_bg.c — нет.  Уровень 4
дворцовый, поэтому оверлей брал спрайт 148, а в дворцовом наборе это
другая картинка.

Этим же объясняется, почему баг не виден «просто так»: пока тайл рисует
статика, кнопка правильная — кирпич появляется ровно в тот момент, когда
персонаж встаёт под кромку и включается оверлей.

Цена: +6 байт в банке 2 (8976 -> 8982, свободно 7402).

Проверять стоит на любом ДВОРЦОВОМ уровне, где у открывающей кнопки
слева пусто и персонаж лезет на неё снизу.  Живая проверка в MAME
пользователем: корректно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
2026-09-02 17:41:29 +03:00
snark13 400f5cba63 SprPoP: поправка в CLIMB-VS-GUARD — звук 11 значит ПРОМАХ
В записи стояло, будто оригинал играет звук 11 «сразу на кадре укола, ДО
проверки расстояния», то есть при ЛЮБОМ уколе, попал тот или нет.  Это
было описание НАШЕГО кода, а не SDLPoP: ровно так звук у нас и стоял —
внутри ветки «не парировано» и до присвоения Opp.action = 99_hurt (снято
предыдущим коммитом).  Классическое нарушение правила «источник истины —
SDLPoP»: за оригинал приняли собственную реализацию, и вывод из неё уехал
в доску как факт.

На деле звук 11 в оригинале защищён условием Opp.action != 99_hurt, то
есть звучит ТОЛЬКО на промахе: на попадании играет боль, на парировании
Opp.frame уже 161.

Что это меняет для самого бага:

- УЦЕЛЕЛО наблюдение «в оригинале 11, у нас 8» — это данные.  Арифметика
  приоритетов их подкрепляет: snd_prio[11] = 0x12 против snd_prio[8] =
  0x4B (меньше значит важнее), взмах не мог быть заглушён упором в стену.
- ОТПАЛ вывод «оригинал ведёт стража атакой с промахом, а мы —
  столкновением»: он опирался на неверную посылку.
- УСИЛИЛАСЬ версия про ПОРОГ ДИСТАНЦИИ: раз в оригинале слышен именно 11,
  укол у стража СОСТОЯЛСЯ и ПРОМАЗАЛ.  Прежняя «версия про столкновение»
  вытеснила дистанцию зря — возвращаем её в главные подозреваемые.

Отдельно предупреждение тому, кто вернётся к багу: диагностика по звуку
теперь значит другое, старые заметки прогона будут вводить в заблуждение,
сцену надо переснимать.  Положена таблица соответствий (11 = промах, 13 =
попадание, 8 = столкновение, тишина = ранний выход из check_hurting).

На механику урона правки не влияют — расхождение «удар убивает» остаётся
открытым как было.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
2026-09-02 17:41:07 +03:00
snark13 cb995bf0fc SprPoP: непрерывный взмах клинка у безоружного Кида
Симптом (пользователь, 2026-09-02): на подъёме Кида шёл нескончаемый
свист клинка, хотя меча у него нет.

ОПОЗНАНИЕ.  Звук снят с живой машины, а не угадан: snd_curr == 11
(sound_11_sword_moving).  Заодно проверено, что тракт исправен —
указатели насоса (pg 2, ptr 0x2B80, left 512) сошлись с записью 11 в
SND/snd.idx (страница 2, off 0x2880, длина 1280, конец 0x2D80) байт в
байт.  То есть играл честно заявленный эффект из своих данных, и виновата
была ЗАЯВКА, а не звук.

ПРИЧИНА.  Хвост check_hurting был портирован не до конца.  В оригинале
(seg002:1039..1044) звук 11 стоит В КОНЦЕ функции, после ОБЕИХ веток, и
защищён тремя вещами: ранним выходом по dir_56_none, кадром 154 и
условием Opp.action != actions_99_hurt.  У нас он стоял ВНУТРИ ветки «не
парировано», до присвоения Opp.action = 99_hurt, без проверки на
попадание и — главное — без гарда по dir_56_none, который автор SDLPoP
подписал прямым текстом: «Fix looping sword moving sound».  Направление
dir_56_none означает «персонаж выключен» (clear_char), махать ему нечем.

Наш движок эту константу знает и применяет в pop_guard_tick — в
check_hurting она просто не доехала.

ТОНКОСТЬ ПОРТА: хвост оригинала ПЕРЕЧИТЫВАЕТ Char.frame и Opp.frame у
персонажей, а не берёт снимки начала функции.  Ветка парирования только
что записала Opp.frame, а play_seq в ней — Char.frame; на кэшированных
cf/of условие дало бы неверный ответ.

Живая проверка в MAME пользователем: корректно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
2026-09-02 17:40:45 +03:00
snark13 38fb3c03bb SprPoP: зацеп больше не срывается на кадре после захвата
Регресс от 4e12aa5: glide_through_wall_guard() звался в do_fall сразу
после check_grab и отменял ТОЛЬКО ЧТО СОСТОЯВШИЙСЯ зацеп.

Причина в имени последовательности: seq_15 — это grab_ledge_MIDAIR, и
после удачного захвата Char.action == 3, то есть персонаж формально всё
ещё «в воздухе», уже подтянутый вплотную к кромке.  А под кромкой в
комнатах оригинала стоит кладка.  Guard видел ровно её (замерено на живой
сцене: t == TILE_WALL, d == 10, col 0, row 1), считал это пролётом сквозь
стену, отбрасывал персонажа на 5 пикселей назад и гасил fall_x.  Зацеп
РИСОВАЛСЯ и тут же срывался — ловилось на длинном прыжке уровня 3
(комната 7) и в attract-демо (комната 2, прыжок с места на кромку 0,2,
после срыва Кид падал на пики).

ФИКС.  check_grab() возвращает признак «зацепился», и при нём guard не
зовётся.  Цена — один тест байта на кадр падения; фикс «падение сквозь
стену» цел, t_wall зелёный.

ПОЧЕМУ ПРЕЖНИЕ НАБОРЫ ЭТОГО НЕ ПОЙМАЛИ — и главный урок.  t_grab, t_phys
(1733 проверки) и t_wall судят по «действие стало вис», а вис-то
наступал, он просто не жил.  Первый A/B guard'а по этой же метрике дал
ЛОЖНО-ОТРИЦАТЕЛЬНЫЙ ответ, и подозрение с него было снято зря.  Разница
между «зацепился» и «зацепился и держится» — это и есть разница между
багом и нормой.

Отсюда новый набор t_hang: критерий — вис ДЕРЖИТСЯ три кадра подряд, и в
отчёте различаются «не наступило» и «наступило и сорвалось».  Окно — 34
фазы разбега из 41; при намеренно возвращённой поломке 0 из 41, тест
краснеет (проверено).  Сцена — геометрия уровня 3 комнаты 7, приведённая
внутрь одной комнаты, чтобы шов не примешивался; кромка-пол и
кромка-решётка проверяются отдельно.

Живая проверка в MAME пользователем: корректно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
2026-09-02 17:40:23 +03:00
880 changed files with 21618 additions and 64 deletions
+56
View File
@@ -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`
+25 -1
View File
@@ -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`**,
начинать чтение оттуда.
@@ -133,9 +151,15 @@ Makefile склеивает архивы прямо в `assets/packed/<КАТА
(memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
Пересобрал образ → MAME ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
**Игра лежит на образе в `D:\GAMES\SPRPOP\`**, а НЕ в корне диска (так с
обобщения HDD-сборки, коммит ea8efdb — один образ рассчитан на несколько
приложений). `dir D:\` показывает только каталог `GAMES`; запуск из корня
отвечает `Bad command or file name`.
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
старт `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** — заморозить
кадр, **2** — продолжить (разбор позы/окклюзии); **ESC** — выход. Читы
+54 -14
View File
@@ -38,7 +38,7 @@
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
| [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) |
---
@@ -1374,23 +1374,63 @@ uint16_t pop_music_left(void) { uint16_t a, b;
В той же сцене оригинал играет ВЗМАХ (звук 11, «клинок движется»), а у нас
слышен звук, похожий на упор Кида в стену (звук 8, `bumped`).
Почему это важно. Звук 11 оригинал играет в `check_hurting` СРАЗУ на
кадре укола — ДО проверки расстояния и до отметки «ранен». То есть он
звучит при ЛЮБОМ уколе, попал тот или нет. Значит его отсутствие
означает не «промахнулись», а «до укола дело вообще не дошло»: страж не в
кадре укола, либо разбор вышел раньше (меч не вынут / ряды не совпали).
А звучащий вместо него упор в стену говорит, что у нас сработало
СТОЛКНОВЕНИЕ, а не атака.
Набор звуков проверен и НЕ виноват: в `assets/packed/SND/snd.idx` слот 11
на месте и содержит собственный короткий сэмпл (1280 Б), слот 8 — другой
(1664 Б). Раскладка не сдвинута.
Отсюда рабочая версия: в этой связке оригинал ведёт стража по ветке
«атака с промахом», а мы — по ветке «столкновение с персонажем»
(`bump_into_opponent` / `check_bumped`). Это же объясняет и провалившегося
сквозь пол стража: столкновение двигает его координату, а не атака.
Проверять надо ветку выбора действия стража, а не только дистанцию.
**ПОПРАВКА 2026-09-02 — прежнее толкование звука было ОШИБОЧНЫМ.**
Здесь стояло, будто оригинал играет звук 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) — поведение в
этой связке расходится широко, чинить нужно целиком, а не по одному
+1
View File
@@ -10,6 +10,7 @@
|----------|-------|
| [`TASKS_OPEN.md`](TASKS_OPEN.md) | **Что берётся в работу сейчас** — начинать отсюда |
| [`BUGS_OPEN.md`](BUGS_OPEN.md) | Открытые баги и незакрытые оптимизации |
| [`../CHANGELOG.md`](../CHANGELOG.md) | **Что менялось по релизам** глазами игрока; версия и дата берутся из git-тега |
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где сделано не дословно и почему. Новое расхождение — записью сюда, а не только комментарием в коде |
| [`keys.txt`](keys.txt) | **Целевая раскладка управления**, к которой подгоняем SprPoP |
| [`keys_plan.md`](keys_plan.md) | План перевода на неё: что совпадает, что переназначить, каких функций нет вовсе |
+23 -2
View File
@@ -1102,8 +1102,6 @@ static void check_hurting(void)
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
/* Соперник НЕ парирует. */
if (cf == FRAME_154_POKING) {
/* seg002:0DAE — свист клинка мимо цели. */
pop_sfx_play(11);
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
distance = pop_char_opp_dist();
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);
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): прогнать проверку с ОБЕИХ сторон. */
+14 -3
View File
@@ -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);
{ /* draw_tile_base (seg008:0A8E) — ЦЕЛИКОМ, вместе с подстановками id:
* у 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-плиты в оверлей не попадал: передняя грань ложилась поверх
* Kid (foretable), а сама плита — нет, и подтягивающийся Kid рисовался
* поверх её верхней плоскости (комната 12). */
uint8_t base_id = t->base_id;
if (code == 11 && row >= 0)
base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])];
else if (code == 0x0F && lcode == 0)
base_id = 148;
else if (code == 0x0F && lcode == 0 && !pop_palace)
base_id = 148; /* ТОЛЬКО ПОДЗЕМЕЛЬЕ (seg008:628) */
if (base_id)
pop_env_b(base_id, x, t->base_y + dmy);
}
+30 -17
View File
@@ -839,30 +839,35 @@ static uint8_t can_grab_front_above(void)
/* check_grab (seg006:0A28): в падении при зажатом Shift — зацепиться за
* уступ спереди-сверху (seq_15), если скорость падения ещё мала и высота
* подходящая. Выравнивает передний край по кромке, гасит fall_y, взводит
* grab_timer (блок climb-up на несколько кадров). */
static void check_grab(void)
* grab_timer (блок climb-up на несколько кадров).
*
* ВОЗВРАТ: 1 — зацеп СОСТОЯЛСЯ на этом кадре. Нужен вызывающему (do_fall):
* дальше по кадру идёт glide_through_wall_guard, и ему нельзя трогать
* персонажа, которого мы только что повесили на кромку (см. там же). */
static uint8_t check_grab(void)
{
uint8_t old_x;
if (!pop_ctrl_shift_held()) return; /* Shift не зажат */
if ((uint8_t)Char.fall_y >= 32) return; /* падает слишком быстро */
if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return;
if (!pop_ctrl_shift_held()) return 0; /* Shift не зажат */
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 0;
old_x = Char.x;
Char.x = (uint8_t)char_dx_forward(-8);
determine_col();
if (!can_grab_front_above()) {
Char.x = old_x; /* не за что — назад */
determine_col();
} else {
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.fall_y = 0;
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
play_seq();
determine_col();
grab_timer = 12;
pop_sfx_play(9); /* seg006 check_grab */
is_screaming = 0; /* seg006:1219 */
return 0;
}
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.fall_y = 0;
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
play_seq();
determine_col();
grab_timer = 12;
pop_sfx_play(9); /* seg006 check_grab */
is_screaming = 0; /* seg006:1219 */
return 1;
}
static void make_loose_fall(int pos, uint8_t modifier);
@@ -972,8 +977,16 @@ static void do_fall(void)
}
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
check_grab(); /* ещё летит — попытка зацепа */
glide_through_wall_guard(); /* и не сквозь кладку (см. выше) */
/* ЗАЦЕП ОТМЕНЯЕТ 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(); /* и не сквозь кладку (см. выше) */
} else if (Char.curr_row <= 2) {
if (get_tile_at_char() == TILE_WALL)
in_wall();
+2
View File
@@ -49,6 +49,8 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
OBJS_grab := $(OBJS_phys)
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
OBJS_wall := $(OBJS_phys)
# t_hang — зацеп обязан ДЕРЖАТЬСЯ (регресс glide_through_wall_guard).
OBJS_hang := $(OBJS_phys)
# t_gate — открытая решётка не должна считаться стеной у осторожного шага
# (нужны pop_map + физика, то есть тот же состав).
OBJS_gate := $(OBJS_phys)
+95
View File
@@ -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;
}
+8
View File
@@ -0,0 +1,8 @@
# Локальные исполняемые файлы платформенных проб собираются рядом с
# исходниками. Общий шаблон репозитория не покрывает этот дополнительный
# уровень вложенности applications/Volkov/tests/<probe>/.
/tests/*/*.exe
# Клон исторических исходников VC служит локальным справочным материалом и
# сохраняет собственный .git; во внешний репозиторий его как gitlink не кладём.
/VolkovCommander/
+222
View File
@@ -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
+112
View File
@@ -0,0 +1,112 @@
# Sprinter Commander
Функциональный аналог классического двухпанельного Commander для Sprinter
Sp2000. Volkov Commander 4.05/4.99 и Dos Navigator используются как
поведенческие и архитектурные референсы; проект не является переносом их
DOS-кода.
Текущее состояние: PoC 0.1 (этапы P1P5) пройден в 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/` — навигация, F1F10, 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`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Some files were not shown because too many files have changed in this diff Show More