Skip to content

Latest commit

 

History

History
769 lines (583 loc) · 51.7 KB

File metadata and controls

769 lines (583 loc) · 51.7 KB

Деобфускация Bounce Back

Цель: Справочник для понимания обфусцированного кода bounce_back_s60.jar
Статус: В процессе заполнения по мере анализа
Использование: Для AI агента и человека при портировании на SDL2


Правила порта на PSP

1. Расширение экрана до размеров PSP (480×272)

Оригинал (J2ME): 176×208 пикселей (~11×13 тайлов)
PSP target: 480×272 пикселей (~30×17 тайлов)

Ключевые принципы:

  • Pixel-perfect рендеринг уровня сохраняется (тайлы 16×16 остаются 16×16)
  • Увеличиваем viewport (обзор на уровень), а не масштабируем графику
  • ✅ Игрок видит больше игрового пространства (wider field of view)
  • ✅ Физика остается идентичной оригиналу (1 пиксель = 1 пиксель)

Viewport размеры:

  • J2ME: 176×208 px = ~11×13 тайлов (176÷16 × 208÷16)
  • PSP: 480×272 px = ~30×17 тайлов (480÷16 × 272÷16)
  • Прирост обзора: +170% по горизонтали, +30% по вертикали

UI адаптация:

  • Элементы UI масштабируются или перепозиционируются
  • Используем widescreen layout (жизни слева, очки справа)
  • Меню центрируется или растягивается на полный экран

2. Камера и физика камеры из bounce_zero

Источник: bounce_zero/src/game.c (строки 60-350)

Реализация из bounce_zero (портировать как есть):

// bounce_zero/src/game.c — camera с мертвой зоной
#define CAMERA_UNINITIALIZED -999
#define CAMERA_DEADZONE_PERCENT 30   // 30% от игровой области - зона без движения камеры
static int s_currentCameraY = CAMERA_UNINITIALIZED;

// Горизонтальная камера — простое центрирование на игроке
int cameraX = player->xPos - SCREEN_WIDTH / 2;

// Вертикальная камера — с мертвой зоной
if (s_currentCameraY == CAMERA_UNINITIALIZED) {
    int gameAreaHeight = SCREEN_HEIGHT - HUD_HEIGHT;
    s_currentCameraY = player->yPos - gameAreaHeight / 2;
}

int gameAreaHeight = SCREEN_HEIGHT - HUD_HEIGHT;
int deadZoneTop = (gameAreaHeight * CAMERA_DEADZONE_PERCENT) / 100;
int deadZoneBottom = gameAreaHeight - deadZoneTop;

int tempPlayerScreenY = player->yPos - s_currentCameraY;
if (tempPlayerScreenY < deadZoneTop) {
    s_currentCameraY = player->yPos - deadZoneTop;
} else if (tempPlayerScreenY > deadZoneBottom) {
    s_currentCameraY = player->yPos - deadZoneBottom;
}

// Clamp камеры по границам уровня
int maxCameraX = g_level.width * TILE_SIZE - SCREEN_WIDTH;
int maxCameraY = g_level.height * TILE_SIZE - gameAreaHeight;
if (cameraX < 0) cameraX = 0;
if (cameraX > maxCameraX && maxCameraX > 0) cameraX = maxCameraX;
if (cameraY < 0) cameraY = 0;
if (cameraY > maxCameraY && maxCameraY > 0) cameraY = maxCameraY;

Ключевые особенности:

  1. Горизонталь (X): Всегда центрируется на игроке (без deadzone)
  2. Вертикаль (Y): С мертвой зоной 30% (игрок может двигаться внутри зоны без сдвига камеры)
  3. Маленькие уровни: Центрируются вертикально, камера не двигается
  4. HUD: Вычитается из игровой области (bounce_zero: HUD_HEIGHT = 17 px)

Адаптация для bounce_back:

  • ✅ Использовать ту же логику для cameraX и cameraY
  • SCREEN_WIDTH = 480, SCREEN_HEIGHT = 272 для PSP
  • CAMERA_DEADZONE_PERCENT = 30 (проверено в bounce_zero)
  • ⚠️ Учесть wrap флаги из /res/tf (clampX/clampY):
    • Если clampX == 0 (wrap) — применить wrap для cameraX
    • Если clampY == 0 (wrap) — применить wrap для cameraY
  • ⚠️ Bounce Back использует framerate 20 FPS (50ms tick), bounce_zero — 30 FPS
    • Deadzone логика работает независимо от FPS
    • Просто вызывать обновление камеры каждый tick

Рендеринг с камерой (из bounce_zero/src/level.c:417):

void level_render_visible_area(int cameraX, int cameraY, 
                                int screenWidth, int screenHeight) {
    int startTileX = cameraX / TILE_SIZE;
    int endTileX   = (cameraX + screenWidth  - 1) / TILE_SIZE;
    int startTileY = cameraY / TILE_SIZE;
    int endTileY   = (cameraY + screenHeight - 1) / TILE_SIZE;
    
    // Рисуем только видимые тайлы
    for (int y = startTileY; y <= endTileY; y++) {
        for (int x = startTileX; x <= endTileX; x++) {
            int screenX = x * TILE_SIZE - cameraX;
            int screenY = y * TILE_SIZE - cameraY;
            // draw_tile(tile, screenX, screenY);
        }
    }
}

