Files
Sprinter-SDCC/applications/PoP/roomtest/pop_kid.c
T
snark13 055d6d7c89 Фикс: чтение файлов нельзя выносить в банк — банковый код сам живёт в W3
Первый запуск после разгрузки встал в halt по мусору в 0xC40E.  Причина:
pop_level_read_file и pop_kid_data_load маппят страницу данных в W3 на время
read() (файл читается по 0xC000), а модули банка исполняются ИЗ ЭТОГО ЖЕ
окна — после sprinter_page_w3() следующая инструкция приходит уже из чужой
страницы.  Обе функции вернулись в резидент; в банке остался код, который
ходит через W0 (gfx_w0_map) или окон не трогает вовсе.

Правило записано в _pop_level.h и _pop_kid.h: банковый модуль НЕ маппит W3
(через W0 — можно).  Поэтому же в резиденте живёт весь libbgi.

Куча 6095 Б (было 902 до разгрузки).  Проверено в MAME: игра стартует,
комната рисуется, Кид управляем.  tests-host 6/6.
2026-08-12 22:17:18 +03:00

459 lines
26 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
* seqtbl-интерпретатор. Данные — kid_data.h.
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_kid.h"
#include "pop_trob.h" /* pop_start_chompers — смена ряда будит чомперы */
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h"
#include <fcntl.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include "kid_data.h"
#include "_pop_kid.h" /* дескриптор страницы данных (её грузит банк 8) */
pop_char_t Kid;
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
int8_t knock;
/* Порт глобала next_level: seqtbl-команда END_LEVEL инкрементит его в конце
* seq_70 (подъём по лестнице за дверью уровня), а главный цикл сравнивает с
* текущим уровнем и грузит следующий. Счётчик, а не флаг — ровно как в
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
* легли сюда же без переделки. */
uint8_t pop_next_level;
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ *
* НЕ static: набор спрайтов слота POP_CH_KID общей отрисовки персонажей
* (pop_cdraw.c, банк 4). Отдан как есть, а не через аксессор: читается раз
* в кадр из другого банка, и вызов стоил бы дороже самого чтения. */
#define KID_MAXPAGES 28
atlas_t kidp[KID_MAXPAGES];
uint8_t kid_npages;
/* Таблица «seq_id -> адрес в seqtbl» (original_seqtbl_offsets). Определена
* ЗДЕСЬ, а не в kid_data.h: заголовок включают почти все модули PoP, и
* `static const` в нём размножал таблицу мёртвой копией по каждому TU —
* 230 Б × 9 модулей, из них 460 Б в дефицитном резиденте W1/W2 (замер
* 2026-08-08). Наружу таблица не нужна: смену последовательности делают
* kid_set_seq / pop_char_set_seq. */
static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT;
/* ---- Данные анимации в EMM-странице (kid_data.bin) ------------------ *
* Таблица кадров (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);
/* Адрес кадра 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)
/* Страница данных анимации: заполняет ЗАГРУЗЧИК (pop_kboot.c, банк 8),
* читает интерпретатор здесь. Не static — контракт в _pop_kid.h. */
uint8_t kdat_blk, kdat_page, kdat_ok;
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
* тикает ПОСЛЕ Кида, и без разделения отрисовка Кида взяла бы кадр стража. */
static kframe cur_frame;
/* Кадр Кида для отрисовки — пара к pop_gframe соперника; читает общая
* отрисовка (pop_cdraw.c), поэтому не static. */
kframe kid_frame;
/* 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;
void pop_loadkid(void) { Char = Kid; }
void pop_savekid(void) { Kid = Char; }
/* Выгрузка после control() — теперь ПОЛНАЯ, как savekid оригинала.
*
* Была выборочной (только curr_seq и sword) ровно потому, что pop_map правил
* геометрию ПРЯМО в Kid, мимо окна Char: полное копирование затирало его
* правки — Kid терял выравнивание x и ряд, и спуск с уступа через вис не
* срабатывал (персонаж просто приседал). Приходилось ещё и сравнивать
* последовательность со снимком на входе в окно, чтобы понять, чья правка
* свежее. С переводом физики на Char (GUARD-PHYS) диспетчер и pop_map
* пишут в одно и то же место, и разбирать это больше не нужно. */
void pop_savekid_state(void) { Kid = Char; }
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;
}
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у 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. */
/* Какая ТАБЛИЦА КАДРОВ у персонажа (seg006:0293). Вынесено из load_frame
* отдельной функцией, потому что тот же вопрос решает ОТРИСОВКА: набор
* атласов обязан совпадать с таблицей, иначе image из таблицы Кида ищется
* среди спрайтов стража. Для тени это не теория — она ходит кадрами Кида
* (и рисуется его спрайтами), а дерётся кадрами стража. */
uint8_t pop_frame_tbl_is_guard(int8_t charid, uint8_t frame)
{
return (uint8_t)(charid == CHARID_2_GUARD || charid == CHARID_4_SKELETON ||
(charid == CHARID_1_SHADOW && frame >= 150 && frame < 190));
}
static void load_frame(void)
{
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
* стража frame_tbl_guard с индексом frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535).
*
* СКЕЛЕТ идёт по той же ветке, что страж (seg006:529 — общий case): у
* него свой атлас, но НУМЕРАЦИЯ кадров та же. Пока здесь стоял только
* charid_2_guard, скелет попадал в таблицу Кида, получал несуществующий
* image (180 при 28 спрайтах в наборе) и не рисовался вовсе.
* ТЕНЬ (charid_1_shadow) — по таблице стража ТОЛЬКО в кадрах 150..189,
* в остальных она ходит кадрами Кида (seg006:532). */
const uint8_t *f;
uint8_t use_guard_tbl = pop_frame_tbl_is_guard(Char.charid, Char.frame);
if (use_guard_tbl) {
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) записывало Киду кадр, который
* на самом деле принадлежал СТРАЖУ: отрисовка брала его 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)
{
if (Char.direction < 0) dx = (int8_t)-dx;
return (uint8_t)(Char.x + dx);
}
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
int8_t kid_cur_dx(void) { return cur_frame.dx; }
int8_t kid_cur_dy(void) { return cur_frame.dy; }
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
* 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]; }
/* То же для АКТИВНОГО персонажа — внутри окна 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. */
/* Отложенный start_chompers: взводится байткодом SEQ_UP/SEQ_DOWN, а
* исполняется после выхода из цикла (окно W0 уже свободно). */
static uint8_t chomp_pending;
void play_seq(void)
{
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
for (;;) {
uint8_t cmd = SEQ(Char.curr_seq);
Char.curr_seq++;
switch (cmd) {
case 0xFB: /* DX */
Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
Char.curr_seq++;
break;
case 0xFA: /* DY */
Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
Char.curr_seq++;
break;
case 0xFE: /* FLIP */
Char.direction = (int8_t)~Char.direction;
break;
case 0xF7: /* JMP_IF_FEATHER (seg006:586): под зельем медленного
* падения — перейти по адресу (в seqtbl это ветки
* stepfloat/bumpfloat: «парящие» кадры падения и удара,
* без урона), иначе пропустить адрес и идти дальше.
* Эффект достаётся только Киду — как и сама физика пера
* (fall_accel, pop_map.c). */
if (!pop_feather || Char.charid != CHARID_0_KID) {
Char.curr_seq += 2;
break;
}
/* иначе — ПРОВАЛ в JMP ниже: адрес лежит ровно там же */
/* fallthrough */
case 0xFF: { /* JMP */
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
Char.curr_seq = a;
break;
}
case 0xFD: /* UP */
Char.curr_row--;
chomp_pending = 1; /* start_chompers — ПОСЛЕ цикла, см. ниже */
break;
case 0xFC: /* DOWN */
Char.curr_row++;
chomp_pending = 1;
break;
case 0xF9: /* ACTION */
Char.action = SEQ(Char.curr_seq);
Char.curr_seq++;
break;
case 0xF8: /* SET_FALL */
Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
break;
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
knock = 1;
break;
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
knock = -1;
break;
case 0xF2: /* SOUND (seg006:0611): звука у нас нет, но опкод несёт
* ВТОРОЙ смысл — «Кид нашумел», и его терять нельзя.
* Шумом считаются id 0..2 (SILENT/FOOTSTEP/BUMP); id 0
* назван silent потому, что НЕ звучит, а стражи его всё
* равно замечают (стоит, например, в seq доставания меча).
* DRINK(3)/LEVEL(4) — не шум. См. BUG-GUARD-DEAF-1. */
if (SEQ(Char.curr_seq) < 3) is_guard_notice = 1;
Char.curr_seq++;
break;
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
* В оригинале это `++next_level`, а главный цикл
* (play_level_2, seg003:0386) выходит, когда next_level
* разошёлся с current_level. У нас так же: счётчик, а
* решение — снаружи (roomtest.c). */
pop_next_level++;
break;
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
* (порт play_seq -> proc_get_object, seg006) */
if (SEQ(Char.curr_seq) == 1)
pop_proc_get_object();
Char.curr_seq++;
break;
case 0xF6: /* DIE nop */
break;
default: /* КАДР */
Char.frame = cmd;
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
gfx_w0_unmap();
/* start_chompers (seg006:0603/0607) — ТОЛЬКО здесь, после
* размаппинга. В оригинале он зовётся прямо из SEQ_UP/SEQ_DOWN,
* но у нас seqtbl читается ЧЕРЕЗ ОКНО W0, а start_chompers лезет
* за тайлами уровня и это окно перемапливает и закрывает — цикл
* дальше исполнял бы мусор вместо байткода. Наступили на это
* 2026-08-08: после спуска Кид проваливался сквозь пол. */
if (chomp_pending) {
chomp_pending = 0;
pop_start_chompers(Char.room, Char.curr_row);
}
return;
}
}
}
/* load_fram_det_col (seg006:0144) = load_frame() + determine_col().
*
* Зачем это отдельной публичной точкой. cur_frame — ОДИН глобал на всех
* персонажей (так и в оригинале), и его владелец — тот, кто последним
* прошёл load_frame. В кадре последним тикает страж, поэтому к моменту
* управления Кидом в cur_frame лежит кадр СТРАЖА. А через cur_frame
* считается вся геометрия: dx_weight -> determine_col ->
* distance_to_edge_weight -> get_edge_distance, то есть буквально выбор
* ветки в check_jump_up.
*
* Оригинал от этого страхуется явно: play_kid_frame (seg000:1209) и
* play_guard_frame (seg000:1246) сразу после loadkid/loadshad зовут
* load_fram_det_col() — ДО control(). У нас этого не было, и Кид считал
* свой прыжок по кадру стража (BUG-CTRL-FRAME-1): при distance 2 вместо 10
* check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и
* зацеп». Результат менялся от кадра к кадру — вместе с анимацией стража.
*
* determine_col теперь общий: pop_map работает с Char, поэтому колонка
* пересчитывается ЛЮБОМУ активному персонажу — ровно как load_fram_det_col
* в оригинале. Раньше вторая половина связки работала только на ветке Кида
* (стражу колонку вёл pop_guard_enter/leave) — прямое следствие того, что
* физика знала один Kid. */
void pop_load_frame(void)
{
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
load_frame();
gfx_w0_unmap();
}
void pop_load_fram_det_col(void)
{
pop_load_frame();
pop_determine_col();
}
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
* 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)
{
Kid.curr_col = col;
Kid.curr_row = row;
Kid.direction = dir;
Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Kid.y = (uint8_t)pop_y_land[row + 1];
Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0;
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
Kid.curr_seq = kid_seq_off[seq_id];
pop_kid_play(); /* до первого кадра */
}
void kid_tick(void)
{
pop_kid_play();
}
/* Отрисовка кадра (load_frame_to_obj, heal, клип, брызги, клинок) живёт в
* pop_cdraw.c — ОДНА на всех Char, см. DRAW-CHAR. Отсюда она получает
* атласы Кида (kidp/kid_npages) и его кадр (kid_frame). */
/* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
/* ЖИВЁТ В РЕЗИДЕНТЕ, хотя зовётся раз за игру: маппит страницу в W3 на
* время чтения, а банковый код сам лежит в W3 (см. _pop_kid.h). */
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;
}