Цель: зафиксировать точную семантику коллизий тайлов как контракт порта (PSP/PSPSDK/SDL2), чтобы дальнейшие подсистемы (физика, враги, триггеры) были доказуемо корректны.
Источники фактов:
bounce_back/original_code/bounce_back_s60.jar.src/g.java— методыa(...)на строках ~315 и ~347 (pixel-perfect collision)./res/tf(bounce_back/original_code/bounce_back_s60.jar.src/res/tf) — метаданные тайлов:renderType v[],transform b[],collisionType l[],aux af[], а также inline-маски дляcollisionType=1.
Связанные документы:
- Game loop (50ms tick):
GAME_LOOP_SPEC.md - Общая деобфускация/ресурсы:
DEOBFUSCATION.md
Ниже все значения tileW/tileH для этой сборки фактически 16×16 (в /res/tf может быть 12×12, но g нормализует 12→16 при чтении).
Функция коллизии в g.java:
boolean a(int xPx, int yPx, int wPx, int hPx, boolean[][] mask, boolean collectHits)
Единицы измерения и соглашения:
xPx,yPx— мировые пиксели, левый верх тестируемого прямоугольника (см. логику overlap вbounce_back/original_code/bounce_back_s60.jar.src/g.java:363иbounce_back/original_code/bounce_back_s60.jar.src/g.java:378).wPx,hPx— размеры прямоугольника в пикселях (используются как ширина/высотаmask; см. вычисленияi6/i7вbounce_back/original_code/bounce_back_s60.jar.src/g.java:365иbounce_back/original_code/bounce_back_s60.jar.src/g.java:380).mask(paramArrayOfboolean) индексируется какmask[u][v], где:u— локальная координата по X (колонка) в объектной маске,v— локальная координата по Y (строка) в объектной маске.
Доказательство ориентации mask:
i6(“ширина overlap”) вычисляется изwPx(paramInt5) и X‑смещенийi2/i4вbounce_back/original_code/bounce_back_s60.jar.src/g.java:365.i7(“высота overlap”) вычисляется изhPx(paramInt6) и Y‑смещенийi3/i5вbounce_back/original_code/bounce_back_s60.jar.src/g.java:380.- В двойном цикле проверка выглядит как
mask[i9 + i4][i8 + i5], гдеi9бежит0..i6-1(локальный X), аi8бежит0..i7-1(локальный Y) —bounce_back/original_code/bounce_back_s60.jar.src/g.java:393иbounce_back/original_code/bounce_back_s60.jar.src/g.java:395.
Флаг карты (tileByte & 0x80) не участвует в коллизии:
- tileId берётся как
this.R[tileY][tileX] & this.z, гдеz=tileIdMask(в этой версии0x7F) —bounce_back/original_code/bounce_back_s60.jar.src/g.java:329.
Что возвращает:
- Возвращает
true, если есть хотя бы один пиксель, гдеmask[u][v]==trueи тайл в этой точке считается коллизионным по своим правилам (collisionType). - Любая
ArrayIndexOutOfBoundsExceptionво время выполненияcollisionTestприводит кtrue(см.catchвbounce_back/original_code/bounce_back_s60.jar.src/g.java:341).- Следствие (частый практический эффект): выход за границы
R[][]ведёт к “стене”, но это не единственный источник AIOOBE (см. ниже).
- Следствие (частый практический эффект): выход за границы
Что заполняется при collectHits=true:
g.Y[0..4]иg.P[0..4]— списки попаданий по тайлам.Y[idx] = tileX,P[idx] = tileY.
- Перед тестом массивы очищаются в
-1. - Попадания добавляются в порядке обхода:
tileXслева→направо (внешний цикл),tileYсверху→вниз (внутренний цикл). - Ограничение: массивы длины 5, а явной проверки
idx < 5нет — если столкновений больше 5, происходит выход за границы и метод возвращаетtrueпо обработчику исключения. То есть приcollectHits=true“много коллизий” корректно детектируется, но “список попаданий” может быть частично заполнен.
Уточнение (важно отделить источники AIOOBE):
- В
collisionTestесть единыйtry/catch(ArrayIndexOutOfBoundsException)вокруг перебора тайлов —bounce_back/original_code/bounce_back_s60.jar.src/g.java:326. - Поэтому любая AIOOBE внутри (в т.ч. из-за:
- выхода за границы
R[tileY][tileX], - переполнения hit‑list
Y/P, - несоответствия размеров
maskзаявленнымwPx/hPx, - иных ошибок индексации)
приводит к
return true.
- выхода за границы
В Level (g) есть два флага из /res/tf:
clampX(this.d) иclampY(this.X) читаются вbounce_back/original_code/bounce_back_s60.jar.src/g.java:153.
Они не применяются внутри collisionTest:
collisionTestвычисляетtileX/tileYнапрямую делением и делает прямой доступR[tileY][tileX]безwrap()/clamp()—bounce_back/original_code/bounce_back_s60.jar.src/g.java:322иbounce_back/original_code/bounce_back_s60.jar.src/g.java:329.- Нормализация координат мира (clamp или wrap) применяется в других местах (например,
setCamera/a()), но это отдельный контракт. Для порта важно: если вызывающая сторона подаст координаты, которые выводятtileX/tileYза пределы карты, collisionTest вернётtrueпо AIOOBE.
Пусть tileW = this.f, tileH = this.A (в этой версии 16).
Диапазоны тайлов:
tileX0 = xPx / tileWtileX1 = (xPx + wPx) / tileWtileY0 = yPx / tileHtileY1 = (yPx + hPx) / tileH
Далее выполняется полный перебор tileX ∈ [tileX0..tileX1], tileY ∈ [tileY0..tileY1].
Привязки к коду:
- Диапазоны тайлов вычисляются ровно так в
bounce_back/original_code/bounce_back_s60.jar.src/g.java:322–bounce_back/original_code/bounce_back_s60.jar.src/g.java:325. - В Java целочисленное деление для отрицательных значений усекает к нулю (например,
-1/16 == 0, а-16/16 == -1). Это влияет на то, когда возникает AIOOBE по “вне карты”.
Overlap‑прямоугольник внутри тайла (используется при pixel‑тесте):
tileOriginX = tileX * tileW,tileOriginY = tileY * tileH—bounce_back/original_code/bounce_back_s60.jar.src/g.java:348–bounce_back/original_code/bounce_back_s60.jar.src/g.java:349.- Для X:
- если
xPx < tileOriginX:skipLeft = tileOriginX - xPx,overlapW = min(tileW, wPx - skipLeft) - если
xPx > tileOriginX:localX0 = xPx - tileOriginX,overlapW = min(wPx, tileW - localX0) - если
xPx == tileOriginX:localX0=0,overlapW = min(wPx, tileW) - это реализовано в
bounce_back/original_code/bounce_back_s60.jar.src/g.java:363–bounce_back/original_code/bounce_back_s60.jar.src/g.java:377.
- если
- Для Y аналогично (смещения
skipTop/localY0/overlapH) —bounce_back/original_code/bounce_back_s60.jar.src/g.java:378–bounce_back/original_code/bounce_back_s60.jar.src/g.java:392. - Если
overlapW==0илиoverlapH==0, внутренние циклы по пикселям не выполняются (потому что они идутfor (i8 = overlapH-1; i8>=0; ...)иfor (i9 = overlapW-1; i9>=0; ...)) —bounce_back/original_code/bounce_back_s60.jar.src/g.java:393–bounce_back/original_code/bounce_back_s60.jar.src/g.java:394.
collisionType хранится в l[tileId] и влияет на то, как проверяется попадание:
| collisionType | Поведение | Источник геометрии |
|---|---|---|
| 0 | Нет коллизии: тайл пропускается до pixel-test | — |
| 2 | Solid full-tile: если есть хотя бы 1 пиксель overlap с mask==true, считается коллизией |
целый тайл 16×16 |
| 1 | Pixel mask без transform: проверяется tileMask[localY][localX] |
inline маска в /res/tf (256 boolean) |
| 3 | Pixel mask с transform + alias: берётся mask от “base tile” (aux), но координаты трансформируются по transform |
alias-маска из /res/tf + transform |
Формат inline mask (collisionType=1) в /res/tf:
- 16×16 значений
readBoolean()(то есть 256 байт, не битпак). - Порядок в файле:
y=0..15, внутриx=0..15.
Ориентация tile‑mask в памяти (критичный момент):
- При чтении inline mask (
collisionType=1) создаётсяnew boolean[tileW][tileH]и заполняется какs[tileId][x][y] = readBoolean()при обходеyзатемx—bounce_back/original_code/bounce_back_s60.jar.src/g.java:225иbounce_back/original_code/bounce_back_s60.jar.src/g.java:226. - При проверке коллизии значение извлекается как
tileMask[localY][localX](и в веткеcollisionType=3какtileMask[transY][transX]) — см.bounce_back/original_code/bounce_back_s60.jar.src/g.java:420иbounce_back/original_code/bounce_back_s60.jar.src/g.java:422. - Следствие (FACT): runtime‑интерпретация inline mask является транспонированной относительно порядка хранения. То есть boolean, считанный из файла в позиции
(fileY, fileX), используется в коллизии как значение в(runtimeY=fileX, runtimeX=fileY).
collisionType=3 (alias) (FACT):
auxчитается какint(readInt) вbounce_back/original_code/bounce_back_s60.jar.src/g.java:231.- Если
collisionType==3, маска назначается через alias:s[tileId] = s[aux]—bounce_back/original_code/bounce_back_s60.jar.src/g.java:232. - Это означает:
auxдляcollisionType=3должен быть валидным tileId (иначе NPE/AIOOBE на доступе).
Практическая проверка по данным этой сборки:
- Base inline masks (
collisionType=1):tileId ∈ {3, 52, 54, 56, 59, 66, 69, 93, 95, 97, 99, 113}. - Варианты (
collisionType=3) — это повёрнутые/флипнутые версии этих масок (например4/5/6— варианты3).
Источник артефакта: artifacts/tf_tiles_dump.txt:1 (сгенерирован python3 scripts/dump_tf_tiles.py).
transform — байт b[tileId] из /res/tf.
Разложение битов:
rot = transform & 0x03:0= 0°1= 90° CW (по фактической формуле)2= 180°3= 270° CW
transform & 0x08— flip по X (инверсияx → 15-x)transform & 0x04— flip по Y (инверсияy → 15-y)
Порядок применения (важно):
- flips (
0x08,0x04) - rotation (
rot)
Точные формулы, как они реализованы в g.java для collisionType=3:
- Инициализация:
x = localX,y = localY - flips:
- если
0x08:x = 15 - x - если
0x04:y = 15 - y
- если
- rotation:
rot==0:(x,y)без измененийrot==1:(x,y) = (y, 15 - x)rot==2:(x,y) = (15 - x, 15 - y)rot==3:(x,y) = (15 - y, x)
Опора на код:
- flips выполняются до rot‑ветвления —
bounce_back/original_code/bounce_back_s60.jar.src/g.java:402. - rot‑ветвления
rot==3/1/2—bounce_back/original_code/bounce_back_s60.jar.src/g.java:406–bounce_back/original_code/bounce_back_s60.jar.src/g.java:419.
Важно: transform влияет на коллизию только для collisionType=3 (ветка b1==3). Для collisionType=1 трансформации не применяются — bounce_back/original_code/bounce_back_s60.jar.src/g.java:396 и bounce_back/original_code/bounce_back_s60.jar.src/g.java:422.
Ниже — не юнит-тесты, а конкретные проверяемые факты: “в уровне есть тайл X в позиции (tileX,tileY), у него collisionType/transform/aux такие-то; для заданной точки в тайле ожидается collide / no-collide”.
Источники для этих кейсов:
- Позиции тайлов в уровнях:
artifacts/lf_tile_positions_collision_cases.txt:1(сгенерированpython3 scripts/dump_lf_tile_positions.py --tile 4 --tile 53 --tile 102). - Метаданные тайлов (
collisionType/transform/aux):artifacts/tf_tiles_dump.txt:1. - Базовые inline‑маски (runtime‑ориентация):
artifacts/tf_inline_masks_runtime.txt:1.
Для привязки к уровням:
tile origin worldPx = (tileX*16, tileY*16).- “Точка в тайле” задаётся как
local (lx, ly); мировая точка:(xPx, yPx) = origin + (lx, ly). - В качестве
maskдостаточно мысленно использовать 1×1 маску с единственнымtrueпикселем.
- Позиция:
level 04,tile (14,42)(из/res/lf). - Метаданные (
/res/tf):tile 4: collisionType=3, aux=3, transform=0x01. - Base mask:
tile 3— диагональная маска (см. Appendix A ниже).
Ожидания (1×1 mask):
local (0,0)collide (transform 0x01 → base(0,15), вtile 3это#).local (1,0)no-collide (transform 0x01 → base(0,14), вtile 3это.).
- Позиция:
level 05,tile (9,31). - Метаданные:
tile 53: collisionType=3, aux=52, transform=0x03. - Base mask:
tile 52— “левая клиновидная” маска (см. Appendix A).
Ожидания:
local (4,0)collide (transform 0x03 → base(15,4), вtile 52это#).local (0,0)no-collide (transform 0x03 → base(15,0), вtile 52это.).
- Позиция:
level 00,tile (43,17). - Метаданные:
tile 102: collisionType=3, aux=97, transform=0x05(0x04flipY +rot==1). - Base mask:
tile 97имеет#в верхней строке наx=7..8(см. Appendix A; это runtime‑ориентация, с учётом транспонирования при хранении).
Ожидания:
local (15,7)collide (mapping 0x05 → base(8,0)).local (0,0)no-collide (mapping 0x05 → base(15,15)).
Факт из реализации: любая AIOOBE внутри collisionTest приводит к true (см. раздел 2).
Проверяемое ожидание:
- Если в переборе тайлов получится
tileX<0илиtileY<0(то есть, например,xPx <= -16илиyPx <= -16при tileSize=16), доступ кR[tileY][tileX]вызовет AIOOBE и результат будетtrue. - Для “малых” отрицательных координат (
-15..-1)xPx / 16может дать 0 из-за усечения к нулю, и AIOOBE может не возникнуть (поведение зависит от маски и overlap).
Формат: 16×16, # = solid, . = empty, оси: x слева→направо, y сверху→вниз.
Важно: это runtime‑ориентация, то есть значения приведены так, как они реально читаются в коллизии (tileMask[localY][localX]). Из‑за того, что загрузка пишет s[x][y], а коллизия читает s[y][x], runtime‑маска является транспонированной относительно “как записано в /res/tf” (см. раздел 5).
Источник артефакта: artifacts/tf_inline_masks_runtime.txt:1 (сгенерирован через python3 scripts/dump_tf_tiles.py --dump-mask 3 --dump-mask 52 --dump-mask 97).
...............#
..............##
.............###
............####
...........#####
..........######
.........#######
........########
.......#########
......##########
.....###########
....############
...#############
..##############
.###############
################
#...............
##..............
###.............
####............
#####.......####
################
################
################
................
................
................
................
................
................
................
................
.......##.......
................
................
................
................
................
................
................
................
................
................
................
................
................
................
................
- Список тайлов с
collisionType=3/aux/transform:python3 scripts/dump_tf_tiles.py --only-collision3 - Общий дамп всех тайлов
/res/tf:python3 scripts/dump_tf_tiles.py - Позиции тайлов по уровням можно извлечь из
/res/lf(tileMap chunk) простым сканированиемtileByte & 0x7F.