Контракт камеры для bounce_back:

typedef struct {
    int x;              // Текущая позиция камеры X (мировые координаты)
    int y;              // Текущая позиция камеры Y (с учетом deadzone)
    bool initialized;   // Флаг первой инициализации
} Camera;

void camera_update(Camera* cam, int player_x, int player_y,
                   int level_width, int level_height,
                   bool clamp_x, bool clamp_y);
void camera_reset(Camera* cam);  // Вызывать при загрузке уровня/респауне

Структура классов

Основные классы

Обфусц. имя Предполагаемое имя Роль Строк Статус анализа
CrystalMidlet.java CrystalMidlet Главный класс MIDlet, entry point 593 ✅ Понятен
a.java Player / Ball Класс игрока/мяча с физикой (ввод, базовая физика, бонусы, инверсия); остаются: враги и полный маппинг ID/тайлов 1676 ✅ Понятен (ядро)
h.java GameCanvas Основной игровой холст (game loop, загрузка ресурсов/уровней, демо/реплей, ввод); остаются: враги и полный маппинг состояний/RMS 1237 ✅ Понятен (ядро)
g.java Level / TileMapLayer Тайловая карта, отрисовка и pixel-perfect коллизии 722 ✅ Понятен (ядро)
i.java MenuCanvas / MenuUI Меню, диалоги, help/records, анимации переходов; остаются: полный маппинг screenId (g) и режимов uiMode (V) 917 ✅ Понятен
c.java ResourceContainer Контейнер бинарных ресурсов /res/* 86 ✅ Понятен
f.java GameTimer Timer обертка для game loop 32 ✅ Понятен
d.java InputReplayPlayer / DemoPlayback Загрузка и воспроизведение демо/реплея из /res/r (подмена ввода) 79 ✅ Понятен
e.java ReplayQueue / InputScriptState (?) Контейнер под список ReplayEvent; в исходниках прод-сборок не используется (нет new e() и обращений к aa.*) 17 ⚠️ Не используется
b.java ReplayEvent (node) Узел связного списка событий реплея: (tick, actionCode, value) 22 ✅ Понятен

Исполняемая модель (game loop) — 50 ms tick как контракт

Для порта на PSP критично сначала зафиксировать что выполняется каждый тик, потому что одновременно меняются таймеры/рендер/ввод/аудио/ФС.

  • Tick = 50 ms (20 FPS), запуск через GameTimer (f.java).
  • Стадии tick в оригинале — это дискретный ввод press/release → demo inject → враги → игрок → тайлы/таймеры → камера/viewport → repaint.

Отдельный “1‑страничный” контракт тика вынесен в GAME_LOOP_SPEC.md.


Класс Player (a.java)

Ключевые поля состояния

Обфусц. имя Предполагаемое имя Тип Описание Строка
this.D xPos int X координата в пикселях (делить на 16 для тайла) 14
this.i yPos int Y координата в пикселях (делить на 16 для тайла) 16
this.m inputMask int Битовая маска ввода (1=left, 2=right, 4=down, 8=jump) 38
this.k enemyCollisionIndex int Индекс врага при коллизии (-1 если нет) 40
this.s xSpeed int Горизонтальная скорость 22
this.h ySpeed int Вертикальная скорость 24
this.z prevYSpeed int Предыдущая вертикальная скорость 28
this.G bounceVelocity int Скорость отскока 26

Флаги состояния (boolean)

Обфусц. имя Предполагаемое имя Описание Строка
this.t isLarge true = большой мяч (16px), false = маленький (12px) 48
this.I isInverted true = инвертированная гравитация 50
this.F isPopped true = лопнувший мяч (слабый прыжок) 52
this.e isDying true = в процессе смерти 54
this.x isGrounded true = на земле 46
this.p gravityDown true = гравитация вниз, false = вверх 58
this.j hasSpeedBonus true = бонус скорости активен 60
this.C hasJumpBonus true = бонус прыжка активен 62
this.l hasGravBonus true = бонус гравитации активен 64

Счетчики (int)

Обфусц. имя Предполагаемое имя Описание Строка
this.B bonusCounter Счетчик длительности бонуса (450 тиков) 66
this.b deathAnimCounter Счетчик анимации смерти 68
this.A transformCounter Счетчик анимации трансформации размера 42
this.g spriteIndex Индекс текущего спрайта мяча (0-24) 44

Размеры спрайта

Обфусц. имя Предполагаемое имя Описание Строка
this.d spriteWidth Ширина текущего спрайта 30
this.u spriteHeight Высота текущего спрайта 32
this.J halfWidth Половина ширины (для центрирования) 34
this.c halfHeight Половина высоты (для центрирования) 36

Ключевые методы

Обфусц. имя Предполагаемое имя Описание Строка
d() update() Основной цикл физики и обновления 599
h() calculateJumpStrength() Расчет силы прыжка (-125/-180/-95) 543
f() calculateBounceStrength() Расчет силы отскока 558
k() die() Смерть игрока ?
l() startEnlarge() Начать увеличение мяча 442
b() startShrink() Начать уменьшение мяча 456
c() resetTransform() Сброс трансформации 470
a(int) setSprite(int) Установить спрайт по индексу 267
c(int) addInput(int) Добавить ввод в маску 207
b(int) removeInput(int) Убрать ввод из маски 214
i() clearInput() Очистить маску ввода 221
c(int,int) collectBonus(tileX,tileY) Сбор бонуса на тайле 489

Что осталось уточнить

  • Полностью подтверждены: ввод (inputMask), базовая физика, инверсия, бонусы по флагам/счетчикам и основные константы.
  • Не закрыто: точная семантика всех tile/entity ID (особенно враги), а также полная связь с форматом /res/tf (collision/transform/animation).

Класс GameCanvas (h.java)

Основные поля

Обфусц. имя Предполагаемое имя Тип Описание Строка
this.H midlet CrystalMidlet Ссылка на главный MIDlet 15
this.e player a (Player) Экземпляр игрока 17
this.Z currentLevel g (Level) Текущий уровень 29
this.A backgroundLevel g (Level) Фоновый слой 29
this.d levelIndex int Индекс текущего уровня (0-21) 45
this.aj gameTimer f (GameTimer) Игровой таймер (50ms) 53

Методы

Обфусц. имя Предполагаемое имя Описание Строка
run() update() Главный игровой цикл 752
a() startTimer() Запуск таймера (50ms interval) 811
h() stopTimer() Остановка таймера 832
b(int) loadLevel(int) Загрузка уровня по индексу 187

Что осталось уточнить

  • Понятно: главный цикл (run()), загрузка ресурсов/уровня, связь слоев g (foreground/background), система демо (d.java), базовая обработка ввода.
  • Не закрыто: полноценная система врагов (данные из /res/lf), все состояния/экраны и часть логики сохранения/восстановления (RMS/RecordStore).

Класс Level (g.java)

g.java — слой тайловой карты (foreground/background) с:

  • загрузкой метаданных тайлов из /res/tf (2 контейнера),
  • загрузкой изображений тайлов из /res/if* (foreground) или /res/ib* (background),
  • хранением tileMap (байты тайлов + флаги),
  • pixel-perfect коллизиями через boolean-маски на тайл,
  • поддержкой wrap-мира (по X/Y) и/или clamp-мира.

Ключевые поля

Обфусц. имя Предполагаемое имя Тип Описание Строка
this.R tileMap byte[][] Карта тайлов: [rows=n][cols=ac], байт содержит tileId + флаги 80
this.n rows int Кол-во строк тайлов (Y) 68
this.ac cols int Кол-во столбцов тайлов (X) 72
this.f tileW int Ширина тайла (обычно 16) 34
this.A tileH int Высота тайла (обычно 16) 36
this.ae worldW int Ширина мира в пикселях (cols * tileW) 74
this.a worldH int Высота мира в пикселях (rows * tileH) 70
this.d clampX boolean clamp по X (если false — wrap) 14
this.X clampY boolean clamp по Y (если false — wrap) 12
this.z tileIdMask int Маска выделения tileId из байта (по умолчанию 0x7F) 24
this.q tileFlagMask int Маска флагов в байте (по умолчанию 0x80) 26
this.v tileType byte[] Тип/класс тайла для рендера/логики (из /res/tf) 40
this.l collisionType byte[] Тип коллизии: 0=нет; 1/2/3 используются в collisionTest 48
this.s collisionMasks boolean[][][] Pixel-mask на тайл (для collisionType=3) 50
this.T imageIndex int[] Индекс картинки/кадра для тайла 42
this.b transform byte[] Трансформации (rotate/flip) 44
this.af aux int[] Для tileType=3 — индекс анимации/группы; также цвет заливки 52
this.U animCount int Кол-во анимационных групп 58
this.m animFrames byte[][] Кадры анимаций (как tileId байты) 62
this.O animPeriod byte[] Период (тик) на группу 60
this.ai animFrameIndex byte[] Текущий кадр на группу 66
this.aa animTimer byte[] Таймер до смены кадра 64
this.Y hitCols int[] Список X тайлов, где была коллизия (до 5) 54
this.P hitRows int[] Список Y тайлов, где была коллизия (до 5) 56

Ключевые методы

Обфусц. имя Предполагаемое имя Описание Строка
g(...) Level(...) Конструктор: парсит заголовок из /res/tf, грузит изображения из paramString1/2, читает tileMap 138
c(int,int) setCamera(pxX, pxY) Установить целевую позицию камеры в пикселях с wrap/clamp 283
d() tick() Обновить анимации + пересчитать/перерисовать видимую область 296
a(Graphics) draw(Graphics) Нарисовать слой в viewport 430
a(int,int,int,int,mask,collect) collisionTest(x,y,w,h,mask,collectHits) Pixel-perfect коллизия с тайлами; при collectHits=true заполняет hitCols/hitRows 315
a(int) worldToScreenX(int) Преобразовать мировую X в экранную (учитывая камеру) 452
b(int) worldToScreenY(int) Преобразовать мировую Y в экранную 456
b(int,int) wrap(int,mod) Нормализация для wrap 699
a(int,int) clamp(int,max) Clamp в [0..max-1] 707

Встраивание в архитектуру

  • В h.b(levelIndex) создаются два слоя g:
    • Z (foreground) через /res/tf + /res/if0 и /res/if{theme}.
    • A (background) через /res/bg + /res/ib0 и опционально /res/ib{theme}.
  • h.run() каждый тик вызывает Z.c(playerX, playerY); Z.d(); и A.c(camX, camY); A.d();, затем Z.a(g)/A.a(g) внутри paint.

Итог: формат /res/tf разобран (см. раздел ниже). В g.java ключевые перечисления читаются так:

  • renderType (v): 0=пусто (не рисовать), 1=рисовать картинку, 3=анимированный тайл (через anim-group).
  • collisionType (l): 0=нет, 1=mask без трансформаций, 2=сплошной solid, 3=mask с учетом transform (rotate/flip) + возможный алиас маски через aux.
  • transform (b): битовое поле (низкие 2 бита — поворот 0/90/180/270; биты 0x8/0x4 — flip), используется и в рендере, и в collisionType=3.

Класс MenuCanvas / MenuUI (i.java)

i.java — универсальный экран-меню/диалог на FullCanvas с таймером анимаций (класс f), поддержкой списка пунктов, длинных текстов (Help/Controls), а также “переходов” (slide-in/slide-out).

Ключевые поля

Обфусц. имя Предполагаемое имя Тип Описание Строка
this.H midlet CrystalMidlet Обратные вызовы, звук (midlet.a/b/d) 54
this.z items String[] Пункты меню / строки списка 24
this.X selectedIndex int Выбранный пункт (или top-index в некоторых режимах) 36
this.g screenId byte ID экрана/меню (читается в CrystalMidlet.commandAction) 56
this.V uiMode byte Режим UI: 0 обычное меню, 1 скролл-лист, 2 текстовые страницы, 3 опции/особый режим 58
this.w commandListener CommandListener Обычно CrystalMidlet 38
this.d selectCmd Command Команда выбора (type 4) 42
this.m backCmd Command Назад (type 2/7) 44
this.u timer f Таймер анимаций (Runnable.run()) 12
this.p opening boolean Анимация открытия активна 32
this.A closing boolean Анимация закрытия активна 34
this.W openTicks int Счетчик тиков открытия (обычно 22) 16
this.v closeTicks int Счетчик тиков закрытия (обычно 22) 20
this.q pages String[] Страницы текста (Help/Controls) 74
this.ab pageIndex int Индекс текущей страницы текста 72
this.x scrollY int Вертикальный скролл текста (режим uiMode=2) 70
this.C menuBg Image Фон меню из /res/im 100
this.M menuEdge Image Боковая “рамка”/градиент из /res/im 102
this.E scrollArrow Image Стрелка скролла (top) из /res/im 96

Ключевые методы

Имя Роль
paint(Graphics) Рендер меню/диалога, режимы opening/closing/uiMode
run() Тик анимаций: open/close + мигание выделения
keyPressed(int) / a(int) Навигация, обработка softkeys, управление скроллом/страницами
a(String[], byte, String, String, String, byte) Настройка экрана списка: items, screenId, подписи softkeys, заголовок, uiMode
a(String[], String[], byte, String, String, String) Настройка экрана с текстом (pages) + меню (items), принудительно uiMode=2
c(boolean) Загрузка графики меню из /res/im (5 объектов)
a(Graphics, String, ...) (static) Утилита переноса строк/рисования текста по ширине (используется и вне меню)
a(String) Эллипсис строки под ширину (~84px) для подписи softkeys

Встраивание в архитектуру

  • CrystalMidlet создает/перенастраивает один экземпляр i (this.D) и показывает его через Display.setCurrent(this.D).
  • Все действия меню отдаются назад в CrystalMidlet.commandAction(...), где логика ветвится по this.D.g (screenId) и this.D.X (выбор).
  • В uiMode=2 (Help/Controls) экран рисует длинный текст и поддерживает скролл (вниз только если i.a(...) вернул, что текст не помещается).

Класс ResourceContainer (c.java)

Назначение: Парсер бинарных контейнеров /res/*

Формат контейнера:

[2 байта] count - количество элементов (big-endian)
[count * 2 байта] размеры элементов (big-endian shorts)
[переменная длина] данные элементов

Методы

Имя Описание
c(int) Загрузить элемент по индексу (для двойных индексов: index << 1)
a() Получить весь файл целиком

Система демо/реплея (d.java + b.java + /res/r)

Класс ReplayEvent (b.java)

Назначение: Узел связного списка событий, загружаемых из /res/r и проигрываемых по тикам.

Поле (обфусц.) Предполагаемое имя Тип Смысл
d tick short Номер тика, на котором срабатывает событие
b actionCode int Код действия (см. таблицу ниже)
c value byte Значение/параметр действия
a next b Следующий узел

Где используется: d.java хранит очередь событий как b-узлы.

Класс InputReplayPlayer / DemoPlayback (d.java)

Назначение: Считать /res/r в связный список ReplayEvent, а затем на каждом тике подменять массив состояния ввода h.b (GameCanvas).

Загрузка данных: d.a()

  • Открывает getResourceAsStream("res/r")
  • Читает до EOF записи формата: short tick, int actionCode, byte value
  • Складывает их в связный список (поля c/a в d.java)

Проигрывание: d.a(byte[] inputState)

  • d.b — текущий тик (инкрементируется каждый вызов)
  • Пока currentEvent.tick == currentTick, выставляет значения в inputState[] и двигается дальше
  • Если события закончились — выставляет d.d = true (флаг завершения демо)

Маппинг actionCode → inputState (h.b):

actionCode inputState index Эффект в h.c()
8 0 Jump (Player.addInput(8) / removeInput(8))
4 1 Down (addInput(4) / removeInput(4))
1 2 Left (addInput(1) / removeInput(1))
2 3 Right (addInput(2) / removeInput(2))
20 4 Выход из демо (в h.c() прекращает au и вызывает midlet.b(false))
21 5 Триггер показа подсказки (в h.c() вызывает h.e() и сбрасывает b[5])

Семантика value:

  • Для actionCode ∈ {1,2,4,8}: 1 = keyPressed, 2 = keyReleased (значение записывается в inputState[index]).
  • Для actionCode = 21: по содержимому /res/r похоже, что value — это hintId (1..5) для выбора строки h.r[hintId-1].
    • В текущем decompile h.c() использует только факт b[5] != 0 и не присваивает this.G, хотя h.e() рисует r[this.G]. Вероятно, в оригинале было присваивание вроде this.G = inputState[5] - 1 перед вызовом e().

Как встраивается в архитектуру игры

  1. CrystalMidlet.h() (старт демо) создает h (GameCanvas), грузит уровень 21, назначает h.au = new d() и вызывает h.au.a() (загрузка /res/r).
  2. В h.run() каждый тик, если au != null, выполняется au.a(this.b) — обновление массива ввода b по сценарию.
  3. h.c() преобразует b[0..3] в битовую маску ввода игрока (addInput/removeInput), а также обрабатывает спец-события b[4] (выход) и b[5] (подсказка).
  4. Когда d.d == true, h.run() завершает демо: au = null; midlet.e().

Класс ReplayQueue / InputScriptState (e.java) — статус: неясно / возможно не используется

e.java — “struct”-класс без методов. По полям очень похож на контейнер-обертку вокруг связного списка ReplayEvent (b.java).

Поля

Поле (обфусц.) Тип Возможный смысл
c short Счетчик/текущий тик/идентификатор (по аналогии с d.b, но short)
a byte Режим/стадия (по умолчанию 1)
e boolean Флаг активности/завершения (по аналогии с d.d)
b b Голова списка ReplayEvent
d b Хвост списка ReplayEvent

Наблюдения по интеграции (архитектурно)

  • В исходниках этой сборки нет ни одного new e() и нет обращений к полям aa.*.
  • Единственное вхождение — поле h.aa и проверка if (this.aa != null) в h.run() при обработке ap (выход/назад).
  • Поэтому в текущем decompile e.java выглядит как заготовка под вторую “ленту” событий (например: запись ввода, скрипт подсказок, альтернативный реплей), но код, который должен ее создавать/обслуживать, либо отсутствует, либо не попал в decompile.

Карта ресурсов /res/

Основные контейнеры

Файл Назначение Формат Размер, КБ Объектов Используется в Примечания
/res/lf Level Files - данные уровней Бинарный контейнер 61.4 44 h.java:187 Индекс: levelNum << 1 (22 уровня × 2)
/res/tf Tile Format - метаданные тайлов Бинарный контейнер 4.2 2 h.java:254 Collision type, sprite index, transform, animations
/res/if0 Foreground Tileset (base) - изображения тайлов (общие) Бинарный контейнер 32.5 104 h.java:259 Подается как paramString1 в new g(..., \"/res/if0\", \"/res/if\"+theme, ...)
/res/if1 Foreground Tileset (theme=1) - изображения тайлов темы 1 Бинарный контейнер 1.0 7 h.java:259 Подается как paramString2; дополняет/заменяет часть изображений из if0
/res/if2 Foreground Tileset (theme=2) - изображения тайлов темы 2 Бинарный контейнер 1.9 7 h.java:259 Подается как paramString2; дополняет/заменяет часть изображений из if0
/res/b Ball sprites - спрайты мяча Бинарный контейнер 7.2 26 a.java:83 Анимация мяча (маленький/большой/лопнувший)
/res/bg Background - фоновые данные Бинарный контейнер 0.1 3 h.java:267 Статичные фоны
/res/ib0 Image Background 0 - анимир. фон 0 Бинарный контейнер 1.0 1 h.java:276 Анимированный фон темы 0
/res/ib1 Image Background 1 - анимир. фон 1 Опциональный контейнер h.java:268-270 В проде может отсутствовать: h.java проверяет ресурс и передает null, тогда фон грузится только из /res/ib0
/res/ib2 Image Background 2 - анимир. фон 2 Опциональный контейнер h.java:268-270 В проде может отсутствовать: h.java проверяет ресурс и передает null, тогда фон грузится только из /res/ib0
/res/ic Image Components - UI элементы Бинарный контейнер 5.0 12 h.java:352 Жизни, кольца, другие UI
/res/im Image Menu - изображения меню Бинарный контейнер 9.4 5 i.java:840 Элементы меню и заставок
/res/s Sounds - звуковые эффекты Бинарный контейнер 0.4 11 CrystalMidlet.java:553 Элементы похожи на Nokia OTT
/res/i Icon - иконка приложения PNG 0.7 1 MANIFEST.MF:6 Один PNG-файл
/res/r Replay / Demo Script - сценарий демо Поток записей 0.2 31 d.java:17 31 запись по 7 байт: short tick + int actionCode + byte value

Дополнительные папки (сгенерированные?)

Папка Назначение
kjmGlobal_images_theme0/ Распакованные текстуры темы 0 (для отладки?)
kjmGlobal_images_theme1/ Распакованные текстуры темы 1
kjmGlobal_images_theme2/ Распакованные текстуры темы 2

Формат данных уровня (/res/lf)

Загрузка: h.java:187-302

/res/lf — контейнер класса c из 44 chunk’ов: для каждого уровня levelIndex:

  • chunk[2*levelIndex] — metadata (header + enemies)
  • chunk[2*levelIndex + 1] — tileMap

Metadata chunk: заголовок:

	byte theme_id;        // b1 - индекс темы (0, 1, 2)
	byte spawn_y;         // b2 - Y координата спавна в тайлах
	byte spawn_x;         // b3 - X координата спавна в тайлах  
	byte ball_type;       // b4 - 0=маленький, другое=большой
	byte exit_y_tiles;    // this.ar - Y тайла выхода (h.java:199, h.java:640)
	byte exit_x_tiles;    // this.D  - X тайла выхода (h.java:200, h.java:641)
	byte enemy_count;     // j - количество врагов
	// Далее: enemy_count записей по 9 байт (moving objects / enemies)
	```

**Подтверждение смысла `exit_x/exit_y` по коду:**
- `h.b(levelIndex)` читает `this.ar` и `this.D` из meta-chunk сразу после `ball_type` (h.java:195-201).
- В `h.c(Graphics)` эти значения используются как координаты тайла выхода: `n=this.ar`, `i1=this.D`; затем один раз вычисляются пиксельные координаты двери `P=i1*16`, `R=n*16` (h.java:640-646).
- Условие завершения уровня в `a.d()` проверяет попадание игрока в прямоугольник `32×48` относительно `E.P/E.R` при `E.o==true` (a.java:607-612), то есть `E.P/E.R` — именно позиция двери в пикселях, а `this.ar/this.D` — её позиция в тайлах.

### Спавн игрока (пиксели)

`h.b(level)` превращает `spawn_x/spawn_y` в пиксели так:
- `spawnPx = spawnTiles * 16 + 8`, если `ball_type == 0`
- `spawnPx = spawnTiles * 16 + 12`, если `ball_type != 0`

И выбирает стартовый спрайт мяча:
- `ball_type == 0` → sprite index `0`
- иначеsprite index `11`

### Enemy record: ровно 9 байт (h.java:208-240)

| Offset | Type | Имя (предлагаемое) | Описание / куда кладется |
|---:|---:|---|---|
| 0 | `i8` | `enemyType` | `this.ao[i]` (используется как тип поведения/спрайта) |
| 1 | `i8` | `tileX0` | `this.f[i][0]` (один угол bbox/области движения в тайлах) |
| 2 | `i8` | `tileY0` | `this.f[i][1]` |
| 3 | `i8` | `tileX1` | `this.k[i][0]` (второй угол bbox/области движения) |
| 4 | `i8` | `tileY1` | `this.k[i][1]` |
| 5 | `i8` | `initOffA` | `byte * 16` → `this.ag[i][1]` (пиксельный оффсет внутри области) |
| 6 | `i8` | `initOffB` | `byte * 16` → `this.ag[i][0]` |
| 7 | `i8` | `speedA` | `this.s[i][1]` (скорость/псевдоскорость по оси A) |
| 8 | `i8` | `speedB` | `this.s[i][0]` (скорость по оси B) |

**Нормализация области:** если `tileX0 > tileX1` **или** `tileY0 > tileY1`, код:
- меняет местами `X0X1` и `Y0Y1`,
- пересчитывает `initOffA/initOffB` как ширину/высоту области в пикселях: `(X1-X0)*16`, `(Y1-Y0)*16`,
- если `speedA>0` или `speedB>0` — инвертирует знак (чтобы движениепоехалов ожидаемую сторону).

Практический дамп этих записей из `bounce_back/original_code/.../res/lf` можно получить скриптом `bounce_back/dump_lf_enemies.py`.

### TileMap chunk (следующий chunk)

`g.java` ожидает `tileMap` в виде:
- `u8 height`
- `u8 width`
- затем `height * width` байт тайлов (в строковом порядке), где:
  - `tileId = tileByte & 0x7F`
  - `tileByte & 0x80` — флаг заливки `bgColor` перед рисованием тайла (и используется в коллизиях как частьтайла”)

---

## Формат `/res/b` (Ball) — маски игрока + спрайты (FACT)

Источник: конструктор `a(...)` (Player) читает `/res/b` через `c("/res/b")` (a.java:83).

`/res/b` — контейнер `c.java`. Порядок чтения элементов:

1) **chunk[0] = player masks (binary, НЕ PNG)**

Формат chunk[0] (a.java:89-99):
- `u8 maskCount` (a.java:89-90)
- для каждой маски `i`:
  - `u8 w`, `u8 h` (a.java:92-94)
  - `boolean[w][h]` заполнение `readBoolean()` (1 байт) (a.java:95-98)

Ориентация маски:
- используется как `mask[x][y]` при вызове tile-engine collision (см. `g.java:395`).

2) **chunk[1..25] = 25 PNG спрайтов мяча**

Оригинал читает 25 изображений подряд через `c.a()` (a.java:104-108) и кладёт в `a.w[0..24]`.

### Выбор маски по spriteIndex (FACT)

Перед коллизиями игрок выбирает индекс маски так (a.java:243-249):
- `spriteIndex ∈ [0..8]` или `[20..22]` → mask `0`
- `spriteIndex ∈ [9..19]` → mask `1`

Размер collision-rect берётся из выбранной маски:
- `w = mask.length`, `h = mask[0].length`, `rect = (x - w/2, y - h/2, w, h)` (a.java:250-255).

---

## Враги / moving objects (h.java)

В этой сборкевраги” — это набор движущихся объектов, полностью задаваемых данными из `/res/lf`.

### Runtimeмодель (массивы h.java)

После чтения enemy records `h.b(level)` инициализирует массивы длиной `enemy_count (j)`:
- `ao[i]` — `enemyType` из `/res/lf`
- `f[i][0..1]`, `k[i][0..1]` — углы bbox/области движения (тайлы)
- `ag[i][0..1]` — текущий пиксельный оффсет внутри области (меняется каждый tick)
- `s[i][0..1]` — скорости/псевдоскорости (меняются каждый tick)

### Размеры хитбокса (h.a/h.b)

Используются в playerenemy коллизии (`a.java`, метод `a(..., boolean)`):

| enemyType | widthPx | heightPx |
|---:|---:|---:|
| `0` | `32` | `32` |
| `1` | `16` | `16` |
| `2` | `24` | `11` |

### Update на каждый tick (h.d)

- Для `enemyType == 0` и `enemyType == 2`:
  - объект двигается по двум осям внутри прямоугольной области, скорость задается как **пиксели на tick** (через `abs(speed)` и знак),
  - при достижении 0 или max (ширины/высоты области) скорость по соответствующей оси разворачивается.
- Для `enemyType == 1`:
  - движение по одной оси спсевдоускорением” (через `speedA`), с ограничением и разворотом на краях.

### Рендер (h.b(Graphics))

`enemyType` выбирает спрайт из `ac[]`:
- `type 2` → `ac[0]`
- `type 0` → `ac[1]`
- `type 1` → `ac[2]` или `ac[3]` (в зависимости от направления `s[i][1]`)

Для отрисовки используется: `baseTile * 16 + agOffset` (то есть `ag[]` — именно **в пикселях**).

---

## Формат метаданных тайлов (/res/tf)

**Загрузка:** `h.java:254-256`

`/res/tf` — бинарный контейнер из **2 объектов** (в этой версии): первый содержит заголовок + таблицу анимаций + метаданные части тайлов, второйхвост метаданных тайлов (см. `g.java`).

### Заголовок (object 0)

| Поле | Тип | Смысл | Значение в этой версии |
|---|---:|---|---|
| `images_total` | `u8` | Всего изображений-тайлов для слоя | `111` |
| `images_base` | `u8` | Сколько из них лежит в base-контейнере (`/res/if0` или `/res/ib0`) | `104` |
| `clampX` | `u8` | **Clamp по X** (1=clamp, 0=wrap) | `1` |
| `clampY` | `u8` | **Clamp по Y** (1=clamp, 0=wrap) | `1` |
| `tileW` | `u8` | Ширина тайла (если `12`, нормализуется в `16`) | `1216` |
| `tileH` | `u8` | Высота тайла (если `12`, нормализуется в `16`) | `1216` |
| `tileCount` | `u8` | Кол-во тайлов (типов) | `117` |
| `splitIndex` | `u8` | Индекс тайла, с которого чтение метаданных переключается на object 1 | `110` |
| `tileIdMask` | `u8` | Маска `tileId` в `tileMap` байте | `0x7F` |
| `tileFlagMask` | `u8` | Маска флага в `tileMap` байте (используется для заливки фона) | `0x80` |
| `bgColor` | `u32` | Цвет заливки (ARGB/RGB в зависимости от платформы) | `0x7F00007F` |

### Таблица анимаций (object 0, сразу после заголовка)

| Поле | Тип | Смысл | Значение в этой версии |
|---|---:|---|---|
| `animCount (U)` | `u8` | Кол-во групп анимации | `20` |

Сразу после 14-байтового заголовка объекта 0 идёт `animCount` (1 байт); отдельногоglobal reserved byteмежду заголовком и `animCount` нет.

**Для каждой группы (0..animCount-1):**

| Поле | Тип | Смысл | Значение в этой версии |
|---|---:|---|---|
| `reserved` | `u8` | Не используется (пропускается в `g.java:193`) | — |
| `period (O[i])` | `u8` | Период в тиках до смены кадра | `5` (для всех групп) |
| `frameCount` | `u8` | Кол-во кадров в группе | 23 |
| `frames[] (m[i])` | `u8[]` | Список `tileId`, которые будут подставляться по кругу | (см. файлы) |

### Метаданные тайла (object 0/1)

Запись на каждый `tileId` (всего `tileCount` записей). Для `tileId < splitIndex` читается из object 0 **после** таблицы анимаций, для `tileId >= splitIndex` — из object 1.

| Поле | Тип | Смысл |
|---|---:|---|
| `tileIdEcho` | `i8` | В этой версии равен текущему `tileId` (0..116) и в коде не сохраняется (похоже на контроль/заглушку) |
| `renderType (v[tile])` | `u8` | `0`=пусто, `1`=картинка, `3`=анимация через `animGroup` |
| `imageIndex (T[tile])` | `u8` | Индекс изображения в `V[]` (tile atlas/images) |
| `transform (b[tile])` | `u8` | Rotate/flip (используется в draw и в `collisionType=3`) |
| `collisionType (l[tile])` | `u8` | `0`=нет, `1`=mask без трансформаций, `2`=solid, `3`=mask с трансформациями + алиас маски |
| `mask` | `bool[tileH][tileW]` | Присутствует только при `collisionType=1` (256 байт при 16×16); индексация как `mask[y][x]` |
| `aux (af[tile])` | `u32` | Перегруженное поле: при `renderType=3` это `animGroup`; при `collisionType=3` это `maskBaseTileId` для алиаса `s[tile]=s[aux]` |

### Семантика байта карты `tileMap`

`tileId = tileByte & tileIdMask` (обычно `0x7F`), а `tileByte & tileFlagMask` (обычно `0x80`) в `g.a(...)` приводит к заливке под тайлом `bgColor` перед рисованием.

---

## Контракт коллизий (g.java)

Самый рискованный участок портаpixel-perfect коллизии тайлов. Детальныйcollision contract” (подпись `collisionTest`, математика выбора тайлов, `collisionType`, `transform`, `aux` alias, и минимальные тесткейсы по реальным уровням) вынесен в [`COLLISION_CONTRACT.md`](COLLISION_CONTRACT.md).

## Константы физики (из анализа a.java)

### Прыжок
- **Normal:** `-125` (метод `h()` строка 543)
- **Inverted:** `-180` (+44% силы, строка 545)
- **Popped:** `-95` (-24% силы, строка 547)

### Бонусы
- **Длительность:** `450` тиков (строки 505, 512, 519)
- **Модификатор прыжка:** `+25%` (строка 549-555)

### Горизонтальное движение
- **Ускорение:** `18` normal, `22` с бонусом (строка 1394)
- **Макс. скорость:** `60` normal, `100` с бонусом (строка 1398)
- **Трение:** `3` в воздухе, `8` на земле (строка 1410)

### Entity ID бонусов
- **ID 11:** Включить инверсию (`this.I = true`)
- **ID 18:** Выключить инверсию (`this.I = false`)
- **ID 15:** Бонус гравитации
- **ID 22:** Бонус прыжка
- **ID 26:** Бонус скорости
- **ID 39:** Бонус скорости 2

---

## Примечания для портирования на SDL2

### Критические различия с bounce_zero

1. **Размер тайла:** 16×16 px (vs 12×12 в zero)
2. **Framerate:** 20 FPS / 50ms (vs 30 FPS в zero)
3. **Формат ресурсов:** Контейнеры `/res/*` (vs отдельные файлы)
4. **Система тем:** 3 визуальные темы (vs 1 в zero)
5. **Инверсия:** Постоянное состояние через boolean (vs временный бонус в zero)

### Приоритеты реализации

1.Парсер контейнеров (`c.java` → SDL2 resource loader)
2.Распаковка `/res/lf` (формат уровней)
3.Распаковка `/res/tf` (метаданные тайлов)
4.Распаковка `/res/if*` (текстуры по темам)
5.Распаковка `/res/b` (спрайты мяча)
6.Система инверсии гравитации
7.Физика с новыми константами

---

## История изменений

- **2026-02-04:** Создан файл, добавлена карта ресурсов, частичный анализ Player (a.java)
- **2026-02-04:** Проанализированы `b.java`/`d.java` и формат/использование `/res/r` (демо/реплей)
- **2026-02-04:** Найдены две исходные сборки (`G313002`/`G313003`) с одинаковой логикой, но разными языками строк (EN/RU); различие по разрешению задается через поля `Nokia-MIDlet-*-Display-Size` в `MANIFEST.MF`
- *Дополняется по мере анализа...*

---

## TODO - Требует анализа

- [ ] Детальная структура `/res/tf` (метаданные тайлов)
- [ ] Формат анимаций тайлов
- [ ] `/res/tf` — точный формат и таблицы: `tileType (v)`, `collisionType (l)`, `transform (b)`, анимации (`U/O/m`)
- [ ] Класс `i.java` (MenuCanvas) - полный маппинг режимов `uiMode (V)` и сценариев экранов `screenId (g)`
- [ ] Класс `e.java` - назначение и связи с UI/score/RMS
- [ ] `/res/lf`: проверить, встречаются ли `enemyType` кроме `0..2` (в коде поведение описано только для них)
- [ ] Формат collision masks в `/res/tf`