Цель: Справочник для понимания обфусцированного кода bounce_back_s60.jar
Статус: В процессе заполнения по мере анализа
Использование: Для AI агента и человека при портировании на SDL2
Оригинал (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 (жизни слева, очки справа)
- Меню центрируется или растягивается на полный экран
Источник: 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;Ключевые особенности:
- Горизонталь (X): Всегда центрируется на игроке (без deadzone)
- Вертикаль (Y): С мертвой зоной 30% (игрок может двигаться внутри зоны без сдвига камеры)
- Маленькие уровни: Центрируются вертикально, камера не двигается
- 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 | ✅ Понятен |
Для порта на PSP критично сначала зафиксировать что выполняется каждый тик, потому что одновременно меняются таймеры/рендер/ввод/аудио/ФС.
- Tick = 50 ms (20 FPS), запуск через
GameTimer (f.java). - Стадии tick в оригинале — это дискретный ввод press/release → demo inject → враги → игрок → тайлы/таймеры → камера/viewport → repaint.
Отдельный “1‑страничный” контракт тика вынесен в GAME_LOOP_SPEC.md.
| Обфусц. имя | Предполагаемое имя | Тип | Описание | Строка |
|---|---|---|---|---|
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 |
| Обфусц. имя | Предполагаемое имя | Описание | Строка |
|---|---|---|---|
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 |
| Обфусц. имя | Предполагаемое имя | Описание | Строка |
|---|---|---|---|
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).
| Обфусц. имя | Предполагаемое имя | Тип | Описание | Строка |
|---|---|---|---|---|
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).
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.
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(...)вернул, что текст не помещается).
Назначение: Парсер бинарных контейнеров /res/*
Формат контейнера:
[2 байта] count - количество элементов (big-endian)
[count * 2 байта] размеры элементов (big-endian shorts)
[переменная длина] данные элементов
| Имя | Описание |
|---|---|
c(int) |
Загрузить элемент по индексу (для двойных индексов: index << 1) |
a() |
Получить весь файл целиком |
Назначение: Узел связного списка событий, загружаемых из /res/r и проигрываемых по тикам.
| Поле (обфусц.) | Предполагаемое имя | Тип | Смысл |
|---|---|---|---|
d |
tick |
short |
Номер тика, на котором срабатывает событие |
b |
actionCode |
int |
Код действия (см. таблицу ниже) |
c |
value |
byte |
Значение/параметр действия |
a |
next |
b |
Следующий узел |
Где используется: d.java хранит очередь событий как b-узлы.
Назначение: Считать /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().
- В текущем decompile
CrystalMidlet.h()(старт демо) создаетh(GameCanvas), грузит уровень21, назначаетh.au = new d()и вызываетh.au.a()(загрузка/res/r).- В
h.run()каждый тик, еслиau != null, выполняетсяau.a(this.b)— обновление массива вводаbпо сценарию. h.c()преобразуетb[0..3]в битовую маску ввода игрока (addInput/removeInput), а также обрабатывает спец-событияb[4](выход) иb[5](подсказка).- Когда
d.d == true,h.run()завершает демо:au = null; midlet.e().
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/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 |
Загрузка: 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`, код:
- меняет местами `X0↔X1` и `Y0↔Y1`,
- пересчитывает `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)
Используются в player↔enemy коллизии (`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`) | `12 → 16` |
| `tileH` | `u8` | Высота тайла (если `12`, нормализуется в `16`) | `12 → 16` |
| `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` | Кол-во кадров в группе | 2–3 |
| `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`