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>
This commit is contained in:
2026-07-30 10:32:15 +03:00
parent 8bbc6b4d07
commit cf06896dbd
4 changed files with 47 additions and 15 deletions
+28 -15
View File
@@ -75,13 +75,20 @@
* автомат held/released/ignore (авто-повтор); ctrl1_* — их память между * автомат held/released/ignore (авто-повтор); ctrl1_* — их память между
* кадрами (rest/save). */ * кадрами (rest/save). */
static int8_t control_x, control_y, control_shift; static 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)
{ {
@@ -125,7 +132,7 @@ 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);
} }
@@ -190,15 +197,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) */
@@ -266,7 +273,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) {
@@ -318,16 +325,16 @@ static void control_jumpup(void)
} }
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */ /* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */
static void control(void) void pop_control(void)
{ {
uint8_t f = Kid.frame; uint8_t f = Char.frame;
/* 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;
} }
@@ -406,12 +413,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 +458,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();
} }
+15
View File
@@ -32,4 +32,19 @@ 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)
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
+3
View File
@@ -134,6 +134,9 @@ pop_char_t Char;
void pop_loadkid(void) { Char = Kid; } void pop_loadkid(void) { Char = Kid; }
void pop_savekid(void) { Kid = Char; kid_frame = cur_frame; } void pop_savekid(void) { Kid = Char; kid_frame = cur_frame; }
/* Выгрузка БЕЗ кадра: control() кадр не трогает, а cur_frame в этот
* момент принадлежит тому, кто последним крутил play_seq. */
void pop_savekid_state(void) { Kid = Char; }
void pop_loadshad(void) { Char = Guard; } void pop_loadshad(void) { Char = Guard; }
void pop_saveshad(void) { Guard = Char; pop_gframe = cur_frame; } void pop_saveshad(void) { Guard = Char; pop_gframe = cur_frame; }
+1
View File
@@ -61,6 +61,7 @@ void play_seq(void); /* работает с Char — см. pop_kid_play
extern pop_char_t Char; extern pop_char_t Char;
void pop_loadkid(void); void pop_loadkid(void);
void pop_savekid(void); void pop_savekid(void);
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
void pop_loadshad(void); void pop_loadshad(void);
void pop_saveshad(void); void pop_saveshad(void);