36 Commits

Author SHA1 Message Date
snark13 2f3e854854 PoP roomtest: отрисовка стража — в собственный банк (банк 2 упёрся в потолок)
Банк 2 (pop_bg + pop_gdraw) подошёл к границе страницы вплотную: 16 021 из
16 384, свободно 363 байта.  А расти ему ещё есть куда — тайлы поздних
уровней, чомперы, зеркало, анимации смерти стража.

pop_gdraw.c уехал в банк 4 (n_banks 3 -> 4):
  банк 2  16 021 -> 13 792  (84.2 %, свободно 2 592)
  банк 4              2 236  (13.6 %, свободно 14 148)

Цена: pop_fore_over_char стал кроссбанковым, поэтому помечен __banked —
один трамплин (~654 такта) за кадр, других вызывающих у него нет.
pop_fore_set_clip уже был __banked, так что там ничего не изменилось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:22:27 +03:00
snark13 75a51fb1db PoP roomtest: выпивание зелья (уровень 1 — склянка здоровья)
Каркас предметов уже был (check_get_item/do_pickup/proc_get_object), пустой
оставалась только ветка зелий.  Порт seg005 get_item + seg006
proc_get_object:

- pop_get_item_action теперь отдаёт 3 = «пить» и делает do_pickup с ТИПОМ
  зелья, который лежит в старших битах модификатора тайла (modif >> 3);
- pop_ctrl на код 3 запускает seq_78_drink;
- эффекты: тип 1 (здоровье) — +1 HP через hitp_delta и красная вспышка,
  причём как в оригинале только если HP не полные; тип 5 («злое») — −1 HP.
  Типы 2/3/4/6 (жизнь, перо, переворот, открыть ворота) — свойства поздних
  уровней, портируем вместе с ними.

Вспышка фона получила цвет: меч даёт ярко-жёлтую (было), зелье — красную
(flash_color оригинала; двух значений достаточно, других в игре нет).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:09:36 +03:00
snark13 a32b66700f toolchain: make_hdd.sh — закрывать mtools ОБА канала запроса, не один
Прошлая правка убрала только управляющий терминал (os.setsid), и mmd
переключился на stdin: lsof показал fd 0 = /dev/ttys002, процесс снова спал,
теперь уже после отметки «копирование файлов».

Каналов, откуда mtools может ждать ответ, два — /dev/tty и stdin — и
закрывать надо оба.  Обёртка mt() теперь и создаёт новую сессию, и подаёт
stdin из /dev/null.

Проверено: с stdin=/dev/null mmd на свежем образе отрабатывает с кодом 0, а
на уже существующем каталоге честно возвращает 1 и ничего не спрашивает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:01:39 +03:00
snark13 59e51f7e83 toolchain: make_hdd.sh не виснет при запуске из терминала
Симптом: сборка образа молча вставала навсегда сразу после эхо-строки
команды.  Появилось не «само» — ровно тогда, когда образ разложили по
подкаталогам (BG/KID/GUARD/LEVELS) и в скрипте появился mmd.

Диагноз по артефакту, а не по догадке: зависший процесс — `mmd z:/BG`,
и lsof показал fd 0 = /dev/null, fd 4 = /dev/tty.  То есть mtools (собран
с enable-raw-term) для интерактивного вопроса открывает УПРАВЛЯЮЩИЙ
ТЕРМИНАЛ напрямую, в обход stdin — поэтому ни `< /dev/null`, ни
перенаправления stdio не помогают.  А `2>/dev/null` на mmd прятал сам
вопрос, из-за чего это выглядело как зависание на пустом месте.
Из НЕинтерактивного запуска (CI, фоновая задача) терминала нет, вопрос не
задаётся, и баг не воспроизводится — потому и жил незамеченным.

Лечение: все вызовы mtools идут через обёртку mt(), которая запускает их в
НОВОЙ СЕССИИ (os.setsid + exec питоном; setsid(1) в macOS нет).  Без
управляющего терминала открывать /dev/tty нечего, и mtools выбирает
неинтерактивный путь.

Заодно добавлены отметки этапов («разметка и формат», «копирование
файлов», «конвертация RAW -> CHD»): если что-то встанет снова, будет сразу
видно где, а не после последнего аргумента команды.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 21:57:39 +03:00
snark13 b22cee3456 PoP roomtest: убитый страж остаётся мёртвым; меч только по подбору или читу S
Смерть стража теперь персистентна между входами в комнату — по механизму
оригинала, а не отдельной таблицей «убит/не убит».  В SDLPoP массивы
level.guards_* лежат в ОЗУ и движок их ПЕРЕПИСЫВАЕТ: leave_guard (seg002:02F5)
кладёт туда позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq;
enter_guard, увидев непустой seq_hi, поднимает стража прямо в этой
последовательности и по кадру смерти (185/177/178) ставит alive = 1.

У нас уровень лежит в EMM-странице только на чтение, поэтому в W2 добавлена
живая копия — 6 байт на комнату (tile/dir/x/skill/seq_lo/seq_hi):
- pop_guard_leave() в начале enter_room запоминает уходящего стража;
- pop_guard_enter поднимает труп сохранённой последовательностью И
  сохранённой X (pos_guards пересчитывает её из колонки только при загрузке
  уровня, дальше ею владеет leave_guard — иначе тело прыгает в центр тайла).

ГРАБЛИ: guards_seq_lo/hi в ФАЙЛЕ уровня не используются, там 0xFF во всех
комнатах (оригинал чистит их в reset_level_unused_fields).  Прочитав их как
есть, я скормил интерпретатору curr_seq = 0xFFFF, и приложение зависало —
бордюр оставался синим, цикл не доходил до vsync.  Живая копия стартует
нулями: 0 = «поднимать стандартной стойкой».

Меч Киду больше не выдаётся автоматически: DEBUG_SWORD_ROOM убран, вместо
него чит S (выдать меч).  Штатный путь — подобрать с пола.

Проверено в MAME: чит K убивает стража, уход из комнаты 3 и возврат —
тело на месте, страж не воскресает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 21:33:21 +03:00
snark13 2e90eaf7d7 PoP roomtest: окно Char больше не затирает правки pop_map (спуск с уступа)
Регрессия от окна Char вокруг control() (cf06896): диспетчер работает с
копией Char, а часть его действий у нас исполняет pop_map (pop_down_action,
pop_jump_up_seq, safe_step, зацеп) — и пишет ПРЯМО в Kid, потому что на Char
он ещё не переведён.  Завершающее `Kid = Char` затирало эти правки:
выравнивание x и ряд терялись, и спуск с уступа через вис не срабатывал —
Кид просто приседал.

pop_savekid_state теперь копирует только то, что диспетчер реально меняет
у персонажа: curr_seq и sword.  Когда pop_map переведём на Char, вернётся
полное копирование — в комментарии это зафиксировано.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:51:49 +03:00
snark13 3983fa4513 PoP roomtest: HP-учёт, индикаторы HP, чит бессмертия; фикс кэша кадра
Боёвка (порт seg002/seg006):
- check_sword_hurting / check_hurting / check_sword_hurt / hurt_by_sword /
  take_hp через дельты; do_delta_hp сводит их раз в кадр;
- парирование (justblocked), refractimer после ранения стража;
- смерть по seq_71_dying — через неё же теперь работает чит K: страж
  действительно погибает, а не замирает на месте;
- парные окна Char/Opp: loadkid_and_opp / savekid_and_opp /
  saveshad_and_opp.

Индикаторы HP (порт draw_kid_hp / draw_guard_hp): Кид слева, страж справа.
Перерисовка ТОЛЬКО при изменении числа и тогда на ОБЕИХ страницах
дабл-буфера (счётчик hp_todo, иначе на второй странице осталось бы старое
значение и мерцало через кадр); pop_hp_invalidate при входе в комнату, где
фон перерисован целиком.

Чит I — бессмертие Кида (нашего изобретения, в оригинале его нет).
Перекрывает и путь «безоружного закалывают насмерть»: тот идёт мимо HP, и
без этого чит бесполезен ровно там, где нужен.

ДВА НАЙДЕННЫХ БАГА:
1. Полосу HP блитил row-major примитивом, а атласы Кида и стража хранятся
   COLUMN-major (ради бесплатного флипа) — марки выходили транспонированными.
   Теперь колоночный блит, стрелки как в оригинале.
2. Кэш кадра для ОТРИСОВКИ заполняли pop_savekid/pop_saveshad.  Любое окно
   Char БЕЗ play_seq — а это оба окна боёвки — записывало Киду кадр, который
   принадлежал СТРАЖУ, и kid_draw искал этот image в атласе Кида, рисуя
   произвольную позу.  Владельцем кэша стал load_frame: он один знает, чей
   кадр загружен (по Char.charid).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:43:39 +03:00
snark13 d014a3f577 PoP roomtest: ИИ стража — подход к Киду и боевые ветки диспетчера
Пункт 2 плана закрыт: страж не только замечает Кида, но и идёт к нему и
дерётся.  Порт по SDLPoP, диспетчер общий — ИИ выставляет те же control_*,
что и клавиатура игрока.

guards.c (банк 1), порт seg002:
- autocontrol_guard_active (737) + kid_in_sight (0A93) + kid_armed (0AC1)
  + kid_far (09CB);
- guard_advance / guard_block / guard_strike с таблицами вероятностей по
  12 градациям мастерства (seg002:26..38), бросок prob > prandom(255);
- move_2_backward / move_3_up / move_6_shift / move_down_back;
- таймеры justblocked / kid_sword_strike / guard_refrac убывают раз в кадр
  в autocontrol_opponent, как в оригинале.

pop_ctrl.c, порт seg005: control_with_sword (964), swordfight (0CDB),
sword_strike, parry, forward_with_sword, back_with_sword.  Ветвление у
Кида и у соперника разное — соперник блокирует только на кадре 152, Кид
ещё и по 153 (и тогда последовательность прокручивается сразу).

pop_guard.c: guard_skill из данных уровня (12 градаций, вне диапазона -> 3),
HP по get_guard_hp (extrastrength[skill] + tbl_guard_hp[уровень]),
собственный сид бросков pop_fight_seed — иначе перерисовка стены сбивала бы
решения стража.

char_opp_dist переехал из банка в pop_kid.c: он нужен по ОБЕ стороны
банковой границы — и ИИ, и диспетчеру боёвки.

Проверено в MAME (комната 3): страж проходит комнату, встаёт в дистанцию и
машет мечом, позы меняются.  Урона пока нет — HP-учёт и check_hurt
следующим шагом, без них бой не заканчивается.

Бюджет В БОЮ (175 кадров): 412 224 – 421 068 = 0.958–0.979 кадра.
В покое было 400 800.  Запас в худшем кадре ~9 000 — тесно, но в один
кадр укладываемся; оптимизация отложена сознательно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:24:53 +03:00
snark13 4f7d9c0596 PoP docs: запасные PRNG (LFSR/LCG, xorshift(7,9,8)) + оценка потолка выигрыша
Тексты обеих Z80-процедур, разбор их устройства и качества, почему НЕ берём
8-битный RND Apple II (вырожденные младшие биты — раскладка кладки читает
prandom(1), вышла бы шахматка), и главное — сколько это реально даст.

Потолок выигрыша 2 814 тактов за кадр (0.65 %): тело генератора уже не
основной расход, остаются обёртка pop_prandom, pop_rnd_fit и ABI вызова.
Поэтому первый шаг, если упрёмся, — слить приведение к диапазону в ту же
asm-процедуру (один call вместо трёх), и только потом менять генератор.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:10:35 +03:00
snark13 37fc572cc3 PoP roomtest: LCG оригинала на ассемблере — точность без потери скорости
Возврат к БИТ-В-БИТ генератору оригинала по умолчанию (POP_PRANDOM_EXACT=1):
по нему проще отлаживать и сверять картинку с эталоном.  Чтобы это не
стоило процента бюджета, сам шаг LCG переписан на Z80-ассемблере —
единственное место в порте, где это сделано, с явного разрешения.

Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
РАЗРЕЖЕННОЙ записи константы.  Вместо 12 сложений (по числу единиц в
0x343FD) — 17 удвоений, три сложения и одно вычитание; величина 3*s,
нужная в конце, попадается по дороге на втором шаге.

Проверка в ДВА этапа:
- схема на хосте: horner(s) == s*214013+2531011 на 3 000 000 сидов;
- сама asm-транскрипция на живой машине: breakpoint на pop_prandom,
  11 последовательных состояний сида из MAME — каждый переход совпал с
  s*214013+2531011 бит-в-бит.

Замер, комната 3, 175 кадров (медиана кадра / prandom->torch_draw):
  C, бит-в-бит (16-бит половины)   403 632 / 10 933
  C, xorshift16 + шаг Вейля        397 986 /  7 927
  asm, бит-в-бит                   400 800 /  9 331
То есть asm вернул половину разрыва (2 832 такта за кадр), сохранив
совместимость с эталоном.  Ветка xorshift оставлена под
-DPOP_PRANDOM_EXACT=0 как запасной ход — брать её имеет смысл, только
если не хватит последних 2 800 тактов.

Итог оптимизационного круга: 416 154 -> 400 800 (0.968 -> 0.932 кадра).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:05:13 +03:00
snark13 99b430f2ed PoP/libbgi: убрана 32-бит арифметика, noclip для column-major, быстрый PRNG
Ревью на 32-бит сделан ПО ASM, а не по коду (искали и безымянные
временные): во всём приложении был ровно ОДИН 32-битный вызов —
__mullong в pop_prandom.  Замер в MAME: 8 430 тактов на вызов, два
вызова за кадр.  Прочие библиотечные вызовы 16-битные (__divsint 16,
__modsint 12, __moduchar 8, __divuchar 5).

1. pop_prandom.  Состояние 32-бит -> две 16-битные половины.  Два
   генератора, выбор через POP_PRANDOM_EXACT:
   - 0 (по умолчанию) — xorshift16 + шаг Вейля, без единого умножения;
   - 1 — LCG оригинала бит-в-бит, посчитанный половинами (для сверки
     картинки с эталоном).
   8-битный RND Apple II (5*x+23 mod 256) НЕ взят: у LCG по модулю 256
   вырождены младшие биты (бит 0 просто чередуется), а раскладка кладки
   берёт как раз prandom(1) и prandom(4) — вместо шума вышла бы
   правильная шахматка.  Шаг Вейля ещё и убирает ноль как неподвижную
   точку xorshift (сид кладки вполне может быть нулём).
   Бит-в-бит эквивалентность half-word версии проверена на хосте:
   70 000 сидов x 8 шагов + 7 крайних сидов x 2000 шагов.
   Остаток 0..maxv: делитель степень двойки — маска вместо __moduint.

2. libbgi: gfx_blit_cols_part_noclip — column-major блит без клипа
   (пара к gfx_blit_cols_part, как gfx_blit_part_noclip к
   gfx_blit_part).  Клипающий вариант платит ~5 622 такта подготовки на
   КАЖДЫЙ вызов независимо от того, вылезает край (замер: подготовка
   5 622 против 13 596 на сам accel-проход).  Kid, страж и клинок
   выбирают путь по pop_onscreen_cols.  size-check: роста нет.

Бюджет (175 кадров, комната 3, медиана):
  было (после клинка)   416 154   0.968 кадра
  стало                 397 986   0.926 кадра
Разница между генераторами, замер на одинаковой сборке:
  xorshift16 + Вейль    397 986   prandom->torch_draw  7 927
  бит-в-бит LCG         403 632   prandom->torch_draw 10 933
то есть точность обходится в 5 646 тактов за кадр (1.3 %).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:48:37 +03:00
snark13 d438a1d3da PoP roomtest: клинок в атласе целиком + раздельный heal накладных спрайтов
Меч в оригинале ОДИН на всех: и Кид, и страж рисуют клинок из chtab_0
(add_sword_to_objtable, seg006:1798).  Раньше sword.atl содержал только
кадры подъёма/ножен (sword_tbl 35..42), поэтому у стража меча не было
видно вовсе.

- pop_pack_kid.py пакует chtab_0 целиком (id 0..33, 5168 Б), индекс в
  атласе = id;
- pop_extract_kid_data.py вытаскивает sword_tbl (53 строки) в kid_data.h
  макро-инициализаторами — таблица ложится в один TU, а не в каждый;
- pop_sword_draw (pop_kid.c) — общая точка отрисовки клинка с полным
  условием оригинала (кадры 229..237 ИЛИ меч обнажён ИЛИ живой страж);
  зовут и kid_draw, и pop_guard_draw.

Раздельный heal накладных спрайтов.  Клинок и брызги урона раньше
объединялись в один прямоугольник с персонажем, а объединение почти вдвое
больше суммы двух (клинок уходит вперёд-вверх) — heal же стоит ровно по
площади.  Теперь у накладных свой прямоугольник и свой gfx_heal, а
объединение осталось ТОЛЬКО для окна fore-клипа: там это 4 сравнения без
рисования, но покрыть клинок обязано, иначе он полезет поверх столба.

Отладка: DEBUG_SWORD_ROOM — Киду выдаётся меч при входе в комнату 3
(там страж), чтобы не бегать за ним в комнату 15.

Бюджет (225 кадров, комната 3): 415 284 – 416 370 = 0.966–0.968 кадра.
Против 384 168 – 384 636 до этого шага, то есть +31 700.  Разложение по
фазам (медианы): process_trobs 83 190, pop_guard_draw 92 483 (спрайт
28 763 + клинок 23 820 + fore_over_char 39 906), kid_draw 59 089, fore
поверх Кида + борта 47 483, heal 40 068, логика 76 338, ввод 13 050.
Видно, что клинок 21x8 стоит почти как спрайт стража — это фиксированные
накладные расходы клипающего блита, а не пиксели; лечится noclip-путём
для column-major (см. gfx_blit_noclip_fast).  Отдельным шагом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:40:08 +03:00
snark13 79ea473910 PoP roomtest: страж замечает Кида и достаёт меч (ИИ, шаг 1)
Первый самостоятельный кусок ИИ стража (план, пункт 2).  В оригинале у
соперника нет своего диспетчера: autocontrol_* (seg002) выставляет те же
глобалы control_*, что и ввод игрока, а дальше исполняется общий control().
Инфраструктура под это встала прошлым коммитом, здесь — сама логика.

Порт:
- check_can_guard_see_kid (seg003:688) — луч видимости по ряду: стены и
  верхи дверей рвут его совсем, loose/чомпер/дыра/неподнятые ворота дают
  «вижу, но не пойду».  В guards.c (банк 1);
- Opp + loadshad_and_opp (seg006:841) и char_opp_dist (seg006:2135);
- autocontrol_guard_inactive (seg002:710) + move_* (seg002:0706..);
- ветки control(): control_guard_inactive (seg006:2123) и draw_sword
  (seg005:945) — соперник уходит сразу в seq_90 en garde;
- pop_guard_tick перестроен по play_guard_frame (seg000:1246): окно
  Char/Opp вокруг ИИ, диспетчера и play_seq.

По дороге:
- Kid.alive не выставлялся (= 0 = «мёртв» в семантике оригинала), из-за
  чего луч видимости не мог сработать в принципе — ставим -1 в kid_init;
- Kid.room не выставлялась вовсе; условие Kid.room == Guard.room всегда
  было ложным.  Ставим в enter_room (полная модель Kid.room != drawn_room
  у шва по-прежнему впереди);
- pop_tile_at — тайл текущей комнаты наружу из pop_map (луч видимости);
- control_x/y/shift открыты в шину: ИИ заполняет оси как есть, без
  flip_control_x (его «вперёд» уже в системе персонажа).

Проверено в MAME (комната 3, страж на tile 17): страж переходит из
стойки 166 в 171 stand_with_sword, sword=2, can_guard_see_kid=2; ввод
игрока не пострадал.  Клинок отдельным спрайтом пока не рисуется —
sword.atl содержит только кадры 229..237 (подъём меча Кидом), остальные
строки sword_tbl приедут с боёвкой.

Бюджет (225 кадров, комната 3): 384 168 – 384 636 тактов, 0.893–0.895
кадра, запас 45 364.  Прошлый замер 382 584 – 383 064 — шаг стоил ~1 570.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:09:12 +03:00
snark13 cf06896dbd PoP roomtest: окно Char вокруг control() + общая шина ввода — база под ИИ стража
Инфраструктура пункта 2.  В оригинале у соперника НЕТ своего диспетчера:
autocontrol_* (seg002) выставляет те же глобалы control_*, что и ввод
игрока, а дальше исполняется тот же control() (seg005:252).  Значит перед
портом ИИ надо было привести к этому обе половины:

- control_forward/backward/up/down/shift2 перестали быть static в
  pop_ctrl.c — это общая шина синтетического ввода, объявлена в pop_ctrl.h
  вместе с POP_CONTROL_*;
- control() переименован в pop_control() и работает с Char, а не с Kid;
- ввод игрока обёрнут в окно Char (loadkid/user_control/savekid), как в
  play_frame оригинала.

Ловушка по дороге (ввод отвалился целиком, Kid не двигался): макрос
seqtbl_offset_char вёл на kid_set_seq, который пишет прямо в Kid, а
следом savekid затирал Kid копией Char со старой curr_seq.  В оригинале
seqtbl_offset_char работает именно с Char — макрос переведён на
pop_char_set_seq.  kid_set_seq остался для вызовов ВНЕ окна (pop_map).

Добавлен pop_savekid_state (Kid = Char без кадра): control() кадр не
трогает, а cur_frame в этот момент принадлежит тому, кто последним крутил
play_seq.

Проверено в MAME: бег и упор в стену работают как прежде.
Бюджет (комната 3, 125 кадров): 382 584..383 064 против 380 292..381 282,
то есть +2 300 тактов на копии окна.  0.891 кадра, запас 46 936.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:32:15 +03:00
snark13 8bbc6b4d07 PoP roomtest: интерпретатор последовательностей стал общим (Char) — стражи
Пункт 1 плана стражей.  play_seq был прибит к Киду, поэтому страж стоял на
захардкоженном кадре 166.  Теперь как в оригинале: интерпретатор работает
с АКТИВНЫМ персонажем Char, а вокруг стоят loadkid/savekid и
loadshad/saveshad (порт seg006:809..825).

Почему копия, а не указатель: так в оригинале, и на Z80 это быстрее —
горячий цикл обращается к глобалу абсолютной адресацией, а 16-байтовое
копирование платится один раз на переключение персонажа, тогда как
указатель дал бы индексную адресацию в каждом обращении.

Сопутствующее:
- kid_t и pop_char_t слиты в один pop_char_t (pop_char.h): в оригинале
  char_type один на всех, и без этого общий интерпретатор невозможен.
  Kid получил поля room/charid/sword/alive — они и так нужны боёвке;
- load_frame выбирает таблицу кадров по Char.charid (у стража своя,
  frame_tbl_guard с индексом frame + add_frame − 149, seg006:0293);
- cur_frame разведён на два кэша: страж тикает ПОСЛЕ Кида, и без этого
  kid_draw брал бы кадр стража.  savekid/saveshad раскладывают кадр по
  своему персонажу;
- kid_set_seq пишет ИМЕННО Kid (его зовут pop_ctrl/pop_map вне окна Char,
  иначе loadkid затёр бы), для окна Char добавлен pop_char_set_seq —
  порт seqtbl_offset_char;
- в pop_map 9 голых play_seq() заменены на pop_kid_play() (load+play+save);
- страж входит в комнату через seq_77_guard_stand_inactive (seg002:0208),
  а не через прибитый кадр.

Проверено в MAME: Kid бегает и упирается в стену как прежде, в комнате 12
плиты проваливаются со щебнем (правка задела 9 вызовов play_seq в
физике), страж в комнате 3 рисуется в той же позе, но теперь
curr_seq=0x19A9 и charid=2 — кадр получен прокруткой последовательности,
а не константой.

Бюджет (комната 3, 150 кадров): 380 292..381 282 тактов против
370 140..371 160 до правки, то есть +10 150 (+2.7 %).  Основное — не
копии Char, а то, что страж теперь реально крутит интерпретатор каждый
кадр, а раньше стоял замороженным.  0.887 кадра, запас 48 718.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:11:10 +03:00
snark13 565ba98852 PoP docs: бэклог идей — начат с «отключать мышь на время игры»
Мышь игре не нужна (управление — raw-клавиатура, которую мы и так
забираем у DSS), а её прерывания воруют такты из бюджета, занятого на
86 %.  Эффект измерен побочно: при движении мыши на хосте кадры выбивались
до 1.5 кадрового периода, при неподвижной — 225 кадров без превышений.

Записано с тем, что проверить (есть ли в RST 30h выключение, сколько
стоит одно прерывание, восстановление состояния на выходе) и почему не
сейчас: выигрыш только когда игрок двигает мышью, риск оставить систему
без мыши после выхода — заметный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:58:40 +03:00
snark13 ac9871c58c PoP roomtest: факел анимируется каждый логический кадр (TORCH_ANIM_DIV)
Делитель (torch_tick & 1) занижал скорость пламени вдвое: один логический
кадр = 3 vsync и соответствует игровому тику оригинала, а animate_torch
(seg007:03C1) меняет кадр КАЖДЫЙ тик.  Теперь темп задаётся явной
константой TORCH_ANIM_DIV (1 = как в оригинале, 2 = прежнее поведение),
счётчик компилируется только когда он реально нужен.

Побочный эффект важнее визуального: раньше половина кадров делала работу
факелов, половина нет, и бюджет кадра «прыгал».  Замер по 100 кадрам
до правки: 349 008..371 262, разброс 22 254 такта (6.2 %).  После: по
225 кадрам 370 140..371 160, разброс 1020 тактов (0.27 %) — каждый кадр
стал худшим случаем, и цифре можно верить.

Бюджет сейчас (комната 3, Kid + страж, статика): 0.861..0.863 кадра,
запас 58 840 тактов до 430 000.  Кроссбанковых вызовов 19 за кадр
(~654 такта каждый = 12 400, 3.3 % кадра) — столько максимум вернёт
батчинг; профиль вызовов снят breakpoint'ом на ___sdcc_bcall_ehl с
печатью HL/E и раскладкой адресов по .map.

Замечание по методике: мерить надо ПО МНОЖЕСТВУ кадров.  Единичные
всплески до 1.5 кадра, которые я сперва принял за проблему движка,
оказались наводкой от прерываний мыши на хосте — при неподвижной мыши
225 кадров подряд без единого превышения.

Проверено, что --w3 (он остался в sprinter-cc) кладёт в W3 только код и
rodata: --dataseg ему не передаётся, глобал --w3 модуля лёг в общий
_DATA — сюрпризов при возврате к резиденту не будет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:57:26 +03:00
snark13 1e6c377edc PoP roomtest: pop_bg и pop_map в банки; --dataseg BANKn стал опцией
Резидента --w3 больше нет: отрисовка (pop_bg + pop_gdraw) уехала в БАНК 2,
физика/коллизия (pop_map) — в БАНК 3.  Куча W1/W2 1294 -> 6750 Б.

Что это разблокировало.  Резидент был тупиком: из банка он недостижим ни
прямо, ни транзитивно, поэтому pop_map (самый крупный модуль, 5.8 КБ) в
банк было не увести — он зовёт mob-отрисовку.  Проверено, что банк->банк
РАБОТАЕТ: ___sdcc_bcall_ehl читает страницу окна портом 0xE2 и кладёт её
на СТЕК своего кадра (runtime/bank.s), поэтому вложенность корректна по
построению.  Подтверждено в MAME цепочкой W1 -> банк1 -> банк2 -> банк1:
nested=124 after=8, ровно ожидаемое.  Значит развязка mob'а (самое
рисковое место, loose-полы) НЕ понадобилась — pop_map зовёт pop_bg
трамплином.

Правила вызовов проверены на сгенерированном asm и записаны в memory
sdcc_banked_call_rules: трамплин выбирает ОБЪЯВЛЕНИЕ (__banked), а не
раскладка — даже внутри одного .c между __banked функциями он есть.
Внутрибанковые функции оставлены непомеченными и зовутся напрямую, в т.ч.
через границу файла (pop_gdraw -> pop_fore_over_char).

sprinter-cc: --dataseg BANKn БОЛЬШЕ НЕ ставится по умолчанию.  Раньше вся
писучая память банкового модуля уезжала в страницу банка и снаружи не
читалась (проверено на .map: глобал лёг по 0x0001C000) — грабли на
каждом переносе.  Теперь данные банков по умолчанию в общем _DATA (W1/W2,
замаплен всегда), а прежнее поведение — по явному --bank-data.

Замеры (комната 3, Kid + страж; кадр Sprinter в турбо = 430 000 тактов):
  до переноса          338 508  (0.79 кадра)
  + pop_bg в банк 2    347 100  (+2.5 %)
  + pop_map в банк 3   371 100  (+9.6 % к исходному, 0.86 кадра)
Плата — трамплины (~654 такта на вызов, ~30 вызовов за кадр).  При
пейсинге в 3 кадра это 29 % логического кадра, но запас до ОДНОГО кадра
всего ~59 000 тактов — под звук его надо возвращать (следующий шаг:
батчить кроссбанковые вызовы, начиная с pop_redraw_needed).

Профилирование бордюром включено по умолчанию (make PROF=0 выключает) и
переведено на реально работающие биты: бит 0 (красный) у бордюра Sprinter
ИГНОРИРУЕТСЯ, поэтому различимых состояний четыре и значения обязаны быть
чётными — 0 чёрный (ждём vsync), 2 синий (логика), 4 зелёный (фон),
6 циан (спрайты).  Раньше нечётные номера сливались и полосы не читались.

Проверено в MAME: комнаты 1/3 рисуются как прежде, бег и коллизия
работают, в комнате 12 плиты проваливаются со щебнем — то есть цепочка
loose банк3 -> банк2 живая.  Полосы бордюра в комнате 3: логика 73
строки, фон 64, спрайты 128, свободно 23.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:31:58 +03:00
snark13 0280b05933 libc/kbd: held-карта клавиш в биты (512 -> 64 Б); эталон размеров принят
Разгрузка W1/W2 под будущий ИИ стражей: _kbdraw_down был БАЙТОМ на
скан-код (512 Б в _DATA при 32-килобайтной раскладке).  Теперь бит на
код: код>>3 = байт, код&7 = бит, расширенные (префикс 0xE0) — смещение
+32 байта вместо +256.

Трамплин прерывания строит маску СДВИГОМ, а не таблицей: таблица
потребовала бы `ld hl,#метка` внутри трамплина, а он копируется в W2
побайтно и обязан быть без абсолютных само-ссылок (см. его шапку).
Маска строится в BC, поэтому в клавиатурной ветке добавлен push/pop bc.
Трамплин вырос 244 -> 267 Б, буфер копии поднят 320 -> 336 (запас 69 Б).

Проверено в MAME на roomtest, все три класса клавиш:
  - обычные: '=' (обход комнат) и 'K' (чит-убийство стража — читал
    guardhp_curr/delta: 3/0 -> 0/-3);
  - расширенные (E0): стрелка вправо — Kid добежал до края комнаты;
  - модификаторы: удержание Shift ставит бит 2 байта 2 карты
    (скан-код 0x12), отпускание снимает.

Скорость: кадр 334 716 -> 338 508 тактов (+1.1 %) на битовой арифметике
в kbd_raw_down (~15 вызовов за кадр); при бюджете 430 000 это 0.79
периода вместо 0.78 — регрессии нет.

Итог по roomtest: данные 4422 -> 4022 Б, куча W2 996 -> 1294 Б.

Эталон размеров принят заново (make size-baseline): _CODE десяти
программ вырос на 14-23 Б — это код битовой арифметики в трамплине и
kbd_raw_down, обмен на -448 Б данных, которые size_check не считает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 00:50:37 +03:00
snark13 f2093e0d89 PoP roomtest: fore-слой — окно клипа, кэш кладки; кадр 2.5 -> 0.78 периода
Жалоба: стойка неподвижных Кида и стража занимала больше полутора
кадровых периодов.  Гипотеза «виноват __banked» ЗАМЕРОМ НЕ
ПОДТВЕРДИЛАСЬ: весь банковый вызов (трамплин + смена страницы + тело
pop_guard_tick + возврат) стоит 654 такта при бюджете кадра 430 000.

Как мерил (выборка PC бесполезна — мост MAME отвечает из фреймового
колбэка, все сэмплы падают в обработчик прерывания): breakpoint'ы MAME с
действием {printf totalcycles; g} на входах фаз главного цикла, разности
соседних меток = стоимость фазы.  Плюс профилирование полосами бордюра
(make PROF=1, макрос PROF() в roomtest.c) для быстрого взгляда.

Замер комнаты 3 (Kid + страж), такты, кадр = 430 000:
  fore поверх стража  431 964
  fore поверх Kid     402 816   -> 78 % всей работы кадра
  остальное           241 956
  ИТОГО             1 076 736   = 2.5 кадра

Две причины, обе устранены:

1. Fore-слой рисовал ЦЕЛЫЕ тайлы, хотя существует ровно для того, чтобы
   вернуть куски поверх спрайта — за его прямоугольником в видеопамяти и
   так правильный фон.  Введено ОКНО клипа (pop_fore_set_clip): спрайт
   сообщает свой итоговый габарит (у Kid — с клинком, брызгами и
   обрезкой clip_char), fore-проход режет по нему.  Отсев трёхступенчатый:
   тайл целиком (tile_in_fclip, до обращения к атласу), кусок по грубому
   габариту (до gfx_w0_map — w/h лежат в EMM-странице), и точный клип в
   blit_b.  Для последнего добавлен libbgi-примитив
   gfx_blit_part_noclip — пара к gfx_blit_noclip, но под-прямоугольник.

2. Оставшиеся 341 К после клипа оказались НЕ пикселями: 18 вызовов
   pop_prandom за проход, ~10 700 тактов каждый (32-битный LCG:
   __mullong ~8 000 + __moduint).  Раскладка кладки тайла — чистая
   функция (комната, ряд, колонка), то есть константа комнаты, а
   wall_pattern пересчитывал её каждый кадр.  Теперь кэшируются готовые
   РЕШЕНИЯ (что рисовать и с каким смещением), 3 байта на тайл, сброс в
   pop_room_draw.  Порядок вызовов prandom воспроизведён один в один,
   включая то, что значение метки берётся только при сработавшем условии.

Итог того же замера: fore поверх стража 37 464, поверх Kid 46 464,
кадр целиком 334 716 = 0.78 периода (было 2.5).  Ускорение 3.2x, сами
fore-проходы — 10x.

Проверка отсутствия регрессии: попиксельная разность скриншотов комнат
1/2/3 до и после — отличаются ТОЛЬКО языки пламени факелов (анимация),
кладка и метки совпадают байт в байт.

Побочно: sprinter-cc научился пробрасывать -DNAME в sdcc.

Память: куча W2 1245 -> 996 Б (кэш кладки 120 Б), резидент W3 12 819 ->
14 512 (свободно 1872 Б — становится тесно), банк 1 236/16384.

ВНИМАНИЕ: make size-check показывает рост 7 программ, но эталон
docs/size_baseline.tsv отстал (последний раз принят в 484b18d, libbgi
менялась в 95c22be/c127a4b/64ce633) — к этой правке рост отношения не
имеет: новый модуль библиотеки в чужие программы не линкуется.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 00:19:32 +03:00
snark13 af5f0a4638 PoP roomtest: отрисовка стража в резидент W3 + fore-окклюзия + off-by-one спрайта
Разгрузка W1/W2 перед ИИ стражей (вариант 2 из двух обсуждённых).

1. pop_guard.c разделён по окнам: состояние/логика (Guard, HP, enter,
   kill, load_frame) остаются в W1/W2 — их обязан видеть банк guards.c;
   ОТРИСОВКА уехала в новый pop_gdraw.c, собираемый как --w3 (резидент).
   Правило границы: резидент = только то, что рисует и зовётся
   исключительно из главного цикла.  Кадр стража стал глобальным
   (pop_gframe): заполняет логика, читает резидент.

2. Страж не окклюдировался передними гранями тайлов — рисовался поверх
   столба.  В оригинале любой Char это запись midtable, а foretable
   рисуется после всех midtable (draw_tile_fore, seg008:690), т.е. столб
   перекрывает всех одинаково.  Футпринт персонажа выделен из
   pop_fore_over_kid в char_footprint(), поверх него добавлен
   pop_fore_over_char() — слой fore + полоса потолка, без оверлеев поз
   виса/полёта/подъёма (у стража их нет; появятся — портируем
   redraw_at_char2 общим кодом, а не догадками).

3. Упаковщик стража: тот же off-by-one, что уже ловили у Kid.
   load_chtab_from_file(id_chtab_5_guard, 750) даёт images[0] = res751,
   а рисование индексирует images[frame.image] — значит image=N это
   res(751+N), а не res(750+N).  Из-за сдвига frame_166_stand_inactive
   рисовался как res767 (выпад) вместо res768 (стойка).

Проверено в MAME: страж в комнатах 3 и 21 стоит в правильной позе;
окклюзия подтверждена патчем Guard.x в живой сессии — при заходе за
столб спрайт корректно срезается его передней гранью.

Память: _CODE 26 149 -> 25 703, куча W2 805 -> 1245 Б, резидент W3
11 656 -> 12 819 (свободно 3565 Б), банк 1 236/16384 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 23:14:21 +03:00
snark13 3dbad6120c PoP roomtest: страж — спрайты, таблица кадров и появление в комнате
Шаги 1-2 из плана стражей:

1. Спрайты (chtab_5_guard, база 750).  Новый упаковщик pop_pack_guard.py:
   data/GUARD/res751..784 -> GUARD\g0..g4.atl (5 EMM-страниц, адресация
   id>>3 / id&7, как у Kid).  Палитра берётся НЕ из PNG, а из res10.bin
   (guard_palettes: 7 палитр по 16 цветов, 6-бит) по level.guards_color —
   на уровне 1 у обоих стражей color = 2; группа слотов 0x90..0x9F
   добавлена в общий kid.pal.

2. Таблица кадров стража у оригинала СВОЯ (frame_tbl_guard, seg006:372,
   41 запись) и индексируется как frame + add_frame - 149, где add_frame
   = 70 для кадров 102..106.  Она дописана в kid_data.bin (3515 -> 3720 Б,
   смещение в KID_BIN_GFRAMES_OFF); pop_kid получил pop_kid_data_frame()
   — чтение кадра из ЛЮБОЙ таблицы страницы данных.

3. Появление: pop_level_guard() читает guards_tile/dir/color/skill из
   уровня, pop_guard_enter() ставит стража по enter_guard (seg002:0112) +
   pos_guards (seg003): row из тайла, y = y_land[row+1], x из колонки,
   charid = guard, sword сложен, alive = -1, HP = 3.  Отрисовка
   pop_guard_draw() — та же математика, что kid_draw (load_frame_to_obj +
   calc_screen_x_coord), но атлас стража и своя таблица кадров; heal по
   странице дабл-буфера, как у Kid.

Интерпретатора последовательностей у стража ПОКА НЕТ: кадр ставится
напрямую (166 = frame_166_stand_inactive, что и даёт seq_77 при входе в
комнату).  play_seq для произвольного персонажа + ИИ — следующая фаза.

Проверено в MAME: в комнате 3 страж появляется на своём месте (ряд 1,
кол 7) и рисуется; цвета совпадают с эталонным рендером спрайта в
палитре color=2.

ВНИМАНИЕ по памяти: куча W2 просела до 805 Б (было 2349).  Перед ИИ
стражей нужен шаг 5 плана (данные: room_modif 720 Б, dl1/dl2 512 Б,
_kbdraw_down 512 Б) либо вынос кода отрисовки стража в резидент W3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:57:38 +03:00
snark13 1dc89b0f26 PoP roomtest: ресурсы по каталогам образа (BG/KID/LEVELS), цель make hdd
Все .atl лежали в корне диска рядом с exe — с атласами стражей корень
зарос бы окончательно.  Теперь ресурсы разложены по каталогам (8.3, как
принято в DSS):
  BG\     фон (env0..4, wall, fore, pot)
  KID\    персонаж (kid0..27, kid.pal, sword, kid_data.bin)
  GUARD\  стражи (появятся здесь)
  LEVELS\ уровни (res2001.bin)

make_hdd.sh принимает аргумент вида КАТАЛОГ:файл — создаёт каталог на
образе и кладёт файл туда; без префикса файл идёт в корень.  В Makefile
roomtest появилась цель `make hdd`, которая собирает образ с этой
раскладкой (раньше команда набиралась руками на 15 строк).

Проверено в MAME: DSS открывает пути вида KID\kid0.atl — комната
рисуется, Kid и факелы на месте, то есть все атласы, палитра, таблицы
анимации и уровень грузятся из подкаталогов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:42:16 +03:00
snark13 e1ee447b7f PoP roomtest: каркас стражей в БАНКЕ + режим читов (K — убить стража)
Раскладка (по подтверждённой пробником модели, tests/w3bankgfx):
- roomtest переведён на MEMORY=huge: та же small-раскладка резидента
  (CODE в W1, DATA за ним) плюс банки кода в W3;
- guards.c собирается как --bank 1=guards.c — там будет ИИ и боёвка;
- СОСТОЯНИЕ стража живёт в W1/W2 (pop_guard.c): писучие статики
  __banked-модуля линкуются в страницу банка и снаружи не читаются, так
  что банк — только код;
- поля pop_char_t повторяют char_type оригинала (types.h:302), чтобы порт
  seg005/seg006 ложился один в один.

Режим читов (порт cheats_enabled, seg000:111): глобальный флаг pop_cheats,
на время разработки включается в main.  Реализован один чит — K (kill
guard, seg000:786): скелета не берёт, живому стражу ставит
guardhp_delta = -guardhp_curr и alive = 0.  Обработка по фронту нажатия.
Остальные читы оригинала не портированы.

Проверено в MAME: приложение в huge-раскладке стартует, комната рисуется,
Kid бегает — то есть банкованный pop_guard_tick() зовётся каждый кадр
через трамплин и корректно возвращается; K не роняет приложение (стража
в комнате пока нет).  Банк занят на 6 Б из 16384.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:31:11 +03:00
snark13 18f5115e0d docs(PoP): план v2 — результат пробника банка (модель подтверждена)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:19:00 +03:00
snark13 b3bf2cca3d tests/w3bankgfx: пробник модели «huge + резидент W3 + банк W3 + графика»
Проверяет то, на чём стоит план раскладки PoP (layout_plan_v2.md §2):

R3  резидент/HOME -> __banked через трамплин работает;
R4  примитивы libbgi можно звать ИЗ БАНКА: _bgi_begin запоминает текущую
    страницу W3 (порт 0xE2), _bgi_end её возвращает — банк переживает
    рисование и продолжает исполняться;
    то же верно для функции W1/W2, вызванной из банка: она рисует, а в W3
    остаётся страница БАНКА, не резидента.

Результат в MAME: все пять полос на месте, вердикт ЗЕЛЁНЫЙ.  Замеры:
страница банка 0xF0 до рисования, после прямого блита и после возврата из
W1/W2-функции — та же 0xF0; резидент 0xF3; банк дожил до конца и вернул
корректное значение.

Два побочных вывода, важных для стражей:
1. Писучие статики __banked-модуля линкуются В СТРАНИЦУ БАНКА (адрес
   0x1C000+), снаружи их не прочитать — состояние банка держать в W1/W2.
2. Инлайновый `in a,(0xE2)` посреди тела функции затирает A, куда SDCC уже
   положил параметр (в первой версии пробника цвет заливки становился
   номером страницы, и «резидент не рисовал»).  Читать порт отдельной
   __naked-функцией.

Имя exe — 8.3 (w3bgfx.exe): DSS длинных имён не понимает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:18:27 +03:00
snark13 3b2dd8bfcc docs(PoP): план v2 — статус выполнения шагов 1..4 и найденная ловушка SDCC
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:08:47 +03:00
snark13 ecf5ecfc14 PoP roomtest (план v2, шаг 2): общая геометрия в pop_geom
Сведены дубли, разъехавшиеся по модулям:
- x_bump[20] был в pop_kid (uint8_t!) и в pop_map (int16_t) — теперь одна
  таблица int16_t;
- y_land[5] — две копии;
- y_to_row_mod4 — в pop_bg и pop_map;
- 32-битный LCG оригинала (prandom) — в pop_bg и pop_trob; функция теперь
  одна, а СИДЫ остались раздельными (у раскладки кладки и у фаз факелов
  свои последовательности, смешивать нельзя — иначе поедет рисунок стен).

Экономия по коду скромная (_CODE 24462 -> 24421, W3 11643 -> 11632: часть
выигрыша съели межмодульные вызовы).  Главное здесь другое: pop_geom лежит
в W1/W2 и не трогает графику, то есть это тот самый «чистый» слой, который
сможет звать __banked-код стражей (docs/layout_plan_v2.md §4, §5.2).

Проверено в MAME: комната 1 после пересборки отрисована ПОБАЙТОВО так же,
как до правки (0 различающихся пикселей в области комнаты) — значит
последовательности PRNG и геометрия не поехали; Kid бегает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:05:24 +03:00
snark13 4cffbc9aa4 PoP roomtest (план v2, фаза 1b): pop_map переведён на пометки перерисовки
Loose-полы, плита-потолок и щебень от приземления больше не зовут pop_bg —
ставят пометки (pop_set_redraw / pop_set_redraw_above), которые разбирает
pop_redraw_needed из главного цикла.  Удалены самодельные счётчики
loose_bake/loose_rest/ceil_rest/ceil_bake/land_bake: их роль (вторая
страница дабл-буфера) теперь у счётчика страниц в пометке.

Осталось ОДНО исключение: падающий кусок (mob) — spawn/tick/pos.  Это
движущийся ОБЪЕКТ, а не перерисовка тайла, и в оригинале он живёт отдельно
(mobs + draw_moving), поэтому разделение его на логику и отрисовку —
следующая фаза.  Из-за него pop_loose_tick остаётся единственной функцией
pop_map, которую нельзя звать из __banked-кода; вся коллизия, физика,
кромки и предметы — то, что понадобится стражам — чисты от графики.

Замер: _CODE 24718 -> 24462, _DATA 4301 -> 4219 (ушли rest-массивы).

Проверено в MAME: комната 12, осторожный шаг на плиту — тряска, падение
плиты, Kid проваливается на ряд 1, дыра и щебень отрисованы; на
замороженном кадре обе страницы дабл-буфера побайтово совпадают в области
изменений (7 строк).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:44:56 +03:00
snark13 24bb724c22 PoP roomtest (план v2, фаза 1a): пометки перерисовки вместо прямых блитов
Порт архитектуры оригинала: логика анимации тайлов НИЧЕГО не рисует, она
ставит флаг (set_redraw_full/set_wipe/redraw_20h/redraw_21h, seg007), а
отрисовка идёт отдельным проходом redraw_needed (seg008:0178).  У нас
появился pop_redraw.c/.h: pop_set_redraw(tilepos, вид, страницы) +
pop_set_redraw_above(col, ...) + pop_redraw_needed(), который зовёт
главный цикл в слое фона (до kid_draw).

Отличие от оригинала (наша платформа): счётчик пометки — это ЧИСЛО СТРАНИЦ
дабл-буфера (обычно 2), а вид перерисовки хранится явно (heal+поверх или
запечь фон), потому что у нас у каждой страницы своя ОЗУ-копия фона.  В
оригинале вид кодируется тем, в какой из таблиц redraw_frames_* стоит флаг.

pop_trob переведён на пометки: пики, кнопки, дверь уровня.  Его самодельные
массивы spike_rest/button_rest/ldoor_rest/rest_pending удалены — их роль
теперь у счётчика страниц в pop_redraw.  Прямыми вызовами pop_bg осталось
только пламя факела и пузырёк зелья: это не тайловая перерисовка, а
покадровый оверлей; из-за них pop_process_trobs остаётся единственной
функцией модуля, которую нельзя звать из банка.

Зачем: из __banked-кода резидентная страница W3 недостижима транзитивно
(docs/layout_plan_v2.md §2 R2), поэтому логика, которую будут звать стражи,
не должна вызывать pop_bg.

Проверено в MAME: комната 6 — Kid на кнопке-открывалке, решётка в шве
поднимается; сошёл с кнопки — закрывается; упал в шахту на пики — пики
выдвинулись и отрисованы (кадр 177).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:37:03 +03:00
snark13 eef6eebd8c PoP roomtest: фикс чтения таблицы кадров — баг кодогенерации SDCC
Кадры Kid читались из EMM-страницы по НЕВЕРНОМУ адресу для индексов >= 52.
Запись

    (const uint8_t *)(0x100) + (uint16_t)i * 5u

SDCC 4.5 собрал так: умножение честно в 16 битах (add hl,hl / add hl,bc),
а затем `ld c,l` + `inc b` — то есть взял только МЛАДШИЙ байт результата и
подставил старший байт константы.  При i*5 >= 256 адрес уезжал на -256*k,
и cur_frame наполнялся чужой строкой таблицы: у кадров бега/шага/подъёма
пропадал бит FRAME_NEEDS_FLOOR — Kid «вкручивался» в пол и проваливался
вниз, последовательности кадров не соответствовали seqtbl.

Фикс: адрес считается в uint16_t (i*5 = i + i<<2, без умножения) и
кастуется один раз — сгенерированный код теперь сохраняет старший байт
(ex de,hl / inc d).

Коварство бага: тот же паттерн в pop_level.c (room_fg_ptr/room_bg_ptr,
links) компилируется ПРАВИЛЬНО — проверил все три места по .asm.  Записано
в memory sdcc_z80_const_ptr_index_bug.

Проверено в MAME на замороженных кадрах: 10 выборок (кадры 4,10,15,49,50,
54,123) — cur_frame совпадает с таблицей во всех, включая те, что раньше
были испорчены.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:24:03 +03:00
snark13 3de8c7500c PoP roomtest (план v2, шаг 4): в W3-резиденте остаётся только pop_bg
--w3 берёт ОДИН файл на флаг, поэтому запись "--w3 pop_trob.c pop_map.c"
означала "W3 = pop_trob", а pop_map всё это время ехал в W1/W2 (в
build-каталоге лежал осиротевший w3_pop_map.rel).  Теперь список явный.

pop_trob переведён в W1/W2: в W3 должно оставаться только то, что банк
никогда не позовёт (из __banked резидентная страница W3 не видна ни
напрямую, ни транзитивно — docs/layout_plan_v2.md §2 R2).  pop_trob же
стражам понадобится: в оригинале они тоже давят кнопки.

Замер: W3 14376 -> 11643 (свободно 2008 -> 4741 Б), W1/W2 _CODE
21634 -> 24367 (куча 5315 -> 2582 Б).  Освободившееся место в W3 —
задел под шаг 3 (loose/потолок из pop_map, чтобы pop_map стал
bank-safe).

Проверено в MAME скриптом: комната рисуется, Kid бежит и тормозит
(кадры 15 -> 10 -> 15), факелы анимируются (152 различающихся пикселя
между соседними кадрами) — то есть pop_trob работает из нового окна.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:30:57 +03:00
snark13 8e33cd07bc PoP roomtest (план v2, шаг 1): таблицы анимации Kid -> EMM-страница
kid_frames (241*5) и kid_seqtbl (2310) занимали 3.5 КБ в _CODE окна W1/W2
— самого дефицитного ресурса.  Теперь они лежат в kid_data.bin (отдельная
EMM-страница), которая маппится в W0 ровно на время play_seq — один
map/unmap за тик, в фазе тика, без конфликта с атласом в W0.

Ключ к переносу — порт load_frame/cur_frame (seg006): оригинал раз за тик
копирует кадр в структуру, и вся коллизия/отрисовка читает ЕЁ, а не
таблицу.  У нас так же: kid_cur_dx/kid_cur_flags (их дёргают несколько раз
за кадр из pop_map) и kid_draw читают cur_frame — 5 байт в _DATA.
kid_seq_off (230 Б) оставлен резидентным: его читает kid_set_seq из
pop_ctrl/pop_map, вне страницы.

pop_extract_kid_data.py теперь пишет и kid_data.bin, и урезанный
kid_data.h (тип kframe, размеры, смещения в бинаре, kid_seq_off).

Замер: _CODE 24881 -> 21634 (-3247 Б), куча W2 2076 -> 5315 Б, W3 без
изменений.  Проверено в MAME скриптом: стойка -> бег (кадр 8) -> стоп,
перемещение и коллизия у кромки работают, спрайт рисуется.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:15:16 +03:00
snark13 a5773ab654 docs(PoP): план v2 — размер кода и раскладка по окнам/банкам/страницам
Новый документ applications/PoP/docs/layout_plan_v2.md по свежему замеру
(коммит 1214785): точные размеры окон/модулей/функций/данных, уточнённая
модель банкинга и пошаговый план.

Главное уточнение против v1: из __banked-кода резидент W3 недостижим — и
транзитивно тоже (bank -> pop_map -> pop_bg сломается).  Отсюда целевая
раскладка: W3-резидент = графика, которую зовёт только главный цикл;
W1/W2 = ядро, достижимое отовсюду (включая банки); банки = новая холодная
логика (стражи/боёвка).  Проверено по libbgi: скобка _bgi_begin/_bgi_end
сохраняет и возвращает ТЕКУЩУЮ страницу W3, поэтому примитивы libbgi
можно звать и из банка; нельзя лишь открывать скобку из кода, лежащего
в W3.

Крупнейшие цели: kid_data.h (3745 Б таблиц в _CODE) -> EMM-страница с
портом load_frame/cur_frame; вынос loose/потолка из pop_map в W3 (делает
pop_map bank-safe); дедуп геометрии в pop_geom.c; разгрузка _DATA.

Попутная находка: --w3 принимает ОДИН файл на флаг, поэтому в Makefile
"--w3 pop_trob.c pop_map.c" кладёт в W3 только pop_trob, а pop_map едет
в W1/W2 (в build-каталоге остался устаревший w3_pop_map.rel).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:55:36 +03:00
snark13 68b8fe5851 .gitignore: не версионировать docs/extra и docs/sources
docs/extra — ~570 МБ архивов чужих исходников (525 МБ из них — четыре
почти одинаковых zip'а bad_apple); docs/sources — клоны чужих
репозиториев со своими .git внутри, которые при обычном add стали бы
битыми gitlink-ссылками (без .gitmodules клон их не подтянет).

Материалы остаются на диске, но в историю не попадают: раздувание репо
необратимо без перезаписи истории.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:28:19 +03:00
snark13 64ce6339eb docs: справочники по железу Sprinter + правка gfx_scroll_h
- docs/new/ — сводные справочники (архитектура, BIOS, DSS, память,
  графика, акселератор, IRQ, порты, ввод, звук, известные баги);
- docs/Original/ — первоисточники, из которых они собраны (BIOS, Estex
  DSS, мануалы, описание акселератора), + Форум.doc/.docx в reference;
- libbgi/common/gfx_scroll_h.c — обход бага скролла при ширине >256
  (правка автора: шаг банды 255 и продвижение указателей на cw; старый
  вариант с 256 оставлен закомментированным с TODO);
- удалён applications/PoP/roomtest/hang_variants.png — рабочая раскладка
  из разбора позы виса, в репозитории ей не место.

Большие архивы (docs/extra ~568 МБ, docs/sources с вложенными git-репо
~68 МБ) в коммит НЕ включены — см. обсуждение.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:26:31 +03:00
91 changed files with 28192 additions and 1004 deletions
+7
View File
@@ -113,3 +113,10 @@ mame/
# Claude Code local settings (per-machine, not for the repo) # Claude Code local settings (per-machine, not for the repo)
.claude/ .claude/
# Тяжёлые справочные материалы, НЕ версионируются: docs/extra — архивы
# исходников (~570 МБ, четыре почти одинаковых bad_apple), docs/sources —
# клоны чужих репозиториев (Sprinter-BIOS, Estex-DSS, SaymanNsk) со своими
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
docs/extra/
docs/sources/
+43
View File
@@ -0,0 +1,43 @@
# Идеи и вопросы «на подумать» (PoP)
Не план работ, а список того, что осознанно отложено: каждая запись —
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
сделать это прямо сейчас.
## Заменить генератор псевдослучайных чисел
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
## Отключать мышь на время игры
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
см. memory `mouse_api`). Игре она не нужна вообще: управление —
raw-клавиатура (`<kbd_raw.h>`), которую мы и так забираем у DSS целиком.
Значит каждое мышиное прерывание за кадр — украденные такты в бюджете,
который у нас и без того занят на 86 %.
**Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три
кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К.
Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225
кадров подряд прошли без единого превышения. То есть эффект реальный и
измеримый, просто в тесте он был наведён извне.
**Что проверить.**
1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» —
разобрать список из 14 обёрток; если нет явной, посмотреть, что делает
«hide cursor» и снимает ли она обработчик.
2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе
(замер: breakpoint на входе ISR + totalcycles, при движении мыши).
3. Не ломает ли отключение выход в DSS: состояние обязано
восстанавливаться при `exit`, включая аварийный (atexit).
**Почему не сейчас.** Выигрыш проявляется только когда игрок реально
двигает мышью, то есть в норме его нет; а риск оставить систему без мыши
после выхода — заметный. Делать после того, как закроем стражей и
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
возможный возврат `pop_bg` в резидент `--w3`).
+446
View File
@@ -0,0 +1,446 @@
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
— рассчитан на старт с пустого контекста.
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в
ограничения окна W3.
---
## СТАТУС ВЫПОЛНЕНИЯ (обновлено 2026-07-29, коммит ecf5ecf)
| Шаг | Статус | Факт |
|-----|--------|------|
| 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
| 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | 41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой |
| 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже |
| 4. Разгрузка/перебалансировка W3 | **сделан** | резидент = `pop_bg` + `pop_gdraw` (отрисовка стража, 2026-07-29); W3 14 376 → 12 819 (свободно 3 565 Б) |
| 5. Данные (`room_modif`, `dl1/dl2`, `_kbdraw_down`) | не начат | потенциал ~1.5 КБ |
| 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже |
| 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» |
**Замер сейчас против замера §1:** `_CODE` 24 881 → 24 421, куча W2 2 076 → 2 592 Б,
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.
**Замер 2026-07-29 (после стража).** Появление стража съело кучу до 805 Б;
разгрузка — вынос ОТРИСОВКИ стража в резидент (`pop_gdraw.c`, `--w3`), логика
и состояние остались в W1/W2, чтобы банк `guards.c` их видел (R2). Итог:
`_CODE` 26 149 → 25 703, куча **805 → 1 245 Б**, W3-резидент 11 656 → 12 819
(свободно 4 728 → 3 565 Б), банк 1 — 236 / 16 384 Б. Граница «что резидент»
теперь формулируется одним правилом: **резидент = только то, что рисует и
зовётся исключительно из главного цикла**; всё, что может понадобиться банку,
остаётся в W1/W2.
### Что сделано вместо §5.3 (вынос loose в W3)
Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
**порт архитектуры оригинала**: логика ставит пометку, отрисовка идёт
отдельным проходом — `set_redraw_*` (seg007) + `redraw_needed` (seg008:0178).
Появился `pop_redraw.c/.h`; `pop_trob` и `pop_map` больше не рисуют тайлы.
Наши самодельные счётчики (`spike_rest`, `button_rest`, `ldoor_rest`,
`loose_bake`, `loose_rest`, `ceil_rest`, `ceil_bake`, `land_bake`) удалены —
их роль (вторая страница дабл-буфера) взял счётчик страниц в пометке.
**Два исключения остались** (обе — функции ТОЛЬКО главного цикла, звать из
банка нельзя):
- `pop_process_trobs` — пламя факела и пузырёк зелья (покадровый оверлей);
- `pop_loose_tick` — падающий кусок (mob): spawn/tick/pos. В оригинале это
отдельная подсистема (`mobs` + `draw_moving`), разделение на логику и
отрисовку — задел следующей фазы.
### Пробник банка (2026-07-29): модель ПОДТВЕРЖДЕНА
`tests/w3bankgfx` (huge + `--w3 res.c` + `--bank 1=bank1.c`, графика 256):
- банк рисует примитивом libbgi НАПРЯМУЮ — работает; страница W3 внутри
банка до блита, после блита и после возврата из вызванной им W1/W2-функции
одна и та же (0xF0), резидент — 0xF3. То есть `_bgi_begin`/`_bgi_end`
корректно возвращают ИМЕННО банковую страницу (правило R4);
- вызов W1/W2-функции из банка работает, и она тоже может рисовать;
- резидент W3 жив и вызывается после возврата из банка (R3).
**Дополнительно выяснено (важно для стражей):** писучие статики
`__banked`-модуля линкуются В СТРАНИЦУ БАНКА (0x1C000+) — снаружи их не
прочитать, из W1/W2 по 0xC000 видна резидентная страница. Значит всё
состояние банкованного кода (позиции стражей, таймеры боя) обязано жить в
W1/W2 как обычные глобалы, а банк — только код.
Ещё одна мина, найденная там же: инлайновый `in a,(#0xE2)` посреди тела
функции затирает A, куда SDCC уже положил параметр (у нас из-за этого цвет
заливки стал номером страницы, и «резидент не рисовал»). Читать порт
отдельной `__naked`-функцией.
### Найденная по дороге ловушка компилятора
`(const T *)КОНСТАНТА + var*K` SDCC 4.5 может собрать неверно: умножение
делает в 16 битах, а потом берёт только младший байт (`ld c,l` / `inc b`).
Кадры Kid с индексом ≥ 52 читались из чужой строки таблицы, у бега/шага
пропадал `FRAME_NEEDS_FLOOR` и персонаж проваливался сквозь пол. Лечение —
считать адрес в `uint16_t` и кастовать один раз. Тот же паттерн в
`pop_level.c` компилируется ПРАВИЛЬНО, т.е. полагаться на «у соседа
работает» нельзя. Подробности: memory `sdcc_z80_const_ptr_index_bug`.
---
## 0. Что уже сделано из v1 (не повторять)
- `--opt-code-size` и `--max-allocs 100000` **уже включены по умолчанию** в
`bin/sprinter-cc` (v1 §2.1 закрыт, выигрыш получен).
- Лишние блиты переднего слоя убраны (v1 §7 п.0): `fore_tile` больше не рисует
`bottom_id`, `_CODE` 388 Б.
- `gfx_blit_noclip` в libbgi (v1 §8 шаг 1): фоновые блиты в 2.9× дешевле.
- `--w3` как резидент окна 3 реализован и обкатан (memory `w3_resident_code`).
---
## 1. ЗАМЕР (сборка 2026-07-29, коммит 1214785)
Команда: `--memory small --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c`.
### 1.1 Окна
| Область | Занято | Свободно | Примечание |
|---|---|---|---|
| W1+W2 `_CODE` | 24 881 Б | — | 0x4100…0xA231 |
| W1+W2 `_HOME`+`_GSINIT`+`_DATA`+`_BSS` | ~4 240 Б | — | до 0xB2E4 |
| **W1+W2 куча** | 0 (никто не malloc'ит) | **2 076 Б** | 0xB2E4…0xBB00 |
| W1+W2 стек | — | 1 279 Б | 0xBB00…0xBFFE |
| **W3 резидент** | 14 376 Б | **2 008 Б** | 0xC000…0xF828 |
| EMM-страницы | 37 атласов + 1 уровень | ~215 страниц свободно | `sprinter_emm_budget` |
**Итого запаса до стены: ≈ 4 КБ** (2 КБ в W1/W2 + 2 КБ в W3). Стражи туда
не влезут.
### 1.2 Код по модулям (точно, из `.rel`)
```
W1/W2 (_CODE 24 881): W3 резидент (_W3CODE 14 376):
pop_kid 6 963 pop_bg 11 643
pop_map 6 157 pop_trob 2 733
roomtest 2 583
pop_level 1 425
pop_ctrl 1 145
crt0 333
libc+libbgi ~6 275
```
### 1.3 Крупнейшие функции/данные (из `.lst`)
```
pop_bg : draw_tile 3080, other_overlay_tile 1146, wall_pattern 944,
mob_render 720, mob_tick_one 661, overlay_mid_tile 498,
fore_only_tile 409, climb_overlay_tile 391, tile_table 371
pop_map : check_loose_fall_on_kid 674, check_bumped 567, jump_up_or_grab 413,
get_tile 266, do_knock 243, check_press 238, check_leave 212
pop_kid : kid_seqtbl 2310 + kid_frames 1205 + kid_seq_off 230 = 3745 Б ДАННЫХ
(в _CODE!), собственно кода ~3.2 КБ
roomtest : enter_room+main ~2.1 КБ
pop_level: room_bg_ptr 1084 (+ 515 Б таблиц LINKLOC/LINKMAP в _DATA)
```
### 1.4 `_DATA` (3 710 Б)
```
pop_trob 963 (room_modif[24][30] = 720 + trobs + rest-массивы)
pop_level 515 (копии LINKLOC/LINKMAP уровня)
pop_map 163, roomtest 141, pop_kid 130, pop_bg 115, pop_ctrl 13
libc: _irq_state 818, _kbdraw_state 515, _gfx_pal_buf 256, прочее ~200
```
### 1.5 Находки замера (мелкие, но чинить)
1. **`--w3` берёт ОДИН файл на флаг.** В `Makefile` написано
`--w3 pop_trob.c pop_map.c --w3 pop_bg.c`, и это значит «W3 = pop_trob и
pop_bg», а `pop_map.c` компилируется как обычный исходник в W1/W2. Судя по
`.sprinter-cc-roomtest/w3_pop_map.rel` (устаревший артефакт), когда-то
pop_map был в W3. **Решить осознанно** (см. §4) и записать явно:
`--w3 pop_trob.c --w3 pop_bg.c`.
2. `libc` тянет `_irq_state` 818 Б + `_kbdraw_state` 515 Б в `_DATA`.
`__irq_vec_buf` (513 Б) — таблица векторов IM2; `__kbdraw_down` (512 Б) —
битмап клавиш на 512 скан-кодов. Оба можно ужать (см. §5.4), это ~0.7 КБ
в самом дефицитном окне.
---
## 2. ПРАВИЛА ПЛАТФОРМЫ, ОТ КОТОРЫХ ПЛЯШЕТ РАСКЛАДКА
Это главное, что изменилось по сравнению с v1: модель банкинга уточнена по
`bin/sprinter-cc` (справка `--w3`/`--bank`) и по коду libbgi.
**(R1) Резидент W3 (`--w3`) и банки W3 (`--bank`) делят одно окно.**
Резидент лежит на своей странице 0xC000…0xFFFF; трамплин на время вызова
`__banked` подменяет страницу W3 на банковую и возвращает резидентную назад.
**(R2) Из банка резидент W3 НЕДОСТИЖИМ — и транзитивно тоже.**
Пока исполняется банк, резидентной страницы в адресном пространстве нет.
Значит нельзя не только `bank → pop_bg()`, но и `bank → pop_map() → pop_bg()`.
**Это ключевое ограничение при выборе, что делать банком.**
**(R3) Резидент → банк работает** (через трамплин в W1), резидент → W1/W2 —
тоже.
**(R4) Графические примитивы libbgi звать можно откуда угодно.**
`_bgi_begin` читает текущую страницу W3 из порта 0xE2, а `_bgi_end` её
возвращает — то есть скобка корректна и из банка, и из резидента. Нельзя
только одно: **звать `_bgi_begin`/`_bgi_end` ИЗ кода, который сам лежит в W3**
(после подмены страницы исчезнет исполняемый код — проверено, белый экран).
Поэтому `pop_bg` (резидент W3) обязан пользоваться готовыми примитивами
(`gfx_blit*`, `bar`, …), а батчинг скобки на весь `draw_tile` (v1 §8 шаг 1)
для него **невозможен** без переноса самого `draw_tile` в W1/W2.
**(R5) `--w3` кладёт в W3 код И rodata модуля** (`--codeseg/--constseg
W3CODE`), а писучие статики оставляет в `_DATA` (W2). То есть `const`-таблицы
переносятся в W3 бесплатно вместе с модулем (так уже лежит `tile_table` 371 Б).
**(R6) Данные в EMM-странице читаются, только пока страница в окне.**
`gfx_w0_map(page)` / `gfx_w0_unmap()` — окно W0 (0x0000…0x3FFF), первые 0x100
занимает ISR-стаб. Так уже работает `pop_level`. Цена — пара `OUT` на
маппинг, поэтому годится для «пачками», а не для чтения по байту в горячем
цикле.
---
## 3. ЧТО ДЕЛАТЬ НЕЛЬЗЯ (анти-паттерны, чтобы не потерять время)
- **Нельзя банковать `pop_bg`.** Он вызывается из pop_map, pop_trob, roomtest,
pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держит
`tile_table` и всю отрисовку. Банк дал бы трамплин на каждый блит.
- **Нельзя банковать `pop_map`, пока `pop_map` зовёт `pop_bg`** (R2). Сейчас
зовёт: `pop_loose_tick` и компания (~30 вызовов графики).
- **Нельзя тащить `kid_frames` в EMM «в лоб»**: он читается несколько раз за
кадр из коллизии (`kid_cur_dx`/`kid_cur_flags``dx_weight`,
`char_x_forward_edge`, …). Нужен кэш кадра (см. §5.1) — иначе маппинг
страницы окажется в горячем пути.
- **Нельзя «причёсывать» семейство `draw_tile` ради экономии, не имея
пиксельного теста.** Мы неделю выравнивали слои по SDLPoP; любой рефактор
этой зоны проверять диффом страниц (заморозка кадра клавишей `1` + сравнение
VRAM обеих страниц, приём из memory `mame_mcp_bridge`).
---
## 4. ЦЕЛЕВАЯ РАСКЛАДКА
Принцип: **W3-резидент = «толстая графика, которую зовёт только главный цикл»;
W1/W2 = ядро, которое должно быть достижимо ОТОВСЮДУ (включая банки); банки =
новая холодная логика (стражи, боёвка, будущие уровни)**.
```
W1/W2 (всегда отображено) W3 резидент (стр. 0xC000) Банки W3
────────────────────────── ───────────────────────── ─────────
libc + libbgi pop_bg (отрисовка тайлов) guards.c
pop_kid (интерпретатор+рисование) pop_trob (анимации тайлов) fight.c
pop_map (коллизия/физика/предметы) pop_loose.c (loose+потолок) debug/roomnav
pop_geom (общая геометрия/тайлы) enter_room-часть roomtest?
pop_ctrl (ввод/диспетчер)
roomtest (главный цикл)
```
Почему так:
- **`pop_map` остаётся в W1/W2** — его зовут и главный цикл, и (в будущем)
банк стражей; в W3 его класть нельзя именно из-за R2. Для этого из него надо
вынести графическую часть (loose/потолок) — она уезжает в W3 к `pop_bg`
(§5.3). После выноса `pop_map` становится **чистой логикой без единого
вызова графики** — тот самый bank-safe API.
- **`pop_kid` остаётся в W1/W2**: `play_seq`/`kid_set_seq`/`Kid` нужны и
стражам (у стражей ТА ЖЕ seqtbl), а `kid_draw` зовёт только libbgi (R4).
- **`pop_trob` остаётся резидентом**: его зовёт только главный цикл, и он сам
зовёт `pop_bg` — идеальный житель W3.
- **Банк стражей не зовёт ничего из W3.** Рисование стражей — либо через
libbgi напрямую (R4), либо (лучше) резидентный `guard_draw()` в W1/W2 рядом
с `kid_draw`, а банк только считает состояние. Тот же приём мы уже
используем для `pop_item_taken`/`pop_loose_fell`/`pop_ceil_fell`: банк
выставляет флаг — резидент рисует.
---
## 5. ПЛАН РАБОТ
Порядок выбран так, чтобы каждый шаг был проверяем отдельно и давал место
следующему.
### Шаг 1. `kid_data.h` (3 745 Б) → EMM-страница + порт `load_frame` — **самый большой выигрыш**
Сейчас `kid_seqtbl` (2310) + `kid_frames` (1205) + `kid_seq_off` (230) лежат в
`_CODE` окна W1/W2 — это 15 % всего дефицитного пространства.
Как переносить:
1. `pop_extract_kid_data.py` дополнительно пишет `kid_data.bin` (те же три
таблицы подряд, фиксированные смещения).
2. Грузим её в отдельную EMM-страницу тем же способом, что уровень
(`pop_level_load` — готовый образец), хэндл держим в `pop_kid`.
3. **Порт `load_frame` (seg006) и глобала `cur_frame`** — в оригинале ровно
так и сделано: раз за тик кадр копируется в структуру, а весь остальной код
читает `cur_frame`, а не таблицу. У нас `kid_cur_dx()/kid_cur_flags()`
станут чтением из `cur_frame` (5 байт в `_DATA`).
4. `play_seq` оборачивается в один `gfx_w0_map(kid_data_page)``unmap` на
вызов (в тике, не в отрисовке — конфликта с атласом в W0 нет).
Выигрыш: **3 745 Б из W1/W2**, цена — один маппинг страницы за тик и 5 байт
`_DATA`. Дополнительный бонус: `load_frame`/`cur_frame` — шаг К СХОДСТВУ с
оригиналом, а не отход от него.
Риск: сломать `play_seq` (сердце анимации). Проверка: прогон по комнатам с
эталонными позами (вис, подтягивание, прыжки, подъём меча).
### Шаг 2. Модуль `pop_geom.c` — дедуп + bank-safe фундамент
Сейчас продублировано между модулями:
| что | где | сколько |
|---|---|---|
| `y_to_row`/`y_to_row_mod4` | pop_bg + pop_map | 2 копии |
| `char_dx_forward` | pop_kid + pop_map | 2 копии |
| `x_bump[20]` | pop_kid (uint8) + pop_map (int16) | 20 + 40 Б, РАЗНЫЕ типы |
| `y_land[5]` | pop_kid + pop_map | 10 + 10 Б |
| `tile_is_floor` | pop_map (+ проверка кодов в roomtest) | 2 места |
| 32-битный LCG `prandom` | pop_bg (`prandom`) + pop_trob (`trob_prandom`) | 2 копии по ~60 Б + 2 сида |
Собрать в один W1/W2-модуль `pop_geom.c`: таблицы `x_bump/y_land/dir_front/
dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
`tile_is_floor`, `prandom`. Выигрыш прямой — сотни байт (оценка 250–400 Б),
но главное — **это и есть тот «чистый» API, который потом сможет звать банк**
(R2): вся геометрия оказывается в W1/W2 по определению.
Осторожно: `prandom` у pop_bg и pop_trob — РАЗНЫЕ последовательности с разными
сидами (стены vs фазы факелов). Объединять функцию можно, **сиды — нет**:
передавать сид указателем/по индексу, иначе поедет раскладка кладки.
### Шаг 3. Вынести loose/потолок из `pop_map` в W3
`pop_map` — единственный модуль W1/W2, который зовёт графику, и делает это
ровно в одном логическом блоке: `pop_loose_tick` + `check_press` + `do_knock` +
`fell_on_your_head` + `check_loose_fall_on_kid` + плита-потолок (~1.4–2 КБ).
Вынести их в `pop_loose.c`, собираемый `--w3` рядом с `pop_bg`/`pop_trob`.
Тогда:
- `pop_map` = чистая логика (bank-safe, R2 соблюдён);
- W1/W2 худеет ещё на ~1.5–2 КБ;
- W3 растёт на столько же — а место там появится после шага 4.
### Шаг 4. Перебалансировка резидента W3
После шага 3 в W3 будет тесно (14.4 + 2 ≈ 16.4 КБ > 16 КБ). Разгружаем:
1. **`wall_pattern` (944 Б) + `mob_render`/`mob_tick_one` (1381 Б)** — кандидаты
на переезд в W1/W2: их зовёт только `pop_bg`/`pop_loose`, но сами они уже
пользуются только libbgi (R4), значит из W1/W2 работают и остаются
достижимыми из банка.
2. `tile_table` и мелкие const-таблицы pop_bg (371 + ~300 Б) можно унести в
EMM-страницу **уровня** (там ~13.8 КБ свободно) — но только если чтение
происходит под уже замапленной страницей. Сейчас `draw_tile` читает
`tile_table` ВНЕ W0-контекста → потребуется явный маппинг на тайл. **Не
делать раньше замера**: 30 тайлов на входе в комнату × map/unmap — терпимо,
а вот в покадровых редроях (пики/loose/кнопка) — уже горячий путь.
3. Если и этого мало — `enter_room` (~2.1 КБ, зовётся только при смене комнаты)
переносится в резидент W3 или в БАНК (он вызывается из главного цикла =
резидента, значит банк допустим по R3).
### Шаг 5. Данные
1. **`room_modif[24][30]` = 720 Б** (pop_trob, `_DATA`). Нужен произвольный
доступ каждый кадр (анимации, ворота) — в EMM не годится. Но 24 комнаты ×
30 байт хранятся ЦЕЛИКОМ, хотя одновременно живут modif'ы только текущей
комнаты и соседей по швам. Вариант: хранить полный массив в EMM-странице
уровня, а в `_DATA` держать кэш на 2–3 комнаты (свою + левого/правого
соседа) с записью обратно при смене комнаты. Выигрыш ~600 Б, цена —
аккуратность на швах (кнопка в одной комнате открывает ворота в другой).
**Делать последним** — это самая «тонкая» правка по семантике.
2. **`dl1[256]`+`dl2[256]` = 512 Б** (pop_level, `_DATA` — копии LINKLOC/
LINKMAP уровня) — читаются при нажатии кнопки
и при отрисовке нажатой кнопки. Кандидат на чтение прямо из страницы
уровня (она и так маппится) — но проверить, что `pop_doorlink2` не зовётся
из отрисовки в тот момент, когда в W0 атлас. Выигрыш ~500 Б.
3. **`_kbdraw_down[512]` 512 Б** (libc): проверено — это **байт на скан-код**
(`libc/kbd/_kbdraw_state.c`), хотя комментарий называет его битовой картой.
Упаковка в биты даёт −448 Б, но добавляет сдвиг/маску в ISR-трамплин и в
`kbd_raw_down`. Трогать осторожно: raw-клавиатура уже дважды была
источником залипаний (memory `kbd_raw_fifo_drain`,
`kbd_overrun_wipe_modifiers`) — правку сопровождать прогоном docs/kbd-games.
4. **`__irq_vec_buf` 513 Б** (libc IM2): таблица векторов обязана быть
выровнена и полна — не трогать.
### Шаг 6. Контракт банка стражей (проектируется ДО написания кода)
Когда дойдём до стражей:
- `guards.c` собирается `--bank 1=guards.c`, режим `huge` (или `big` с
`BANKED=W1`, если W3 окажется тесен для трамплинов).
- **Банк зовёт только:** `pop_map` (чистая логика после шага 3), `pop_kid`
(`play_seq`, `kid_set_seq`, `cur_frame`), `pop_geom`, libc/libbgi.
- **Банк НЕ зовёт:** `pop_bg`, `pop_trob`, `pop_loose` (резидент W3) — ни
прямо, ни через промежуточные функции. Нужна отрисовка — выставляет флаг,
рисует резидент (идиома `pop_item_taken`).
- Первым делом — **пробник** (`tests/` или `--bank` на пустышке): банк зовёт
`pop_map`-функцию, та зовёт libbgi-примитив; убедиться в MAME, что скобка
W3 корректно возвращает банковую страницу (R4) — это проверка модели, а не
веры в неё.
### Шаг 7. Мелкий дедуп в `pop_bg` (после того, как появится тест страниц)
- Пять функций-редроев (`pop_spike_redraw`, `pop_loose_shake_draw`,
`pop_floor_bake`, `pop_button_redraw`, `pop_leveldoor_redraw`) отличаются
только прямоугольником heal, банком и набором тайлов — свести к одному
параметризованному хелперу (оценка −150…250 Б).
- `env_b/wall_b/fore_b/pot_b` — четыре одинаковых обёртки над `blit_b`
(оставить: экономия единицы байт, читаемость дороже).
- `overlay_mid_tile` / `fore_only_tile` / `climb_overlay_tile` / `draw_tile`
— общая структура «взять code/lcode, посчитать x/dmy/dby, разобрать слои».
Тут экономия потенциально сотни байт, но это **та самая зона риска из §3**
только с пиксельным диффом до/после и по одному слою за раз.
---
## 6. Ожидаемый итог
| Шаг | W1/W2 | W3 | Риск |
|---|---|---|---|
| 1. kid_data → EMM + load_frame | **3 745** | — | средний (сердце анимации) |
| 2. pop_geom (дедуп) | 250…400 | — | низкий |
| 3. loose → W3 | 1 500…2 000 | +1 500…2 000 | низкий (перенос как есть) |
| 4. разгрузка W3 (wall_pattern, mob) | +2 300 | 2 300 | низкий |
| 5. данные (room_modif, LINKLOC, kbd) | 1 000…1 600 | — | средний/высокий |
| 7. дедуп редроев pop_bg | — | −150…250 | средний |
Суммарно: **W1/W2 освобождается ~4.5–6 КБ**, W3 остаётся примерно в нынешнем
объёме, но становится «правильно заполненным» — в нём только то, что банк
никогда не позовёт. Плюс открывается путь к банкам: стражи и боёвка получают
до 16 КБ на банк, не трогая резидент.
---
## 7. Как мерить и проверять (обязательно к каждому шагу)
1. **До/после по `.rel`** — точные размеры на модуль:
`for f in .sprinter-cc-roomtest/*.rel; do grep '^A ' $f; done`
(области `_CODE`/`_W3CODE`/`_DATA`). Итоги окон печатает сам `sprinter-cc`.
2. **Функции** — из `.lst` (метки `_name:` и адреса), скрипт в истории этой
сессии; полезно ловить «функция распухла после рефактора».
3. **MAME**: любой перенос кода между окнами/страницами — это класс «молча
ломается» (`sprinter_memory_modes`). Минимум: комната 1 (loose), 12
(вис/подтягивание), 15 (меч), 9 (дверь уровня), 6 (кнопка/ворота).
4. **Пиксельный дифф** для правок отрисовки: заморозить кадр (`1`), сравнить
обе страницы дабл-буфера через `vram` (см. memory `mame_mcp_bridge`) и/или
сверить с эталонным рендером `render_room.py`.
5. **Скорость** — после шагов 1 и 4 замерить кадр маркерами в порт 0xFE
(приём из v1 §8), чтобы маппинг страницы за тик не съел бюджет.
## 8. Ссылки
- `bin/sprinter-cc` — справка по `--w3`, `--bank`, `--memory`, `--memory-manual`.
- `runtime/crt0_banked.s`, `runtime/bank.s` — трамплины и захват резидентной
страницы W3.
- `libbgi/common/_bgi_begin.c` / `_bgi_end.c` — механика скобки W3 (R4).
- memory: `w3_resident_code`, `pop_banking_architecture`, `sdcc_banking`,
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
`libc_one_function_per_module`.
- `applications/PoP/docs/size_optimization_plan.md` — v1 (замер 2026-07-21,
раздел §8 про скорость отрисовки актуален и не дублируется здесь).
+148
View File
@@ -0,0 +1,148 @@
# Генераторы псевдослучайных чисел: запасные варианты
Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально
можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра —
**сейчас менять ничего не нужно**.
## Что стоит сейчас
`pop_geom.c`, ветка `POP_PRANDOM_EXACT=1` (по умолчанию) — LCG оригинала
`s = s*214013 + 2531011`, шаг написан на Z80-ассемблере (единственное такое
место в порте). Схема Горнера по разреженной записи константы:
```
214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3
```
17 удвоений, три сложения, одно вычитание; величина `3*s`, нужная в конце,
попадается по дороге на втором шаге. Тело — **≈1 020 тактов** по статическому
подсчёту. Бит-в-бит совместим с SDLPoP, поэтому по картинке можно сверяться
с эталоном.
Вторая ветка, `POP_PRANDOM_EXACT=0` — xorshift16 + шаг Вейля на C.
Совместимость теряется.
Замер в MAME, комната 3, 175 кадров (медиана кадра):
| вариант | кадр | prandom → torch_draw |
|---|---|---|
| C, бит-в-бит (16-битные половины) | 403 632 | 10 933 |
| C, xorshift16 + Вейль | 397 986 | 7 927 |
| **asm, бит-в-бит (сейчас)** | **400 800** | **9 331** |
## Вариант A — комбинированный LFSR + LCG, ~148 тактов
Период > 4 млрд (lcm(65536, 65535) ≈ 4.29e9), младшие биты не вырождены.
```z80
prng16:
seed1=$+1
ld hl, 9999
ld b, h
ld c, l
add hl, hl
add hl, hl
inc l
add hl, bc
ld (seed1), hl
seed2=$+1
ld hl, 987
add hl, hl
sbc a, a
and 101101b
xor l
ld l, a
ld (seed2), hl
add hl, bc
ret
```
Устройство: `seed1` — LCG `x = 5x + 1` (по модулю 2^16; `inc l` вместо
`inc hl` — экономия байта, на период не влияет). `seed2` — 16-битный
LFSR Галуа: сдвиг влево, и если выехала единица, XOR младшего байта с маской
`0x2D` (примитивный многочлен `x^16 + x^5 + x^3 + x^2 + 1`). На выходе
сумма обоих состояний — она и разрушает регулярность младших бит LCG.
**Что мешает взять как есть:** сиды зашиты в код (SMC), а нам нужны ДВЕ
независимые последовательности — раскладка кладки и анимация тайлов.
Пришлось бы передавать состояние через указатель, как сейчас у `pop_prandom`
(это +20…40 тактов, не принципиально).
## Вариант B — xorshift(7,9,8), ~86 тактов
Самый быстрый, период 65535.
```z80
xrnd:
ld hl, 1 ; seed must not be 0
ld a, h
rra
ld a, l
rra
xor h
ld h, a
ld a, l
rra
ld a, h
rra
xor l
ld l, a
xor h
ld h, a
ld (xrnd+1), hl
ret
```
**Две оговорки.** Ноль — неподвижная точка, а сид раскладки кладки у нас
считается как `номер комнаты + смещение ряда + колонка` и вполне может
оказаться нулём: нужен либо guard, либо шаг Вейля поверх. И тот же SMC-сид,
что в варианте A.
## Чего НЕ брать: RND из Apple II
Оригинальный `Prince-of-Persia-Apple-II`:
```
RNDseed := (5 * RNDseed + 23) mod 256
```
```asm
RND
lda RNDseed
asl
asl
clc
adc RNDseed
clc
adc #23
sta RNDseed
rts
```
Полный период 256 (`a ≡ 1 mod 4`, `c` нечётное), и для своего движка он
работал. Нам не годится: у LCG по модулю 256 младшие биты вырождены — бит 0
просто чередуется. Наши вызовы это увидят: раскладка кладки берёт
`prandom(1)` (ОДИН бит) и `prandom(4)`, то есть вместо шума получилась бы
аккуратная шахматка.
## Сколько реально можно выиграть
Меньше, чем кажется по числам 86/148 против 1 020. Тело генератора — уже не
весь расход: остаются обёртка `pop_prandom`, приведение к диапазону
`pop_rnd_fit` и ABI вызова. Верхняя граница выигрыша видна из замера выше:
между нынешним asm-LCG и самым дешёвым из проверенных вариантов разница
**2 814 тактов за кадр (0.65 %)** при двух вызовах за кадр, и это ПОТОЛОК —
любой из вариантов A/B ниже него не опустится.
Порядок действий, если понадобится:
1. Сначала убрать обёртки: слить `pop_rnd_fit` в ту же asm-процедуру, чтобы
на вызов приходился один `call`, а не три. Это ничего не ломает и не
трогает совместимость с эталоном.
2. И только если этого мало — менять генератор, начиная с варианта A
(качество последовательности у него не хуже LCG, в отличие от B).
Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр
(факелы), а `guard_advance` / `guard_block` / `guard_strike` дёргают
`prandom(255)` каждый по разу за кадр боя — то есть при драке станет 5–6, и
цена вопроса вырастет во столько же раз.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -37,5 +37,5 @@ static const char *const kid_atl[KID_PAGES] = {
}; };
#define KID_PAL_FILE "kid.pal" #define KID_PAL_FILE "kid.pal"
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */ #define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
#define KID_SWORD_ID0 20 /* индекс в атласе = sword_tbl.id - ID0 */ #define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
#endif #endif
Binary file not shown.
Binary file not shown.
+43 -6
View File
@@ -5,9 +5,23 @@ PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := roomtest EXAMPLE := roomtest
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе # small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge). # CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
MEMORY ?= small MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
EXTRA_FLAGS ?= --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c # Резидента --w3 БОЛЬШЕ НЕТ: отрисовка уехала в БАНК 2 (pop_bg + pop_gdraw
EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c # в одной странице — вызовы между ними прямые, если функция не помечена
# __banked). Так снято главное ограничение резидента: из банка его было
# не достать ни прямо, ни транзитивно, а банк→банк работает (трамплин
# сохраняет страницу окна на СТЕКЕ, проверено вложенной цепочкой
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
# sdcc_banked_call_rules.
EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c $(PROF_FLAGS)
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
# MAME = время фазы в растровых строках.
PROF ?= 1
PROF_FLAGS := -DPROF_BORDER=$(PROF)
EXTRA_SRCS := pop_state.c pop_kid.c pop_ctrl.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid KID_DIR := $(CURDIR)/../poc/res/kid
@@ -22,16 +36,39 @@ BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.
# ЕДИНАЯ игровая (env+wall+kid). # ЕДИНАЯ игровая (env+wall+kid).
KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl) KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl)
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl
# Таблицы анимации (кадры+seqtbl) — отдельный бинарь в EMM-странице, его
# делает pop_extract_kid_data.py (не упаковщик спрайтов) — своё правило ниже.
KID_BIN := $(KID_DIR)/kid_data.bin
GUARD_DIR := $(CURDIR)/../poc/res/guard
GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \
$(GUARD_DIR)/g3.atl $(GUARD_DIR)/g4.atl
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(LVL_DATA) EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(LVL_DATA)
include $(PROJ_ROOT)/app.mk include $(PROJ_ROOT)/app.mk
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
# чтобы корень диска не зарастал десятками .atl.
# BG\ — фон (env/wall/fore/pot)
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
# GUARD\ — стражи (появятся здесь же)
# LEVELS\ — уровни
hdd: $(EXAMPLE).exe
$(PROJ_ROOT)/toolchain/make_hdd.sh $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd \
$(EXAMPLE).exe \
$(foreach f,$(BG_DATA),BG:$(f)) \
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
LEVELS:$(LVL_DATA)
TC := $(PROJ_ROOT)/applications/PoP/toolchain TC := $(PROJ_ROOT)/applications/PoP/toolchain
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py $(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
cd $(TC) && python3 pop_pack_bg.py cd $(TC) && python3 pop_pack_bg.py
$(GUARD_DATA): $(TC)/pop_pack_guard.py
cd $(TC) && python3 pop_pack_guard.py
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
cd $(TC) && python3 pop_pack_kid.py cd $(TC) && python3 pop_pack_kid.py
kid_data.h: $(TC)/pop_extract_kid_data.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
cd $(TC) && python3 pop_extract_kid_data.py cd $(TC) && python3 pop_extract_kid_data.py
$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(LVL_DATA) $(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
+499
View File
@@ -0,0 +1,499 @@
/*
* guards.c — ЛОГИКА стража, собирается в БАНК (--bank 1=guards.c, окно W3).
*
* Правила банка (проверено пробником tests/w3bankgfx):
* - состояние держим в W1/W2 (pop_guard.c): статики банка линкуются в
* страницу банка и снаружи не читаются;
* - звать можно W1/W2-код (pop_map, pop_kid, pop_geom, libc, примитивы
* libbgi) — скобка _bgi_begin/_bgi_end возвращает страницу БАНКА;
* - звать НЕЛЬЗЯ резидент W3 (pop_bg) — ни напрямую, ни транзитивно:
* пока исполняется банк, резидентной страницы в адресном пространстве
* нет. Поэтому фон/окклюзию за стража рисует главный цикл.
*
* Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000
* play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой:
* ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а
* дальше работает общий pop_control().
*/
#include "pop_guard.h"
#include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
#include "pop_geom.h"
#include "pop_trob.h"
#include "pop_state.h" /* pop_immortal — чит бессмертия */
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
#define TILE_GATE 4
#define TILE_DOORTOP_FLOOR 7
#define TILE_LOOSE 11
#define TILE_DOORTOP 12
#define TILE_CHOMPER 18
#define TILE_WALL 20
/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и
* «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его
* через границу банка дороже, чем повторить. */
static uint8_t tile_is_floor(uint8_t t)
{
switch (t) {
case 0: case 9: case 12: case 20:
case 26: case 27: case 28: case 29:
return 0;
default:
return 1;
}
}
/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid,
* seg003:761): xpos 7 делится на ширину тайла с округлением вниз. */
static int8_t tile_col; /* колонка последнего tile_at_kid */
static uint8_t tile_at_kid(int16_t xpos)
{
/* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos 7 58)/14),
* СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]).
* Оригинал берёт её из tile_div_tbl, у нас — делением с округлением
* вниз, как в pop_map. */
int16_t x = xpos - 7 - SCREENSPACE_X;
int16_t col = x / TILE_SIZEX;
if (x % TILE_SIZEX < 0) --col;
tile_col = (int8_t)col;
return pop_tile_at(tile_col, Kid.curr_row);
}
/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду.
* Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт /
* 2 видит и пойдёт. */
void pop_check_can_guard_see_kid(void) __banked
{
uint8_t kid_frame = Kid.frame;
int16_t left_pos, right_pos;
uint8_t tile;
if (!(Guard.charid != 0 &&
/* кадры 219..228 — подъём по лестнице: там страж Кида не видит */
kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) &&
Kid.alive < 0 && Guard.alive < 0 &&
Kid.room == Guard.room && Kid.curr_row == Guard.curr_row)) {
can_guard_see_kid = 0;
return;
}
can_guard_see_kid = 2;
left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
if (left_pos > right_pos) {
int16_t t = left_pos; left_pos = right_pos; right_pos = t;
}
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
if (tile_at_kid(left_pos) == TILE_CHOMPER)
left_pos += TILE_SIZEX;
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
if (tile_at_kid(right_pos) == TILE_GATE)
right_pos -= TILE_SIZEX;
while (left_pos <= right_pos) {
tile = tile_at_kid(left_pos);
/* Сквозь это не видно вовсе. */
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) {
can_guard_see_kid = 0;
return;
}
/* Видно, но страж не пойдёт: провалится / попадёт под чомпер /
* упрётся в неоткрытые ворота / шагнёт в дыру. */
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
can_guard_see_kid = 1;
} else if (tile == TILE_GATE &&
pop_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) {
can_guard_see_kid = 1; /* ворота подняты не до конца */
}
left_pos += TILE_SIZEX;
}
}
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
static void move_0_nothing(void)
{
control_shift = control_y = control_x = control_shift2 = 0;
control_down = control_up = control_backward = control_forward = 0;
}
static void move_1_forward(void)
{
control_x = POP_CONTROL_HELD_FORWARD;
control_forward = POP_CONTROL_HELD;
}
static void move_2_backward(void)
{
control_backward = POP_CONTROL_HELD;
control_x = POP_CONTROL_HELD_BACKWARD;
}
static void move_3_up(void)
{
control_y = POP_CONTROL_HELD_UP;
control_up = POP_CONTROL_HELD;
}
static void move_4_down(void)
{
control_down = POP_CONTROL_HELD;
control_y = POP_CONTROL_HELD_DOWN;
}
static void move_6_shift(void)
{
control_shift = POP_CONTROL_HELD;
control_shift2 = POP_CONTROL_HELD;
}
static void move_down_back(void)
{
control_down = POP_CONTROL_HELD;
move_2_backward();
}
static void move_down_forw(void)
{
control_down = POP_CONTROL_HELD;
move_1_forward();
}
/* autocontrol_guard_inactive (seg002:710): страж с убранным мечом.
* Пока Кид за спиной — страж не реагирует; исключение — «шум» Кида
* (is_guard_notice), от которого страж оборачивается. */
static void autocontrol_guard_inactive(void)
{
int16_t distance;
if (Kid.alive >= 0) return;
distance = pop_char_opp_dist();
/* В оригинале условие записано как (word)distance < (word)-8 —
* беззнаковое сравнение, истинное для ВСЕГО, кроме диапазона −8..−1.
* То есть: Кид не «вплотную за спиной». */
if (Opp.curr_row != Char.curr_row || distance >= 0 || distance < -8) {
if (is_guard_notice) {
is_guard_notice = 0;
if (distance < 0) {
if (distance < -4) move_4_down(); /* обернуться на шум */
return;
}
} else if (distance < 0) {
return;
}
}
if (can_guard_see_kid) {
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
move_down_forw();
}
}
/* ---- Активный страж (меч обнажён), seg002:737.. -------------------- *
* Таблицы вероятностей по 12 градациям мастерства (seg002:26..38). Бросок
* — prandom(255) с СОБСТВЕННЫМ сидом: у нас последовательности разведены
* по доменам (кладка / анимация тайлов / бой), см. pop_geom.h. */
static const uint8_t STRIKEPROB[NUM_GUARD_SKILLS] = { 61,100, 61, 61, 61, 40,100,220, 0, 48, 32, 48};
static const uint8_t RESTRIKEPROB[NUM_GUARD_SKILLS] = { 0, 0, 0, 5, 5,175, 16, 8, 0,255,255,150};
static const uint8_t BLOCKPROB[NUM_GUARD_SKILLS] = { 0,150,150,200,200,255,200,250, 0,255,255,255};
static const uint8_t IMPBLOCKPROB[NUM_GUARD_SKILLS] = { 0, 61, 61,100,100,145,100,250, 0,145,255,175};
static const uint8_t ADVPROB[NUM_GUARD_SKILLS] = {255,200,200,200,255,255,200, 0, 0,255,100,100};
/* Бросок «сработало ли»: в оригинале prob > prandom(255). */
static uint8_t roll(uint8_t prob)
{
return (uint8_t)(prob > (uint8_t)pop_prandom(&pop_fight_seed, 255));
}
/* guard_advance (seg002:0AF5): наступать. Мастерство 0 наступает всегда,
* остальные — только пока Кид не замахнулся. */
static void guard_advance(void)
{
if (guard_skill == 0 || kid_sword_strike == 0) {
if (roll(ADVPROB[guard_skill])) move_1_forward();
}
}
/* guard_block (seg002:0B1D): блокировать замах Кида. Если Кид только что
* пробил блок (justblocked) — своя, «импровизационная» вероятность. */
static void guard_block(void)
{
uint8_t of = Opp.frame;
if (of == FRAME_152_STRIKE_2 || of == FRAME_153_STRIKE_3 ||
of == FRAME_162_BLOCK_TO_STRIKE) {
if (roll(justblocked ? IMPBLOCKPROB[guard_skill]
: BLOCKPROB[guard_skill]))
move_3_up();
}
}
/* guard_strike (seg002:0B73): бить. По замаху Кида не бьём (кадры 169/151);
* из своего парирования — по отдельной вероятности «добить». */
static void guard_strike(void)
{
uint8_t of = Opp.frame, cf = Char.frame;
if (of == FRAME_169_BEGIN_BLOCK || of == FRAME_151_STRIKE_1) return;
if (cf == FRAME_161_PARRY || cf == FRAME_150_PARRY) {
if (roll(RESTRIKEPROB[guard_skill])) move_6_shift();
} else {
if (roll(STRIKEPROB[guard_skill])) move_6_shift();
}
}
/* autocontrol_guard_kid_armed (seg002:0AC1): Кид с мечом. Вплотную и
* далеко — наступаем; в «рабочей» дистанции 10..28 сначала пробуем блок, и
* только отдышавшись бьём. */
static void autocontrol_guard_kid_armed(int16_t distance)
{
if (distance < 10 || distance >= 29) {
guard_advance();
} else {
guard_block();
if (guard_refrac == 0) {
if (distance < 12 || distance >= 29) guard_advance();
else guard_strike();
}
}
}
/* autocontrol_guard_kid_in_sight (seg002:0A93). */
static void autocontrol_guard_kid_in_sight(int16_t distance)
{
if (Opp.sword == SWORD_2_DRAWN) {
autocontrol_guard_kid_armed(distance);
} else if (guard_refrac == 0) {
/* Кид безоружен: подойти и рубить. */
if (distance < 29) move_6_shift();
else move_1_forward();
}
}
/* Тайл перед стражем / через один (порт get_tile_infrontof_char и
* get_tile_infrontof2_char, seg004) — но для Char, а не для Kid. */
static uint8_t tile_infront(int8_t step)
{
int8_t d = (int8_t)(Char.direction < 0 ? -step : step);
return pop_tile_at((int8_t)(Char.curr_col + d), Char.curr_row);
}
/* autocontrol_guard_kid_far (seg002:09CB): идём, если впереди есть пол хотя
* бы через тайл; иначе пятимся (не шагать в пропасть). */
static void autocontrol_guard_kid_far(void)
{
if (tile_is_floor(tile_infront(1)) || tile_is_floor(tile_infront(2)))
move_1_forward();
else
move_2_backward();
}
/* autocontrol_guard_active (seg002:737). */
static void autocontrol_guard_active(void)
{
uint8_t cf = Char.frame;
uint8_t of;
int16_t distance;
/* Кадр 166 (неактивная стойка) и всё, что ниже 150, — не боевые позы. */
if (cf == FRAME_166_STAND_INACTIVE || cf < 150 || can_guard_see_kid == 1)
return;
if (can_guard_see_kid == 0) {
/* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе
* с физикой стража; скелета (charid 4) ветка тоже не берёт. */
if (Char.charid != CHARID_4_SKELETON) move_down_back();
return;
}
/* can_guard_see_kid == 2 */
of = Opp.frame;
distance = pop_char_opp_dist();
/* Кид падает/приземляется в отскоке — не мешаем ему, ждём. */
if (distance >= 12 && of >= 102 && of < 118 && Opp.action == 5)
return;
if (distance < 35) {
if ((Char.sword < SWORD_2_DRAWN && distance < 8) || distance < 12) {
/* Слишком близко: если смотрим в одну сторону — развернуться. */
if (Char.direction == Opp.direction) move_2_backward();
else move_1_forward();
} else {
autocontrol_guard_kid_in_sight(distance);
}
return;
}
if (guard_refrac != 0) return;
if (Char.direction != Opp.direction) {
/* Кид разбежался — встречаем ударом, а не шагом. */
if (of >= 7 && of < 15) { /* бег */
if (distance < 40) move_6_shift();
return;
} else if (of >= 34 && of < 44) { /* разбег в прыжок */
if (distance < 50) move_6_shift();
return;
}
}
autocontrol_guard_kid_far();
}
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
static void autocontrol_opponent(void)
{
move_0_nothing();
/* Таймеры боя убывают раз в кадр — здесь, как в оригинале. */
if (justblocked) --justblocked;
if (kid_sword_strike) --kid_sword_strike;
if (guard_refrac) --guard_refrac;
if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive();
else autocontrol_guard_active();
}
/* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */
#define ACTION_99_HURT 99
#define ACTION_1_RUN 1
#define FRAME_154_POKING 154
#define SEQ_69_ATTACK_WAS_PARRIED 69
#define SEQ_74_HIT_BY_SWORD 74
#define SEQ_85_STABBED_TO_DEATH 85
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
{ 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0};
/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту.
* Возврат 1 — HP кончились. Чит бессмертия ловим здесь: анимацию удара
* Кид получает, а HP не теряет. */
static uint8_t take_hp(uint8_t count)
{
if (Char.charid == CHARID_0_KID) {
if (pop_immortal) return 0;
if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
hitp_delta = (int8_t)-(int8_t)count;
} else {
if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; }
guardhp_delta = (int8_t)-(int8_t)count;
}
return 0;
}
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
* «получил удар».
* УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки
* меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map
* пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал
* идёт ровно нашей веткой. */
static void hurt_by_sword(void)
{
if (Char.alive >= 0) return;
/* Чит бессмертия перекрывает и «безоружного закалывают насмерть»:
* без этого Кид гибнет с одного удара мимо стойки, и чит бесполезен
* ровно там, где нужен. Анимацию удара оставляем. */
if (Char.charid == CHARID_0_KID && pop_immortal) {
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
play_seq();
return;
}
if (Char.sword != SWORD_2_DRAWN) {
take_hp(100);
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
} else if (Char.charid != CHARID_4_SKELETON && take_hp(1)) {
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
} else {
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
}
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
play_seq(); /* TODO: звук 12/13 */
}
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
* Бьют кадры 153/154; если соперник в этот момент парирует — удар отбит. */
static void check_hurting(void)
{
int16_t distance;
uint8_t cf, of, min_range;
if (Char.sword != SWORD_2_DRAWN) return;
if (Char.curr_row != Opp.curr_row) return;
cf = Char.frame;
if (cf != FRAME_153_STRIKE_3 && cf != FRAME_154_POKING) return;
distance = pop_char_opp_dist();
of = Opp.frame;
if (distance < 0 || distance >= 29 ||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
/* Соперник НЕ парирует. */
if (cf == FRAME_154_POKING) {
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
distance = pop_char_opp_dist();
if (distance >= (int16_t)min_range && distance < 29)
Opp.action = ACTION_99_HURT;
}
} else {
/* Парировано. */
Opp.frame = FRAME_161_PARRY;
if (Char.charid != CHARID_0_KID) justblocked = 4;
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
play_seq();
}
}
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
void pop_check_sword_hurting(void) __banked
{
uint8_t kf = Kid.frame;
if (kf == 0 || (kf >= 219 && kf < 229)) return; /* подъём по лестнице */
pop_loadshad_and_opp();
check_hurting();
pop_saveshad_and_opp();
pop_loadkid_and_opp();
check_hurting();
pop_savekid_and_opp();
}
/* check_sword_hurt (seg002:0CD4): применить помеченные попадания. Если
* ранен страж — Киду в этом кадре урон уже не проходит (в оригинале так же:
* его action сбрасывается ДО проверки). */
void pop_check_sword_hurt(void) __banked
{
if (Guard.action == ACTION_99_HURT) {
if (Kid.action == ACTION_99_HURT) Kid.action = ACTION_1_RUN;
pop_loadshad();
hurt_by_sword();
pop_saveshad();
guard_refrac = (int8_t)REFRACTIMER[guard_skill];
} else if (Kid.action == ACTION_99_HURT) {
pop_loadkid();
hurt_by_sword();
pop_savekid();
}
}
/* play_guard (seg006): ИИ + общий диспетчер. Отдельная ветка «HP кончились»:
* пока страж жив (alive < 0) и HP есть — обычный ход; как только HP дошли до
* нуля, он переводится в «умирает» (alive = 0), и дальше тот же control()
* ставит ему seq_71_dying. Уже умирающий сюда попадает с alive >= 0 и просто
* доигрывает последовательность. */
static void play_guard(void)
{
if (Char.alive < 0 && guardhp_curr == 0) {
Char.alive = 0;
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
* которого нет; для уровня 1 других эффектов там нет. */
}
autocontrol_opponent();
pop_control();
}
/* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у
* нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора
* последовательностей. Физика стража (fall_accel/коллизия/бамп/пики) —
* отдельным шагом. */
void pop_guard_tick(void) __banked
{
if (Guard.charid == 0) return; /* стража в комнате нет */
pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */
play_guard();
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
* выбирается по Char.charid внутри load_frame (seg006:0293). */
play_seq();
pop_saveshad();
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+38 -440
View File
@@ -8,449 +8,20 @@
#define SEQTBL_BASE 0x196Eu #define SEQTBL_BASE 0x196Eu
/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */ /* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid).
* САМА таблица кадров и seqtbl лежат в kid_data.bin (EMM-
* страница); здесь только тип и размеры. */
typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe; typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe;
#define KID_NFRAMES 241 #define KID_NFRAMES 241
static const kframe kid_frames[KID_NFRAMES] = {
{255, 0, 0,0x00,0x00},
{ 0, 1, 0,0xC4,0x00},
{ 1, 1, 0,0x44,0x00},
{ 2, 3, 0,0x47,0x00},
{ 3, 4, 0,0x48,0x00},
{ 4, 0, 0,0xE6,0x00},
{ 5, 0, 0,0x49,0x00},
{ 6, 0, 0,0x4A,0x00},
{ 7, 0, 0,0xC5,0x00},
{ 8, 0, 0,0x44,0x00},
{ 9, 0, 0,0x47,0x00},
{ 10, 0, 0,0x4B,0x00},
{ 11, 0, 0,0x43,0x00},
{ 12, 0, 0,0xC3,0x00},
{ 13, 0, 0,0x47,0x00},
{ 14, 0, 0,0x43,0x09},
{ 15, 0, 0,0xC3,0x00},
{ 16, 0, 0,0x44,0x00},
{ 17, 0, 0,0x46,0x00},
{ 18, 0, 0,0x48,0x00},
{ 19, 0, 0,0x89,0x00},
{ 20, 0, 0,0x0B,0x00},
{ 21, 0, 0,0x8B,0x00},
{ 22, 0, 0,0x11,0x00},
{ 23, 0, 0,0x07,0x00},
{ 24, 0, 0,0x05,0x00},
{ 25, 0, 0,0xC1,0x00},
{ 26, 0, 0,0xC6,0x00},
{ 27, 0, 0,0x43,0x00},
{ 28, 0, 0,0x48,0x00},
{ 29, 0, 0,0x42,0x00},
{ 30, 0, 0,0x42,0x00},
{ 31, 0, 0,0xC2,0x00},
{ 32, 0, 0,0xC2,0x00},
{ 33, 0, 0,0x43,0x00},
{ 34, 0, 0,0x48,0x00},
{ 35, 0, 0,0xCE,0x00},
{ 36, 0, 0,0xC1,0x00},
{ 37, 0, 0,0x45,0x00},
{ 38, 0, 0,0x8E,0x00},
{ 39, 0, 0,0x0B,0x00},
{ 40, 0, 0,0x8B,0x00},
{ 41, 0, 0,0x8A,0x00},
{ 42, 0, 0,0x01,0x00},
{ 43, 0, 0,0xC4,0x00},
{ 44, 0, 0,0xC3,0x00},
{ 45, 0, 0,0xC3,0x00},
{ 46, 0, 0,0xA5,0x00},
{ 47, 0, 0,0xA4,0x00},
{ 48, 0, 0,0x66,0x00},
{ 49, 4, 0,0x67,0x00},
{ 50, 3, 0,0x66,0x00},
{ 51, 1, 0,0x44,0x00},
{ 64, 0, 0,0xC2,0x00},
{ 65, 0, 0,0x41,0x00},
{ 66, 0, 0,0x42,0x00},
{ 67, 0, 0,0x00,0x00},
{ 68, 0, 0,0x00,0x00},
{ 69, 0, 0,0x80,0x00},
{ 70, 0, 0,0x00,0x00},
{ 71, 0, 0,0x80,0x00},
{ 72, 0, 0,0x00,0x00},
{ 73, 0, 0,0x80,0x00},
{ 74, 0, 0,0x00,0x00},
{ 75, 0, 0,0x00,0x00},
{ 76, 0, 0,0x80,0x00},
{255, 0, 0,0x00,0x00},
{ 80, -2, 0,0x41,0x00},
{ 81, -2, 0,0x41,0x00},
{ 82, -1, 0,0xC2,0x00},
{ 83, -2, 0,0x42,0x00},
{ 84, -2, 0,0x41,0x00},
{ 85, -2, 0,0x41,0x00},
{ 86, -2, 0,0x41,0x00},
{ 87, -1, 0,0x07,0x00},
{ 88, -1, 0,0x05,0x00},
{ 89, 2, 0,0x07,0x00},
{ 90, 2, 0,0x07,0x00},
{ 91, 2, -3,0x00,0x00},
{ 92, 2, -10,0x00,0x00},
{ 93, 2, -11,0x80,0x00},
{ 94, 3, -2,0x43,0x00},
{ 95, 3, 0,0xC3,0x00},
{ 96, 3, 0,0xC3,0x00},
{ 97, 3, 0,0x63,0x00},
{ 98, 4, 0,0xE3,0x00},
{ 28, 0, 0,0x00,0x00},
{ 99, 7, -14,0x80,0x00},
{100, 7, -12,0x80,0x00},
{101, 4, -12,0x00,0x00},
{102, 3, -10,0x80,0x00},
{103, 2, -10,0x80,0x00},
{104, 1, -10,0x80,0x00},
{105, 0, -11,0x00,0x00},
{106, -1, -12,0x00,0x00},
{107, -1, -14,0x00,0x00},
{108, -1, -14,0x00,0x00},
{109, -1, -15,0x80,0x00},
{110, -1, -15,0x80,0x00},
{111, 0, -15,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{112, 0, 0,0xC6,0x00},
{113, 0, 0,0x46,0x00},
{114, 0, 0,0xC5,0x00},
{115, 0, 0,0x45,0x00},
{116, 0, 0,0xC2,0x00},
{117, 0, 0,0xC4,0x00},
{118, 0, 0,0xC5,0x00},
{119, 0, 0,0x46,0x00},
{120, 0, 0,0x47,0x00},
{121, 0, 0,0x47,0x00},
{122, 0, 0,0x49,0x00},
{123, 0, 0,0xC8,0x00},
{124, 0, 0,0xC9,0x00},
{125, 0, 0,0x49,0x00},
{126, 0, 0,0x45,0x00},
{127, 2, 0,0x45,0x00},
{128, 2, 0,0xC5,0x00},
{129, 0, 0,0xC3,0x00},
{255, 0, 0,0x00,0x00},
{133, 0, 0,0x43,0x00},
{134, 0, 0,0xC4,0x00},
{135, 0, 0,0xC5,0x00},
{136, 0, 0,0x48,0x00},
{137, 0, 0,0x6C,0x00},
{138, 0, 0,0xEF,0x00},
{139, 0, 0,0x63,0x00},
{140, 0, 0,0xC3,0x00},
{141, 0, 0,0x43,0x00},
{142, 0, 0,0x43,0x00},
{143, 0, 0,0x44,0x00},
{144, 0, 0,0x44,0x00},
{172, 0, 1,0xC1,0x00},
{173, 0, 1,0xC7,0x00},
{145, 0, -12,0x01,0x00},
{146, 0, -21,0x00,0x00},
{147, 1, -26,0x80,0x00},
{148, 4, -32,0x80,0x00},
{149, 6, -36,0x81,0x00},
{150, 7, -41,0x82,0x00},
{151, 2, 17,0x42,0x00},
{152, 4, 9,0xC4,0x00},
{153, 4, 5,0xC9,0x00},
{154, 4, 4,0xC8,0x00},
{155, 5, 0,0x69,0x00},
{156, 5, 0,0xE9,0x00},
{157, 5, 0,0xE8,0x00},
{158, 5, 0,0x69,0x00},
{159, 5, 0,0x69,0x00},
{184, 0, 2,0x80,0x10},
{174, 0, 2,0x80,0x1A},
{175, 3, 2,0x00,0x12},
{176, 7, 2,0xC4,0x16},
{177, 10, 2,0x00,0x15},
{178, 7, 2,0x80,0x17},
{179, 4, 2,0x80,0x19},
{180, 0, 2,0xCE,0x18},
{181, 0, 2,0xCD,0x0F},
{182, 3, 2,0x00,0x14},
{183, 3, 2,0x00,0x1F},
{184, 0, 2,0x80,0x10},
{185, 0, 2,0x80,0x11},
{186, 0, 2,0x00,0x20},
{187, 0, 2,0x80,0x21},
{188, 2, 2,0xC3,0x22},
{ 14, 0, 0,0x43,0x00},
{189, 7, 2,0x80,0x13},
{190, 1, 2,0x80,0x0E},
{191, 0, 2,0x80,0x1B},
{181, 0, 2,0xCD,0x0F},
{181, 0, 2,0xCD,0x0F},
{112, 0, 0,0xC6,0x2B},
{113, 0, 0,0x46,0x2C},
{114, 0, 0,0xC5,0x2D},
{115, 0, 0,0x45,0x2E},
{114, 0, 0,0xC5,0x00},
{ 78, 0, 3,0x8A,0x00},
{ 77, 4, 3,0x87,0x00},
{211, 0, 1,0x44,0x00},
{212, 0, 1,0x44,0x00},
{213, 0, 1,0x44,0x00},
{214, 0, 1,0x47,0x00},
{215, 0, 7,0x4B,0x00},
{255, 0, 0,0x00,0x00},
{ 79, 4, 7,0x49,0x00},
{130, 0, 0,0x44,0x00},
{131, 0, 0,0x44,0x00},
{132, 0, 2,0x44,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{192, 0, 0,0x00,0x00},
{193, 0, 1,0x00,0x00},
{194, 0, 0,0x80,0x00},
{195, 0, 0,0x00,0x00},
{196, -1, 0,0x00,0x00},
{197, -1, 0,0x00,0x00},
{198, -1, 0,0x00,0x00},
{199, -4, 0,0x00,0x00},
{200, -4, 0,0x80,0x00},
{201, -4, 0,0x00,0x00},
{202, -4, 0,0x00,0x00},
{203, -4, 0,0x00,0x00},
{204, -4, 0,0x00,0x00},
{205, -5, 0,0x00,0x00},
{206, -5, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{207, 0, 1,0x46,0x00},
{208, 0, 1,0xC6,0x00},
{209, 0, 1,0xC8,0x00},
{210, 0, 1,0x4A,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{ 52, 0, 0,0x80,0x00},
{ 53, 0, 0,0x00,0x00},
{ 54, 0, 0,0x00,0x00},
{ 55, 0, 0,0x00,0x00},
{ 56, 0, 0,0x80,0x00},
{ 57, 0, 0,0x00,0x00},
{ 58, 0, 0,0x00,0x00},
{ 59, 0, 0,0x00,0x00},
{ 60, 0, 0,0x80,0x00},
{ 61, 0, 0,0x00,0x00},
{ 62, 0, 0,0x80,0x00},
{ 63, 0, 0,0x00,0x00},
{160, 1, 1,0xC3,0x23},
{161, 0, 1,0x49,0x24},
{162, 0, 1,0xC3,0x25},
{163, 0, 1,0x49,0x26},
{164, 0, 1,0xC3,0x27},
{165, 1, 1,0x49,0x28},
{166, 1, 1,0x43,0x29},
{167, 1, 1,0xC9,0x2A},
{168, 4, 1,0xC6,0x00},
{169, 3, 1,0xCA,0x00},
{170, 1, 1,0x43,0x00},
{171, 1, 1,0xC8,0x00},
};
#define KID_SEQTBL_LEN 2310 #define KID_SEQTBL_LEN 2310
static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {
0xF9,0x01,0xFF,0x81,0x19,0xF9,0x01,0x01,0x02,0x03,0x04,0xFB, /* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */
0x08,0x05,0xFB,0x03,0x06,0xFB,0x03,0x07,0xFB,0x05,0x08,0xFB, #define KID_BIN_FRAMES_OFF 0
0x01,0xF2,0x01,0x09,0xFB,0x02,0x0A,0xFB,0x04,0x0B,0xFB,0x05, #define KID_BIN_SEQTBL_OFF 1205
0x0C,0xFB,0x02,0xF2,0x01,0x0D,0xFB,0x03,0x0E,0xFB,0x04,0xFF, /* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс
0x81,0x19,0xF9,0x00,0x0F,0xFF,0xA0,0x19,0xF9,0x01,0xA6,0xFF, * frame + add_frame - 149 (add_frame=70 для кадров 102..106). */
0xA8,0x19,0xF9,0x05,0xFB,0x0A,0xB1,0xB1,0xFB,0xF9,0xFA,0xFE, #define KID_NGFRAMES 41
0xB2,0xFB,0x05,0xFA,0x02,0xA6,0xFB,0xFF,0xFF,0xD2,0x19,0xFF, #define KID_BIN_GFRAMES_OFF 3515
0xD2,0x19,0xF9,0x01,0xFB,0x02,0xCF,0xD0,0xFB,0x02,0xD1,0xFB,
0x02,0xD2,0xFB,0x03,0xF9,0x01,0xF2,0x00,0x9E,0xAA,0xAB,0xFF,
0xD8,0x19,0xF9,0x05,0xF8,0xFF,0x00,0xAC,0xFB,0xFF,0xFA,0x01,
0xAD,0xFB,0xFF,0xAE,0xFB,0xFF,0xFA,0x02,0xFB,0xFE,0xFA,0x01,
0xFB,0xFB,0xFA,0xFC,0xFF,0x4D,0x1A,0xF9,0x01,0xF8,0x01,0x00,
0x9B,0xFB,0x02,0xA5,0xFB,0xFE,0xFF,0xD2,0x19,0xF9,0x01,0xF8,
0xFF,0x00,0x9B,0x9C,0x9D,0x9E,0xFF,0x2E,0x1A,0xF9,0x01,0xF8,
0x01,0x00,0xFB,0x02,0xA3,0xFB,0x04,0xA4,0xA5,0xFF,0xD2,0x19,
0xF9,0x01,0xF8,0x01,0x00,0xFB,0x06,0xA4,0xA5,0xFF,0xD2,0x19,
0xF9,0x01,0xF8,0xFF,0x00,0xFB,0xFD,0xA0,0xFB,0xFE,0x9D,0xFF,
0xD2,0x19,0xF9,0x01,0xF8,0xFF,0x00,0xA8,0xF9,0x01,0x97,0xF9,
0x01,0x98,0x99,0x9A,0xF9,0x05,0x9B,0xF9,0x01,0x9C,0x9D,0xFF,
0xD2,0x19,0xF9,0x01,0xA7,0xFF,0x4A,0x1A,0xA2,0xFF,0x45,0x1A,
0xA9,0x96,0xFF,0xD2,0x19,0x9F,0xA0,0xFF,0x5F,0x1A,0xF9,0x01,
0xF4,0xFF,0xD2,0x19,0xF9,0x05,0xFB,0xF8,0xFF,0xD2,0x19,0xF9,
0x05,0xA0,0x9D,0xFF,0xD2,0x19,0xF9,0x07,0xFB,0xF8,0xFF,0x39,
0x1B,0xF9,0x05,0xFE,0xFB,0x05,0xFF,0x2E,0x1A,0xF9,0x05,0xFE,
0xFB,0x12,0xFF,0xA6,0x19,0xF9,0x01,0x10,0x11,0xFB,0x02,0x12,
0xFB,0x02,0x13,0xFB,0x02,0x14,0xFB,0x02,0x15,0xFB,0x02,0x16,
0xFB,0x07,0x17,0xFB,0x09,0x18,0xFB,0x05,0xFA,0xFA,0x19,0xFB,
0x01,0xFA,0x06,0x1A,0xFB,0x04,0xF4,0xF2,0x01,0x1B,0xFB,0xFD,
0x1C,0xFB,0x05,0x1D,0xF2,0x01,0x1E,0x1F,0x20,0x21,0xFB,0x01,
0xFF,0xA0,0x19,0xF9,0x01,0xF2,0x01,0x22,0xFB,0x05,0x23,0xFB,
0x06,0x24,0xFB,0x03,0x25,0xFB,0x05,0xF2,0x01,0x26,0xFB,0x07,
0x27,0xFB,0x0C,0xFA,0xFD,0x28,0xFB,0x08,0xFA,0xF7,0x29,0xFB,
0x08,0xFA,0xFE,0x2A,0xFB,0x04,0xFA,0x0B,0x2B,0xFB,0x04,0xFA,
0x03,0x2C,0xFB,0x05,0xF4,0xF2,0x01,0xFF,0x81,0x19,0xF9,0x01,
0xFB,0x01,0x6B,0xFB,0x02,0xFB,0x02,0x6C,0xFB,0x02,0x6D,0xFB,
0x02,0x6D,0xFB,0x02,0x6D,0xFF,0x16,0x1B,0x00,0xF9,0x01,0xFB,
0x01,0x6E,0x6F,0xFB,0x02,0x70,0xFB,0x02,0x6C,0xFB,0x02,0x6D,
0xFF,0x29,0x1B,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E,
0xFF,0xC4,0x19,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E,
0xFB,0x02,0x2F,0xFB,0xFF,0x30,0xFB,0x01,0x31,0xFB,0xFE,0x32,
0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,0xFF,0xFF,0x75,0x19,
0xF9,0x01,0xFB,0x01,0x35,0xFB,0x01,0xF2,0x01,0x36,0xFB,0x08,
0x37,0xF2,0x01,0x38,0xFB,0x07,0x39,0xFB,0x03,0x3A,0xFB,0x01,
0x3B,0x3C,0xFB,0x02,0x3D,0xFB,0xFF,0x3E,0x3F,0x40,0xFB,0xFF,
0x41,0xFB,0xF2,0xFE,0xFF,0x95,0x19,0xF9,0x03,0xFA,0xFF,0x66,
0xFB,0xFE,0xFA,0x06,0x67,0xFB,0xFE,0xFA,0x09,0x68,0xFB,0xFF,
0xFA,0x0C,0x69,0xFB,0xFD,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0xF9,
0x03,0xFA,0xFF,0xFB,0xFE,0x66,0xFB,0xFD,0xFA,0x06,0x67,0xFB,
0xFD,0xFA,0x09,0x68,0xFB,0xFE,0xFA,0x0C,0x69,0xFB,0xFD,0xF8,
0x00,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0xFF,0x66,
0xFB,0x02,0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,
0x69,0xFB,0xFE,0xF8,0x01,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,
0x01,0xFA,0x03,0xF7,0x06,0x1C,0x66,0xFB,0x02,0xFA,0x06,0x67,
0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,0xFE,0xF8,0x01,
0x0F,0xFF,0x49,0x1D,0xFB,0xFF,0xFA,0xFD,0xFF,0xE4,0x1B,0xFB,
0x01,0xFF,0xDB,0x1B,0x66,0xFB,0x02,0xFA,0x03,0x67,0xFB,0xFF,
0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,0x01,0x06,0xFF,
0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,0x66,0xFB,0x02,0xFA,
0x06,0x67,0xFB,0x01,0xFA,0x09,0x68,0xFB,0x02,0xFA,0x0C,0x69,
0xF8,0x02,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,
0x66,0xFB,0x03,0xFA,0x06,0x67,0xFB,0x02,0xFA,0x09,0x68,0xFB,
0x03,0xFA,0x0C,0x69,0xF8,0x03,0x0F,0xFF,0x49,0x1D,0xF9,0x01,
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,
0x02,0x4E,0x4F,0x50,0xFF,0xA1,0x1C,0xF9,0x01,0x43,0x44,0x45,
0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,0x02,0xFB,0x01,
0x4E,0xFB,0x02,0x4F,0xFB,0x01,0x50,0xFF,0xA1,0x1C,0xF9,0x01,
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFB,0xFF,
0x4D,0xF9,0x02,0xFB,0xFE,0x4E,0xFB,0xFF,0x4F,0xFB,0xFF,0x50,
0xFF,0xA1,0x1C,0xF9,0x02,0x5B,0x5A,0x59,0x58,0x57,0x57,0x57,
0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63,
0x61,0x60,0x5F,0x5E,0x5D,0x5C,0x5B,0x5A,0x59,0x58,0x57,0x58,
0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x5F,0x5E,0x5D,0x5C,
0xFF,0x25,0x1D,0xF9,0x06,0x5C,0x5D,0x5D,0x5C,0x5C,0x5B,0xFF,
0xD8,0x1C,0x87,0x88,0x89,0x89,0x8A,0x8A,0x8A,0x8A,0x89,0x88,
0x87,0xFB,0xF9,0xFF,0x25,0x1D,0xF9,0x01,0x94,0x91,0x90,0x8F,
0x8E,0x8D,0xFB,0xFB,0xFA,0x3F,0xFC,0xF9,0x03,0x8C,0x8A,0x88,
0x5B,0xF9,0x02,0xFF,0xA4,0x1C,0xF9,0x01,0x87,0x88,0x89,0x8A,
0x8B,0x8C,0xFB,0x05,0xFA,0xC1,0xFD,0x8D,0x8E,0x8F,0x90,0x91,
0x92,0x93,0x94,0xF9,0x05,0x95,0xF9,0x01,0x76,0x77,0xFB,0x01,
0xFF,0xA0,0x19,0x51,0x52,0xF9,0x05,0x53,0xF9,0x01,0xF4,0xF2,
0x00,0x54,0x55,0xFB,0x03,0xFF,0xA0,0x19,0xF9,0x03,0x51,0xFA,
0x06,0x51,0xFA,0x09,0x51,0xFA,0x0C,0xFB,0x02,0xF8,0x00,0x0C,
0xFF,0x49,0x1D,0xF9,0x04,0x6A,0xFF,0x4B,0x1D,0xF9,0x01,0x35,
0xFB,0x02,0xF2,0x01,0x36,0xFB,0x07,0x37,0xF2,0x01,0x38,0xFB,
0x02,0x31,0xFB,0xFE,0x32,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,
0xF9,0x00,0xF5,0x4F,0xFF,0x25,0x1D,0xF9,0x01,0x43,0x44,0x45,
0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0xFA,0xFC,
0x4F,0xFA,0xFE,0x4F,0x4F,0xFA,0x02,0x4F,0xFA,0x04,0xFF,0x25,
0x1D,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFA,
0xFF,0x4D,0xFA,0xFD,0x4E,0xFA,0xFC,0x4F,0xFA,0xF6,0x4F,0xFA,
0xF7,0x4F,0xFA,0xF8,0x4F,0xFA,0xF9,0x4F,0xFA,0xFA,0x4F,0xFA,
0xFB,0x4F,0xFA,0xFC,0x4F,0xFA,0xFD,0x4F,0xFA,0xFE,0x4F,0xFA,
0xFE,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0x4F,
0x4F,0x4F,0xFA,0x01,0x4F,0xFA,0x01,0x4F,0xFA,0x02,0x4F,0xFA,
0x02,0x4F,0xFA,0x03,0x4F,0xFA,0x04,0x4F,0xFA,0x05,0x4F,0xFA,
0x06,0x4F,0xF8,0x00,0x06,0xFF,0x49,0x1D,0xF9,0x03,0x50,0xFF,
0xA1,0x1C,0xF9,0x05,0xFB,0xFC,0x32,0x33,0x34,0xFF,0xA0,0x19,
0xF9,0x05,0xFB,0x01,0xFA,0x03,0xF7,0x25,0x1E,0x66,0xFB,0x02,
0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,
0xFE,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0x66,0xFB,0x02,0xFA,0x03,
0x67,0xFB,0xFF,0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,
0x00,0x06,0xFF,0x49,0x1D,0xF9,0x05,0xFB,0xFF,0xFA,0xFC,0x66,
0xFB,0xFF,0xFA,0x03,0xFB,0xFD,0xFA,0x01,0xF4,0xFB,0x01,0xF2,
0x01,0x6B,0xFB,0x02,0x6C,0xF2,0x01,0x6D,0xFF,0xB3,0x1F,0x79,
0xFB,0x01,0x7A,0x7B,0xFB,0x02,0x7C,0xFB,0x04,0x7D,0xFB,0x03,
0x7E,0xFB,0xFC,0x56,0xF2,0x01,0xF4,0xFB,0xFC,0x74,0xFB,0xFE,
0x75,0x76,0x77,0xFF,0xA0,0x19,0xFB,0xFB,0xFF,0xA0,0x19,0xF9,
0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,
0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x03,0x7F,0x80,0x81,
0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,
0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,
0xFB,0xFF,0xFB,0x02,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x01,0x7F,
0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,
0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,
0x03,0x7E,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFE,0x80,0x81,0x82,
0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFF,0xFC,0x1E,0xF9,
0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,
0x04,0x7D,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
0x7C,0xFB,0x02,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,
0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x02,
0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,
0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x01,0x81,0x82,0x83,
0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,
0x7B,0xFB,0x02,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,
0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x01,0x83,0x84,0xFF,0xA0,0x19,
0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x84,0xFF,0xA0,0x19,
0xF9,0x01,0x79,0xFB,0x01,0x84,0xFF,0xA0,0x19,0xF9,0x01,0xFB,
0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFF,0xAF,0x1F,0xF9,0x05,0xFB,
0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0x72,0x73,0x74,
0xFB,0xFC,0x75,0x76,0x77,0xFF,0xA0,0x19,0xF9,0x01,0xF3,0x01,
0xE5,0xE5,0xE5,0xE5,0xE5,0xE5,0xE6,0xE7,0xE8,0xFF,0xDA,0x1F,
0xF9,0x01,0xFB,0xFB,0xE9,0xEA,0xEB,0xEC,0xED,0xEE,0xEF,0xF0,
0x85,0x85,0x86,0x86,0x86,0x30,0xFB,0x01,0x31,0xFB,0xFE,0xF9,
0x05,0x32,0xF9,0x01,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,
0xFB,0xEA,0xEC,0xEE,0xF0,0x86,0xFB,0xFF,0xFF,0xA0,0x19,0xF9,
0x01,0xFB,0x04,0xBF,0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xF2,0x03,
0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCD,0xCD,0xF3,0x01,
0xCD,0xCD,0xC9,0xC6,0xFB,0xFC,0xFF,0xA0,0x19,0xF9,0x05,0xF4,
0xFB,0x01,0x6B,0xFB,0x02,0x6C,0xF9,0x01,0x6D,0xFF,0x36,0x20,
0xF9,0x01,0xFA,0xFE,0xFB,0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFB,
0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,0x01,0x72,
0xFA,0x01,0x73,0xFB,0xFE,0xFF,0x78,0x19,0xF9,0x05,0xF4,0xFA,
0xFE,0xFB,0x01,0xFB,0x02,0x6C,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,
0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,
0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0xFB,
0x01,0x6E,0x6E,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,
0x01,0x72,0xFA,0x01,0x73,0x74,0xFB,0xFC,0x75,0x76,0x77,0xFF,
0xA0,0x19,0xF9,0x05,0xF4,0xFA,0xFE,0xFB,0x03,0xB9,0xF6,0xB9,
0xFF,0xA5,0x20,0xF9,0x05,0xFF,0xAE,0x20,0xF9,0x01,0xF6,0xB3,
0xB4,0xB5,0xB6,0xFB,0x01,0xB7,0xFB,0xFC,0xB9,0xFF,0xBA,0x20,
0xF9,0x01,0xF4,0xFB,0x04,0xB1,0xF6,0xB1,0xFF,0xC5,0x20,0xF9,
0x01,0xB2,0xF6,0xB2,0xFF,0xCD,0x20,0xFF,0x5A,0x20,0xF8,0x00,
0x00,0xF9,0x04,0xB9,0xFF,0xD9,0x20,0xF9,0x05,0xFB,0xFB,0xFA,
0xFF,0xF2,0x01,0xD9,0xDA,0xDB,0xFB,0x01,0xDC,0xFB,0xFC,0xFA,
0xFD,0xF2,0x01,0xDD,0xFB,0xFC,0xFA,0xFE,0xDE,0xFB,0xFE,0xFA,
0xFD,0xDF,0xFB,0xFD,0xFA,0xF8,0xF2,0x04,0xF2,0x01,0xE0,0xFB,
0xFF,0xFA,0xFF,0xE1,0xFB,0xFD,0xFA,0xFC,0xE2,0xFB,0xFF,0xFA,
0xFB,0xF2,0x01,0xE3,0xFB,0xFE,0xFA,0xFF,0xE4,0x00,0xF2,0x01,
0x00,0x00,0x00,0xF2,0x01,0x00,0x00,0x00,0xF2,0x01,0x00,0x00,
0x00,0xF2,0x01,0xF1,0x00,0xFF,0x2E,0x21,0x36,0xFF,0x32,0x21,
0x55,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x44,
0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x53,0x54,0x4C,0xFF,0x4B,
0x21,0xFB,0x01,0x30,0xFB,0x02,0x31,0xFB,0x06,0x32,0xFB,0x01,
0x33,0xFB,0xFF,0x34,0xFB,0x01,0x35,0xFB,0x01,0xFF,0x51,0x21,
0xFB,0x01,0x37,0x38,0xFF,0x32,0x21,0x4D,0x4E,0x4F,0x50,0x51,
0x52,0xFB,0x01,0x36,0x36,0x36,0x36,0x36,0x36,0x39,0x3A,0x3B,
0x3C,0x3D,0xFB,0x02,0x3E,0xFB,0xFF,0x3F,0xFB,0xFD,0x40,0x41,
0xFB,0xFF,0x42,0xFE,0xFB,0x10,0xFB,0x03,0xFF,0x54,0x21,0x0B,
0xFF,0x95,0x21,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFE,
0xFB,0x08,0x0B,0xFF,0x95,0x21,0xFE,0xFB,0x0B,0x0C,0xFB,0x01,
0x0D,0xFB,0x01,0x0E,0xFB,0x03,0x0F,0xFB,0x01,0x10,0x11,0xFF,
0xB8,0x21,0x13,0xFF,0xBC,0x21,0x14,0xFF,0xC0,0x21,0x15,0xFB,
0x01,0x16,0x17,0x18,0xFB,0x01,0x19,0xFB,0xFD,0x1A,0xFB,0xFE,
0x1B,0xFB,0xFC,0x1C,0xFB,0xFD,0x1D,0xFB,0xFE,0x1E,0xFB,0xFD,
0x1F,0xFB,0xFF,0x20,0x21,0xFF,0xE2,0x21,0x25,0xFF,0xE6,0x21,
0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0xFE,
0xFB,0x0C,0x0B,0xFF,0xF8,0x21,0x0B,0x0B,0xFE,0xFB,0x0D,0x2F,
0x2E,0x2D,0x2C,0x2B,0x2A,0x29,0x28,0x27,0x26,0x25,0x24,0x24,
0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,
0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,
0x22,0x22,0x23,0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,
0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,0x23,0x23,0x24,0xFF,
0x3C,0x22,0x01,0x12,0xFF,0x41,0x22,0xF9,0x01,0xBA,0xFB,0x05,
0xBA,0xFB,0x03,0xBB,0xFB,0x04,0xFF,0x47,0x22,0xBA,0xFF,0x53,
0x22,0xBC,0xFF,0x57,0x22,0xF9,0x00,0xBA,0xBA,0xBA,0xBC,0xBC,
0xBC,0xBC,0xBC,0xBC,0xBC,0xBC,0xFE,0xFB,0x08,0xFF,0x47,0x22,
0xBA,0xBA,0xBC,0xFF,0x70,0x22,
};
#define KID_NSEQ 115 #define KID_NSEQ 115
static const uint16_t kid_seq_off[KID_NSEQ] = { static const uint16_t kid_seq_off[KID_NSEQ] = {
@@ -471,4 +42,31 @@ static const uint16_t kid_seq_off[KID_NSEQ] = {
0x21FC,0x2240,0x2257, 0x21FC,0x2240,0x2257,
}; };
/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.
* Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.
* Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),
* x — «вперёд» по направлению персонажа. */
#define KID_NSWORD 51
#define KID_SWORD_ID_INIT { \
0xFF,0x00,0x05,0x01,0x02,0x06,0x07,0x03,0x04,0x1E,0x08,0x09, \
0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0x13,0x11, \
0x11,0x07,0x01,0x1C,0x08,0x04,0x04,0x0A,0x0D,0x0D,0x0D,0x14, \
0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0xFF,0xFF,0x1D,0x08,0x1F, \
0x20,0x21,0x08, \
}
#define KID_SWORD_DX_INIT { \
0,0,-9,7,17,7,0,17,16,12,13,7, \
10,10,22,28,13,0,0,21,14,21,22,7, \
15,0,7,14,7,6,9,11,24,19,21,7, \
14,14,14,28,28,21,14,14,21,0,8,14, \
14,7,8, \
}
#define KID_SWORD_DY_INIT { \
0,-9,-29,-25,-26,-14,-5,-16,-19,-9,-34,-25, \
-16,-11,-21,-23,-35,-38,-29,-19,-23,-22,-23,-13, \
-18,-8,-27,-28,-27,-23,-21,-18,-23,-23,-23,-32, \
-32,-31,-29,-28,-28,-25,-22,-25,-25,-16,-37,-24, \
-24,-14,-37, \
}
#endif #endif
+280 -98
View File
@@ -14,6 +14,8 @@
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include "pop_bg.h" #include "pop_bg.h"
#include "pop_state.h"
#include "pop_geom.h"
#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */ #include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */ #include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */
@@ -24,22 +26,25 @@ static atlas_t fore_at;
static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */ static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */
static const char *const ENV_ATL[5] = { static const char *const ENV_ATL[5] = {
"pop_env0.atl", "pop_env1.atl", "pop_env2.atl", /* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон,
"pop_env3.atl", "pop_env4.atl" * KID — персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска
* зарастает десятками .atl. */
"BG\\pop_env0.atl", "BG\\pop_env1.atl", "BG\\pop_env2.atl",
"BG\\pop_env3.atl", "BG\\pop_env4.atl"
}; };
int pop_bg_load(void) int pop_bg_load(void) __banked
{ {
int i; int i;
for (i = 0; i < 5; i++) for (i = 0; i < 5; i++)
if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1; if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1;
if (atlas_load(&wall_at, "pop_wall.atl") != 0) return -1; if (atlas_load(&wall_at, "BG\\pop_wall.atl") != 0) return -1;
if (atlas_load(&fore_at, "pop_fore.atl") != 0) return -1; if (atlas_load(&fore_at, "BG\\pop_fore.atl") != 0) return -1;
if (atlas_load(&pot_at, "pop_pot.atl") != 0) return -1; if (atlas_load(&pot_at, "BG\\pop_pot.atl") != 0) return -1;
return 0; return 0;
} }
void pop_bg_free(void) void pop_bg_free(void) __banked
{ {
int i; int i;
for (i = 0; i < 5; i++) for (i = 0; i < 5; i++)
@@ -58,33 +63,73 @@ static int g_clip_top;
/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x, /* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x,
* прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0). * прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0).
* При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */ * При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */
/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал). Пока оно включено,
* blit_b рисует только ту часть куска, что попадает в окно.
*
* Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов
* поверх нарисованного персонажа — за пределами его прямоугольника в
* видеопамяти и так лежит правильный фон. А рисовал он тайлы ЦЕЛИКОМ:
* стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход
* до девяти. Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход
* поверх Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей
* работы кадра при бюджете 430 000 тактов на кадр. С окном большинство
* кусков отсекается целиком, остальные режутся до пересечения. */
static int fclip_x0, fclip_y0, fclip_x1, fclip_y1;
static uint8_t fclip_on;
void pop_fore_set_clip(int x, int y, int w, int h) __banked
{
fclip_x0 = x; fclip_y0 = y;
fclip_x1 = x + w; fclip_y1 = y + h;
}
static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
{ {
const uint8_t *img; const uint8_t *img;
uint16_t w, h; uint16_t w, h;
int top; int top, dx, dy, dw, dh, sx = 0, sy = 0;
if (idx >= a->count) if (idx >= a->count)
return; return;
/* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке
* ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать
* gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее
* 64×64) и, если он не задевает окно, уходим не заплатив ни за
* atlas_image, ни за map/unmap. */
if (fclip_on &&
(x >= fclip_x1 || x + 64 <= fclip_x0 ||
ybottom + POP_YOFF + 1 <= fclip_y0 ||
ybottom + POP_YOFF - 64 >= fclip_y1))
return;
img = (const uint8_t *)atlas_image(a, idx); img = (const uint8_t *)atlas_image(a, idx);
gfx_w0_map(a->page); gfx_w0_map(a->page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */ top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */
if (w && h) { dx = x; dy = top; dw = (int)w; dh = (int)h;
if (g_clip_top && top < g_clip_top) { if (g_clip_top && dy < g_clip_top) { /* полоса у потолка */
int sy = g_clip_top - top; /* пропустить верхние строки */ sy = g_clip_top - dy; dy = g_clip_top; dh -= sy;
if (sy < (int)h) }
gfx_blit_part(x, g_clip_top, img, 0, sy, (int)w, (int)h - sy); if (fclip_on) { /* окно fore-слоя */
} else if (x >= 0 && top >= 0 && w < 256 && h < 256 && if (dx < fclip_x0) { int d = fclip_x0 - dx; sx += d; dx += d; dw -= d; }
x + (int)w <= 320 && top + (int)h <= 256) { if (dy < fclip_y0) { int d = fclip_y0 - dy; sy += d; dy += d; dh -= d; }
/* Спрайт целиком на экране — линейное ядро без клипа: ~2.9× if (dx + dw > fclip_x1) dw = fclip_x1 - dx;
* быстрее (замер: 4617 против 13288 тактов на спрайт 32×3; if (dy + dh > fclip_y1) dh = fclip_y1 - dy;
* общее ядро gfx_blit платит за клип/16-бит/split, а не за }
* пиксели). W3-скобку ставит сама libbgi: из кода, собранного if (w && h && dw > 0 && dh > 0) {
* с --w3, её вызывать нельзя (окно W3 уходит под видеобанк). */ /* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее
gfx_blit_noclip(x, top, img); * (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро
* gfx_blit платит за клип/16-бит/split, а не за пиксели).
* W3-скобку ставит сама libbgi: из кода, собранного с --w3, её
* вызывать нельзя (окно W3 уходит под видеобанк). */
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256) {
if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h)
gfx_blit_noclip(dx, dy, img);
else
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
(uint8_t)dw, (uint8_t)dh);
} else { } else {
gfx_blit(x, top, img); /* край экрана — с клипом */ gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */
} }
} }
gfx_w0_unmap(); gfx_w0_unmap();
@@ -125,7 +170,7 @@ static void ov_mark(int x, int y, int w, int h)
ov_pending = 3; /* обе страницы */ ov_pending = 3; /* обе страницы */
} }
void pop_fore_heal(void) void pop_fore_heal(void) __banked
{ {
uint8_t pg = gfx_get_draw_page() & 1; uint8_t pg = gfx_get_draw_page() & 1;
if (!(ov_pending & (1 << pg))) return; if (!(ov_pending & (1 << pg))) return;
@@ -249,12 +294,12 @@ static uint8_t g_room;
* floorright, когда тайл нижнего ряда стал видимым (empty после падения * floorright, когда тайл нижнего ряда стал видимым (empty после падения
* loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты * loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты
* снизу (кромка уровня = стена). */ * снизу (кромка уровня = стена). */
void pop_room_set_below(const uint8_t *below_row0_fg) { g_below = below_row0_fg; } void pop_room_set_below(const uint8_t *below_row0_fg) __banked { g_below = below_row0_fg; }
/* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки /* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки
* у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom * у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */ * при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
{ g_above = above_row2_fg; g_abovemod = above_row2_mod; } { g_above = above_row2_fg; g_abovemod = above_row2_mod; }
static uint8_t tile_code(int row, int col) static uint8_t tile_code(int row, int col)
@@ -309,13 +354,9 @@ static uint8_t wall_modifier(int row, int col)
} }
/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */ /* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */
static unsigned long rnd_seed; static pop_rnd_t rnd_seed;
static uint16_t prandom(uint16_t maxv) #define prandom(maxv) pop_prandom(&rnd_seed, (maxv))
{
rnd_seed = rnd_seed * 214013UL + 2531011UL;
return (uint16_t)((uint16_t)(rnd_seed >> 16) % (uint16_t)(maxv + 1));
}
/* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */ /* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */
static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col) static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col)
@@ -338,65 +379,135 @@ static void draw_right_mark(uint16_t dv, int arg1, int row, int col)
wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]); wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]);
} }
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
/* КЭШ РАСКЛАДКИ КЛАДКИ. Серия prandom для тайла зависит только от
* (комната, ряд, колонка) — пока мы в комнате, это КОНСТАНТА. А
* wall_pattern зовётся из fore-слоя КАЖДЫЙ кадр, и на Z80 это самая
* дорогая вещь в кадре: замер в MAME 2026-07-29 (комната 3) — 18 вызовов
* pop_prandom за один fore-проход, ~10 700 тактов каждый (32-битный LCG:
* __mullong ~8 000 + __moduint), то есть 193 К из 341 К всего прохода при
* бюджете кадра ~430 К. Кэшируем не сид, а ГОТОВЫЕ РЕШЕНИЯ (что рисовать
* и с каким смещением) — тогда в кадре не остаётся ни одного prandom.
*
* Порядок вызовов prandom воспроизведён один в один, включая то, что
* значение метки берётся ТОЛЬКО когда сработало её условие: иначе
* последовательность разъедется и кладка станет другой. which_part=0
* использует префикс той же серии, поэтому кэш общий.
*
* Упаковка: wp_a = mid_div | mid_off<<1 | bot_div<<4 | bot_off<<5;
* wp_b = rndblk | right<<1 | right_val<<2 | left<<4 | left_val<<5.
* Индекс — (row+1)*10+col, ряды 1..2. */
#define WP_N 40
static uint8_t wp_a[WP_N], wp_b[WP_N], wp_ok[WP_N];
static void wall_pattern_reset(void) /* смена комнаты — сид другой */
{
uint8_t i;
for (i = 0; i < WP_N; i++) wp_ok[i] = 0;
}
/* Выходы wall_rnd — в файловых статиках: возвращать девять значений
* через указатели на Z80 дороже самой функции. */
static uint8_t wr_mid_div, wr_mid_off, wr_bot_div, wr_bot_off;
static uint8_t wr_rndblk, wr_right, wr_rightv, wr_left, wr_leftv;
static void wall_rnd(int row, int col)
{
/* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
uint8_t a, b, bg;
pop_rnd_t saved;
if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
a = wp_a[idx]; b = wp_b[idx];
wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7);
wr_bot_div = (uint8_t)((a >> 4) & 1); wr_bot_off = (uint8_t)((a >> 5) & 7);
wr_rndblk = (uint8_t)(b & 1);
wr_right = (uint8_t)((b >> 1) & 1); wr_rightv = (uint8_t)((b >> 2) & 3);
wr_left = (uint8_t)((b >> 4) & 1); wr_leftv = (uint8_t)((b >> 5) & 7);
return;
}
saved = rnd_seed;
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col);
prandom(1); /* сброс */
wr_mid_div = (uint8_t)prandom(1);
wr_mid_off = (uint8_t)prandom(4);
wr_bot_div = (uint8_t)prandom(1);
wr_bot_off = (uint8_t)prandom(4);
wr_rndblk = wr_right = wr_left = 0;
wr_rightv = wr_leftv = 0;
bg = wall_modifier(row, col);
if (bg == 3) { /* WWW */
wr_rndblk = (uint8_t)(prandom(4) == 0);
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
} else if (bg == 0) { /* SWS */
if (prandom(6) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(1); }
} else if (bg == 1) { /* SWW */
wr_rndblk = (uint8_t)(prandom(4) == 0);
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(3); }
} else { /* WWS (bg==2) */
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)(prandom(1) + 2); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
}
rnd_seed = saved;
if (idx >= 0 && idx < WP_N) {
wp_a[idx] = (uint8_t)(wr_mid_div | (wr_mid_off << 1) |
(wr_bot_div << 4) | (wr_bot_off << 5));
wp_b[idx] = (uint8_t)(wr_rndblk | (wr_right << 1) | (wr_rightv << 2) |
(wr_left << 4) | (wr_leftv << 5));
wp_ok[idx] = 1;
}
}
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */ /* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
static void wall_pattern(int row, int col, int which_part) static void wall_pattern(int row, int col, int which_part)
{ {
int xh = COL_XH[col]; int xh = COL_XH[col];
int dby = 63 * row + 65; int dby = 63 * row + 65;
unsigned long saved = rnd_seed; uint8_t bg = wall_modifier(row, col);
uint16_t mid_div, mid_off, bot_div, bot_off;
uint8_t bg;
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
rnd_seed = (unsigned long)(g_room + TBL_LINE[row < 0 ? 2 : row] + col);
prandom(1); /* сброс */
mid_div = prandom(1);
mid_off = prandom(4);
bot_div = prandom(1);
bot_off = prandom(4);
bg = wall_modifier(row, col);
wall_rnd(row, col);
if (bg == 3) { /* WWW */ if (bg == 3) { /* WWW */
if (which_part) { if (which_part) {
if (prandom(4) == 0) if (wr_rndblk)
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
} }
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
if (which_part) { if (which_part) {
if (prandom(4) == 0) if (wr_right)
draw_right_mark(prandom(3), mid_off, row, col); draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (prandom(4) == 0) if (wr_left)
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col); draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
} }
} else if (bg == 0) { /* SWS */ } else if (bg == 0) { /* SWS */
if (which_part) { if (which_part && wr_left)
if (prandom(6) == 0) draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
draw_left_mark(prandom(1), mid_off - mid_div, bot_off - bot_div, row, col);
}
} else if (bg == 1) { /* SWW */ } else if (bg == 1) { /* SWW */
if (which_part) { if (which_part) {
if (prandom(4) == 0) if (wr_rndblk)
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
if (prandom(4) == 0) if (wr_right)
draw_right_mark(prandom(3), mid_off, row, col); draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (prandom(4) == 0) if (wr_left)
draw_left_mark(prandom(3), mid_off - mid_div, bot_off - bot_div, row, col); draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
} }
} else { /* WWS (bg==2) */ } else { /* WWS (bg==2) */
if (which_part) if (which_part)
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
if (which_part) { if (which_part) {
if (prandom(4) == 0) if (wr_right)
draw_right_mark((uint16_t)(prandom(1) + 2), mid_off, row, col); draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (prandom(4) == 0) if (wr_left)
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col); draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
} }
} }
rnd_seed = saved;
} }
/* topright тайла снизу-слева (seg008.c:410). */ /* topright тайла снизу-слева (seg008.c:410). */
@@ -636,7 +747,7 @@ static void draw_tile(int row, int col)
* Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать * Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать
* ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif * ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif
* внутри draw_tile). */ * внутри draw_tile). */
void pop_loose_shake_draw(int row, int col) void pop_loose_shake_draw(int row, int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
/* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх — /* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх —
@@ -658,7 +769,7 @@ void pop_loose_shake_draw(int row, int col)
* читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать * читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать
* на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile. * на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile.
* heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */ * heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */
void pop_spike_redraw(int row, int col) void pop_spike_redraw(int row, int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
/* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ /* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ
@@ -685,7 +796,7 @@ void pop_spike_redraw(int row, int col)
* room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake): * room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake):
* heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары * heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары
* оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */
void pop_room_redraw_seam_left(void) void pop_room_redraw_seam_left(void) __banked
{ {
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK); setfillstyle(SOLID_FILL, BLACK);
@@ -707,7 +818,7 @@ void pop_room_redraw_seam_left(void)
* после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */ * после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */
#define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */ #define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */
void pop_ceil_shake_draw(int col) void pop_ceil_shake_draw(int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */ heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */
@@ -719,7 +830,7 @@ void pop_ceil_shake_draw(int col)
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
void pop_ceil_bake_empty(int col) void pop_ceil_bake_empty(int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
@@ -742,7 +853,7 @@ void pop_ceil_bake_empty(int col)
* draw_main_y 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька * draw_main_y 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька
* potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y * potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y
* (pot_size<<2) 14 (seg008:668). */ * (pot_size<<2) 14 (seg008:668). */
void pop_torch_draw(int row, int col, uint8_t modif) void pop_torch_draw(int row, int col, uint8_t modif) __banked
{ {
/* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1 /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1
* (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env * (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env
@@ -759,7 +870,7 @@ void pop_torch_draw(int row, int col, uint8_t modif)
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
void pop_potion_draw(int row, int col, uint8_t modif) void pop_potion_draw(int row, int col, uint8_t modif) __banked
{ {
uint8_t type = potion_type(modif); uint8_t type = potion_type(modif);
int pot_size = (type >= 2 && type < 5) ? 1 : 0; int pot_size = (type >= 2 && type < 5) ? 1 : 0;
@@ -777,7 +888,7 @@ void pop_potion_draw(int row, int col, uint8_t modif)
* (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный +
* redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе
* в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */ * в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */
void pop_floor_bake(int row, int col) void pop_floor_bake(int row, int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
int yb = POP_YOFF + 63 * row; int yb = POP_YOFF + 63 * row;
@@ -795,13 +906,13 @@ void pop_floor_bake(int row, int col)
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
void pop_button_redraw(int row, int col) { pop_floor_bake(row, col); } void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); }
/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка, /* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка,
* см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет, * см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет,
* а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое. * а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое.
* Куски двери непрозрачные — wipe не нужен. */ * Куски двери непрозрачные — wipe не нужен. */
void pop_leveldoor_redraw(int row, int col) void pop_leveldoor_redraw(int row, int col) __banked
{ {
gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_set_bank(GFX_BANK_TRANSPARENT);
draw_tile(row, col); draw_tile(row, col);
@@ -814,7 +925,7 @@ void pop_leveldoor_redraw(int row, int col)
* рисовал loose-правый-край env_b(42); теперь пусто → чистая грань). * рисовал loose-правый-край env_b(42); теперь пусто → чистая грань).
* g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые * g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые
* пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */ * пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */
void pop_loose_bake_empty(int row, int col) void pop_loose_bake_empty(int row, int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
int xr = COL_XH[col + 1] * 8; int xr = COL_XH[col + 1] * 8;
@@ -877,7 +988,8 @@ static mob_t mobs[MOB_MAX];
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на /* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
* который он лёг → приложение/pop_map ставит debris + do_knock. */ * который он лёг → приложение/pop_map ставит debris + do_knock. */
uint8_t pop_loose_landed; /* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а
* писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */
/* y_loose_land / y_something (seg007): [row+1], row = 1..3. Ряд 1 — /* y_loose_land / y_something (seg007): [row+1], row = 1..3. Ряд 1 —
* плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный * плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный
@@ -886,7 +998,7 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25}; static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
#define MOB_Y_NONE 0x7FFF #define MOB_Y_NONE 0x7FFF
void pop_loose_mob_spawn(int row, int col) void pop_loose_mob_spawn(int row, int col) __banked
{ {
uint8_t i; uint8_t i;
mob_t *m = 0; mob_t *m = 0;
@@ -923,7 +1035,7 @@ static int8_t obj_col, obj_row;
static void mob_render(mob_t *m, uint8_t pg); static void mob_render(mob_t *m, uint8_t pg);
void pop_loose_mob_reset(void) void pop_loose_mob_reset(void) __banked
{ {
uint8_t i; uint8_t i;
for (i = 0; i < MOB_MAX; i++) { for (i = 0; i < MOB_MAX; i++) {
@@ -988,7 +1100,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
mob_render(m, pg); mob_render(m, pg);
} }
void pop_loose_mob_tick(void) void pop_loose_mob_tick(void) __banked
{ {
uint8_t pg = gfx_get_draw_page() & 1, i; uint8_t pg = gfx_get_draw_page() & 1, i;
for (i = 0; i < MOB_MAX; i++) for (i = 0; i < MOB_MAX; i++)
@@ -1038,7 +1150,7 @@ static void mob_render(mob_t *m, uint8_t pg)
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать /* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
* после kid_draw, до fore-слоя. */ * после kid_draw, до fore-слоя. */
void pop_loose_mob_draw_over(void) void pop_loose_mob_draw_over(void) __banked
{ {
uint8_t pg = gfx_get_draw_page() & 1, i; uint8_t pg = gfx_get_draw_page() & 1, i;
for (i = 0; i < MOB_MAX; i++) for (i = 0; i < MOB_MAX; i++)
@@ -1047,7 +1159,7 @@ void pop_loose_mob_draw_over(void)
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и /* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
* y (низ куска). 0 — куска нет. */ * y (низ куска). 0 — куска нет. */
uint8_t pop_loose_mob_pos(int *col, int *y) uint8_t pop_loose_mob_pos(int *col, int *y) __banked
{ {
uint8_t i; uint8_t i;
for (i = 0; i < MOB_MAX; i++) for (i = 0; i < MOB_MAX; i++)
@@ -1062,7 +1174,7 @@ uint8_t pop_loose_mob_pos(int *col, int *y)
* кадрах падения — гейт по флагу). */ * кадрах падения — гейт по флагу). */
static uint8_t border_dirty; static uint8_t border_dirty;
void pop_clip_sprite(int x, int w, int sy_top, int h) void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
{ {
int sy_bot = sy_top + h; int sy_bot = sy_top + h;
(void)x; (void)w; (void)x; (void)w;
@@ -1074,7 +1186,7 @@ void pop_clip_sprite(int x, int w, int sy_top, int h)
* вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает * вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает
* чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу * чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу
* + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */ * + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */
void pop_room_clip_borders(void) void pop_room_clip_borders(void) __banked
{ {
if (!border_dirty) return; if (!border_dirty) return;
border_dirty--; border_dirty--;
@@ -1084,10 +1196,11 @@ void pop_room_clip_borders(void)
void pop_room_draw(uint8_t room_num, void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg, const uint8_t *fg, const uint8_t *bg,
const uint8_t *lcol_fg, const uint8_t *lcol_bg) const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked
{ {
int row, col; int row, col;
g_room = room_num; g_room = room_num;
wall_pattern_reset(); /* кэш кладки привязан к номеру комнаты */
g_fg = fg; g_bg = bg; g_fg = fg; g_bg = bg;
g_lfg = lcol_fg; g_lbg = lcol_bg; g_lfg = lcol_fg; g_lbg = lcol_bg;
@@ -1125,10 +1238,7 @@ static int8_t col_from_x(int xpos) /* get_tile_div_mod: колонка (
return (int8_t)xh; return (int8_t)xh;
} }
static int8_t y_to_row(int y) /* y_to_row_mod4 (seg006, TILE_SIZEY=63) */ #define y_to_row(y) pop_y_to_row((int16_t)(y)) /* общий (pop_geom) */
{
return (int8_t)((y + 60) / 63 % 4 - 1);
}
static void fore_only_tile(int row, int col) static void fore_only_tile(int row, int col)
{ {
@@ -1180,8 +1290,25 @@ static void ceil_over_kid_tile(int col)
fore_only_tile(-1, col); /* draw_tile_fore */ fore_only_tile(-1, col); /* draw_tile_fore */
} }
/* Пересекается ли габарит тайла с окном fore-слоя. Шаг тайла — 32 px
* (COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры
* и метки рисуются в ячейке правого соседа) и вверх на 42+высота кадра.
* Даёт отсев ЦЕЛОГО тайла ДО обращения к атласу: спрайт 27..40 px шириной
* задевает не все тайлы футпринта, а платили за каждый (atlas_image +
* gfx_w0_map + разбор заголовка на КАЖДЫЙ кусок). */
static uint8_t tile_in_fclip(int row, int col)
{
int x0, y0;
if (!fclip_on) return 1;
x0 = COL_XH[col] * 8;
y0 = POP_YOFF + 63 * row;
return (uint8_t)(x0 < fclip_x1 && x0 + 40 > fclip_x0 &&
y0 - 8 < fclip_y1 && y0 + 70 > fclip_y0);
}
static void fore_tile(int row, int col) /* передний слой одного тайла */ static void fore_tile(int row, int col) /* передний слой одного тайла */
{ {
if (!tile_in_fclip(row, col)) return;
/* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал /* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал
* кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в * кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в
* backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ * backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ
@@ -1214,7 +1341,9 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx)
{ {
/* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую /* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую
* Kid спускается/подтягивается, закрашивается целым тайлом */ * Kid спускается/подтягивается, закрашивается целым тайлом */
uint8_t code = tile_code_drawn(row, col); uint8_t code;
if (!tile_in_fclip(row, col)) return;
code = tile_code_drawn(row, col);
int x, dmy, dby; int x, dmy, dby;
const piece *t; const piece *t;
if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */ if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */
@@ -1304,8 +1433,9 @@ static void overlay_mid_tile(int row, int col)
* сразу), поэтому порядок эмулируется явной проверкой. */ * сразу), поэтому порядок эмулируется явной проверкой. */
static void other_overlay_tile(int row, int col) static void other_overlay_tile(int row, int col)
{ {
uint8_t code = tile_code(row, col); uint8_t code, left;
uint8_t left; if (!tile_in_fclip(row, col)) return;
code = tile_code(row, col);
/* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для /* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для
* него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань * него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань
* пустого соседа). В оригинале она уходит в y-сортируемый midtable и * пустого соседа). В оригинале она уходит в y-сортируемый midtable и
@@ -1328,13 +1458,20 @@ static void other_overlay_tile(int row, int col)
} }
} }
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, /* Футпринт персонажа в тайлах (порт set_char_collision, seg006:0723).
int8_t dir, uint8_t frame, uint8_t action) * Выделено из pop_fore_over_kid, потому что тем же счётом пользуется страж
* (pop_fore_over_char): в оригинале это общий код для любого Char.
* Результат — в файловых статиках, а не через 6 указателей: на Z80 такой
* возврат стоил бы дороже самой функции. */
static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw;
static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir)
{ {
int wh = (int)((w + 1) >> 1); /* char_width_half */ int wh = (int)((w + 1) >> 1); /* char_width_half */
int xl = obj_x / 2 + 58; /* char_x_left (58-база) */ int xl = obj_x / 2 + 58; /* char_x_left (58-база) */
int xr; int xr;
int8_t cL, cR, cLraw, rT, rB, r, c, rTraw; int8_t cL, cR, cLraw, rT, rB, rTraw;
if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; xr = xl + wh;
cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */ cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */
@@ -1362,6 +1499,18 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
* (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим * (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим
* включение ряда rB-1. */ * включение ряда rB-1. */
if (rB > 0 && rT >= rB) rT = rB - 1; if (rB > 0 && rT >= rB) rT = rB - 1;
fp_cL = cL; fp_cR = cR; fp_cLraw = cLraw;
fp_rT = rT; fp_rB = rB; fp_rTraw = rTraw;
}
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir, uint8_t frame, uint8_t action) __banked
{
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir);
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт /* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
* Kid попадает в midtable — база порядка окклюзии в other_overlay_tile. * Kid попадает в midtable — база порядка окклюзии в other_overlay_tile.
* run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/ * run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/
@@ -1429,4 +1578,37 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
g_clip_top = 0; g_clip_top = 0;
} }
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
fclip_on = 0;
}
/* Передние грани тайлов поверх ПРОИЗВОЛЬНОГО персонажа (страж, дальше —
* тень/скелет). В оригинале любой Char — запись midtable, а foretable
* рисуется после всех midtable (draw_tile_fore, seg008:690), поэтому
* колонна и кромка пола перекрывают всех одинаково.
*
* Отличие от pop_fore_over_kid: только слой fore (+ полоса потолка).
* Оверлеи тела пола (draw_other_overlay) завязаны на позы Kid — вис,
* полёт, подъём, — которых у стража нет: он ходит и дерётся на полу
* (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий
* порт redraw_at_char2, а не догадки сейчас. */
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir) __banked
{
int8_t r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir);
gfx_set_bank(GFX_BANK_SPRITE);
for (r = fp_rT; r <= fp_rB; r++)
for (c = fp_cL; c <= fp_cR; c++)
fore_tile(r, c);
if (fp_rTraw < 0) {
g_clip_top = POP_YOFF;
for (c = fp_cL; c <= fp_cR; c++) {
ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H);
ceil_over_kid_tile(c);
}
g_clip_top = 0;
}
gfx_set_bank(GFX_BANK_NORMAL);
fclip_on = 0;
} }
+58 -28
View File
@@ -25,13 +25,26 @@
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */ /* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H) #define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip-
* примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий
* вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от
* того, вылезает край или нет.
* static inline, а не чистый inline: SDCC оставляет мёртвое тело в каждом TU
* (memory sdcc_inline_codegen_findings), но здесь это ~30 байт на два TU —
* дешевле риска неразрешённой ссылки. */
static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h)
{
return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 &&
x + (int)w <= 320 && y + (int)h <= 256);
}
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load /* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ * трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */ * после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
int pop_bg_load(void); int pop_bg_load(void) __banked;
/* Освободить EMM-страницы атласов. */ /* Освободить EMM-страницы атласов. */
void pop_bg_free(void); void pop_bg_free(void) __banked;
/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы /* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы
* (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца * (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца
@@ -39,97 +52,114 @@ void pop_bg_free(void);
* тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */ * тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */
void pop_room_draw(uint8_t room_num, void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg, const uint8_t *fg, const uint8_t *bg,
const uint8_t *lcol_fg, const uint8_t *lcol_bg); const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked;
/* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright, /* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright,
* когда тайл нижнего ряда стал empty (после падения loose): «стена вниз» * когда тайл нижнего ряда стал empty (после падения loose): «стена вниз»
* рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */ * рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */
void pop_room_set_below(const uint8_t *below_row0_fg); void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы /* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет * кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
* комнаты сверху (кромка уровня → пол). */ * комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod); void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него — /* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
* звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ * звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
* спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики * спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики
* берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */ * берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir, uint8_t frame, uint8_t action); int8_t dir, uint8_t frame, uint8_t action) __banked;
/* Окно fore-слоя: прямоугольник СПРАЙТА (экранные координаты, y уже с
* POP_YOFF), поверх которого fore-проход возвращает куски тайлов. Ставит
* тот, кто нарисовал спрайт (kid_draw / pop_guard_draw) — он один знает
* итоговый габарит (у Kid — с учётом клинка и обрезки clip_char). Вне
* fore-проходов окно игнорируется: фон рисуется целиком. Мотивация и
* замер — в pop_bg.c у fclip_on. */
void pop_fore_set_clip(int x, int y, int w, int h) __banked;
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
* без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата
* (до ×8/7), как у kid_fp_obj_x(). */
/* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому
* вызов стал кроссбанковым и обязан идти через трамплин. Зовётся раз в
* кадр, других вызывающих нет — цена одного трамплина здесь не видна. */
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir) __banked;
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ /* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за * (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
* прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице * прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */ * дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
void pop_fore_heal(void); void pop_fore_heal(void) __banked;
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в /* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick. * pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
* shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту * shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту
* после падения (NORMAL, в ОЗУ-копию). */ * после падения (NORMAL, в ОЗУ-копию). */
void pop_loose_shake_draw(int row, int col); void pop_loose_shake_draw(int row, int col) __banked;
void pop_loose_bake_empty(int row, int col); void pop_loose_bake_empty(int row, int col) __banked;
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в /* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала. * полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
* Состояние — pop_ceil_modif[10] в pop_map. */ * Состояние — pop_ceil_modif[10] в pop_map. */
void pop_ceil_shake_draw(int col); void pop_ceil_shake_draw(int col) __banked;
void pop_ceil_bake_empty(int col); void pop_ceil_bake_empty(int col) __banked;
/* Кусок приземлился (loose_land): 0 = нет, иначе tilepos+1 тайла, на который /* Кусок приземлился (loose_land) — переехал в pop_state.h: переменную
* он лёг. Ставит pop_loose_mob_tick, разбирает pop_loose_tick (debris+knock). */ * читают по обе стороны банковой границы. */
extern uint8_t pop_loose_landed; #include "pop_state.h"
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому /* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */ * modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
void pop_spike_redraw(int row, int col); void pop_spike_redraw(int row, int col) __banked;
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них): /* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела * modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
* (пламя рисуется в ячейке правого соседа). */ * (пламя рисуется в ячейке правого соседа). */
void pop_torch_draw(int row, int col, uint8_t modif); void pop_torch_draw(int row, int col, uint8_t modif) __banked;
void pop_potion_draw(int row, int col, uint8_t modif); void pop_potion_draw(int row, int col, uint8_t modif) __banked;
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева. /* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла * g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */ * на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
void pop_room_redraw_seam_left(void); void pop_room_redraw_seam_left(void) __banked;
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице /* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте: * (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
* кнопка нажата/отжата, floor → debris от упавшей плиты. */ * кнопка нажата/отжата, floor → debris от упавшей плиты. */
void pop_floor_bake(int row, int col); void pop_floor_bake(int row, int col) __banked;
void pop_button_redraw(int row, int col); /* = pop_floor_bake (кнопка) */ void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кнопка) */
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с /* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
* запечкой — створка анимируется (pop_trob animate_leveldoor). */ * запечкой — створка анимируется (pop_trob animate_leveldoor). */
void pop_leveldoor_redraw(int row, int col); void pop_leveldoor_redraw(int row, int col) __banked;
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла /* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick * (row,col) и покадровое продвижение+отрисовка на back-странице. tick
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */ * зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
void pop_loose_mob_spawn(int row, int col); void pop_loose_mob_spawn(int row, int col) __banked;
void pop_loose_mob_tick(void); void pop_loose_mob_tick(void) __banked;
void pop_loose_mob_reset(void); /* остановить кусок (смена комнаты) */ void pop_loose_mob_reset(void) __banked; /* остановить кусок (смена комнаты) */
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs, /* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
* seg008:1561). Звать после kid_draw, перед fore-слоем. */ * seg008:1561). Звать после kid_draw, перед fore-слоем. */
void pop_loose_mob_draw_over(void); void pop_loose_mob_draw_over(void) __banked;
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */ /* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
uint8_t pop_loose_mob_pos(int *col, int *y); uint8_t pop_loose_mob_pos(int *col, int *y) __banked;
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом /* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше * sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра. * верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */ * Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
void pop_clip_sprite(int x, int w, int sy_top, int h); void pop_clip_sprite(int x, int w, int sy_top, int h) __banked;
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах, /* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite): * где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать * даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */ * КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
void pop_room_clip_borders(void); void pop_room_clip_borders(void) __banked;
#endif #endif
+32
View File
@@ -0,0 +1,32 @@
/*
* pop_char.h — состояние ПЕРСОНАЖА, общее для Кида, стража и активной
* копии Char. Порт char_type (types.h:302).
*
* Зачем отдельным заголовком: интерпретатор последовательностей в
* оригинале один на всех (работает с Char, а вокруг стоят
* loadkid/savekid и loadshad/saveshad, seg006:809). Пока у Кида был свой
* усечённый тип, а у стража свой, общий интерпретатор был невозможен.
* Поля и их порядок — как в char_type, чтобы порт seg005/seg006 ложился
* один в один.
*/
#ifndef POP_CHAR_H
#define POP_CHAR_H
#include <stdint.h>
typedef struct {
uint8_t frame;
uint8_t x, y;
int8_t direction; /* -1 влево, 0 вправо */
int8_t curr_col, curr_row;
uint8_t action;
int8_t fall_x, fall_y;
uint8_t room;
uint8_t repeat;
uint8_t charid; /* 0 = Kid, 2 = страж (см. CHARID_* в pop_guard.h) */
uint8_t sword;
int8_t alive; /* -1 = жив (как в оригинале), 0 = убит */
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
} pop_char_t;
#endif
+23
View File
@@ -0,0 +1,23 @@
/*
* pop_cheat.h — режим читов (порт cheats_enabled, seg000:111).
*
* В оригинале включается ключом командной строки megahit/debug и меняет
* набор доступных клавиш. У нас конфигурации/UI пока нет, поэтому это
* просто глобальный флаг: main ставит его при старте (на время разработки —
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
*
* Реализовано: K — убить стража (seg000:786). Остальные читы оригинала
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
*/
#ifndef POP_CHEAT_H
#define POP_CHEAT_H
#include <stdint.h>
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */
#endif
+260 -19
View File
@@ -19,6 +19,7 @@
#include <kbd_raw.h> #include <kbd_raw.h>
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_map.h" #include "pop_map.h"
#include "pop_guard.h" /* charid, состояние меча, seq стража */
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в /* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2 * MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
@@ -51,6 +52,7 @@
#define SEQ_49_STAND_UP_FROM_CROUCH 49 #define SEQ_49_STAND_UP_FROM_CROUCH 49
#define SEQ_50_CROUCH 50 #define SEQ_50_CROUCH 50
#define SEQ_79_CROUCH_HOP 79 #define SEQ_79_CROUCH_HOP 79
#define SEQ_78_DRINK 78 /* выпить зелье */
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */ #define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */ #define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */ #define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
@@ -74,14 +76,21 @@
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный * X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный
* автомат held/released/ignore (авто-повтор); ctrl1_* — их память между * автомат held/released/ignore (авто-повтор); ctrl1_* — их память между
* кадрами (rest/save). */ * кадрами (rest/save). */
static int8_t control_x, control_y, control_shift; int8_t control_x, control_y, control_shift;
static int8_t control_forward, control_backward, control_up, control_down, control_shift2; /* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт
* СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же
* control(), что и у игрока. Поэтому шина общая. */
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2; static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ * /* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
* Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl * Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl
* не дублируются в этот TU). */ * не дублируются в этот TU). */
#define seqtbl_offset_char(seq) kid_set_seq(seq) /* В оригинале seqtbl_offset_char работает с АКТИВНЫМ персонажем, и
* control() вызывается внутри окна Char. Раньше макрос вёл на
* kid_set_seq (пишет прямо в Kid) — после введения окна это ломало ввод:
* savekid затирал Kid копией Char со старой curr_seq. */
#define seqtbl_offset_char(seq) pop_char_set_seq(seq)
static int8_t release_arrows(void) static int8_t release_arrows(void)
{ {
@@ -118,6 +127,11 @@ static uint8_t get_item_action(void)
seqtbl_offset_char(SEQ_91_PICKUP_SWORD); seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
return 1; return 1;
} }
if (a == 3) { /* зелье — выпить (get_item) */
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_78_DRINK);
return 1;
}
return 0; return 0;
} }
@@ -125,13 +139,14 @@ static void run_jump(void)
{ {
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это /* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
* полировка K3; K2b просто запускает run-jump. */ * полировка K3; K2b просто запускает run-jump. */
if (Kid.frame >= FRAME_7_RUN) { if (Char.frame >= FRAME_7_RUN) {
control_up = release_arrows(); control_up = release_arrows();
seqtbl_offset_char(SEQ_4_RUN_JUMP); seqtbl_offset_char(SEQ_4_RUN_JUMP);
} }
} }
static void safe_step(void); /* fwd */ static void safe_step(void); /* fwd */
static void draw_sword(void); /* fwd: нужен в control_standing */
static void forward_pressed(void) static void forward_pressed(void)
{ {
@@ -190,15 +205,15 @@ static void safe_step(void)
control_forward = CONTROL_IGNORE; control_forward = CONTROL_IGNORE;
distance = pop_edge_distance(); distance = pop_edge_distance();
if (distance) { if (distance) {
Kid.repeat = 1; Char.repeat = 1;
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE)); seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
} else if (pop_edge_type() == EDGE_WALL) { } else if (pop_edge_type() == EDGE_WALL) {
/* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад /* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад
* (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */ * (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */
Kid.repeat = 1; Char.repeat = 1;
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE)); seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
} else if (Kid.repeat != 0) { } else if (Char.repeat != 0) {
Kid.repeat = 0; Char.repeat = 0;
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE); seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
} else { } else {
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */ seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
@@ -225,7 +240,32 @@ static void control_standing(void)
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD && if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
get_item_action()) get_item_action())
return; return;
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */ /* Ветку «charid != kid && вниз+вперёд -> draw_sword» не портируем: у нас
* соперник до control_standing не доходит, его перехватывает
* control_guard_inactive в самом диспетчере (порядок как в seg005). */
/* seg005:352 — Кид с мечом ВИДИТ активного стража: достаёт клинок сам.
* Без этого игрок стоит с мечом в ножнах, пока его рубят. */
if (pop_have_sword) {
if (offguard != 0 && control_shift >= CONTROL_RELEASED) {
/* Меч убран намеренно (down в бою) — не доставать, пока не
* отпустят Shift; в оригинале это переход на loc_6213. */
} else if (can_guard_see_kid >= 2) {
int16_t d = pop_char_opp_dist();
if (d >= -10 && d < 90) {
holding_sword = 1;
/* (word)d < (word)-6 — беззнаковое: истинно для ВСЕГО, кроме
* −6..−1. То есть «страж вплотную за спиной» -> отступить,
* иначе достать меч. */
if (d >= -6 && d < 0) { back_pressed(); return; }
draw_sword();
return;
}
} else {
offguard = 0;
}
}
if (control_shift == CONTROL_HELD) { if (control_shift == CONTROL_HELD) {
if (control_backward == CONTROL_HELD) back_pressed(); if (control_backward == CONTROL_HELD) back_pressed();
else if (control_up == CONTROL_HELD) up_pressed(); else if (control_up == CONTROL_HELD) up_pressed();
@@ -266,7 +306,7 @@ static void control_running(void)
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок /* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
* seq_47 при беге в стену (как оригинал), с корректной позицией. */ * seq_47 при беге в стену (как оригинал), с корректной позицией. */
if (control_x == CONTROL_RELEASED && if (control_x == CONTROL_RELEASED &&
(Kid.frame == FRAME_7_RUN || Kid.frame == FRAME_11_RUN)) { (Char.frame == FRAME_7_RUN || Char.frame == FRAME_11_RUN)) {
control_forward = release_arrows(); control_forward = release_arrows();
seqtbl_offset_char(SEQ_13_STOP_RUN); seqtbl_offset_char(SEQ_13_STOP_RUN);
} else if (control_x == CONTROL_HELD_BACKWARD) { } else if (control_x == CONTROL_HELD_BACKWARD) {
@@ -317,21 +357,216 @@ static void control_jumpup(void)
standing_jump(); standing_jump();
} }
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */ /* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
static void control(void) * клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
* уже как выпад. */
static void draw_sword(void)
{ {
uint8_t f = Kid.frame; uint8_t seq_id = SEQ_55_DRAW_SWORD;
control_forward = control_shift2 = release_arrows();
if (Char.charid == CHARID_0_KID) {
offguard = 0; /* TODO: play_sound(19) — звука пока нет */
} else if (Char.charid != CHARID_1_SHADOW) {
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
}
Char.sword = SWORD_2_DRAWN;
seqtbl_offset_char(seq_id);
}
/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в
* кадре 166 (стойка) и только на «вниз»: вниз+вперёд — достать меч,
* вниз без «вперёд» — развернуться (stand flipped). */
static void control_guard_inactive(void)
{
if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) {
if (control_forward == CONTROL_HELD) {
draw_sword();
} else {
control_down = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_80_STAND_FLIPPED);
}
}
}
/* ---- Боёвка (seg005:964..) ---------------------------------------- *
* Всё это ветки ОБЩЕГО диспетчера: у стража тот же control(), просто
* control_* ему выставляет ИИ (autocontrol_*, guards.c), а не клавиатура. */
/* back_with_sword (seg005): шаг назад — только из стоек. */
static void back_with_sword(void)
{
uint8_t f = Char.frame;
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_backward = CONTROL_IGNORE; /* без авто-повтора */
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD);
}
}
/* forward_with_sword (seg005): шаг вперёд; у стража своя последовательность. */
static void forward_with_sword(void)
{
uint8_t f = Char.frame;
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_forward = CONTROL_IGNORE;
seqtbl_offset_char(Char.charid != CHARID_0_KID ? SEQ_56_GUARD_FORWARD
: SEQ_86_FORWARD_WITH_SWORD);
}
}
/* sword_strike (seg005): выпад из стойки/шага либо добивание после парирования. */
static void sword_strike(void)
{
uint8_t f = Char.frame;
uint8_t seq_id;
if (f == FRAME_157_WALK_WITH_SWORD || f == FRAME_158_STAND_WITH_SWORD ||
f == FRAME_170_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD ||
f == FRAME_165_WALK_WITH_SWORD) {
seq_id = (Char.charid == CHARID_0_KID) ? SEQ_75_STRIKE : SEQ_58_GUARD_STRIKE;
} else if (f == FRAME_150_PARRY || f == FRAME_161_PARRY) {
seq_id = SEQ_66_STRIKE_AFTER_PARRY;
} else {
return;
}
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(seq_id);
}
/* parry (seg005): блок. Условие сильно разное у Кида и у соперника —
* соперник блокирует ТОЛЬКО на кадре 152 (второй кадр удара), а Кид ещё и
* по 153 (и тогда последовательность прокручивается сразу). */
static void parry(void)
{
uint8_t cf = Char.frame, of = Opp.frame;
uint8_t seq_id = SEQ_62_PARRY;
uint8_t do_play_seq = 0;
if (cf == FRAME_158_STAND_WITH_SWORD || cf == FRAME_170_STAND_WITH_SWORD ||
cf == FRAME_171_STAND_WITH_SWORD || cf == FRAME_168_BACK ||
cf == FRAME_165_WALK_WITH_SWORD) {
if (pop_char_opp_dist() >= 32 && Char.charid != CHARID_0_KID) {
back_with_sword();
return;
} else if (Char.charid == CHARID_0_KID) {
if (of == FRAME_168_BACK) return;
if (of != FRAME_151_STRIKE_1 && of != FRAME_152_STRIKE_2 &&
of != FRAME_162_BLOCK_TO_STRIKE) {
if (of == FRAME_153_STRIKE_3) do_play_seq = 1;
}
} else {
if (of != FRAME_152_STRIKE_2) return;
}
} else {
if (cf != FRAME_167_BLOCKED) return;
seq_id = SEQ_61_PARRY_AFTER_STRIKE;
}
control_up = CONTROL_IGNORE;
seqtbl_offset_char(seq_id);
if (do_play_seq) play_seq();
}
/* swordfight (seg005:0CDB) — разбор ввода в бою. Порядок веток важен:
* удар старше «убрать меч», тот старше блока, блок старше шагов. */
static void swordfight(void)
{
uint8_t f = Char.frame;
uint8_t seq_id;
if (f == FRAME_161_PARRY && control_shift2 >= CONTROL_RELEASED) {
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD); /* отскок после парирования */
return;
} else if (control_shift2 == CONTROL_HELD) {
if (Char.charid == CHARID_0_KID) kid_sword_strike = 15;
sword_strike();
if (control_shift2 == CONTROL_IGNORE) return;
}
if (control_down == CONTROL_HELD) {
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_down = CONTROL_IGNORE;
Char.sword = SWORD_0_SHEATHED;
if (Char.charid == CHARID_0_KID) {
offguard = 1; /* убрал намеренно — сразу не доставать */
guard_refrac = 9; /* и страж на это время придержан */
holding_sword = 0;
seq_id = SEQ_93_PUT_SWORD_AWAY_FAST;
} else if (Char.charid == CHARID_1_SHADOW) {
seq_id = SEQ_92_PUT_SWORD_AWAY;
} else {
seq_id = SEQ_87_GUARD_BECOME_INACT; /* Кид ушёл из виду */
}
seqtbl_offset_char(seq_id);
}
} else if (control_up == CONTROL_HELD) {
parry();
} else if (control_forward == CONTROL_HELD) {
forward_with_sword();
} else if (control_backward == CONTROL_HELD) {
back_with_sword();
}
}
/* control_with_sword (seg005:964): пока соперник виден и близко — бой;
* иначе страж убирает меч, а Кид просто стоит. */
static void control_with_sword(void)
{
if (Char.action >= 2) return; /* actions_2_hang_climb и выше */
if (pop_tile_at(Char.curr_col, Char.curr_row) == 11 /* loose */ ||
can_guard_see_kid >= 2) {
int16_t d = pop_char_opp_dist();
if (d < 90) {
if (d >= 0) { swordfight(); return; }
/* Соперник за спиной: далеко — развернуться, вплотную — драться. */
if (d < -4) { seqtbl_offset_char(SEQ_60_TURN_WITH_SWORD); return; }
swordfight();
return;
}
}
/* Не видит / далеко. */
if (Char.charid < CHARID_2_GUARD) {
if (Char.frame == FRAME_171_STAND_WITH_SWORD) {
Char.sword = SWORD_0_SHEATHED;
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); /* страж убит */
}
} else {
swordfight();
}
}
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
void pop_control(void)
{
uint8_t f = Char.frame;
/* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0).
* Из любой стойки он валится в seq_71_dying; остальное управление в
* этом состоянии не обрабатывается. Через это же место проходит чит K
* (pop_guard_kill ставит Guard.alive = 0). */
if (Char.alive >= 0) {
if (f == FRAME_15_STAND || f == FRAME_166_STAND_INACTIVE ||
f == FRAME_158_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD)
seqtbl_offset_char(SEQ_71_DYING);
return;
}
/* seg005 control(): в отскоке (bumped) и в свободном падении управление /* seg005 control(): в отскоке (bumped) и в свободном падении управление
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок * НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и * прерывает присед после удара (medland/плита по голове) нажатием ↑, и
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие * последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на * стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
* пару пикселей меньше, чем должен»). */ * пару пикселей меньше, чем должен»). */
if (Kid.action == 5 || Kid.action == 4) { /* bumped / in_freefall */ if (Char.action == 5 || Char.action == 4) { /* bumped / in_freefall */
release_arrows(); release_arrows();
return; return;
} }
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53)) /* Порядок веток — как в seg005: меч старше charid, charid старше кадров.
* Пока Kid безоружен, обе ветки — про стража. */
if (Char.sword == SWORD_2_DRAWN)
control_with_sword();
else if (Char.charid >= CHARID_2_GUARD)
control_guard_inactive();
else if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
control_standing(); control_standing();
else if (f == FRAME_48_TURN) else if (f == FRAME_48_TURN)
control_turning(); control_turning();
@@ -406,12 +641,12 @@ static void flip_control_x(void)
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */ /* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
static void user_control(void) static void user_control(void)
{ {
if (Kid.direction >= DIR_RIGHT) { if (Char.direction >= DIR_RIGHT) {
flip_control_x(); flip_control_x();
control(); pop_control();
flip_control_x(); flip_control_x();
} else { } else {
control(); pop_control();
} }
} }
@@ -451,6 +686,12 @@ void pop_ctrl_tick(void)
rest_ctrl_1(); rest_ctrl_1();
read_input(); read_input();
read_user_control(); read_user_control();
user_control(); /* -> control(): может сменить Kid.curr_seq */ /* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
* персонажем (как в оригинале), поэтому ввод игрока применяется к
* Киду только внутри loadkid/savekid. Кадр не перезагружаем —
* play_seq в kid_tick сделает это следующим шагом. */
pop_loadkid();
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
pop_savekid_state();
save_ctrl_1(); save_ctrl_1();
} }
+23
View File
@@ -32,4 +32,27 @@ void pop_ctrl_tick(void);
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */ * зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
int8_t pop_ctrl_shift_held(void); int8_t pop_ctrl_shift_held(void);
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
* диспетчера: он ВЫСТАВЛЯЕТ эти переменные, а дальше исполняется тот же
* control(), что и для игрока. Значения — CONTROL_RELEASED/IGNORE/HELD. */
#define POP_CONTROL_RELEASED 0
#define POP_CONTROL_IGNORE 1
#define POP_CONTROL_HELD (-1)
/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх /
* +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он
* заполняет как есть, без flip_control_x. */
extern int8_t control_x, control_y, control_shift;
#define POP_CONTROL_HELD_FORWARD (-1)
#define POP_CONTROL_HELD_BACKWARD 1
#define POP_CONTROL_HELD_UP (-1)
#define POP_CONTROL_HELD_DOWN 1
extern int8_t control_forward, control_backward, control_up, control_down,
control_shift2;
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж — из
* своего тика напрямую. */
void pop_control(void);
#endif #endif
+223
View File
@@ -0,0 +1,223 @@
/*
* pop_gdraw.c — ОТРИСОВКА стража. Резидент окна W3 (--w3 pop_gdraw.c).
*
* Почему отдельным файлом, а не внутри pop_guard.c: --w3 забирает МОДУЛЬ
* целиком (код+rodata в 0xC000), а состояние стража и его логика обязаны
* остаться в W1/W2 — из банка (guards.c) резидент W3 недостижим ни прямо,
* ни транзитивно (docs/layout_plan_v2.md §2 R2). Значит граница проходит
* ровно между «что рисует» (зовёт только главный цикл → резидент) и «что
* знает» (зовёт и банк → W1/W2).
*
* Писучие статики резидента (--dataseg не трогаем) остаются в обычном
* _DATA, то есть в W2 — сюда переезжает КОД, не данные.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_guard.h"
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */
#include "kid_data.h"
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
#define GUARD_PAGES 5
static atlas_t gp[GUARD_PAGES];
static uint8_t g_ok;
/* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid:
* heal обязан стирать спрайт именно той страницы, в которую рисуем). */
static int gx_l[2], gy_l[2];
static uint16_t gw_l[2], gh_l[2];
static uint8_t gvalid[2];
/* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение
* почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит
* ровно по площади. Объединение считаем только для окна fore-клипа. */
static int sx_l[2], sy_l[2];
static uint16_t sw_l[2], sh_l[2];
static uint8_t svalid[2];
int pop_guard_load(void) __banked
{
static const char *const names[GUARD_PAGES] = {
"GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl",
"GUARD\\g3.atl", "GUARD\\g4.atl"
};
uint8_t i;
for (i = 0; i < GUARD_PAGES; i++)
if (atlas_load(&gp[i], names[i]) != 0) return -1;
g_ok = 1;
return 0;
}
void pop_guard_free(void) __banked
{
uint8_t i;
if (!g_ok) return;
for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]);
g_ok = 0;
}
void pop_guard_heal(void) __banked
{
uint8_t p = gfx_get_draw_page() & 1;
if (gvalid[p]) gfx_heal(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
if (svalid[p]) gfx_heal(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]);
gvalid[p] = svalid[p] = 0;
}
void pop_guard_draw(void) __banked
{
int obj_x, obj_y, top, fwd, fp_x;
uint8_t page, idx, p, flip;
const uint8_t *img;
uint16_t w, h;
/* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам
* футпринт (pop_fore_over_char) считается по спрайту стража, поэтому
* w/h ниже НЕ трогаем. */
int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0;
if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return;
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
fwd = (int)Guard.x + ((Guard.direction < 0) ? -pop_gframe.dx : pop_gframe.dx);
obj_x = (fwd << 1) - 116;
obj_y = (int)Guard.y + pop_gframe.dy;
if ((int8_t)(pop_gframe.flags ^ Guard.direction) >= 0) obj_x++;
fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — футпринт */
obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */
page = (uint8_t)(pop_gframe.image >> 3);
idx = (uint8_t)(pop_gframe.image & 7);
if (page >= GUARD_PAGES) return;
img = (const uint8_t *)atlas_image(&gp[page], idx);
gfx_w0_map(gp[page].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
int bx = obj_x;
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
if (flip) bx -= (int)w;
top = obj_y - (int)h + 1;
p = gfx_get_draw_page() & 1;
gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
else
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1;
clip_x = bx; clip_y = top; clip_w = w; clip_h = h;
}
/* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
* seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник
* heal; окно fore-клипа — объединение (без него клинок полез бы поверх
* переднего столба). */
{
int sx, sy; uint16_t sw, sh;
if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y,
&sx, &sy, &sw, &sh)) {
p = gfx_get_draw_page() & 1;
sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh;
svalid[p] = 1;
if (!clip_w) {
clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh;
} else {
int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h;
if (sx < clip_x) clip_x = sx;
if (sy < clip_y) clip_y = sy;
if (sx + (int)sw > x1) x1 = sx + (int)sw;
if (sy + (int)sh > y1) y1 = sy + (int)sh;
clip_w = (uint16_t)(x1 - clip_x);
clip_h = (uint16_t)(y1 - clip_y);
}
}
}
gfx_w0_unmap();
/* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c,
* fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов. */
pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h);
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
* midtable, а foretable рисуется после всех midtable-записей, поэтому
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
* столб. */
if (w && h)
pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction);
}
/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- *
* Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри
* тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража
* справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2
* id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы
* просто не печатаем — полоса и так на чёрном фоне.
*
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
* pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */
#define HP_Y 194
#define HP_STEP 7
#define HP_KID_FULL 216
#define HP_KID_EMPTY 217
#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */
/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ
* страницах дабл-буфера (иначе на второй останется старое значение и его
* будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг.
*
* Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
* sdcc_z80_cmp_store_a_bug). */
static uint8_t hp_todo; /* сколько страниц ещё обновить */
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
* комнату (фон перерисован целиком и стёр её) и при старте. */
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
void pop_hp_draw(void) __banked
{
uint8_t i, n;
uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0);
uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0);
{ /* Изменилось хоть одно число — обновить обе страницы. */
uint8_t k = hitp_curr, km = hitp_max;
uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev ||
gd != hp_gd_prev || gm != hp_gdmax_prev);
hp_kid_prev = k; hp_kidmax_prev = km;
hp_gd_prev = gd; hp_gdmax_prev = gm;
if (changed) hp_todo = 2;
}
if (hp_todo == 0) return;
hp_todo--;
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);
for (i = hitp_curr; i < n; i++)
pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF);
for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++)
pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF);
/* Страж: только полные деления, справа налево. Скелет полосы не имеет. */
if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) {
const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
uint16_t w, h;
gfx_w0_map(gp[0].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
/* Атлас стража тоже column-major — блит колоночный. */
n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd);
for (i = 0; i < n; i++)
gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP,
HP_Y + POP_YOFF, img, 0, 0, 0);
}
gfx_w0_unmap();
}
}
+236
View File
@@ -0,0 +1,236 @@
/*
* pop_geom.c — общая геометрия + PRNG оригинала. См. pop_geom.h.
*/
#include "pop_geom.h"
/* data.h: шаг 14 (ширина тайла в координатах персонажа), [5] = col 0. */
const int16_t pop_x_bump[20] = {
-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
};
const int16_t pop_y_land[5] = {-8, 55, 118, 181, 244};
int8_t pop_y_to_row(int16_t y)
{
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
}
/* Уложить значение в диапазон 0..maxv. Вызовы оригинала — prandom(1),
* prandom(255), prandom(0xFF): делитель степень двойки, то есть маска, а не
* деление. __moduint на Z80 стоит заметно дороже проверки n & (n-1). */
static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv)
{
uint16_t n = (uint16_t)(maxv + 1);
if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
return (uint16_t)(v & (uint16_t)(n - 1));
return (uint16_t)(v % n);
}
#if POP_PRANDOM_EXACT
/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ *
*
* ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет
* умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong — 8 430
* тактов на вызов (замер в MAME). Написанное на C через 16-битные
* половины это ~3 500. Здесь ~1 000, при полностью той же
* последовательности, что в SDLPoP. Альтернатива «взять генератор
* попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает
* совместимость с эталоном, по которому сверяем картинку.
*
* Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
* РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
* 0x343FD) — 17 удвоений, ТРИ сложения и одно вычитание. Величина 3*s,
* нужная в конце, попадается по дороге на втором шаге — её и сохраняем.
*
* ABI __sdcccall(1): arg1 (указатель на сид) -> HL, возврат -> DE. IX не
* используется, сохранять нечего. Клоббер AF/BC/DE/HL. Аккумулятор —
* DE:HL (DE старшее слово); s.lo живёт в BC, s.hi и 3*s — в статиках
* (регистров на всё не хватает).
*/
uint16_t pop_pr_ptr; /* адрес сида */
uint16_t pop_pr_sh; /* старшее слово исходного сида */
uint16_t pop_pr_3s_lo, pop_pr_3s_hi; /* 3*s, нужное в конце */
static uint16_t pop_lcg_step(pop_rnd_t *seed) __naked
{
(void)seed;
__asm
ld (_pop_pr_ptr), hl
ld c, (hl)
inc hl
ld b, (hl) ; BC = s.lo
inc hl
ld a, (hl)
ld (_pop_pr_sh), a
inc hl
ld a, (hl)
ld (_pop_pr_sh + 1), a ; (pop_pr_sh) = s.hi
ld h, b
ld l, c
ld de, (_pop_pr_sh) ; акк = s
;; акк = акк*2 + s => 3s
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh) ; LD A,(nn) флаги НЕ трогает
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
ld (_pop_pr_3s_lo), hl
ld (_pop_pr_3s_hi), de
;; акк = акк*4 + s => 13s
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh)
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
;; акк = акк*16 + s => 209s
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh)
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
;; акк *= 1024 => 214016s (10 удвоений)
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
;; акк -= 3s => 214013s
ld bc, (_pop_pr_3s_lo)
or a ; CY = 0
sbc hl, bc
ld bc, (_pop_pr_3s_hi) ; LD BC,(nn) флаги НЕ трогает
ex de, hl
sbc hl, bc
ex de, hl
;; акк += 2531011 = 0x0026_9EC3
ld bc, #0x9EC3
add hl, bc
ld bc, #0x0026
ex de, hl
adc hl, bc
ex de, hl
;; сохранить сид, вернуть СТАРШЕЕ слово в DE
push de
ex de, hl ; DE = младшее слово
ld hl, (_pop_pr_ptr)
ld (hl), e
inc hl
ld (hl), d
inc hl
pop de ; DE = старшее слово
ld (hl), e
inc hl
ld (hl), d
ret
__endasm;
}
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
{
return pop_rnd_fit(pop_lcg_step(seed), maxv);
}
#else /* !POP_PRANDOM_EXACT — генератор попроще, НЕ совместимый с эталоном */
/* xorshift16 + шаг Вейля. Зачем не LCG оригинала: тот 32-битный, и даже
* без __mullong (см. ветку выше) стоит ~7 000 тактов на вызов — при двух
* вызовах за кадр это 3 % бюджета на генератор случайных чисел. Здесь
* умножений нет вовсе, только сдвиги и xor.
*
* Почему НЕ 8-битный RND Apple II (RNDseed := 5*RNDseed + 23 mod 256,
* Prince-of-Persia-Apple-II): он ещё быстрее, но у LCG по модулю 256
* младшие биты вырождены — бит 0 просто чередуется. Наши вызовы это
* видят: раскладка кладки берёт prandom(1) (один бит) и prandom(4), и
* вместо шума получилась бы правильная шахматка. У xorshift такой
* структуры нет, а стоит он примерно столько же.
*
* Шаг Вейля (+0x9E37, нечётный) нужен ещё и потому, что у чистого
* xorshift ноль — неподвижная точка, а сид кладки (номер комнаты + ряд +
* колонка) вполне может оказаться нулём.
*
* ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP — другая (но
* статистически такая же) раскладка кладки и другие броски в боёвке.
* Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG
* на ассемблер выигрыш от смены генератора почти исчез, а сверять
* картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
{
uint16_t s = seed->lo;
s ^= (uint16_t)(s << 7);
s ^= (uint16_t)(s >> 9);
s ^= (uint16_t)(s << 8);
s = (uint16_t)(s + 0x9E37u);
seed->lo = s;
seed->hi = 0; /* старшая половина не используется */
/* Отдаём СТАРШИЙ байт: у него нет короткопериодической структуры
* младших бит, а вызывающие берут от результата именно младшие. */
return pop_rnd_fit((uint16_t)(s >> 8), maxv);
}
#endif
+61
View File
@@ -0,0 +1,61 @@
/*
* pop_geom.h — общая геометрия PoP и PRNG оригинала.
*
* Сюда сведено то, что раньше дублировалось по модулям (x_bump/y_land в
* pop_kid и pop_map, y_to_row в pop_bg и pop_map, 32-битный LCG в pop_bg и
* pop_trob). Кроме экономии это ФУНДАМЕНТ bank-safe API: модуль живёт в
* W1/W2 и не трогает графику, поэтому его может звать и главный цикл, и
* резидент W3, и будущий __banked-код стражей (docs/layout_plan_v2.md §4).
*/
#ifndef POP_GEOM_H
#define POP_GEOM_H
#include <stdint.h>
#define FIRST_ONSCREEN_COLUMN 5 /* индекс col 0 комнаты в x_bump */
#define TILE_SIZEX 14 /* ширина тайла в координатах персонажа */
#define TILE_SIZEY 63 /* высота ряда */
/* x_bump (data.h): левая граница колонки в координатах персонажа; колонка
* комнаты col = x_bump[col + FIRST_ONSCREEN_COLUMN]. y_land: пол ряда
* (индекс row+1; [0] — ряд «над комнатой»). */
extern const int16_t pop_x_bump[20];
extern const int16_t pop_y_land[5];
/* y_to_row_mod4 (seg006): ряд по координате y с оборотом mod 4 (−1 = полоса
* над комнатой, 3 = под комнатой). */
int8_t pop_y_to_row(int16_t y);
/* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
* анимаций тайлов (фаза факелов) свои последовательности, смешивать их
* нельзя.
*
* ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c):
* 1 (по умолчанию) — LCG оригинала (s*214013+2531011) БИТ-В-БИТ: та же
* раскладка кладки и те же броски, что в SDLPoP, поэтому по картинке
* можно сверяться с эталоном;
* 0 — xorshift16 + шаг Вейля, без единого умножения. Дешевле ровно на
* 5 646 тактов за кадр, 1.3 % бюджета (замер A/B в MAME, комната 3,
* 175 кадров: 403 632 против 397 986). Последовательность другая —
* статистически эквивалентная, но не та же.
*
* Обе версии считают 16-битными половинами, а не unsigned long: на 32-бит
* умножении SDCC зовёт __mullong, и это 8 430 тактов на вызов (замер в
* MAME, breakpoint на __mullong).
*
* ЗАДЕЛ: разрыв между версиями определяется не алгоритмом, а кодогенерацией
* SDCC — те же генераторы на Z80-ассемблере укладываются в 86..148 тактов.
* Если процент понадобится, сначала переписать в asm ОРИГИНАЛЬНЫЙ LCG (там
* умножение на константу), а менять генератор — только если и этого мало. */
typedef struct { uint16_t lo, hi; } pop_rnd_t;
#ifndef POP_PRANDOM_EXACT
#define POP_PRANDOM_EXACT 1
#endif
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv);
/* Сид из 16-битного числа (старшая половина = 0). */
#define pop_prandom_set(s, v) do { (s).lo = (uint16_t)(v); (s).hi = 0; } while (0)
#endif
+164
View File
@@ -0,0 +1,164 @@
/*
* pop_guard.c — СОСТОЯНИЕ и ЛОГИКА стража (W1/W2). См. pop_guard.h.
*
* Здесь только то, что должно оставаться достижимым из банка (guards.c):
* поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы.
* ОТРИСОВКА — в pop_gdraw.c, резиденте W3 (её зовёт лишь главный цикл).
*/
#include <stdint.h>
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_level.h"
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
#include "pop_map.h" /* hitp_* — HP Кида */
#include "kid_data.h"
pop_char_t Guard;
uint8_t guardhp_curr, guardhp_max;
int8_t guardhp_delta;
int8_t can_guard_see_kid;
int8_t is_guard_notice;
uint8_t guard_skill;
int8_t guard_refrac, justblocked, kid_sword_strike;
int8_t offguard, holding_sword;
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
* см. pop_geom.h — иначе перерисовка стены сбивала бы броски стража. */
pop_rnd_t pop_fight_seed;
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
kframe pop_gframe;
uint8_t pop_guard_present(void)
{
return (uint8_t)(Guard.charid != 0 && Guard.alive < 0 && guardhp_curr != 0);
}
void pop_guard_reset(void)
{
Guard.charid = 0;
Guard.alive = 0;
Guard.direction = 0;
guardhp_curr = 0;
guardhp_delta = 0;
can_guard_see_kid = 0;
is_guard_notice = 0;
guard_refrac = justblocked = kid_sword_strike = 0;
offguard = holding_sword = 0;
}
/* kill guard cheat (seg000:786):
* if (Guard.charid != charid_4_skeleton) {
* guardhp_delta = -guardhp_curr;
* Guard.alive = 0;
* }
* Скелета чит не убивает — он и в оригинале бессмертен. */
void pop_guard_kill(void)
{
if (Guard.charid == CHARID_4_SKELETON) return;
if (!pop_guard_present()) return;
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
* общий control() ставит ему seq_71_dying. */
guardhp_delta = (int8_t)-guardhp_curr;
Guard.alive = 0;
}
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
void pop_do_delta_hp(void)
{
int16_t hp;
if (hitp_delta) {
hp = (int16_t)hitp_curr + hitp_delta;
if (hp < 0) hp = 0;
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
hitp_curr = (uint8_t)hp;
pop_kid_hurt = 1; /* -> «брызги» поверх Кида */
hitp_delta = 0;
}
if (guardhp_delta) {
hp = (int16_t)guardhp_curr + guardhp_delta;
if (hp < 0) hp = 0;
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
guardhp_curr = (uint8_t)hp;
guardhp_delta = 0;
}
}
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535). */
void pop_guard_load_frame(void)
{
int16_t idx = (int16_t)Guard.frame;
if (idx >= 102 && idx < 107) idx += 70; /* add_frame */
idx -= 149;
if (idx < 0 || idx >= KID_NGFRAMES) { pop_gframe.image = 255; return; }
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
}
/* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё
* держит стража уходящей комнаты. Мёртвого запоминаем вместе с
* последовательностью — тогда при возврате он лежит там же, а не встаёт
* живым. */
void pop_guard_leave(void)
{
if (Guard.charid == 0) return; /* стража не было */
pop_guard_state_save(Guard.room,
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
Guard.direction, Guard.x, guard_skill,
Guard.curr_seq,
(uint8_t)(Guard.alive >= 0));
}
void pop_guard_enter(uint8_t room)
{
uint8_t tile, color, skill;
int8_t dir;
pop_guard_reset();
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
* колонки тайла, а не берётся из файла. */
Guard.room = room;
Guard.curr_row = (int8_t)(tile / 10);
Guard.y = (uint8_t)pop_y_land[Guard.curr_row + 1];
Guard.x = (uint8_t)(pop_x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Guard.curr_col = (int8_t)(tile % 10);
Guard.direction = dir;
Guard.charid = CHARID_2_GUARD;
Guard.sword = 0; /* sword_0_sheathed */
Guard.alive = -1;
Guard.action = 1; /* actions_1_run_jump */
Guard.fall_x = Guard.fall_y = 0;
/* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций;
* всё, что вне диапазона, оригинал сводит к 3. */
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
* Уровень 1 -> 3; extrastrength ненулевой только у skill 4. */
guardhp_max = guardhp_curr = (uint8_t)(3 + (guard_skill == 4 ? 1 : 0));
guardhp_delta = 0;
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит
* ровно там, где лёг (порт ветки guards_seq_hi != 0). */
{
uint16_t saved_seq = pop_guard_state_seq(room);
/* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620)
* пересчитывает её из колонки только при загрузке уровня, а дальше
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр
* тайла. */
if (saved_seq) Guard.x = pop_guard_state_x(room);
pop_loadshad();
if (saved_seq) Char.curr_seq = saved_seq;
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
pop_saveshad();
pop_guard_play(); /* до первого кадра */
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
Guard.alive = 1;
guardhp_curr = 0;
}
}
}
+164
View File
@@ -0,0 +1,164 @@
/*
* pop_guard.h — СОСТОЯНИЕ стража и точки входа в его логику.
*
* Разделение по окнам (docs/layout_plan_v2.md §4, подтверждено пробником
* tests/w3bankgfx):
* - СОСТОЯНИЕ живёт здесь, в W1/W2. Писучие статики __banked-модуля
* линкуются В СТРАНИЦУ БАНКА и снаружи не читаются, поэтому банк —
* только код;
* - ЛОГИКА (ИИ, боёвка) — в guards.c, собираемом как --bank 1=guards.c;
* - ОТРИСОВКА стража — в pop_gdraw.c, РЕЗИДЕНТЕ W3 (--w3): её зовёт
* только главный цикл, а из банка резидент недостижим. Так код
* отрисовки не занимает дефицитные W1/W2 (куча).
*
* Поля char_type — как в оригинале (types.h:302), чтобы порт seg005/seg006
* ложился один в один.
*/
#ifndef POP_GUARD_H
#define POP_GUARD_H
#include <stdint.h>
#include "kid_data.h" /* kframe */
#include "pop_char.h"
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
/* seqids (types.h:1052+) — последовательности боя и стойки. */
#define SEQ_55_DRAW_SWORD 55
#define SEQ_56_GUARD_FORWARD 56 /* шаг вперёд с мечом (страж) */
#define SEQ_57_BACK_WITH_SWORD 57
#define SEQ_58_GUARD_STRIKE 58
#define SEQ_60_TURN_WITH_SWORD 60
#define SEQ_61_PARRY_AFTER_STRIKE 61
#define SEQ_62_PARRY 62
#define SEQ_66_STRIKE_AFTER_PARRY 66
#define SEQ_71_DYING 71
#define SEQ_75_STRIKE 75 /* удар (Кид) */
#define SEQ_77_GUARD_STAND_INACTIVE 77
#define SEQ_80_STAND_FLIPPED 80
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
#define SEQ_87_GUARD_BECOME_INACT 87
#define SEQ_90_EN_GARDE 90
#define SEQ_92_PUT_SWORD_AWAY 92
#define SEQ_93_PUT_SWORD_AWAY_FAST 93
/* frameids (types.h:911+) и состояния меча (types.h:332). */
#define FRAME_150_PARRY 150
#define FRAME_151_STRIKE_1 151
#define FRAME_152_STRIKE_2 152
#define FRAME_153_STRIKE_3 153
#define FRAME_157_WALK_WITH_SWORD 157
#define FRAME_158_STAND_WITH_SWORD 158
#define FRAME_161_PARRY 161
#define FRAME_162_BLOCK_TO_STRIKE 162
#define FRAME_165_WALK_WITH_SWORD 165
#define FRAME_166_STAND_INACTIVE 166
#define FRAME_167_BLOCKED 167
#define FRAME_168_BACK 168
#define FRAME_169_BEGIN_BLOCK 169
#define FRAME_170_STAND_WITH_SWORD 170
#define FRAME_171_STAND_WITH_SWORD 171
#define SWORD_0_SHEATHED 0
#define SWORD_2_DRAWN 2
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
#define CHARID_0_KID 0
#define CHARID_1_SHADOW 1
#define CHARID_2_GUARD 2
#define CHARID_4_SKELETON 4
#define CHARID_5_PRINCESS 5
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
#define NUM_GUARD_SKILLS 12
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
extern uint8_t guardhp_curr, guardhp_max;
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
* 0 — не видит, 1 — видит, но не пойдёт (между ними loose/чомпер/дыра),
* 2 — видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в
* кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */
extern int8_t can_guard_see_kid;
/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного
* стража обернуться, даже если Кид у него за спиной. Ставит источник шума,
* снимает сам ИИ. */
extern int8_t is_guard_notice;
/* Боевые таймеры/уровень мастерства (seg000:295..311). Все убывают раз в
* кадр в autocontrol_opponent — как в оригинале.
* guard_skill — 0..11, из данных уровня; индекс в таблицы вероятностей;
* guard_refrac — «отдышка» стража: пока >0, он не атакует и не наступает;
* justblocked — Кид только что парировал (влияет на вероятность блока);
* kid_sword_strike — Кид только что бил (стража это придерживает). */
extern uint8_t guard_skill;
extern int8_t guard_refrac, justblocked, kid_sword_strike;
/* Состояние меча КИДА (seg005/seg006):
* offguard — меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не
* достаёт клинок автоматически, даже видя стража;
* holding_sword — клинок в руке (для индикатора HP стража). */
extern int8_t offguard, holding_sword;
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
extern pop_rnd_t pop_fight_seed;
/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале
* check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
void pop_check_can_guard_see_kid(void) __banked;
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
void pop_check_sword_hurting(void) __banked;
void pop_check_sword_hurt(void) __banked;
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
extern kframe pop_gframe;
void pop_guard_load_frame(void);
/* Есть ли живой страж в отрисованной комнате (для главного цикла). */
uint8_t pop_guard_present(void);
/* Атласы стража (GUARD\\g0..g4.atl) — грузить ДО initgraph, как Kid.
* 0 — OK, -1 — ошибка (тогда страж просто не рисуется). */
int pop_guard_load(void) __banked;
void pop_guard_free(void) __banked;
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
* seg002:0112) либо убрать его, если в комнате стража нет. */
void pop_guard_enter(uint8_t room);
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
* enter_room, пока Guard ещё держит прежнего. */
void pop_guard_leave(void);
/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
* как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
* ПОСЛЕ персонажей — она ничего не перекрывает. */
void pop_hp_draw(void) __banked;
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
void pop_hp_invalidate(void) __banked;
void pop_guard_heal(void) __banked;
void pop_guard_draw(void) __banked;
/* Убрать стража со сцены (смена комнаты / инициализация). */
void pop_guard_reset(void);
/* Чит «убить стража» (seg000:786, K): скелета не берёт. Вызывать только
* при включённых читах — проверку делает вызывающий. */
void pop_guard_kill(void);
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале — в play_frame). */
void pop_do_delta_hp(void);
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
void pop_guard_tick(void) __banked;
#endif
+430 -110
View File
@@ -2,61 +2,125 @@
* pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame + * pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
* seqtbl-интерпретатор. Данные — kid_data.h. * seqtbl-интерпретатор. Данные — kid_data.h.
*/ */
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <graphics.h> #include <graphics.h>
#include <gfx.h> #include <gfx.h>
#include <sprite.h> #include <sprite.h>
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */ #include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */ #include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h"
#include "kid_data.h" #include "kid_data.h"
kid_t Kid; pop_char_t Kid;
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN /* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */ * в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
int8_t knock; int8_t knock;
/* Координатные таблицы (data.h). */
static const uint8_t x_bump[20] = {
(uint8_t)-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
};
static const int16_t y_land[5] = {-8, 55, 118, 181, 244};
#define FIRST_ONSCREEN_COLUMN 5
#define TILE_SIZEX 14
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */ /* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
#define KID_MAXPAGES 28 #define KID_MAXPAGES 28
static atlas_t kidp[KID_MAXPAGES]; static atlas_t kidp[KID_MAXPAGES];
static uint8_t kid_npages; static uint8_t kid_npages;
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): отдельный /* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
* спрайт клинка поверх Kid в кадрах «нашёл меч»/убирание в ножны (229..237). * ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
* Из sword_tbl взяты только строки 35..42 — те, что адресуют эти кадры (бой * Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
* не портирован); индекс в атласе = sword_frame − 35. Смещения таблицы — * sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
* в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ calc_screen_x_coord. */ * Смещения — в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
#define SWORD_SF_LO 35 * calc_screen_x_coord, x — «вперёд» по направлению персонажа. */
#define SWORD_SF_HI 42 static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
static const int8_t SWORD_DX[8] = { 7, 14, 14, 14, 28, 28, 21, 14 }; static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
static const int8_t SWORD_DY[8] = {-32,-32,-31,-29,-28,-28,-25,-22 }; static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
static atlas_t swordp; static atlas_t swordp;
static uint8_t sword_ok; static uint8_t sword_ok;
int pop_sword_load(void) int pop_sword_load(void)
{ {
if (atlas_load(&swordp, "sword.atl") != 0) return -1; if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1;
sword_ok = 1; sword_ok = 1;
return 0; return 0;
} }
/* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в
* кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч
* в руке всегда, даже пока sword ещё sheathed). Страницу W0 оставляем
* замапленной — вызывающий делает gfx_w0_unmap сам. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
int obj_x, int obj_y,
int *rx, int *ry, uint16_t *rw, uint16_t *rh)
{
uint8_t sf, id, flip;
const uint8_t *simg;
uint16_t w, h;
int bx, top;
if (!sword_ok) return 0;
if (!((ch->frame >= 229 && ch->frame < 238) ||
ch->sword != 0 ||
(ch->charid == CHARID_2_GUARD && ch->alive < 0)))
return 0;
sf = (uint8_t)(fr->sword & 0x3F);
if (sf == 0 || sf >= KID_NSWORD) return 0;
id = SWORD_ID[sf];
if (id == 0xFF) return 0;
simg = (const uint8_t *)atlas_image(&swordp, id);
gfx_w0_map(swordp.page);
w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (!w || !h) return 0;
flip = (uint8_t)(ch->direction >= 0);
bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE);
/* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
* подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0);
else
gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
*rx = bx; *ry = top; *rw = w; *rh = h;
return 1;
}
/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y).
* Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры
* Кида, а держать вторую копию раскладки в pop_gdraw ни к чему. */
void pop_kid_img_blit(uint8_t image, int x, int y)
{
uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */
uint8_t idx = (uint8_t)(image & 7);
const uint8_t *img;
uint16_t w, h;
if (pg >= kid_npages) return;
img = (const uint8_t *)atlas_image(&kidp[pg], idx);
gfx_w0_map(kidp[pg].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
/* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid —
* ради бесплатного горизонтального флипа), поэтому блит обязан быть
* колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной. */
if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
gfx_w0_unmap();
}
int pop_kid_load(uint8_t npages) int pop_kid_load(uint8_t npages)
{ {
static const char *const names[KID_MAXPAGES] = { static const char *const names[KID_MAXPAGES] = {
"kid0.atl","kid1.atl","kid2.atl","kid3.atl","kid4.atl","kid5.atl", "KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl",
"kid6.atl","kid7.atl","kid8.atl","kid9.atl","kid10.atl","kid11.atl", "KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl",
"kid12.atl","kid13.atl","kid14.atl","kid15.atl","kid16.atl","kid17.atl", "KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl",
"kid18.atl","kid19.atl","kid20.atl","kid21.atl","kid22.atl","kid23.atl", "KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl",
"kid24.atl","kid25.atl","kid26.atl","kid27.atl" "KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl"
}; };
uint8_t i; uint8_t i;
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES; if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
@@ -75,63 +139,272 @@ void pop_kid_free(void)
kid_npages = 0; kid_npages = 0;
} }
/* ---- seqtbl-доступ (база SEQTBL_BASE) ------------------------------ */ /* ---- Данные анимации в EMM-странице (kid_data.bin) ------------------ *
#define SEQ(cs) kid_seqtbl[(uint16_t)((cs) - SEQTBL_BASE)] * Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик), а
* весь остальной код читает cur_frame — 5-байтовую копию текущего кадра.
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
* и коллизия/отрисовка работают с ним, а не с таблицей.
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
extern void _gfx_w0_isr(void);
#define KD_DATA_OFF 0x100u
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
* берёт только МЛАДШИЙ байт результата и подставляет старший байт константы
* (`ld c,l` / `inc b`). Для кадров i >= 52 (i*5 >= 256) адрес уезжает на
* 256*k, и cur_frame наполняется чужой строкой таблицы: у бега/шага/подъёма
* пропадал бит FRAME_NEEDS_FLOOR, и Kid проваливался сквозь пол.
* Артефакт: .sprinter-cc-roomtest/pop_kid.asm, функция load_frame. */
static const uint8_t *kd_frame_ptr(uint8_t i)
{
uint16_t off = (uint16_t)i;
off += (uint16_t)(off << 2); /* i*5 = i + i*4, без умножения */
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
return (const uint8_t *)off;
}
/* То же для таблицы кадров СТРАЖА (frame_tbl_guard дописана в ту же
* страницу данных, см. pop_extract_kid_data.py). */
static const uint8_t *kd_gframe_ptr(uint8_t i)
{
uint16_t off = (uint16_t)i;
off += (uint16_t)(off << 2);
off += (uint16_t)(KD_DATA_OFF + KID_BIN_GFRAMES_OFF);
return (const uint8_t *)off;
}
#define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
(uint16_t)((cs) - SEQTBL_BASE)))
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
* страницей данных — то есть внутри play_seq. */
#define SEQ(cs) KD_SEQ(cs)
static uint8_t kdat_blk, kdat_page, kdat_ok;
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
* тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */
static kframe cur_frame;
static kframe kid_frame; /* кадр Kid для kid_draw */
/* Char (seg006): АКТИВНЫЙ персонаж. Интерпретатор последовательностей в
* оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг
* стоят loadkid/savekid и loadshad/saveshad (seg006:809..825). Копия, а
* не указатель, — и так же, как в оригинале, и быстрее на Z80: обращения
* в горячем цикле идут абсолютной адресацией глобала, а 16-байтовое
* копирование платится один раз на переключение персонажа. */
pop_char_t Char;
/* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
* рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё
* ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
* ними: для стража Opp = Kid, для Кида Opp = Guard. */
pop_char_t Opp;
/* Снимок последовательности на момент входа в окно Char: по нему
* pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)»
* от «её сменил pop_map (прямо в Kid)». */
static uint16_t kid_seq_at_load;
void pop_loadkid(void) { Char = Kid; kid_seq_at_load = Kid.curr_seq; }
void pop_savekid(void) { Kid = Char; }
/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально
* меняет у активного персонажа — последовательность и состояние меча.
*
* Почему не `Kid = Char`, как делает savekid оригинала: у нас часть
* действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq,
* safe_step...), и он пишет ПРЯМО в Kid — там ещё не портирован переход на
* Char. Полное копирование затирало эти правки: Kid терял выравнивание x
* и ряд, и спуск с уступа через вис не срабатывал — персонаж просто
* приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */
void pop_savekid_state(void)
{
/* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char
* (pop_char_set_seq), pop_map — прямо в Kid (kid_set_seq: спуск с
* уступа SEQ_68, зацеп, осторожный шаг). Решение pop_map старше:
* диспетчер про эти переходы не знает и, копируя Char вслепую, затирал
* их — спуск в проём не срабатывал, а персонаж даже не приседал, потому
* что pop_down_action уже вернул «обработано». */
uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load);
if (!map_changed) Kid.curr_seq = Char.curr_seq;
Kid.sword = Char.sword;
}
void pop_loadshad(void) { Char = Guard; }
void pop_saveshad(void) { Guard = Char; }
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; }
void pop_loadkid_and_opp(void) { Char = Kid; Opp = Guard; }
void pop_savekid_and_opp(void) { Kid = Char; Guard = Opp; }
/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной.
* +13 при встречных направлениях: у стоящих лицом друг к другу «ноль»
* приходится на ширину тайла между ними. Живёт здесь, а не в guards.c,
* потому что нужен по обе стороны банковой границы — и ИИ, и диспетчеру
* боёвки (pop_ctrl). */
int16_t pop_char_opp_dist(void)
{
int16_t distance;
if (Char.room != Opp.room) return 999;
distance = (int16_t)Opp.x - (int16_t)Char.x;
if (Char.direction < 0) distance = -distance;
if (distance >= 0 && Char.direction != Opp.direction) distance += 13;
return distance;
}
int pop_kid_data_load(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
kdat_blk = blk;
kdat_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(kdat_page);
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
pg[0x38] = 0xC3;
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
kdat_ok = 1;
return 0;
}
void pop_kid_data_free(void)
{
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
}
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у Kid и у стража они
* разные: KID_BIN_FRAMES_OFF / KID_BIN_GFRAMES_OFF). Сама маппит страницу,
* поэтому годится для вызова извне (pop_guard) — но НЕ в горячем цикле. */
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
{
const uint8_t *f;
uint16_t off = (uint16_t)idx;
if (!kdat_ok) { out5[0] = 255; return; }
off += (uint16_t)(off << 2); /* idx*5, без умножения */
off += (uint16_t)(KD_DATA_OFF + tbl_off);
gfx_w0_map(kdat_page);
f = (const uint8_t *)off;
out5[0] = f[0]; out5[1] = f[1]; out5[2] = f[2];
out5[3] = f[3]; out5[4] = f[4];
gfx_w0_unmap();
}
/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
static void load_frame(void)
{
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
* стража frame_tbl_guard с индексом frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535). */
const uint8_t *f;
if (Char.charid == CHARID_2_GUARD) {
int16_t idx = (int16_t)Char.frame;
if (idx >= 102 && idx < 107) idx += 70;
idx -= 149;
if (idx < 0 || idx >= KID_NGFRAMES) { cur_frame.image = 255; return; }
f = kd_gframe_ptr((uint8_t)idx);
} else {
f = kd_frame_ptr(Char.frame);
}
cur_frame.image = f[0];
cur_frame.dx = (int8_t)f[1];
cur_frame.dy = (int8_t)f[2];
cur_frame.flags = f[3];
cur_frame.sword = f[4];
/* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
* персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/
* pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
* check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
* на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа
* Кида и рисовал произвольную позу. Теперь владелец один — тот, для
* кого кадр реально загружен. */
if (Char.charid == CHARID_0_KID) kid_frame = cur_frame;
else pop_gframe = cur_frame;
}
static uint8_t char_dx_forward(int8_t dx) static uint8_t char_dx_forward(int8_t dx)
{ {
if (Kid.direction < 0) dx = (int8_t)-dx; if (Char.direction < 0) dx = (int8_t)-dx;
return (uint8_t)(Kid.x + dx); return (uint8_t)(Char.x + dx);
} }
/* Аксессоры к текущему кадру (для pop_map — без дублирования kid_frames). */ /* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
int8_t kid_cur_dx(void) { return kid_frames[Kid.frame].dx; } * их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
uint8_t kid_cur_flags(void) { return kid_frames[Kid.frame].flags; } int8_t kid_cur_dx(void) { return cur_frame.dx; }
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
/* seqtbl_offset_char: сменить последовательность Kid (единая точка — /* seqtbl_offset_char: сменить последовательность Kid (единая точка —
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */ * pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; } void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0). */ /* То же для АКТИВНОГО персонажа — внутри окна Char (порт
* seqtbl_offset_char, seg006). Нужен стражу: его последовательность
* ставится, когда Char уже загружен из Guard. */
void pop_char_set_seq(uint8_t seq_id) { Char.curr_seq = kid_seq_off[seq_id]; }
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
* Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце —
* load_frame: пока страница ещё замаплена, копируем кадр в cur_frame. */
void play_seq(void) void play_seq(void)
{ {
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
for (;;) { for (;;) {
uint8_t cmd = SEQ(Kid.curr_seq); uint8_t cmd = SEQ(Char.curr_seq);
Kid.curr_seq++; Char.curr_seq++;
switch (cmd) { switch (cmd) {
case 0xFB: /* DX */ case 0xFB: /* DX */
Kid.x = char_dx_forward((int8_t)SEQ(Kid.curr_seq)); Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xFA: /* DY */ case 0xFA: /* DY */
Kid.y = (uint8_t)(Kid.y + (int8_t)SEQ(Kid.curr_seq)); Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xFE: /* FLIP */ case 0xFE: /* FLIP */
Kid.direction = (int8_t)~Kid.direction; Char.direction = (int8_t)~Char.direction;
break; break;
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */ case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
Kid.curr_seq += 2; Char.curr_seq += 2;
break; break;
case 0xFF: { /* JMP */ case 0xFF: { /* JMP */
uint16_t a = (uint16_t)(SEQ(Kid.curr_seq) | uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
((uint16_t)SEQ((uint16_t)(Kid.curr_seq + 1)) << 8)); ((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
Kid.curr_seq = a; Char.curr_seq = a;
break; break;
} }
case 0xFD: /* UP */ case 0xFD: /* UP */
Kid.curr_row--; Char.curr_row--;
break; break;
case 0xFC: /* DOWN */ case 0xFC: /* DOWN */
Kid.curr_row++; Char.curr_row++;
break; break;
case 0xF9: /* ACTION */ case 0xF9: /* ACTION */
Kid.action = SEQ(Kid.curr_seq); Char.action = SEQ(Char.curr_seq);
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xF8: /* SET_FALL */ case 0xF8: /* SET_FALL */
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++; Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++; Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
break; break;
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */ case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
knock = 1; knock = 1;
@@ -140,41 +413,51 @@ void play_seq(void)
knock = -1; knock = -1;
break; break;
case 0xF2: /* SOUND — потребить 1 байт */ case 0xF2: /* SOUND — потребить 1 байт */
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xF1: /* END_LEVEL */ case 0xF1: /* END_LEVEL */
break; break;
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
* (порт play_seq -> proc_get_object, seg006) */ * (порт play_seq -> proc_get_object, seg006) */
if (SEQ(Kid.curr_seq) == 1) if (SEQ(Char.curr_seq) == 1)
pop_proc_get_object(); pop_proc_get_object();
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xF6: /* DIE nop */ case 0xF6: /* DIE nop */
break; break;
default: /* КАДР */ default: /* КАДР */
Kid.frame = cmd; Char.frame = cmd;
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
gfx_w0_unmap();
return; return;
} }
} }
} }
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
* работает с Char и сам по себе не знает, чей это персонаж. */
void pop_kid_play(void) { pop_loadkid(); play_seq(); pop_savekid(); }
void pop_guard_play(void) { pop_loadshad(); play_seq(); pop_saveshad(); }
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir) void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
{ {
Kid.curr_col = col; Kid.curr_col = col;
Kid.curr_row = row; Kid.curr_row = row;
Kid.direction = dir; Kid.direction = dir;
Kid.x = (uint8_t)(x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX); Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Kid.y = (uint8_t)y_land[row + 1]; Kid.y = (uint8_t)pop_y_land[row + 1];
Kid.fall_x = Kid.fall_y = 0; Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0; Kid.repeat = 0;
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
Kid.curr_seq = kid_seq_off[seq_id]; Kid.curr_seq = kid_seq_off[seq_id];
play_seq(); /* до первого кадра */ pop_kid_play(); /* до первого кадра */
} }
void kid_tick(void) void kid_tick(void)
{ {
play_seq(); pop_kid_play();
} }
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy /* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
@@ -204,6 +487,64 @@ static int kid_lx[2], kid_ly[2];
static uint16_t kid_lw[2], kid_lh[2]; static uint16_t kid_lw[2], kid_lh[2];
static uint8_t kid_lvalid[2]; static uint8_t kid_lvalid[2];
/* ОТДЕЛЬНЫЙ прямоугольник накладных спрайтов (клинок chtab_0, брызги урона).
* Раньше он объединялся с прямоугольником персонажа — но объединение сильно
* больше суммы двух: клинок уходит вперёд-вверх, и общий габарит выходил
* почти вдвое шире и выше самого Kid, а heal стоит ровно по площади.
* Поэтому heal делаем двумя вызовами по своим прямоугольникам, а объединение
* оставляем ТОЛЬКО для окна fore-клипа (там это 4 сравнения, без рисования —
* зато клинок/брызги честно уходят под передние грани тайлов). */
static int kid_ox[2], kid_oy[2];
static uint16_t kid_ow[2], kid_oh[2];
static uint8_t kid_ovalid[2];
/* Добавить накладной спрайт в overlay-прямоугольник страницы dp. Клинок и
* брызги в одном кадре встречаются редко и оба маленькие — их объединение
* дешевле третьего слота. */
static void kid_overlay_add(uint8_t dp, int x, int y, uint16_t w, uint16_t h)
{
if (!kid_ovalid[dp]) {
kid_ox[dp] = x; kid_oy[dp] = y; kid_ow[dp] = w; kid_oh[dp] = h;
kid_ovalid[dp] = 1;
return;
}
{
int x0 = kid_ox[dp], y0 = kid_oy[dp];
int x1 = x0 + (int)kid_ow[dp], y1 = y0 + (int)kid_oh[dp];
if (x < x0) x0 = x;
if (y < y0) y0 = y;
if (x + (int)w > x1) x1 = x + (int)w;
if (y + (int)h > y1) y1 = y + (int)h;
kid_ox[dp] = x0; kid_oy[dp] = y0;
kid_ow[dp] = (uint16_t)(x1 - x0); kid_oh[dp] = (uint16_t)(y1 - y0);
}
}
/* Окно fore-клипа = объединение «персонаж + накладные». Считается по
* прямоугольникам, ничего не рисует. */
static void kid_fore_clip(uint8_t dp)
{
int x0, y0, x1, y1;
if (!kid_lvalid[dp] && !kid_ovalid[dp]) { pop_fore_set_clip(0, 0, 0, 0); return; }
if (!kid_ovalid[dp]) {
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
(int)kid_lw[dp], (int)kid_lh[dp]);
return;
}
if (!kid_lvalid[dp]) {
pop_fore_set_clip(kid_ox[dp], kid_oy[dp] + POP_YOFF,
(int)kid_ow[dp], (int)kid_oh[dp]);
return;
}
x0 = kid_lx[dp] < kid_ox[dp] ? kid_lx[dp] : kid_ox[dp];
y0 = kid_ly[dp] < kid_oy[dp] ? kid_ly[dp] : kid_oy[dp];
x1 = kid_lx[dp] + (int)kid_lw[dp];
y1 = kid_ly[dp] + (int)kid_lh[dp];
if (kid_ox[dp] + (int)kid_ow[dp] > x1) x1 = kid_ox[dp] + (int)kid_ow[dp];
if (kid_oy[dp] + (int)kid_oh[dp] > y1) y1 = kid_oy[dp] + (int)kid_oh[dp];
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
}
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg): /* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */ * obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
static int kid_fp_x, kid_fp_y; static int kid_fp_x, kid_fp_y;
@@ -247,23 +588,18 @@ void kid_draw_splash(void)
int top = obj_y - (int)h + 1; int top = obj_y - (int)h + 1;
if (flip) bx -= (int)w; if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
else
gfx_blit_cols(bx, top + POP_YOFF, img, flip); gfx_blit_cols(bx, top + POP_YOFF, img, flip);
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h); pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе /* Брызги — накладной спрайт: свой прямоугольник heal (см.
* брызги останутся на экране после кадра. */ * kid_overlay_add), окно fore пересчитываем объединением. */
{ {
uint8_t dp = gfx_get_draw_page(); uint8_t dp = gfx_get_draw_page();
if (kid_lvalid[dp]) { kid_overlay_add(dp, bx, top, w, h);
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx; kid_fore_clip(dp);
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
int x1 = kid_lx[dp] + (int)kid_lw[dp];
int y1 = kid_ly[dp] + (int)kid_lh[dp];
if (bx + (int)w > x1) x1 = bx + (int)w;
if (top + (int)h > y1) y1 = top + (int)h;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
}
} }
} }
gfx_w0_unmap(); gfx_w0_unmap();
@@ -275,17 +611,23 @@ void kid_heal(void)
uint8_t p = gfx_get_draw_page(); uint8_t p = gfx_get_draw_page();
if (kid_lvalid[p]) if (kid_lvalid[p])
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */ gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
kid_lvalid[p] = 0; if (kid_ovalid[p])
gfx_heal(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]);
kid_lvalid[p] = kid_ovalid[p] = 0;
} }
void kid_draw(void) void kid_draw(void)
{ {
kframe fr = kid_frames[Kid.frame]; kframe fr = kid_frame; /* кадр Kid, сохранённый savekid */
int fwd, obj_x, obj_y, top; int fwd, obj_x, obj_y, top;
uint8_t page, idx; uint8_t page, idx;
const uint8_t *img; const uint8_t *img;
uint16_t w, h; uint16_t w, h;
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
* кадра / страница не загружена) перекрывать нечего, и fore-проход
* не должен трогать экран. */
pop_fore_set_clip(0, 0, 0, 0);
if (fr.image == 255) if (fr.image == 255)
return; /* пустой кадр */ return; /* пустой кадр */
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠ /* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
@@ -330,8 +672,14 @@ void kid_draw(void)
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */ if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
} }
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
if (skip < (int)h) /* +YOFF: центрирование */ if (skip < (int)h) { /* +YOFF: центрирование */
if (pop_onscreen_cols(bx, top + skip + POP_YOFF, w,
(uint16_t)((int)h - skip)))
gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip,
(uint8_t)skip, 0);
else
gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0); gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0);
}
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) — /* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
* узко, только по прямоугольнику Кида и только при выходе. */ * узко, только по прямоугольнику Кида и только при выходе. */
@@ -344,44 +692,16 @@ void kid_draw(void)
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */ kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
} }
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх /* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
* Kid. Прямоугольник heal расширяем объединением — иначе клинок, выйдя * Kid — со СВОИМ прямоугольником heal (kid_overlay_add). */
* за габарит Kid, оставит след. */ {
if (sword_ok && Kid.frame >= 229 && Kid.frame < 238) { int sx, sy; uint16_t sw, sh;
uint8_t sf = (uint8_t)(fr.sword & 0x3F); if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh))
if (sf >= SWORD_SF_LO && sf <= SWORD_SF_HI) { kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh);
uint8_t si = (uint8_t)(sf - SWORD_SF_LO);
const uint8_t *simg = (const uint8_t *)atlas_image(&swordp, si);
uint16_t sw, sh;
gfx_w0_map(swordp.page);
sw = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
sh = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (sw && sh) {
uint8_t sflip = (Kid.direction >= 0);
int sbx = obj_x + ((Kid.direction < 0) ? -(int)SWORD_DX[si]
: (int)SWORD_DX[si]);
int stop = obj_y + (int)SWORD_DY[si] - (int)sh + 1;
uint8_t dp = gfx_get_draw_page() & 1;
if (sflip) sbx -= (int)sw;
gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit_cols_part(sbx, stop + POP_YOFF, simg, sflip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
if (!kid_lvalid[dp]) {
kid_lx[dp] = sbx; kid_ly[dp] = stop;
kid_lw[dp] = sw; kid_lh[dp] = sh;
kid_lvalid[dp] = 1;
} else {
int x0 = kid_lx[dp], y0 = kid_ly[dp];
int x1 = x0 + (int)kid_lw[dp], y1 = y0 + (int)kid_lh[dp];
if (sbx < x0) x0 = sbx;
if (stop < y0) y0 = stop;
if (sbx + (int)sw > x1) x1 = sbx + (int)sw;
if (stop + (int)sh > y1) y1 = stop + (int)sh;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0);
kid_lh[dp] = (uint16_t)(y1 - y0);
}
}
}
} }
gfx_w0_unmap(); gfx_w0_unmap();
/* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
* fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
* тайлами — см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего:
* нулевое окно = fore-проход не трогает экран, что и правильно. */
kid_fore_clip(gfx_get_draw_page() & 1);
} }
+57 -14
View File
@@ -11,20 +11,11 @@
#define POP_KID_H #define POP_KID_H
#include <stdint.h> #include <stdint.h>
#include "pop_char.h"
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
/* Состояние персонажа (подмножество char_type, types.h). */ /* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
typedef struct { extern pop_char_t Kid;
uint8_t frame;
uint8_t x, y;
int8_t direction; /* -1 влево, 0 вправо */
int8_t curr_col, curr_row;
uint8_t action;
int8_t fall_x, fall_y;
uint8_t repeat;
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
} kid_t;
extern kid_t Kid;
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN /* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */ * (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
@@ -34,6 +25,18 @@ extern int8_t knock;
* 0 — OK, -1 — ошибка. */ * 0 — OK, -1 — ошибка. */
int pop_kid_load(uint8_t npages); int pop_kid_load(uint8_t npages);
/* Данные анимации (таблица кадров + seqtbl) — отдельная EMM-страница
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
* Страница маппится в W0 внутри play_seq, раз за тик. */
int pop_kid_data_load(const char *path);
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
* Маппит страницу сама; для горячего цикла есть cur_frame. */
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5);
void pop_kid_data_free(void);
/* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах /* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах
* подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798). * подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798).
* Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда * Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда
@@ -41,6 +44,15 @@ int pop_kid_load(uint8_t npages);
int pop_sword_load(void); int pop_sword_load(void);
void pop_kid_free(void); void pop_kid_free(void);
/* Нарисовать клинок chtab_0 поверх персонажа (порт add_sword_to_objtable,
* seg006:1798) — общая точка для Кида и стража: меч в оригинале ОДИН на
* всех. obj_x/obj_y — та же опорная точка, что у спрайта персонажа.
* Возвращает 1 и прямоугольник клинка (для heal), 0 — клинка нет.
* Страницу W0 оставляет замапленной: вызывающий делает gfx_w0_unmap. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
int obj_x, int obj_y,
int *rx, int *ry, uint16_t *rw, uint16_t *rh);
/* Инициализировать Kid: последовательность seq_id, клетка col/row, /* Инициализировать Kid: последовательность seq_id, клетка col/row,
* направление dir (-1/0). Ставит x/y по x_bump/y_land. */ * направление dir (-1/0). Ставит x/y по x_bump/y_land. */
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir); void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir);
@@ -51,7 +63,34 @@ void kid_tick(void);
/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же /* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же
* physics-шаг, как оригинал). */ * physics-шаг, как оригинал). */
void play_seq(void); void play_seq(void); /* работает с Char — см. pop_kid_play ниже */
/* Активный персонаж (порт Char, seg006) и его загрузка/выгрузка.
* Интерпретатор последовательностей общий для Kid и стража: у них общая
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
extern pop_char_t Char;
/* Соперник активного персонажа (порт Opp): ставится вместе с Char через
* pop_loadshad_and_opp. Читают ИИ стража и боёвка (char_opp_dist). */
extern pop_char_t Opp;
void pop_loadkid(void);
void pop_savekid(void);
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
void pop_loadshad(void);
void pop_saveshad(void);
void pop_loadshad_and_opp(void); /* Char = Guard, Opp = Kid (seg006:841) */
void pop_saveshad_and_opp(void); /* Guard = Char, Kid = Opp */
void pop_loadkid_and_opp(void); /* Char = Kid, Opp = Guard */
void pop_savekid_and_opp(void); /* Kid = Char, Guard = Opp */
/* Дистанция до соперника в системе Char: >0 впереди, <0 за спиной,
* 999 — разные комнаты (порт char_opp_dist, seg006:2135). */
int16_t pop_char_opp_dist(void);
/* Прокрутить последовательность персонажа целиком (load + play_seq +
* save). Звать вместо голого play_seq. */
void pop_char_set_seq(uint8_t seq_id); /* seq активного персонажа */
void pop_kid_play(void);
void pop_guard_play(void);
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования /* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
* таблицы во втором TU). */ * таблицы во втором TU). */
@@ -78,6 +117,10 @@ void kid_draw(void);
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */ * звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
void kid_draw_splash(void); void kid_draw_splash(void);
/* Блит спрайта chtab_2 по id, левым верхним углом в (x,y) — для полосы HP
* (id 216 полное деление / 217 пустое). */
void pop_kid_img_blit(uint8_t image, int x, int y);
/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid — /* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid —
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */ * вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
void kid_heal(void); void kid_heal(void);
+108
View File
@@ -23,10 +23,22 @@ extern void _gfx_w0_isr(void);
#define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */ #define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */
#define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */ #define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */
#define BP_LINKS 1952u #define BP_LINKS 1952u
/* Стражи (level_type, types.h:228): по байту на комнату в каждом массиве. */
#define BP_GTILE 2119u
#define BP_GDIR 2143u
#define BP_GX 2167u
#define BP_GSEQLO 2191u
#define BP_GSKILL 2215u
#define BP_GSEQHI 2239u
#define BP_GCOLOR 2263u
#define BP_START 2112u #define BP_START 2112u
#define ROOM_TILES 30u #define ROOM_TILES 30u
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */ #define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
/* fwd: инициализация живой копии стражей (тело ниже). */
static void gstate_init(void);
static uint8_t lvl_blk, lvl_page, lvl_ok; static uint8_t lvl_blk, lvl_page, lvl_ok;
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */ /* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
static uint8_t dl1[256], dl2[256]; static uint8_t dl1[256], dl2[256];
@@ -68,6 +80,7 @@ int pop_level_load(const char *path)
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; } if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
lvl_ok = 1; lvl_ok = 1;
gstate_init(); /* живая копия состояния стражей — из уровня */
return 0; return 0;
} }
@@ -234,6 +247,101 @@ static uint8_t start_byte(uint16_t off)
return v; return v;
} }
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
* колонки (pos_guards), поэтому guards_x из файла не читаем. */
/* ---- ЖИВОЕ состояние стражей (порт level.guards_*) ------------------ *
* В оригинале массивы стражей — часть структуры уровня в ОЗУ, и движок их
* ПЕРЕПИСЫВАЕТ: leave_guard при выходе из комнаты кладёт туда текущие
* позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq. Благодаря
* этому, вернувшись, находишь труп там же, а не воскресшего стража.
* У нас уровень лежит в EMM-странице только на чтение, поэтому держим
* мутабельную копию в W2: 6 байт на комнату. */
#define GS_TILE 0
#define GS_DIR 1
#define GS_X 2
#define GS_SKILL 3
#define GS_SEQL 4
#define GS_SEQH 5
static uint8_t gstate[24 * 6];
static void gstate_init(void)
{
const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
uint8_t r;
gfx_w0_map(lvl_page);
for (r = 0; r < 24; r++) {
uint8_t *g = gstate + (uint16_t)r * 6;
g[GS_TILE] = p[BP_GTILE + r];
g[GS_DIR] = p[BP_GDIR + r];
g[GS_X] = p[BP_GX + r];
g[GS_SKILL] = p[BP_GSKILL + r];
/* guards_seq_lo/hi в ФАЙЛЕ не используются — там 0xFF во всех
* комнатах (оригинал чистит их в reset_level_unused_fields).
* Осмысленное значение туда кладёт только leave_guard, поэтому
* стартуем нулями: 0 = «поднимать стандартной стойкой».
* Без этого в интерпретатор уходил curr_seq = 0xFFFF и он вис. */
g[GS_SEQL] = 0;
g[GS_SEQH] = 0;
}
gfx_w0_unmap();
}
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты. seq
* сохраняем ТОЛЬКО у мёртвого — у живого seq_hi = 0 означает «поднять
* стандартной стойкой», как в оригинале. */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead)
{
uint8_t *g;
if (room < 1 || room > 24) return;
g = gstate + (uint16_t)(room - 1) * 6;
g[GS_TILE] = tile;
g[GS_DIR] = (uint8_t)dir;
g[GS_X] = x;
g[GS_SKILL] = skill;
if (dead) { g[GS_SEQL] = (uint8_t)seq; g[GS_SEQH] = (uint8_t)(seq >> 8); }
else { g[GS_SEQH] = 0; }
}
uint16_t pop_guard_state_seq(uint8_t room)
{
const uint8_t *g;
if (room < 1 || room > 24) return 0;
g = gstate + (uint16_t)(room - 1) * 6;
if (g[GS_SEQH] == 0) return 0; /* живой — стандартная стойка */
return (uint16_t)(g[GS_SEQL] | ((uint16_t)g[GS_SEQH] << 8));
}
/* Запомненная X стража комнаты (для трупа: он лежит там, где лёг, а не в
* центре тайла). */
uint8_t pop_guard_state_x(uint8_t room)
{
if (room < 1 || room > 24) return 0;
return gstate[(uint16_t)(room - 1) * 6 + GS_X];
}
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill)
{
const uint8_t *g;
uint8_t t;
if (!lvl_ok || room < 1 || room > 24) return 0;
/* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
* pop_guard_state_save); цвет статичен, его читаем со страницы. */
g = gstate + (uint16_t)(room - 1) * 6;
t = g[GS_TILE];
if (t < 30) {
*tile = t;
*dir = (int8_t)g[GS_DIR];
*skill = g[GS_SKILL];
gfx_w0_map(lvl_page);
*color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
gfx_w0_unmap();
}
return (uint8_t)(t < 30);
}
uint8_t pop_level_start_room(void) { return start_byte(BP_START + 0); } uint8_t pop_level_start_room(void) { return start_byte(BP_START + 0); }
uint8_t pop_level_start_pos(void) { return start_byte(BP_START + 1); } uint8_t pop_level_start_pos(void) { return start_byte(BP_START + 1); }
int8_t pop_level_start_dir(void) { return (int8_t)start_byte(BP_START + 2); } int8_t pop_level_start_dir(void) { return (int8_t)start_byte(BP_START + 2); }
+17
View File
@@ -73,6 +73,23 @@ void pop_doorlink2_set(uint8_t i, uint8_t v);
int8_t pop_room_col_landing(uint8_t room, uint8_t col); int8_t pop_room_col_landing(uint8_t room, uint8_t col);
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */ /* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
* Порт enter_guard (seg002:0112). */
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты в ЖИВОЙ
* копии массивов уровня. dead != 0 — сохранить и последовательность, тогда
* при возврате в комнату страж поднимется трупом там же, где лёг.
* tile == 30 (или больше) — «стража в комнате больше нет». */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead);
/* Последовательность запомненного стража (0 — поднимать стандартной
* стойкой, как у живого). */
uint16_t pop_guard_state_seq(uint8_t room);
uint8_t pop_guard_state_x(uint8_t room);
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill);
uint8_t pop_level_start_room(void); uint8_t pop_level_start_room(void);
uint8_t pop_level_start_pos(void); uint8_t pop_level_start_pos(void);
int8_t pop_level_start_dir(void); int8_t pop_level_start_dir(void);
+134 -117
View File
@@ -5,7 +5,7 @@
* FRAME_NEEDS_FLOOR (0x40), под персонажем не floor -> start_fall(); * FRAME_NEEDS_FLOOR (0x40), под персонажем не floor -> start_fall();
* стена -> in_wall(); * стена -> in_wall();
* freefall (action 4) -> do_fall(): fall_y-ускорение (в fall_accel/speed), * freefall (action 4) -> do_fall(): fall_y-ускорение (в fall_accel/speed),
* достиг y_land[curr_row+1] -> land()/inc_curr_row. * достиг pop_y_land[curr_row+1] -> land()/inc_curr_row.
* *
* Координатная система PoP: экран.left = 58, тайл = 14 ед. (TILE_SIZEX); * Координатная система PoP: экран.left = 58, тайл = 14 ед. (TILE_SIZEX);
* col = (xpos-58)/14 (округл. вниз), xl = остаток; get_tile_div_mod_m7 = * col = (xpos-58)/14 (округл. вниз), xl = остаток; get_tile_div_mod_m7 =
@@ -19,7 +19,16 @@
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_map.h" #include "pop_map.h"
#include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */ #include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */
#include "pop_bg.h" /* pop_loose_shake_draw/bake_empty — перерисовка */ #include "pop_geom.h" /* общая геометрия (x_bump/y_land/y_to_row) */
#include "pop_redraw.h" /* пометки перерисовки (порт set_redraw_*) */
/* pop_bg нужен ТОЛЬКО падающему куску (mob): spawn/tick/pos — это движущийся
* ОБЪЕКТ, а не перерисовка тайла (в оригинале он и живёт отдельно: mobs +
* draw_moving). Из-за него pop_loose_tick — единственная функция модуля,
* которую нельзя звать из __banked-кода; вся коллизия/физика/предметы, то
* есть то, что понадобится стражам, чиста от графики. Разделение mob на
* логику и отрисовку — задел следующей фазы. */
#include "pop_bg.h"
#include "pop_state.h"
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */ #include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ /* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
@@ -81,23 +90,17 @@
#define FRAME_NEEDS_FLOOR 0x40 #define FRAME_NEEDS_FLOOR 0x40
#define FRAME_WEIGHT_X 0x1F #define FRAME_WEIGHT_X 0x1F
#define TILE_SIZEX 14
#define TILE_RIGHTX 13 #define TILE_RIGHTX 13
#define SCREENSPACE_X 58 #define SCREENSPACE_X 58
#define FALL_ACCEL 3 #define FALL_ACCEL 3
#define FALL_MAX 33 #define FALL_MAX 33
#define TILE_MIDX 7 #define TILE_MIDX 7
#define FIRST_ONSCREEN_COLUMN 5
#define FRAME_THIN 0x20 #define FRAME_THIN 0x20
static const int16_t y_land[5] = {-8, 55, 118, 181, 244};
static const int8_t dir_front[2] = {-1, 1}; /* [dir+1]: dir=-1->-1, 0->+1 */ static const int8_t dir_front[2] = {-1, 1}; /* [dir+1]: dir=-1->-1, 0->+1 */
static const int8_t dir_behind[2] = {1, -1}; /* [dir+1]: назад по направлению */ static const int8_t dir_behind[2] = {1, -1}; /* [dir+1]: назад по направлению */
static const int16_t x_bump[20] = {
-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
};
/* wall_type(tile): gate/doortop=1, mirror=2, chomper=3, wall=4, иначе 0. */ /* wall_type(tile): gate/doortop=1, mirror=2, chomper=3, wall=4, иначе 0. */
static const int8_t wall_dl[6] = {0, 10, 0, -1, 0, 0}; /* wall_dist_from_left */ static const int8_t wall_dl[6] = {0, 10, 0, -1, 0, 0}; /* wall_dist_from_left */
static const int8_t wall_dr[6] = {0, 0, 10, 13, 0, 0}; /* wall_dist_from_right */ static const int8_t wall_dr[6] = {0, 0, 10, 13, 0, 0}; /* wall_dist_from_right */
@@ -111,6 +114,12 @@ uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавше
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */ uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */ uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */ uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
/* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки
* (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале.
* Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и
* проверен до боёвки, и переводить его на дельту без нужды рискованно. */
int8_t hitp_delta;
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */ static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
@@ -142,20 +151,18 @@ static uint8_t g_lcol[6], g_rcol[6];
* отдельное — pop_ceil_modif[col] с той же семантикой (0 покой, 0x80.. — * отдельное — pop_ceil_modif[col] с той же семантикой (0 покой, 0x80.. —
* тряска, 1..11 — отсчёт до падения). */ * тряска, 1..11 — отсчёт до падения). */
uint8_t pop_ceil_modif[10]; /* публично: читает pop_bg при отрисовке */ uint8_t pop_ceil_modif[10]; /* публично: читает pop_bg при отрисовке */
static uint8_t ceil_rest[10]; /* «перерисовать ПОКОЙ» (2 = обе страницы) */
static uint8_t ceil_bake[10]; /* «запечь колодец» на второй странице */
static uint8_t land_bake, land_bake_pos; /* щебень от приземления — вторая страница */
static uint8_t *g_above; /* ряд 2 комнаты сверху (fg), мутабельный */ static uint8_t *g_above; /* ряд 2 комнаты сверху (fg), мутабельный */
uint8_t pop_ceil_fell; /* 0 = нет; иначе col+1 провалившейся плиты-потолка */ uint8_t pop_ceil_fell; /* 0 = нет; иначе col+1 провалившейся плиты-потолка */
uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */ uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */
void pop_map_set_above(uint8_t *above_row2_fg) { g_above = above_row2_fg; } void pop_map_set_above(uint8_t *above_row2_fg) __banked { g_above = above_row2_fg; }
void pop_map_set(uint8_t *fg) { g_fg = fg; } void pop_map_set(uint8_t *fg) __banked { g_fg = fg; }
void pop_map_set_room(uint8_t room) { g_room = room; } void pop_map_set_room(uint8_t room) __banked { g_room = room; }
void pop_kid_hp_reset(void) { hitp_curr = 3; pop_kid_dead = 0; } void pop_kid_hp_reset(void) __banked { hitp_curr = hitp_max = 3; hitp_delta = 0; pop_kid_dead = 0; }
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */ /* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
static uint8_t take_hp(uint8_t count) static uint8_t take_hp(uint8_t count)
@@ -166,7 +173,7 @@ static uint8_t take_hp(uint8_t count)
} }
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg) const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked
{ {
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down; g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; } if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; }
@@ -201,6 +208,16 @@ static uint8_t get_tile(int8_t col, int8_t row)
return g_curr_tile; return g_curr_tile;
} }
/* Тайл ТЕКУЩЕЙ комнаты по клетке — наружу (порт get_tile(room,col,row) для
* случая room == текущая). Нужен лучу видимости стража
* (check_can_guard_see_kid, seg003:688): тот идёт по ряду между Кидом и
* стражем, а оба по условию в одной (= отрисованной) комнате. Колонки за
* пределами 0..9 get_tile разрешает через швы — здесь не используются. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked
{
return get_tile(col, row);
}
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и /* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто * идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в * проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
@@ -367,7 +384,7 @@ static int dist_from_wall_forward(uint8_t tiletype, int8_t tcol)
uint8_t type = wall_type(tiletype); uint8_t type = wall_type(tiletype);
int coll_left, cx; int coll_left, cx;
if (type == 0) return 127; if (type == 0) return 127;
coll_left = x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
cx = char_front_coll(); cx = char_front_coll();
if (Kid.direction < 0) /* лицом влево */ if (Kid.direction < 0) /* лицом влево */
return cx - (coll_left + TILE_RIGHTX - wall_dr[type]); return cx - (coll_left + TILE_RIGHTX - wall_dr[type]);
@@ -423,7 +440,7 @@ static uint8_t fell_on_spikes(void)
static void land(void) static void land(void)
{ {
uint8_t seq; uint8_t seq;
Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
if (fell_on_spikes()) return; /* упал на вредные пики — смерть */ if (fell_on_spikes()) return; /* упал на вредные пики — смерть */
/* к краю пола — чуть назад (как оригинал) */ /* к краю пола — чуть назад (как оригинал) */
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3)
@@ -441,7 +458,7 @@ static void land(void)
if (deadly || take_hp(1)) { /* 3+ этажа или последнее HP */ if (deadly || take_hp(1)) { /* 3+ этажа или последнее HP */
take_hp(100); take_hp(100);
kid_set_seq(SEQ_22_CRUSHED); kid_set_seq(SEQ_22_CRUSHED);
play_seq(); pop_kid_play();
determine_col(); determine_col();
pop_kid_dead = 1; pop_kid_dead = 1;
return; return;
@@ -450,7 +467,7 @@ static void land(void)
} }
Kid.fall_x = Kid.fall_y = 0; Kid.fall_x = Kid.fall_y = 0;
kid_set_seq(seq); kid_set_seq(seq);
play_seq(); pop_kid_play();
determine_col(); determine_col();
} }
@@ -464,7 +481,7 @@ static void start_fall(void)
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP; else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */ else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
kid_set_seq(seq); kid_set_seq(seq);
play_seq(); pop_kid_play();
determine_col(); determine_col();
/* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2). /* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2).
* Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose) * Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose)
@@ -518,7 +535,7 @@ static void check_grab(void)
uint8_t old_x; uint8_t old_x;
if (!pop_ctrl_shift_held()) return; /* Shift не зажат */ if (!pop_ctrl_shift_held()) return; /* Shift не зажат */
if ((uint8_t)Kid.fall_y >= 32) return; /* падает слишком быстро */ if ((uint8_t)Kid.fall_y >= 32) return; /* падает слишком быстро */
if ((uint16_t)y_land[Kid.curr_row + 1] > (uint16_t)(Kid.y + 25)) return; if ((uint16_t)pop_y_land[Kid.curr_row + 1] > (uint16_t)(Kid.y + 25)) return;
old_x = Kid.x; old_x = Kid.x;
Kid.x = (uint8_t)char_dx_forward(-8); Kid.x = (uint8_t)char_dx_forward(-8);
determine_col(); determine_col();
@@ -527,10 +544,10 @@ static void check_grab(void)
determine_col(); determine_col();
} else { } else {
Kid.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight()); Kid.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight());
Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
Kid.fall_y = 0; Kid.fall_y = 0;
kid_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR); kid_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
play_seq(); pop_kid_play();
determine_col(); determine_col();
grab_timer = 12; /* (TODO: sound grab) */ grab_timer = 12; /* (TODO: sound grab) */
} }
@@ -539,8 +556,8 @@ static void check_grab(void)
static void do_fall(void) static void do_fall(void)
{ {
uint8_t nrow = (uint8_t)(Kid.curr_row + 1); uint8_t nrow = (uint8_t)(Kid.curr_row + 1);
if (nrow > 4) nrow = 4; /* защита y_land[] от выхода */ if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
if ((uint16_t)y_land[nrow] > (uint16_t)Kid.y) { if ((uint16_t)pop_y_land[nrow] > (uint16_t)Kid.y) {
check_grab(); /* ещё летит — попытка зацепа */ check_grab(); /* ещё летит — попытка зацепа */
} else { } else {
if (get_tile_at_char() == TILE_WALL) if (get_tile_at_char() == TILE_WALL)
@@ -587,7 +604,7 @@ static int edge_classify(int d)
return d; return d;
} }
int pop_edge_distance(void) int pop_edge_distance(void) __banked
{ {
uint8_t t; int d; uint8_t t; int d;
determine_col(); determine_col();
@@ -629,7 +646,7 @@ int pop_edge_distance(void)
return distance_to_edge_weight(); return distance_to_edge_weight();
} }
uint8_t pop_edge_type(void) { return g_edge_type; } uint8_t pop_edge_type(void) __banked { return g_edge_type; }
/* ---- Прыжок вверх с зацепом (seg005 check_jump_up и производные) --- * /* ---- Прыжок вверх с зацепом (seg005 check_jump_up и производные) --- *
* pop_jump_up_seq выбирает seq для ↑ в стойке: чистый прыжок (seq_28 в * pop_jump_up_seq выбирает seq для ↑ в стойке: чистый прыжок (seq_28 в
@@ -682,7 +699,7 @@ static uint8_t jump_up_or_grab(void)
return grab_up_with_floor_behind(); return grab_up_with_floor_behind();
} }
uint8_t pop_jump_up_seq(void) uint8_t pop_jump_up_seq(void) __banked
{ {
uint8_t through = get_tile_above_char(); uint8_t through = get_tile_above_char();
get_tile_front_above_char(); /* g_curr_tile = спереди-сверху */ get_tile_front_above_char(); /* g_curr_tile = спереди-сверху */
@@ -697,7 +714,7 @@ uint8_t pop_jump_up_seq(void)
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига — /* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */ * позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
int pop_wall_ahead(void) int pop_wall_ahead(void) __banked
{ {
int d = pop_edge_distance(); int d = pop_edge_distance();
return (g_edge_type == EDGE_WALL && d <= 2); return (g_edge_type == EDGE_WALL && d <= 2);
@@ -708,7 +725,7 @@ int pop_wall_ahead(void)
* тайловые предикаты + выбор seq (+ выравнивание Kid.x). */ * тайловые предикаты + выбор seq (+ выравнивание Kid.x). */
/* grab_timer истёк — можно подтягиваться? */ /* grab_timer истёк — можно подтягиваться? */
uint8_t pop_hang_grab_ready(void) { return grab_timer == 0; } uint8_t pop_hang_grab_ready(void) __banked { return grab_timer == 0; }
/* can_climb_up (seg005:09DF): тайл над персонажем — обычный подъём (seq_10) /* can_climb_up (seg005:09DF): тайл над персонажем — обычный подъём (seq_10)
* либо подъём к ЗАКРЫТОЙ калитке/зеркалу/чомперу (seq_73 = «влез и съехал * либо подъём к ЗАКРЫТОЙ калитке/зеркалу/чомперу (seq_73 = «влез и съехал
@@ -716,7 +733,7 @@ uint8_t pop_hang_grab_ready(void) { return grab_timer == 0; }
* curr_tile2 == tiles_4_gate && direction != right && modif >> 2 < 6 * curr_tile2 == tiles_4_gate && direction != right && modif >> 2 < 6
* — под поднятой решёткой (modif >> 2 >= 6) подъём обычный. Без проверки * — под поднятой решёткой (modif >> 2 >= 6) подъём обычный. Без проверки
* модификатора Kid срывался и с открытых ворот. */ * модификатора Kid срывался и с открытых ворот. */
uint8_t pop_hang_climb_up_seq(void) uint8_t pop_hang_climb_up_seq(void) __banked
{ {
uint8_t t = get_tile_above_char(); uint8_t t = get_tile_above_char();
if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Kid.direction == 0) || if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Kid.direction == 0) ||
@@ -728,7 +745,7 @@ uint8_t pop_hang_climb_up_seq(void)
/* Shift в висе: 1 = висит у стены/двери (caller играет seq_25); -1 = /* Shift в висе: 1 = висит у стены/двери (caller играет seq_25); -1 =
* сорваться (hang_fall); 0 = сверху пол, остаётся висеть (ничего). */ * сорваться (hang_fall); 0 = сверху пол, остаётся висеть (ничего). */
int8_t pop_hang_wall(void) int8_t pop_hang_wall(void) __banked
{ {
uint8_t t = get_tile_at_char(); uint8_t t = get_tile_at_char();
if (Kid.action != ACT_HANGSTRAIGHT && if (Kid.action != ACT_HANGSTRAIGHT &&
@@ -743,7 +760,7 @@ int8_t pop_hang_wall(void)
/* hang_fall (seg005:0A46): сорваться. Нет пола ни сзади, ни под /* hang_fall (seg005:0A46): сорваться. Нет пола ни сзади, ни под
* персонажем — seq_23 (отпустить и падать); иначе прижать к стене (-7) * персонажем — seq_23 (отпустить и падать); иначе прижать к стене (-7)
* и seq_11 (встать, конец спуска). Меняет Kid.x, возвращает seq. */ * и seq_11 (встать, конец спуска). Меняет Kid.x, возвращает seq. */
uint8_t pop_hang_fall_seq(void) uint8_t pop_hang_fall_seq(void) __banked
{ {
uint8_t behind = get_tile_behind_char(); uint8_t behind = get_tile_behind_char();
uint8_t at; uint8_t at;
@@ -760,7 +777,7 @@ uint8_t pop_hang_fall_seq(void)
* либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 = * либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
* приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки * приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки
* пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */ * пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */
uint8_t pop_down_action(void) uint8_t pop_down_action(void) __banked
{ {
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) { if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) {
Kid.x = (uint8_t)char_dx_forward(5); /* у передней кромки — отступ */ Kid.x = (uint8_t)char_dx_forward(5); /* у передней кромки — отступ */
@@ -792,6 +809,9 @@ uint8_t pop_down_action(void)
uint8_t pop_have_sword; uint8_t pop_have_sword;
uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */ uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */ uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это
* flash_color; нам хватает двух значений, которые реально встречаются. */
uint8_t pop_flash_red;
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */ static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить /* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
@@ -805,7 +825,7 @@ static void do_pickup(int8_t obj_type, uint8_t tilepos)
/* proc_get_object (seg006:16CB): эффект поднятого предмета. Зовётся из /* proc_get_object (seg006:16CB): эффект поднятого предмета. Зовётся из
* play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid). */ * play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid). */
void pop_proc_get_object(void) void pop_proc_get_object(void) __banked
{ {
if (pickup_obj_type == -1) { if (pickup_obj_type == -1) {
pop_have_sword = 1; /* в оригинале have_sword = -1 */ pop_have_sword = 1; /* в оригинале have_sword = -1 */
@@ -815,13 +835,35 @@ void pop_proc_get_object(void)
* чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */ * чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */
pop_flash_time = 8; pop_flash_time = 8;
} }
pickup_obj_type = 0; /* зелья (типы 1..6) не портированы */ else {
/* proc_get_object (seg006:1927), ветка зелий. Типы 3/4/6
* (перо/переворот/открыть все ворота) — свойства поздних уровней,
* их портируем вместе с этими уровнями. */
switch (pickup_obj_type) {
case 1: /* здоровье: +1 HP, красная вспышка */
if (hitp_curr != hitp_max) {
hitp_delta = 1;
pop_flash_red = 1;
pop_flash_time = 2;
}
break;
case 5: /* «злое» зелье: −1 HP */
hitp_delta = -1;
pop_flash_red = 1;
pop_flash_time = 2;
break;
default: /* 2 жизнь / 3 перо / 4 переворот /
* 6 открыть — TODO с их уровнями */
break;
}
}
pickup_obj_type = 0;
} }
/* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет /* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет
* (управление идёт обычной веткой); 1 — присесть (caller зовёт crouch()); * (управление идёт обычной веткой); 1 — присесть (caller зовёт crouch());
* 2 — поднять меч (caller ставит seq_91). */ * 2 — поднять меч (caller ставит seq_91). */
uint8_t pop_get_item_action(void) uint8_t pop_get_item_action(void) __banked
{ {
uint8_t t; uint8_t t;
int8_t icol; int8_t icol;
@@ -844,9 +886,17 @@ uint8_t pop_get_item_action(void)
Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2)); Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
return 1; return 1;
} }
if (t != TILE_SWORD) return 0; /* питьё зелий не портировано */ {
do_pickup(-1, (uint8_t)(Kid.curr_row * 10 + icol)); uint8_t tp = (uint8_t)(Kid.curr_row * 10 + icol);
return 2; if (t == TILE_SWORD) {
do_pickup(-1, tp);
return 2; /* поднять меч (seq_91) */
}
/* Зелье: тип лежит в СТАРШИХ битах модификатора (get_item,
* seg005: do_pickup(curr_room_modif[curr_tilepos] >> 3)). */
do_pickup((int8_t)(pop_trob_modif(g_room)[tp] >> 3), tp);
return 3; /* пить (seq_78) */
}
} }
/* ---- Диспетчер ----------------------------------------------------- */ /* ---- Диспетчер ----------------------------------------------------- */
@@ -885,7 +935,7 @@ static void bumped_fall(void)
Kid.fall_x = 0; Kid.fall_x = 0;
} else { } else {
kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */ kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */
play_seq(); pop_kid_play();
} }
} }
@@ -900,11 +950,11 @@ static void bumped_floor(void)
uint8_t frame; uint8_t frame;
/* Оригинал сравнивает БЕЗЗНАКОВО: если персонаж НИЖЕ уровня пола, /* Оригинал сравнивает БЕЗЗНАКОВО: если персонаж НИЖЕ уровня пола,
* разность заворачивается в большое число и тоже даёт «падать». */ * разность заворачивается в большое число и тоже даёт «падать». */
if ((uint16_t)(y_land[Kid.curr_row + 1] - (int16_t)Kid.y) >= 15) { if ((uint16_t)(pop_y_land[Kid.curr_row + 1] - (int16_t)Kid.y) >= 15) {
bumped_fall(); /* высоко над полом — в воздухе */ bumped_fall(); /* высоко над полом — в воздухе */
return; return;
} }
Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; /* ПРИЖАТЬ к полу */ Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; /* ПРИЖАТЬ к полу */
if (Kid.fall_y >= 22) { /* влетел быстро — только отжать */ if (Kid.fall_y >= 22) { /* влетел быстро — только отжать */
Kid.x = (uint8_t)char_dx_forward(-5); Kid.x = (uint8_t)char_dx_forward(-5);
return; return;
@@ -919,7 +969,7 @@ static void bumped_floor(void)
kid_set_seq(SEQ_46_HARDBUMP); kid_set_seq(SEQ_46_HARDBUMP);
else else
kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */ kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */
play_seq(); pop_kid_play();
} }
/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять /* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять
@@ -942,7 +992,7 @@ static void check_bumped(void)
* hang->hang_fall->seq_11 (правильное приземление). */ * hang->hang_fall->seq_11 (правильное приземление). */
if (Kid.frame >= 87 && Kid.frame < 100) return; if (Kid.frame >= 87 && Kid.frame < 100) return;
/* Вне рядов комнаты (падение мимо пола): get_tile даёт WALL, а /* Вне рядов комнаты (падение мимо пола): get_tile даёт WALL, а
* dist_from_wall_forward/x_bump[] — мусор с большим отриц. сдвигом → * dist_from_wall_forward/pop_x_bump[] — мусор с большим отриц. сдвигом →
* underflow X и уход в стену. fell_out ловит do_fall. */ * underflow X и уход в стену. fell_out ловит do_fall. */
if (Kid.curr_row < 0 || Kid.curr_row > 2) return; if (Kid.curr_row < 0 || Kid.curr_row > 2) return;
/* seg004 check_collisions: на кадрах разворота коллизии НЕ считаются /* seg004 check_collisions: на кадрах разворота коллизии НЕ считаются
@@ -973,7 +1023,7 @@ static void check_bumped(void)
{ {
uint8_t f = 0, prev, need; uint8_t f = 0, prev, need;
if (wt) { if (wt) {
int coll_left = x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; int coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
int lw = wall_dl[wt] + coll_left; /* get_left_wall_xpos */ int lw = wall_dl[wt] + coll_left; /* get_left_wall_xpos */
int rw = coll_left - wall_dr[wt] + TILE_RIGHTX; /* get_right_wall_xpos */ int rw = coll_left - wall_dr[wt] + TILE_RIGHTX; /* get_right_wall_xpos */
int e = char_x_forward_edge(), wh = ((int)kid_fp_width() + 1) >> 1; int e = char_x_forward_edge(), wh = ((int)kid_fp_width() + 1) >> 1;
@@ -1027,8 +1077,6 @@ static void check_bumped(void)
* отсчёт до падения (на тайле стоят/пробили). Порт animate_loose/ * отсчёт до падения (на тайле стоят/пробили). Порт animate_loose/
* make_loose_fall/check_press. Перерисовка — в pop_bg (shake/bake). */ * make_loose_fall/check_press. Перерисовка — в pop_bg (shake/bake). */
uint8_t pop_loose_modif[30]; /* публично: читает pop_bg при отрисовке */ uint8_t pop_loose_modif[30]; /* публично: читает pop_bg при отрисовке */
static uint8_t loose_bake[30]; /* счётчик «запечь пусто» (2 = обе страницы) */
static uint8_t loose_rest[30]; /* счётчик «перерисовать ПОКОЙ» после тряски (2 = обе стр.) */
/* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */ /* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */
@@ -1115,7 +1163,7 @@ static void do_knock(int tile_row)
/* check_knock (seg003:0706): по флагу knock (ставит play_seq на seqtbl- /* check_knock (seg003:0706): по флагу knock (ставит play_seq на seqtbl-
* командах KNOCK_UP/DOWN) тряхнуть ряд. KNOCK_DOWN (knock=-1, приземление) → * командах KNOCK_UP/DOWN) тряхнуть ряд. KNOCK_DOWN (knock=-1, приземление) →
* ряд curr_row; KNOCK_UP (knock=+1) → ряд curr_row-1. */ * ряд curr_row; KNOCK_UP (knock=+1) → ряд curr_row-1. */
void pop_check_knock(void) void pop_check_knock(void) __banked
{ {
if (knock) { if (knock) {
do_knock(Kid.curr_row - (knock > 0)); do_knock(Kid.curr_row - (knock > 0));
@@ -1126,23 +1174,18 @@ void pop_check_knock(void)
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest /* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
* индексируются позицией тайла и не должны утечь из старой комнаты в новую. * индексируются позицией тайла и не должны утечь из старой комнаты в новую.
* Плюс останавливает падающий кусок (pop_bg). */ * Плюс останавливает падающий кусок (pop_bg). */
void pop_loose_reset(void) void pop_loose_reset(void) __banked
{ {
int i; int i;
for (i = 0; i < 30; i++) { for (i = 0; i < 30; i++) {
pop_loose_modif[i] = 0; pop_loose_modif[i] = 0;
loose_bake[i] = 0;
loose_rest[i] = 0;
} }
for (i = 0; i < 10; i++) { for (i = 0; i < 10; i++) {
pop_ceil_modif[i] = 0; pop_ceil_modif[i] = 0;
ceil_rest[i] = 0;
ceil_bake[i] = 0;
} }
pop_ceil_fell = 0; pop_ceil_fell = 0;
pop_debris_at = 0; pop_debris_at = 0;
pop_loose_landed = 0; pop_loose_landed = 0;
land_bake = 0;
pop_loose_mob_reset(); pop_loose_mob_reset();
} }
@@ -1159,18 +1202,18 @@ static void fell_on_your_head(void)
uint8_t frame = Kid.frame, action = Kid.action; uint8_t frame = Kid.frame, action = Kid.action;
if ((frame < 5 || frame >= 15) && if ((frame < 5 || frame >= 15) &&
(action < ACT_HANGCLIMB || action == ACT_TURN)) { (action < ACT_HANGCLIMB || action == ACT_TURN)) {
Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */ pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */
if (take_hp(1)) { if (take_hp(1)) {
kid_set_seq(SEQ_22_CRUSHED); kid_set_seq(SEQ_22_CRUSHED);
play_seq(); pop_kid_play();
determine_col(); determine_col();
pop_kid_dead = 1; pop_kid_dead = 1;
} else if (frame != FRAME_109_CROUCH) { } else if (frame != FRAME_109_CROUCH) {
if (get_tile_behind_char() == TILE_EMPTY) if (get_tile_behind_char() == TILE_EMPTY)
Kid.x = (uint8_t)char_dx_forward(-2); Kid.x = (uint8_t)char_dx_forward(-2);
kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID); kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID);
play_seq(); pop_kid_play();
determine_col(); determine_col();
} }
} }
@@ -1190,7 +1233,7 @@ static void check_loose_fall_on_kid(void)
/* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ /* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ
* (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */ * (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */
void pop_loose_tick(void) void pop_loose_tick(void) __banked
{ {
int pos; int pos;
/* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80 /* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80
@@ -1201,24 +1244,17 @@ void pop_loose_tick(void)
* Инкремент+сравнение считают row/col без деления. */ * Инкремент+сравнение считают row/col без деления. */
uint8_t row = 0, col = 0; uint8_t row = 0, col = 0;
for (pos = 0; pos < 30; pos++) { for (pos = 0; pos < 30; pos++) {
if (loose_bake[pos]) { /* упал: запечь пусто (2 кадра=2 стр.) */ if (pop_loose_modif[pos] != 0) {
pop_loose_bake_empty(row, col);
loose_bake[pos]--;
} else if (loose_rest[pos]) { /* тряска кончилась: перерисовать ПОКОЙ */
/* modif уже 0 → shake_draw рисует базовый кадр (покой) обоих тайлов
* (col и col+1: правая грань loose живёт в 2,7). 2 кадра = обе
* страницы дабл-буфера — иначе на одной остаётся застывший
* дрожащий кадр правой грани → мерцание («бесконечная тряска»). */
pop_loose_shake_draw(row, col);
loose_rest[pos]--;
} else if (pop_loose_modif[pos] != 0) {
uint8_t m = ++pop_loose_modif[pos]; uint8_t m = ++pop_loose_modif[pos];
if (m & 0x80) { /* тряска (do_knock) */ if (m & 0x80) { /* тряска (do_knock) */
if (m >= 0x84) { /* конец тряски: сброс + перерисовать покой */ if (m >= 0x84) { /* конец тряски: сброс + покой */
pop_loose_modif[pos] = 0; pop_loose_modif[pos] = 0;
loose_rest[pos] = 2; /* покой на ОБЕИХ страницах */ /* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
* правая грань loose живёт в соседе). ОБЕ страницы —
* иначе на одной застывает дрожащий кадр → мерцание. */
pop_set_redraw(pos, POP_RD_LOOSE, 2);
} else { } else {
pop_loose_shake_draw(row, col); /* дрожащий кадр */ pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */
} }
} else if (m >= 11) { /* loose_floor_delay — падение */ } else if (m >= 11) { /* loose_floor_delay — падение */
/* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!). /* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!).
@@ -1228,12 +1264,11 @@ void pop_loose_tick(void)
* (стена-вниз от тайла снизу + чёрный фон). */ * (стена-вниз от тайла снизу + чёрный фон). */
g_fg[pos] = TILE_EMPTY; g_fg[pos] = TILE_EMPTY;
pop_loose_modif[pos] = 0; pop_loose_modif[pos] = 0;
pop_loose_bake_empty(row, col); /* запечь пусто на ТЕКУЩЕЙ странице сразу */ pop_set_redraw(pos, POP_RD_LOOSE_GONE, 2); /* запечь пусто, обе стр. */
loose_bake[pos] = 1; /* + вторая страница следующим кадром */ pop_loose_mob_spawn(row, col); /* отрыв падающего куска */
pop_loose_mob_spawn(row, col); /* отрыв падающего куска (L4) */ pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению: кусок улетел */
} else { /* отсчёт 1..10 — дрожащий кадр */ } else { /* отсчёт 1..10 — дрожащий кадр */
pop_loose_shake_draw(row, col); pop_set_redraw(pos, POP_RD_LOOSE, 1);
} }
} }
if (++col == 10) { col = 0; row++; } /* row/col без деления */ if (++col == 10) { col = 0; row++; } /* row/col без деления */
@@ -1242,27 +1277,21 @@ void pop_loose_tick(void)
* рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в * рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в
* НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */ * НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */
for (col = 0; col < 10; col++) { for (col = 0; col < 10; col++) {
if (ceil_rest[col]) { if (pop_ceil_modif[col] != 0) {
pop_ceil_shake_draw(col); /* modif уже 0 → кадр покоя */
ceil_rest[col]--;
} else if (pop_ceil_modif[col] != 0) {
uint8_t m = ++pop_ceil_modif[col]; uint8_t m = ++pop_ceil_modif[col];
if (m & 0x80) { /* тряска от сотрясения (do_knock) */ if (m & 0x80) { /* тряска от сотрясения (do_knock) */
if (m >= 0x84) { pop_ceil_modif[col] = 0; ceil_rest[col] = 2; } if (m >= 0x84) { pop_ceil_modif[col] = 0;
else pop_ceil_shake_draw(col); pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
else pop_set_redraw_above(col, POP_RDA_CEIL, 1);
} else if (m >= 11) { /* loose_floor_delay — плита рушится */ } else if (m >= 11) { /* loose_floor_delay — плита рушится */
pop_ceil_modif[col] = 0; pop_ceil_modif[col] = 0;
if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */ if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */
pop_ceil_bake_empty(col); /* колодец наверх на ТЕКУЩЕЙ странице */ pop_set_redraw_above(col, POP_RDA_CEIL_GONE, 2); /* колодец, обе стр. */
ceil_bake[col] = 1; /* + вторая страница следующим кадром */
pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */ pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */
pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */ pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */
} else { } else {
pop_ceil_shake_draw(col); /* отсчёт 1..10 — дрожащий кадр */ pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */
} }
} else if (ceil_bake[col]) {
pop_ceil_bake_empty(col);
ceil_bake[col]--;
} }
} }
pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */ pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
@@ -1270,34 +1299,22 @@ void pop_loose_tick(void)
/* Отложенная допечка ПРОШЛОГО приземления на вторую страницу — ПЕРЕД /* Отложенная допечка ПРОШЛОГО приземления на вторую страницу — ПЕРЕД
* разбором нового: иначе при двух плитах, севших подряд, land_bake * разбором нового: иначе при двух плитах, севших подряд, land_bake
* затирался новым и на одной странице оставался старый пол. */ * затирался новым и на одной странице оставался старый пол. */
if (land_bake && pop_loose_landed) {
uint8_t r = 0, c = land_bake_pos;
while (c >= 10) { c -= 10; r++; }
pop_floor_bake(r, c);
land_bake = 0;
}
if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */ if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */
uint8_t pos = (uint8_t)(pop_loose_landed - 1); uint8_t pos = (uint8_t)(pop_loose_landed - 1);
uint8_t r = 0, c = pos; uint8_t r = 0, c = pos;
pop_loose_landed = 0; pop_loose_landed = 0;
while (c >= 10) { c -= 10; r++; } while (c >= 10) { c -= 10; r++; }
g_fg[pos] = TILE_DEBRIS; g_fg[pos] = TILE_DEBRIS;
/* Перерисовка ПО ПЛОЩАДИ ПОЛА (не bake_empty: та стирает и верх стены /* Перерисовка ПО ПЛОЩАДИ ПОЛА (не LOOSE_GONE: та стирает и верх стены
* ряда ниже — оставался чёрный прямоугольник под тайлом). */ * ряда ниже — оставался чёрный прямоугольник под тайлом). Обе
pop_floor_bake(r, c); * страницы дабл-буфера. */
land_bake_pos = pos; land_bake = 1; /* вторая страница дабл-буфера */ pop_set_redraw(pos, POP_RD_FLOOR, 2);
do_knock(r); /* удар плиты трясёт loose этого ряда */ do_knock(r); /* удар плиты трясёт loose этого ряда */
pop_debris_at = (uint8_t)(pos + 1); /* сигнал приложению (персистентность) */ pop_debris_at = (uint8_t)(pos + 1); /* сигнал приложению (персистентность) */
} else if (land_bake) {
uint8_t r = 0, c = land_bake_pos;
while (c >= 10) { c -= 10; r++; }
pop_floor_bake(r, c);
land_bake = 0;
} }
} }
/* y_to_row_mod4 (seg006): ряд по y с оборотом mod4 (TILE_SIZEY=63). */
static int8_t y_to_row(int16_t y) { return (int8_t)(((y + 60) / 63) % 4 - 1); }
/* ---- clip_char (seg006:1749), первый блок: обрезка спрайта СВЕРХУ ----- * /* ---- clip_char (seg006:1749), первый блок: обрезка спрайта СВЕРХУ ----- *
* Оригинал перед add_objtable кладёт в objtable прямоугольник клипа; блиттер * Оригинал перед add_objtable кладёт в objtable прямоугольник клипа; блиттер
@@ -1313,7 +1330,7 @@ static int8_t y_to_row(int16_t y) { return (int8_t)(((y + 60) / 63) % 4 - 1); }
* check_spike_below колонки берутся get_tile_div_mod БЕЗ 7. * check_spike_below колонки берутся get_tile_div_mod БЕЗ 7.
* *
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */ * Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
{ {
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 }; static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
int wh = ((int)w + 1) >> 1; /* char_width_half */ int wh = ((int)w + 1) >> 1; /* char_width_half */
@@ -1326,7 +1343,7 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h)
xr = xl + wh; /* char_x_right */ xr = xl + wh; /* char_x_right */
top_y = obj_y - (int)h + 1; /* char_top_y */ top_y = obj_y - (int)h + 1; /* char_top_y */
if (top_y >= 192) top_y = 0; /* весь спрайт ниже комнаты */ if (top_y >= 192) top_y = 0; /* весь спрайт ниже комнаты */
trow = y_to_row((int16_t)top_y); /* char_top_row */ trow = pop_y_to_row((int16_t)top_y); /* char_top_row */
cL = get_tile_div_mod(xl); if (cL < 0) cL = 0; cL = get_tile_div_mod(xl); if (cL < 0) cL = 0;
cR = get_tile_div_mod(xr); if (cR > 9) cR = 9; cR = get_tile_div_mod(xr); if (cR > 9) cR = 9;
@@ -1364,13 +1381,13 @@ static void check_leave_below(void)
Kid.action != ACT_MIDAIR && Kid.action != ACT_MIDAIR &&
(int8_t)Kid.y < 10 && (int8_t)Kid.y > -16) { (int8_t)Kid.y < 10 && (int8_t)Kid.y > -16) {
Kid.y = (uint8_t)(Kid.y + 189); Kid.y = (uint8_t)(Kid.y + 189);
Kid.curr_row = y_to_row((int16_t)Kid.y); Kid.curr_row = pop_y_to_row((int16_t)Kid.y);
pop_leave_dir = 3; pop_leave_dir = 3;
return; return;
} }
if (Kid.y >= 211) { if (Kid.y >= 211) {
Kid.y = (uint8_t)(Kid.y - 189); Kid.y = (uint8_t)(Kid.y - 189);
Kid.curr_row = y_to_row((int16_t)Kid.y); Kid.curr_row = pop_y_to_row((int16_t)Kid.y);
pop_fell_out = 1; pop_fell_out = 1;
} }
} }
@@ -1456,13 +1473,13 @@ static int is_spike_harmful(uint8_t tilepos)
static void spiked(uint8_t tilepos) static void spiked(uint8_t tilepos)
{ {
pop_trob_modif(g_room)[tilepos] = 0xFF; pop_trob_modif(g_room)[tilepos] = 0xFF;
Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
Kid.x = (uint8_t)(x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + 10); Kid.x = (uint8_t)(pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
Kid.x = (uint8_t)char_dx_forward(8); Kid.x = (uint8_t)char_dx_forward(8);
Kid.fall_y = 0; Kid.fall_y = 0;
take_hp(100); /* (TODO: sound_48_spiked) */ take_hp(100); /* (TODO: sound_48_spiked) */
kid_set_seq(SEQ_51_SPIKED); kid_set_seq(SEQ_51_SPIKED);
play_seq(); pop_kid_play();
determine_col(); determine_col();
pop_kid_dead = 1; pop_kid_dead = 1;
} }
@@ -1514,7 +1531,7 @@ static void check_spike_below(void)
} }
} }
void pop_phys_tick(void) void pop_phys_tick(void) __banked
{ {
if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */ if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */
if (grab_timer) grab_timer--; if (grab_timer) grab_timer--;
+30 -21
View File
@@ -17,25 +17,32 @@
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив * col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
* МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию, * МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию,
* что и в pop_room_draw, чтобы отрисовка видела изменения. */ * что и в pop_room_draw, чтобы отрисовка видела изменения. */
void pop_map_set(uint8_t *fg); void pop_map_set(uint8_t *fg) __banked;
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии: /* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2) * lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2)
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая * в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */ * колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg); const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked;
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob). /* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
* Звать в enter_room. */ * Звать в enter_room. */
void pop_map_set_room(uint8_t room); void pop_map_set_room(uint8_t room) __banked;
/* Тайл-код текущей комнаты по клетке (col 0..9, row 0..2) — порт
* get_tile(room,col,row) для текущей комнаты. Нужен лучу видимости стража
* (guards.c): он идёт по ряду между Кидом и стражем. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP. /* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/ * pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
* респавн). */ * респавн). */
extern uint8_t pop_kid_dead; extern uint8_t pop_kid_dead;
extern uint8_t hitp_curr; extern uint8_t hitp_curr;
void pop_kid_hp_reset(void); extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
void pop_kid_hp_reset(void) __banked;
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object). /* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
* pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или * pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или
@@ -46,8 +53,10 @@ extern uint8_t pop_item_taken;
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока /* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */ * НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
extern uint8_t pop_flash_time; extern uint8_t pop_flash_time;
uint8_t pop_get_item_action(void); /* 0 нет / 1 присесть / 2 поднять меч */ /* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */
void pop_proc_get_object(void); /* эффект — из play_seq (опкод GET_ITEM) */ extern uint8_t pop_flash_red;
uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */
void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для /* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
* pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189). * pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189).
@@ -66,8 +75,8 @@ extern uint8_t pop_leave_timer;
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back), * на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
* рядом с kid-отрисовкой. */ * рядом с kid-отрисовкой. */
extern uint8_t pop_loose_modif[30]; extern uint8_t pop_loose_modif[30];
void pop_loose_tick(void); void pop_loose_tick(void) __banked;
void pop_loose_reset(void); /* сброс loose-состояния при смене комнаты */ void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ). /* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */ * Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
@@ -81,7 +90,7 @@ extern uint8_t pop_loose_fell;
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности * pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты, * между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */ * pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
void pop_map_set_above(uint8_t *above_row2_fg); void pop_map_set_above(uint8_t *above_row2_fg) __banked;
extern uint8_t pop_ceil_modif[10]; extern uint8_t pop_ceil_modif[10];
extern uint8_t pop_ceil_fell; extern uint8_t pop_ceil_fell;
extern uint8_t pop_debris_at; extern uint8_t pop_debris_at;
@@ -92,7 +101,7 @@ extern uint8_t pop_kid_hurt;
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда /* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
* приземления/удара. Зовётся из pop_phys_tick. */ * приземления/удара. Зовётся из pop_phys_tick. */
void pop_check_knock(void); void pop_check_knock(void) __banked;
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру /* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр * или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
@@ -104,14 +113,14 @@ extern uint8_t pop_fell_out;
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed + /* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
* determine_col + check_action (триггер падения / do_fall / приземление). * determine_col + check_action (триггер падения / do_fall / приземление).
* Порядок как в оригинале (seg000 play_kid_frame). */ * Порядок как в оригинале (seg000 play_kid_frame). */
void pop_phys_tick(void); void pop_phys_tick(void) __banked;
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл /* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
* над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid, * над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid,
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели). * ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
* obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер. * obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер.
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */ * Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h); int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после /* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */ * pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
@@ -120,26 +129,26 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h);
#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */ #define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */
/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */ /* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */
int pop_edge_distance(void); int pop_edge_distance(void) __banked;
uint8_t pop_edge_type(void); uint8_t pop_edge_type(void) __banked;
/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной /* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной
* позиции (передний край у стены, симметрично) и возвращает 1. Для * позиции (передний край у стены, симметрично) и возвращает 1. Для
* control_running (чистый стоп у стены). */ * control_running (чистый стоп у стены). */
int pop_wall_ahead(void); int pop_wall_ahead(void) __banked;
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО /* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x. * прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
* check_jump_up/grab_up (K4.4). control_up гасит caller. */ * check_jump_up/grab_up (K4.4). control_up гасит caller. */
uint8_t pop_jump_up_seq(void); uint8_t pop_jump_up_seq(void) __banked;
/* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- * /* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- *
* Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в * Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в
* pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */ * pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */
uint8_t pop_hang_grab_ready(void); /* grab_timer истёк — можно climb-up */ uint8_t pop_hang_grab_ready(void) __banked; /* grab_timer истёк — можно climb-up */
uint8_t pop_hang_climb_up_seq(void); /* seq_10 / seq_73 (подтягивание) */ uint8_t pop_hang_climb_up_seq(void) __banked; /* seq_10 / seq_73 (подтягивание) */
int8_t pop_hang_wall(void); /* Shift в висе: 1=у стены,-1=fall,0=держ.*/ int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
uint8_t pop_hang_fall_seq(void); /* seq_23 / seq_11 (сорваться), +Kid.x */ uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */
uint8_t pop_down_action(void); /* 1=обработано(спуск/отступ), 0=присед */ uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */
#endif #endif
+78
View File
@@ -0,0 +1,78 @@
/*
* pop_redraw.c — пометки перерисовки тайлов и их разбор. См. pop_redraw.h.
*
* Живёт в W1/W2 (НЕ в резиденте W3): пометки ставит логика, которая должна
* оставаться достижимой из будущих __banked-модулей. Сам разбор зовёт
* pop_bg (резидент W3) — это законно, потому что разбор дёргает только
* главный цикл.
*/
#include <stdint.h>
#include "pop_redraw.h"
#include "pop_bg.h"
#define NTILES 30
#define NCOLS 10
static uint8_t rd_kind[NTILES], rd_cnt[NTILES];
static uint8_t rda_kind[NCOLS], rda_cnt[NCOLS];
/* Сколько всего непустых пометок: без него проход по 40 ячейкам крутился бы
* КАЖДЫЙ кадр впустую (замер 2026-07-27 на прежнем rest-цикле: ~12 % кадра). */
static uint8_t rd_pending;
void pop_redraw_reset(void)
{
uint8_t i;
for (i = 0; i < NTILES; i++) { rd_kind[i] = 0; rd_cnt[i] = 0; }
for (i = 0; i < NCOLS; i++) { rda_kind[i] = 0; rda_cnt[i] = 0; }
rd_pending = 0;
}
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)
{
if (tilepos >= NTILES) return;
if (!rd_cnt[tilepos]) rd_pending++;
rd_kind[tilepos] = kind;
if (pages > rd_cnt[tilepos]) rd_cnt[tilepos] = pages;
}
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages)
{
if (col >= NCOLS) return;
if (!rda_cnt[col]) rd_pending++;
rda_kind[col] = kind;
if (pages > rda_cnt[col]) rda_cnt[col] = pages;
}
/* redraw_needed (seg008:0178): пройти пометки и нарисовать. row/col ведём
* бегущими счётчиками, а не через i/10 и i%10 — у Z80 нет деления, и SDCC
* выносит __divuchar/__moduchar в вершину цикла как общее подвыражение
* аргументов (30*2 делений за кадр даже когда рисовать нечего). */
void pop_redraw_needed(void)
{
uint8_t i, row = 0, col = 0, left = 0;
if (!rd_pending) return;
for (i = 0; i < NTILES; i++) {
if (rd_cnt[i]) {
switch (rd_kind[i]) {
case POP_RD_SPIKE: pop_spike_redraw(row, col); break;
case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break;
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break;
case POP_RD_FLOOR: pop_floor_bake(row, col); break;
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break;
default: break;
}
if (--rd_cnt[i]) left++;
else rd_kind[i] = 0;
}
if (++col == 10) { col = 0; row++; }
}
for (i = 0; i < NCOLS; i++) {
if (rda_cnt[i]) {
if (rda_kind[i] == POP_RDA_CEIL) pop_ceil_shake_draw(i);
else pop_ceil_bake_empty(i);
if (--rda_cnt[i]) left++;
else rda_kind[i] = 0;
}
}
rd_pending = left;
}
+50
View File
@@ -0,0 +1,50 @@
/*
* pop_redraw.h — пометки «перерисовать тайл» и их разбор.
*
* Порт архитектуры оригинала: логика анимации НИЧЕГО не рисует, она ставит
* флаг (`set_redraw_full`/`set_wipe`/`redraw_20h`/`redraw_21h`, seg007), а
* отрисовка идёт отдельным проходом (`redraw_needed`, seg008:0178). У нас
* так же: pop_map/pop_trob зовут pop_set_redraw*, а pop_redraw_needed()
* (главный цикл) разбирает пометки и дёргает pop_bg.
*
* ЗАЧЕМ (кроме сходства с SDLPoP): из __banked-кода резидентная страница W3
* недостижима — ни напрямую, ни транзитивно (docs/layout_plan_v2.md §2 R2).
* Пока pop_map звал pop_bg, его нельзя было использовать из банка стражей.
* Пометки живут в W1/W2 и зовутся откуда угодно.
*
* ОТЛИЧИЕ ОТ ОРИГИНАЛА: у нас не один экран с dirty-rect, а две страницы
* дабл-буфера со своей ОЗУ-копией фона, поэтому счётчик пометки — это ЧИСЛО
* СТРАНИЦ (обычно 2), а вид перерисовки хранится явно: heal+рисовать поверх
* (транзиент) или запечь новый фон. В оригинале вид кодируется тем, в какой
* из таблиц redraw_frames_* стоит флаг.
*/
#ifndef POP_REDRAW_H
#define POP_REDRAW_H
#include <stdint.h>
/* Виды перерисовки тайла (row*10+col). */
#define POP_RD_SPIKE 1 /* пики: heal + оба тайла (redraw_21h) */
#define POP_RD_LOOSE 2 /* loose: дрожащий кадр / кадр покоя */
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
/* Виды перерисовки полосы у потолка (по колонке). */
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
#define POP_RDA_CEIL_GONE 2 /* плита-потолок провалилась: колодец */
/* Пометить тайл/колонку: kind — вид, pages — на скольких страницах
* дабл-буфера перерисовать (1 = только текущая, 2 = обе). Повторная
* пометка перекрывает вид и берёт максимум счётчика. */
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages);
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages);
/* Разобрать пометки: нарисовать всё помеченное на ТЕКУЩЕЙ (back) странице и
* уменьшить счётчики. Звать из главного цикла в слое ФОНА — до kid_draw. */
void pop_redraw_needed(void);
/* Сбросить все пометки (смена комнаты/уровня). */
void pop_redraw_reset(void);
#endif
+19
View File
@@ -0,0 +1,19 @@
/*
* pop_state.c — данные, которые читают ОБЕ стороны банковой границы.
*
* Зачем отдельный модуль: sprinter-cc собирает банковый модуль с
* --dataseg BANKn, поэтому ВСЯ его писучая память (глобалы, file-static,
* статики внутри функций) линкуется В СТРАНИЦУ БАНКА и снаружи не читается
* — проверено на .map (2026-07-30: глобал банкового модуля лёг по
* 0x0001C000, а не в _DATA). Значит любую переменную, к которой нужен
* доступ и из банка, и снаружи, надо держать в НЕбанковом модуле — здесь.
*
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
*/
#include "pop_state.h"
uint8_t pop_loose_landed;
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
* но HP не теряет — иначе проверять боёвку приходится с трёх попаданий. */
uint8_t pop_immortal;
+21
View File
@@ -0,0 +1,21 @@
/*
* pop_state.h — состояние, общее для банков и W1/W2. См. pop_state.c.
*
* Правило: если переменную читают/пишут по обе стороны банковой границы —
* её место здесь, а НЕ в банковом модуле (там она уедет в страницу банка).
*/
#ifndef POP_STATE_H
#define POP_STATE_H
#include <stdint.h>
/* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе
* tilepos+1 тайла, на который он лёг. Ставит отрисовка mob (банк),
* разбирает логика loose-полов (pop_map, W1/W2). */
extern uint8_t pop_loose_landed;
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
extern uint8_t pop_immortal;
#endif
+45 -74
View File
@@ -5,7 +5,14 @@
*/ */
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_level.h" #include "pop_level.h"
#include "pop_bg.h" /* pop_spike_redraw */ #include "pop_geom.h"
#include "pop_redraw.h" /* пометки перерисовки тайлов (вместо прямых вызовов) */
/* pop_bg нужен ТОЛЬКО для пламени факела и пузырька зелья: это не тайловая
* перерисовка, а покадровый оверлей поверх фона (в оригинале — свои
* add_backtable/add_foretable внутри draw_tile). Всё остальное идёт через
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
* нельзя звать из __banked-кода. */
#include "pop_bg.h"
/* Коды тайлов (fg & 0x1F), которые анимируются. */ /* Коды тайлов (fg & 0x1F), которые анимируются. */
#define TILE_SPIKE 0x02 #define TILE_SPIKE 0x02
@@ -30,11 +37,6 @@ static trob_t trobs[TROBS_MAX];
static uint8_t trobs_count; static uint8_t trobs_count;
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */ static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
/* Отложенная перерисовка «покоя» законченных пик/кнопок текущей комнаты
* (2 кадра = обе страницы дабл-буфера, как loose_rest). */
static uint8_t spike_rest[POP_ROOMTILES];
static uint8_t button_rest[POP_ROOMTILES];
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button: /* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
* признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её * признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под * изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
@@ -43,36 +45,19 @@ static uint8_t button_rest[POP_ROOMTILES];
* расхождение было именно в безусловном редрое). */ * расхождение было именно в безусловном редрое). */
static uint8_t trob_drawn[POP_ROOMTILES]; static uint8_t trob_drawn[POP_ROOMTILES];
/* Сколько тайлов ждут отложенного редроя (spike_rest/button_rest). Без
* него цикл по 30 тайлам крутился КАЖДЫЙ кадр впустую: замер в MAME
* (2026-07-27) — служебная часть pop_process_trobs ~50 000 тактов при
* кадре 430 080, то есть ~12 % кадра на пустой проход. */
static uint8_t rest_pending;
/* Дверь уровня: отложенный редрой ВТОРОЙ страницы дабл-буфера. Створка
* анимируется покадрово в back-страницу, поэтому последний кадр анимации
* попадает только на одну из двух страниц — вторая застревает на шаг раньше
* и дверь «дрожит» через кадр. ldoor_pos = tilepos ПРАВОЙ половины. */
static uint8_t ldoor_rest, ldoor_pos;
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и /* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
* выбора следующего кадра пламени. */ * выбора следующего кадра пламени. */
static unsigned long trob_seed; static pop_rnd_t trob_seed;
static uint16_t trob_prandom(uint16_t maxv) #define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
{
trob_seed = trob_seed * 214013UL + 2531011UL;
return (uint16_t)((uint16_t)(trob_seed >> 16) % (uint16_t)(maxv + 1));
}
void pop_trob_reset(void) void pop_trob_reset(void)
{ {
uint8_t i; uint8_t i;
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0; for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
rest_pending = 0; pop_redraw_reset();
ldoor_rest = 0;
trobs_count = 0; trobs_count = 0;
} }
@@ -116,9 +101,8 @@ void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type)
void pop_trob_room_changed(void) void pop_trob_room_changed(void)
{ {
uint8_t i; uint8_t i;
for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
rest_pending = 0; pop_redraw_reset();
ldoor_rest = 0;
} }
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её /* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
@@ -361,38 +345,35 @@ uint8_t pop_trob_room_active(uint8_t room)
return 0; return 0;
} }
static uint8_t torch_tick; /* делитель скорости анимации пламени */
/* Темп анимации пламени в ЛОГИЧЕСКИХ кадрах (один логический кадр = 3
* vsync и соответствует игровому тику оригинала):
* 1 — кадр пламени каждый тик, как в оригинале; кадры при этом ровные по
* тактам, что нужно для счёта бюджета;
* 2 — вдвое реже (так было до 2026-07-30; пламя шло на половинной
* скорости, а бюджет кадра «прыгал» на 6.2 % от того, попал кадр на
* анимацию или нет). */
#define TORCH_ANIM_DIV 1
#if TORCH_ANIM_DIV > 1
static uint8_t torch_tick;
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
#else
#define TORCH_ANIM_NOW 1
#endif
void pop_process_trobs(uint8_t cur_room) void pop_process_trobs(uint8_t cur_room)
{ {
uint8_t i, w = 0; uint8_t i, w = 0;
#if TORCH_ANIM_DIV > 1
torch_tick++; torch_tick++;
#endif
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы). /* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC * row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее * выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
* подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет * подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */ * активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
if (rest_pending) {
uint8_t row = 0, col = 0, left = 0;
for (i = 0; i < POP_ROOMTILES; i++) {
if (spike_rest[i]) {
pop_spike_redraw(row, col);
if (--spike_rest[i]) left++;
}
if (button_rest[i]) {
pop_button_redraw(row, col);
if (--button_rest[i]) left++;
}
if (++col == 10) { col = 0; row++; }
}
rest_pending = left;
}
if (ldoor_rest) { /* доложить финальный кадр створки на 2-ю страницу */
pop_leveldoor_redraw(ldoor_pos / 10, ldoor_pos % 10);
ldoor_rest--;
}
if (trobs_count == 0) return; if (trobs_count == 0) return;
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не /* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
@@ -430,11 +411,9 @@ void pop_process_trobs(uint8_t cur_room)
break; break;
case TILE_TORCH: case TILE_TORCH:
case TILE_TORCH_DEBRIS: case TILE_TORCH_DEBRIS:
/* animate_torch (seg007:03C1): кадр пламени; анимация вечная — /* animate_torch (seg007:03C1): анимация вечная — type остаётся
* type остаётся 1, пока комната отрисована. Смена кадра — через * 1, пока комната отрисована. Темп — TORCH_ANIM_DIV. */
* ЛОГИЧЕСКИЙ кадр (torch_tick): у нас кадр короче игрового тика if (room == cur_room && TORCH_ANIM_NOW)
* оригинала, и пламя «кипело» вдвое быстрее нужного. */
if (room == cur_room && (torch_tick & 1) == 0)
mod[tp] = get_torch_frame(mod[tp]); mod[tp] = get_torch_frame(mod[tp]);
break; break;
case TILE_POTION: case TILE_POTION:
@@ -477,24 +456,20 @@ void pop_process_trobs(uint8_t cur_room)
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с * неизменной сигнатурой, и стёртое уже не возвращается: рядом с
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной * Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
* странице дабл-буфера из двух). */ * странице дабл-буфера из двух). */
uint8_t trow = tp / 10, tcol = tp % 10; /* Последний кадр (пика убралась, trob умирает) — пометить ОБЕ
pop_spike_redraw(trow, tcol); * страницы: живой trob метит каждый кадр по одной. */
/* Последний кадр (пика убралась, trob умирает) — на ВТОРУЮ страницу pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
* его уже никто не положит: ставим отложенный редрой. */
if (type < 0) { spike_rest[tp] = 1; rest_pending++; }
} }
if (room == cur_room && code == TILE_LEVELDOOR) { if (room == cur_room && code == TILE_LEVELDOOR) {
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там /* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе * её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
* kid_heal вернул бы из фона закрытую створку. Куски двери * kid_heal вернул бы из фона закрытую створку. Куски двери
* непрозрачны, поэтому wipe не нужен. */ * непрозрачны, поэтому wipe не нужен. */
uint8_t trow = tp / 10, tcol = tp % 10; /* Створку рисует ПРАВАЯ половина двери; последний кадр анимации
if (tcol + 1 < 10) { * помечаем на обе страницы, иначе дверь дрожит через кадр. */
pop_leveldoor_redraw(trow, tcol + 1); if (tp % 10 + 1 < 10)
/* Анимация кончилась — этот кадр лёг только на ОДНУ страницу; pop_set_redraw((uint8_t)(tp + 1), POP_RD_LEVELDOOR,
* повторить его на второй, иначе дверь дрожит через кадр. */ (uint8_t)(type < 0 ? 2 : 1));
if (type < 0) { ldoor_pos = (uint8_t)(tp + 1); ldoor_rest = 1; }
}
} }
if (room == cur_room && code == TILE_SWORD) { if (room == cur_room && code == TILE_SWORD) {
/* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1): /* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1):
@@ -503,9 +478,7 @@ void pop_process_trobs(uint8_t cur_room)
* следующим кадром через rest-цикл. */ * следующим кадром через rest-цикл. */
uint8_t sig = (uint8_t)(mod[tp] == 1); uint8_t sig = (uint8_t)(mod[tp] == 1);
if (sig != trob_drawn[tp]) { if (sig != trob_drawn[tp]) {
uint8_t trow = tp / 10, tcol = tp % 10; pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
pop_button_redraw(trow, tcol);
button_rest[tp] = 1; rest_pending++;
trob_drawn[tp] = sig; trob_drawn[tp] = sig;
} }
} }
@@ -517,9 +490,7 @@ void pop_process_trobs(uint8_t cur_room)
* считаем только в момент редроя (редко), а не каждый кадр. */ * считаем только в момент редроя (редко), а не каждый кадр. */
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1); uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
if (sig != trob_drawn[tp]) { if (sig != trob_drawn[tp]) {
uint8_t trow = tp / 10, tcol = tp % 10; pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
pop_button_redraw(trow, tcol);
button_rest[tp] = 1; rest_pending++;
trob_drawn[tp] = sig; trob_drawn[tp] = sig;
} }
} }
+104 -16
View File
@@ -15,9 +15,48 @@
#include "pop_map.h" #include "pop_map.h"
#include "pop_level.h" #include "pop_level.h"
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_redraw.h"
#include "pop_guard.h"
#include "pop_cheat.h"
__sfr __at (0xFE) io_border; __sfr __at (0xFE) io_border;
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (включено по умолчанию; make PROF=0 —
* выключить). Цвет бордюра держится, пока фаза считает, поэтому высота
* полосы на скриншоте = время фазы в растровых строках. Это единственный
* способ смотреть профиль без дебаггера: выборка PC вырождается (мост MAME
* отвечает из фреймового колбэка, все сэмплы падают в обработчик
* прерывания); точные такты — breakpoint'ы с {printf totalcycles; g}.
*
* ЦВЕТА: у бордюра Sprinter бит 0 (красный) ИГНОРИРУЕТСЯ, работают только
* бит 1 (синий) и бит 2 (зелёный) — значит различимых состояний ровно
* ЧЕТЫРЕ, и значения обязаны быть чётными. Отсюда фазы сгруппированы по
* четыре, а не по семь (нечётные номера раньше сливались с соседями и
* полосы не читались):
* 0 чёрный — ожидание vsync (запас кадра)
* 2 синий — ввод, heal и ЛОГИКА (kid_tick/физика/страж)
* 4 зелёный — слой ФОНА (loose/trobs/redraw_needed/шов)
* 6 циан — СПРАЙТЫ (страж, Kid, fore поверх них) */
#ifndef PROF_BORDER
#define PROF_BORDER 1
#endif
#if PROF_BORDER
#define PROF(n) io_border = (n)
#else
#define PROF(n) ((void)0)
#endif
/* Сколько банков грузит crt0_banked (читает его стартовый код).
* 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map
* (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP).
* Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
* вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних
* уровней, чомперы, зеркало. */
const uint8_t n_banks = 4;
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats;
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */ /* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
#define SEQ_STAND 2 #define SEQ_STAND 2
#define ROOM_1 1 #define ROOM_1 1
@@ -39,6 +78,7 @@ __sfr __at (0xFE) io_border;
* Убирается закомментированием ОДНОЙ строки ниже. */ * Убирается закомментированием ОДНОЙ строки ниже. */
#define ROOMNAV 1 #define ROOMNAV 1
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы /* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope, * соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */ * чтобы enter_room переключал комнату без длинного списка параметров. */
@@ -130,6 +170,7 @@ static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t
static void enter_room(uint8_t room) static void enter_room(uint8_t room)
{ {
uint8_t pg; uint8_t pg;
pop_guard_leave(); /* запомнить стража уходящей комнаты */
uint8_t *room_mod; uint8_t *room_mod;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */ apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */
@@ -157,9 +198,16 @@ static void enter_room(uint8_t room)
} }
} }
pop_map_set_room(room); pop_map_set_room(room);
/* Kid.room — как в оригинале: «комната персонажа». Полная модель
* (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
* стража никогда не срабатывает. */
Kid.room = room;
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним, * per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
* а не по свежему backtable. Фаза P0. */ * а не по свежему backtable. Фаза P0. */
@@ -197,17 +245,25 @@ int main(void)
#ifdef ROOMNAV #ifdef ROOMNAV
uint8_t nav_prev = 0; uint8_t nav_prev = 0;
#endif #endif
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0;
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */ if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
puts("pop_bg_load failed"); puts("pop_bg_load failed");
return 1; return 1;
} }
pop_cheats = 1; /* режим разработки: читы включены */
pop_guard_reset();
if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */
printf("kid_data.bin load failed\n");
return 1;
}
pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */ pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
puts("kid atlas load failed"); puts("kid atlas load failed");
return 1; return 1;
} }
if (pop_level_load("res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */ if (pop_level_load("LEVELS\\res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */
pop_level_load("a:\\res2001.bin") != 0) { pop_level_load("a:\\res2001.bin") != 0) {
puts("level load failed"); puts("level load failed");
return 1; return 1;
@@ -215,7 +271,7 @@ int main(void)
initgraph(); initgraph();
gfx_set_visible_page(0); gfx_set_visible_page(0);
if (gfx_pal_fload(0, "kid.pal") < 0) /* единая игровая палитра */ if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */
gfx_pal_fload(0, "a:\\kid.pal"); gfx_pal_fload(0, "a:\\kid.pal");
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */ gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
@@ -249,7 +305,7 @@ int main(void)
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */ * переключ). Выключение возвращает показ на страницу 0. */
io_border = 2; /* синий - начало */ PROF(2); /* ввод + heal */
uint8_t sp = kbd_raw_down(KBD_SPACE); uint8_t sp = kbd_raw_down(KBD_SPACE);
if (sp && !sp_prev) { if (sp && !sp_prev) {
@@ -266,8 +322,25 @@ int main(void)
* стоит, можно спокойно разбирать позу Kid и окклюзию. */ * стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0; if (kbd_raw_down(0x1E)) frozen = 0;
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
* K — убить стража;
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза);
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
* берётся с пола (pop_have_sword ставит подбор). */
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal;
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
cheat_prev = k;
immo_prev = i;
sword_prev = s;
}
if (frozen) { if (frozen) {
io_border = 0; /* черный: ждём vsync */ PROF(0); /* чёрный: ждём vsync */
gfx_wait_vsync(); gfx_wait_vsync();
continue; continue;
} }
@@ -307,11 +380,20 @@ int main(void)
back = dbuf ? (uint8_t)(front ^ 1) : 0; back = dbuf ? (uint8_t)(front ^ 1) : 0;
gfx_set_draw_page(back); gfx_set_draw_page(back);
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ pop_ctrl_tick(); /* ввод -> control(): смена seq */
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
pop_guard_heal(); /* и прошлый кадр стража */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */ pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
PROF(2); /* логика (тот же синий) */
kid_tick(); /* play_seq: следующий кадр */ kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */ pop_phys_tick(); /* K3: падение/приземление/стена */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
* попадания с обеих сторон, потом применить, потом свести HP. */
pop_check_sword_hurting();
pop_check_sword_hurt();
pop_do_delta_hp();
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_fell_out = 0; pop_fell_out = 0;
@@ -334,14 +416,14 @@ int main(void)
} }
} }
io_border = 4; /* зеленый */ PROF(4); /* слой фона */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */ pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */ /* — ДО Kid, чтобы Kid всегда был поверх плиты) */
io_border = 6; /* голубой */ pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
pop_process_trobs(cur_room); /* пики/ворота: продвинуть + перерисовать (тоже */ /* process_trobs — рисования внутри нет) */
/* слой фона — ДО Kid) */ pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
io_border = 4; /* синий */ /* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
{ /* ворота соседа слева (room8) видны в col0 — */ { /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */ uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) { if (lft) {
@@ -358,7 +440,6 @@ int main(void)
} }
} }
} }
io_border = 2; /* зеленый */
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++; dead_frames++;
if (dead_frames > RESPAWN_SETTLE && if (dead_frames > RESPAWN_SETTLE &&
@@ -371,13 +452,11 @@ int main(void)
pop_kid_hp_reset(); pop_kid_hp_reset();
} }
} }
io_border = 6; /* голубой */
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */ if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1); uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
uint8_t fcol = (uint8_t)(fpos % 10); uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0; pop_loose_fell = 0;
io_border = 2; /* синий */
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */ add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
if (down) { if (down) {
int8_t lrow = pop_room_col_landing(down, fcol); int8_t lrow = pop_room_col_landing(down, fcol);
@@ -414,22 +493,29 @@ int main(void)
* Приоритет как в оригинале: активный flash_time > кадр урона * Приоритет как в оригинале: активный flash_time > кадр урона
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */ * (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
if (pop_flash_time) { if (pop_flash_time) {
flash_bg(255, 255, 85); /* color_14_brightyellow */ /* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (flash_color
* оригинала; нам хватает этих двух). */
if (pop_flash_red) flash_bg(255, 0, 0); /* color_4_red */
else flash_bg(255, 255, 85); /* color_14_brightyellow */
pop_flash_time--; pop_flash_time--;
if (!pop_flash_time) pop_flash_red = 0;
flash_on = 1; flash_on = 1;
} else if (pop_kid_hurt) { } else if (pop_kid_hurt) {
flash_bg(255, 85, 85); /* color_12_brightred */ flash_bg(255, 85, 85); /* color_12_brightred */
flash_on = 1; flash_on = 1;
} }
io_border = 4; /* зеленый */
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(6); /* спрайты: страж, Kid, fore поверх */
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */
PROF(6); /* kid_draw (тот же циан) */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */ pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
kid_draw_splash(); kid_draw_splash();
pop_kid_hurt = 0; pop_kid_hurt = 0;
} }
io_border = 0; /* черный */ pop_hp_draw(); /* полоса HP — только при изменении */
PROF(6); /* fore поверх Kid + борта */
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(), kid_fp_width(), kid_fp_height(),
Kid.direction, Kid.frame, Kid.action); Kid.direction, Kid.frame, Kid.action);
@@ -439,7 +525,7 @@ int main(void)
* сделан (скрытая страница), wait_vsync ловит начало кадра, * сделан (скрытая страница), wait_vsync ловит начало кадра,
* set_visible_page переключает во время бланка (gfx.h паттерн). */ * set_visible_page переключает во время бланка (gfx.h паттерн). */
io_border = 0; /* черный: ждём vsync */ PROF(0); /* чёрный: ждём vsync */
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО /* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера), * при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
@@ -484,6 +570,8 @@ int main(void)
closegraph(); closegraph();
pop_bg_free(); pop_bg_free();
pop_kid_free(); pop_kid_free();
pop_guard_free();
pop_kid_data_free();
pop_level_free(); pop_level_free();
return 0; return 0;
} }
@@ -14,7 +14,9 @@ seqtbl адресуется базой SEQTBL_BASE=0x196E: элемент = kid_
import os import os
import re import re
SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/src" SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "SDLPoP", "src")
OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "poc", "res", "kid")
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "roomtest", "kid_data.h") "..", "roomtest", "kid_data.h")
SEQTBL_BASE = 0x196E SEQTBL_BASE = 0x196E
@@ -24,10 +26,11 @@ def read(name):
return open(os.path.join(SRC, name)).read() return open(os.path.join(SRC, name)).read()
def parse_frame_table(txt): def parse_frame_table(txt, name="frame_table_kid"):
"""frame_table_kid[] -> список (image, dx, dy, flags). Записи вида """frame_table_kid[]/frame_tbl_guard[] -> список (image, dx, dy, flags).
{ IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как OR-выражения.""" Записи вида { IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как
m = re.search(r'const frame_type frame_table_kid\[\]\s*=\s*\{(.*?)\n\};', txt, re.S) OR-выражения."""
m = re.search(r'const frame_type ' + name + r'\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
body = m.group(1) body = m.group(1)
frames = [] frames = []
for row in re.findall(r'\{([^}]*)\}', body): for row in re.findall(r'\{([^}]*)\}', body):
@@ -41,6 +44,18 @@ def parse_frame_table(txt):
return frames return frames
def parse_sword_tbl(txt):
"""sword_tbl[] (seg006:2047) -> список (id, x, y). id 255 = клинка нет.
Индекс — sword_frame (cur_frame.sword & 0x3F); один и тот же chtab_0 и у
Кида, и у стража."""
m = re.search(r'const sword_table_type sword_tbl\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
rows = []
for row in re.findall(r'\{([^}]*)\}', m.group(1)):
a, b, c = [int(p.strip(), 0) for p in row.split(',')]
rows.append((a & 0xFF, b & 0xFF, c & 0xFF))
return rows
def parse_byte_array(txt, name): def parse_byte_array(txt, name):
m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S) m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S)
return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))] return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))]
@@ -63,8 +78,10 @@ def main():
seqc = read("seqtbl.c") seqc = read("seqtbl.c")
frames = parse_frame_table(seg006) frames = parse_frame_table(seg006)
gframes = parse_frame_table(seg006, "frame_tbl_guard")
seqtbl = parse_byte_array(seqc, "original_seqtbl") seqtbl = parse_byte_array(seqc, "original_seqtbl")
offs = parse_word_array(seqc, "original_seqtbl_offsets") offs = parse_word_array(seqc, "original_seqtbl_offsets")
swtbl = parse_sword_tbl(seg006)
with open(OUT, "w") as f: with open(OUT, "w") as f:
f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n" f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n"
@@ -74,28 +91,57 @@ def main():
f.write("#ifndef KID_DATA_H\n#define KID_DATA_H\n#include <stdint.h>\n\n") f.write("#ifndef KID_DATA_H\n#define KID_DATA_H\n#include <stdint.h>\n\n")
f.write("#define SEQTBL_BASE 0x%04Xu\n\n" % SEQTBL_BASE) f.write("#define SEQTBL_BASE 0x%04Xu\n\n" % SEQTBL_BASE)
# frame table # frame table и seqtbl В .h НЕ ПОПАДАЮТ — они уезжают в kid_data.bin
f.write("/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */\n") # (EMM-страница): 3.5 КБ в _CODE окна W1/W2 нам дороже, чем один
# gfx_w0_map за тик. См. applications/PoP/docs/layout_plan_v2.md §5.1.
f.write("/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid).\n"
" * САМА таблица кадров и seqtbl лежат в kid_data.bin (EMM-\n"
" * страница); здесь только тип и размеры. */\n")
f.write("typedef struct { uint8_t image; int8_t dx, dy; " f.write("typedef struct { uint8_t image; int8_t dx, dy; "
"uint8_t flags, sword; } kframe;\n") "uint8_t flags, sword; } kframe;\n")
f.write("#define KID_NFRAMES %d\n" % len(frames)) f.write("#define KID_NFRAMES %d\n" % len(frames))
f.write("static const kframe kid_frames[KID_NFRAMES] = {\n") f.write("#define KID_SEQTBL_LEN %d\n\n" % len(seqtbl))
for (img, dx, dy, fl, sw) in frames:
dxs = dx - 256 if dx > 127 else dx
dys = dy - 256 if dy > 127 else dy
f.write(" {%3d,%4d,%4d,0x%02X,0x%02X},\n" % (img, dxs, dys, fl, sw))
f.write("};\n\n")
# seqtbl bytes # раскладка kid_data.bin (смещения ОТ НАЧАЛА ДАННЫХ страницы)
f.write("#define KID_SEQTBL_LEN %d\n" % len(seqtbl)) f.write("/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */\n")
f.write("static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {\n") f.write("#define KID_BIN_FRAMES_OFF 0\n")
f.write(emit_c(seqtbl) + "\n};\n\n") f.write("#define KID_BIN_SEQTBL_OFF %d\n" % (len(frames) * 5))
f.write("/* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс\n"
" * frame + add_frame - 149 (add_frame=70 для кадров 102..106). */\n")
f.write("#define KID_NGFRAMES %d\n" % len(gframes))
f.write("#define KID_BIN_GFRAMES_OFF %d\n\n" % (len(frames) * 5 + len(seqtbl)))
# seq offsets # seq offsets
f.write("#define KID_NSEQ %d\n" % len(offs)) f.write("#define KID_NSEQ %d\n" % len(offs))
f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n") f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n")
f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n") f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n")
f.write("#endif\n")
# sword_tbl — макро-инициализаторами, а не готовыми массивами: так
# 159 байт таблицы лягут в ОДИН TU (pop_kid.c), а не в каждый, кто
# включил заголовок.
f.write("/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.\n"
" * Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.\n"
" * Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),\n"
" * x — «вперёд» по направлению персонажа. */\n")
f.write("#define KID_NSWORD %d\n" % len(swtbl))
for i, (nm, fmt) in enumerate((("ID", "0x%02X"), ("DX", "%d"), ("DY", "%d"))):
# dx/dy — знаковые: печатаем со знаком, иначе int8_t = overflow
vals = [r[i] if i == 0 else (r[i] - 256 if r[i] > 127 else r[i])
for r in swtbl]
f.write("#define KID_SWORD_%s_INIT { \\\n" % nm)
f.write(emit_c(vals, 12, fmt).replace("\n", " \\\n") + " \\\n}\n")
f.write("\n#endif\n")
# kid_data.bin: кадры (по 5 байт) + seqtbl подряд
os.makedirs(OUT_BIN_DIR, exist_ok=True)
binpath = os.path.join(OUT_BIN_DIR, "kid_data.bin")
with open(binpath, "wb") as b:
for (img, dx, dy, fl, sw) in frames:
b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF)))
b.write(bytes(seqtbl))
for (img, dx, dy, fl, sw) in gframes:
b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF)))
print(f"kid_data.bin: {len(frames)*5 + len(seqtbl) + len(gframes)*5} Б (кадры Kid + seqtbl + кадры стража) -> {binpath}")
print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, " print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, "
f"{len(offs)} seq-оффсетов -> {OUT}") f"{len(offs)} seq-оффсетов -> {OUT}")
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
pop_pack_guard.py — упаковщик спрайтов СТРАЖА (chtab_5_guard, база 750).
Оригинал грузит стражей из отдельного .DAT по типу уровня
(GUARD/FAT/SKEL/VIZIER/SHADOW, seg000:1092); уровень 1 — обычный «guard»,
то есть data/GUARD/res(750+id).png. Палитра — НЕ из PNG: оригинал
подменяет её из res10.bin (guard_palettes, 7 палитр по 16 цветов, 6-бит) по
level.guards_color; на уровне 1 у обоих стражей color = 2.
Выход: poc/res/guard/g0..gN.atl (по 8 спрайтов на страницу, адресация
id>>3 / id&7 — как у Kid) + группа палитры 0x90..0x9F в общий kid.pal
(его собирает pop_pack_kid.py, см. GUARD_PAL_BASE там).
"""
import os
import sys
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pop_pack_bg as B
from pop_pack_kid import transpose_cols
GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD")
PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin")
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "guard")
PAL_BASE = 0x90 # слоты палитры Sprinter под стража
COLOR = 2 # level.guards_color обоих стражей уровня 1
SHIFT = 3 # g[id>>3], idx id&7 (спрайты крупные)
MASK = (1 << SHIFT) - 1
MAX_ID = 33 # атлас-индексы 0..33 = res751..res784
def load_guard(id):
"""Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp
COLUMN-MAJOR); 0 -> 0xFF (прозрачность).
Off-by-one как у Kid (см. pop_pack_kid.load_kid): load_chtab_from_file
зовётся с ресурсом 750, а images[i-1] = res(750+i), то есть images[0] =
res751. Рисование берёт get_image(chtab, obj_id+1 1) = images[image],
значит frame.image=N -> res(751+N). С res(750+N) страж стоял в позе
предыдущего кадра (замечено на frame_166_stand_inactive: image 17
рисовался как res767 — выпад — вместо res768). """
path = os.path.join(GUARD_DIR, f"res{751 + id}.png")
if not os.path.exists(path):
return None
im = Image.open(path)
if im.mode != "P":
B.die(f"res{750+id}: mode {im.mode}, ожидался P")
src = im.tobytes()
rowmajor = bytes((0xFF if v == 0 else PAL_BASE + v) for v in src)
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
def guard_palette():
"""16 цветов (r,g,b 0..63) из res10.bin для COLOR."""
b = open(PAL_BIN, "rb").read()
off = 0x30 * COLOR - 0x30
return [(b[off + i * 3], b[off + i * 3 + 1], b[off + i * 3 + 2]) for i in range(16)]
def main():
os.makedirs(OUT_DIR, exist_ok=True)
pages = {}
n = 0
for id in range(0, MAX_ID + 1):
g = load_guard(id)
if g is None:
continue
pages.setdefault(id >> SHIFT, {})[id & MASK] = g
n += 1
if not pages:
B.die("нет спрайтов стража")
total = 0
for pg in range(max(pages) + 1):
nm = f"g{pg}.atl"
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {}))
total += sz
print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б")
print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}")
if __name__ == "__main__":
main()
+14 -3
View File
@@ -25,7 +25,11 @@ KID_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDL
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid") OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid")
PAL_BASE = 0x70 # слоты палитры Sprinter под Kid PAL_BASE = 0x70 # слоты палитры Sprinter под Kid
SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F
SWORD_IDS = tuple(range(20, 28)) # sword_tbl для кадров 229..236 (найти-убрать) GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F
# chtab_0 целиком: sword_tbl (seg006:2047) адресует id 0..33 — подъём меча,
# ножны И вся боёвка. Меч в оригинале ОДИН на всех: и Кид, и страж рисуют
# клинок из этого же chtab_0 (add_sword_to_objtable, seg006:1798).
SWORD_IDS = tuple(range(0, 34))
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ) SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
MASK = (1 << SHIFT) - 1 MASK = (1 << SHIFT) - 1
@@ -108,6 +112,14 @@ def build_palette(out_path):
for i, (r, g, bl) in enumerate(_read16(path)): for i, (r, g, bl) in enumerate(_read16(path)):
o = (base + i) * 4 o = (base + i) * 4
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0)) pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
# Палитра стража берётся НЕ из .pal, а из res10.bin (guard_palettes:
# 7 палитр по 16 цветов, 6-бит) по level.guards_color; на уровне 1 у
# обоих стражей color = 2. См. toolchain/pop_pack_guard.py.
import pop_pack_guard as G
for i, (r, g, bl) in enumerate(G.guard_palette()):
o = (GUARD_PAL_BASE + i) * 4
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
with open(out_path, "wb") as f: with open(out_path, "wb") as f:
f.write(pal) f.write(pal)
@@ -141,8 +153,7 @@ def main():
names.append(None) names.append(None)
print(f" (kid стр {pg}: пусто)") print(f" (kid стр {pg}: пусто)")
# chtab_0: меч в руке. Пакуем ТОЛЬКО кадры подъёма/ножен (sword_tbl # chtab_0: меч в руке — весь chtab (id 0..33), индекс в атласе = id.
# 35..42 -> id 20..27); индекс в атласе = id - 20 (бой не портирован).
sw = {} sw = {}
for i, id in enumerate(SWORD_IDS): for i, id in enumerate(SWORD_IDS):
w, h, pix = load_sword(id) w, h, pix = load_sword(id)
+21 -2
View File
@@ -23,6 +23,7 @@
# CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3 # CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3
# Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3 # Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3
# -I PATH additional include path (repeatable) # -I PATH additional include path (repeatable)
# -DNAME[=VAL] preprocessor define, passed to sdcc as is (repeatable)
# -L 0xADDR code load address (default: derived from --memory mode) # -L 0xADDR code load address (default: derived from --memory mode)
# -E 0xADDR entry address (default: same as -L) # -E 0xADDR entry address (default: same as -L)
# -S 0xADDR stack address (default: 0xBFFE) # -S 0xADDR stack address (default: 0xBFFE)
@@ -32,6 +33,9 @@
# --data-loc 0xN override SDCC --data-loc (default: derived from --memory) # --data-loc 0xN override SDCC --data-loc (default: derived from --memory)
# -Wl FLAG extra linker flag (repeatable) # -Wl FLAG extra linker flag (repeatable)
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked # --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
# --bank-data put bank modules' writable data INTO the bank page
# (default: it goes to the shared _DATA in W1/W2, which
# stays mapped and is visible from everywhere)
# automatically and adds -Wl-b_BANKN=0x{N}C000 # automatically and adds -Wl-b_BANKN=0x{N}C000
# --w3 FILE.c place FILE.c resident in window 3 (0xC000), called # --w3 FILE.c place FILE.c resident in window 3 (0xC000), called
# DIRECTLY (no trampoline). Repeatable. Defaults to # DIRECTLY (no trampoline). Repeatable. Defaults to
@@ -106,6 +110,8 @@ LD_EXTRA=()
MKEXE_EXTRA=() MKEXE_EXTRA=()
BANK_SPECS=() # entries like "1=engine.c" BANK_SPECS=() # entries like "1=engine.c"
W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3) W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3)
USER_DEFS=() # -DFOO / -DFOO=bar — пробрасываются в sdcc как есть
BANK_LOCAL_DATA=0 # 1 (--bank-data): писучие данные банков — В СТРАНИЦУ банка
W3_RELS=() # заполняется при компиляции W3-модулей W3_RELS=() # заполняется при компиляции W3-модулей
W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3 W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3
USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small) USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small)
@@ -139,7 +145,9 @@ while [[ $# -gt 0 ]]; do
--w3) W3_SPECS+=("$2"); shift 2;; --w3) W3_SPECS+=("$2"); shift 2;;
--stack-size) STACK_SIZE="$2"; shift 2;; --stack-size) STACK_SIZE="$2"; shift 2;;
-Wl) LD_EXTRA+=("$2"); shift 2;; -Wl) LD_EXTRA+=("$2"); shift 2;;
-D*) USER_DEFS+=("$1"); shift;;
--bank) BANK_SPECS+=("$2"); shift 2;; --bank) BANK_SPECS+=("$2"); shift 2;;
--bank-data) BANK_LOCAL_DATA=1; shift;;
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;; --mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
--max-allocs) MAX_ALLOCS="$2"; shift 2;; --max-allocs) MAX_ALLOCS="$2"; shift 2;;
--gfx) GFX_MODE="$2"; shift 2;; --gfx) GFX_MODE="$2"; shift 2;;
@@ -366,7 +374,7 @@ run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
# 2. user sources → .rel (HOME) # 2. user sources → .rel (HOME)
USER_RELS=() USER_RELS=()
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}") CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}" "${USER_DEFS[@]}")
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT) [[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS") [[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
for src in "${SOURCES[@]}"; do for src in "${SOURCES[@]}"; do
@@ -420,8 +428,19 @@ if [[ "$CRT0_TYPE" == "banked" ]]; then
bank_n="${spec%%=*}" bank_n="${spec%%=*}"
bank_src="${spec#*=}" bank_src="${spec#*=}"
rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel" rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel"
# По умолчанию --dataseg НЕ задаём: писучие данные банкового
# модуля (глобалы, file-static, статики функций) остаются в
# общем _DATA, то есть в W1/W2 — они замаплены всегда и видны
# и банку, и остальному коду. С --dataseg BANKn они уезжают в
# СТРАНИЦУ БАНКА и снаружи не читаются (проверено на .map:
# глобал банкового модуля лёг по 0x0001C000): это экономит
# W1/W2, но требует ручного контроля видимости, поэтому
# включается явно через --bank-data.
bank_data_flags=()
[[ $BANK_LOCAL_DATA -eq 1 ]] && bank_data_flags=(--dataseg "BANK${bank_n}")
run "$SDCC" "${CC_FLAGS[@]}" \ run "$SDCC" "${CC_FLAGS[@]}" \
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" --dataseg "BANK${bank_n}" \ --codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" \
"${bank_data_flags[@]}" \
-c -o "$rel" "$bank_src" -c -o "$rel" "$bank_src"
BANK_RELS+=("$rel") BANK_RELS+=("$rel")
# Virtual address: bank_n in upper byte, BANK_LOW16 in low half. # Virtual address: bank_n in upper byte, BANK_LOW16 in low half.
Binary file not shown.
+893
View File
@@ -0,0 +1,893 @@
# Функции BIOS v2.12
### Вызов функций BIOS
Вызов функций BIOS осуществляется из ассемблерного кода.
Номер функции задается в регистре C процессора. В остальные регистры,
при необходимости, загружаются входные параметры функции. После исполнения
функции происходит возврат в программу, из которой произошел вызов функции.
Установленный флаг CF (CF=1) означает, что работа функции произошла с
ошибкой. В некоторых регистрах передаются выходные параметры.
Ниже приведены таблицы входных и выходных параметров для каждой функции:
- Функции работы с памятью
- Функции управления 'железом'
- Функции управления окнами и режимами экрана
- Функции вывода текста на экран
- Графические функции
- Функции работы с жесткими дисками и дисководами
При работе функций BIOS необходимо что бы стек находился в области
8000h..0BFFFh, так как часть функций использует переключение страниц
PAGE1 и PAGE3.
Вызов функций BIOS возможен в различных конфигурациях компьютера Sprinter.
Конфигурация Spectrum: вызов функций BIOS осуществляется через 3D13h.
При этом работают и все функции TR-DOS.
Конфигурации Sprinter: вызов функций BIOS осуществляется через RST 18h
при подключенном системном ПЗУ.
Для постоянного подключения системного ПЗУ можно воспользоваться
такой последовательностью команд:
```asm
LD A,0
OUT (07Ch),A
```
После ее исполнения в 0-м окне Z80 будет включено ПЗУ BIOSа и программа
может вызывать функции BIOSа через RST 18h.
Отключение ПЗУ BIOSа из нулевого окна Z80 производится следующей
последовательностью команд:
```asm
LD A,0
OUT (03Ch),A
```
При необходимости, функции BIOSа могут быть вызваны программой, находящейся
в ОЗУ непосредственно в нулевом окне Z80. Для этого надо установить в
адресе 0008h следующий код:
```asm
ADDRESS_0008h:
PUSH AF
LD A,0
OUT (7Ch),A
POP AF
RET
```
После этого BIOS можно вызывать командой RST 8. (Функции TR-DOS, так
же как и в случае RST 18 остаются недоступны.) Вызывая программы таким
образом, через RST 8, следует помнить что адреса 3FFFh..0000h после входа
в BIOS будут содержать код ПЗУ, поэтому, если фунция использует данные в
ОЗУ, они должны находиться в других адресах.
Оптимизация программы для RST 8 недопустима, так как в ПЗУ, для
обратного переключения, стоит такая же программа, только порт 3Ch для
отключения ПЗУ BIOSа.
Вызов функций BIOS в exe-файлах, вызываемых с помощью операционной
системы Estex, производится командой RST 8. Необходимая программа в адресе
0008h уже имеется в блоке кода ОС Estex.
### Функции работы с памятью
**0C0h (192) EMM_FN0 Определение объемов ОЗУ**
Значение регистров на входе:
C=0C0h
Значение регистров на выходе:
HL - общий объем ОЗУ в страницах по 16k
BC - объем свободного ОЗУ в страницах по 16k
**0C1h (193) EMM_FN1 Инициализация распределения памяти**
Стирается вся информация о выделенных ранее блоках ОЗУ.
Блоки с системной информацией и первые 256K ОЗУ объявляются занятыми.
Значение регистров на входе:
C=0C1h
Значение регистров на выходе:
нет
**0C2h (194) EMM_FN2 Выделение блока ОЗУ**
Значение регистров на входе:
C=0C2h
B - число запрашиваемых страниц
Значение регистров на выходе:
CF=0 - нормальное завершение - A - идентификатор блока
CF=1 - ошибка - A=1 - не хватает памяти
**0C3h (195) EMM_FN3 Освободить блок ОЗУ**
Значение регистров на входе:
C=0C3h
A - идентификатор блока
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неправильный идентификатор
идентификатор не всегда отслеживается правильно
**0C4h (196) EMM_FN4 Получить физический номер страницы из блока памяти**
Значение регистров на входе:
C=0C4h
A - идентификатор блока
B - логическая номер страницы в блоке
Значение регистров на выходе:
CF=0 - A - логический номер страницы
CF=1 - ошибка:
A=0 - блок не существует
A=255 - запрашиваемый номер страницы слишком велик
**0C5h (197) EMM_FN5 Получить список физических страниц блока**
Значение регистров на входе:
C=0C5h
A - идентификатор блока
HL - буфер 256 байт для размещения списка страниц
Значение регистров на выходе:
CF=0 - нормальное завершение:
B - число страниц в блоке
HL - тот же адрес буфера, в буфере список физических
страниц по порядку, заканчивающийся байтом 0FFh
CF=1 - неверный идентификатор блока; старая информация в буфере
может быть затерта
**0C6h (198) EMM_FN6 Получение адресов портов окон**
Примечание по использованию функции получения адресов портов окон (0C6h):
>Cледует хотя бы один раз вызвать эти функции и сравнить адреса
>портов с теми, что используются в программе и, если они не совпадают,
>выдать соответствующее предупреждение. В данный момент эти порты
>таковы: PAGE0=82h, PAGE1=0A2h, PAGE2=0C2h, PAGE3=0E2h
Значение регистров на входе:
C=0C6h
A - номер окна процессора - 0,1,2 или 3
Значение регистров на выходе:
CF=0 - нормальное завершение
C - 8-битный адрес порта окна
B - физический номер подключенной в окно страницы
CF=1 - ошибка - неверный номер окна
**0C7h (199) EMM_FN7 Получить номер следующей страницы блока**
Примечание по использованию функции EMM_FN7 (0C7h):
> Информация о распределении памяти хранится в виде RAM Allocation Table,
> похожей на дисковый FAT. Поэтому нахождение физического номера
> следующей страницы по предыдущему физическому номеру происходит
> значительно быстрее, чем поиск по увеличенному на единицу логическому
> номеру.
Значение регистров на входе:
C=0C7h
A - физическая страница
Значение регистров на выходе:
CF=0 - нормальное завершение
A - следуюшая физическая страница блока
A=0FFh - индицирует конец блока
CF=1 - ошибка - страница не принадлежит никакому блоку.
фактически, это означает, что она свободна.
**09Eh (158) EMM_FN8 Слияние блоков**
Значение регистров на входе:
A - идентификатор блока 1
B - идентификатор блока 2
Значение регистров на выходе:
CF=0 - нормальное завершение
A - идентификатор объединенного блока
CF=1 - ошибка - неверный идентификатор блока
**09Dh (157) EMM_FN9 Разделение блока**
Значение регистров на входе:
C=09Dh
A - идентификатор блока
B - новая длина блока
Значение регистров на выходе:
CF=0 - нормальное завершение
A - идентификатор блока результата
B - идентификатор блока остатка
CF=1 - ошибка - неверный идентификатор блока
### Функции управления 'железом'
**0EFh (239) FN_VERSION Выдача информации о версии BIOSа и железа.**
Значение регистров на входе:
C=0EFh
HL - буфер, куда будет помещена ASCII строка с несколькими
полями, номером версии BIOS и названием модели
компьютера. Конец строки отмечен двойным нулем.
Значение регистров на выходе:
CF=0 - нормальное завершение
HL - тот же буфер с записанной строкой.
DE - версия BIOSа
BC - версия железа подробности ниже
A - количество полей в буфере (в данный момент - 2)
Первое поле - версия BIOS.
Второе - название модели компьютера.
CF=1 - ошибка - Очень старая версия,
не имеющая данной функции
**0F2h (242) FN_SICF=0 Установка синхронизации, очистка страницы режима экрана**
Значение регистров на входе:
C=0F2h
A - режим синхронизации
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неверный номер режима синхронизации
### Функции работы с CMOS данными и Turbo режимом
Комментарий к функциям CMOS (0F5h-0F7h):
> Функции CMOS_RD, CMOS_WR, CMOS_TEST работают всегда. Если в машине нет
> микросхемы CMOS, то эмулируется ее память. Наличие микросхемы
> определяется функцией CMOS_TEST.
**0F5h (245) CMOS_TEST Проверить наличие CMOS**
Значение регистров на входе:
C=0F5h
Значение регистров на выходе:
CF=0 - часы есть
CF=1 - часов нет
**0F6h (246) CMOS_RD Читать из регистра CMOS**
Значение регистров на входе:
C=0F6h
D - номер регистра CMOS
Значение регистров на выходе:
A - считанные данные
CF=0 - часы есть
CF=1 - часов нет
**0F7h (247) CMOS_WR Писать в регистр CMOS**
Значение регистров на входе:
C=0F7h
D - номер регистра CMOS
A - записываемые данные
Значение регистров на выходе:
CF=0 - часы есть
CF=1 - часов нет
**08Fh (143) FN_TURBO Функция управления турбо режимом.**
Комментарий к функции FN_TURBO (08Fh):
Переключение режима турбо может не произойти, если прошивка не
поддерживает это переключение. При этом ошибки не происходит. Так
же, переключение режима TURBO блокируется кнопкой "Turbo" в
режиме Turbo-OFF
Значение регистров на входе:
C=08Fh
A - режим турбо: 2 - off, 3 - on
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неверный режим турбо
### Функции управления окнами и режимами экрана
**0B0h (176) WIN_OPEN Функция открытия окна.**
Значение регистров на входе:
C=0B0h
IX - описатель окна
E - флаги окна:
бит 0 указывает какую страницу режима включать после
исполнения функции;
бит 4 указывает на какой странице режима открывать окно
Значение регистров на выходе:
CF=0 - нормальное завершение:
A - идентификатор окна
CF=1 - ошибка слишком много окон
**0B1h (177) WIN_CLOSE закрытие окна**
Значение регистров на входе:
C=0B1h
A - идентификатор окна
Значение регистров на выходе:
CF=0 - успешное завершение
CF=1 - ошибка - неверный идентификатор
Окно с номером 0 никогда не закрывается и попытка
закрытия приводит к ошибке
**0B2h (178) WIN_COPY_WIN Копирование данных текстового окна в память (запоминание окна)**
При работе этой функции через RST 18h или RST 8, обязателен запрет
прерываний, так как функция пользуется стеком для ускорения своей работы.
Значение регистров на входе:
C=0B2h
A - идентификатор глобального окна
H - размер окна в символах по вертикали
L - размер окна в символах по горизонтали
D - вертикальное положение окна в глобальном окне
E - горизонтальное положение окна в глобальном окне
IX - адрес буфера для запоминания данных
адрес буфера указывается для окна 0C000h если адрес
указан с 8000h, номер страницы буфера не действителен
ниже 8000h адрес указывать нельзя
A' - страница буфера для данных окна эта страница должна
принадлежать программе
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка - неверный идентификатор окна
**0B3h (179) WIN_RESTORE_WIN Копирование из памяти в текстовое окно (восстановление окна)**
При работе этой функции через RST 18h или RST 8, обязателен запрет
прерываний, так как функция пользуется стеком для ускорения своей работы.
Значение регистров на входе:
C=0B2h
A - идентификатор глобального окна
H - размер окна в символах по вертикали
L - размер окна в символах по горизонтали
D - вертикальное положение окна в глобальном окне
E - горизонтальное положение окна в глобальном окне
IX - адрес буфера для запоминания данных
адрес буфера указывается для окна 0C000h если адрес
указан с 8000h, номер страницы буфера не действителен
ниже 8000h адрес указывать нельзя
A' - страница буфера для данных окна эта страница должна
принадлежать программе
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка - неверный идентификатор окна
**0B4h (180) WIN_GET_SYM Взять символ с экрана**
Значение регистров на входе:
C=0B4h
A - идентификатор окна
DE - положение символа в окне:
D - вертикаль, E - горизонталь
Значение регистров на выходе:
CF=0 - нормальное завершение
L - символ, H - атрибут,
B - знакогенератор
CF=1 - ошибка неверный идентификатор окна
**0B5h (181) WIN_PUT_SYM Положить символ на экран**
Значение регистров на входе:
C=0B5h
A - идентификатор окна
DE - положение символа в окне:
D - вертикаль, E - горизонталь
L - символ, H - атрибут символа
B - знакогенератор
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка неверный идентификатор окна
**0B6h (182) WIN_SET_ZG установка знакогенератора**
Значение регистров на входе:
C=0B6h
A - системный номер знакогенератора
DE - указатель на 2Kb блок данных знакогенератора
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка (старая версия, нет функции)
**0B7h (183) WIN_MOVE_WIN Перемещение окна**
При работе этой функции через RST 18h или RST 8, обязателен запрет
прерываний, так как функция пользуется стеком для ускорения своей работы.
Значение регистров на входе:
C=0B7h
A - идентификатор глобального окна
H - размер локального окна по вертикали в символах
L - размер локального окна по горизонтали в символах
D - положение локального окна по вертикали в символах
E - положение локального окна по горизонтали в символах
IX - новое положение локального окна (подобно DE)
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка - неверный идентификатор окна
**0B8h (184) WIN_GET_ZG Получение знакогенератора**
Значение регистров на входе:
C=0B8h
DE - адрес, куда будет загружено 2kb знакогенератора
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка (старая версия, нет функции)
### Функции вывода текста на экран
**081h (129) LP_PRINT_ALL Печать символов с атрибутом**
На экран выводится строка из B одинаковых символов
Значение регистров на входе:
C=081h
A - символ
E - атрибут
B - число выводимых символов
регистры HL,IX - сохраняются
Значение регистров на выходе:
CF=0 - всегда
**082h (131) LP_PRINT_SYM Вывод символов на экран без атрибута**
На экран выводится строка из B одинаковых символов атрибут
остается тот, который был на экране
Значение регистров на входе:
C=082h
A - символ
B - число выводимых символов
регистры HL,IX - сохраняются
Значение регистров на выходе:
CF=0 - всегда
**083h (131) LP_PRINT_ATR печать атрибутов**
На экран выводится строка из B одинаковых атрибутов.
Символы не меняются.
Значение регистров на входе:
C=083h
E - атрибут
B - число выводимых символов
регистры HL,IX - сохраняются
Значение регистров на выходе:
CF=0 - всегда
**084h (132) LP_SET_PLACE Установка текущего знакоместа в окне**
Позиция печати устанавливается в соответстии с регистром DE
Значение регистров на входе:
C=084h
E - положение символа по горизонтали
D - номер символа по вертикали
Превышение границ приводит не к ошибке, а к переустановке
с начала, за вычетом полного размера окна
Значение регистров на выходе:
CF=0 - всегда
**085h (133) LP_PRINT_LN Вывод строки символов на экран с текущего знакоместа**
Значение регистров на входе:
C=085h
HL - адрес строки должен быть между 04000h и 0BFFFh
E - атрибут, с которым будет выведена строка
B - длина выводимой строки
Значение регистров на выходе:
CF=0 - всегда
**086h (134) LP_PRINT_LN2 Вывод строки символов на экран без атрибутов**
Значение регистров на входе:
C=086h
HL - адрес строки должен быть между 04000h и 0BFFFh
B - длина выводимой строки
Значение регистров на выходе:
CF=0 - всегда
**087h (135) LP_PRINT_LN3 Вывод строки символов до разделителя**
После разделителя выводятся пробелы, что бы вывести B символов
Значение регистров на входе:
C=087h
HL - адрес строки должен быть между 04000h и 0BFFFh
E - атрибут, с которым будет выведена строка
B - длина выводимой строки
D - символ-разделитель, указывающий конец строки
Значение регистров на выходе:
CF=0 - всегда
**088h (136) LP_PRINT_LN4 Вывод строки символов до разделителя, без атрибутов**
символы из выводятся на экран, пока не встретится символ равный D,
далее печатаются пробелы, как дополнение строки до B символов.
Атрибуты не изменяются.
Значение регистров на входе:
C=088h
HL - адрес строки должен быть между 04000h и 0BFFFh
B - длина выводимой строки
D - символ-разделитель, указывающий конец строки
Значение регистров на выходе:
CF=0 - всегда
**089h (137) LP_CLS_WIN Очистка экрана**
Выполнение производится выводом пробелов с заданным атрибутом
Значение регистров на входе:
C=089h
DE - положение локального окна
H - размер в символах локального окна по вертикали
L - размер в символах локального окна по горизонтали
B - атрибут очистки
Значение регистров на выходе:
CF=0 - всегда
**08Ah (138) LP_SCROLL_UD Скроллинг части глобального окна вверх/вниз**
Скроллируются полные строки глобального окна
Значение регистров на входе:
C=08Ah
B - тип скроллинга: 1 - вверх; 2 - вниз
D - начальная строка скроллинга
E - число скроллируемых строк
Значение регистров на выходе:
CF=0 - всегда
**08Bh (139) LP_PRINT_LN5 Вывод строки символов на экран до разделителя**
После разделителя вывод останавливается
Значение регистров на входе:
HL - адрес строки должен быть между 04000h и 0BFFFh
E - атрибут, с которым будет выведена строка
B - максимальная длина выводимой строки
D - символ-разделитель, указывающий конец строки
Значение регистров на выходе:
CF=0 - всегда
**08Ch (140) LP_PRINT_LN6 Вывод строки символов на экран до разделителя без атрибутов**
После разделителя вывод останавливается.
Значение регистров на входе:
C=08Ch
HL - адрес строки должен быть между 04000h и 0BFFFh
B - максимальная длина выводимой строки
D - символ-разделитель, указывающий конец строки
Значение регистров на выходе:
CF=0 - всегда
**08Dh (141) LP_CLS_WIN2 Очистка экрана с указанием символа заполнения**
Значение регистров на входе:
C=08Dh
DE - положение локального окна
H - размер в символах локального окна по вертикали
L - размер в символах локального окна по горизонтали
B - атрибут очистки
A - символ очистки
Значение регистров на выходе:
CF=0 - всегда
**08Eh (142) LP_GET_PLACE Получить текущее положение вывода на экран**
Значение регистров на входе:
C=08Eh
Значение регистров на выходе:
CF=0 - всегда
DE - координаты, в которых будет напечатан
следующий символ:
D - вертикаль, E - горизонталь
### Графические функции
**0A1h (161) PIC_POINT Установить точку**
Значение регистров на входе:
C=0A1h
DE - координата по вертикали (пиксели)
HL - координата по горизонтали (пиксели)
Координаты считаются от верхнего левого угла экрана
A - идентификатор окна
B - цвет точки
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неверный идентификатор окна
**0A4h (164) PIC_SET_PAL Установка палитры**
Значение регистров на входе:
C=0A4h
HL - данные палитры
E - номер начального цвета
D - количество устанавливаемых цветов
B - маска при установке палитры.
Для нормального режима должнa быть 0FFh
A - номер палитры 0..15; от 8 до 15 - резервные
Значение регистров на выходе:
CF=0 - всегда
**0A6h (166) SET_PAL_INIT Установка внутренней палитры**
Значение регистров на входе:
C=0A6h
A - страница палитры
B - номер палитры:
B=2 - установка спектрумовской палитры
B=1 - установка графической плаитры
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неверный номер палитры
### Функции работы с жесткими дисками и дисководами
**50h,52h,53h (80,82,83) Зарезервированы**
Значение регистров на входе и на выходе:
нет
**51h (81) DRV_RESET Сброс контроллера и настройка на диск**
Значение регистров на входе:
C=51h
A - номер и тип устройства
бит 7..4 - тип устройства:
#0x - FDD
#6x - RAM-DISK
#8x - HDD
#Cx - CD-ROM
бит 3..0 - номер устройства
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка, нет диска или нет устройства
**54h (84) DRV_VERIFY Проверка секторов**
Проверка внутренняя на совпадение ECC
Значение регистров на входе:
C=54h
A - номер и тип устройства (см. выше)
HL:IX - номер сектора (IX - младшая часть номера сектора)
B - количество проверяемых секторов
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - проверка с ошибкой
**55h (85) DRV_READ Чтение с устройства**
Значение регистров на входе:
C=55h
A - номер и тип устройства (см. выше)
HL:IX - номер сектора (IX - младшая часть номера сектора)
B - количество секторов
DE - адрес буфера для данных
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка чтения
HL:IX - номер сектора + кол-во прочитанных секторов
DE - адрес буфера для данных + (кол-во прочитанных секторов * размер сектора)
**56h (86) DRV_WRITE Запись на устройства**
Значение регистров на входе:
C=56h
A - номер и тип устройства (см. выше)
HL:IX - номер сектора (IX - младшая часть номера сектора)
B - количество секторов
DE - адрес буфера данных для записи
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка записи
HL:IX - номер сектора + кол-во прочитанных секторов
DE - адрес буфера для данных + (кол-во прочитанных секторов * размер сектора)
**57h (87) DRV_DETECT Определение параметров устройства**
Значение регистров на входе:
C=57h
A - номер и тип устройства (см. выше)
Значение регистров на выходе:
CF=0 - нормальное завершение
A - bit7=0 диск 720Кb
bit7=1 диск 1.44Mb
CF=1 - нет устройства или нет носителя
**58h (88) DRV_GET_PAR Получить параметры носителя**
Значение регистров на входе:
C=58h
A - номер и тип устройства (см. выше)
Значение регистров на выходе:
CF=0 - нормальное завершение
L - число секторов
H - число головок
DE - количество цилиндров
если HL=DE=0FFFFh - устройства нет
IX - размер сектора в байтах
B - доп. параметры для дискет:
бит7 - тип 1.44Mb/720Kb
CF=1 - нет устройства
**59h (89) DRV_SET_PAR Установить параметры носителя**
Значение регистров на входе:
A - номер и тип устройства (см. выше)
L - число секторов
H - число головок
DE - количество цилиндров
IX - размер сектора в байтах
B - доп. параметры для дискет
бит7 - тип 1.44Mb/720Kb
Значение регистров на выходе:
CF=0 - нормальное завершение
**5Ah (90) EXT_VERSION Номер версии дисковой спецификации.**
Значение регистров на входе:
C=5Ah
Значение регистров на выходе:
CF=0 - нормальное завершение
D - версия
E - модификация
CF=1 - ошибка
**5Fh (95) DRV_LIST Список дисковых устройств**
Значение регистров на входе:
C=5Fh
IX - буфер для списка устройств
Значение регистров на выходе:
CF=0 - нормальное завершение
В буфере список дисков в формате:
IX+0 - размер заполненого буфера
IX+1 - кол-во устройств FDD
IX+2 - кол-во устройств HDD
IX+3 - кол-во устройств CD DRIVE
IX+4 - #00 - конец списка, иначе кол-во устройств нового типа
### Примечания и комментарии.
##### *Примечание по использованию функции получения адресов портов окон (0C6h).*
Cледует хотя бы один раз вызвать эти функции и сравнить адреса портов с
теми, что используются в программе и, если они не совпадают, выдать
соответствующее предупреждение. В данный момент эти порты таковы:
PAGE0=82h, PAGE1=0A2h, PAGE2=0C2h, PAGE3=0E2h
##### *Примечание по использованию функции EMM_FN7 (0C7h).*
Информация о распределении памяти хранится в виде RAM Allocation Table,
похожей на дисковый FAT. Поэтому нахождение физического номера следующей
страницы по предыдущему физическому номеру происходит значительно быстрее,
чем поиск по увеличенному на единицу логическому номеру.
##### *Комментарий к функции FN_VERSION (0EFh).*
Значения регистра BC на выходе и соответствующая ему конфигурация
BC=FFFF - Не определено
BC=FFFE - Конфигурация Spectrum, режим Sprinter ZX
BC=FFFD - Конфигурация Sprinter
BC=FFFC - Зарезервировано
BC=FFFB - Зарезервировано
BC=FFFA - Зарезервировано
BC=FFF9 - Зарезервировано
Иные значения BC - новые прошивки.
##### *Комментарий к функциям CMOS (0F5h-0F7h)*
Функции CMOS_RD, CMOS_WR, CMOS_TEST работают всегда. Если в машине нет
микросхемы CMOS, то эмулируется ее память. Наличие микросхемы
определяется функцией CMOS_TEST.
##### *Комментарий к функции FN_TURBO (08Fh)*
Переключение режима турбо может не произойти, если прошивка не
поддерживает это переключение. При этом ошибки не происходит. Так же,
переключение режима TURBO блокируется кнопкой "Turbo" в режиме Turbo-OFF
##### *Комментарий к функциям печати текста.*
Эти функции работают с текущим окном, которым всегда является последнее
открытое окно. К графическому экрану функции печати текста не применимы.
##### *Описатель окна.*
Для открытия окон используется 32-хбайтовый описатель окна (дескриптор),
адрес которого указывается в регистре IX.
IX - 32-хбайтовый описатель окна
(IX+0) - горизонтальный размер окна в знакоместах
(IX+1) - вертикальный размер в знакоместах
(IX+2) - положение окна по горизонтали на экране в знакоместах
(IX+3) - положение окна по вертикали на экране в знакоместах
(IX+4) - режим знакоместа
bit4=1 - text_mode bit4=0 - graf_mode
bit5=0 - 16, bit5=1 - 8 точек в знакоместе
graf_mode bit3..0 - не существенны
bit7..6 - номер палитры
text_mode bit7..6,3..0 - номер знакогенератора
исключение: bit7..6=B"11" - бордер
(IX+5) - дополнительный режим знакоместа
bit0=1 - указывает на включение спектрумовской
адресации экрана
(IX+6) - положение по X в поле графики (по знакоместам)
(IX+7) - положение по Y в поле графики (по знакоместам)
разъяснения о положении в поле графики - ниже
(IX+8..31) - зарезервировано (переменные окна)
в этих байтах должны быть нули
При открытии окна описатель копируется в системную страницу ОЗУ и
программа может не сохранять его. Что бы описатель окна не потерялся,
программа получает идентификатор окна. Он же идентификатор глобального
окна. В функциях запоминания, восстановления, перемещения, а так же
функциях стирания, скроллинга и т.п. идентификатор окна определяет
область экрана, относительно которой производится работа с локальными
окнами. Подразумеваются локальные окна в смысле "окно в окне".
Идентификатор окна определяет глобальное окно, отнoсительно которого
адресуются локальные. В части функций глобальное окно определяется по
умолчанию, как последнее, с которым производились действия с явным
указанием идентификатора.
В данный момент BIOS хранит только один описатель окна - последний,
с которым была произведена функция открытия. Идентификатор окна
выставляется в 0. В дальнейшем планитруется разработка функций со
множеством окон, потому, во избежание неприятностей в будущем, при
работе с окнами, программисту следует запоминать идентификатор окна
и пользоваться этим значением при работе с ним.
Типы стандартных окон:
0 - окно 32x24 в формате ZX Spectrum
1 - текстовое окно 64x24
2 - текстовое окно 40x32
3 - текстовое окно 80x32
4 - окно в формате ZX Spectrum, HL - положение окна на экране в знакоместах
5 - текстовое окно 64x24, HL - положение окна на экране в знакоместах
6 - текстовое окно 40x32, HL - положение окна на экране в знакоместах
7 - текстовое окно 80x32, HL - положение окна на экране в знакоместах
8 - графическое окно 0, HL - положение окна на экране
9 - графическое окно 1, HL - положение окна на экране
Данные палитры должны представлять собой список приблизительно такого вида:
DB blue1,green1,red1,0
DB blue2,green2,red2,0
.....................
DB blueN,greenN,redN,0
N - количество цветов. Значеное равное 0 соответствует 256-ти цветам.
При записи в видео-ОЗУ все данные предварительно проходят функцию AND
со значением регистра маски - B.
Страницы палитры 0..3 соответствуют графическим режимам. Для вывода в
соответствующей палитре нужно задать соответствующее значение bit7..6 в
байте режима знакоместа
Страницы 4..7 соответствуют текстовому режиму и спектрумовскому режиму.
В странице 4 задается цвет PAPER для каждого атрибута. В странице 5
задается цвет INK для каждого атрибута. В странице 6 задается цвет PAPER,
которым он будет моргать в режиме FLASH В странице 7 задается цвет INK,
которым он будет моргать в режиме FLASH. Таким образом, для каждого из
256-ти атрибутов задается четыре цвета если цвета 4,5 совпадают с
цветами 6,7 то режим FLASH оказывается отключенным. Для его включения
в спектрумовском режиме надо поменять местами цвета 6 и 7. Если надо
включить FLASH в режим IBM-CGA, следует установить цвета 6 и 7
одинаковыми и равными цвету 4. По сути режим FLASH всегда включен, и
на экране постоянно меняются цвета PAPER с 4-го на 6-й, а цвета INK с
5 на 7-й. Если эти пары цветов для атрибута знакоместа устанавливаются
одинаковыми, то FLASH в этом месте не виден.
##### *Комментарий к функциям работы с устройствами хранения информации.*
В этих функциях в регистре A обычно задается номер и тип устройства:
бит 0..3 - номер устройства
бит 4..7 - тип устройства:
0 - дисковод
6 - ram-disk
8 - HDD
C - CD-ROM
остальные номера не используются
А так же задаются: старшая часть номера сектора в регисте HL,
младшая часть номера сектора в регистре IX.
@@ -0,0 +1,901 @@
# Функции BIOS v3.00
### Вызов функций BIOS
**Вызов функций BIOS осуществляется из ассемблерного кода.**
Номер функции задается в регистре C процессора. В остальные
регистры, при необходимости, загружаются входные параметры функции.
После исполнения функции происходит возврат в программу, из которой
произошел вызов функции. Установленный флаг CF (CF=1) означает, что
работа функции произошла с ошибкой. В некоторых регистрах передаются
выходные параметры.
Ниже приведены таблицы входных и выходных параметров для каждой функции:
- Функции работы с памятью
- Функции управления 'железом'
- Функции управления окнами и режимами экрана
- Функции вывода текста на экран
- Графические функции
- Функции работы с жесткими дисками и дисководами
При работе функций BIOS необходимо что бы стек находился в области
8000h..0BFFFh, так как часть функций использует переключение страниц
PAGE1 и PAGE3.
Вызов функций BIOS возможен в различных конфигурациях компьютера
Sprinter.
**Конфигурация Spectrum:** вызов функций BIOS осуществляется через
3D13h. При этом работают и все функции TR-DOS.
**Конфигурации Sprinter:** вызов функций BIOS осуществляется через RST
18h при подключенном системном ПЗУ.
Для постоянного подключения системного ПЗУ можно воспользоваться такой
последовательностью команд:
```asm
LD A,0
OUT (07Ch),A
```
После ее исполнения в 0-м окне Z80 будет включено ПЗУ BIOSа и программа
может вызывать функции BIOSа через RST 18h.
Отключение ПЗУ BIOSа из нулевого окна Z80 производится следующей
последовательностью команд:
```asm
LD A,0
OUT (03Ch),A
```
При необходимости, функции BIOSа могут быть вызваны программой,
находящейся в ОЗУ непосредственно в нулевом окне Z80. Для этого надо
установить в адресе 0008h следующий код:
```asm
ADDRESS_0008h:
PUSH AF
LD A,0
OUT (7Ch),A
POP AF
RET
```
После этого BIOS можно вызывать командой RST 8. (Функции TR-DOS, так
же как и в случае RST 18 остаются недоступны.) Вызывая программы таким
образом, через RST 8, следует помнить что адреса 3FFFh..0000h после
входа в BIOS будут содержать код ПЗУ, поэтому, если фунция использует
данные в ОЗУ, они должны находиться в других адресах.
Оптимизация программы для RST 8 недопустима, так как в ПЗУ, для
обратного переключения, стоит такая же программа, только порт 3Ch для
отключения ПЗУ BIOSа.
Вызов функций BIOS в exe-файлах, вызываемых с помощью операционной
системы Estex, производится командой RST 8. Необходимая программа в
адресе 0008h уже имеется в блоке кода ОС Estex.
### Функции работы с памятью
**0C0h (192) EMM_FN0 Определение объемов ОЗУ**
Значение регистров на входе:
C=0C0h
Значение регистров на выходе:
HL - общий объем ОЗУ в страницах по 16k
BC - объем свободного ОЗУ в страницах по 16k
**0C1h (193) EMM_FN1 Инициализация распределения памяти**
Стирается вся информация о выделенных ранее блоках ОЗУ.
Блоки с системной информацией и первые 256K ОЗУ объявляются занятыми.
Значение регистров на входе:
C=0C1h
Значение регистров на выходе:
нет
**0C2h (194) EMM_FN2 Выделение блока ОЗУ**
Значение регистров на входе:
C=0C2h
B - число запрашиваемых страниц
Значение регистров на выходе:
CF=0 - нормальное завершение - A - идентификатор блока
CF=1 - ошибка - A=1 - не хватает памяти
**0C3h (195) EMM_FN3 Освободить блок ОЗУ**
Значение регистров на входе:
C=0C3h
A - идентификатор блока
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неправильный идентификатор
идентификатор не всегда отслеживается правильно
**0C4h (196) EMM_FN4 Получить физический номер страницы из блока памяти**
Значение регистров на входе:
C=0C4h
A - идентификатор блока
B - логическая номер страницы в блоке
Значение регистров на выходе:
CF=0 - A - логический номер страницы
CF=1 - ошибка:
A=0 - блок не существует
A=255 - запрашиваемый номер страницы слишком велик
**0C5h (197) EMM_FN5 Получить список физических страниц блока**
Значение регистров на входе:
C=0C5h
A - идентификатор блока
HL - буфер 256 байт для размещения списка страниц
Значение регистров на выходе:
CF=0 - нормальное завершение:
B - число страниц в блоке
HL - тот же адрес буфера, в буфере список физических
страниц по порядку, заканчивающийся байтом 0FFh
CF=1 - неверный идентификатор блока; старая информация в буфере
может быть затерта
**0C6h (198) EMM_FN6 Получение адресов портов окон**
Примечание по использованию функции получения адресов портов окон (0C6h):
>Cледует хотя бы один раз вызвать эти функции и сравнить адреса
>портов с теми, что используются в программе и, если они не совпадают,
>выдать соответствующее предупреждение. В данный момент эти порты
>таковы: PAGE0=82h, PAGE1=0A2h, PAGE2=0C2h, PAGE3=0E2h
Значение регистров на входе:
C=0C6h
A - номер окна процессора - 0,1,2 или 3
Значение регистров на выходе:
CF=0 - нормальное завершение
C - 8-битный адрес порта окна
B - физический номер подключенной в окно страницы
CF=1 - ошибка - неверный номер окна
**0C7h (199) EMM_FN7 Получить номер следующей страницы блока**
Примечание по использованию функции EMM_FN7 (0C7h):
> Информация о распределении памяти хранится в виде RAM Allocation Table,
> похожей на дисковый FAT. Поэтому нахождение физического номера
> следующей страницы по предыдущему физическому номеру происходит
> значительно быстрее, чем поиск по увеличенному на единицу логическому
> номеру.
Значение регистров на входе:
C=0C7h
A - физическая страница
Значение регистров на выходе:
CF=0 - нормальное завершение
A - следуюшая физическая страница блока
A=0FFh - индицирует конец блока
CF=1 - ошибка - страница не принадлежит никакому блоку.
фактически, это означает, что она свободна.
**09Eh (158) EMM_FN8 Слияние блоков**
Значение регистров на входе:
A - идентификатор блока 1
B - идентификатор блока 2
Значение регистров на выходе:
CF=0 - нормальное завершение
A - идентификатор объединенного блока
CF=1 - ошибка - неверный идентификатор блока
**09Dh (157) EMM_FN9 Разделение блока**
Значение регистров на входе:
C=09Dh
A - идентификатор блока
B - новая длина блока
Значение регистров на выходе:
CF=0 - нормальное завершение
A - идентификатор блока результата
B - идентификатор блока остатка
CF=1 - ошибка - неверный идентификатор блока
### Функции управления 'железом'
**0EFh (239) FN_VERSION Выдача информации о версии BIOSа и железа.**
Значение регистров на входе:
C=0EFh
HL - буфер, куда будет помещена ASCII строка с несколькими
полями, номером версии BIOS и названием модели
компьютера. Конец строки отмечен двойным нулем.
Значение регистров на выходе:
CF=0 - нормальное завершение
HL - тот же буфер с записанной строкой.
DE - версия BIOSа
BC - версия железа подробности ниже
A - количество полей в буфере (в данный момент - 2)
Первое поле - версия BIOS.
Второе - название модели компьютера.
CF=1 - ошибка - Очень старая версия,
не имеющая данной функции
**0F2h (242) FN_SICF=0 Установка синхронизации, очистка страницы режима экрана**
Значение регистров на входе:
C=0F2h
A - режим синхронизации
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неверный номер режима синхронизации
### Функции работы с CMOS данными и Turbo режимом
Комментарий к функциям CMOS (0F5h-0F7h):
> Функции CMOS_RD, CMOS_WR, CMOS_TEST работают всегда. Если в машине нет
> микросхемы CMOS, то эмулируется ее память. Наличие микросхемы
> определяется функцией CMOS_TEST.
**0F5h (245) CMOS_TEST Проверить наличие CMOS**
Значение регистров на входе:
C=0F5h
Значение регистров на выходе:
CF=0 - часы есть
CF=1 - часов нет
**0F6h (246) CMOS_RD Читать из регистра CMOS**
Значение регистров на входе:
C=0F6h
D - номер регистра CMOS
Значение регистров на выходе:
A - считанные данные
CF=0 - часы есть
CF=1 - часов нет
**0F7h (247) CMOS_WR Писать в регистр CMOS**
Значение регистров на входе:
C=0F7h
D - номер регистра CMOS
A - записываемые данные
Значение регистров на выходе:
CF=0 - часы есть
CF=1 - часов нет
**08Fh (143) FN_TURBO Функция управления турбо режимом.**
Комментарий к функции FN_TURBO (08Fh):
Переключение режима турбо может не произойти, если прошивка не
поддерживает это переключение. При этом ошибки не происходит. Так
же, переключение режима TURBO блокируется кнопкой "Turbo" в
режиме Turbo-OFF
Значение регистров на входе:
C=08Fh
A - режим турбо: 2 - off, 3 - on
A - режим FDD: 12h - 720Kb, 13h - 1.44Mb
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неверный режим турбо
### Функции управления окнами и режимами экрана
**0B0h (176) WIN_OPEN Функция открытия окна.**
Значение регистров на входе:
C=0B0h
IX - описатель окна
E - флаги окна:
бит 0 указывает какую страницу режима включать после
исполнения функции;
бит 4 указывает на какой странице режима открывать окно
Значение регистров на выходе:
CF=0 - нормальное завершение:
A - идентификатор окна
CF=1 - ошибка слишком много окон
**0B1h (177) WIN_CLOSE закрытие окна**
Значение регистров на входе:
C=0B1h
A - идентификатор окна
Значение регистров на выходе:
CF=0 - успешное завершение
CF=1 - ошибка - неверный идентификатор
Окно с номером 0 никогда не закрывается и попытка
закрытия приводит к ошибке
**0B2h (178) WIN_COPY_WIN Копирование данных текстового окна в память (запоминание окна)**
При работе этой функции через RST 18h или RST 8, обязателен запрет
прерываний, так как функция пользуется стеком для ускорения своей работы.
Значение регистров на входе:
C=0B2h
A - идентификатор глобального окна
H - размер окна в символах по вертикали
L - размер окна в символах по горизонтали
D - вертикальное положение окна в глобальном окне
E - горизонтальное положение окна в глобальном окне
IX - адрес буфера для запоминания данных
адрес буфера указывается для окна 0C000h если адрес
указан с 8000h, номер страницы буфера не действителен
ниже 8000h адрес указывать нельзя
A' - страница буфера для данных окна эта страница должна
принадлежать программе
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка - неверный идентификатор окна
**0B3h (179) WIN_RESTORE_WIN Копирование из памяти в текстовое окно (восстановление окна)**
При работе этой функции через RST 18h или RST 8, обязателен запрет
прерываний, так как функция пользуется стеком для ускорения своей работы.
Значение регистров на входе:
C=0B2h
A - идентификатор глобального окна
H - размер окна в символах по вертикали
L - размер окна в символах по горизонтали
D - вертикальное положение окна в глобальном окне
E - горизонтальное положение окна в глобальном окне
IX - адрес буфера для запоминания данных
адрес буфера указывается для окна 0C000h если адрес
указан с 8000h, номер страницы буфера не действителен
ниже 8000h адрес указывать нельзя
A' - страница буфера для данных окна эта страница должна
принадлежать программе
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка - неверный идентификатор окна
**0B4h (180) WIN_GET_SYM Взять символ с экрана**
Значение регистров на входе:
C=0B4h
A - идентификатор окна
DE - положение символа в окне:
D - вертикаль, E - горизонталь
Значение регистров на выходе:
CF=0 - нормальное завершение
L - символ, H - атрибут,
B - знакогенератор
CF=1 - ошибка неверный идентификатор окна
**0B5h (181) WIN_PUT_SYM Положить символ на экран**
Значение регистров на входе:
C=0B5h
A - идентификатор окна
DE - положение символа в окне:
D - вертикаль, E - горизонталь
L - символ, H - атрибут символа
B - знакогенератор
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка неверный идентификатор окна
**0B6h (182) WIN_SET_ZG установка знакогенератора**
Значение регистров на входе:
C=0B6h
A - системный номер знакогенератора
DE - указатель на 2Kb блок данных знакогенератора
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка (старая версия, нет функции)
**0B7h (183) WIN_MOVE_WIN Перемещение окна**
При работе этой функции через RST 18h или RST 8, обязателен запрет
прерываний, так как функция пользуется стеком для ускорения своей работы.
Значение регистров на входе:
C=0B7h
A - идентификатор глобального окна
H - размер локального окна по вертикали в символах
L - размер локального окна по горизонтали в символах
D - положение локального окна по вертикали в символах
E - положение локального окна по горизонтали в символах
IX - новое положение локального окна (подобно DE)
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка - неверный идентификатор окна
**0B8h (184) WIN_GET_ZG Получение знакогенератора**
Значение регистров на входе:
C=0B8h
DE - адрес, куда будет загружено 2kb знакогенератора
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка (старая версия, нет функции)
### Функции вывода текста на экран
**081h (129) LP_PRINT_ALL Печать символов с атрибутом**
На экран выводится строка из B одинаковых символов
Значение регистров на входе:
C=081h
A - символ
E - атрибут
B - число выводимых символов
регистры HL,IX - сохраняются
Значение регистров на выходе:
CF=0 - всегда
**082h (131) LP_PRINT_SYM Вывод символов на экран без атрибута**
На экран выводится строка из B одинаковых символов атрибут
остается тот, который был на экране
Значение регистров на входе:
C=082h
A - символ
B - число выводимых символов
регистры HL,IX - сохраняются
Значение регистров на выходе:
CF=0 - всегда
**083h (131) LP_PRINT_ATR печать атрибутов**
На экран выводится строка из B одинаковых атрибутов.
Символы не меняются.
Значение регистров на входе:
C=083h
E - атрибут
B - число выводимых символов
регистры HL,IX - сохраняются
Значение регистров на выходе:
CF=0 - всегда
**084h (132) LP_SET_PLACE Установка текущего знакоместа в окне**
Позиция печати устанавливается в соответстии с регистром DE
Значение регистров на входе:
C=084h
E - положение символа по горизонтали
D - номер символа по вертикали
Превышение границ приводит не к ошибке, а к переустановке
с начала, за вычетом полного размера окна
Значение регистров на выходе:
CF=0 - всегда
**085h (133) LP_PRINT_LN Вывод строки символов на экран с текущего знакоместа**
Значение регистров на входе:
C=085h
HL - адрес строки должен быть между 04000h и 0BFFFh
E - атрибут, с которым будет выведена строка
B - длина выводимой строки
Значение регистров на выходе:
CF=0 - всегда
**086h (134) LP_PRINT_LN2 Вывод строки символов на экран без атрибутов**
Значение регистров на входе:
C=086h
HL - адрес строки должен быть между 04000h и 0BFFFh
B - длина выводимой строки
Значение регистров на выходе:
CF=0 - всегда
**087h (135) LP_PRINT_LN3 Вывод строки символов до разделителя**
После разделителя выводятся пробелы, что бы вывести B символов
Значение регистров на входе:
C=087h
HL - адрес строки должен быть между 04000h и 0BFFFh
E - атрибут, с которым будет выведена строка
B - длина выводимой строки
D - символ-разделитель, указывающий конец строки
Значение регистров на выходе:
CF=0 - всегда
**088h (136) LP_PRINT_LN4 Вывод строки символов до разделителя, без атрибутов**
символы из выводятся на экран, пока не встретится символ равный D,
далее печатаются пробелы, как дополнение строки до B символов.
Атрибуты не изменяются.
Значение регистров на входе:
C=088h
HL - адрес строки должен быть между 04000h и 0BFFFh
B - длина выводимой строки
D - символ-разделитель, указывающий конец строки
Значение регистров на выходе:
CF=0 - всегда
**089h (137) LP_CLS_WIN Очистка экрана**
Выполнение производится выводом пробелов с заданным атрибутом
Значение регистров на входе:
C=089h
DE - положение локального окна
H - размер в символах локального окна по вертикали
L - размер в символах локального окна по горизонтали
B - атрибут очистки
Значение регистров на выходе:
CF=0 - всегда
**08Ah (138) LP_SCROLL_UD Скроллинг части глобального окна вверх/вниз**
Скроллируются полные строки глобального окна
Значение регистров на входе:
C=08Ah
B - тип скроллинга: 1 - вверх; 2 - вниз
D - начальная строка скроллинга
E - число скроллируемых строк
Значение регистров на выходе:
CF=0 - всегда
**08Bh (139) LP_PRINT_LN5 Вывод строки символов на экран до разделителя**
После разделителя вывод останавливается
Значение регистров на входе:
HL - адрес строки должен быть между 04000h и 0BFFFh
E - атрибут, с которым будет выведена строка
B - максимальная длина выводимой строки
D - символ-разделитель, указывающий конец строки
Значение регистров на выходе:
CF=0 - всегда
**08Ch (140) LP_PRINT_LN6 Вывод строки символов на экран до разделителя без атрибутов**
После разделителя вывод останавливается.
Значение регистров на входе:
C=08Ch
HL - адрес строки должен быть между 04000h и 0BFFFh
B - максимальная длина выводимой строки
D - символ-разделитель, указывающий конец строки
Значение регистров на выходе:
CF=0 - всегда
**08Dh (141) LP_CLS_WIN2 Очистка экрана с указанием символа заполнения**
Значение регистров на входе:
C=08Dh
DE - положение локального окна
H - размер в символах локального окна по вертикали
L - размер в символах локального окна по горизонтали
B - атрибут очистки
A - символ очистки
Значение регистров на выходе:
CF=0 - всегда
**08Eh (142) LP_GET_PLACE Получить текущее положение вывода на экран**
Значение регистров на входе:
C=08Eh
Значение регистров на выходе:
CF=0 - всегда
DE - координаты, в которых будет напечатан
следующий символ:
D - вертикаль, E - горизонталь
### Графические функции
**0A1h (161) PIC_POINT Установить точку**
Значение регистров на входе:
C=0A1h
DE - координата по вертикали (пиксели)
HL - координата по горизонтали (пиксели)
Координаты считаются от верхнего левого угла экрана
A - идентификатор окна
B - цвет точки
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неверный идентификатор окна
**0A4h (164) PIC_SET_PAL Установка палитры**
Значение регистров на входе:
C=0A4h
HL - данные палитры
E - номер начального цвета
D - количество устанавливаемых цветов
B - маска при установке палитры.
Для нормального режима должнa быть 0FFh
A -
0..3 биты - значения 0..7 - номер палитры
- значения 8..15 - зарезервированы
4..6 биты - зарезервированы (установить в 0)
7 бит -
- значение 0 - установить
- значение 1 - загрузить палитру в память
Значение регистров на выходе:
CF=0 - всегда
**0A6h (166) SET_PAL_INIT Установка внутренней палитры**
Значение регистров на входе:
C=0A6h
A - страница палитры
B - номер палитры:
B=3 - установка текстовой палитры CGA
B=2 - установка спектрумовской палитры
B=1 - установка графической плаитры
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - неверный номер палитры
### Функции работы с жесткими дисками и дисководами
**50h,52h,53h (80,82,83) Зарезервированы**
Значение регистров на входе и на выходе:
нет
**51h (81) DRV_RESET Сброс контроллера и настройка на диск**
Значение регистров на входе:
C=51h
A - номер и тип устройства
бит 7..4 - тип устройства:
#0x - FDD
#6x - RAM-DISK
#8x - HDD
#Cx - CD-ROM
бит 3..0 - номер устройства
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка, нет диска или нет устройства
**54h (84) DRV_VERIFY Проверка секторов**
Проверка внутренняя на совпадение ECC
Значение регистров на входе:
C=54h
A - номер и тип устройства (см. выше)
HL:IX - номер сектора (IX - младшая часть номера сектора)
B - количество проверяемых секторов
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - проверка с ошибкой
**55h (85) DRV_READ Чтение с устройства**
Значение регистров на входе:
C=55h
A - номер и тип устройства (см. выше)
HL:IX - номер сектора (IX - младшая часть номера сектора)
B - количество секторов
DE - адрес буфера для данных
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка чтения
HL:IX - номер сектора + кол-во прочитанных секторов
DE - адрес буфера для данных + (кол-во прочитанных секторов * размер сектора)
**56h (86) DRV_WRITE Запись на устройства**
Значение регистров на входе:
C=56h
A - номер и тип устройства (см. выше)
HL:IX - номер сектора (IX - младшая часть номера сектора)
B - количество секторов
DE - адрес буфера данных для записи
Значение регистров на выходе:
CF=0 - нормальное завершение
CF=1 - ошибка записи
HL:IX - номер сектора + кол-во прочитанных секторов
DE - адрес буфера для данных + (кол-во прочитанных секторов * размер сектора)
**57h (87) DRV_DETECT Определение параметров устройства**
Значение регистров на входе:
C=57h
A - номер и тип устройства (см. выше)
Значение регистров на выходе:
CF=0 - нормальное завершение
A - bit7=0 диск 720Кb
bit7=1 диск 1.44Mb
CF=1 - нет устройства или нет носителя
**58h (88) DRV_GET_PAR Получить параметры носителя**
Значение регистров на входе:
C=58h
A - номер и тип устройства (см. выше)
Значение регистров на выходе:
CF=0 - нормальное завершение
L - число секторов
H - число головок
DE - количество цилиндров
если HL=DE=0FFFFh - устройства нет
IX - размер сектора в байтах
B - доп. параметры для дискет:
бит7 - тип 1.44Mb/720Kb
CF=1 - нет устройства
**59h (89) DRV_SET_PAR Установить параметры носителя**
Значение регистров на входе:
A - номер и тип устройства (см. выше)
L - число секторов
H - число головок
DE - количество цилиндров
IX - размер сектора в байтах
B - доп. параметры для дискет
бит7 - тип 1.44Mb/720Kb
Значение регистров на выходе:
CF=0 - нормальное завершение
**5Ah (90) EXT_VERSION Номер версии дисковой спецификации.**
Значение регистров на входе:
C=5Ah
Значение регистров на выходе:
CF=0 - нормальное завершение
D - версия
E - модификация
CF=1 - ошибка
**5Fh (95) DRV_LIST Список дисковых устройств**
Значение регистров на входе:
C=5Fh
IX - буфер для списка устройств
Значение регистров на выходе:
CF=0 - нормальное завершение
В буфере список дисков в формате:
IX+0 - размер заполненого буфера
IX+1 - кол-во устройств FDD
IX+2 - кол-во устройств HDD
IX+3 - кол-во устройств CD DRIVE
IX+4 - #00 - конец списка, иначе кол-во устройств нового типа
### Примечания и комментарии.
##### *Примечание по использованию функции получения адресов портов окон (0C6h).*
Cледует хотя бы один раз вызвать эти функции и сравнить адреса портов с
теми, что используются в программе и, если они не совпадают, выдать
соответствующее предупреждение. В данный момент эти порты таковы:
PAGE0=82h, PAGE1=0A2h, PAGE2=0C2h, PAGE3=0E2h
##### *Примечание по использованию функции EMM_FN7 (0C7h).*
Информация о распределении памяти хранится в виде RAM Allocation Table,
похожей на дисковый FAT. Поэтому нахождение физического номера следующей
страницы по предыдущему физическому номеру происходит значительно быстрее,
чем поиск по увеличенному на единицу логическому номеру.
##### *Комментарий к функции FN_VERSION (0EFh).*
Значения регистра BC на выходе и соответствующая ему конфигурация
BC=FFFF - Не определено
BC=FFFE - Конфигурация Spectrum, режим Sprinter ZX
BC=FFFD - Конфигурация Sprinter
BC=FFFC - Зарезервировано
BC=FFFB - Зарезервировано
BC=FFFA - Зарезервировано
BC=FFF9 - Зарезервировано
Иные значения BC - новые прошивки.
##### *Комментарий к функциям CMOS (0F5h-0F7h)*
Функции CMOS_RD, CMOS_WR, CMOS_TEST работают всегда. Если в машине нет
микросхемы CMOS, то эмулируется ее память. Наличие микросхемы
определяется функцией CMOS_TEST.
##### *Комментарий к функции FN_TURBO (08Fh)*
Переключение режима турбо может не произойти, если прошивка не
поддерживает это переключение. При этом ошибки не происходит. Так же,
переключение режима TURBO блокируется кнопкой "Turbo" в режиме Turbo-OFF
##### *Комментарий к функциям печати текста.*
Эти функции работают с текущим окном, которым всегда является последнее
открытое окно. К графическому экрану функции печати текста не применимы.
##### *Описатель окна.*
Для открытия окон используется 32-хбайтовый описатель окна (дескриптор),
адрес которого указывается в регистре IX.
IX - 32-хбайтовый описатель окна
(IX+0) - горизонтальный размер окна в знакоместах
(IX+1) - вертикальный размер в знакоместах
(IX+2) - положение окна по горизонтали на экране в знакоместах
(IX+3) - положение окна по вертикали на экране в знакоместах
(IX+4) - режим знакоместа
bit4=1 - text_mode bit4=0 - graf_mode
bit5=0 - 16, bit5=1 - 8 точек в знакоместе
graf_mode bit3..0 - не существенны
bit7..6 - номер палитры
text_mode bit7..6,3..0 - номер знакогенератора
исключение: bit7..6=B"11" - бордер
(IX+5) - дополнительный режим знакоместа
bit0=1 - указывает на включение спектрумовской
адресации экрана
(IX+6) - положение по X в поле графики (по знакоместам)
(IX+7) - положение по Y в поле графики (по знакоместам)
разъяснения о положении в поле графики - ниже
(IX+8..31) - зарезервировано (переменные окна)
в этих байтах должны быть нули
При открытии окна описатель копируется в системную страницу ОЗУ и
программа может не сохранять его. Что бы описатель окна не потерялся,
программа получает идентификатор окна. Он же идентификатор глобального
окна. В функциях запоминания, восстановления, перемещения, а так же
функциях стирания, скроллинга и т.п. идентификатор окна определяет
область экрана, относительно которой производится работа с локальными
окнами. Подразумеваются локальные окна в смысле "окно в окне".
Идентификатор окна определяет глобальное окно, отнoсительно которого
адресуются локальные. В части функций глобальное окно определяется по
умолчанию, как последнее, с которым производились действия с явным
указанием идентификатора.
В данный момент BIOS хранит только один описатель окна - последний,
с которым была произведена функция открытия. Идентификатор окна
выставляется в 0. В дальнейшем планитруется разработка функций со
множеством окон, потому, во избежание неприятностей в будущем, при
работе с окнами, программисту следует запоминать идентификатор окна
и пользоваться этим значением при работе с ним.
Типы стандартных окон:
0 - окно 32x24 в формате ZX Spectrum
1 - текстовое окно 64x24
2 - текстовое окно 40x32
3 - текстовое окно 80x32
4 - окно в формате ZX Spectrum, HL - положение окна на экране в знакоместах
5 - текстовое окно 64x24, HL - положение окна на экране в знакоместах
6 - текстовое окно 40x32, HL - положение окна на экране в знакоместах
7 - текстовое окно 80x32, HL - положение окна на экране в знакоместах
8 - графическое окно 0, HL - положение окна на экране
9 - графическое окно 1, HL - положение окна на экране
Данные палитры должны представлять собой список приблизительно такого вида:
DB blue1,green1,red1,0
DB blue2,green2,red2,0
.....................
DB blueN,greenN,redN,0
N - количество цветов. Значеное равное 0 соответствует 256-ти цветам.
При записи в видео-ОЗУ все данные предварительно проходят функцию AND
со значением регистра маски - B.
Страницы палитры 0..3 соответствуют графическим режимам. Для вывода в
соответствующей палитре нужно задать соответствующее значение bit7..6 в
байте режима знакоместа
Страницы 4..7 соответствуют текстовому режиму и спектрумовскому режиму.
В странице 4 задается цвет PAPER для каждого атрибута. В странице 5
задается цвет INK для каждого атрибута. В странице 6 задается цвет PAPER,
которым он будет моргать в режиме FLASH В странице 7 задается цвет INK,
которым он будет моргать в режиме FLASH. Таким образом, для каждого из
256-ти атрибутов задается четыре цвета если цвета 4,5 совпадают с
цветами 6,7 то режим FLASH оказывается отключенным. Для его включения
в спектрумовском режиме надо поменять местами цвета 6 и 7. Если надо
включить FLASH в режим IBM-CGA, следует установить цвета 6 и 7
одинаковыми и равными цвету 4. По сути режим FLASH всегда включен, и
на экране постоянно меняются цвета PAPER с 4-го на 6-й, а цвета INK с
5 на 7-й. Если эти пары цветов для атрибута знакоместа устанавливаются
одинаковыми, то FLASH в этом месте не виден.
##### *Комментарий к функциям работы с устройствами хранения информации.*
В этих функциях в регистре A обычно задается номер и тип устройства:
бит 0..3 - номер устройства
бит 4..7 - тип устройства:
0 - дисковод
6 - ram-disk
8 - HDD
C - CD-ROM
остальные номера не используются
Binary file not shown.
+155
View File
@@ -0,0 +1,155 @@
Estex: Disk SubSystem (DSS) Programming Guide
Table of Contents
1. Introducing
2. Identification of system functions
3. Disk devices functions
1. Introducing
This document contains the list of functions and concepts of
interaction with a disk subsystem.
DSS is a collection of very useful functions that reside in
DSS itself, ready for use by any your programs. These functions
are stored in library SYSTEM.DOS and allow management of files,
memory allocation, loading and execution of the programs.
File specification
The file specification is a string, containing a names of disk,
directories separated by a symbol "\" and name of file. The names
of disk drive and directories can be discard.
for example:
C:\TEXT\DOC\text.doc
A:file.txt
\TEXT\info.txt
The DSS used chars with colon suffix as names of disk devices
(A:, B:, C: etc.) The name of disk can be written down before
filename for specified disk there it placed.
For example: command
DIR C:DATFILE
searches for DATFILE in the current directory of disk C:.
When disk name not specified DSS used current disk. At start
DSS, the current disk is a disk whence was loaded DSS.
The filenames consist of two parts. The first part contain 8
chars of file name. The second part is not necessary and contain
3 chars of file type (also known as extentsion). At the writing
of filename, both parts are separated by char point.
For example: the names "NAME" and "NAME." is specified same file.
In the name don't allows symbols with codes less 32 and chars
. " / \ [ ] : | < > + = ; ,
As the subdirectories files too, their names are formed by
same way. The name of root directory always "\". And each
subdirectories contain two items with names "." and "..". The
name "." specified a current directory and name ".." specified
name of parent(uplevel) directory.
Some console commands and DSS functions allow to use global
symbols ? and * which can be used for filename templates.
The symbol ? means that any one char of filename. The
symbol * means that it char can be replaced by any symbols.
For example:
*.txt - means, all files with type "txt"
a??.* - means, files which contain three or less symbols and
first symbol is "a"
dc*.exe - means, files with type "exe" and began "dc"
File attributes
The each bit of byte attributes specified various attribute.
And it can be changed by DSS function.
bit 0 - Read only
bit 1 - Hidden
bit 2 - System
bit 3 - Volume label
bit 4 - Directory
bit 5 - Archive
bit 6 - Reserved
bit 7 - Reserved
Attribute "read only". When value is 1, file can be read,
but can't be written or deleted.
Attribute "hidden". When value is 1, DSS can't manipulate
with this file.
Attribute "system". Specified system file.
Attribute "volume label". In old version of MSDOS used for
specified volume laben, now it can be used for long filenames.
And must be 0 for compatibles.
Attribute "directory". When value is 1, means that this
file is directory.
Attribute "archive". This bite sets in 1 when DSS writing
in this file. It can be used in backup utilities for detect
changed files.
File handle
When any file are opened, DSS build File Control Block in
DSS working areas.
The Handle (and assigned file) identified by number which
returned DSS to program after file opening and used it in all
further DSS calls. In other words, when file is opening, the
program informs DSS his name and has taken back file handle.
Which used in further file operations.
All necessary information for working with file are placed
in DSS working areas.
2. Identification of system functions
00h (00) VERSION (Version of DSS)
input:
C - 00h
output:
D - version number
E - modification
The function return version number of DSS.
3. Disk devices functions
01h (01) CHDISK (Change current disk)
input:
A - disk number (0-A,1-B...)
C - 01h
output:
A - error code, if CF=1
A - number of disks, if CF=0
The function changes current disk device.
02h (02) CURDISK (Current disk number)
input:
C - 02h
output:
A - current disk number (0-A,1-B...)
The function returns number of current disk device.
03h (03) DSKINFO (Disk information)
input:
A - disk number (0-A,1-B...0FFh-current)
C - 03h
output:
A - error code, if CF=1
A - sectors per cluster, if CF=0
HL - clusters per disk
DE - free clusters
BC - bytes per sector
The function returns information about disk device
(capacity and free space).
for example:
LD C,03h ;Function DSKINFO
LD A,0FFh ;Information about current disk
RST 10h ;Execution of function
LD A,D ;There is a free
OR E ;space?
JR Z,NO_SPACE ;No, the disk is completely filled
09h (09) BOOTDSK (Number of boot disk)
input:
C - 09h
B = 0
output:
A - number of boot disk (0-A,1-B...)
The function returns number of boot disk device whence was loaded DSS.
+126
View File
@@ -0,0 +1,126 @@
Estex: Дисковая подсистема (DSS) – Обзор
1. Введение
2. Загрузка подсистемы
3. Системная консоль
4. Файловая система
1. Введение
Estex - операционная система компьютера Sprinter, включающая
в себя различные модули. Данный документ описывает модуль дисковой
подсистемы.
В DSS используется та же самая файловая система, как и в
MS-DOS FAT16 и полностью с ней совместима.
2. Загрузка подсистемы
После включения питания или сброса компьютера, BIOS считывает
первичный загрузчик с 1-го сектора загрузочного диска.
Если загрузка происходит с HDD или дискеты, то сначала загрузочный
сектор считывается в память и ему передается управление по загрузке
модуля дисковой подсистемы SYSTEM.DOS.
Затем выполняются следующие действия:
• инициализация дисковой подсистемы и вывод сообщения "Starting DOS..."
• загрузка системной консоли SYSTEM.EXE
• выполнение команд указанных в файле SYSTEM.BAT
Обычно файл SYSTEM.BAT содержит путь к программе файловой навигации
пользователя или другому часто используемому приложения.
Например "c:\fn\fn.exe".
Если во время загрузки вы хотите пропустить выполнение SYSTEM.BAT.
То вам следует нажать клавишу "SHIFT", как только появиться
сообщение "Starting DOS..." и удерживать пока не появится приглашение
консоли ("C:\>").
3. Системная консоль
В DSS многие задачи могут быть выполнены через интерфейс командной
строки называемой системной консолью. Основная задача консоли ввод
команд и их исполнение. Также она имеет ряд функций, которые выполняют
такие действия как управление файлами, перемещение по файловой структуре
каталогов, редактирование командной строки и переменных среды.
Системная консоль позволяет пользователю взаимодействовать с
операционной системой. Для DOS системная консоль это SYSTEM.EXE. Если вы
видите на экране приглашение командной строки (A:\> или C:\>), то это
означает что SYSTEM.EXE загружен и активизирован. Когда вы вводите
командную строку, командный процессор интерпретирует команду и выполняет
необходимые действия.
На сегодняшний день в консоли доступны следующие команды:
CD Displays the name of or changes the current directory.
CHDIR Displays the name of or changes the current directory.
CLS Clears the screen.
DATE Displays or sets the date.
DEL Deletes one or more files.
DIR Displays a list of files and subdirectories in a directory.
ECHO Displays messages, or turns command echoing on or off.
ERASE Deletes one or more files.
EXIT Quits the SYSTEM.EXE program (command interpreter).
HELP Provides Help information for console commands.
MD Creates a directory.
MKDIR Creates a directory.
PAUSE Suspends processing of a batch file and displays a message.
RD Removes a directory.
REM Records comments (remarks) in batch files or SYSTEM.BAT.
REN Renames a file or files.
RENAME Renames a file or files.
RMDIR Removes a directory.
TIME Displays or sets the system time.
VER Displays the System version.
4. Файловая система
Сейчас, в качестве файловой системы Estex использует FAT12 и FAT16.
С помощью файловой системы FAT (File Allocation Table) организуются
данные на винчестере и дискетах.
Для указания спецификации файла используется следующая форма:
[drive:][directory\]filename[.ext]
Файловая спецификация - это строка символов содержащая наименования
диска, директорий отделенных символом "\" и имя файла. Имена диска и
директории могут быть опущены, если требуемый файл расположен в текущей
директории.
Например:
C:\TEXT\DOC\text.doc
A:file.txt
\TEXT\info.txt
В DSS в качестве имен дисковых устройств используются буквы с
последующим символом двоеточия (A:, B:, C: и.т.д.) Имя диска может быть
набрано перед именем файла для указания диска, на котором он расположен.
Например:
команда
DIR C:TESTFILE
ищет TESTFILE в текущей директории диска C:.
Если имя диска не указанно используется текущий диск. После запуска DSS,
текущим диском является диск, с которого была загружена DSS.
Имена файлов состоят из двух частей. Первая часть может содержать
8 букв, цифр или следующие специальные символы:
$ % ' _ @ { } ~ ` ! # ( ).
Вторая часть не является обязательной и содержит любую комбинацию из трех
букв, цифр или специальных символов с предшествующей точкой (.).
Например имена "NAME" и "NAME." указывают на одинаковый файл.
В имени файла не допускаются символы с кодом меньше 32, а также символы
. " / \ [ ] : | < > + = ; ,
Поскольку директории также являются файлами их имена образуются по
тем же правилам.
Имя корневой директории всегда "\". И каждая поддиректория содержит
два элемента с именами "." and "..". Имя "." указывает на текущую
директорию, а имя ".." указывает на родительную (на уровень выше)
директорию.
Некоторые команды и функции DSS позволяют использовать глобальные
символы * и ? которые могут использоваться для задания шаблона имени
файла.
Символ ? означает любой один символ в имени файла.
Символ * означает, что он может быть заменен на любое количество
любых символов.
for example:
*.txt - означает, все файлы с типом "txt"
a??.* - означает, файлы содержащие три и менее символов и первый символ "a"
dc*.exe - означает, файлы с типом "exe" и начинающиеся на "dc"
В именах файлов не делается различий между заглавными и прописными
символами.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,97 @@
Акселератор компьютера Sprinter.
Акселератор предназначен для ускорения операций по пересылке блоков данных в ОЗУ и видео-ОЗУ.
Акселератор позволяет:
- быстро заполнять горизонтальную или вертикальную линию длиной до 256 точек одним цветом (в режиме 640x256 - одинарную/двойную горизонтальную линию длиной до 512 точек)
- быстро копировать горизонтальную или вертикальную линию длиной до 256 точек (в режиме 640x256 - горизонтальную линию до 512 точек)
- проводить быстрые операции AND, OR, XOR с блоками памяти.
Акселератор не может работать с блоками данных ПЗУ и Быстрого-ОЗУ.
ОЗУ акселератора является частью внутреннего ОЗУ ППЛМ.
Операции по пересылке данных производятся путем записи блока данных в это
внутреннее ОЗУ, а затем копировании его в нужное место ОЗУ из ОЗУ акселератора.
Блок данных, записываемый в ОЗУ акселератора может иметь различную
длинну из диапазона 1..256 байт.
После одной записи копирование может производиться несколько раз и, таким
образом, можно производить заполнение экрана текстурами.
Для заполнения экрана одним цветом используется другой режим
акселератора. В нем вместо копируемого блока данных из внутреннего ОЗУ
производится запись данных с шины процессора, которые в этот момент не
изменяются.
Управление акселератором производится непосредственно из программы.
Для этого изпользуются команды процессора, которые, фактически, являются
операциями типа NOP.
LD B,B - выключить акселетарор.
LD D,D - включить акселератор в режим приема байта размера блока
далее следует команда типа LD A,dat, где dat и будет новым
размером блока. Если размер блока был установлен ранее,
его можно не устанавливать.
LD C,C - Операция Fill - заполнение одним байтом. Последующая
команда типа LD (HL),A приведет к заполнению указанного
ранее количества байт значением A
LD E,E - Операция Fill для графического экрана - заполнение
вертикальных линий.
LD H,H - rezerved
LD L,L - копирование блока. Последующая команда типа LD A,(HL)
приведет к заполнению ОЗУ акселератора данными из адреса (HL),
а команда типа LD (DE),A приведет к перезаписи данных из ОЗУ
акселератора в ОЗУ или видео-ОЗУ.
LD A,A - копирование блока для графического экрана подобна команде
LD L,L, но работает с вертикальными линиями экрана.
Пример использования акселератора:
; Считаем, что экранная страница уже открыта по адресу #C000
LD HL,#C040 ; адрес начала линии первого экрана
LD DE,#C180 ; адрес начала линии второго экрана
LD BC,#140 ; длина экрана по горизонтали
DI ; запретить прерывания для работы с акселератором
LD D,D ; включить акселератор на установку размера блока
LD A,0 ; установить размер блока - 256 байт
LD A,A ; установить акселератор на копирование
; вертикальных линий.
LDIR ; копировать
LD B,B ; выключить акселератор
EI ; включить прерывания
Эта часть программы произведет копирование всего содержимого первого экрана на другой.
Время исполения составляет примерно 26 милисекунд.
Дополнительные функции акселератора (AND, OR, XOR) работают таким же образом.
Для выполнения логических функций используются команды XOR (HL); OR (HL); AND (HL).
Пример кодирования блока в 256 байт.
LD HL,ADRES_1
LD DE,XOR_DAT
DI
LD D,D
LD A,0 ; число байт, которые надо закодировать
LD L,L
LD A,(DE) ; взять блок данных в ОЗУ акселератора
XOR (HL) ; произвести операцию XOR с данными акселератора
LD (HL),A ; запомнить в ОЗУ результат операции
LD B,B
EI
Скорость работы акселератора ограничивается только физической
скоростью работы основного ОЗУ. Определить примерное время работы команды с
акселератором можно по такой формуле:
Время работы = время работы команды без акселератора + время работы
акселератора
Время работы акселератора = число пересылаемых байт /7 микросекунд
Во время работы акселератора необходимо отключать прерывания, так как в этот момент
изменяется система команд процессора и программа на прерывании не сможет работать корректно.
@@ -0,0 +1,635 @@
ÜÜÜÜÜÜ
ÜÛ°°°°°°
Û°° Û°°
Û°°ÜÜÜÛ°°
Û°°°°°°°°
Û°° Û°°
Û°° Û°° àå¨â¥ªâãà  ª®¬¯ìîâ¥à  Sprinter.
ß°° ß°°
‚¢¥¤¥­¨¥.
„ ­­®¥ ®¯¨á ­¨¥ ¯à¥¤¯®« £ ¥â ­ «¨ç¨¥ ®¯à¥¤¥«¥­­ëå §­ ­¨© ç¨â â¥«ï,
  ¨¬¥­­® §­ ­¨¥  àå¨â¥ªâãàë ª®¬¯ìîâ¥à  ZX-Spectrum ¨ ¨å à §­®¢¨¤­®á⥩, ¢
ç áâ­®á⨠Pentagon-128 ¨ Scorpion-256,   â ª ¦¥ §­ ­¨¥ ï§ëª  BASIC ¨
­¥ª®â®à®¥ §­ ª®¬á⢮ á ï§ëª®¬  áᥬ¡«¥à  Z80.
‡¤¥áì ï ¡ã¤ã ­ §ë¢ âì ª®­ä¨£ãà æ¨¥© ¬ è¨­ë - ª®­ªà¥â­ãî ॠ«¨§ æ¨î
ª®­ªà¥â­®© áå¥¬ë ¢ ¯¥à¥¯à®£à ¬¬¨à㥬®© «®£¨ç¥áª®© ¬¨ªà®á奬¥ (‹Œ).
â® ®§­ ç ¥â, çâ® ¬ è¨­  ¨¬¥¥â ¬­®¦¥á⢮ ª®­ä¨£ãà æ¨©, ª ¦¤ ï ¨§ ª®â®àëå
¨¬¥¥â ᢮î á奬ã.
Ÿ â ª ¦¥ ¨á¯®«ì§ãî ¯®­ï⨥ Š˜-އ“. â® ­¥ Š˜ ¢ ä®à¬ «ì­®¬
á¬ëá«¥,   ¡ëáâ஥ އ“, ¢ ª®â®à®¬ ¯à®æ¥áá®à ¬®¦¥â à ¡®â âì ­  ¢ë᮪®©
ç áâ®â¥ ¡¥§ ®¦¨¤ ­¨ï. Š˜-¥¬ íâ® Ž‡“ ­ §ë¢ ¥âáï ⮫쪮 ¯® âà ¤¨æ¨¨,
¯®¤®¡­® Š˜-ã ­  Š537“10 ¢ ª®¬¯ìîâ¥à å Pentagon-128.
Šà âª¨¥ ¤ ­­ë¥ ª®¬¯ìîâ¥à  Sprinter.
p®æ¥áá®p . . . . . . . . . . . Z84C15
’ ªâ®¢ ï ç áâ®â  . . . . 21MHz/3.5MHz
އ“ . . . . . . . . . . . . . . 4096Kb
Š˜ އ“ . . . . . . . . . . . . . 64Kb
‡“ . . . . . . . . . . . . . . .128Kb
‚¨¤¥®-އ“ . . . . . . . . . 256Kb(512)
Š®­âp®««¥p ¤¨áª®¢ . . . . . Šp1818‚ƒ93
®¤¤¥p¦ª  1.44Mb ä®p¬ â  . . 3.5"¤¨áª 
Š®­âp®««¥p ¢¨­ç¥áâ¥p  . . . . . IDE/AT
Š®­âp®««¥p ª« ¢¨ âãpë . . . 101key/AT
Š®­âp®««¥p ¬ëè¨ . . . . . . . MS-Mouse
„¢  á«®â  . . . . . . . áâ ­¤ pâ ISA-8
†¥«¥§­ ï í¬ã«ïæ¨ï AY-3-8910 áâ¥à¥®-OUT
COVOX . . . . . . . . . 8bit x 4chanel
‚¨¤¥®-p¥¦¨¬ë: . . . Spectrum standart
GRAF 320x256x256,640x256x16, TXT 80x32
‚ë室 ¢¨¤¥® ­  TV ¨«¨ CGA ¬®­¨â®p, RGB
’¥å­¨ç¥áª ï ॠ«¨§ æ¨ï.
Ÿ¤à®¬ ¬ è¨­ë ïîâáï ¯à®æ¥áá®à Z84C15 ¨ ‹Œ EPF10K10QC208.
Šà®¬¥ ­¨å ­  ¯« â¥ ¯à¨áãâáâ¢ãîâ ¬¨ªà®á奬a ‡“, 72å-¯¨­®¢ë© SIMM
­  4Mb, 256Kb ¢¨¤¥®-އ“, 64Kb Š˜-އ“, á奬  ª®­â஫«¥à  ¤¨áª®¢®¤  ­ 
ˆ‘ Š1818‚ƒ93, ¡ãä¥àë ¤«ï ¯®¤ª«î祭¨ï ¤¦®©á⨪ , ¬ £­¨â®ä®­ , ¯à¨­â¥à ,
ª« ¢¨ âãàë, ¤¨áª®¢®¤®¢, ¢¨­ç¥áâ¥à , ¬ëè¨, ¡ãä¥à­ë¥ ¬¨ªà®á奬ë 設ë ISA-8
¨ ¥é¥ ®¤­  ‹Œ ä¨à¬ë ALTERA - EPM7032LC44. â  ‹Œ ­¥ ¬¥­ï¥â ᢮¥©
ª®­ä¨£ãà æ¨¨ ¨ ¯à¥¤­ §­ ç¥­  ¤«ï ®¡¥á¯¥ç¥­¨ï ᨭåà®­¨§ æ¨¨ ¨ ­ ç «ì­®£®
§ ¯ã᪠ ª®¬¯ìîâ¥à .   ¯« â¥ â ª ¦¥ ¯à¥¤ãᬮâ७  ¢®§¬®¦­®áâì ¯®¤ª«î祭¨ï
CMOS ç á®¢ ­  ®á­®¢¥ ¬¨ªà®á奬ë DALLAS. Šà®¬¥ ¯¥à¨ä¥à¨¨ ¨ ¡ãä¥à®¢ ¨¬¥îâáï
¬¨ªà®áå¥¬ë ¤¥è¨äà æ¨¨, ¢å®¤ë ª®â®àëå ¯®¤ª«îç îâáï ª ¯à®æ¥áá®àã ç¥à¥§ ‹Œ.
â® ¯®§¢®«ï¥â «¥£ª® ¬¥­ïâì  ¤à¥á æ¨î ãáâனá⢠¡¥§ ª ª®£® «¨¡® ¨§¬¥­¥­¨ï
à §¢®¤ª¨ ¯¥ç â­®© ¯« âë.
‚®§¬®¦­®á⨠ àå¨â¥ªâãàë ¬ è¨­ë.
‘奬  ª®¬¯ìîâ¥p  ®á­®¢ ­  ­  ¡®«ì让 ¯¥p¥¯p®£p ¬¬¨p㥬®© «®£¨ç¥áª®©
¬¨ªp®á奬¥. ®¤ª«î祭¨¥ ¯¥à¨ä¥à¨©­ëå ãáâனá⢠ç¥à¥§ ‹Œ ¯®§¢®«ï¥â ¯®«ãç¨âì
¢ë᮪ãî £¨¡ª®áâì ¬ è¨­ë ¯® ª®­ä¨£ãà æ¨ï¬.
p®£p ¬¬¨p®¢ ­¨¥ ‹Œ ®áãé¥á⢫ï¥âáï ­¥¯®áp¥¤á⢥­­® ¢ ¬®¬¥­â
¢ª«î祭¨ï,   â ª ¦¥ ¯p¨ ¯¥p¥§ £p㧪¥, çâ® ¯®§¢®«ï¥â ª p¤¨­ «ì­® ¬¥­ïâì
á奬㠢 ‹Œ ­¥¯®áp¥¤á⢥­­® ¢® ¢p¥¬ï p ¡®âë. ⮠ᨫ쭮 ¢ë¤¥«ï¥â
 àå¨â¥ªâãàã ª®¬¯ìîâ¥à  ¨§ à鸞 áãé¥áâ¢ãîé¨å ª®¬¯ìîâ¥à®¢ ¨ ¯®í⮬㠬­®£¨¥
¯®­ïâ¨ï, ¯à¨áã騥 ®¡ëç­ë¬ ¬ è¨­ ¬, ¬¥­ïîâ ᢮© á¬ëá«. ” ªâ¨ç¥áª¨ ª®¬¯ìîâ¥à
¨¬¥¥â ¨§¬¥­ï¥¬ãî  àå¨â¥ªâãàã, ¢ ª®â®à®© ¢®§¬®¦­ë ¨§¬¥­¥­¨ï ¢® ¬­®£¨å ç áâïå
á奬ë. ’ ª, ­ ¯à¨¬¥à, ­¥«ì§ï £®¢®à¨âì ® ª®­ªà¥â­ëå  ¤à¥á å ¯®à⮢
¯®¤ª«î祭¨ï ¯¥à¨ä¥à¨¨, â ª ª ª ®­¨ ¬®£ãâ ¡ëâì ¨§¬¥­¥­ë ¢ ®¤­ã ᥪ㭤ã
¯ã⥬ ¯¥à¥¯à®£à ¬¬¨à®¢ ­¨ï ‹Œ ¨ ¤ ­­ëå ¢ އ“, ®â¢¥ç îé¨å §  ª®­ä¨£ãà æ¨î
¯®à⮢. Š®­ªà¥â­ë¥  ¤à¥á  ¯®ï¢«ïîâáï ⮫쪮 ¢ ª®­ªà¥â­ëå ª®­ä¨£ãà æ¨ïå,
­ ¯à¨¬¥à, â ª®© ª ª ª®­ä¨£ãà æ¨ï ZX-Spectrum.
¥à¥¯à®£à ¬¬¨à㥬®áâì áå¥¬ë ¤ ¥â ¤®¢®«ì­® ¡®«ìèãî ᢮¡®¤ã
ä ­â §¨¨ ¯à®£à ¬¬¨áâ  ¯® ª®­ä¨£ãà æ¨¨ ¬ è¨­ë. ‡ ¤ã¬ë¢ ï ª®­ªà¥â­ãî
à ¡®â㠯ணࠬ¬¨áâ ¬®¦¥â ®¯à¥¤¥«¨âì ¢ ª ª®© ª®­ä¨£ãà æ¨¨ ¥¥ ¬®¦­®
ᤥ« âì «ãçè¥,  , ¢®§¬®¦­®, ¨ ¯à¨¤ã¬ âì á¢®î ª®­ä¨£ãà æ¨î, ª®â®àãî
§ â¥¬ ¬®¦­® ॠ«¨§®¢ âì ¢ ‹Œ ¨ ¢ª«îç¨âì ¯¥à¥¤ § ¯ã᪮¬ í⮩ ¯à®£à ¬¬ë.
«®ç­ ï á奬  ª®¬¯ìîâ¥à  Sprinter.
ÚÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄ>Sinc
³à¨­â¥à³ ³ 2 ISA SLOTS ³ ³ ÚÄÄÄÄÄÄÄ>R
ÃÄÄÄÄÄÄÄ´ ÀÄÄÄÂÄÂÄÂÄÂÄÂÄÄÄÄÄÄÄÂÄÂÄÂÄÂÄÂÄÄÙ ³ ³ ÚÄÄÄÄÄ>G
³ Œëèì ³ ÚÄÄÄÁÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄÁÄÁÄÁÄÁÄÁÄÄ¿ ³ ³ ³ ÚÄÄÄ>B
ÀÄÂÄÂÄÂÄÙ ³ ãä¥àë ¨ ¤¥è¨äà â®àë ³ ³ ÚÄÁÄÁÄÁÄ¿
³ ³ ³ ÀÄÄÄÂÄÄÄÄÄÄÄÄÄÄÂÄÂÄÄÂÄÂÄÄÄÄÂÄÂÄÙ ³ ³ –€ ³
³ ³ ³ ³INT ³ ³ ³ ³ ³ ³ ³ ³ ¡ãä¥à ³
ÚÄÄÁÄÁÄÁÂÄ¿ ³ ³ ³ ³ ³ ÚÁÄÁÄÄÄÄÄÄÁÄ¿ÀÄÄÂÄÂÄÄÙ ÚÄÄÄÄÄÄÄÄ¿
³ ‚­ãâà ³ ÃÄÄÄijÄÄÄÄÄÄÄÄÄÄÙ ÀÄij ³ÄÄÄ´ EPF10K10 ÃÄÄÄÙ ÀÄÄÄÄÄÄ´ ¢¨¤¥® ³
³ ¯®àâë ³ ÃÄÄÄijÄÄÄÄÄÄÄÄÄÄDATAij ³ÄÄÄ´ ÃÄÄV_DATAÄÄÄÄ´ ³
ÃÄÄÄÄÄÄÄÙ ÃÄÄÄijÄÄÄ¿ ÚÄÄÄÄÄÄÄÄij ³ÄÄÄ´ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ އ“ ³
³ Ã<ÄÄÄÙ ³ ³ ³ ³ ³ ³ ³ ³
³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄ´ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ ³
³ Z84C15 ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄADRESSÄÄÄÄÄ´ ÃÄV_ADRESSÄÄÄ´ ³
³ ÃÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ ³
³ ³ ³ ³ ³ ³ ³ ³ ³ ³
³ Ã<ÄÄij ³Ä³ ³ÄÄÄÄÄÄÄÄÄÄÄÄÄÄ>´ ÃÄÄWE,CSiÄÄÄ>´ ³
³ Ã<ÄÄij ³Ä³ ³ÄÄÄÄDIRÄÄÄÄÄÄÄ>´ ³ ÀÄÄÄÄÄÄÄÄÙ
³ Ã<ÄÄij ³Ä³ ³ÄÄÄÄÄÄÄÄÄÄÄÄÄÄ>´ ÃÄÄÄÄÄÄÄ> Audio OUTs
³ ³ ³ ³ ³ ³ ³ ³ ÚÄÄÄÄÄÄÄÄ¿
³ ³ ÚÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄ¿ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ MAIN ³
³ ³ ³ ‡“ ¨ Š˜-އ“ Ã<ÄADR'Ä´ ÃÄÄDATAÄÄÄÄÄÄ´ RAM ³
ÀÄÂÄÂÄÂÄÄÂÙ ³ CS Ã<ÄCSÄÄÄ´ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ ³
³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ SIMM ³
³ D ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ ³
³ A ³ ³ ³ EPM7032 ÃÄÄÄÄÄÄÄ>´ ÃÄÄADRESSÄÄÄÄ´ ³
³ T ³ ÀÄÄ>´ Sinchro ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ ³
³ A ³ ³ HDD_DIR Ã<ÄÄÄÄÄÄÄ´ ³ ³ ³
³ ³ ³ ³ ”€— Ã<ÄÄÄÄÄÄÄ´ ÃÄRAS,CAS,WEÄ´ ³
³ ³ ³ ÀÄÂÄÂÄÂÄÂÄÂÄÄÄÄÙ ³ ³ ÀÄÄÄÄÄÄÄÄÙ
³ ³ ³ ³ ³ ³ ³ ³ ³ ³
ÚÄÁÄÁÄÁÄÄÄÄÄÄÄÄÁÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄ¿ ³ ³
³ ¥à¨ä¥à¨©­ë¥ ãáâனá⢠ Ã<ÄÄÄÄ´ ³
³ FDD,HDD,KEMPSTON Ã<DIRÄ´ ³
³ Ã<ÄÄÄÄ´ ³
ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ>´ ³
ÚÄÄÄÄÄÄÄÄ¿ ³ ÚÄÄÄÄÄÄÄÄ¿ ³ ³
³ TAPE Ã<ÄÄÙ ³KeyboardÃÄÄÄÄÄÄÄÄÄ>´ ³
³ in/out ³ ô ³ ³
ÀÄÄÄÄÄÄÄÄÙ ÀÁÁÁÁÁÁÁÁÙ ÀÄÄÄÄÄÄÄÄÄÄÄÙ
¨áã­®ª 1.
„«ï ¯à®áâ®âë ­¥ª®â®àë¥ ¡ãä¥àë ¨ ¤¥è¨äà â®àë ­  á奬¥ ­¥ 㪠§ ­ë.
Š®«¨ç¥á⢮ ஢®¤®¢ ¢ 設 å â ª ¦¥ ãá«®¢­ë. — áâì ᨣ­ «®¢ ã¯à ¢«¥­¨ï
ãáâனáâ¢ á ‹Œ á¬ã«ì⨯«¨æ¨à®¢ ­ë á  ¤à¥á ¬¨ SIMM- .
„ «ì­¥©è¥¥ ®¯¨á ­¨¥  àå¨â¥ªâãàë ï¥âáï ®¯¨á ­¨¥¬ ª®­ªà¥â­ëå
ª®­ä¨£ãà æ¨© ¨ ç á⥩ ª®­ä¨£ãà æ¨©. ® ¯¥à¥¤ í⨬ á«¥¤ã¥â ᪠§ âì ­¥áª®«ìª®
á«®¢ ® ¯¥à¥ª«î祭¨¨ á ¬¨å ª®­ä¨£ãà æ¨©.
‡ £à㧪  ª®­ä¨£ãà æ¨©.
‚ ¬®¬¥­â ¢ª«î祭¨ï ª®¬¯ìîâ¥à ,   â ª ¦¥ ¯®á«¥ ­ ¦ â¨ï ­  RESET ¢áï
¨­ä®à¬ æ¨ï, ­ å®¤¨¢è ïáï ¢ ‹Œ ®â¢¥ç îé ï §  ª®­ªà¥â­ãî ª®­ä¨£ãà æ¨î,
áâ¨à ¥âáï. ‹Œ ¯¥à¥å®¤¨â ¢ ०¨¬ ®¦¨¤ ­¨ï § £à㧪¨ ¡«®ª  ¤ ­­ëå á奬ë.
‚ íâ®â ¬®¬¥­â ¯à®æ¥áá®à ¯®«­®áâìî ®âª«î祭 ®â ª ª®© «¨¡® ¯¥à¨ä¥à¨¨.
‚ ¥£®  ¤à¥á­®¥ ¯à®áâà ­á⢮ ¯ ¬ï⨠®ª §ë¢ ¥âáï ¢ª«î祭  ®¤­  áâà ­¨æ  ‡“ ¨
®¤­  áâà ­¨æ  އ“ Š˜-¯ ¬ïâ¨. ‹î¡ ï § ¯¨áì ¢  ¤à¥á­®¥ ¯à®áâà ­á⢮ ¯ ¬ïâ¨
¯à®æ¥áá®à  ¢ íâ®â ¬®¬¥­â ¯à¨¢®¤¨â ª § ¯¨á¨ ¤ ­­ëå ¢ ‹Œ ¨ ¯à®£à ¬¬  ¢
¯®¤ª«î祭­®© áâà ­¨æ¥ ‡“ ¨¬¥¥â ⮫쪮 ®¤­ã ¥¤¨­á⢥­­ãî æ¥«ì - § £à㧨âì
¢ ‹Œ ¤ ­­ë¥ ª®­ä¨£ãà æ¨¨. ‚ í⮩ ¦¥ áâà ­¨æ¥ ‡“ ­ å®¤ïâáï ¤ ­­ë¥
­ ç «ì­®© ª®­ä¨£ãà æ¨¨. (‚ ¤ ­­ë© ¬®¬¥­â íâ® ª®­ä¨£ãà æ¨ï Sprinter-1.)
ணࠬ¬  § £à㧪¨ ª®­ä¨£ãà æ¨¨ ¯à®¢¥àï¥â ä« £ ¢ Š˜-¯ ¬ï⨠¨, ¥á«¨ ®­
ãáâ ­®¢«¥­, § £à㦠¥â ¢ ‹Œ ¤ ­­ë¥ ¨§ އ“, ¥á«¨ á¡à®è¥­, â® ¤ ­­ë¥ ¨§
‡“.   í⮬ ®á­®¢ ­® ¯¥à¥ª®­ä¨£ãà¨à®¢ ­¨¥ áå¥¬ë ª®¬¯ìîâ¥à .
„«ï ¨§¬¥­¥­¨ï áå¥¬ë ­ ¤® § £à㧨âì ¢ ¯®á«¥¤­îî áâà ­¨æã Š˜-¯ ¬ïâ¨
¡«®ª ¤ ­­ëå ª®­ä¨£ãà æ¨¨ ᮠᬥ饭¨ï #100 ¨ ¢ëáâ ¢¨âì ä« £, ª®â®àë¬ ï¢«ï¥âáï
⥪á⮢ ï áâப  "FLEX_10K_LOADING", § ¯¨á ­­ ï ¯® ᬥ饭¨î #80 ¢ í⮩ ¦¥
áâà ­¨æ¥ Š˜- . ®á«¥ í⮣® ­ ¤® ¯à®¨§¢¥á⨠¯®«­ë© á¡à®á, ª®â®àë©
®áãé¥á⢫ï¥âáï ¯à®£à ¬¬­® § ¯¨áìî ¢ ᯥ樠«ì­ãî áâà ­¨æã ¯ ¬ï⨠RESET_PAGE.
ணࠬ¬  ¢ ‡“, § ¯ã᪠¥¬ ï ¯® á¡à®áã ­ å®¤¨â ä« £ FLEX_10K_LOADING ¨
­ ç¨­ ¥â § £à㧪㠤 ­­ëå ¢ ‹Œ. ਠí⮬ ®­  ®¤­®¢à¥¬¥­­® § â¨à ¥â ä« £,
çâ® ¯à¥¤®â¢à é ¥â ¯®¢â®à­ãî § £à㧪㠭®¢®© ª®­ä¨£ãà æ¨¨ ¯à¨ ­ ¦ â¨¨ ­ 
ª­®¯ªã RESET ¨ ¯®§¢®«ï¥â ¢¥à­ãâìáï ¯®á«¥ "àãç­®£®" á¡à®á  ¢ ­ ç «ì­ãî
ª®­ä¨£ãà æ¨î. ‡ â¨à ­¨¥ ä« £  â ª ¦¥ ¨§¡ ¢«ï¥â ®â ¬ã祭¨© ¢ á«ãç ¥
¯®¤ª«î祭¨ï ­¥¯à ¢¨«ì­®© ª®­ä¨£ãà æ¨¨ ¢® ¢à¥¬ï íªá¯¥à¨¬¥­â®¢ á ¯à®£à ¬¬ ¬¨.
 ¦ â¨¥ ­  RESET ¢á¥£¤  ¢¥à­¥â á奬㠢 ­ ç «ì­ãî ª®­ä¨£ãà æ¨î.
ਬ¥ç ­¨¥:
‚­ãâ७­ïï ¨­ä®à¬ æ¨ï ¡«®ª  ¤ ­­ëå ‹Œ ï¥âáï § ªàë⮩
¨­ä®à¬ æ¨¥© ä¨à¬ë ALTERA. Šà®¬¥ á ¬¨å ¬¨ªà®á奬 ‹Œ ALTERA ¯®áâ ¢«ï¥â
¨ ¯à®£à ¬¬­®¥ ®¡¥á¯¥ç¥­¨¥ ¤«ï à §¢®¤ª¨ á奬 ¢­ãâਠ‹Œ. Š á®¦ «¥­¨î, íâ 
¯à®£à ¬¬  ­¥ ¬®¦¥â à ¡®â âì ­  ª®¬¯ìîâ¥à¥ ⨯  ZX-Spectrum ¨ ¢ ¡«¨¦ ©è¥¬
®¡®§à¨¬®¬ ¡ã¤ã饬 ­¥ ¯à¥¤¢¨¤¨âáï ¥¥ ¢¥àá¨ï ¤«ï Sprinter- . ®í⮬ã
à §à ¡®âª  ­®¢ëå ª®­ä¨£ãà æ¨© ¬®¦¥â ¯à®¨§¢®¤¨âáï ⮫쪮 ¯à¨ ­ «¨ç¨¨
¤®áâ â®ç­® ¬®é­®© ¬ è¨­ë (¢á¥ ¤¥« «®áì ­  Pentium-166) ¨ ¯à®£à ¬¬ë à §¢®¤ª¨
á奬 ¢ ‹Œ, 業  ­  ª®â®àãî á®áâ ¢«ï¥â á®â­¨ ¤®«« à®¢ ‘˜€.
‚ á¢ï§¨ á í⨬, ¢ ¤ ­­ë© ¬®¬¥­â Sprinter ¨¬¥¥â ­¥áª®«ìª® ª®­ªà¥â­ëå
ª®­ä¨£ãà æ¨©, ¤¢¥ ¨§ ª®â®àëå § ¯¨á ­ë ¢ ‡“,   ®áâ «ì­ë¥ ¬®£ãâ ¡ëâì
¯®¤£à㦥­ë á ¤¨áª¥âë ¨«¨ ¢¨­ç¥áâ¥à . ®áâ®ï­­® ¢¥¤¥âáï ᮢ¥à襭á⢮¢ ­¨¥
ª®­ªà¥â­ëå ª®­ä¨£ãà æ¨© ¨ à §à ¡®âª  ­®¢ëå.
Š®­ä¨£ãà æ¨ï Sprinter-1.
‚ª«î砥⠢ á¥¡ï ª®­ä¨£ãà æ¨î Spectrum-128/256, à á¯à¥¤¥«¥­¨¥ ¯ ¬ïâ¨
¤® 4Mb, à áè¨à¥­­ë© íªà ­ á ०¨¬ ¬¨ Spectrum, Text-80x32, Graf-320x256x256,
ª®­â஫«¥à ¤¨áª®¢®¤ , ª®­â஫«¥à IDE ¢¨­ç¥áâ¥à , ª®­â஫«¥à ª« ¢¨ âãàë AT,
¯®¤ª«î祭­®© ª ª ZX-Keyboard, 8-bit COVOX.
â  ª®­ä¨£ãà æ¨ï ¬ ªá¨¬ «ì­® ¯à¨¡«¨¦¥­  ª ª®­ä®£ãà æ¨¨ ZX-Spectrum
¨ ¯®§¢®«ï¥â à ¡®â âì ­  ®¡ëç­ëå ᯥªâà㬮¢áª¨å ¯à®£à ¬¬ å ¨ ¯®á⥯¥­­®
¬¥­ïâì ¨å ¯®¤ à áè¨à¥­­ë¥ ०¨¬ë íªà ­  ¨ ¯ ¬ïâ¨,   â ª ¦¥ ¤«ï à ¡®âë á
­®¢ë¬¨ ãáâனá⢠¬¨.
Š®­ä¨£ãà æ¨ï Sprinter-2.
‚ª«î砥⠢ á¥¡ï ª®­ä¨£ãà æ¨î Spectrum-128/256, à á¯à¥¤¥«¥­¨¥ ¯ ¬ïâ¨
¤® 4Mb, à áè¨à¥­­ë© íªà ­ á ०¨¬ ¬¨ Spectrum, Text-80x32, Graf-320x256x256,
ª®­â஫«¥à ¤¨áª®¢®¤ , ª®­â஫«¥à IDE ¢¨­ç¥áâ¥à , ª®­â஫«¥à ª« ¢¨ âãàë AT,
¯®¤ª«î祭­®© ª ª ZX-Keyboard, Accelerator.
Š®­ä¨£ãà æ¨ï, ª ª ¨ Sprinter-1 ¯à¨¡«¨¦¥­  ª ᯥªâà㬮¢áª®©, ­®
¨¬¥¥â ¡®«¥¥ ¦¥á⪨¥ âॡ®¢ ­¨ï ª ¯à®£à ¬¬ ¬ ¯® ᮢ¬¥á⨬®áâ¨. ®§¢®«ï¥â
¨á¯®«ì§®¢ âì  ªá¥«¥à â®à ®¯¥à æ¨© á ®á­®¢­ë¬ ¨ ¢¨¤¥®-އ“. €ªá¥«¥à â®à
ã᪮àï¥â ®¯¥à æ¨¨ ¯¥à¥á뫪¨ ¡«®ª®¢ ¤ ­­ëå ¨ § ¯®«­¥­¨ï އ“ ®¤­¨¬ ¡ ©â®¬
¤® 䨧¨ç¥áª®£® ¯à¥¤¥«  ᪮à®á⨠®á­®¢­®£® އ“.
‚ ¯®á«¥¤­¥© ¢¥àᨨ ª®­ä¨£ãà æ¨ï Sprinter-2 ­¥ ¨¬¥¥â Spectrum-®¢áª®©
ª« ¢¨ âãàë. ‚¬¥áâ® ­¥¥ ¨§ ¯®àâ  0FEh áç¨â뢠¥âáï ᪠­ª®¤ ¯à¨è¥¤è¨© á
AT-ª« ¢¨ âãàë.
Š®­ä¨£ãà æ¨ï ZX-Spectrum-256/AY.
â  ª®­ä¨£ãà æ¨ï ¬ ªá¨¬ «ì­® ¯à¨¡«¨¦¥­  ª ZX-Spectrum-128/256
¨ ¢ª«î砥⠢ ᥡï á奬㠬ã§ëª «ì­®£® á®¯à®æ¥áá®à  AY-3-8910. ‚ í⮩
ª®­ä¨£ãà æ¨¨ ®âáãâáâ¢ãîâ à áè¨à¥­­ë¥ ०¨¬ë íªà ­ .
‚â®à ï ¢¥àá¨ï á奬ë AY ¢ª«î砥⠢ ᥡï âਠ£¥­¥à â®à  £®«®á®¢,
£¥­¥à â®à è㬠 ¨ ॣã«ïâ®àë  ¬¯«¨âã¤ë. ƒ¥­¥à â®à ®£¨¡ î饩 ®âáãâáâ¢ã¥â.
’ ª ¦¥ ®âáãâáâ¢ã¥â ¢®§¬®¦­®áâì ç⥭¨ï ¨§ ¯®à⮢ ¤ ­­ëå á®¯à®æ¥áá®à .
‚ âà¥â쥩 ¢¥àᨨ AY ¯à¥¤¯®« £ ¥âáï ¤ ­­ë¥ ­¥¤®áâ âª¨ ¨áª«îç¨âì.
Š®­ä¨£ãà æ¨ï Sprinter-3.
Š®­ä¨£ãà æ¨ï ®â¢ï§ ­  ®â ª®­ä¨£ãà æ¨¨ ZX-Spectrum. ®«­®áâìî
®âª«îç ¥âáï ‡“ ¨ ¢á¥  ¤à¥á­®¥ ¯à®áâà ­á⢮ à §¡¨â® ­  ç¥âëॠ®ª­  ¯® 16k,
¢ ª ¦¤®¥ ¨§ ª®â®àëå ¯®¤ª«îç ¥âáï «î¡ ï ¨§ 256-⨠áâà ­¨æ އ“. Žâáãâáâ¢ã¥â
ᯥªâà㬮¢áª¨© íªà ­, £à ä¨ç¥áª¨© íªà ­ â ª®© ¦¥, ª ª ¢ ª®­ä¨£ãà æ¨ïå
Sprinter-1 ¨ Sprinter-2. ˆ¬¥¥â ¤®¯®«­¨â¥«ì­ë¥ ä㭪樨  ªá¥«¥à â®à .
®§¢®«ï¥â ¯à®¨§¢®¤¨âì ®¯¥à æ¨¨ AND, OR ¨ XOR á ¡«®ª ¬¨ ¤ ­­ëå. ˆ¬¥¥â 8-bit
COVOX.
‚ ¤ «ì­¥©è¥¬ ¯à¥¤¯®« £ ¥âáï ¯®¤ª«î祭¨¥ ¢ í⮩ ª®­ä¨£ãà æ¨¨
á¯à¨­â¥à®¢áª®© §¢ãª®¢®© ª àâë.
Š®­ä¨£ãà æ¨ï Game-1.
®å®¦  ­  ª®­ä¨£ãà æ¨î Sprinter-3. €ªá¥«¥à â®à ­¥ ¨¬¥¥â «®£¨ç¥áª¨å
ä㭪権,   ¤«ï ¢ë¢®¤  §¢ãª  ¨¬¥¥â COVOX-Blaster - COVOX á ¡ãä¥à­ë¬ އ“,
¯®§¢®«ïî騬 ¢ë¢®¤¨âì §¢ãª ¯®¡«®ç­® ¨ ®á¢®¡®¦¤ âì ¯à®æ¥áá®à­®¥ ¢à¥¬ï ¤«ï
¤à㣮© à ¡®âë. Š®­ä¨£ãà æ¨ï ®à¨¥­â¨à®¢ ­  ­  ¨á¯®«ì§®¢ ­¨¥ ¢ ¨£à å ¤«ï
Sprinter- .
‘奬  à á¯à¥¤¥«¥­¨ï ¯ ¬ïâ¨.
‘奬ë à á¯à¥¤¥«¥­¨ï ¯ ¬ï⨠¢ ª®­ä¨£ãà æ¨ïå Sprinter-1 ¨ Sprinter-2
®¤¨­ ª®¢ë ¨ ¤®áâ â®ç­® ¯à®§à ç­ë. ” ªâ¨ç¥áª¨ ®­  ¯à¥¤áâ ¢«ï¥â ᮡ®© á奬ã
à á¯à¥¤¥«¥­¨ï ¯ ¬ï⨠ª®¬¯ìîâ¥à  Scorpion, á ­ «®¦¥­­®© ­  ­¥¥
¤®¯®«­¨â¥«ì­®© á奬®©, ª®â®à ï ¯®§¢®«ï¥â ¯à®¨§¢®«ì­® ãáâ ­ ¢«¨¢ âì ¢á¥
áâà ­¨æë ¯ ¬ïâ¨, ª ª ‡“, â ª ¨ އ“.
Š ¦¤ ï áâà ­¨æ  ‡“ ¨«¨ އ“ ¨¬¥¥â ᢮© ¯®àâ, ¢ ª®â®à®¬ 㪠§ë¢ ¥âáï
¤¥©á⢨⥫ì­ë© ­®¬¥à áâà ­¨æë ¨§ 256-⨠áâà ­¨æ ¢á¥å 4Mb. ‘âà ­¨æë,
¯à®¥æ¨àã¥¬ë¥ ¢ à §«¨ç­ë¥ ®ª­   ¤à¥á­®£® ¯à®áâà ­á⢠ ¯à®æ¥áá®à  ¨¬¥îâ ᢮¨
ᮡá⢥­­ë¥ ¯®àâë. ’.¥. ‘âà ­¨æ , ¢ª«îç ¥¬ ï ¢  ¤à¥á  #4000..#7FFF, ¨
áâà ­¨æ  ­®¬¥à 5 ®¡ëç­®£® Spectrum-®¢áª®£® à á¯à¥¤¥«¥­¨ï ¯ ¬ïâ¨, ¢ª«îç ¥¬ ï
¢  ¤à¥á  #C000..#FFFF ¨¬¥îâ à §¤¥«ì­ë¥ ¯®àâë.
‚ᥣ® â ª¨å ¯®à⮢ áâà ­¨æ ¯ ¬ï⨠- 32.
16 ¯®à⮢ ®â¢¥ç îâ §  ­®¬¥à  áâà ­¨æ އ“, ¯®¤ª«îç ¥¬ë¥ ª  ¤à¥á ¬
#C000..#FFFF. …é¥ âਠ¯®àâ  ®â¢¥ç îâ §  ¯®¤ª«î祭¨¥ áâà ­¨æ އ“ ª  ¤à¥á ¬
#0000..#3FFF, #4000..#7FFF ¨ #8000..#BFFF. ‚®á¥¬ì ¯®à⮢ ¨á¯®«ì§ãîâáï ¤«ï
¯®¤ª«î祭¨ï à §«¨ç­ëå áâà ­¨æ ‡“. ޤ¨­ ¯®àâ - ¤«ï ¯®¤ª«î祭¨ï áâà ­¨æë
Š˜-  ¢¬¥áâ® ‡“. ˆ ®¤¨­ ¯®àâ - íâ® ¯®àâ á¨á⥬­®£® ‡“, ¯®¤ª«îç ¥¬®£® ­ 
¬¥áâ® ‡“ áà §ã ¯®á«¥ á¡à®á  ¬ è¨­ë ¯® ª« ¢¨è ¬ Ctrl+Alt+Del.
Žá⠢訥áï 3 ¯®àâ  áâà ­¨æ ¯ ¬ï⨠®áâ îâáï ­  ¤ ­­ë© ¬®¬¥­â ¢ १¥à¢¥.
‘奬  à á¯à¥¤¥«¥­¨ï ¯ ¬ï⨠¯®§¢®«ï¥â ¯®¤ª«îç¨âì ¢  ¤à¥á­®¥ ¯à®áâà ­á⢮
¯à®æ¥áá®à  ­¥ ⮫쪮 އ“ ¨«¨ ‡“, ­® ¨ ¯®àâë ¨ ¯ ¬ïâì ISA ª àâ, ¢áâ ¢«ï¥¬ëå
¢ á«®â.
ਠ¯®¤ª«î祭¨¨ ¢  ¤à¥á  #C000..#FFFF ᪮௨®­®¢áª¨å à áè¨à¥­­ëå
áâà ­¨æ އ“, ­  ¨å ¬¥áâ® ¬®¦­® ¯¥à¥ ¤à¥á®¢ âì á«®âë. „«ï í⮣® ­ ¤® ¯à®áâ®
§ ¯¨á âì ¢ ¯®àâ ®¤­®© ¨§ íâ¨å áâà ­¨æ §­ ç¥­¨¥, ᮮ⢥âáâ¢ãî饥 ISA-á«®âã,
ª ª®â®à®¬ã ­¥®¡å®¤¨¬® ¯à®¨§¢¥á⨠®¡à é¥­¨¥. â® §­ ç¥­¨¥ â ª ¦¥ 㪠§ë¢ ¥â
ª 祬㠢¥¤¥âáï ®¡à é¥­¨¥, ª ¯®àâ ¬ ¨«¨ ¯ ¬ïâ¨.
‘奬  à á¯à¥¤¥«¥­¨ï ¯ ¬ï⨠Sprinter- .
€¤à¥á­®¥ ¯à®áâà ­á⢮ ‘âà ­¨ç­ë¥ ¯®àâë ‡“ ª®¬¯ìîâ¥à 
¯à®æ¥áá®à  ¯®¤ª«îç ¥¬ëå áâà ­¨æ áâà ­¨æë ¯® 16k
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
³ #0000..#3FFF ÃÄ>´ ¯®àâë ÃÄÄ>´ ROM_BASIC ÃÄÄ>ÂÄÄÂÄÄÂÄ>´ EXPANSION ³
³ ³ ³ #7FFD,³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ ³ ³ #1FFD ÃÄ¿ ³ ROM_TR-DOS ÃÄÄ>´ ³ ÃÄ>´ TR-DOS ³
ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ÀÄÄÄÄÄÄÄÙ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ #4000..#7FFF ÃÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ³ ÃÄ>´ BASIC128 ³
³ ³ ³ ³ ÃÄ Ä Ä Ä Ä Ä Ä ´ ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ ³ ³ ³ ³ ROM_SYSTEM ÃÄÄ>Ù ³ ÃÄ>´ BASIC48 ³
ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ #8000..#BFFF ÃÄÄÄÄ¿ ³ À>´ RAM_0000 ÃÄÄÄ>¿ ³ ÃÄ>´ SYSTEM ROM ³
³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ ³ ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ÃÄ>´ SYSTEM ROM2 ³
ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ÀÄÄ>´ RAM_4000 ÃÄÄÄ>´ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ #C000..#FFFF ÿ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ÃÄ>´ CONFIG 2 ³
³ ³³ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ ³³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ÀÄ>´ CONFIG 1 ³
ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ³ ÀÄÄÄÄÄÄÄÄ>´ RAM_8000 ÃÄÄÄ>´ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³
³ ³ ³ އ“ ª®¬¯ìîâ¥à 
³ ÚÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ áâà ­¨æë ¯® 16k
³ ³ ®àâë ÃÄÄ>´ RAM_0 ÃÄÄÄ>´ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
³ ³ #7FFD,³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ÃÄÄÄ>ÂÄ>´ RAM_00 ³
À>´ #1FFD ÃÄÄ>´ ÃÄÄÄ>´ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ ³ ÃÄ Ä Ä Ä Ä Ä Ä ³ ³ ³ ÃÄ>´ RAM_01 ³
³ ÃÄÄ>´ RAM_7 ÃÄÄÄ>´ ÀÄ>´ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³ ³
³ ÃÄÄ>´ RAM_8 ÃÄ>ÂijÄÄÄ>´ ÃÄ Ä Ä Ä Ä Ä Ä ´
³ ³ ÃÄ Ä Ä Ä Ä Ä Ä ´ ³ ³ ÀÄÄ´ RAM_7F ³
³ ÃÄÄ>´ ÃÄ>´ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ÀÄÄÄ>ÂÄ>´ RAM_80 ³
³ ÃÄÄ>´ RAM_F ÃÄ>ÁÄ>¿ ³ ÃÄ Ä Ä Ä Ä Ä Ä ´
ÀÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ³
‚­¥è­¨¥ ãáâனá⢠ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ÃÄ>´ RAM_FE ³
®àâë ÃÄÄÄÄ>´ ISA_1 Ã<ÄÄÄÄ´ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´
ª®¬¯ìîâ¥à  ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ÀÄ>´ RAM_FF ³
ÃÄÄÄÄ>´ ISA_2 Ã<ÄÄÄÄ´ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³
ÃÄÄÄÄ>´ HDD Ã<ÄÄÄÄ´
³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³
ÃÄÄÄÄ>´ OVER DEVICES Ã<ÄÄÄÄÙ
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
¨áã­®ª 2.
‚ ¤àã£¨å ª®­ä¨£ãà æ¨ïå á奬  à á¯à¥¤¥«¥­¨ï ¯ ¬ï⨠ã¯à®é ¥âáï ¤«ï
®á¢®¡®¦¤¥­¨ï à¥áãàᮢ ‹Œ. Œ®£ãâ ®âáãâá⢮¢ âì ¯®àâë #1FFD ¨ #7FFD,   â ª
¦¥ ã¯à®é ¥âáï á奬  à ¡®âë á ãáâனá⢠¬¨, ®â®¡à ¦ ¥¬ë¬¨ ­  ¯ ¬ïâì.
‘奬  à á¯à¥¤¥«¥­¨ï ¯®à⮢.
Sprinter ¨¬¥¥â ¤¢¥ ®¡®á®¡«¥­­ë¥ £àã¯¯ë ¯®à⮢. ¥à¢ ï £à㯯 , íâ®
¢­ãâ७­¨¥ ¯®àâë ¯à®æ¥áá®à  Z84C15, ¢â®à ï - ¢­¥è­¨¥ ¯®àâë. €¤à¥á æ¨ï
¯®à⮢ ¯¥à¢®© £àã¯¯ë ­¥ ¬®¦¥â ¡ëâì ¨§¬¥­¥­ , â ª ª ª í⨠¯®àâë ­  ®¤­®¬
ªà¨áâ ««¥ á ¯à®æ¥áá®à®¬. ‚â®à ï £à㯯  ¯®£ª«îç ¥âáï ç¥à¥§ ‹Œ ¨ ¨å  ¤à¥á 
¬®£ãâ ¨§¬¥­ïâìáï ª ª 㣮¤­®, á ¥¤¨­á⢥­­ë¬ ãá«®¢¨¥¬, ­¥¯¥à¥á¥ç¥­¨ï á
 ¤à¥á ¬¨ ¯¥à¢®© £à㯯ë.
Ž â®¬ ª ª¨¥ ¯®àâë ¨¬¥îâáï ­  ªà¨áâ ««¥ Z84C15 ¬®¦­® ¯à®ç¨â âì ¢
¤®ªã¬¥­â æ¨¨ ¯® íâ®¬ã ¯à®æ¥áá®àã ¨ §¤¥áì ï 㯮¬ï­ã ­¥ª®â®àë¥ ¨§ ­¨å. ޤ¨­
¨§ ¯®á«¥¤®¢ â¥«ì­ëå ¯®à⮢ ¨á¯®«ì§ã¥âáï ¤«ï ¢¢®¤  ¤ ­­ëå á  ªâ¨¢­®© ¬ëè¨.
ޤ¨­ ¨§ ¯ à ««¥«ì­ëå ¨á¯®«ì§ã¥âáï ¤«ï ¢ë¢®¤  ¤ ­­ëå, ­  ¢â®à®© ¯ à ««¥«ì­ë©
¯®àâ § ¢¥¤¥­ë ᨣ­ «ë ¯à¥à뢠­¨© ¨ § ¯à®á®¢ ¯àאַ£® ¤®áâ㯠 ᮠ᫮⮢ ISA.
 à ««¥«ì­ë© ¯®àâ ¯à®æ¥áá®à  Z84C15 ãáâ஥­ â ª¨¬ ®¡à §®¬, çâ® ­  ­¥¬
¢®§¬®¦­  ®à£ ­¨§ æ¨ï ¯à¥à뢠­¨© ¯® ᨣ­ « ¬ ¯à¨å®¤ï騬 ç¥à¥§ ¯ à ««¥«ì­ë©
¯®àâ. ” ªâ¨ç¥áª¨ ¢â®à®© ¯ à ««¥«ì­ë© ¯®à⠨ᯮ«ì§ã¥âáï ª ª ª®­â஫«¥à
¯à¥à뢠­¨©.
‘奬  à á¯à¥¤¥«¥­¨ï ¯®à⮢ ¢â®à®© £àã¯¯ë ¨¬¥¥â á¢®î ®á®¡¥­­®áâì.
ƒ« ¢­®© ¨¤¥¥© ¡ë«® ¯®«ã祭¨¥ ¢®§¬®¦­®á⨠¡ëáà® ¨§¬¥­ïâì ª®­ä¨£ãà æ¨î ¯®à⮢
¡¥§ ¯¥à¥£à㧪¨ ‹Œ. â® ¤®á⨣­ãâ® ¯ã⥬ ¯à¨¬¥­¥­¨ï ª àâë à á¯à¥¤¥«¥­¨ï
¯®à⮢, à á¯®« £ î饩áï ­  ᯥ樠«ì­®© áâà ­¨æ¥ އ“.
ਠ¯®ï¢«¥­¨¨ 横«  ®¡à é¥­¨ï ª ¯®àâã á­ ç «  ¯à®¨á室¨â ®¡à é¥­¨¥
ª އ“ ª àâë ¯®à⮢. ‚ ª à⥠¯®à⮢ § ¯¨á ­® ª ª®© ¨¬¥­­® ¯®àâ ¯®¤ª«î祭 ª
¤ ­­®¬ã  ¤à¥áã. „ «¥¥ ¯à®¨á室¨â ¢­ãâ७­ïï ¤¥è¨äà æ¨ï ¯® ¡ ©âã ¨§ ª àâë
¯®à⮢ ¨ ®¡à é¥­¨¥ ª ¢ë¡à ­­®¬ã ¯®àâã. ‚ ०¨¬¥ ­¥âãà¡® íâ® ¯à®¨á室¨â ¡¥§
ª ª¨å «¨¡® § ¤¥à¦¥ª,   ¢ ०¨¬¥ âãà¡® ¯à®æ¥áá®àã ¢ëáâ ¢«ï¥âáï ᨣ­ « WAIT ¢
§ ¢¨á¨¬®á⨠®â ­¥®¡å®¤¨¬®© ¤«¨­ë 横«  ®¡à é¥­¨ï ª ¯®àâã.
„«ï ¯®¤ª«î祭¨ï ª ª ª®¬ã «¨¡®  ¤à¥áã ¨«¨ ®âª«î祭¨ï ®â ­¥£® ª ª®£®
«¨¡® ¯®àâ  ¤®áâ â®ç­® ®âªàëâì ª àâã ¯®à⮢ ¨ ¢¯¨á âì ¢ ­ã¦­®¥ ¬¥áâ® ®¤¨­
¡ ©â.
‚ áâà ­¨æ¥ ª àâë ¯®à⮢ ᮤ¥à¦¨âáï ç¥âëॠª àâë, ª®â®àë¥ ¬®£ãâ
¯¥à¥ª«îç âìáï ç¥à¥§ á¨á⥬­ë© ¯®àâ. ’ ª¨¬ ®¡à §®¬ ¬®¦­® ®áãé¥á⢨âì ¡ëáâ஥
¯¥à¥ª«î祭¨¥ ª®­ä¨£ãà æ¨¨ ¯®à⮢, çâ® ¬®¦¥â ¡ëâì ¯®«¥§­® ¯à¨ à ¡®â¥
Spectrum-®¢áª¨å ¯à®£à ¬¬ ᮢ¬¥áâ­® á® Sprinter-®¢áª¨¬ ¡¨®á®¬.
Š®­ªà¥â­ë¥  ¤à¥á  ¯®à⮢, ¨á¯®«ì§ã¥¬ë¥ ¢ Sprinter-¥.
‡¤¥áì ï ¯à¨¢¥¤ã  ¤à¥á æ¨î ¯®à⮢ ¤«ï ª®­ä¨£ãà æ¨© Sprinter-1 ¨
Sprinter-2. ‘ࠧ㠮⬥çã, çâ® í⨠ ¤à¥á  «¥£ª® ¬®£ãâ ¡ëâì ¨§¬¥­¥­ë ¯à®á⮩
¯à®£à ¬¬®©, ¢ á«ãç ¥ ¯®ï¢«¥­¨ï â ª®© ­¥®¡å®¤¨¬®áâ¨.
‘â ­¤ àâ­ë¥ ¯®àâë.
#FE - RD_KBD - ¯®àâ ª« ¢¨ âãàë
#FE - WR_BRD - ¯®àâ ¡®à¤îà 
#7FFD - ¯®àâ à áè¨à¥­¨ï ZX-Spectrum 128k
#1FFD - ¯®àâ à áè¨à¥­¨ï Scorpion ZS-256
#1F,#0F - RD_KEMPS - ¯®àâ ¤¦®©á⨪ . ‚ ª®­ä¨£ãà æ¨¨ Sprinter-1 ¯®àâ
#1F  ¯¯ à â­® ¯¥à¥ ¤à¥áã¥âáï ­  ¯®àâ #0F
#BFFD,#FFFD - AY-PORTS - ¯®àâë AY-á®¯à®æ¥áá®à  (ZX-Spectrum-256/AY)
¥ ᮢᥬ áâ ­¤ àâ­ë¥ ¯®àâë.
#FB,#4F - ¯®àâ COVOX- .
„®¯®«­¨â¥«ì­ë¥ 8-¡¨â­ë¥ ¯®àâë Sprinter- .
#82 - PAGE0 - áâà ­¨æ  އ“, ¯®¤ª«îç ¥¬ ï ¢¬¥áâ® ‡“ ç¥à¥§ ¯®àâ #1FFD
#A2 - PAGE1 - áâà ­¨æ  އ“, ¯®¤ª«î祭­ ï ¯®  ¤à¥áã #4000
#C2 - PAGE2 - áâà ­¨æ  އ“, ¯®¤ª«î祭­ ï ¯®  ¤à¥áã #8000
#E2 - PAGE2 - áâà ­¨æ  އ“, ¯®¤ª«î祭­ ï ¯®  ¤à¥áã #C000
‡¤¥áì ­ ¤® ®â¬¥â¨âì ®á®¡®, ç¥à¥§ ¯®àâ #E2 ¬®¦­® ¨§¬¥­¨âì «î¡ãî ¨§
16-⨠áâà ­¨æ ᪮௨®­®¢áª®£® à á¯à¥¤¥«¥­¨ï ¯ ¬ïâ¨.
#89 - PORT_Y - ¢¥à⨪ «ì­ ï ª®®à¤¨­ â  â®çª¨ ­  £à ä¨ç¥áª®¬ íªà ­¥
¨«¨ áâà ­¨æ  VIDEO-RAM ¤«ï ᯥªâà㬮¢áª®£® ०¨¬ 
#C9 - RGMOD - ¯®àâ ०¨¬  íªà ­ . ¥à¥ª«îç ¥â áâà ­¨æë ०¨¬  íªà ­ .
#3C,#7C - SYS_PORT - á¨á⥬­ë© ¯®àâ âண âì ­¥ ४®¬¥­¤ã¥âáï
#10..#1F,#EE,#EF,#F0,#F1,#F4 - ¢­ãâ७­¨¥ ¯®àâë Z84C15
®àâë áâà ­¨æ އ“ ®âªàëâë ª ª ­  § ¯¨áì, â ª ¨ ­  ç⥭¨¥. â®
¯®§¢®«ï¥â «¥£ª® ¢ë¯®«­ïâì ¯à®£à ¬¬ë, ¨á¯®«ì§ãî騥 ¯¥à¥ª«î祭¨¥ áâà ­¨æ,  
§ â¥¬ ¢®§¢à é âì í⨠áâà ­¨æë ­ § ¤. ਠࠡ®â¥ BIOS-  ¢á¥ áâà ­¨æë
á®åà ­ïîâáï.
„®¯®«­¨â¥«ì­ë¥ 16-⨡¨â­ë¥ ¯®àâë Sprinter- .
#xx50..#xx55 - ¯®àâë HDD - ¨á¯®«ì§®¢ âì ¢­¥è­¨¬¨ ¯à®£à ¬¬ ¬¨ ­¥
४®¬¥­¤ã¥âáï. ”㭪樨 à ¡®âë á HDD § ¯¨á ­ë ¢ ‡“.
‘ªàëâë¥ ¯®àâë Sprinter- .
‘ªàëâ묨 ïîâáï ¯®àâë ª®â®àë¥ ­¤®áâã¯­ë ¢ ª®­ªà¥â­ë© ¬®¬¥­â
¢à¥¬¥­¨, ­® ¬®£ãâ áâ âì ¤®áâ㯭묨 ¯®á«¥ ¯à®¢¥¤¥­¨ï ¨§¬¥­¥­¨© ¢ ª à⥠¯®à⮢.
ˆå  ¤à¥á  ­¥ 㪠§ë¢ îâáï, â ª ª ª ®­¨ ¬®£ãâ ¡ëâì ¢ëáâ ¢«¥­ë ¢ «î¡®¥ ¬¥áâ®.
®àâ ‡“ BASIC48
®àâ ‡“ BASIC128
®àâ ‡“ TR-DOS
®àâ ‡“ EXPANSION
®àâ ‡“ SYSTEM
—¥à¥§ í⨠¯®àâë ¬®¦­® ãáâ ­®¢¨âì ­®¢ë¥ ¯à®è¨¢ª¨ ‡“. „«ï í⮣® ¨å
¤®áâ â®ç­® § ¯¨á âì ¢ އ“ á ­®¬¥à ¬¨ áâà ­¨æ ¬¥­ìè¥ #80 ¨ § ¯¨á âì ¢
ᮮ⢥âáâ¢ãî騩 ¯®àâ ­®¬¥à í⮩ áâà ­¨æë. ਠ⠪®¬ ¯®¤ª«î祭¨¨ áâà ­¨æë
⨠áâà ­¨æë ¡ã¤ãâ § é¨é¥­ë ®â § ¯¨á¨.
— áâ¨ç­® áªàëâ묨, â ª ¦¥ ïîâáï ¨ ¯®àâë #7FFD,#1FFD ¢ ®¡ëç­®¬
á®áâ®ï­¨¨ ®­¨ ¤®áâ㯭ë ⮫쪮 ­  § ¯¨áì, ­® §­ ç¥­¨ï, § ¯¨á뢠¥¬ë¥ ¢ íâ¨
¯®àâë ¬®¦­® ¯à®ç¨â âì, ®âªàë¢ á®®â¢¥âáâ¢ãî騥 ¯®àâë ­  ç⥭¨¥.
‚ ¤àã£¨å ª®­ä¨£ãà æ¨ïå ¬®¦¥â ®âáãâá⢮¢ âì ç áâì ¯®à⮢ ¨«¨
¯à¨áãâá⢮¢ âì ­®¢ë¥ ¯®àâë.
Žà£ ­¨§ æ¨ï ¢¨¤¥®¯ ¬ï⨠¨ ¢¨¤¥®à¥¦¨¬®¢.
‚¨¤¥®-އ“ Sprinter-  á®áâ ¢«ï¥â 256 ª¨«®¡ ©â. ‚ ¤ «ì­¥©è¥¬
¯à¥¤¯®« £ ¥âáï ¥£® à áè¨à¥­¨¥ ¤® 512 ª¨«®¡ ©â, ¤«ï ¯®«ã祭¨ï ¡®«¥¥
¢ë᮪¨å ०¨¬®¢ à §à¥è¥­¨ï.
‚ ०¨¬¥ Spectrum-®¢áª®£® íªà ­  ¢áï ®à£ ­¨§ æ¨ï â ª ï ¦¥ ª ª ¢
áâ ­¤ àâ­®¬ ZX-Spectrum. ‚ ®áâ «ì­ëå ०¨¬ å ¢ª«îç ¥âáï Sprinter-®¢áª¨©
íªà ­, áâàãªâãà  ª®â®à®£® ¢ª«î砥⠢ á¥¡ï ‘¯¥ªâà㬮¢áª¨© íªà ­ ª ª ç áâì
á奬ë.
“áâனá⢮ íªà ­ .
‚¥áì íªà ­ à §¡¨â ­  ª¢ ¤à âë, à §¬¥à®¬ ¢ áâ ­¤ àâ­®¥
‘¯¥ªâà㬮¢áª®¥ §­ ª®¬¥áâ®. „«ï ª ¦¤®£® ª¢ ¤à â  ãáâ ­ ¢«¨¢ ¥âáï ᢮©
ᮡá⢥­­ë© ०¨¬ ¢ë¢®¤ ,   â ª ¦¥  ¤à¥á ¢¨¤¥®-އ“, ®âªã¤  ¯à®¨§¢®¤¨âáï
¢ë¢®¤ ¢ íâ®â ª¢ ¤à â.
‚ ª ¦¤®¬ §­ ª®¬¥á⥠¬®¦¥â ¡ëâì § ¤ ­ ᢮© ᮡá⢥­­ë© ०¨¬ ¢ë¢®¤ .
‚ ¤ ­­ë© ¬®¬¥­â ¬®¦­® ãáâ ­ ¢«¨¢ âì â ª¨¥ ०¨¬ë:
ZX-40 - ®¡ëç­ë© ᯥªâà㬮¢áª¨© ०¨¬ á ®¤­¨¬ ¡¨â¯« ­®¬ ¨
®¤­¨¬  âਡã⮬ ­  §­ ª®¬¥áâ®.
ZX-80 - ¥¦¨¬, ¯®å®¦¨© ­  ᯥªâà㬮¢áª¨© ¯® áâ஥­¨î
ᨬ¢®«®¢, ­® ¢ ª ¦¤®¬ §­ ª®¬¥á⥠®ª §ë¢ ¥âáï ¤¢  ᨬ¢®« , ᦠâë¥ ¯®
£®à¨§®­â «¨.
G256-8 - ƒà ä¨ç¥áª¨© ०¨¬. Š¢ ¤à â ¯à¥¤áâ ¢«ï¥â ᮡ®© ¬ áᨢ 8x8
â®ç¥ª. ‚ ª ¦¤®© â®çª¥ § ¤ ¥âáï ®¤¨­ ¨§ 256-⨠梥⮢, ¢ë¡¨à ¥¬ëå ¨§ ¯ «¨âàë
16 ¬¨««¨®­®¢ 梥⮢. Š¢ ¤à âë ¬®£ãâ ¨¬¥âì à §­ë¥ ¯ «¨âàë. ’ ª¨å ¯ «¨âà ¤«ï
०¨¬  G256-8 - ç¥âëà¥.
G16-16 - ƒà ä¨ç¥áª¨© ०¨¬. Š¢ ¤à â ¯à¥¤áâ ¢«ï¥â ᮡ®© ¬ áᨢ
16x8 â®ç¥ª. Š ¦¤ ï â®çª  ¨¬¥¥â ®¤¨­ ¨§ 16 梥⮢, ¢ë¡¨à ¥¬ëå ¨§ ¯ «¨âàë 16
¬¨««¨®­®¢ 梥⮢. ’ ª ¦¥, ª ª ¨ G256-8 ¢ ª¢ ¤à â¥ ¬®¦¥â ¡ëâì ãáâ ­®¢«¥­ 
®¤­  ¨§ 4-å ¯ «¨âà.  «¨âàë £à ä¨ç¥áª¨å ०¨¬®¢ ¯¥à¥á¥ª îâáï ¤àã£ á ¤à㣮¬.
 «¨âà  16-â¨æ¢¥â­®£® ०¨¬  íâ® ¯¥à¢ë¥ 16 梥⮢ ¨§ ¯ «¨âàë 256-â¨æ¢¥â­®£®.
BORDER - ‚ ª¢ ¤à â ¢ë¢®¤¨âáï æ¢¥â ¡®à¤¥à .
BLANK - Š¢ ¤à â £ á¨âáï - áâ ­®¢¨âáï ç¥à­ë¬.
Ž¡ê¥¬ ¤ ­­ëå ०¨¬  ª¢ ¤à â  á®áâ ¢«ï¥â 2 ¡ ©â ,
¯®í⮬㠨§¬¥­¥­¨¥ ०¨¬  ¢á¥£® íªà ­  ᢮¤¨âáï ª ¯¥à¥§ ¯¨á¨ 2.5 ª¨«®¡ ©â
¤ ­­ëå ¢ ¢¨¤¥®-އ“.
®¤®¡­ ï áâàãªâãà  íªà ­  ¯®§¢®«ï¥â «¥£ª® ¯à®¨§¢®¤¨âì áªà®««¨­£¨
ª ª ¢á¥£®, â ª ¨ ç á⥩ íªà ­  ¯® §­ ª®¬¥áâ ¬.
¥¦¨¬ íªà ­  ãáâ ­ ¢«¨¢ ¥âáï ¯à¨ ¢ª«î祭¨¨,   â ª ¦¥ á ¯®¬®éìî
ä㭪権 ¡¨®á . ”㭪樨 ¡¨®á  ¯®§¢®«ïîâ ®âªà뢠âì ­  íªà ­¥ £à ä¨ç¥áª¨¥ ¨
⥪áâ®¢ë¥ ®ª­  ¢ ­ã¦­ëå ¬¥áâ å ¨ ­ã¦­®£® à §¬¥à .
‚ ¡¨®á¥ ¨¬¥îâáï ä㭪樨 ®âªàëâ¨ï £à ä¨ç¥áª®£® íªà ­  ­  ¢¥áì íªà ­
320x256 â®ç¥ª. ®á«¥ ®âªàëâ¨ï í⮣® ०¨¬  íªà ­ ¯à¥¤áâ ¢«ï¥â ᮡ®©.
­ ¡®à ¨§ 256-⨠«¨­¨©, ¤«¨­®© ¯® 320 ¡ ©â. ‘®á¥¤­¨¥ â®çª¨ ¢ «¨­¨¨ - íâ®
á®á¥¤­¨¥ ¡ ©âë. ¥à¥ª«î祭¨¥ «¨­¨© ¯à®¨§¢®¤¨âáï ç¥à¥§ PORT_Y, ¢ ª®â®à®¬
ãáâ ­ ¢«¨¢ ¥âáï ­®¬¥à «¨­¨¨, ¢ë¢®¤¨¬®© ­  íªà ­. ®¬¥à  «¨­¨© áç¨â îâáï
ᢥàåã íªà ­ , ­ ç¨­ ï á ­ã«¥¢®©.
„«ï ¢ë¢®¤  ¢ £à ä¨ç¥áª¨© íªà ­ â ª ¦¥ âॡã¥âáï ®âªàëâì
ᮮ⢥âáâ¢ãîéãî áâà ­¨æã ®á­®¢­®£® އ“. ‚ í⮩ áâà ­¨æ¥ ¡ã¤¥â ᮤ¥à¦ âìáï
ª®¯¨ï ¢¨¤¥®¨§®¡à ¦¥­¨ï.
‚¨¤¥®-އ“ ï¥âáï ⥭¥¢ë¬ އ“, ¯®í⮬㠨­ä®à¬ æ¨ï, ­ å®¤ïé ïáï ¢
®á­®¢­®¬ އ“, ¯®¤ ª®â®àë¬ ­ å®¤¨âáï ¢¨¤¥®-އ“ ­¥ ®¡ï§ â¥«ì­® ¡ã¤¥â
ᮢ¯ ¤ âì á ¨­ä®à¬ æ¨¥©, ­ å®¤ï饩áï ¢ í⮬ ¢¨¤¥®-އ“. ‡ ¯¨áì ¢¨¤¥®-¤ ­­ëå
¬®¦¥â ¯à®¨§¢®¤¨âìáï ¨ ¡¥§ ¯¥à¥§ ¯¨á¨ ¤ ­­ëå ¢ ®á­¢­®¬ އ“, çâ® ®ª §ë¢ ¥âáï
¯®«¥§­ë¬ ¯à¨ à ¡®â¥, ­ ¯à¨¬¥à, á® á¯à ©â ¬¨. „«ï à ¡®âë á® á¯à ©â ¬¨ â ª ¦¥
¯à¥¤ãᬮâ७ ०¨¬ § ¯¨á¨ ¢ ¢¨¤¥®-އ“ á ¯à®§à ç­ë¬ 梥⮬. ‚ í⮬ ०¨¬¥
¨­ä®à¬ æ¨ï, ¯¥à¥¤ ¢ ¥¬ ï ¢ ¢¨¤¥®-އ“ ¯à®¢¥àï¥âáï ­  ­ «¨ç¨¥ ¡ ©â  #FF. …᫨
íâ®â ¡ ©â ®¡­ à㦨¢ ¥âáï, ⮠横« § ¯¨á¨ ¯à®¯ã᪠¥âáï ¨ ­  íªà ­¥ ¢ í⮬
¬¥á⥠®áâ ¥âáï ⥠¤ ­­ë¥, ª ª¨¥ ¡ë«¨ à ­¥¥. ’ ª¨¬ ®¡à §®¬ ­  íªà ­¥ ¬®¦­®
¡ëáâà® ¯à®à¨á®¢ë¢ âì á¯à ©âë, ¯à¥¤áâ ¢«ïî騥 ¨§ á¥¡ï ¯àאַ㣮«ì­ë¥ ª à⨭ª¨
á "¯à®§à ç­ë¬¨" 梥⠬¨.
ਬ¥à ¯à®£à ¬¬ë ¢ë¢®¤  ¯àאַ㣮«ì­®© ª à⨭ª¨ ­  íªà ­:
PAGE3 EQU #E2
RGADR EQU #89
LD A,#50 ; áâà ­¨æ  £à ä¨ç¥áª®£® ¢¨¤¥®íªà ­ 
OUT (PAGE3),A ; ãáâ ­®¢¨âì ¢ PAGE3
LD HL,Pucture ;  ¤à¥á ª à⨭ª¨ (àï¬ë¥ „ ­­ë¥)
LD DE,#C040+HorPlace ; ¯®«®¦¥­¨¥ ª à⨭ª¨ ­  íªà ­¥ ¯® £®à¨§®­â «¨
LD A,VerPlace ; ¯®«®¦¥­¨¥ ª à⨭ª¨ ­  íªà ­¥ ¯® ¢¥à⨪ «¨
OUT (RGADR),A
LD B,VerSize ; ¢ëá®â  ª à⨭ª¨
LOOP: PUSH DE ; § ¯®¬­¨âì ¯®«®¦¥­¨¥ ­  «¨­¨¨
PUSH BC ; § ¯®¬­¨âì áç¥â稪 ¢ëá®âë
LD BC,HorSize ; ¤«¨­  ª à⨭ª¨
LDIR ; ª®¯¨à®¢ âì «¨­¨î
POP BC
POP DE
INC A ; á«¥¤ãîé ï ª®®à¤¨­ â  ¯® Y
OUT (RGADR),A
DJNZ LOOP ; ¯®¢â®àïâì ­ã¦­®¥ ª®«¨ç¥á⢮ à §
“¯à ¢«¥­¨¥ ०¨¬®¬ ¢ë¢®¤  ­  íªà ­ (¢ª«î祭¨¥ ¢ë¢®¤  á ¯à®§à ç­ë¬¨
梥⠬¨, ®âª«î祭¨¥ ª®¯¨à®¢ ­¨ï ¢ ®á­®¢­®¥ އ“) ®áãé¥á⢫ï¥âáï ç¥à¥§
¬« ¤è¨¥ ¡¨âë ¯®àâ  áâà ­¨æë £à ä¨ç¥áª®£® íªà ­ .
€ªá¥«¥à â®à ®¯¥à æ¨© á Ž‡“.
€ªá¥«¥à â®à ®¯¥à æ¨© á Ž‡“ ¯à¥¤­ §­ ç¥­ ¤«ï ã᪮७¨ï ®¯¥à æ¨©
¯® ¯¥à¥á뫪¥ ¤ ­­ëå ¨«¨ ¯® § ¯®«­¥­¨î އ“ ®¤­¨¬ ¡ ©â®¬. €ªá¥«¥à â®à
¯à¨áãâáâ¢ã¥â ¢ ç¨áâ® Sprinter-®¢áª¨å ª®­ä¨£ãà æ¨ïå ¨ ¯®í⮬㠭¨ª ª ­¥
¬¥è ¥â à ¡®â¥ ®¡ëç­ëå Spectrum-®¢áª¨å ¯à®£à ¬¬.
Žá­®¢®©  ªá¥«¥à â®à  ï¥âáï ¡ëáâ஥ ¢­ãâ७­¥¥ އ“ ¢ ‹Œ.
ޝ¥à æ¨¨ ¯® ¯¥à¥á뫪¥ ¤ ­­ëå ¯à®¨§¢®¤ïâáï ¯ã⥬ § ¯¨á¨ ¡«®ª  ¤ ­­ëå ¢ íâ®
¢­ãâ७­¥¥ އ“,   § â¥¬ ª®¯¨à®¢ ­¨¨ ¥£® ¢ ­ã¦­®¥ ¬¥áâ® ¯ ¬ï⨠¨§ í⮣® އ“.
®á«¥ ®¤­®© § ¯¨á¨ ª®¯¨à®¢ ­¨¥ ¬®¦¥â ¯à®¨§¢®¤¨âìáï ­¥áª®«ìª® à § ¨ â ª¨¬
®¡à §®¬ ¬®¦­® ¯à®¨§¢®¤¨âì § ¯®«­¥­¨¥ íªà ­  ⥪áâãà ¬¨.
„«ï § ¯®«­¥­¨ï íªà ­  ®¤­¨¬ 梥⮬ ¨á¯®«ì§ã¥âáï ¤à㣮© ०¨¬
 ªá¥«¥à â®à . ‚ ­¥¬ ¢¬¥áâ® ª®¯¨à㥬®£® ¡«®ª  ¤ ­­ëå ¨§ ¢­ãâ७­¥£® އ“
¯à®¨§¢®¤¨âáï § ¯¨áì ¤ ­­ëå á è¨­ë ¯à®æ¥áá®à , ª®â®àë¥ ¢ íâ®â ¬®¬¥­â ­¥
¨§¬¥­ïîâáï.
«®ª ¤ ­­ëå, § ¯¨á뢠¥¬ë© ¢ އ“  ªá¥«¥à â®à  ¬®¦¥â ¨¬¥âì à §«¨ç­ãî
¤«¨­­ã ¨§ ¤¨ ¯ §®­  1..256 ¡ ©â.
“¯à ¢«¥­¨¥  ªá¥«¥à â®à®¬ ¯à®¨§¢®¤¨âáï ­¥¯®á।á⢥­­® ¨§ ¯à®£à ¬¬ë.
„«ï í⮣® ¨§¯®«ì§ãîâáï ª®¬ ­¤ë ¯à®æ¥áá®à , ª®â®àë¥ ä ªâ¨ç¥áª¨ ïîâáï
®¯¥à æ¨ï¬¨ ⨯  NOP.
â® ª®¬ ­¤ë LD A,A; LD B,B; LD C,C; LD D,D; LD E,E; LD H,H, LD L,L
 §­ ç¥­¨¥ ª®¬ ­¤ á«¥¤ãî饥:
LD B,B - ¢ëª«îç¨âì  ªá¥«¥â à®à.
LD D,D - ¢ª«îç¨âì  ªá¥«¥à â®à ¢ ०¨¬ ¯à¨¥¬  ¡ ©â  à §¬¥à  ¡«®ª 
¤ «¥¥ á«¥¤ã¥â ª®¬ ­¤  ⨯  LD A,dat, £¤¥ dat ¨ ¡ã¤¥â ­®¢ë¬
à §¬¥à®¬ ¡«®ª . …᫨ à §¬¥à ¡«®ª  ¡ë« ãáâ ­®¢«¥­ à ­¥¥,
¥£® ¬®¦­® ­¥ ãáâ ­ ¢«¨¢ âì.
LD C,C - ޝ¥à æ¨ï Fill - § ¯®«­¥­¨¥ ®¤­¨¬ ¡ ©â®¬. ®á«¥¤ãîé ï
ª®¬ ­¤  ⨯  LD (HL),A ¯à¨¢¥¤¥â ª § ¯®«­¥­¨î 㪠§ ­­®£®
à ­¥¥ ª®«¨ç¥á⢠ ¡ ©â §­ ç¥­¨¥¬ A
LD E,E - ޝ¥à æ¨ï Fill ¤«ï £à ä¨ç¥áª®£® íªà ­  - § ¯®«­¥­¨¥
¢¥à⨪ «ì­ëå «¨­¨©.
LD H,H - rezerved
LD L,L - ª®¯¨à®¢ ­¨¥ ¡«®ª . ®á«¥¤ãîé ï ª®¬ ­¤  ⨯  LD A,(HL)
¯à¨¢¥¤¥â ª § ¯®«­¥­¨î އ“  ªá¥«¥à â®à  ¤ ­­ë¬¨ ¨§  ¤à¥á  (HL),
  ª®¬ ­¤  ⨯  LD (DE),A ¯à¨¢¥¤¥â ª ¯¥à¥§ ¯¨á¨ ¤ ­­ëå ¨§ އ“
 ªá¥«¥à â®à  ¢ ®á­®¢­®¥ ¨«¨ ¢¨¤¥®-އ“.
LD A,A - ª®¯¨à®¢ ­¨¥ ¡«®ª  ¤«ï £à ä¨ç¥áª®£® íªà ­  ¯®¤®¡­  ª®¬ ­¤¥
LD L,L, ­® à ¡®â ¥â á ¢¥à⨪ «ì­ë¬¨ «¨­¨ï¬¨ íªà ­ .
ਬ¥à ¨á¯®«ì§®¢ ­¨ï  ªá¥«¥à â®à :
; ‘ç¨â ¥¬, çâ® íªà ­­ ï áâà ­¨æ  㦥 ®âªàëâ  ¯®  ¤à¥áã #C000
LD HL,#C040 ;  ¤à¥á ­ ç «  «¨­¨¨ ¯¥à¢®£® íªà ­ 
LD DE,#C180 ;  ¤à¥á ­ ç «  «¨­¨¨ ¢â®à®£® íªà ­ 
LD BC,#140 ; ¤«¨­  íªà ­  ¯® £®à¨§®­â «¨
DI ; § ¯à¥â¨âì ¯à¥à뢠­¨ï ¤«ï à ¡®âë á  ªá¥«¥à â®à®¬
LD D,D ; ¢ª«îç¨âì  ªá¥«¥à â®à ­  ãáâ ­®¢ªã à §¬¥à  ¡«®ª 
LD A,0 ; ãáâ ­®¢¨âì à §¬¥à ¡«®ª  - 256 ¡ ©â
LD A,A ; ãáâ ­®¢¨âì  ªá¥«¥à â®à ­  ª®¯¨à®¢ ­¨¥
; ¢¥à⨪ «ì­ëå «¨­¨©.
LDIR ; ª®¯¨à®¢ âì !
LD B,B ; ¢ëª«îç¨âì  ªá¥«¥à â®à
EI ; ¢ª«îç¨âì ¯à¥à뢠­¨ï
â®â ®â१®ª ¯à®£à ¬¬ë ¯à®¨§¢¥¤¥â ª®¯¨à®¢ ­¨¥ ¢á¥£® íªà ­  á ®¤­®£®
íªà ­  ­  ¤à㣮©. ‚à¥¬ï ¥£® ¨á¯®«¥­¨ï á®áâ ¢«ï¥â ¯à¨¬¥à­® 1.2 ¨­â .
„®¯®«­¨â¥«ì­ë¥ ä㭪樨  ªá¥«¥à â®à  ¯®ï¢«ïî騥áï ¢ ª®­ä¨£ãà æ¨¨
Sprinter-3 à ¡®â îâ ¯®¤®¡­ë¬ ¦¥ ®¡à §®¬. „«ï ¢ë¯®«­¥­¨ï «®£¨ç¥áª¨å ä㭪権
¨á¯®«ì§ãîâáï ª®¬ ­¤ë XOR (HL); OR (HL); AND (HL).
ਬ¥à ªá®àª¨ ¡«®ª  ¢ 256 ¡ ©â.
LD HL,ADRES_1
LD DE,XOR_DAT
DI
LD D,D
LD A,0 ; ç¨á«® ¡ ©â, ª®â®àë¥ ­ ¤® ¯à®ªá®à¨âì
LD L,L
LD A,(DE) ; ‚§ïâì ¡«®ª ¢ އ“  ªá¥«¥à â®à 
XOR (HL) ; ¯à®¨§¢¥á⨠®¯¥à æ¨î XOR á ¤ ­­ë¬¨  ªá¥«¥à â®à 
LD (HL),A ; § ¯®¬­¨âì ¢ އ“ १ã«ìâ â ®¯¥à æ¨¨
LD B,B
EI
‘ª®à®áâì à ¡®âë  ªá¥«¥à â®à  ®£à ­¨ç¨¢ ¥âáï ⮫쪮 䨧¨ç¥áª®©
᪮à®áâìî à ¡®âë ®á­®¢­®£® އ“. Ž¯à¥¤¥«¨âì ¢à¥¬ï à ¡®âë ª®¬ ­¤ë á
 ªá¥«¥à â®à®¬ ¬®¦­® ¯® â ª®© ¯à¨¬¥à­®© ä®à¬ã«¥:
‚६ï à ¡®âë = ¢à¥¬ï à ¡®âë ª®¬ ­¤ë ¡¥§  ªá¥«¥à â®à  + ¢à¥¬ï
à ¡®âë  ªá¥«¥à â®à 
‚६ï à ¡®âë  ªá¥«¥à â®à  = ç¨á«® ¯¥à¥áë« ¥¬ëå ¡ ©â /7000000 (ᥪ㭤)
Žâª«î祭¨¥ ¯à¥à뢠­¨© ¢® ¢à¥¬ï à ¡®âë  ªá¥«¥à â®à  ­¥®¡å®¤¨¬®, â ª
ª ª ¢ íâ®â ¬®¬¥­â ç áâ¨ç­® ¬¥­ï¥âáï á¨á⥬  ª®¬ ­¤ ¯à®æ¥áá®à  ¨ ¯à®£à ¬¬ 
­  ¯à¥à뢠­¨¨ ­¥ ᬮ¦¥â à ¡®â âì ­®à¬ «ì­®.
‡ ª«î祭¨¥.
 ¡®â  ­ ¤ Sprinter-®¬ ¯à®¤®«¦ ¥âáï. ‘®¢¥à襭áâ¢ã¥âáï ¦¥«¥§® ¨
¡¨®á. ¨è¥âáï á®äâ, ¯®¤¤¥à¦¨¢ î騩 à áè¨à¥­­ë¥ ०¨¬ë à ¡®âë ª®¬¯ìîâ¥à .
® ¢á¥¬ ª®¬¬¥àç¥áª¨¬ ¢®¯à®á ¬ á¢ï§ ­­ë¬ á ¯à¨®¡à¥â¥­¨¥¬ ª®¬¯ìîâ¥à 
¬®¦­® ®¡à é âìáï ¢ ä¨à¬ã "¥â¥àá":
Adress: ‘ ­ªâ-¥â¥à¡ãà£, ã«. ‚®ááâ ­¨ï, ¤. 35, ®ä. 31.
Phone: (812)-327-35-31
E-mail: peters@atlant.ru
® â¥å­¨ç¥áª¨¬ ¢®¯à®á ¬ ®¡à é âìáï ª® ¬­¥:
Fido: Ivan Mak (2:5030/529.24)
E-mail: ivan_mak@yahoo.com (¢à¥¬¥­­® § ªàëâ)
File diff suppressed because it is too large Load Diff
+272
View File
@@ -0,0 +1,272 @@
# 1. Архитектура компьютера Sprinter Sp2000
## 1.1 Общие сведения
ZX Sprinter — универсальный компьютер на базе 8-битного процессора Z80. Основой является перепрограммируемая логическая матрица (ППЛМ/CPLD/FPGA), что обеспечивает гибкую изменяемую архитектуру — конфигурация схемы может меняться непосредственно во время работы.
Материнская плата — Sp2000 (разработана в конце 2000 г.).
| Параметр | Значение |
|---|---|
| Процессор | Z84C15 (Z80-совместимый, внутренние PIO/SIO/CTC) |
| Тактовая частота | 21 МГц (turbo) / 3.5 МГц (slow) |
| ОЗУ (main) | 4 МБ (72-pin SIMM) |
| Быстрое ОЗУ (КЭШ-ОЗУ) | 64 КБ (SRAM, без wait states) |
| ПЗУ | 256 КБ (8 страниц по 32 КБ) |
| Видео-ОЗУ | 256 КБ |
| Контроллер дисков | КР1818ВГ93 (аналог WD1793) |
| Поддержка НЖМД | IDE/AT |
| Контроллер клавиатуры | AT-совместимая (101 key), PS/2 |
| Контроллер мыши | Serial MS-Mouse (RS-232 через Z84C15 SIO) |
| Слоты расширения | 2 × ISA-8 |
| Звук | AY-3-8910 (в ППЛМ), 16-бит ЦАП TDA1543 (COVOX + AY) |
| COVOX-Blaster (CBL) | 16-bit с буферным ОЗУ 256 байт, 15/22 кГц |
| CMOS-часы | Dallas DS12887A (опционально, эмулируется при отсутствии) |
| Видеовыход | Аналоговый CGA-монитор, RGB, TV (SCART) |
## 1.2 Системная архитектура
Упрощённая структура:
```
┌─────────────┐
┌──────────┐ │ ISA-8 │
│ Принтер │ │ слот 1..2 │
│ Мышь │ └──────┬──────┘
│ Джойстик │ │
└─────┬────┘ ┌─────┴──────┐
│ │ Буферы и │
┌─────┴──────┐ │ дешифраторы│
│ Z84C15 │ └──────┬─────┘
│ (CPU + │◄─────────┤
│ PIO/SIO/ ├──────────┤
│ CTC) ├──────────┤
└─────┬──────┘ │
│ ┌────┴──────────┐
┌─────┴──────┐ │ EP1K30QC208 │
│ ROM 256K │◄───►│ (CPLD/FPGA) │◄───► VRAM 256K
│ Cache 64K │ │ │◄───► Main RAM
└────────────┘ │ │
│ AY-3-8910 │
│ COVOX/CBL │
│ Accel. │
└──────┬────────┘
┌───┴───┐
│TDA1543│
│ ЦАП │
└───────┘
```
Периферия (FDD, HDD, Kempston, клавиатура AT) подключается через ППЛМ. Вспомогательная ППЛМ (EPM7064 на Sp2000) обеспечивает синхронизацию и начальный запуск. Дешифрация адресов портов выполняется через ППЛМ, что позволяет перепрограммировать адреса портов.
## 1.3 ППЛМ и загрузка конфигураций
### Механизм конфигурирования
При включении или RESET информация в ППЛМ стирается. ППЛМ переходит в режим ожидания загрузки блока данных конфигурации. Процессор отключён от периферии, в его адресное пространство включено ПЗУ и возможно КЭШ-ОЗУ. Любая запись процессора в адресное пространство в этот момент записывает данные в ППЛМ.
Программа загрузчика в ПЗУ:
1. Проверяет флаг в КЭШ-ОЗУ по смещению `#80` (последняя страница КЭШа)
2. Если флаг (строка `ACEX_30K_LOADING`) установлен → загружает конфигурацию из КЭШ-ОЗУ (смещение `#100`)
3. Если флаг сброшен → загружает конфигурацию из ПЗУ
### Смена конфигурации программно
1. Загрузить файл прошивки в последнюю страницу КЭШ-ОЗУ (смещение `#100`)
2. Записать флаг `ACEX_30K_LOADING` по смещению `#80`
3. Выполнить программный сброс записью в страницу RESET_PAGE (`#A0`),
4. После сброса загрузчик находит флаг, загружает прошивку в ППЛМ и стирает флаг
При аппаратном RESET флаг отсутствует — загружается начальная конфигурация из ПЗУ.
### Ограничения
- Внутренний формат данных ППЛМ — закрытая информация Altera
- Программа разводки схем — MAX+Plus II — не работает на ZX-Spectrum/Sprinter (требуется PC)
## 1.4 Конфигурации ППЛМ
### Sprinter-1
Максимально совместима с ZX-Spectrum. Включает:
- Режимы Spectrum-128/Scorpion-256/Pentagon-512
- Расширенная память до 4 МБ
- Расширенный экран: Spectrum, Text-80×32, Graf-320×256×256
- Контроллер дисковода (ВГ93), контроллер IDE
- AT-клавиатура, подключённая как ZX-Keyboard (через порт `#FE`)
- COVOX 8-bit
- AY-3-8910 (доступен)
### Sprinter-2
Требования к совместимости жёстче. Добавляет:
- Акселератор операций с ОЗУ (fill, copy, AND, OR, XOR)
- AT-клавиатура через внутренний последовательный порт Z84C15 (сканкоды, не ZX-матрица)
### ZX-Spectrum+AY
Максимальное приближение к ZX-Spectrum-128/256. AY-3-8910 (3-я версия, в ППЛМ): 3 голоса, шум, амплитуда, огибающая (обнаружена ошибка в формирователе огибающей [IvanMak.txt:224229]).
### Game-1
На базе Sprinter-2. Акселератор без логических функций. COVOX-Blaster (15 кГц).
### DooM
Развитие Game-1. Акселератор с аппаратным растяжением/сжатием вертикальных и горизонтальных линий.
### Video
Похожа на Game-1, добавлена возможность передачи данных с HDD прямо в видео-память. Режим `GR-256-4×4` — 160×128 с аппаратным удвоением пикселя.
### Плата Sp2000
Конфигурации Sprinter-1, Sprinter-2 и ZX+AY объединены в одну прошивку. Переключение — через системный порт. Game-1, DooM, Video также планируется свести в одну.
## 1.5 Система памяти (обзор)
Адресное пространство Z80 (64 КБ) разделено на 4 окна по 16 КБ:
| Окно | Адреса | Название | Назначение (по умолчанию) |
|---|---|---|---|
| W0 | `#0000..#3FFF` | PAGE0 | ПЗУ (системное) |
| W1 | `#4000..#7FFF` | PAGE1 | ОЗУ, страница 5 |
| W2 | `#8000..#BFFF` | PAGE2 | ОЗУ, страница 2 |
| W3 | `#C000..#FFFF` | PAGE3 | ОЗУ, любая страница (0..N) |
Основная память — 4..64 МБ — делится на блоки по 16 КБ. Управление через порты страниц (`#82`, `#A2`, `#C2`, `#E2` для окон 0..3). ПЗУ: `#E0..#EF`. КЭШ-ОЗУ: `#F0..#FF`.
Детально — `04-memory.md`.
## 1.6 Видеосистема (обзор)
Основа — ППЛМ + 256 КБ видео-ОЗУ. Два режима адресации VRAM:
- **Спектрумовский** — экран 32 блока по 8 КБ, адрес через RGADR
- **Графический** — 256 строк × 1024 байта, линейная адресация; страницы `#50..#5F`
Режимы вывода (задаются для каждого квадратика 8×8):
- ZX-40 — текстовый 40×32 (Spectrum-совместимый)
- ZX-80 — текстовый 80×32, до 36 знакогенераторов
- GR-256-8 — графический 320×256×256 цветов
- GR-16-16 — графический 640×256×16 цветов
Палитра: 8 палитр по 256 цветов из 16 млн. Цвет: 3 байта RGB (BGR) на цвет.
четыре из этих восьми палитр используются для графических режимов и оставшиеся четыре для текстовой палитры — 4 текстовые палитры (paper, ink, flash-paper, flash-ink) по адресам `#03F0..#03FE`.
Детально — `05-graphics.md`.
## 1.7 Акселератор (обзор)
Акселератор — быстрое внутреннее ОЗУ в ППЛМ. Присутствует в Sprinter-2 и выше. Управляется через NOP-команды процессора (`LD D,D`, `LD L,L`, `LD C,C` и др.), которые ППЛМ распознаёт как инструкции акселератора.
Операции: fill, copy, AND, OR, XOR.
**время копирования полного экрана.** - «~1.2 инта» (~24 мс при 50 Гц)
**формула скорости.**
«Время работы акселератора = число пересылаемых байт / 7000000 (секунд)»
**акселератор и прерывания.** требуется DI/EI (акселератор сильно меняет систему команд).
Детально — `06-accel.md`.
## 1.8 Быстрое ОЗУ (КЭШ-ОЗУ)
64 КБ SRAM с доступом без wait states на частоте 21 МГц. Физические страницы `#F0..#FF`, реально 4 страницы по 16 КБ.
Подключается через порт `#FB` (стандартный Pentagon-овский), чтение `IN A,(#FB)` — включает, `IN A,(#7B)` — выключает.
Ограничения:
- Несовместимо с акселератором
- Не сохраняется между процессами
- Конфликт с COVOX по порту `#FB`
- На Sp2000 используется для загрузки прошивок ППЛМ и как временное хранилище
Детально — `04-memory.md`.
## 1.9 ISA-слоты
Два 8-битных ISA-слота.
Возможны два способа доступа к ISA — вероятно, разные конфигурации ППЛМ:
- Через страницы памяти `#D0..#DF` [IvanMak.txt:855858]: «Бит 1 номера означает выбор доступа к порту или памяти ISA, а бит 2 определяет к какому из двух слотов осуществляется доступ.»
- Через управляющие порты [Parinov.txt:798857]: (1) запись `#10` в порт `1FFDh`, (2) запись управляющего байта в порт `0E2h` (бит D2=слот, D1=порт/память), (3) использование порта `9FBDh` для старших адресов.
## 1.10 Звук (обзор)
COVOX: ЦАП TDA1543 — 16-битный стерео, COVOX — 8-битный, реально используется 10 бит для одновременного вывода COVOX и AY.
**порт CBL.**
«Порт управления: 004Eh (16-bit port!!!, писать только через OUT (C),reg)» — 16-бит, требуется `OUT (C),A`.
Компоненты звукового тракта:
- **16-битный ЦАП TDA1543** (реально 10 бит) — подключён к ППЛМ
- **AY-3-8910** — эмуляция в ППЛМ (3-я версия: 3 голоса + шум + огибающая)
- **COVOX 8-bit** — порты `#FB` / `#4F`, через TDA1543
- **Бипер** — бит 3 порта `#FE`
- **COVOX-Blaster (CBL)** — COVOX с буферным ОЗУ 256 байт, 15/22 кГц. Управление порт `#4E`
Детально — `08-io.md`.
## 1.11 Клавиатура (обзор)
В Sprinter-1 — ZX-клавиатурная матрица через порт `#FE`. В Sprinter-2 и выше — AT-клавиатура через внутренний SIO Z84C15 (порты `#18`/`#19`). Сканкоды напрямую, FIFO на 3 байта.
Детально — `08-io.md`.
## 1.12 Мышь
Microsoft Mouse (2 кнопки) через последовательный порт Z84C15. Порт команд: `#1B`, порт данных: `#1A`.
Детально — `08-io.md`.
## 1.13 Дисковая подсистема
- **FDD**: КР1818ВГ93 (WD1793). Порт `#1F` перенаправляется на `#0F` в ПЗУ. Поддержка 720 КБ / 1.44 МБ.
- **HDD**: IDE/AT. Порты `#XX50..#XX55`, регистры `#20..#29`.
- **CMOS Dallas DS12887A**: порты `#FFBD` (read), `#BFBD` (write), `#DFBD` (address).
Детально — `08-io.md`.
## 1.14 Система прерываний (обзор)
Поддерживаются IM1 и IM2. Генерация INT — через бит 0 порта `#FE`. CTC Z84C15 — таймер, SIO — клавиатура. ISA-слоты — IRQ через PIO порт B (`#1F`).
Детально — `07-irq.md`.
## 1.15 Сброс и старт машины
Программный сброс (без перезагрузки ППЛМ) [IvanMak.txt:13851394]:
```
DI
LD A, 16
LD BC, 1FFDh
OUT (C), A
LD A, 0A0h
OUT (PAGE3), A ; PAGE3 = #E2
LD (0C000h), A ; в этот момент происходит RESET
DI
HALT
```
Сброс с перезагрузкой ППЛМ — через функции BIOS (`02-bios.md`). Аппаратный RESET — начальная конфигурация из ПЗУ.
## 1.16 Плата Sp2000 — особенности
- Конфигурации объединены в две прошивки ППЛМ (Sprinter-1+Sprinter-2+ZX+AY — одна; Game-1+DooM+Video — другая)
- AY-сопроцессор доступен и в Sprinter-1, и в Sprinter-2
- ОЗУ: 72-pin SIMM, от 4 до 64 МБ
- Внутренние порты Z84C15: `#10..#1F`, `#EE`, `#EF`, `#F0`, `#F1`, `#F4` — их адреса вне ППЛМ
- Обнаружена ошибка в формирователе огибающей AY (3-я версия)
## 1.17 Кросс-ссылки
- BIOS: `02-bios.md`
- DSS/ESTEX: `03-dss.md`
- Память, режимы, банки: `04-memory.md`
- Видео, палитра: `05-graphics.md`
- Акселератор: `06-accel.md`
- Прерывания: `07-irq.md`
- Диски, джойстик, принтер, ISA: `08-io.md`
- Клавиатура, мышь: `09-input.md`
- Звук: `10-sound.md`
- Карта портов: `11-ports.md`
- Баги: `12-bugs.md`
+1234
View File
File diff suppressed because it is too large Load Diff
+1082
View File
File diff suppressed because it is too large Load Diff
+387
View File
@@ -0,0 +1,387 @@
# 4. Управление памятью Sprinter Sp2000
## 4.1 Аппаратная модель памяти
Z80 видит 64 КБ, разбитые на **четыре окна (window) по 16 КБ**. Каждое окно независимо маппится на физическую страницу (page) ОЗУ или ПЗУ через порт-регистр страницы:
| Окно | Адреса | Порт страницы | Назначение |
|------|--------|---------------|------------|
| W0 | `#0000..#3FFF` | `#82` (PAGE0, ROM.SLOT0) | RST-обработчики, BIOS/DSS ядро |
| W1 | `#4000..#7FFF` | `#A2` (SLOT1, PAGE1) | Приложение |
| W2 | `#8000..#BFFF` | `#C2` (SLOT2, PAGE2) | **Стек — обязательно здесь** |
| W3 | `#C000..#FFFF` | `#E2` (SLOT3, PAGE3) | Разделяемые страницы DSS, графика, резидентный код |
- Запись в порт `#82`/`#A2`/`#C2`/`#E2` меняет физическую страницу окна; чтение возвращает текущую.
- Для совместимости со старыми моделями (ZX-Spectrum 128, Scorpion, Pentagon) работают также порты `#7FFD`, `#1FFD`, `#DFFD`, `#EFF7`:
| Порт | Описание |
|------|----------|
| `#7FFD` | Биты 0-2: страница W3; бит 3: выбор ПЗУ (0=DOS, 1=Basic/48) |
| `#1FFD` | Биты 0-2: страница W0; бит 3: дополнительная память; бит 4: турбо |
| `#DFFD` | Страница W1 (Pentagon, когда бит 5 порта `#7FFD`=0) |
| `#EFF7` | Страница W2 (Pentagon) |
> **Страница `#FF`:** если окно не выделено программе (образ меньше 16/32 КБ), DSS маппит его на специальную страницу `#FF`. Чтение даёт `#FF`, запись игнорируется. Это ключевая причина «молчаливой» порчи данных — см. §4.9.
> **Физические страницы:** порты `#82`/`#A2`/`#C2`/`#E2` адресуют физические 16-КБ страницы в адресном пространстве Sprinter. Диапазон страниц зависит от установленной памяти (обычно 0..127 для 2 МБ, 0..255 для 4 МБ). Старшие страницы заняты видео-ОЗУ и ПЗУ.
---
## 4.2 Окно W0 (#0000..#3FFF) — системное
W0 — критическое окно: в нём живут обработчики `RST 08h` (BIOS), `RST 10h` (DSS/ESTEX) и `RST 18h` (BIOS-EXP). Пока в W0 стоит системная страница, вызовы доступны.
**ROM/RAM switching:**
| Действие | Код |
|----------|-----|
| Включить ПЗУ в W0 | `OUT (#7C),A` (SYS_PORT.ROM) |
| Выключить ПЗУ, вернуть ОЗУ | `OUT (#3C),A` (SYS_PORT.RAM) |
При включённом ПЗУ в W0 отображается системная страница BIOS. Для вызова RST-функций W0 должен быть в ПЗУ — BIOS сам переключает его при входе через `RST 08h`.
**Требования к прерываниям:**
Sprinter работает в режиме **IM1** — вектор прерывания фиксирован на `#0038` (всегда в ПЗУ/ОЗУ W0). Обработчик EXP.asm по адресу `#0038`:
1. Переключает W3 на SYS_PAGE.
2. Проверяет флаг `INT_ID` = `#AA`.
3. Если есть пользовательский обработчик — вызывает его (адрес из SYS_PAGE).
4. Восстанавливает W3, `EI`, `RETI`.
Регистр `I` инициализирован в `#3F` (наследие ZX-Spectrum), но при IM1 не используется для диспетчеризации. Если программа переключается в **IM2**`I` и расположение таблицы векторов определяются программой произвольно, стандартного расположения нет.
**DSS и W0:** при вызове `RST 10h` DSS переключает W0 на свою рабочую страницу (COREPAGE = 4), выполняет функцию и восстанавливает W0. Если в W0 до вызова была ОЗУ, DSS восстановит её; если ПЗУ — вернёт ПЗУ.
**Практические рекомендации:**
- Для вызова системных функций держите W0 в ПЗУ (или не заботьтесь — RST 08h и RST 10h его переключат сами).
- При IM1 (режим по умолчанию) прерывание идёт на `#0038` — это ПЗУ, переключение W0 не требуется.
- При IM2 программа сама управляет таблицей векторов — не забывайте про `DI`/`EI` при переключении W0.
**Размещение своей RAM-страницы в W0:**
Если программе нужно дополнительное окно (например, для атласов спрайтов или кода,
не требующего системных вызовов), можно временно маппить свою страницу в W0. Для
безопасной работы страница должна содержать корректную таблицу RST-векторов в
первые `#40` байт:
| Адрес | Инструкция | Назначение |
|-------|-----------|------------|
| `#0000` | `JP boot` или `RST 00h` (обычно не используется) | |
| `#0008` | `JP rst08_stub` | RST 08h — BIOS |
| `#0010` | `JP rst10_stub` | RST 10h — ESTEX/DSS |
| `#0018` | `JP rst18_stub` | RST 18h — BIOS-EXP |
| `#0020..#0030` | `JP _w0_unused_stub` | RST 20h-30h — не используются |
| `#0038` | `JP isr_stub` | IM1 (RST 38h) |
| `#0066` | `RETN` или `JP nmi_stub` | NMI |
Каждый стаб (в этой же странице, выше `#40`) выполняет одну задачу: сохранить
текущий номер страницы W0, переключить W0 обратно на ПЗУ (`OUT (#7C),A`
SYS_PORT.ROM), выполнить настоящий системный вызов, после чего восстановить
страницу программы и вернуться к исходному caller'у. Типовой паттерн для ESTEX
(`RST 10h`):
```asm
rst10_stub:
IN A, (#82) ; сохранить свою страницу W0
PUSH AF
OUT (#7C), A ; включить ПЗУ в W0 (SYS_PORT.ROM)
; теперь в W0 — системная страница, RST 10h будет обработан корректно
RST #10 ; вызвать настоящий ESTEX (DSS сам переключит W0
; на COREPAGE и восстановит ПЗУ на выходе)
POP BC
LD A, B
OUT (#82), A ; вернуть свою страницу в W0
RET ; возврат к исходному caller'у
```
Такой же паттерн для RST 08h и RST 18h. Для IM1 (`RST 38h`) — аналогично, но
прерывание может прийти в любой момент, поэтому критические секции (своя
страница в W0) обязаны быть обёрнуты в `DI`/`EI` (или проверять вложенность
через счётчик), иначе повторное прерывание при уже замапленной странице
попадёт в стаб, а не в EXP.asm.
**Важно:** пока в W0 стоит ваша страница, системные вызовы (`RST 08h`/`10h`/`18h`)
проходят через стабы и работают корректно, но с накладными расходами
(~10+ тактов на каждый вызов). Код на странице не должен вызывать DSS/BIOS
напрямую — только через RST. При этом сам стаб не должен использовать
RST-инструкции, так как они адресуют W0, который в момент вызова стаба
всё ещё содержит вашу страницу, а не ПЗУ — сначала переключите ПЗУ через
`OUT (#7C),A`.
Перед первым маппингом своей страницы в W0 сохраните текущее значение порта
`#82` (`IN A,(#82)` — оно даёт номер системной/ПЗУ-страницы). Эта страница
понадобится стабам для восстановления после вызова во время прерываний,
когда ПЗУ уже может быть в W0, а стаб не успел его переключить.
Подтверждённая реализация: `tests/w0page` (см. `sprite-api-design.md §9в`).
---
## 4.3 Окно W1 (#4000..#7FFF) — приложение
W1 — основное окно для кода и данных приложения. По умолчанию содержит страницу 5 (RAM, инициализируется BIOS).
**Использование BIOS/DSS:**
BIOS и DSS могут **временно** переключать W1 на свою рабочую страницу. Это происходит:
- При файловых операциях DSS: чтении FAT, загрузке секторов — DSS маппит одну из страниц разделяемого пула в W1 (DIRPAGE, FATPAGE, TXTPAGE, DRVPAGE).
- При графических операциях BIOS: `PIC_FN2`/`PIC_FN3` (блоковые копии) могут переключать W1 на страницу видео-ОЗУ.
- **Важно:** перед возвратом из вызова BIOS/DSS **восстанавливает** предыдущую страницу W1. Типовой паттерн в исходниках DSS:
```asm
IN A,(SLOT1) ; сохранить текущую страницу W1
PUSH AF
... работа с разделяемым буфером ...
POP BC
LD C,SLOT1
OUT (C),B ; восстановить W1
```
Для вызывающей программы переключение W1 полностью прозрачно — после возврата из любой функции DSS/BIOS содержимое W1 идентично тому, что было до вызова.
**Практические рекомендации:**
- Размещайте код и данные в W1, рассчитывая, что он остаётся стабильным между вызовами.
- При длительных файловых операциях (последовательный `READ` больших объёмов) W1 может быть затронут только в моменты внутреннего обращения DSS к FAT/буферам, но не во время собственно чтения данных в буфер программы.
- Если нужно гарантированно сохранить данные через вызов DSS — используйте W2, который BIOS/DSS **никогда не трогают**.
---
## 4.4 Окно W2 (#8000..#BFFF) — стек (критично)
W2 — **единственное окно, которое BIOS и DSS гарантированно не трогают**. Поэтому стек ОБЯЗАН находиться здесь:
- **SP ≤ #BFFF** при вызовах DSS (`RST 10h`).
- **SP ≥ #8000** при вызовах BIOS (`RST 08h`).
- Пересечение → стек в `#8000..#BFFF`.
- По умолчанию SP инициализируется в `#BFFE` (значение из EXE-заголовка, §4.7).
**Почему W2 — единственное стабильное окно:**
| Окно | Может быть переключено BIOS/DSS | Почему |
|------|--------------------------------|--------|
| W0 | Да | DSS переключает на COREPAGE при `RST 10h`; BIOS переключает на ПЗУ |
| W1 | Да, временно | DSS маппит разделяемые буферы для FAT/директорий; BIOS при графических операциях — и **восстанавливает** перед возвратом |
| W2 | **Нет** | **Никогда. Стек должен быть доступен всегда.** |
| W3 | Да, временно | DSS маппит разделяемые буферы; BIOS при графике переключает на видео-ОЗУ; оба **восстанавливают** исходную страницу перед возвратом |
**EXEC и W2:** при загрузке новой программы (`EXEC`, функция 40h) DSS сохраняет страницу W2 в EXSTACK (вместе с W1 и W3). После завершения программы (`EXIT`, функция 41h) W2 восстанавливается.
**Практические рекомендации:**
- Размещайте стек строго в W2. Стандартное значение `SP = #BFFE` — начало стека с запасом ~14 КБ до `#8000`. Можно уменьшить, если программа неглубокая.
- W1 и W3 при вызовах DSS и BIOS **сохраняются и восстанавливаются** (см. §4.3, §4.5), но W2 — единственное окно, которое **никогда не трогается** ни DSS, ни BIOS.
- Если программе нужно больше памяти, чем даёт W2 (например, для хранения больших массивов), выделяйте блоки через DSS `GETMEM` (функция 3Dh) и мапьте их в W1 или W3.
---
## 4.5 Окно W3 (#C000..#FFFF) — разделяемые страницы
W3 — окно, в которое DSS временно маппит страницы своего разделяемого пула для дисковых буферов и служебных данных (адрес `#C000`):
| Страница | Назначение | Адрес в W3 |
|----------|------------|-------------|
| DIRPAGE (0) | Буфер директории | `#C000` |
| FATPAGE (1) | Кеш FAT | `#C000` |
| TXTPAGE / ENVPAGE (2) | Буфер строки/окружения | `#C000..` (PATH_PNT_ARRAY `#FC80`, ENVTEMP `#FE00`) |
| DRVPAGE (3) | Страница драйвера | `#C000` |
| COREPAGE (4) | Ядро DSS (маппится в W0 при `RST 10h`) | — |
**DSS СОХРАНЯЕТ и ВОССТАНАВЛИВАЕТ W3** после каждой операции. Типовой паттерн в исходниках:
```asm
IN A,(SLOT3) ; сохранить текущую страницу W3
PUSH AF
... работа с разделяемым буфером ...
POP BC
LD C,SLOT3
OUT (C),B ; восстановить W3
```
Это означает, что для вызывающей программы переключение W3 полностью прозрачно — после возврата из любой функции DSS содержимое W3 идентично тому, что было до вызова.
**Какие функции трогают W3:**
Функции, обращающиеся к файловой системе, временно маппят разделяемые страницы в W3: поиск файла (`F_FIRST`/`F_NEXT`), чтение FAT (`OPEN`, `READ`, `WRITE`), работа с каталогами (`CHDIR`, `MKDIR`), буферы драйвера диска. Функции без дисковой активности (`VERSION`, `CURDISK`, `WAITKEY`, `LOCATE`, `SYSTIME`, `ENVIRON` и т.п.) W3 не трогают вообще.
BIOS при графических операциях также временно переключает W3 на страницы видео-ОЗУ (например, `PIC_SET_PAL`, `PIC_FN2`) — и тоже восстанавливает.
**Практические рекомендации:**
- W3 можно использовать для данных программы — после возврата из любой функции DSS содержимое W3 не меняется.
- Единственное исключение — `EXEC` (функция 40h): она загружает новую программу, и W3 получает страницы загруженного образа. Это ожидаемое поведение.
- Если программа не вызывает дисковых и графических функций — W3 полностью стабилен.
- **Стек нельзя размещать в W3** — не потому что W3 «теряется», а потому что переключение W3 происходит ВО ВРЕМЯ вызова. Если `SP` указывает на W3, то в момент, когда DSS временно маппит свой буфер, все push/pop внутри DSS будут обращаться к буферной странице, а не к данным программы. Кроме того, при прерывании IM1 обработчик EXP.asm сам переключает W3 на SYS_PAGE — что также сломает стек, если он в W3.
---
## 4.6 Физическая память и страницы
Физическая память Sprinter — не сплошной массив RAM. Разные области адресного
пространства зарезервированы под видео-ОЗУ, ПЗУ и кеш. Номер страницы — это
значение, которое записывается в порт окна (`#82`/`#A2`/`#C2`/`#E2`);
8-битный порт даёт **256 значений** (`#00..#FF`).
**Фиксированная карта страниц:**
| Диапазон | Страниц | Назначение |
|----------|---------|------------|
| `#00..#4F` (0..79) | 80 (1,25 МБ) | RAM общего назначения |
| `#50..#5F` (80..95) | **16 (256 КБ)** | **Видео-ОЗУ (VRAM) — фиксировано в железе** |
| `#60..#DF` (96..223) | 128 (2 МБ) | RAM общего назначения (верхний диапазон) |
| `#E0..#EF` (224..239) | **16 (256 КБ)** | **ПЗУ: BIOS + EXP + ZX ROMs + bitstream — фиксировано** |
| `#F0..#FF` (240..255) | 16 (256 КБ) | **Fast RAM (кеш) / системные страницы** |
**Выделенные страницы:**
| Страница | Назначение |
|----------|------------|
| `4` | **COREPAGE** — ядро DSS (маппится в W0 при `RST 10h`) |
| `5` | Стандартная страница приложения W1 (по умолчанию) |
| `#41` (65) | **Spec_Page** — сохранение состояния при soft-reset [EXP.asm:859,1019] |
| `#FE` (254) | **SYS_PAGE** — системные данные BIOS (INT_ID, INT_ADRESS, буферы) [MAIN.asm:58] |
| `#FF` (255) | **Dummy-страница**: чтение → `#FF`, запись игнорируется |
**Ключевые моменты:**
- **VRAM (`#50..#5F`) фиксирована аппаратно**, независимо от объёма RAM. Эти
16 страниц не являются частью RAM SIMM — это отдельная микросхема 256 КБ.
Биты 2-3 номера страницы кодируют режим записи: `#50` normal, `#54` без
ОЗУ-тени, `#58` прозрачный (байт `#FF` не пишется), `#5C` оба режима.
- **ПЗУ (`#E0..#EF`)** — 256 КБ флеш-памяти с BIOS, EXP, ZX Spectrum 48/128 ROM,
TR-DOS, логотипом и битстримом для CPLD.
- **Fast RAM (`#F0..#FF`)** — до 512 КБ быстрого SRAM (0 wait-states),
доступна через порт `#FB`. `#FE` (SYS_PAGE) и `#FF` (dummy) — часть этого
диапазона. При включённом кеше порт `#FB` конфликтует с CBL-звуком.
- Для 2 МБ RAM: `#60..` заполнены до `#7F` (32 стр. = 512 КБ); остаток
`#80..#DF` — неиспользуемые страницы (отображаются как `#FF`).
- Для 4 МБ RAM: `#60..#DF` целиком заполнены RAM (128 стр. = 2 МБ).
> **Нумерация:** порты `#82`/`#A2`/`#C2`/`#E2` дают прямой доступ по
> физическому номеру (#00..#FF). Порты `#7FFD`/`#1FFD` (совместимость с
> ZX-Spectrum 128) используют сквозную нумерацию: номер в `#7FFD` (0..7)
> маппится на физические страницы видеоОЗУ `#50..#5F` младшими 3 битами;
> для работы с произвольными страницами используйте порты `#82`/`#A2`/`#C2`/`#E2`.
---
## 4.7 EXE-файл: формат и загрузка
Загрузка исполняемых файлов выполняется функцией DSS `EXEC` (40h). Формат EXE-файла:
**Заголовок (512 байт):**
| Смещение | Размер | Поле | Описание |
|----------|--------|------|----------|
| +0 | 3 | `EXE_EXT` | Сигнатура `"EXE"` |
| +3 | 1 | `VERSION` | Минимальная версия DSS (1 = v1.xx, 0 = без пути/аргументов) |
| +4 | 2 | `OFFCOD1` | Младшее слово смещения образа в файле |
| +6 | 2 | `OFFCOD2` | Старшее слово смещения (OFFCOD2 << 16 + OFFCOD1 = offset) |
| +8 | 2 | `LOADER` | Размер первичного загрузчика (0 = весь образ сразу) |
| +10 | 6 | `RESERVED` | Зарезервировано |
| +16 | 2 | `LD_ADDR` | Адрес загрузки (`#4100..#FFFF`, старший байт маскирован `#3F`) |
| +18 | 2 | `PC_REG` | Точка входа (entry point) |
| +20 | 2 | `SP_REG` | Начальный SP (по умолчанию `#BFFE`) |
| +22 | 1 | `UnUsedPoint` | Не используется |
| +23 | 489 | `RESERVED2` | Зарезервировано (обнуляется; используется как временный стек EXEC) |
**Процесс загрузки (EXEC, функция 40h):**
1. Открыть файл на чтение.
2. Считать 512-байтный заголовок в буфер ядра.
3. Проверить сигнатуру `"EXE"` и версию (≥ минимальной 1).
4. Если `LOADER=0`: seek на смещение `OFFCOD2:OFFCOD1`, считать весь образ по адресу `LD_ADDR`.
5. Если `LOADER>0`: считать только первичный загрузчик по адресу `LD_ADDR` (он дочитает остальные сегменты).
6. Выделить блок памяти через `GETMEM` (функция 3Dh), зарегистрировать в таблице RAM.
7. Сохранить текущие страницы окон (SLOT1-3) в EXSTACK.
8. Определить, сколько страниц нужно, и замаппить их через `SETWIN1`/`SETWIN2`/`SETWIN3`:
- Если `LD_ADDR ≥ #C000`: маппится только W3.
- Если `LD_ADDR ≥ #8000`: маппятся W2 и W3.
- Иначе: маппятся W1, W2 и W3.
9. Построить **PSP** (Program Segment Prefix) — блок данных сразу ниже `LD_ADDR`:
- `LD_ADDR-3`: файловый манипулятор (file handle).
- `LD_ADDR-2`: идентификатор блока памяти.
- `LD_ADDR-1`: номер задачи.
- `LD_ADDR+0`: размер CLP (командной строки).
- `LD_ADDR+1..+128`: CLP-буфер.
- После CLP: строка пути `"A:\DIR\FILE.EXE",0`.
10. Установить SP из заголовка (`SP_REG`), сохранить IX → адрес PSP.
11. Если `VERSION=0` — установить текущий каталог из пути PSP.
12. Затолкать в стек адрес возврата (RETFAR к DSS), **EI**, JP на точку входа.
**Multi-bank EXE:**
При наличии нескольких банков (`LOADER>0`) файл устроен так:
```
[512-байт заголовок] [HOME-сегмент] [Банк 1 (16 КБ)] [Банк 2 (16 КБ)] ...
```
- HOME-сегмент: образ, загружаемый DSS по `LD_ADDR`.
- Банки: 16-КБ блоки, каждый на своей физической странице.
- `LOADER` = размер HOME-сегмента (DSS загружает только его; первичный загрузчик сам дочитывает банки через `READ` из оставшегося открытым файла).
---
## 4.8 Прерывания и память
**Обработчик IM1 (`RST 38h`):**
Вектор фиксирован на `#0038` (W0). Реализация в EXP.asm:
1. Переключает W3 на SYS_PAGE (BIOS).
2. Проверяет флаг `INT_ID` = `#AA` (установлен при инициализации BIOS).
3. Если флаг совпадает — вызывает пользовательский обработчик, адрес которого хранится в SYS_PAGE (`INT_ADDRESS`, `INT_PAGE`).
4. После возврата восстанавливает W3 и выполняет `EI; RETI`.
Это позволяет пользовательским программам устанавливать свои обработчики INT без полного перехвата вектора.
**Обработчик IM1 (системный):**
При возникновении прерывания Z80 исполняет `RST 38h` → вызов по адресу `#0038`. Реализация в EXP.asm (всегда доступна, т.к. `#0038` в W0):
1. Переключает W3 на SYS_PAGE (BIOS).
2. Проверяет флаг `INT_ID` = `#AA` (установлен при инициализации BIOS).
3. Если флаг совпадает — вызывает пользовательский обработчик, адрес и страница которого хранятся в SYS_PAGE (`INT_ADDRESS`, `INT_PAGE`). Это позволяет программам устанавливать свои ISR без полного перехвата вектора.
4. После возврата восстанавливает W3 и выполняет `EI; RETI`.
**Обработчик IM2 (опционально, для продвинутых программ):**
Sprinter по умолчанию не использует IM2. Исходники содержат шаблон `IM2_INT.asm` для программ, желающих переключиться на IM2:
1. Сохраняет AF, BC, DE, HL, IX, IY и теневые регистры.
2. Включает ПЗУ (`OUT (#7C),A`) — KEYSCAN требует ПЗУ в W0.
3. Вызывает KEYSCAN (опрос AT-клавиатуры).
4. Восстанавливает все регистры, `EI`, `RETI`.
При переходе на IM2 программа сама выбирает значение `I` и размещает таблицу векторов в соответствующем диапазоне памяти.
**Рекомендации:**
- Если программа не использует прерывания — держите W0 в ПЗУ.
- Если программа использует IM1 — можно переопределить обработчик через структуру в SYS_PAGE (флаг `INT_ID`, поля `INT_ADDRESS`/`INT_PAGE`).
- Если программа использует IM2 — переключение полностью на ответственности программы; ядро Sprinter не предоставляет стандартного расположения таблицы векторов.
---
## 4.9 Типовые грабли
- **Данные читаются как `#FF` / не сохраняются** — код или данные попали в незамапленное окно (страница `#FF`). Причина: образ программы меньше 16 КБ, DSS выделил только одну страницу, а код/данные распределены линкером в незамапленные окна. Лечение: убедитесь, что код и данные укладываются в одно окно, или явно выделяйте и мапьте остальные окна через `GETMEM`/`SETWIN1`-`SETWIN3`.
- **Крэш при вызове RST при нестандартном W0** — если перед вызовом W0 содержал произвольную страницу ОЗУ, DSS при `RST 10h` переключит его, но может не восстановить корректно. Решение: перед вызовом RST включите ПЗУ (`OUT (#7C),A`), если не уверены в состоянии W0.
- **Стек в W3 (не-рабочий вариант)** — если стек размещён в W3, при любом дисковом вызове W3 будет переключён на разделяемый буфер DSS, и стековые данные будут забиты. Результат: мгновенное зависание при возврате из функции. Единственное рабочее место для стека — W2.
- **CBL не работает при включённом кеше** — порт `#FB` используется как кеш-память (Fast RAM) и как порт CBL-звука. Если кеш включён, запись в `#FB` уходит в кеш, а не в CBL. Детали: `11-ports.md`, `12-bugs.md`.
- **Прерывание уходит не туда при IM2** — если программа переключилась в IM2 с неинициализированной таблицей векторов, любое прерывание приведёт к исполнению мусора. Решение: используйте IM1 (режим по умолчанию) или инициализируйте таблицу перед переключением.
- **Не путать W3 с «теряется после вызова»** — многие источники утверждают, что DSS сбрасывает W3. На самом деле DSS **сохраняет и восстанавливает** W3 после каждой операции. Стек всё равно нельзя размещать в W3 (см. §4.5), но для данных W3 полностью стабилен между вызовами. Если ваша программа полагается на старый миф — проверьте код.
---
## 4.10 Кросс-ссылки
- Порты страниц и детали регистров: `11-ports.md`
- BIOS (графика, окна): `02-bios.md`
- DSS/ESTEX (EXEC, GETMEM, SETWIN): `03-dss.md`
- Прерывания: `07-irq.md`
- Баги (CBL vs кеш, порт #FB): `12-bugs.md`
- EXE-формат и PSР: `memory/exe-format.md`
+279
View File
@@ -0,0 +1,279 @@
# 5. Графика и видеосистема
## 5.1 Общие сведения
Видеосистема Sprinter Sp2000 реализована в ППЛМ. Видео-ОЗУ — 256 КБ
(512 КБ на некоторых платах [Architecture.txt:17]).
| Параметр | Значение |
|---|---|
| VRAM | 256 КБ, независимая микросхема (не часть RAM SIMM) |
| Физические страницы | `#50..#5F` (16 страниц × 16 КБ), фиксировано в железе |
| Видеорежимы | ZX-40, ZX-80, GR-256-8, GR-16-16 |
| Палитра | 8 палитр × 256 цветов из 16 млн (RGB, BGR) |
| Видеовыход | CGA-монитор, RGB, TV (SCART) |
**Теневая память (Shadow RAM):** Видео-ОЗУ Спринтера является теневой памятью
(§5.4). Программа управляет режимом записи через номер подключаемой страницы
(§5.3): банк `0x50` пишет одновременно в VRAM и в основное ОЗУ; банк `0x54`
пишет только в VRAM, не трогая ОЗУ. На экране **всегда** отображается
содержимое VRAM, независимо от режима записи [IvanMak.txt:317322].
---
## 5.2 Режимы адресации видео-ОЗУ
Видео-ОЗУ может адресоваться двумя способами — **спектрумовским** и
**графическим** [IvanMak.txt:296300]. Спектрумовский режим используется для
Spectrum-совместимого вывода и загрузки знакогенераторов; графический — для
всех остальных видеорежимов (320×256×256, 640×256×16, текст 80×32).
### 5.2.1 Спектрумовский режим адресации
В этом режиме VRAM разбивается на **32 блока по 8 КБ**. Адрес блока задаётся
через порт `RGADR` (`PORT_Y`, `#89`):
| Бит | Назначение |
|-----|-----------|
| 4..0 | Номер блока (0..31) |
| 5 | Не используется |
| 6 | Запрет вывода (1 = отключить спектрумовский вывод) |
| 7 | Разрешение 16-КБ страницы (1 = два блока в `#4000..#7FFF`) |
Бит 0 RGADR вместе с битом 1 порта `#7FFD` определяет чётность подключаемого
блока VRAM: чётный RGADR → блок N в `#4000..#5FFF`, блок N+1 в `#C000..#DFFF`;
нечётный → наоборот [IvanMak.txt:333339].
Для записи в определённую пару спектрумовских блоков:
1. Записать номер блока в `RGADR` (`OUT (#89),A`).
2. Убедиться, что в окне (`#4000` или `#C000`) стоит подходящая страница
основного ОЗУ (любая, кроме `#50..#5F`).
3. Чтение/запись по спектрумовским адресам (`#4000..#5FFF`,
`#C000..#DFFF`) попадает в VRAM через текущий блок.
**Выключение спектрумовского вывода:** Если программа не использует
Spectrum-совместимую графику, `RGADR` рекомендуется установить в `#C0..#FF`
(бит 6 = 1). В этом случае вывод в VRAM через спектрумовский режим не
производится [IvanMak.txt:346348].
### 5.2.2 Графический режим адресации
В графическом режиме видео-ОЗУ организовано как матрица **256 строк
(rows) × 1024 байта** [IvanMak.txt:357358]. Адресация:
```
VRAM_адрес = (RGADR << 10) | (Z80_A[9:0])
где RGADR[7:0] = Y (номер строки, 0..255)
Z80_A[9:0] = X (смещение в строке, 0..1023)
```
Страницы `#50..#5F` при маппинге в любое окно Z80 включают графическую
адресацию. При этом младшие 4 бита номера страницы не влияют на адрес
памяти, а задают подрежим вывода (§5.3).
**Важная особенность:** Z80-адрес внутри окна (биты A[13:10] для окна W3)
**игнорируется** — весь 16-КБ диапазон окна отображает **одну и ту же
строку**, выбранную `RGADR`. Доступ к другим строкам — только через смену
`RGADR`. Это означает, что утверждение из SprinterGraphics programming.txt
о доступе к строкам 18..31 через адреса `#C400..#FC00` **неверно**.
**Структура 1-КБ строки (1024 байта):**
| Смещение в строке | Назначение |
|-------------------|-----------|
| `#000..#13F` (0..319) | Экранная страница 0 (первые 320 байт) |
| `#140..#27F` (320..639) | Экранная страница 1 (следующие 320 байт) |
| `#280..#3FF` (640..1023) | Не используется / данные палитры |
Две экранные страницы (0 и 1) переключаются битом 0 порта `RGMOD` [IvanMak.txt:409].
**Работа с адресацией из программы:**
```asm
; Установить страницу VRAM (0x50) в W3
IN A, (#E2) ; сохранить текущую W3
LD (old_page), A
LD A, #50
OUT (#E2), A ; W3 = VRAM страница 0x50
; Выбрать строку Y
LD A, Y ; Y = 0..255
OUT (#89), A ; RGADR = Y
; Чтение/запись пикселя (X,Y) для экранной страницы 0
LD HL, #C000 + X ; X = 0..319
LD (HL), colour ; запись байта пикселя
; Восстановить страницу
LD A, (old_page)
OUT (#E2), A
```
---
## 5.3 Подрежимы вывода (страницы `#50..#5F`)
Биты 2 и 3 номера страницы (из диапазона `#50..#5F`) задают подрежимы вывода
[IvanMak.txt:364386]:
| Бит | Страницы | Эффект |
|-----|----------|--------|
| 3 | `#58..#5F` | **Прозрачный цвет** — запись байта `#FF` игнорируется (для спрайтов) |
| 2 | `#54..#57`, `#5C..#5F` | **Без тени** — запись только в VRAM, не в основное ОЗУ (для курсора мыши) |
Биты 0 и 1 должны быть 0 для совместимости с будущими прошивками
[IvanMak.txt:384386].
---
## 5.4 Теневая память (Shadow RAM) и двойная запись
Видео-ОЗУ Спринтера является теневой памятью по отношению к основному ОЗУ
[IvanMak.txt:317322]. Видеоконтроллер **всегда** читает изображение из VRAM
для вывода на экран; основное ОЗУ в формировании изображения не участвует.
Однако при записи через графические банки `#50..#5F` поведение зависит от
выбранного подрежима:
- **Банк `0x50` (normal):** запись идёт **одновременно** в VRAM и в
соответствующую область основного ОЗУ. Чтение из того же Z80-адреса
возвращает данные из основного ОЗУ, а не из VRAM — VRAM для процессора
на чтение не видна.
- **Банк `0x54` (noshadow):** запись — только в VRAM, основное ОЗУ не
изменяется. Чтение — из основного ОЗУ (там хранится «фон»).
- **Банк `0x58` (transparent):** как normal, но байт `#FF` не пишется.
- **Банк `0x5C` (sprite):** transparent + noshadow.
- **Обычные банки (не `#50..#5F`):** никакого доступа к VRAM нет,
чтение/запись идут напрямую в основное ОЗУ.
**Зачем это нужно:**
1. **Восстановление после переключения видеорежима:** данные в основном ОЗУ
остаются нетронутыми, так что при возврате в графический режим экран можно
восстановить без перерисовки [IvanMak.txt:325].
2. **Heal спрайтов** (libbgi): фон, на который был выведен спрайт в банке
`GFX_BANK_SPRITE` (`0x5C`, без тени), остался в основном ОЗУ. После
перемещения спрайта фон копируется из основного ОЗУ обратно в VRAM
(операция heal), стирая спрайт.
3. **Курсор мыши** без сохранения/восстановления: временный вывод через
`GFX_BANK_NOSHADOW` (`0x5C`) — в основном ОЗУ данные под курсором не
портятся, достаточно при следующем обновлении просто перерисовать фон.
**Режимы записи (см. §5.3):**
| Режим | Пишет в VRAM | Пишет в осн. ОЗУ | Применение |
|-------|:---:|:---:|-----------|
| `0x50` (normal) | Да | Да | Обычный вывод |
| `0x54` (noshadow) | Да | **Нет** | временные эффекты |
| `0x58` (transparent) | Да (кроме `#FF`) | Да | Спрайты с прозрачностью без heal |
| `0x5C` (sprite) | Да (кроме `#FF`) | **Нет** | Курсор, спрайты (прозрачность + heal) |
---
## 5.5 Видеорежимы
Система поддерживает независимую установку режима для каждого квадратика
8×8 (или 16×8 для 640-режимов) на экране [IvanMak.txt:405469].
### Режимы вывода
| Режим | Разрешение | Цветов | Описание |
|-------|-----------|--------|----------|
| ZX-40 | 320×256 | 16 | Текстовый 40×32, Spectrum-совместимый |
| ZX-80 | 640×256 | 16 | Текстовый 80×32, до 36 знакогенераторов |
| GR-256-8 | 320×256 | 256 | Графический, 1 байт/пиксель |
| GR-16-16 | 640×256 | 16 | Графический, 4 бита/пиксель |
### Формат пикселей
**GR-256-8 (320×256×256):** один байт = один пиксель. Строка содержит
320 байт для экранной страницы 0 (смещения `#000..#13F`) и 320 байт для
страницы 1 (`#140..#27F`). Полный экран: 320 × 256 = 81 920 байт на страницу.
**GR-16-16 (640×256×16):** один байт = два пикселя: младший полубайт —
левый/первый пиксель, старший — правый/второй. Строка содержит 320 байт на
экранную страницу, что даёт 640 пикселей (320 × 2). Структура строки та же:
320 байт на страницу 0, 320 на страницу 1.
### Установка режима
Режим задаётся 4 байтами `Mode0..Mode3` в области режимов VRAM
(`#0300..#039F`). Полное описание формата Modei — [IvanMak.txt:448469].
Для переключения видеорежима из программ используется функция DSS `SETVMOD`
(50h) [DiskSyscalls.txt:345349]:
| A (режим) | Описание |
|-----------|----------|
| 02h | Текстовый 40×32×16 |
| 03h | Текстовый 80×32×16 |
| 81h | Графический 320×256×256 |
| 82h | Графический 640×256×16 |
B — страница экрана (0/1). Вызов: `RST 10h` с C=50h. Флаг C = ошибка
(например, режим не поддерживается данным монитором).
---
## 5.6 Палитра
8 палитр по 256 цветов, каждая занимает 1 КБ в VRAM по адресам
`#03E0..#03FE` [IvanMak.txt:540568].
| Адреса | Палитра | Назначение |
|--------|---------|-----------|
| `#03E0..#03E2` | Граф. 0 | Графические режимы |
| `#03E4..#03E6` | Граф. 1 | —//— |
| `#03E8..#03EA` | Граф. 2 | —//— |
| `#03EC..#03EE` | Граф. 3 | —//— |
| `#03F0..#03F2` | Текст. 4 | Цвет бумаги |
| `#03F4..#03F6` | Текст. 5 | Цвет символа |
| `#03F8..#03FA` | Текст. 6 | Цвет мерцания бумаги |
| `#03FC..#03FE` | Текст. 7 | Цвет мерцания символа |
Каждый четвёртый байт в каждой палитре не используется — не может попасть
на ЦАП [IvanMak.txt:545547]. Выбор палитры для каждого квадратика — через
биты 7..6 байта Mode1 [BIOS_v3.txt:348].
> **Примечание:** Architecture.txt упоминает всего 5 палитр (4 графические +
> 1 текстовая), но IvanMak описывает все 8: 4 графические (0–3) и 4 текстовые
> (4–7: бумага, символ, мерцание бумаги, мерцание символа)
> [IvanMak.txt:547548, 561568].
---
## 5.7 Структура VRAM в терминах BIOS
При использовании BIOS распределение VRAM [IvanMak.txt:387404]:
| Адреса (линии) | Назначение |
|----------------|-----------|
| `#0000..#003F` | Спектрумовский экран (блоки 0..1) |
| `#0040..#017F` | Первый графический экран (блоки 2..11) |
| `#0180..#02BF` | Второй графический экран (блоки 12..21) |
| `#02C0..#02FF` | Знакогенераторы текстового режима |
| `#0300..#039F` | Область описания режимов экрана |
| `#03A0..#03DF` | Зарезервировано |
| `#03E0..#03FF` | Палитра |
---
## 5.8 Заблуждения и неподтверждённое
**Миф о строках 18–31:** В документе SprinterGraphics programming.txt
утверждается, что после выбора строки Y через RGADR, по адресам
`#C400..#FC00` доступны строки Y+1..Y+15 (с шагом 1 КБ). В эмуляторе MAME
это **не работает** — весь 16-КБ диапазон страницы `#50..#5F` отображает
только строку Y, независимо от бит A[13:10] адреса Z80. Требуется проверка
на реальном Sprinter Sp2000.
---
## 5.9 Кросс-ссылки
- Палитра и видеорежимы BIOS: `02-bios.md §2.7`
- SETVMOD (DSS): `03-dss.md §3.10`
- Акселератор: `06-accel.md`
- Быстрое ОЗУ и конфликт с акселератором: `04-memory.md §4.6`
- Порты RGADR/RGMOD: `11-ports.md`
- Баги акселератора: `12-bugs.md`
- Проектные документы: `sprite-api-design.md`, `accel-fill-budget.md`
+221
View File
@@ -0,0 +1,221 @@
# 6. Акселератор
## 6.1 Общие сведения
Акселератор — внутреннее устройство, реализованное в ППЛМ. Присутствует почти
во всех конфигурациях Sprinter (Sprinter-2 и выше). Предназначен для ускорения
пересылки блоков данных между RAM и VRAM (до физического предела скорости ОЗУ).
Не поддерживает ПЗУ и Fast RAM [accelerator_doc.txt:5].
**Возможности:**
- Быстрая заливка горизонтальной или вертикальной линии экрана (1..256 пикселей)
одним цветом; в режиме 640×256 — линия 1..512 пикселей.
- Быстрое копирование горизонтальной или вертикальной линии (1..256 пикселей).
- Логические операции AND, OR, XOR с блоками данных.
**Внутренняя память:** 256 байт в ППЛМ. Данные загружаются в эту память,
затем копируются в выбранный участок RAM/VRAM. Операция копирования может
повторяться многократно — это позволяет заполнять экран текстурой без
перезагрузки данных.
**Автоматический инкремент адреса:**
- **Горизонтальные** операции (`LD L,L` — копирование, `LD C,C` — заливка):
акселератор автоматически инкрементирует регистр HL (адрес приёмника) на
каждом шаге. Для копирования источник (DE) также инкрементируется.
- **Вертикальные** операции (`LD A,A` — копирование, `LD E,E` — заливка):
акселератор автоматически меняет порт `RGADR` (`#89`) — координату Y.
После завершения операции `RGADR` содержит новое значение
(Y + размер_блока, если не было переполнения).
- **Комбинированный** режим (горизонтальный источник + вертикальный приёмник):
сначала загружается байт из последовательных данных, затем выполняется
вертикальная запись. `RGADR` инкрементируется; регистр-источник (DE)
тоже инкрементируется — можно последовательно читать спрайт и рисовать
его вертикальными строками (§6.3, пример «Спрайт»).
## 6.2 Команды
Акселератор управляется специальными инструкциями Z80, которые выполняются
как NOP обычным процессором, но распознаются ППЛМ [IvanMak.txt:810848].
| Инструкция | Код | Действие |
|-----------|:---:|----------|
| `LD B,B` | `#40` | **Выключить** акселератор |
| `LD D,D` | `#52` | **Включить** + режим «задать размер блока» |
| `LD A,size` | `#3E size` | Установить размер блока (сразу после `LD D,D`) |
| `LD C,C` | `#4F` | **Горизонтальная заливка** одним байтом |
| `LD E,E` | `#5B` | **Вертикальная заливка** одним байтом |
| `LD L,L` | `#6D` | **Горизонтальное копирование** блока |
| `LD A,A` | `#7F` | **Вертикальное копирование** блока |
| `LD H,H` | `#64` | Зарезервировано |
**Логические операции** (AND, OR, XOR) выполняются командами
`AND (HL)`, `OR (HL)`, `XOR (HL)` после загрузки блока в память акселератора.
## 6.3 Примеры
### Копирование экрана (страница 0 → страница 1)
```asm
; страница VRAM уже открыта по адресу #C000
LD HL, #C000 ; адрес начала линии первого экрана
LD DE, #C140 ; адрес начала линии второго экрана
LD BC, #140 ; длина строки по горизонтали (320)
DI ; запретить прерывания
LD D,D ; включить акселератор: режим размера блока
LD A, 0 ; размер блока = 256 байт (0 = 256)
LD A,A ; вертикальное копирование
LDIR ; копировать
LD B,B ; выключить акселератор
EI ; разрешить прерывания
```
Время исполнения: ~1.2 прерывания (~24–26 мс на полный экран)
[IvanMak.txt:822823], [accelerator_doc.txt:37].
### Заливка блока одним цветом
```asm
LD DE, #C000 + X ; X — смещение по горизонтали
LD A, Y
OUT (#89), A ; RGADR = Y
DI
LD D,D ; режим размера блока
LD A, width ; ширина блока
LD B,B ; стоп
LD A, colour ; цвет заливки
LD C,C ; горизонтальная заливка
LD (DE), A ; залить
LD B,B ; стоп
EI
```
### XOR блока данных
```asm
LD HL, addr_1
LD DE, xor_data
DI
LD D,D
LD A, 0 ; 256 байт
LD L,L ; горизонтальное копирование
LD A, (DE) ; загрузить блок в память акселератора
XOR (HL) ; XOR с данными в RAM
LD (HL), A ; сохранить результат
LD B,B ; стоп
EI
```
### Копирование вертикальной линии с разделением load/write
Загрузка байта в буфер акселератора и запись из буфера в VRAM разнесены —
между ними можно выполнять произвольный код (например, сменить Y).
Буфер акселератора и размер блока **сохраняются** между включениями и
выключениями (`LD B,B`), что позволяет гибко разделять чтение
и запись.
```asm
LD HL, #C000 + X1 ; источник
LD DE, #C000 + X2 ; приёмник
DI
LD D,D ; акселератор вкл, режим размера
LD A, size ; размер блока
LD B,B ; выключить (размер запомнен)
LD A, Y1
OUT (#89), A ; RGADR = Y1
LD A,A ; вертикальное копирование
LD A, (HL) ; загрузить байт из (HL) в буфер акселератора
LD B,B ; выключить (буфер запомнен)
LD A, Y2
OUT (#89), A ; RGADR = Y2 (смена Y между load и write)
LD A,A ; вертикальное копирование
LD (DE), A ; записать буфер в (DE) — акселератор пишет
; вертикальную линию size пикселей
LD B,B ; выключить
EI
```
### Спрайт вертикальными строками из последовательных данных
Спрайт хранится как последовательность байт вертикальных линий. Для каждой
колонки спрайта: загружается байт из последовательных данных, затем
акселератор пишет его вертикально (автоинкремент `RGADR`). После завершения
колонки — сдвиг по X (`INC HL`) и повтор.
```asm
LD DE, sprite_data ; DE → последовательные данные спрайта
LD HL, #C000 + X_COORD ; HL → начальная позиция на экране
LD B, sprite_width ; ширина спрайта в колонках
LD D,D ; акселератор вкл, режим размера
LD A, sprite_height ; высота одной вертикальной линии
LD B,B ; акселератор выключили (размер акселератор запомнил)
row:
LD A, Y_COORD
OUT (#89), A ; RGADR = стартовая Y
DI
LD L,L ; режим «загрузить из последовательных данных»
LD A, (DE) ; загрузить байт спрайта в буфер акселератора
LD A,A ; вертикальное копирование
LD (HL), A ; записать — акселератор копирует байт
; вертикально, RGADR++ на каждом шаге
LD B,B ; выключить акселератор
EI
INC HL ; следующая колонка по X
DJNZ row
```
## 6.4 Скорость
Время работы акселератора ограничено только физической скоростью ОЗУ
[IvanMak.txt:840843]:
```
Время ≈ время команды без акселератора + число_байт / 7 000 000 (сек)
```
## 6.5 Комбинированный режим: общая схема
Общий паттерн для работы с акселератором в комбинированном режиме
(горизонтальный источник + вертикальный приёмник):
```
LD D,D ; 1. Включить акселератор, режим размера
LD A, size ; 2. Задать размер блока (1..256)
LD L,L ; 3. Режим «горизонтальный источник»
<чтение байта> ; 4. Загрузить байт в буфер акселератора (LD A,(DE) и т.п.)
LD A,A ; 5. Режим «вертикальный приёмник»
<запись байта> ; 6. Записать — акселератор делает вертикальное копирование
; с автоинкрементом RGADR и приёмника
LD B,B ; 7. Выключить акселератор
```
После шага 6 `RGADR` содержит Y + size, HL — X + 1 (если был `INC HL`).
## 6.6 Прерывания
**Старая прошивка:** DI обязателен на время работы акселератора — система
команд Z80 сильно меняется, и ISR не сможет корректно выполниться
[IvanMak.txt:844846].
**Новая прошивка:** акселератор может работать с EI. По приходу прерывания
он отключается, а по `RETI` включается обратно [IvanMak.txt:846848].
Использовать этот режим следует с осторожностью — без проверки на реальном
железе неизвестно, какая версия прошивки установлена.
---
## 6.7 Кросс-ссылки
- Баги акселератора: `12-bugs.md`
- Использование акселератора в libbgi: `sprite-api-design.md`
- Ограничение скорости: `accel-fill-budget.md`
- Fast RAM и конфликт с портом CBL: `04-memory.md §4.6`
+92
View File
@@ -0,0 +1,92 @@
# 7. Прерывания (IRQ)
## 7.1 Общие сведения
Sprinter Sp2000 поддерживает режимы прерываний IM1 и IM2.
### Источники прерываний
| Источник | Происхождение | Описание |
|----------|---------------|----------|
| Кадровые (INT) | Видеоконтроллер | Квадратик экрана в 8-й линии |
| CTC | Z84C15 | Таймерные прерывания |
| SIO | Z84C15 | AT-клавиатура |
| PIO | Z84C15 (бит 0 порта B, `#1F`) | ISA-слоты |
| CBL | COVOX-Blaster | Запрос данных |
### Различение источников
Кадровые и клавиатурные прерывания приходят с вектором `0FFh` [Parinov.txt:956]. Способ различения:
- Клавиатура выставляет бит 0 в порте `19h` — признак приёма байта [Parinov.txt:1049]
- Если бит 0 порта `19h` не установлен — прерывание кадровое
> **Примечание:** [Parinov.txt:956] описывает альтернативный метод — «отличать по биту приема байта в порте клавиатуры». Оба метода сводятся к проверке бита 0 в порте `19h`.
### Выбор режима прерываний (IM1 vs IM2)
| Режим | Контекст | Источник |
|-------|----------|----------|
| IM2 | Рекомендован для работы с BIOS через RST | IvanMak.txt:14691473 |
| IM1 | Используется DSS для своих обработчиков | — |
| IM1 | Fast-RAM секция предполагает обработчик по `#0038` | Parinov.txt:601 |
| IM2 | Рекомендован при выставленных прерываниях от CBL | Parinov.txt:762763 |
> **Примечание:** разные подсистемы рекомендуют разные режимы. IM2 предпочтителен при интенсивной работе с прерываниями (CBL, BIOS). IM1 используется DSS по умолчанию.
## 7.2 Кадровые прерывания (INT)
Генерируются в момент прохождения 8-й строки квадратика (настраивается через бит 0 порта `#FE`). Положение INT подстраивается под стандарт Pentagon (320 строк) или Scorpion (312 строк).
> **Мышь — есть ли аппаратные прерывания?** [Parinov.txt:956] утверждает: «От мыши прерывания не приходят. Сделать можно, но сейчас их нет.» [ProgrammerManual.txt:17841786] описывает драйвер мыши, где «каждое нажатие и отпускание клавиш или перемещение мыши вызывает прерывание», и документирует функцию «80h MOUSE HARDWARE INTERRUPT» [ProgrammerManual.txt:19301937]. Parinov (разработчик драйвера) отрицает их наличие — возможно, ProgrammerManual описывает планируемую функциональность.
## 7.3 CTC (Z84C15)
Таймер CTC используется для:
- Генерации периодических прерываний
- Задания частоты CBL (COVOX-Blaster)
- Синхронизации звука
## 7.4 SIO (Z84C15)
Внутренний SIO Z84C15 обслуживает:
- AT-клавиатуру (порты `#18`/`#19`)
- RS-232 (для мыши и других последовательных устройств)
## 7.5 IM1 — вектор #0038
Стандартный Z80-режим. Все INT ведут на адрес `#0038`. Используется DSS для своих обработчиков.
## 7.6 IM2 — таблица векторов
IM2 использует таблицу векторов (256 байт) в фиксированной странице (обычно W3). Вектор прерывания — `0FFh`.
Для работы IM2 требуется:
1. Таблица векторов в резиденте (W3)
2. Регистр I = старший байт адреса таблицы
3. Каждый вектор — адрес обработчика (2 байта)
## 7.7 Акселератор и прерывания
При работе акселератора система команд Z80 сильно меняется.
| Режим | Описание | Источник |
|-------|----------|----------|
| Старый | DI обязателен | IvanMak.txt:844846, Parinov.txt:954 |
| Новый | EI/RETI: по INT акселератор отключается, по RETI включается | IvanMak.txt:846848 |
> **Примечание:** IvanMak описывает два режима (старый — DI, новая прошивка — EI/RETI). Parinov знает только старый. Без тестирования на железе неизвестно, какая версия прошивки установлена.
## 7.8 Цепочка обработчиков
TODO (проектный `im2_isr_design.md`): цепочка irq_install/remove, FPS-делитель, обработчики клавиатуры.
## 7.9 Кросс-ссылки
- Порты прерываний: `11-ports.md`
- Проектный документ по IM2/ISR: `im2_isr_design.md`
- Клавиатура: `09-input.md §9.1`
- CBL (прерывания): `10-sound.md §9.4.6`
+197
View File
@@ -0,0 +1,197 @@
# 8. Ввод-вывод
## 8.1 Клавиатура и мышь
Клавиатура и мышь вынесены в отдельный документ: `09-input.md`.
Сводная информация по портам и прерываниям — `09-input.md §§9.19.6`.
## 8.2 Звук
Звуковая подсистема (AY-3-8910, COVOX, COVOX-Blaster, бипер) вынесена
в отдельный документ: `10-sound.md`.
Сводная карта звуковых портов: `11-ports.md §11.6`.
## 8.3 Дисковая подсистема
### 8.3.1 FDD (WD1793 / КР1818ВГ93)
Контроллер дисковода — стандартный Beta Disk Interface (WD1793).
Аппаратное перенаправление `#1F → #0F` (см. §8.4).
**Регистры (по данным MAME sprinter.cpp):**
| Z80-адрес | Регистр |
|-----------|---------|
| `#0F` | Status (чт) / Command (зп) |
| `#1F` | —//— (аппаратный ремап на `#0F`) |
| `#3F` | Track |
| `#5F` | Sector |
| `#7F` | Data |
| `#FF` | Drive Control (выбор дисковода) |
**Противоречие: адресация регистров ВГ93.**
> - [IvanMak.txt:12701274]: 4 регистра + `#FF` — управление дисководом.
> - [Parinov.txt:963]: 3 регистра (data-регистр `#7F` пропущен).
>
> MAME реализует полный набор по IvanMak: Status/Command (`#0F`), Track (`#3F`),
> Sector (`#5F`), Data (`#7F`). Data-регистр важен — через него читаются/пишутся
> сектора. Parinov, вероятно, упустил его.
**Turbo-режим FDD (DCP 0x16/0x17, биты):**
- Бит 0 = скорость (0=норм, 1=турбо)
- Бит 1 = отключение FDD [sprinter.cpp:914920]
### 8.3.2 HDD (IDE/ATA)
Два ATA-интерфейса: первичный и вторичный. Доступ — через DCP-декодер
(внутренние коды 0x20–0x2b).
**Регистры ATA CS0 (по sprinter.cpp):**
| DCP код | Z80-адрес (старш.) | Регистр |
|---------|-------------------|---------|
| 0x20 | `#xx50` | Data (16-bit через защёлку байтов) |
| 0x21 | `#xx51` | Error (чт) / Features (зп) |
| 0x22 | `#xx52` | Sector Count |
| 0x23 | `#xx53` | Sector Number |
| 0x24 | `#xx54` | Cylinder Low |
| 0x25 | `#xx55` | Cylinder High |
| 0x26 | `#xx56` | Device/Head |
| 0x27 | `#xx57` | Status (чт) / Command (зп) |
| 0x28 | `#xx58` | Alt Status (чт) / Device Control (зп) (CS1) |
| 0x2a | HDD1 select (вторичный) |
| 0x2b | HDD2 select (первичный) |
**16-битный data-порт:** MAME реализует защёлку байтов: чтение `#xx50`
возвращает младший байт (bit8=0), затем старший (bit8=1) [sprinter.cpp:958972].
**Противоречие: HDD — разные адреса для чтения и записи.**
> - [IvanMak.txt:1366]: кратко: «#xx50..#xx55 порты HDD»
> - [Parinov.txt:958959]: таблица с разными адресами для R/W:
> `0050h` (чт) / `0150h` (зп) — data, `4053h` (чт) / `4153h` (зп) — status/command.
>
> MAME использует единую карту DCP-кодов (0x20–0x28) без разделения R/W.
> Parinov описывает реальное поведение на Sprinter-2, где адресная линия A8
> управляет направлением — это внешняя особенность конкретной ППЛМ-прошивки.
> IvanMak упрощает до базовых DCP-кодов.
Доступ к IDE открыт в режиме Sprinter-ZX, в режимах Pentagon/Scorpion — закрыт.
### 8.3.3 CMOS (Dallas DS12887A)
Микросхема DS12887A (RTC + 114 байт NV RAM). Совместима с MC146818A.
Доступ через DCP-коды 0x1c0x1e.
| Z80-адрес | DCP код | Функция |
|-----------|---------|---------|
| `#DFBD` | 0x1d | Запись адреса регистра CMOS |
| `#FFBD` | 0x1c | Чтение данных из выбранного регистра |
| `#BFBD` | 0x1e | Запись данных в выбранный регистр |
При отсутствии микросхемы CMOS эмулируется BIOS [BIOS_v3.txt:308310].
**Карта регистров RTC (совместимо с MC146818A):**
| Рег. | Назначение | Формат |
|------|-----------|--------|
| 0x00 | Секунды | BCD |
| 0x01 | Секунды (будильник) | BCD |
| 0x02 | Минуты | BCD |
| 0x03 | Минуты (будильник) | BCD |
| 0x04 | Часы | BCD |
| 0x05 | Часы (будильник) | BCD |
| 0x06 | День недели (1=воскресенье) | BCD |
| 0x07 | Число | BCD |
| 0x08 | Месяц | BCD |
| 0x09 | Год | BCD |
| 0x0a | Регистр A (UIP, divider, rate) | биты |
| 0x0b | Регистр B (SET, PIE, AIE, UIE, SQWE, DM, 24/12, DSE) | биты |
| 0x0c | Регистр C (IRQF, PF, AF, UF) | биты (чт) |
| 0x0d | Регистр D (VRT) | биты (чт) |
| 0x0e0x7f | 114 байт NV RAM | пользовательские |
---
## 8.4 Джойстик
**Два Kempston-совместимых джойстика** [sprinter.cpp:11061175]:
- **JOY1** — через CIO_DTRB (DCP 0x15, совмещён с Beta state при чтении)
- **JOY2** — через PIO PB
**Формат данных (Kempston):**
| Бит | Кнопка |
|:---:|--------|
| 0 | Вправо |
| 1 | Влево |
| 2 | Вниз |
| 3 | Вверх |
| 4 | Fire |
| 5 | — |
| 6 | — |
| 7 | — |
**Противоречие: джойстик #1F vs #0F.**
> - [IvanMak.txt:1344]: «#1F,#0F — RD_KEMPS — порт джойстика.
> В Sprinter-1 порт #1F аппаратно переадресуется на порт #0F
> - [IvanMak.txt:861868]: команды `OUT (#1F),A` не срабатывают
> из-за перенаправления. Надо использовать `LD BC,#1F : OUT (C),A`.
>
> В Sprinter-1 включено аппаратное перенаправление `#1F → #0F`,
> из-за чего `IN A,(#1F)` не читает джойстик. MAME подтверждает:
> на Sp2000 (`m_cnf` bit 0) перенаправление отключается
> [sprinter.cpp:1106]. Ремап действует только на запись: при OUT
> порт `#1F` маппится на `#0F` через DCP [sprinter.cpp:1080].
JOY2 читается через PIO port A [sprinter.cpp:11361140].
---
## 8.5 Параллельный порт (принтер)
Centronics-совместимый через ППЛМ. BIOS-функции печати: 082h–08Ch
(см. `02-bios.md`).
---
## 8.6 Шина ISA
Два 8-битных ISA-слота на частоте X_SP/5 (~7 МГц). Доступ — через
DCP-декодер и страницы памяти.
**Управляющие порты ISA (DCP 0xD00xDF, 0xF80xFF):**
| Бит | Назначение |
|:---:|-----------|
| 2 | 0 = ISA memory access, 1 = ISA I/O access |
| 1 | Выбор слота (0 = slot 0, 1 = slot 1) |
Адресное расширение: `m_isa_addr_ext` (запись через DCP 0x1b) — биты
расширения адреса для ISA-циклов [sprinter.cpp:10321039].
**Memory-маппинг ISA:** При pg3 & 0xF9 == 0xD0 (бит 10 окна W3 = 1,
BANK_ISA_MASK) обращения к окну W3 (`#C000#FFFF`) направляются на ISA-шину
вместо RAM [sprinter.cpp:178181, 530532]. Длина — 16 КБ.
**I/O-доступ к ISA:** через тот же DCP-декодер. Устройства устанавливают
сигнал `m_io_input_wait` = true при совпадении адреса.
> **Примечание:** В MAME есть TODO: «ISA memory slots» — ISA memory-циклы
> пока не эмулируются [sprinter.cpp:576]. На реальном Sprinter поддержка
> зависит от прошивки ППЛМ.
Слот 0 обычно занят ZX-Bus-адаптером [sprinter.cpp:26].
---
## 8.7 Кросс-ссылки
- BIOS-функции: `02-bios.md`
- Клавиатура, мышь: `09-input.md`
- Звук: `10-sound.md`
- Порты: `11-ports.md`
- Акселератор: `06-accel.md`
- Прерывания: `07-irq.md`
+595
View File
@@ -0,0 +1,595 @@
# 9. Клавиатура и мышь
## 9.1 Общие сведения
Клавиатура Sprinter — AT-совместимая (101 key, PS/2). В Sprinter-1
дополнительно присутствует ZX-клавиатурная матрица через порт `#FE`
(скан-код через AT-контроллер преобразуется в ZX-матрицу). Sprinter-2+
использует AT-клавиатуру через внутренний SIO Z84C15 (канал A).
Мышь — Microsoft Serial Mouse (2 кнопки) через RS-232 на SIO-B (канал B
Z84C15).
### Порты SIO
| Порт | Назначение |
|------|-----------|
| `#18` | SIO-A data — клавиатура |
| `#19` | SIO-A control — статус/управление |
| `#1A` | SIO-B data — мышь |
| `#1B` | SIO-B control — мышь |
Бит 0 порта `#19` — признак прерывания от клавиатуры (используется в IM2
для различения источников с общим вектором `#FF`).
### Прерывания
Клавиатура и мышь работают в IM2, общий вектор `#FF` (совместно с
кадровым и CBL). Различение источника — по портам SIO и флаговым битам
(`07-irq.md §7.2`).
---
## 9.2 Клавиатура: BIOS и ESTEX
### FN_KBD_OUT (BIOS $EA)
Отправляет байт непосредственно на AT-клавиатуру [BIOS_v3.txt:0EAh].
```
Вход: C = $EA
A = байт команды/данных для AT-клавиатуры
Выход: нет
```
Используется для низкоуровневой настройки клавиатуры (LED-индикаторы,
повтор, задержка).
### ESTEX: функции клавиатуры (30h–37h)
Через `RST 10h` с C=30h37h [DiskSyscalls.txt:30h37h].
**WAITKEY (30h):** ждёт нажатия, возвращает код.
```
Вход: C = 30h
Выход: A = код символа
D = скан-код
E = ASCII-код (0 для функциональных клавиш)
```
**SCANKEY (31h):** опрос без ожидания.
```
Вход: C = 31h
Выход: A = код символа (0 если нажатий нет)
D = скан-код
E = ASCII-код
```
**ECHOKEY (32h):** ждёт + эхо-вывод.
```
Вход: C = 32h
Выход: A = код символа
D = скан-код
E = ASCII-код
```
**CTRLKEY (33h):** состояние модификаторов и режимов. libc: `kbd_mod_state()`.
```
Вход: C = 33h
Выход: C = mode (биты режимов — см. ниже)
B = shift (биты зажатых модификаторов)
```
| Бит C | Режим |
|-------|-------|
| 0 | РУС/ЛАТ (0=ЛАТ, 1=РУС) |
| 1 | NumLock |
| 2 | ScrollLock |
| 3 | CapsLock |
| 4 | Insert |
| Бит B | Модификатор |
|-------|------------|
| 0 | Left Shift |
| 1 | Right Shift |
| 2 | Ctrl (любой) |
| 3 | Alt (любой) |
| 4 | Left Ctrl |
| 5 | Left Alt |
| 6 | Right Ctrl |
| 7 | Right Alt |
**K_CLEAR (35h):** сбросить буфер клавиатуры.
```
Вход: C = 35h
```
**K_SETUP (36h):** настройка раскладки. [DiskSyscalls.txt:36h]
```
Вход: C = 36h
A = 0 — получить текущий номер раскладки
A = 1 — установить раскладку по номеру из B
A = 2 — получить имя текущей раскладки (адрес HL)
Выход: при A=0: A = номер раскладки
при A=2: HL = адрес строки с именем
```
| Номер | Раскладка |
|-------|----------|
| 0 | Английская (US) |
| 1 | Русская |
| 2 | Английская (UK) |
| 3 | Украинская |
Переключение раскладки по Ctrl+Space (если не переназначено).
**TESTKEY (37h):** тест на нажатие (без извлечения из буфера).
```
Вход: C = 37h
Выход: A = код символа (0 если нет)
D = скан-код
E = ASCII-код
```
---
## 9.3 Клавиатура: необработанный (raw) доступ
Штатный DSS (WAITKEY/SCANKEY/CTRLKEY) даёт событийный интерфейс —
нажатия, а не состояния клавиш. Для игр и real-time приложений требуется
чтение **зажатых клавиш в каждый момент**. Это реализуется через
перехват SIO-A напрямую в обработчике IM2, минуя DSS.
### 9.3.1 Принцип
1. Установить свой обработчик IM2 (общий вектор `#FF`).
2. В обработчике читать байты с SIO-A (порт `#18`), декодировать
PS/2 Scan Code Set 2, обновлять битовую карту held-состояний.
3. Основной код по кадровому прерыванию (или раз в кадр) читает карту
опросом — без ожидания.
4. DSS не получает байт с клавиатуры, пока обработчик их забирает.
Для восстановления нормального ввода — временно отключить перехват.
### 9.3.2 SIO-A и прерывания
SIO-A (порт данных `#18`, управление `#19`) работает в IM2.
**Инициализация SIO-A** (выполняется BIOS при загрузке, повторно не требуется):
| WR | Значение | Эффект |
|:--|:---------|--------|
| WR0 | `0x10` | Сброс RR1 (ошибка) |
| WR1 | `0x07` | Прерывания по Rx готовности |
| WR3 | `0xC1` | Rx 8 бит, без CRC |
| WR4 | `0x44` | ×1 клок, 1 стоп-бит, без паритета |
| WR5 | `0xEA` | Tx 8 бит, DTR+RTS=1 |
**Различение в IM2:** при входе в обработчик проверить бит 0 порта `#19`.
Если 0 — прерывание не от SIO-A (кадровое, CBL — пропустить).
### 9.3.3 PS/2 Scan Code Set 2 — протокол
SIO-A передаёт скан-коды PS/2 Set 2. Каждый байт с порта `#18`
один байт протокола. Размер сообщения:
| Последовательность | Значение |
|:------------------|:---------|
| `<code>` | Make — клавиша нажата (код < 0xF0) |
| `0xF0 <code>` | Break — клавиша отпущена |
| `0xE0 <code>` | Extended — расширенная клавиша (стрелки, RAlt, RCtrl) |
| `0xE0 0xF0 <code>` | Extended break |
Make-байт устанавливает бит клавиши в held-карте.
Break-байт сбрасывает.
**FSM декодирования (2 состояния):**
```
PEND_MAKE:
получить байт B
if B == 0xF0 → переход PEND_BREAK
if B == 0xE0 → переход PEND_EXT, Bзап = B
иначе → make(B)
PEND_BREAK:
получить байт B
переход PEND_MAKE
if (ext_flag) → break_ext(B); ext_flag = 0
иначе → break(B)
PEND_EXT:
получить байт B
if B == 0xF0 → ext_flag = 1, переход PEND_BREAK_EXT
иначе → make_ext(B); переход PEND_MAKE
PEND_BREAK_EXT:
получить байт B
ext_flag = 0
break_ext(B)
переход PEND_MAKE
```
### 9.3.4 Битовая карта held-состояний
Массив 128 байт (1024 бита для кодов 0..1023, расширенные — с битом
`0x0100`).
Простая реализация:
```asm
; HL = code (0..0x1FF)
; IX = base адреса карты
LD A, H
AND A, 0x07 ; номер бита 8..10
LD D, A
LD A, L
RRCA ; HL/8 (3 сдвига)
RRCA
RRCA
AND A, 0x1F
LD L, A
LD A, H
AND A, 0x07
ADD A, A ; +8*...
ADD A, A
ADD A, A
ADD A, L
LD L, A
LD H, 0x00
ADD IX, HL ; IX = &bitmap[code / 8]
LD A, D
INC A
LD B, A ; B = номер бита в байте + 1
XOR A
SCF ; carry=1 для make, 0 для break
RL A
DJNZ $-2 ; A = 1 << (code & 7)
; make: OR (IX), A ; break: CPL + AND (IX), A
```
### 9.3.5 Overrun recovery
SIO FIFO глубиной 3 байта. Если обработчик прерывания не успевает
забрать байты (длинная DI-секция, акселератор), FIFO переполняется —
RR1 bit5 = 1. С этого момента цепочка make+break нарушена, возможны
залипшие клавиши.
**Алгоритм восстановления:**
1. Обнаружить overrun (читать SIO RR1, бит 5).
2. Сбросить held-карту **всех клавиш, кроме**:
| Код PS/2 | Клавиша | Почему |
|:--------:|:--------|:-------|
| `0x12` | Left Shift | PS/2 автоповтор — только последняя |
| `0x59` | Right Shift | нажатая клавиша. Если сбросить |
| `0x14` | Left Ctrl | модификатор, он не восстановится до |
| `0x11` | Left Alt | физического отпускания. Игра «теряет» |
| `0xE014` | Right Ctrl | шифт/контрол/альт при каждом overrun. |
| `0xE011` | Right Alt | |
3. Взвести флаг «был overrun» — основной код может проигнорировать
held-состояние на этот кадр.
### 9.3.6 Координация с DSS
Пока обработчик забирает байты с SIO-A, FIFO пуст — DSS не видит
клавиатуру. Для вызова DSS-функций (консоль, диалоговые окна):
1. Переключить флаг «raw active = 0» в IM2-обработчике.
2. Обработчик начинает пропускать байты в DSS (JP `0038h` или
через штатный трамплин).
3. После завершения ввода — переключить флаг обратно.
**Внимание:** если обработчик просто перестаёт читать SIO-A, но не
возвращает управление DSS, порт SIO-A перестанет генерировать
прерывания (FIFO полон, Rx готов = 0). Для корректной передачи
управления нужно разрешить DSS-обработчику читать SIO-A.
### 9.3.7 Пример: минимальный raw-обработчик
```asm
; Флаги: raw_active = 1 — перехватывать, 0 — пропускать в DSS
; Карта: kbd_held — 128 байт, занулена при старте
; FSM: kbd_state — 0=PEND_MAKE, 1=PEND_BREAK, 2=PEND_EXT, 3=PEND_BREAK_EXT
; kbd_ext — флаг extended (0/1)
KBD_ISR:
; Проверить, что прерывание от SIO-A
IN A, (#19) ; SIO-A RR0
BIT 0, A ; бит 0 = прерывание от SIO-A?
RET Z ; нет — пропустить
IN A, (#18) ; прочитать байт скан-кода
BIT raw_active ; перехват активен?
JP Z, DSS_IRQ ; нет — пусть DSS обработает
; FSM: текущее состояние в kbd_state
LD HL, kbd_state
LD A, (HL)
AND A, 3
JP Z, .pend_make
DEC A
JP Z, .pend_break
DEC A
JP Z, .pend_ext
; .pend_break_ext
LD (HL), 0 ; → PEND_MAKE
LD A, B ; код клавиши
CALL break_ext
EI
RETI
.pend_make:
LD A, B ; байт с SIO-A (уже в B из IN)
CP 0xF0
JR Z, .set_break
CP 0xE0
JR Z, .set_ext
CALL make ; обычный make
LD (HL), 0 ; остаёмся PEND_MAKE
EI
RETI
.set_break:
LD (HL), 1 ; → PEND_BREAK
EI
RETI
.set_ext:
LD (HL), 2 ; → PEND_EXT
EI
RETI
.pend_break:
LD (HL), 0 ; → PEND_MAKE
CALL break
EI
RETI
.pend_ext:
LD A, B
CP 0xF0
JR Z, .set_ext_break
CALL make_ext
LD (HL), 0
EI
RETI
.set_ext_break:
LD (HL), 3 ; → PEND_BREAK_EXT
EI
RETI
```
Размер кода — ~60 байт.
---
## 9.4 Мышь
Драйвер мыши установлен в системном shell Sprinter и доступен через
SST-вызов `RST 30h`.
### 9.4.1 SST-функции мыши
Вызов: `RST 30h` с A = номер функции. Выход в зависимости от функции
(см. таблицу).
| № | Назначение | Вход | Выход |
|:-:|:-----------|:-----|:------|
| `$00` | Инициализация | A=$00 | — |
| `$01` | Показать курсор | A=$01 | — |
| `$02` | Спрятать курсор | A=$02 | — |
| `$03` | Читать состояние | A=$03 | DE=X, HL=Y, A=buttons (b0=left, b1=right) |
| `$04` | Переместить курсор | A=$04, DE=X, HL=Y | — |
| `$05` | Установить границы X | A=$05, DE=min, HL=max | — |
| `$06` | Установить границы Y | A=$06, DE=min, HL=max | — |
| `$07` | Вид курсора (текст) | A=$07, DE=sym_and:sym_xor, HL=attr_and:attr_xor | — |
| `$09` | Загрузить курсор (граф.) | A=$09, IX=pointer | — |
| `$0B` | Прочитать курсор | A=$0B | IX=pointer (копия) |
| `$0C` | Чувствительность X | A=$0C, D=value (1..255) | — |
| `$0D` | Чувствительность Y | A=$0D, D=value (1..255) | — |
| `$0E` | Прочитать чувств. X | A=$0E | D=value |
| `$0F` | Прочитать чувств. Y | A=$0F | D=value |
| `$11` | Уведомить о смене режима | A=$11, D=режим (0=текст, 1=граф-256, 2=граф-16) | — |
**Пример: прочитать состояние мыши в asm:**
```asm
LD A, $03 ; READ
RST 30h
; DE = X, HL = Y, A = buttons
LD (mouse_x), DE
LD (mouse_y), HL
AND A, 3
LD (mouse_btn), A
```
### 9.4.2 Чувствительность
Значение — **делитель**: драйвер считает N raw-шагов мыши, прежде чем
сдвинуть курсор на 1 пиксель. **Меньше = быстрее** (чувствительнее).
Рекомендуемое начальное значение: 2 по обеим осям.
```asm
LD A, $0C ; SET_SENS_H
LD D, 2
RST 30h
LD A, $0D ; SET_SENS_V
LD D, 2
RST 30h
```
### 9.4.3 Порядок инициализации
После загрузки драйвер мыши уже инициализирован shell. Если программа
начинает «с чистого листа» (без shell), инициализация обязательна:
```asm
LD A, $00 ; INIT — инициализировать драйвер
RST 30h
LD A, $05 ; BOUNDS_X: границы экрана
LD DE, 0
LD HL, 319 ; для 320×256
RST 30h
LD A, $06 ; BOUNDS_Y
LD DE, 0
LD HL, 255
RST 30h
LD A, $01 ; SHOW — показать курсор
RST 30h
```
### 9.4.4 Смена видеорежима
После переключения видеорежима (графический → текстовый и обратно)
обязательно вызвать `$11`:
```asm
LD A, $11
LD D, 1 ; 0=текст, 1=граф-256, 2=граф-16
RST 30h
```
Иначе драйвер применяет старые координаты, и курсор рисуется
неправильно.
### 9.4.5 Прямой доступ к SIO-B (raw mouse)
Мышь подключена через SIO-B (порты `#1A`/`#1B`), Microsoft Serial Mouse,
1200 бод 7E1. Если программа хочет читать мышь напрямую, минуя
SST-драйвер (например для нестандартного протокола или экономии на
SST), SIO-B нужно инициализировать самостоятельно:
**Инициализация SIO-B (1200 7E1):**
```asm
LD HL, .sio_init
LD B, 6
LD C, #1B ; SIO-B control
OTIR
JR .done
.sio_init:
DB #10 ; WR0: сброс RR1
DB #04 ; WR1: прерывания выкл
DB #44 ; WR4: ×1 клок, 1 стоп, без паритета
DB #C1 ; WR3: Rx 8 бит
DB #EA ; WR5: Tx 8 бит, DTR+RTS=1
DB #15 ; WR0: сброс ошибки + выбран RR1
.done:
```
**Чтение байта с мыши (опрос, без прерываний):**
```asm
IN A, (#1B) ; SIO-B RR0
BIT 0, A ; бит 0 = Rx готов?
JR Z, .no_data
IN A, (#1A) ; прочитать байт
; обработать ...
```
**Формат пакета Microsoft Serial Mouse:**
Длина: 3 байта.
| Байт | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|:----:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| 0 | 1 | 0 | 0 | 0 | L | R | Y7 | Y6 |
| 1 | 0 | X6 | X5 | X4 | X3 | X2 | X1 | X0 |
| 2 | 0 | Y5 | Y4 | Y3 | Y2 | Y1 | Y0 | X7 |
- Координаты X/Y — **относительные** (приращения с последнего пакета), знаковые.
- `X7` — старший бит X (знак), `X6..X0` — младшие.
- `Y7..Y6` — старшие биты Y, `Y5..Y0` — младшие.
- `L` — левая кнопка (1 = нажата), `R` — правая.
**Декодирование:**
```asm
; B = байт 0, C = байт 1, D = байт 2
; X:
LD A, C ; байт 1: X6..X0
LD E, A
LD A, D ; байт 2: X7
RLCA
RLCA
AND A, 0x80
OR A, E ; A = X (7 бит + знак)
; знаковое расширение в HL
LD L, A
RLCA
SBC A, A
LD H, A ; HL = X (знаковое 16-бит)
; Y:
LD A, B ; байт 0: Y7..Y6
RLCA
RLCA ; A[1:0] = Y7..Y6
AND A, 0xC0
LD E, A
LD A, D ; байт 2: Y5..Y0
RLCA
RLCA ; A[7:2] = Y5..Y0
AND A, 0xFC
OR A, E ; A = Y (7 бит + знак)
; знаковое расширение в DE
LD E, A
RLCA
SBC A, A
LD D, A ; DE = Y (знаковое 16-бит)
; Кнопки:
LD A, B
AND A, 0x30 ; биты L (4) и R (5)
RRCA ; A = 0..3
RRCA
RRCA
RRCA
LD (buttons), A
```
---
## 9.5 Особенности игровой клавиатуры
Документ: `kbd-games.md`. Ниже — ключевые паттерны для реализации.
- **Held-state vs edge-detect:** DSS — событийный (сообщает только момент
нажатия). Raw-канал (§9.3) — state-based (зажата/не зажата прямо сейчас).
Играм нужен второй.
- **Sync once per frame:** раз в кадр (по кадровому прерыванию) проверять
held-карту. Между кадрами карта обновляется только в IM2-обработчике.
- **Edge-detect:** для одноразовых действий (прыжок, выстрел) — запоминать
предыдущее held-состояние и сравнивать:
```
pressed = held & ~prev_held
prev_held = held
```
- **Типоматик:** игнорировать повторные make, пока не придёт break.
Реализация: при make — установить бит только если он ещё не был
установлен; при break — сбросить.
- **Автоотмена (reject):** если за кадр нажато больше N клавиш (например
4) — игнорировать все (защита от баунса и мусора).
- **Recovery:** при overrun — очистить held-карту всех клавиш, кроме
модификаторов (LShift 0x12, RShift 0x59, LCtrl 0x14, LAlt 0x11,
RCtrl 0xE014, RAlt 0xE011).
---
## 9.6 Кросс-ссылки
- Прерывания (IM2, вектор #FF): `07-irq.md §7.2`
- DSS-функции клавиатуры: `03-dss.md §3.6`
- BIOS FN_KBD_OUT: `02-bios.md §2.7`
- Порт клавиатуры `#FE`: `11-ports.md §11.3`
- Игровая клавиатура: `kbd-games.md`
- Баги: `12-bugs.md`
- SIO и порты прерываний: `11-ports.md §11.2`
+377
View File
@@ -0,0 +1,377 @@
# 10. Звук
## 10.1 Общие сведения
> Документ описывает звуковую подсистему платы **Sp2000** и более новых моделей.
> Информация о Sprinter-97 не рассматривается.
Звуковая подсистема Sprinter Sp2000 включает три компонента:
| Компонент | Тип | Описание |
|-----------|-----|----------|
| GM80C760 (AY-3-8910 совм.) | Музыкальный сопроцессор | 3 голоса + шум + огибающая, эмуляция в ППЛМ |
| COVOX | 8-битный ЦАП | Прямой вывод сэмплов через порт `#FB`/`#4F` |
| CBL (COVOX-Blaster) | COVOX с буфером | 256 байт буфера, прерывания, разные режимы |
Весь звук выводится через **16-битный ЦАП TDA1543** (двухканальный, стерео).
Реально используется 10 бит для одновременного смешивания 8-bit COVOX и AY
[IvanMak.txt:632634].
**На Sp2000 все три компонента присутствуют в основной прошивке ППЛМ.**
CBL включается через порт `#4E` (бит 7 = 1). При выключенном CBL (бит 7 = 0)
работает обычный 8-bit COVOX [IvanMak.txt:662666].
## 10.2 GM80C760 (AY-3-8910)
### 10.2.1 Реализация
AY-функция реализована на микросхеме **GM80C760** (ППЛМ ALTERA EP1K30),
AY-3-8910/8912 совместимая. Это не отдельная дискретная микросхема
[IvanMak.txt:636637].
На EP1K30 (Sp2000) реализована **3-я версия** AY. Включает три голоса,
шум и генератор огибающей (envelope). В формирователе огибающей обнаружена
небольшая ошибка, детали не документированы [IvanMak.txt:224229],
[12-bugs.md §11.2].
### 10.2.2 Порты
| Порт | Назначение |
|------|-----------|
| `#BFFD` | AY-8910 — запись номера регистра (address) |
| `#FFFD` | AY-8910 — чтение/запись данных регистра (data) |
Внутренние номера в карте дешифратора ППЛМ: `90h` (`#BFFD`), `91h` (`#FFFD`)
[IvanMak.txt:12981299].
### 10.2.3 Программирование
Программирование — **стандартное для AY-3-8910/8912**
[IvanMak.txt:636637]:
```asm
; Установить регистр AY
LD BC, #BFFD
OUT (C), A ; A = номер регистра (0..15)
LD BC, #FFFD
OUT (C), A ; A = значение
; Прочитать регистр AY (3-я версия)
LD BC, #BFFD
OUT (C), A ; A = номер регистра
LD BC, #FFFD
IN A, (C) ; читать данные
```
**Регистры AY-3-8910** (стандартные):
| № | Назначение |
|:-:|-----------|
| 0 | Частота канала A (младший байт) |
| 1 | Частота канала A (старшие 4 бита) |
| 2 | Частота канала B (младший байт) |
| 3 | Частота канала B (старшие 4 бита) |
| 4 | Частота канала C (младший байт) |
| 5 | Частота канала C (старшие 4 бита) |
| 6 | Период шума |
| 7 | Микшер (включение каналов и шума) |
| 8 | Громкость A |
| 9 | Громкость B |
| 10 | Громкость C |
| 11 | Период огибающей (младший байт) |
| 12 | Период огибающей (старшие 8 бит) |
| 13 | Форма огибающей |
| 14 | Порт I/O A (не используется на Sprinter) |
| 15 | Порт I/O B (не используется на Sprinter) |
**Стерео:** AY имеет два выходных канала, но в стандартной конфигурации
Sprinter оба канала получают монофонический сигнал.
---
## 10.3 COVOX (8-бит)
### 10.3.1 Порты
| Порт | Назначение |
|------|-----------|
| `#FB` | COVOX data (он же порт включения КЭШ-ОЗУ по IN) |
| `#4F` | COVOX data (альтернативный адрес) |
[IvanMak.txt:642, 1347]
### 10.3.2 Программирование
Простейший вывод сэмплов — последовательная запись байтов в порт `#FB`
или `#4F`:
```asm
; Цикл вывода сэмплов (8 бит, моно)
LD HL, sample_data
LD BC, sample_length
loop:
LD A, (HL)
OUT (#FB), A
INC HL
DEC BC
LD A, B
OR C
JR NZ, loop
```
### 10.3.3 Конфликт с Fast RAM (КЭШ-ОЗУ)
Порт `#FB` используется **для двух целей**:
- `OUT (#FB), A` — запись в COVOX
- `IN A, (#FB)` — включение КЭШ-ОЗУ
Если КЭШ-ОЗУ включено, запись в `#FB` уходит в кэш, а не в COVOX.
Перед работой с COVOX кэш нужно отключать: `IN A, (#7B)`
[IvanMak.txt:850851], [12-bugs.md §11.4].
---
## 10.4 COVOX-Blaster (CBL)
### 10.4.1 Общее описание
**CBL (COVOX-Blaster)** — COVOX с буферным ОЗУ на **256 байт**. Позволяет
выводить звук поблочно, освобождая процессор для другой работы
[IvanMak.txt:647], [Parinov.txt:688].
### 10.4.2 Порт управления `#4E`
**Порт `#4E` — 16-битный! Только через `OUT (C),A`** (с `LD BC,004Eh`)
[Parinov.txt:692693].
> **Примечание:** IvanMak.txt упоминает его как 8-битный, но Parinov.txt явно
> указывает 16-битный доступ. Современный код использует `OUT (C),A`.
**Биты порта `#4E`** [Parinov.txt:695698]:
| Бит | Мнемоника | Назначение |
|-----|-----------|-----------|
| 7 | **CBL_ON** | 1 = CBL включён, 0 = обычный COVOX |
| 6 | **STEREO** | 1 = стерео, 0 = моно |
| 5 | **16BIT** | 1 = 16-битные сэмплы, 0 = 8-битные |
| 4 | **IRQ_EN** | 1 = включить прерывания от CBL |
| 30 | **FREQ** | Частота дискретизации (см. таблицу) |
### 10.4.3 Частоты дискретизации
[Parinov.txt:700718]:
| Код | Частота | Примечание |
|:---:|---------|-----------|
| 0 | 16 kHz | старые режимы, **НЕ ИСПОЛЬЗОВАТЬ** |
| 1 | 22 kHz | старые режимы, **НЕ ИСПОЛЬЗОВАТЬ** |
| 2–7 | — | зарезервировано |
| **8** | **7.8125 kHz** | mono/stereo, 8/16-bit |
| **9** | **10.9375 kHz** | mono/stereo, 8/16-bit |
| **A** | **15.625 kHz** | mono/stereo, 8/16-bit |
| **B** | **21.875 kHz** | mono/stereo, 8/16-bit |
| **C** | **31.25 kHz** | mono/stereo, 8/16-bit |
| **D** | **43.75 kHz** | mono/stereo, 8/16-bit |
| **E** | **54.6875 kHz** | mono/stereo, 8/16-bit |
| **F** | **109.375 kHz** | mono/stereo, 8/16-bit |
> **Важно:** коды 0 и 1 — старые режимы из первых прошивок, несовместимы с
> новыми режимами (8–F). Использовать только коды 8–F.
### 10.4.4 Режимы и формат данных
[Parinov.txt:767783]:
| Режим | Нулевой уровень | Размер блока | Формат данных |
|-------|:---------------:|:------------:|--------------|
| mono 8-bit | `80h` | 128 байт | `DB 80h, 81h, 7Fh, ...` |
| mono 16-bit | `0000h` | 256 байт | `DW 0, 1000, -1000, ...` |
| stereo 8-bit | `80h,80h` | 128 байт | `DB 80h,80h, 81h,80h, ...` (L, R чередуются) |
| stereo 16-bit | `0000h,0000h` | 256 байт | `DW 0,0, 1000,0, ...` (L, R чередуются) |
Формат данных выбран так, чтобы WAV-файлы можно было пересылать в CBL
простой `OTIR` [Parinov.txt:791].
### 10.4.5 Буфер CBL и механизм банок
CBL имеет буферное ОЗУ на **256 байт**, организованное как кольцевой буфер
[Parinov.txt:758760]:
- Счётчик CBL считает **«назад»** — из-за особенности команды `OUTI`, где `B`
уменьшается и попадает на A15..A8.
- Для 8-битных режимов буфер условно разбит на **две банки по 128 байт**.
- Для 16-битного режима блок = **256 байт** (весь буфер).
**Механизм работы:**
1. Процессор записывает блок данных (128 байт для 8-бит, 256 байт для 16-бит)
в буфер CBL.
2. Аппаратный счётчик CBL автоматически считывает данные из буфера и
отправляет в ЦАП на заданной частоте.
3. Бит 7 порта `#FE` указывает, какая банка сейчас выводится в ЦАП.
4. Когда банка переключается — CBL запрашивает следующую порцию данных
(через прерывание или polling).
5. Запрос данных возникает для **каждых 128 байт (256 в 16-битном режиме)**.
### 10.4.6 Порт статуса `#FE`
**Бит 7 порта `#FE`** — запрос данных для CBL [Parinov.txt:720724]:
| Бит | Назначение |
|-----|-----------|
| 7 | **CBL data request** — 1 = CBL запрашивает новую порцию данных |
| 5 | **Frame sync** — кадровый импульс (4 мс длиной, 20 мс период) |
> **Примечание:** В MAME при выключенном CBL бит 7 порта #FE всегда = 1.
> Это следует учитывать при разработке обработчика прерываний
> [`im2_isr_design.md:2631`].
**Противоречие** [08-io.md §8.3]:
- IvanMak.txt описывает бит 7 как старший бит счётчика CBL для polling
(определение, какая половина буфера выводится).
- Parinov.txt описывает его как запрос прерывания.
- Оба описывают одну физическую линию, но в разных режимах (polling vs interrupt).
### 10.4.7 Прерывания и IM2
- При включённых прерываниях CBL (бит 4 порта `#4E` = 1) необходим **режим IM 2**.
- Если остаться на IM1, DOS-процедура прерываний будет вызываться слишком часто,
что вызовет тормоза на высоких частотах [Parinov.txt:762764].
- **Вектор прерывания CBL — `#FF`** (как и у клавиатуры, и у кадрового).
Отличать — по биту 7 порта `#FE` [`sprinterIntLib.asm:2426`].
**Обработчик прерываний на IM2:**
```asm
; Проверка источника
IN A, (#FE)
RLCA ; бит 7 → CF
JR NC, not_cbl ; не CBL — на другой обработчик
; CBL запрашивает данные
LD BC, 804Fh ; B = 128 (счётчик), C = 4Fh (порт CBL data)
LD HL, next_block
OTIR ; вывести 128 байт в буфер CBL
...
```
### 10.4.8 Запись данных в CBL
**Способ 1: OTIR/OUTI через порт `#4F`**
```asm
LD BC, 804Fh ; B = 128 (счётчик), C = #4F (CBL data)
LD HL, data_block
OTIR ; вывести 128 байт
```
**Способ 2: через страницу ОЗУ `#FD` с акселератором**
Страница `#FD`, отображённая в окно `#C000`, аппаратно связана с буфером CBL.
Для 8-битного режима — записать 128 байт; для 16-битного — 256 байт
[Parinov.txt:727731].
```asm
; Запись блока данных в CBL через страницу #FD с акселератором
IN A, (#E2) ; сохранить текущую страницу W3
LD (.save_page), A
LD A, #FD
OUT (#E2), A ; W3 = страница CBL
LD DE, #C000 ; приёмник (CBL buffer)
LD B, 0 ; B = 0 → 256 байт (или 128 для 8-bit)
LD D,D ; ACC_ON: включить акселератор
LD L,L ; ACC_CopyBlock_Horizontal
LD A, (HL) ; загрузить байт в буфер акселератора
LD (DE), A ; записать в CBL
LD B,B ; ACC_OFF: выключить акселератор
LD A, (.save_page)
OUT (#E2), A ; восстановить страницу
```
### 10.4.9 Полный пример
Из `docs/samples/Пример для CBL.asm` [sprinterIntLib.asm]:
```asm
; Инициализация IM2
LD A, #BE
LD I, A ; таблица векторов по #BE00
IM 2
; Вектор прерывания CBL = #BEFF
; (Interrupt_Vector OR 0FFh)
; Включение CBL
LD BC, #4E
LD A, %10011000b ; bit7=1 CBL on, bit4=1 IRQ en, bits3-0=1000 = 7.8125 kHz
OUT (C), A
; В ISR:
; IN A, (#FE)
; RLCA
; JR NC, .NoSound
; ... копировать 128 байт через страницу #FD с акселератором
; После проигрывания:
; Установить NumberPage=2, подождать HALT
; Выключение CBL:
LD BC, #4E
LD A, 0
OUT (C), A
```
### 10.4.10 Известные проблемы
1. **CBL vs Fast RAM:** порт `#FB` используется и для COVOX/CBL, и для
включения КЭШ-ОЗУ (см. §10.3.3).
2. **Щелчок перед первым звуком:** известная проблема при старте CBL
[`TODO.md:263`].
3. **Помехи при движении мыши:** часть тиков прерываний, обслуживающих
CBL-насос, уходит на обслуживание мыши [`TODO.md:270277`].
4. **Порт #4E — 8-бит vs 16-бит:** IvanMak подразумевает 8-битный,
Parinov требует 16-битный `OUT (C),A` (см. §10.4.2).
5. **MAME: бит 7 порта #FE = 1 при выключенном CBL:** может вызвать
ложные срабатывания обработчика (см. §10.4.6).
---
## 10.5 Бипер
Стандартный Spectrum-бипер — бит 3 (в некоторых конфигурациях бит 5)
порта `#FE`. Выведен через ту же схему TDA1543, что COVOX и AY
[IvanMak.txt:639640].
```asm
; Включить бипер
LD A, %00001000 ; бит 3 = 1
OUT (#FE), A
; Выключить бипер
LD A, %00000000 ; бит 3 = 0
OUT (#FE), A
```
---
## 10.6 SSC (Sprinter Sound Card)
Разрабатывалась, но **не реализована** ни в одной прошивке Sp2000
из-за большого объёма ПЛМ [IvanMak.txt:763772].
Параметры проекта:
- 2 канала
- 16 восьмибитных голосов
- 256-байтные Wave-таблицы для каждого голоса
- 8-битный регулятор амплитуды
---
## 10.7 Кросс-ссылки
- Порты звука: `11-ports.md`
- Баги AY и CBL: `12-bugs.md`
- Fast RAM и конфликт с COVOX: `04-memory.md §4.6`
- 08-io.md (остальной ввод-вывод): `08-io.md`
+103
View File
@@ -0,0 +1,103 @@
# 11. Карта портов
## 11.1 Системные порты Z84C15 (внутренние)
Эти адреса изменить невозможно — находятся вне ППЛМ [IvanMak.txt:869870].
| Порт | Назначение | Доступ |
|------|-----------|--------|
| `#10` | SIO A data / CTC channel 0 | R/W |
| `#11` | SIO A control / CTC channel 1 | R/W |
| `#12` | SIO B data / CTC channel 2 | R/W |
| `#13` | SIO B control / CTC channel 3 | R/W |
| `#14` | PIO A data | R/W |
| `#15` | PIO A control | R/W |
| `#16` | PIO B data | R/W |
| `#17` | PIO B control | R/W |
| `#18` | SIO data channel A (клавиатура) | R/W |
| `#19` | SIO control channel A (статус клавиатуры) | R/W |
| `#1A` | SIO data channel B (мышь) | R/W |
| `#1B` | SIO control channel B (мышь) | R/W |
| `#1E` | PIO B бит 0 — IRQ от ISA | R |
| `#1F` | PIO B + джойстик (Kempston) | R/W |
| `#EE` | CTC channel 2 (альтернативный) | R/W |
| `#EF` | CTC channel 3 (альтернативный) | R/W |
| `#F0` | CTC channel 0 (альтернативный) | R/W |
| `#F1` | CTC channel 1 (альтернативный) | R/W |
| `#F4` | CTC — альтернативный доступ | |
## 11.2 Управление памятью
| Порт | Назначение | Источник |
|------|-----------|----------|
| `#7FFD` | Основной: W3 page (02), RAM/ROM select, lock | IvanMak.txt:1343 |
| `#1FFD` | Расширенный: W0 page (02), turbo, extended RAM, VRAM segment | IvanMak.txt:1345 |
| `#DFFD` | W1 page select (Pentagon-512) | IvanMak.txt:1346 |
| `#EFF7` | W2 page select (Pentagon-512) | IvanMak.txt:1346 |
| `#82` | W0 page (PAGE0) — всегда доступен | IvanMak.txt:1348 |
| `#A2` | W1 page (PAGE1) — всегда доступен | IvanMak.txt:1348 |
| `#C2` | W2 page (PAGE2) — всегда доступен | IvanMak.txt:1348 |
| `#E2` | W3 page (PAGE3) — всегда доступен | IvanMak.txt:1348 |
## 11.3 Видео
| Порт | Назначение | Источник |
|------|-----------|----------|
| `#89` | RGADR (PORT_Y) — номер строки/блока видео-ОЗУ | IvanMak.txt:1349 |
| `#C9` | RGMOD — страница режима экрана (бит 0: 0/1) | IvanMak.txt:1350 |
## 11.4 Управление конфигурацией/прошивкой
| Порт | Назначение | Источник |
|------|-----------|----------|
| `#FB` | Включение КЭШ-ОЗУ (IN), COVOX data | IvanMak.txt:1351 |
| `#7B` | Выключение КЭШ-ОЗУ (IN) | Parinov.txt:598 |
## 11.5 Дисковая подсистема
| Порт | Назначение | Источник |
|------|-----------|----------|
| `#0F` | ВГ93 Command/Status (после ремапинга #1F#0F) | IvanMak.txt:1270 |
| `#1F` | ВГ93 Command/Status (оригинал, ремапится) | IvanMak.txt:1344 |
| `#3F` | ВГ93 Track register | IvanMak.txt:1271 |
| `#5F` | ВГ93 Sector register | IvanMak.txt:1272 |
| `#7F` | ВГ93 Data register | IvanMak.txt:1273 |
| `#FF` | ВГ93 Drive Control (write); IRQ/джойстик (read) | IvanMak.txt:1274 |
| `#BD` | Переключение режима FDD (720K/1.44M) | IvanMak.txt:1347 |
| `xx50` | IDE Data | Parinov.txt:958 |
| `xx51` | IDE Error/Features | Parinov.txt:958 |
| `xx52` | IDE Sector Count | Parinov.txt:958 |
| `xx53` | IDE Sector Number | Parinov.txt:958 |
| `xx54` | IDE Cylinder Low | Parinov.txt:958 |
| `xx55` | IDE Cylinder High | Parinov.txt:958 |
| `4052h` (R) / `4152h` (W) | IDE Device/Head | Parinov.txt:959 |
| `4053h` (R) / `4153h` (W) | IDE Status/Command | Parinov.txt:959 |
| `FFBD` | CMOS data read | BIOS_v3.txt:308 |
| `BFBD` | CMOS data write | BIOS_v3.txt:308 |
| `DFBD` | CMOS address write | BIOS_v3.txt:308 |
## 11.6 Звук
| Порт | Назначение | Источник |
|------|-----------|----------|
| `#FB` | COVOX data (also КЭШ-ОЗУ enable) | IvanMak.txt:642 |
| `#4F` | COVOX/CBL data | IvanMak.txt:642 |
| `#4E` | CBL control (8/16-bit — см. `10-sound.md §9.4.2`) | IvanMak.txt:662, Parinov.txt:692 |
## 11.7 Джойстик и системные
| Порт | Назначение | Источник |
|------|-----------|----------|
| `#FE` | ZX-клавиатурная матрица, бипер, CBL статус | IvanMak.txt:1344 |
| `#1F` | Джойстик (Kempston) / PIO B | IvanMak.txt:1344 |
## 11.8 Кросс-ссылки
- BIOS-функции: `02-bios.md`
- DSS-функции: `03-dss.md`
- Управление памятью: `04-memory.md`
- Звук (AY, COVOX, CBL): `10-sound.md`
- Видео/палитра: `05-graphics.md`
- Прерывания: `07-irq.md`
- Детали I/O: `08-io.md`
- Баги: `12-bugs.md`
+54
View File
@@ -0,0 +1,54 @@
# 12. Баги BIOS, DSS и железа Sp2000
## 12.1 DRV_VERIFY (54h) не существует
Функция `54h DRV_VERIFY` описана во всей документации BIOS, но в реальности отсутствует — всегда возвращает CF=1.
> [bugs.txt:5859]: «Нет биосной функции "54h" (DRV_VERIFY) верификации секторов, описанной в документации. Поэтому она всегда возвращает установленный флаг "Carry".»
Источники, содержащие описание несуществующей функции:
- [BIOS_v3.txt:258261]
- [IvanMak.txt:21982210]
## 12.2 AY-3-8910 — ошибка в формирователе огибающей
Третья версия AY в ППЛМ Sp2000 содержит ошибку в схеме формирователя огибающей (envelope generator). Детали не документированы.
> [IvanMak.txt:224229]: «Третья версия схемы AY ... включает в себя ... генератор огибающей. ... В схеме формирователя огибающей обнаружена небольшая ошибка. ... В следующей версии AY предполагается данный недостаток исключить.»
## 12.3 Акселератор — конфликт с прерываниями
При работе акселератора система команд Z80 сильно меняется, что может приводить к сбоям в обработчиках прерываний.
> [IvanMak.txt:844846]: «Отключение прерываний во время работы акселератора необходимо, так как в этот момент сильно меняется система команд процессора и программа на прерывании не сможет работать нормально.»
Новая прошивка акселератора позволяет работать с прерываниями — при приходе INT акселератор отключается и включается обратно по RETI [IvanMak.txt:846848]. Без тестирования на реальном железе неизвестно, какая версия прошивки установлена.
## 12.4 Быстрое ОЗУ — конфликт с COVOX
Порт `#FB` используется как для включения КЭШ-ОЗУ (IN), так и для вывода данных в COVOX (OUT). Если КЭШ-ОЗУ включено, запись в COVOX может давать неожиданные результаты.
> [IvanMak.txt:850851, 642]
Перед вызовом RST или выходом в DOS КЭШ-ОЗУ обязательно отключать (`IN A,(#7B)`).
## 12.5 STA_VID — потеря старшего байта (0A8h?)
> [bugs.txt:33]: «В БИОСе в функции STA_VID (возможно 0A8h) теряется старший байт параметра. В v2.12 функцию рекомендуется вызывать с учетом этого.»
Относится к BIOS v2.12. Не проверялось, исправлено ли в v3.00.
## 12.6 Несовместимость с реальным железом — только MAME
Многие перечисленные баги и квирки зафиксированы в документации, но не подтверждены на реальном Sprinter Sp2000. Все тесты тулчейна выполняются в MAME. Актуальность багов для реального железа неизвестна.
> [TODO.md]: «Проверка на реальном железе — в планах»
## 12.7 Кросс-ссылки
- BIOS v3.00: `02-bios.md`
- DSS/ESTEX: `03-dss.md`
- Акселератор: `06-accel.md`
- Fast RAM: `04-memory.md §4.6`
- Проектный TODO: `TODO.md`
- Проектный список багов: `converted/bugs.txt`
+169
View File
@@ -0,0 +1,169 @@
# 13. Информация из Telegram-чатов (Sp2022 и развитие Sprinter)
## 13.1 Источники
Данные извлечены из 319 HTML-файлов чатов Telegram, выгруженных из чата по Sprinter (n/w). Обработано 317 602 сообщения, отфильтровано технических — 72 307 (15 МБ текста).
Основные авторы технических сообщений: RomanRom2, KromosS, solegstar, nzeemin, holub.
## 13.2 BIOS 3.06 / DSS 1.71.57
KromosS (23 января 2026): «I am running the DSS 1.71.57 / Bios 3.06».
Зафиксированные версии BIOS:
- 3.05 — предыдущая стабильная
- 3.06 — текущая, 23.01.2026
- Прошивка ППЛМ и ROM собираются через Intel Quartus
BIOS 3.06 собрана из ROM-файла, загружаемого через загрузчик. Процесс сборки:
1. Конфигурация ППЛМ через Quartus
2. Сборка ROM-образа (IS28F020 — 256 КБ flash)
3. Программирование через специальный загрузчик
## 13.3 Эволюция аппаратных платформ
RomanRom2 (11 ноября 2025) подробно описал историю развития Sprinter в 10 пунктах:
| Этап | Платформа | Описание | Период |
|------|-----------|----------|--------|
| 1 | Sp2000 | Оригинальная плата, 8 DIP VRAM | 20002001 |
| 2 | Sp2000s | Баг в разводке ALTERA: ISA зеркалированы | ~2001 |
| 3 | Sp2000s Light | Баг фикс с MGTF, ISA распаяны не полностью | ~2002 |
| 4 | Sp2021s | Новая ревизия от solegstar | ~2021 |
| 5 | Sp2022s/d | Собственный дизайн (Zeal Hardware / RomanRom2) | 2022 |
| 6+ | Sp2024d/du | Планируется, с USB-контроллером | ~2024 |
### Детали:
**Sp2000 (оригинал)**
- Плата 2000–2001 года
- ПЗУ 256 КБ, VRAM 256 КБ (8 DIP-микросхем)
- ALTERA ACEX 1K EP1K30
**Sp2000s**
- ALTERA-баг: в оригинальной плате зеркально разведены ISA-слоты, на Sp2000s исправлено
- Новый баг разводки: ISA адреса «перевёрнуты» — при устанавке платы в слот слоты просто не видятся. В Sp2000s Light фикс с MGTF (соединительная плата), один ISA-слот не распаян
**Sp2022 (Zeal Hardware)**
- Новая разработка на современной элементной базе
- Варианты: Sp2022s (Sp2000 layout, SMD), Sp2022d (две платы, digitized)
- SMD-компоненты, технология 2022 года
- Расширенная память, улучшенное питание
- Sp2022du — вариант с USB-контроллером (планируется для Sp2024)
**Sp2024**
- Sp2024d — следующее поколение
- Sp2024du — с интегрированным USB для клавиатуры/мыши
- Планируется USB-контроллер
### Sp2022 — модификации
| Модификация | Описание |
|-------------|----------|
| sp2022s | SMD, layout аналогичен Sp2000 |
| sp2022d | Две платы (digitized), расширенная память |
| sp2022d-black | Чёрная текстолит |
| sp2022release | Готовая плата для пользователей |
| sp2022m | Мобильная версия (будущее) |
| sp2022i | ITX-формфактор |
| sp2022atx | ATX-формфактор |
| sp2022t | Новая ревизия |
| sp2022du | С USB-контроллером |
## 13.4 Изменения компонентов
### ПЗУ и загрузка
- ROM: IS28F020 (256 КБ Flash, 5V)
- Загрузка через Quartus → ROM-образ → загрузчик
- ROM-файлы обсуждаются в чате
### VRAM
- Оригинал Sp2000: CY7C109D-10VXI (5V, 128K×8)
- Sp2022: CY7C1019DV33-10VXI (3.3V, 128K×8)
- Переход на 3.3V логику
### Video-DAC
- Sp2022: обсуждаются TDA8772 (трипл 8-бит video-DAC) или ADV7120/ADV7125
- Некоторые платы используют ADV7125 (трипл 10-бит) ???
### CMOS
- DS12887A (оригинал)
- На Sp2022 — замена на современный аналог
### КЭШ-ОЗУ
- W24512 (64K×8) — для Sp2022
### SDRAM
- Оригинал: 72-pin SIMM
- Sp2022: SDRAM (DDR) на плате
## 13.5 Аппаратные баги
### Sp2000s — ALTERA routing bug
При переразводке Sp2000 под Sp2000s допущена ошибка в разводке ALTERA (ACEХ 1K). ISA-адреса оказались «зеркальными» — при установке платы в слот половинка линий данных перевёрнута, из-за чего слоты не видятся CPU. Исправлено на Sp2000s Light с помощью MGTF (мезонинная плата-переходник).
### Sp2000s Light — ISA unsoldered
На Sp2000s Light один ISA-слот не распаян, второй — через MGTF.
### IDE active short
В некоторых ревизиях — короткое замыкание на линиях IDE при активном доступе. Требует подтягивающих резисторов.
### FDISK CHS-LBA
Типовые проблемы с разметкой дисков — ошибки пересчёта CHS в LBA в утилитах FDISK для Sprinter.
## 13.6 MAME-эмуляция
- holub разрабатывает форк MAME для Sprinter
- Timing issues: Z80 Z84C15 эмулируется не полностью (внутренние PIO/SIO/CTC)
- Работа над корректной эмуляцией Sp2022
## 13.7 Статистика по платформам
| Платформа | Упоминаний | Период |
|-----------|-----------|--------|
| sp2000 | 243 | 20002001 |
| sp2022 | 122 | 20222024 |
| sp2022d | 66 | 20222024 |
| sp2000s | 44 | 20012002 |
| sp2022s | 43 | 20222024 |
| sp2024d | 6 | 2024+ |
| sp2024du | 4 | 2024+ (план) |
| sp2022i | 3 | 20222024 |
| sp2022m | 1 | будущее |
## 13.8 Компонентная статистика
| Компонент | Упоминаний |
|-----------|-----------|
| Z80 | 1317 |
| USB | 800 |
| BIOS v3.xx | 813 |
| MAME | 496 |
| DDR | 172 |
| ACEX | 147 |
| ALTERA | 115 |
| CY7C | 82 |
| Z84C15 | 87 |
| TDA1543 | 55 |
| SDRAM | 48 |
| IS28F020 | 30+ |
| Quartus | упоминается |
## 13.9 Противоречия с docs/new/
### BIOS v3.06 / DSS 1.71.57
Основные источники (BIOS_v3.txt, IvanMak.txt) документируют BIOS 3.00, не 3.06. Версия 3.06 обнаружена только в Telegram. DSS 1.71.57 значительно новее, чем 1.60 в документации.
### Sp2022 — новое железо
Вся документация в docs/converted/, part2/ и reference/ описывает Sp2000 (20002001). Sp2022 — полностью новая платформа с современной элементной базой (SMD, 3.3V VRAM, USB, SDRAM). Документация отсутствует.
### Состав BIOS
Неизвестно, какие функции из IvanMak.txt (HDD, RAM-disk, конфигурационные, портовые) реализованы в BIOS 3.06, а какие нет. Также неизвестно, исправлены ли баги (DRV_VERIFY, AY envelope).
## 13.10 Кросс-ссылки
- Аппаратная архитектура Sp2000: `01-architecture.md`
- BIOS v3.00: `02-bios.md`
- Проектный TODO: `TODO.md`
- Сырые данные: `docs/tg/_messages.jsonl`, `docs/tg/_technical.txt`
- Фильтр: `docs/tg/_filter.py`
+112
View File
@@ -0,0 +1,112 @@
# INDEX — сводный указатель документации Sp2000
## Документы
| № | Файл | Содержание |
|---|------|-----------|
| 01 | `01-architecture.md` | Общая архитектура: процессор, ППЛМ, конфигурации, обзор памяти/видео/акселератора/звука/ISA/клавиатуры/мыши/дисков/прерываний |
| 02 | `02-bios.md` | BIOS v3.00: вызовы (RST 8/18h, CALL 3D13h), полная таблица функций, видео/палитра/диск/принтер |
| 03 | `03-dss.md` | DSS/ESTEX v1.60: файловые функции, диски, память, консоль, коды ошибок |
| 04 | `04-memory.md` | Память: 4 окна, страницы, порты (#7FFD/#1FFD/#DFFD/#EFF7), EXE-формат, Fast RAM |
| 05 | `05-graphics.md` | Графика: VRAM, адресация (спектрумовская/графическая), видеорежимы, палитра, shadow RAM |
| 06 | `06-accel.md` | Акселератор: команды, примеры, скорость, прерывания |
| 07 | `07-irq.md` | Прерывания: IM1/IM2, INT, CTC, SIO, CBL, цепочка обработчиков |
| 08 | `08-io.md` | Ввод-вывод: FDD, HDD, CMOS, джойстик, принтер, ISA, карта портов |
| 09 | `09-input.md` | Клавиатура (BIOS/DSS/raw) и мышь (RST 30h): API, протоколы, прерывания |
| 10 | `10-sound.md` | Звук: AY-3-8910, COVOX, COVOX-Blaster (CBL), бипер, SSC |
| 11 | `11-ports.md` | Сводная карта портов: Z84C15, память, видео, диски, звук, конфигурация |
| 12 | `12-bugs.md` | Баги: DRV_VERIFY, AY envelope, акселератор+IRQ, Fast RAM+COVOX |
| 13 | `13-telegram.md` | Telegram-чаты: BIOS 3.06, эволюция Sp2000→Sp2022, Sp2022 варианты, Sp2024, аппаратные баги, MAME |
## Противоречия — сводка
### Критические (меняют ABI)
| Тема | Файл | Источники |
|------|------|-----------|
| SETWIN2: 39h vs 3Ah | `03-dss.md` | DiskSyscalls.txt vs ProgrammerManual.txt |
| EXCMDLN subfunc 5: смещения | `03-dss.md` | Все источники дают неверные DE+3/DE+4 |
### Средние
| Тема | Файл | Источники |
|------|------|-----------|
| WIN_RESTORE_WIN: C=0B2h (опечатка) | `02-bios.md` | BIOS_v3.txt vs IvanMak.txt |
| WIN_MOVE_WIN: C=0B2h (опечатка) | `02-bios.md` | IvanMak.txt vs BIOS_v3.txt |
| WIN_GET_SYM: A=0 обязательно? | `02-bios.md` | Parinov.txt vs BIOS_v3.txt |
| PIC_SET_PAL: битфилд vs номер | `02-bios.md` | BIOS_v3.txt vs IvanMak.txt |
| LP_PRINT_SYM: 082h=(131) неверно | `02-bios.md` | Ошибка десятичной аннотации |
| FN_VERSION: значения BC | `02-bios.md` | BIOS_v3.txt vs IvanMak.txt |
### Низкие
| Тема | Файл | Источники |
|------|------|-----------|
| CBL порт 8/16-бит | `10-sound.md` | IvanMak vs Parinov |
| CBL bit7 #FE: статус vs IRQ | `10-sound.md` | IvanMak vs Parinov |
| Адресация ВГ93: #7F data reg | `08-io.md` | IvanMak vs Parinov; MAME подтверждает IvanMak |
| HDD: R/W разные адреса | `08-io.md` | IvanMak vs Parinov; MAME использует единую карту DCP |
| Джойстик #1F vs #0F | `08-io.md` | IvanMak: ремап #1F#0F; MAME на Sp2000 отключает |
## Кросс-ссылки между документами
```
01-architecture.md ← 02-bios.md (BIOS call conventions)
← 04-memory.md (memory overview)
← 05-graphics.md (video overview)
← 06-accel.md (accel overview)
← 07-irq.md (interrupt overview)
← 08-io.md (disk, joystick, printer, ISA overview)
← 09-input.md (keyboard, mouse overview)
← 10-sound.md (sound overview)
← 11-ports.md (ISA ports)
02-bios.md ← 05-graphics.md (PIC_SET_PAL, SET_MODE)
← 11-ports.md
← 12-bugs.md (DRV_VERIFY)
03-dss.md ← 04-memory.md (SETWIN/SETWIN1/SETWIN2)
← 11-ports.md
← 12-bugs.md
04-memory.md ← 06-accel.md (Fast RAM conflict with accel)
← 11-ports.md
← 03-dss.md (window functions)
05-graphics.md ← 02-bios.md (PIC_SET_PAL, SETVMOD)
← 06-accel.md (accelerator for fast pixel ops)
← 10-sound.md (CBL for video)
← 11-ports.md (RGADR, RGMOD)
07-irq.md ← 10-sound.md (CBL interrupt sources)
← 11-ports.md
← 08-io.md (disk I/O interrupt sources)
← 09-input.md (keyboard, mouse interrupt sources)
08-io.md ← 02-bios.md (printer functions)
← 09-input.md (keyboard, mouse moved here)
← 10-sound.md
← 11-ports.md
← 07-irq.md (mouse IRQ)
11-ports.md → все документы
```
## Проектные документы (не входят в docs/new/)
| Файл | Связь |
|------|-------|
| `memory-management.md` | C-specific memory models (legacy, перекрывается `04-memory.md`) |
| `fast_ram.md` | Детальный анализ Fast RAM — уточняет `04-memory.md §4.6` |
| `file-buffering-design.md` | FILE* дизайн — уточняет `03-dss.md` |
| `im2_isr_design.md` | IM2/ISR дизайн — уточняет `07-irq.md` |
| `sprite-api-design.md` | Спрайтовое расширение BGI |
| `kbd-games.md` | Клавиатура в играх — уточняет `09-input.md §9.5` |
| `libc-reference.md` | API libc |
| `libc-roadmap.md` | План работ по libc |
| `libc-split-asm-cases.md` | Правила сплита asm |
| `libc-headers.md` | Стратегия заголовков |
| `accel-fill-budget.md` | Бюджет тактов акселератора |
| `TODO.md` | Roadmap проекта |
| `bugs/` | Баг-репорт SDCC z80 |
Binary file not shown.
Binary file not shown.
+17 -10
View File
@@ -1,5 +1,5 @@
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
accfill 3772 accfill 3786
argv 3431 argv 3431
assrtest 3847 assrtest 3847
atlas 9193 atlas 9193
@@ -9,15 +9,15 @@ banked 1056
bankedbg 1067 bankedbg 1067
banklocl 4683 banklocl 4683
banktest 3767 banktest 3767
bgi_img 7057 bgi_img 7200
bgitest 3737 bgitest 3751
bios_text 4461 bios_text 4461
blitperf 5860 blitperf 5860
blitw 4939 blitw 4939
cat 913 cat 913
cblstream 6217 cblstream 6431
cbltest 6366 cbltest 6580
cblwav 6434 cblwav 6648
conio 4605 conio 4605
conio2 3929 conio2 3929
dec_test 860 dec_test 860
@@ -27,12 +27,12 @@ fdmax 6023
filetest 10547 filetest 10547
fpsdiv 4870 fpsdiv 4870
gets 509 gets 509
gfx_dbuf 5032 gfx_dbuf 5112
gfx_demo 4103 gfx_demo 4117
gfxbanks 6044 gfxbanks 6044
hello 4167 hello 4167
hello2 4244 hello2 4244
irqtest 6527 irqtest 6647
kbdraw 4911 kbdraw 4911
ls 4835 ls 4835
malloc 4461 malloc 4461
@@ -45,7 +45,8 @@ palfile 5406
ptime 5744 ptime 5744
rpgprof 12082 rpgprof 12082
rpgwalk 10309 rpgwalk 10309
rt_test 4892 rt_test 5154
scroll 2974
seek 4188 seek 4188
simple 955 simple 955
solidt 11531 solidt 11531
@@ -60,4 +61,10 @@ strtest 1340
text_palette 5024 text_palette 5024
timedir 5454 timedir 5454
w0page 8831 w0page 8831
w3bankgfx 5240
w3bgfx 5246
w3big 3673
w3huge 3700
w3probe 3556
w3tiny 3471
winrest 4459 winrest 4459
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
2 accfill
3 argv
4 assrtest
5 atlas
9 bankedbg
10 banklocl
11 banktest
12 bgi_img
13 bgitest
14 bios_text
15 blitperf
16 blitw
17 cat
18 cblstream
19 cbltest
20 cblwav
21 conio
22 conio2
23 dec_test
27 filetest
28 fpsdiv
29 gets
30 gfx_dbuf
31 gfx_demo
32 gfxbanks
33 hello
34 hello2
35 irqtest
36 kbdraw
37 ls
38 malloc
45 ptime
46 rpgprof
47 rpgwalk
48 rt_test
49 scroll
50 seek
51 simple
52 solidt
61 text_palette
62 timedir
63 w0page
64 w3bankgfx
65 w3bgfx
66 w3big
67 w3huge
68 w3probe
69 w3tiny
70 winrest
+49
View File
@@ -0,0 +1,49 @@
/*
* gfx_blit_cols_part_noclip блит спрайта column-major БЕЗ клипа по экрану,
* с обрезкой сверху (как gfx_blit_cols_part). Пара к gfx_blit_cols_part
* ровно так же, как gfx_blit_part_noclip к gfx_blit_part.
*
* Зачем: у персонажей PoP спрайт почти всегда целиком на экране, а общий
* gfx_blit_cols_part платит за проверку четырёх границ, пересчёт w/h/cx/sy и
* 16-битную арифметику независимо от того, вылезает край или нет замер в
* MAME (roomtest, страж + клинок): 5 622 такта подготовки против 13 596 на
* сам accel-проход. Здесь та же колонка уходит в _bgi_blit_cols_raw без
* подготовки.
*
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
* - прямоугольник назначения целиком на экране;
* - ширина и высота кадра <= 255, y <= 255;
* - skip < высоты кадра.
* Не выполняется зовите gfx_blit_cols_part.
*
* Флип, как и в gfx_blit_cols_part, бесплатный: подаём src с последней
* колонки и отрицательный страйд accel сам кладёт её в левый x.
*
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
* собранного с --w3, её вызывать нельзя после _bgi_begin окно W3 занято
* видеобанком и код вызывающего исчезает из адресного пространства.
*/
#include <stdint.h>
#include "../_bgi.h"
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows)
{
const uint8_t *p = (const uint8_t *)img;
uint8_t w = p[0]; /* ширина кадра (< 256) */
uint8_t fh = p[2]; /* высота кадра (< 256) */
const uint8_t *src = p + 4;
uint8_t h;
if (w == 0 || fh == 0 || skip >= fh) return;
h = (uint8_t)(fh - skip);
if (rows && rows < h) h = rows;
if (flip) src += (uint16_t)(w - 1) * fh; /* последняя колонка -> левый x */
src += skip;
_bgi_begin();
_bgi_blit_cols_raw(src, (uint8_t *)(_gfx_addr_base + (uint16_t)x),
w, h, flip ? -(int)fh : (int)fh, (uint8_t)y);
_bgi_end();
}
+38
View File
@@ -0,0 +1,38 @@
/*
* gfx_blit_part_noclip блит ПОД-ПРЯМОУГОЛЬНИКА картинки getimage-формата
* БЕЗ клипа по экрану, в ТЕКУЩЕМ банке (gfx_set_bank). Пара к
* gfx_blit_noclip ровно так же, как gfx_blit_part к gfx_blit.
*
* Зачем: тайловому движку часто нужно положить поверх спрайта не весь
* тайл, а только ту его часть, что перекрывает спрайт. Через
* gfx_blit_part это идёт общим ядром _gfx_blit_full (16-битная
* арифметика, клип, нарезка полос) ~13 К тактов накладных на вызов
* независимо от размера; здесь тот же под-прямоугольник рисуется линейным
* спрайтовым ядром (~4.6 К на 32×3). Замер PoP roomtest 2026-07-29:
* fore-слой поверх персонажа рисовал 9 целых тайлов за 432 К тактов
* (стена = блок 64×63); с обрезкой по прямоугольнику спрайта тех же
* пикселей остаётся втрое меньше.
*
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
* - под-прямоугольник (sx,sy,w,h) лежит внутри картинки;
* - прямоугольник назначения целиком на экране;
* - w,h <= 255, y <= 255.
* Не выполняется зовите gfx_blit_part.
*
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
* собранного с --w3, её вызывать нельзя после _bgi_begin окно W3 занято
* видеобанком и код вызывающего исчезает из адресного пространства.
*/
#include "../_bgi.h"
void gfx_blit_part_noclip(int x, int y, const void *img,
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h)
{
const uint8_t *p = (const uint8_t *)img;
uint16_t stride = (uint16_t)(p[0] | (p[1] << 8));
const uint8_t *src = p + 4 + (uint16_t)sy * stride + sx;
_bgi_begin();
_gfx_blit_sprite_noclip(x, y, src, stride, w, h);
_bgi_end();
}
+9 -3
View File
@@ -46,10 +46,16 @@ void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
* DI/EI-банды по 16 строк leaf делает сам (h: 0=256). Между бандами по * DI/EI-банды по 16 строк leaf делает сам (h: 0=256). Между бандами по
* ширине CPU-адрес двигаем на 256 (Port_Y выбирает строку). */ * ширине CPU-адрес двигаем на 256 (Port_Y выбирает строку). */
while (w) { while (w) {
uint8_t cw = (w > 256) ? 256 : (uint8_t)w; /* 0 => 256 */ // Закомментированный блок должен работать правильно но не работает
// почему-то при w > 256 второй проход мусорит
// TODO: разобраться
// uint8_t cw = (w > 256) ? 256 : (uint8_t)w; /* 0 => 256 */
// _bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y);
// src += 256; dst += 256;
// w -= (w > 256) ? 256 : w;
uint8_t cw = (w > 255) ? 255 : (uint8_t)w; /* 0 => 256 */
_bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y); _bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y);
src += 256; dst += 256; src += cw; dst += cw; w -= cw;
w -= (w > 256) ? 256 : w;
} }
_bgi_end(); _bgi_end();
+18
View File
@@ -105,6 +105,14 @@ void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h);
void gfx_blit_part(int x, int y, const void *img, void gfx_blit_part(int x, int y, const void *img,
int sx, int sy, int w, int h); int sx, int sy, int w, int h);
/* Тот же под-прямоугольник БЕЗ клипа по экрану — линейным спрайтовым
* ядром (пара к gfx_blit_noclip). Вызывающий гарантирует: под-
* прямоугольник внутри img, назначение целиком на экране, w,h,y <= 255.
* Для тайловых движков, кладущих поверх спрайта только перекрывающую его
* часть тайла (PoP: fore-слой). */
void gfx_blit_part_noclip(int x, int y, const void *img,
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h);
/* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel- /* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel-
* проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без * проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без
* CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot- * CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot-
@@ -120,6 +128,16 @@ void gfx_blit_cols(int x, int y, const void *img, uint8_t flip);
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip, void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows); int skip, int rows);
/* То же БЕЗ клипа по экрану — пара к gfx_blit_cols_part так же, как
* gfx_blit_part_noclip к gfx_blit_part. Вызывающий гарантирует: спрайт
* целиком на экране, ширина/высота кадра и y <= 255, skip < высоты.
* rows = 0 «до низа кадра». Экономит ~5.6 К тактов подготовки на вызов
* (замер PoP roomtest: подготовка 5 622 против 13 596 на сам accel-проход),
* что для персонажа со спрайтом-компаньоном (клинок) даёт заметную долю
* кадра. */
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows);
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/ /* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
* оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда * оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда
* работает банком GFX_BANK_NORMAL независимо от gfx_set_bank; * работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;
+4 -4
View File
@@ -30,10 +30,10 @@
#define _IRQ_VEC_BUF_SIZE (256 + 257) #define _IRQ_VEC_BUF_SIZE (256 + 257)
/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в /* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в
* W1). С запасом над реальным размером трамплина (244 Б после * W1). С запасом над реальным размером трамплина (267 Б после
* FIFO-drain + overrun-recovery в raw-клавиатурной ветке, 2026-07-18 * упаковки held-карты клавиатуры в биты, 2026-07-30 проверять
* проверять __irq_tramp_end при правках, запас держать 60 Б). */ * __irq_tramp_end при правках, запас держать >= 60 Б). */
#define _IRQ_TRAMP_BUF_SIZE 320 #define _IRQ_TRAMP_BUF_SIZE 336
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE]; extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
View File
+31 -5
View File
@@ -10,7 +10,8 @@
* (порт данных 0x18 читается один раз, деструктивно либо мы, * (порт данных 0x18 читается один раз, деструктивно либо мы,
* либо DSS, не оба), декодируется make/break (PS/2 Scan Code * либо DSS, не оба), декодируется make/break (PS/2 Scan Code
* Set 2: 0xF0 префикс отпускания, 0xE0 префикс расширенной * Set 2: 0xF0 префикс отпускания, 0xE0 префикс расширенной
* клавиши) в битовую карту `_kbdraw_down[]`, и к DSS НЕ идём * клавиши) в битовую карту `_kbdraw_down[]` (бит на код, 64 Б),
* и к DSS НЕ идём
* (private RETI тот же паттерн, что и у CBL ниже). Пока raw * (private RETI тот же паттерн, что и у CBL ниже). Пока raw
* открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>). * открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>).
* - CBL (бит 7 порта 0xFE) только при зарегистрированном хуке * - CBL (бит 7 порта 0xFE) только при зарегистрированном хуке
@@ -61,6 +62,7 @@ void _irq_tramp(void) __naked
;; break-кода = клавиша «залипает» навсегда (найдено 2026-07-16). ;; break-кода = клавиша «залипает» навсегда (найдено 2026-07-16).
push hl push hl
push de push de
push bc ; маска бита строится в BC (см. ниже)
tr_kbd_drain: tr_kbd_drain:
in a, (#_IRQ_SIO_DATA) ; читаем тем самым дренируем байт in a, (#_IRQ_SIO_DATA) ; читаем тем самым дренируем байт
cp #0xF0 cp #0xF0
@@ -68,7 +70,27 @@ void _irq_tramp(void) __naked
cp #0xE0 cp #0xE0
jr Z, tr_kbd_setext jr Z, tr_kbd_setext
;; --- обычный байт кода: индекс в _kbdraw_down[код(+256 EXT)] --- ;; --- обычный байт кода: БИТ code&7 в байте _kbdraw_down[code>>3]
;; (карта упакована по битам: 64 Б вместо 512, см. _kbdraw.h).
;; Маску строим сдвигом, а НЕ таблицей: таблица потребовала бы
;; `ld hl,#метка` внутри трамплина, а он копируется в W2 побайтно
;; и обязан быть без абсолютных само-ссылок (см. шапку). ---
ld e, a ; E = код
and #7
ld b, a
inc b ; B = (код&7)+1 для djnz
ld a, #1
jr tr_kbd_mskdec
tr_kbd_mskloop:
add a, a
tr_kbd_mskdec:
djnz tr_kbd_mskloop ; A = 1 << (код&7)
ld c, a ; C = маска бита
ld a, e
rrca
rrca
rrca
and #0x1F ; A = код>>3 (код <= 255)
ld hl, #__kbdraw_down ld hl, #__kbdraw_down
ld d, #0 ld d, #0
ld e, a ld e, a
@@ -76,16 +98,19 @@ void _irq_tramp(void) __naked
ld a, (__kbdraw_pending) ld a, (__kbdraw_pending)
bit 1, a ; _KBDRAW_PEND_EXT? bit 1, a ; _KBDRAW_PEND_EXT?
jr Z, tr_kbd_noext jr Z, tr_kbd_noext
ld de, #256 ld de, #32 ; +256 кодов = +32 байта карты
add hl, de add hl, de
tr_kbd_noext: tr_kbd_noext:
ld a, (__kbdraw_pending) ld a, (__kbdraw_pending)
bit 0, a ; _KBDRAW_PEND_BREAK? bit 0, a ; _KBDRAW_PEND_BREAK?
jr Z, tr_kbd_mkdown jr Z, tr_kbd_mkdown
xor a, a ; break снять (0) ld a, c
cpl ; break сбросить бит
and a, (hl)
jr tr_kbd_wr jr tr_kbd_wr
tr_kbd_mkdown: tr_kbd_mkdown:
ld a, #1 ; make взвести (1) ld a, c ; make взвести бит
or a, (hl)
tr_kbd_wr: tr_kbd_wr:
ld (hl), a ld (hl), a
xor a, a xor a, a
@@ -120,6 +145,7 @@ void _irq_tramp(void) __naked
ld a, #1 ld a, #1
ld (__kbdraw_overrun), a ld (__kbdraw_overrun), a
tr_kbd_noovr: tr_kbd_noovr:
pop bc
pop de pop de
pop hl pop hl
pop af pop af
+7 -2
View File
@@ -23,7 +23,12 @@ extern uint8_t _kbdraw_pending; /* FSM декодера make/break, см. в
extern uint8_t _kbdraw_overrun; /* 1 = ISR поймал Rx-overrun SIO (потерян extern uint8_t _kbdraw_overrun; /* 1 = ISR поймал Rx-overrun SIO (потерян
* байт возможно потерян break); снимает * байт возможно потерян break); снимает
* kbd_raw_sync (чистит _kbdraw_down) */ * kbd_raw_sync (чистит _kbdraw_down) */
extern uint8_t _kbdraw_down[512]; /* 0/1 по коду; +256 для расширенных /* Held-состояние: БИТ на скан-код, код>>3 = байт, код&7 = номер бита;
* (префикс 0xE0) см. kbd_raw_down */ * коды 0..255 обычные, 256..511 расширенные (префикс 0xE0), т.е.
* смещение +32 байта. Упаковка вместо байта на код: 64 Б вместо 512
* заметная доля _DATA на 32-килобайтной раскладке (замер PoP roomtest
* 2026-07-30). Ведёт трамплин прерывания (libc/irq/_irq_tramp.c),
* читает kbd_raw_down(). */
extern uint8_t _kbdraw_down[64];
#endif #endif
+1 -1
View File
@@ -9,4 +9,4 @@ uint8_t _kbdraw_active;
uint8_t _kbdraw_pending; uint8_t _kbdraw_pending;
uint8_t _kbdraw_overrun; /* ISR взвёл при Rx-overrun SIO (потерян uint8_t _kbdraw_overrun; /* ISR взвёл при Rx-overrun SIO (потерян
* break) kbd_raw_sync снимет залипшее */ * break) kbd_raw_sync снимет залипшее */
uint8_t _kbdraw_down[512]; uint8_t _kbdraw_down[64]; /* БИТ на скан-код (см. _kbdraw.h) */
+1 -1
View File
@@ -10,5 +10,5 @@
uint8_t kbd_raw_down(uint16_t code) uint8_t kbd_raw_down(uint16_t code)
{ {
if (code >= 512) return 0; if (code >= 512) return 0;
return _kbdraw_down[code]; return (uint8_t)((_kbdraw_down[code >> 3] >> (code & 7)) & 1);
} }
+10 -2
View File
@@ -36,7 +36,15 @@ void kbd_raw_sync(void)
if (!_kbdraw_overrun) return; if (!_kbdraw_overrun) return;
_kbdraw_overrun = 0; _kbdraw_overrun = 0;
for (i = 0; i < 6; i++) save[i] = _kbdraw_down[_kbd_mod_codes[i]]; /* Карта упакована по битам (см. _kbdraw.h): сохраняем/возвращаем
* именно БИТ модификатора, а не байт. */
for (i = 0; i < 6; i++) {
uint16_t c = _kbd_mod_codes[i];
save[i] = (uint8_t)((_kbdraw_down[c >> 3] >> (c & 7)) & 1);
}
memset(_kbdraw_down, 0, sizeof(_kbdraw_down)); memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
for (i = 0; i < 6; i++) _kbdraw_down[_kbd_mod_codes[i]] = save[i]; for (i = 0; i < 6; i++) {
uint16_t c = _kbd_mod_codes[i];
if (save[i]) _kbdraw_down[c >> 3] |= (uint8_t)(1 << (c & 7));
}
} }
+24
View File
@@ -0,0 +1,24 @@
# w3bankgfx — пробник модели «huge + резидент W3 + банк W3 + графика».
#
# Проверяем ровно то, на чём стоит план раскладки PoP (docs/layout_plan_v2.md):
# R2 из __banked-кода резидентная страница W3 недостижима (НЕ зовём);
# R3 резидент -> банк работает через трамплин;
# R4 примитивы libbgi можно звать ИЗ БАНКА: _bgi_begin запоминает текущую
# страницу W3 (порт 0xE2) и _bgi_end её возвращает — значит банк
# переживает рисование и продолжает исполняться.
#
# Раскладка huge: CODE 0x4100 (W1), DATA 0x8000 (W2), банк 0x1C000 (W3),
# резидент --w3 делит W3 с банком (crt0 запоминает резидентную страницу).
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := w3bgfx
MEMORY := huge
EXTRA_FLAGS := --gfx 256 --w3 res.c --bank 1=bank1.c
include $(PROJ_ROOT)/app.mk
# Ожидаемый результат (проверено 2026-07-29, MAME dev):
# пять полос сверху вниз: синяя (резидент), бирюзовая (банк рисует сам),
# жёлтая (W1/W2-функция, вызванная из банка), ЗЕЛЁНАЯ (все проверки ок),
# синяя (резидент жив после возврата). Красная вместо зелёной = модель
# не подтвердилась. Диагностика в переменных bank_page_*/home_page_seen/
# res_page_seen: банк 0xF0, резидент 0xF3, страница W3 внутри банка не
# менялась вокруг рисования.
+48
View File
@@ -0,0 +1,48 @@
/*
* bank1.c __banked модуль (страница W3 подменяется трамплином на время
* вызова). Задача: проверить, что ИЗ БАНКА можно рисовать примитивами
* libbgi и звать функции W1/W2, и что после этого банк ЖИВ (его страница
* вернулась в W3).
*
* Резидент (res_draw) отсюда НЕ зовём сознательно: пока исполняется банк,
* резидентной страницы в адресном пространстве нет (правило R2).
*/
#include <graphics.h>
#include <stdint.h>
extern uint8_t home_helper(uint8_t x); /* функция в W1/W2 (рисует сама) */
/* ВАЖНО: писучие статики __banked-модуля линкуются В СТРАНИЦУ БАНКА
* (адрес 0x1C000+), поэтому снаружи их не прочитать из W1/W2 по 0xC000
* видна РЕЗИДЕНТНАЯ страница. Состояние банка держим в главном модуле. */
extern uint8_t bank_page_before, bank_page_after, bank_page_after_home;
extern uint8_t bank_alive;
/* Текущая страница W3 (порт 0xE2). ОТДЕЛЬНОЙ __naked-функцией: инлайновый
* `in a,(0xE2)` посреди тела затирает A, куда SDCC уже положил параметр
* в первой версии пробника из-за этого цвет заливки становился номером
* страницы (0xF3 = чёрный), и «резидент не рисовал». */
static uint8_t w3_page(void) __naked
{
__asm__("in a, (#0xE2)\n\tret");
}
uint8_t bank_run(uint8_t arg) __banked
{
uint8_t r;
bank_page_before = w3_page();
/* 1) прямое рисование примитивом libbgi (он живёт в W1/W2 и сам
* открывает/закрывает W3-скобку) */
setfillstyle(SOLID_FILL, CYAN);
bar(70, 40, 120, 52);
bank_page_after = w3_page();
/* 2) вызов функции W1/W2, которая тоже рисует */
r = home_helper(arg);
bank_page_after_home = w3_page();
/* 3) банк ещё исполняется — значит его страница на месте */
bank_alive = 1;
return (uint8_t)(r + 1);
}
+27
View File
@@ -0,0 +1,27 @@
/*
* res.c РЕЗИДЕНТНЫЙ модуль окна W3 (--w3): лежит на 0xC000 и вызывается
* напрямую из W1/W2. Рисует через готовые примитивы libbgi это законно:
* скобку _bgi_begin/_bgi_end они открывают внутри себя (сам W3-код открывать
* её НЕ имеет права, иначе исполняемая страница подменяется видеобанком).
*/
#include <graphics.h>
#include <stdint.h>
uint8_t res_page_seen; /* какая страница была в W3 во время вызова */
/* Текущая страница W3 (порт 0xE2). ОТДЕЛЬНОЙ __naked-функцией: инлайновый
* `in a,(0xE2)` посреди тела затирает A, куда SDCC уже положил параметр
* в первой версии пробника из-за этого цвет заливки становился номером
* страницы (0xF3 = чёрный), и «резидент не рисовал». */
static uint8_t w3_page(void) __naked
{
__asm__("in a, (#0xE2)\n\tret");
}
void res_draw(int y, int color)
{
res_page_seen = w3_page();
setfillstyle(SOLID_FILL, color);
bar(10, y, 60, y + 12);
}
+85
View File
@@ -0,0 +1,85 @@
/*
* w3bankgfx пробник раскладки «huge + резидент W3 + банк W3 + графика».
* См. Makefile: проверяются правила R2/R3/R4 из
* applications/PoP/docs/layout_plan_v2.md §2.
*
* Итог рисуется полосами, чтобы читалось и глазом, и скриптом по VRAM:
* y=20 синяя рисовал РЕЗИДЕНТ W3 (до банка)
* y=40 бирюз. рисовал БАНК напрямую примитивом libbgi
* y=60 жёлтая рисовала функция W1/W2, вызванная ИЗ БАНКА
* y=80 зелёная все проверки прошли (иначе КРАСНАЯ)
* y=100 синяя резидент W3 жив ПОСЛЕ возврата из банка
*/
#include <graphics.h>
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
const uint8_t n_banks = 1; /* читает crt0_banked */
extern uint8_t res_page_seen;
uint8_t bank_page_before, bank_page_after, bank_page_after_home, bank_alive;
extern uint8_t bank_pages[];
void res_draw(int y, int color); /* резидент W3 (прямой вызов) */
uint8_t bank_run(uint8_t arg) __banked; /* банк (через трамплин) */
uint8_t home_page_seen;
/* Текущая страница W3 (порт 0xE2). ОТДЕЛЬНОЙ __naked-функцией: инлайновый
* `in a,(0xE2)` посреди тела затирает A, куда SDCC уже положил параметр
* в первой версии пробника из-за этого цвет заливки становился номером
* страницы (0xF3 = чёрный), и «резидент не рисовал». */
static uint8_t w3_page(void) __naked
{
__asm__("in a, (#0xE2)\n\tret");
}
/* Функция W1/W2, которую зовёт БАНК. Рисует — то есть внутри неё libbgi
* подменит W3 на видеобанк и вернёт обратно страницу БАНКА, а не резидента. */
uint8_t home_helper(uint8_t x)
{
home_page_seen = w3_page();
setfillstyle(SOLID_FILL, YELLOW);
bar(130, 60, 180, 72);
return (uint8_t)(x * 2);
}
int main(void)
{
uint8_t ret, ok;
uint8_t res_page, bank_page;
initgraph();
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
res_draw(20, BLUE); /* R3-часть: W1/W2 -> резидент W3 */
res_page = res_page_seen;
ret = bank_run(21); /* R3: резидент/HOME -> банк */
bank_page = bank_page_before;
/* Критерии: банк дожил до конца, вернул r+1 = 21*2+1 = 43, страница W3
* ВНУТРИ банка не менялась вокруг рисования (её вернули), и она НЕ равна
* резидентной. */
ok = (uint8_t)(bank_alive == 1 && ret == 43 &&
bank_page_after == bank_page &&
bank_page_after_home == bank_page &&
home_page_seen == bank_page &&
bank_page != res_page);
setfillstyle(SOLID_FILL, ok ? GREEN : RED); /* зелёная / красная */
bar(10, 80, 60, 92);
res_draw(100, BLUE); /* резидент жив после возврата? */
printf("res_page=%02X bank_page=%02X after=%02X afterhome=%02X home=%02X ret=%u alive=%u ok=%u\n",
res_page, bank_page, bank_page_after, bank_page_after_home,
home_page_seen, ret, bank_alive, ok);
printf("bank_pages[1]=%02X\n", bank_pages[1]);
while (1) { } /* держим картинку для скрипта */
return 0;
}
+41 -5
View File
@@ -11,6 +11,9 @@
# #
# Использование: # Использование:
# toolchain/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ... # toolchain/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ...
# toolchain/make_hdd.sh out.chd prog.exe KID:kid0.atl GUARD:g0.atl ...
# — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог образа
# (каталог создаётся); без префикса файл идёт в корень.
set -e set -e
if [ $# -lt 2 ]; then if [ $# -lt 2 ]; then
@@ -35,18 +38,51 @@ dd if=/dev/zero of="$RAW" bs=512 count=$SECTORS status=none
printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC" printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC"
export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1 export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1
# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16. # mtools собран с enable-raw-term и, когда ему нужно что-то спросить,
mpartition -I -c -t $CYLS -h $HEADS -s $SECS z: # открывает УПРАВЛЯЮЩИЙ ТЕРМИНАЛ напрямую (/dev/tty) — в обход stdin.
mformat z: # Поэтому ни `< /dev/null`, ни перенаправление stdio не спасают: из
# терминала сборка молча вставала навсегда на `mmd z:/BG` (подтверждено
# lsof: fd 0 = /dev/null, fd 4 = /dev/tty, процесс спит). Лечится только
# отсутствием управляющего терминала — запускаем mtools в новой сессии,
# тогда открыть /dev/tty нечего и mtools идёт неинтерактивным путём.
#
# setsid(1) в macOS нет, поэтому делаем то же самое питоном.
# Каналов, откуда mtools может ждать ответ, ДВА, и закрывать надо оба:
# /dev/tty — убирается новой сессией (os.setsid);
# stdin — убирается перенаправлением из /dev/null.
# Закрыть только один недостаточно: с одним лишь setsid mmd переключался на
# stdin и вис уже на нём (lsof: fd 0 = /dev/ttys002).
mt() {
python3 -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \
"$@" < /dev/null
}
# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16.
echo "--- разметка и формат образа ---"
mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z:
mt mformat z:
echo "--- копирование файлов ---"
# Аргумент может быть либо файлом (кладём в корень), либо парой
# КАТАЛОГ:файл — тогда каталог создаётся на образе и файл кладётся туда.
# Имена на Sprinter/DSS — 8.3, каталоги тоже (KID, GUARD, BG, LEVELS...).
for f in "$@"; do for f in "$@"; do
mcopy -o "$f" z: case "$f" in
*:*) dir="${f%%:*}"; src="${f#*:}"
# Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе
# непрошедший вопрос mtools снова окажется невидимым.
mt mmd "z:/$dir" >/dev/null 2>&1 || true
mt mcopy -o "$src" "z:/$dir/" ;;
*) mt mcopy -o "$f" z: ;;
esac
done done
echo "--- содержимое образа ---" echo "--- содержимое образа ---"
mdir z: mt mdir -/ z:
# RAW -> CHD (без сжатия = быстро; та же геометрия). # RAW -> CHD (без сжатия = быстро; та же геометрия).
echo "--- конвертация RAW -> CHD ---"
chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \ chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \
-chs $CYLS,$HEADS,$SECS -ss 512 -f -chs $CYLS,$HEADS,$SECS -ss 512 -f
echo "готово: $OUT_CHD" echo "готово: $OUT_CHD"