Aller au contenu principal

Session 54 — polyModelIndex : lecture correcte depuis le binaire

Analyse ASM (objdrawhires.s)

  • cmp.b #$ff, 6(a0) : byte +6 de l’objet = marqueur polygon model
  • move.w 6(a0), d5 (apres move.w (a0)+) = WORD a l’offset original +8 = poly model index
  • Cet index pointe dans Draw_PolyObjects_vl = GLFT_VectorNames_l

LevelBinaryParser.java

  • Parse byte +6 : polyMarker (0xFF = polygon)
  • Parse WORD +8 : polyModelIndex (index dans GLFT_VectorNames_l, 0-21)
  • Ajout isPolygon et polyModelIndex dans ObjData

LevelJsonExporter.java

  • Export isPolygon et polyModelIndex dans le JSON par objet

LevelSceneBuilder.java

  • VECTOR_NAMES_TABLE[] : table exacte GLFT_VectorNames_l depuis LevelED.txt
  • Priorite 1 : polyModelIndex direct (source binaire, fiable a 100%)
  • Priorite 2 : fallback par nom via OBJECT_NAME_TO_VECTOBJ

Pipeline complet

./gradlew convertLevels buildScenes run

LevelSceneBuilder – tryLoadVectObj

  • OBJECT_NAME_TO_VECTOBJ : table nom objet -> fichier vectobj
  • ALIEN_NAME_TO_VECTOBJ : table nom alien -> fichier vectobj (SnakeScanner, Wasp, Mantis, Crab)
  • tryLoadVectObj(name, targetH) : charge le .j3o depuis Scenes/vectobj/, scale au colHeight
  • addItems : priorite 1=vectobj, 2=sprite bitmap, 3=cube fallback

Pipeline

./gradlew convertVectObj buildScenes run

Format SBP decodé empiriquement

"SBP Object\0"
BYTE numVerts, BYTE padding
numVerts x SWORD[3] big-endian, fixed-point /256 = coordonnées réelles
6 bytes info + BYTE numPolys + BYTE padding
Polygones : nv*4 + 18 bytes chacun
  BYTE nv + 3 bytes flags
  nv x (BYTE 1-based_idx + BYTE u + BYTE v + BYTE normal)
  14 bytes footer (WORD color 12-bit Amiga + BYTE brightness + ...)

SbpObjParser.java

  • parseSbpObject() : parse un fichier SBP
  • parseProject() : lit le manifest SBPProjV01 (liste des parties)
  • loadProject() : assemble toutes les parties d’un dossier .prj
  • amigaColorToJme() : couleur Amiga 12-bit → ColorRGBA

VectObjConverter – mode SBP

  • Si name.prj/ existe : utilise SBP (géométrie complète)
  • Sinon : binaire vectobj (fallback)
  • buildFromSbp() : construit le node JME depuis SbpMesh

Pour utiliser les fichiers SBP :

Copier les dossiers .prj dans src/main/resources/vectobj/

./gradlew convertVectObj

Fixes triangulation

  • numLines=2 = 3 vertices (v0,v1,v2) → triangle valide (avant : 0 tris car fan de 2 = vide)
  • numLines=1 = ligne → skip (pas de mesh)
  • Suppression du break sur numLines invalide (remplace par continue implicite)

Couleurs par polygone

Chaque polygone a un footer a poly + numLines*4 + 8 :
+8 : WORD texIdx (15-bit, bit15=secondary texture set)
+10 : BYTE brightness (0=sombre, 100=clair inverse)

Mappings : texIdx[11:8] → 1 sur 16 teintes de base, brightness → shade (0.1-1.0)
Vertex colors activees via VertexColor=true sur Unshaded.j3md

Pipeline

./gradlew convertVectObj

VectObjConverter.java

Format binaire analyse depuis objdrawhires.s (draw_PolygonModel) :

+0 : WORD numPoints, WORD numFrames
+4 : frame table [numFrames * 4 bytes] : WORD ptsOfs, WORD angOfs
+4+numFrames*4 : part list : SWORD partId, WORD bodyOfs (termine par -1)
body @ bodyOfs : polygones (18 + numLines*4 bytes chacun, -1 = fin)
  +0 SWORD numLines, +2 WORD flags
  +4 vertex list : WORD ptIdx, BYTE u, BYTE v
  +numLines*4+12 : WORD texIdx, BYTE brightness, BYTE polyAngle, WORD gouraud
pointData @ ptsOfs : numPoints * 6 bytes (SWORD x,y,z)

Tache Gradle

./gradlew convertVectObj

Sortie dans assets/Scenes/vectobj/*.j3o


Sprites manquants : worm + robotright

worm.wad et robotright.wad existent dans media/hqn mais ne sont pas
references dans GLFT_ObjGfxNames_l du LNK. WadConverter ne les convertissait
jamais. Fix : liste extraAlienSprites ajoutee dans WadConverter.main().

Root cause cubes grises au runtime : fixMaterials

fixMaterials() dans GameAppState traversait toutes les geometries incluant
les sprites Unshaded (ColorMap + BlendMode.Alpha + Transparent). Elle cherchait
DiffuseMap (Lighting) -> null -> remplacait par gris (0.6,0.6,0.6) et ecrasait
BlendMode.Alpha. Fix : skiper les materiaux deja Unshaded.

Pipeline

./gradlew convertWads buildScenes run

Bug identifie dans LevelBinaryParser

Structure EntT (defs.i) :

+50..+53 : LONG EntT_DoorsAndLiftsHeld_l
           high word (+50..+51) = lock bits hauts
           low word  (+52..+53) = EntT_Timer3_w = door/lift bits
+54      : BYTE EntT_Type_b = defIndex !!
+55      : BYTE EntT_WhichAnim_b

Bug : getInt() a +50 avance a +54, puis getShort() lisait bytes 54-55
= (defIndex << 8) | whichAnim -> stocke dans « liftLocks »
Et get() lisait byte 56 = padding -> defIndex = toujours 0 !

Ex: liftLocks=3584=0x0E00 -> byte 54=0x0E=14 (glarebox) ← c’etait le vrai defIndex

Fix LevelBinaryParser

// Avant (FAUX) :
int defIndex = b.get() & 0xFF;  // lisait byte 56 = padding = 0

// Apres (CORRECT) :
int doorsLifts = b.getInt();         // +50..+53, avance a +54
int defIndex   = b.get() & 0xFF;     // +54 = EntT_Type_b

Pipeline

./gradlew convertLevels buildScenes run

Structure HQN expliquee

Chaque sprite HQN (guard, insect, priest, triclaw…) a 3 fichiers :
.wad : donnees sprite, 1 byte/pixel, index 0=transparent
.ptr : table de pointeurs, 4 bytes/colonne = offset byte dans .wad
.256pal : 1024 bytes = 4 types * 32 brightness * 8 screen-color-indices

Mapping correct (draw_bitmap_lighted)

draw_Pals_vl[pixel] = hqnPal256[light_type*256 + brightness*8 + pixel%8]
screen_color = draw_Pals_vl[pixel]
rgb = globalPalette[screen_color]

On utilisait globalPalette[pixel] directement = FAUX.

Fix

  • renderFrameHqn(wad, ptr, gPal, fd, woff, hqnPal256) : prend le .256pal brut
  • buildHqnPalsVl(hqnPal256, lightType=0, brightness=31) : construit la table
    draw_Pals_vl pour preview en plein eclairage
  • convertObject(..., rawPalData, ...) : passe le palData brut

Pipeline

./gradlew convertWads buildScenes run

Root cause identifie dans objdrawhires.s

Deux formats WAD coexistent dans AB3D2 :

Sprites standards (ALIEN2, PICKUPS, KEYS…) — draw_right_side :

move.b  1(a0,d1.w*2),d0   ; stride=2 bytes, 3 pixels/word (5-bit chacun)
and.b   #%00011111,d0      ; masque 5 bits
move.b  (a4,d0.w*2),(a6)  ; palette[idx5bit * 2]

Sprites HQN (GUARD, INSECT, PRIEST, TRICLAW…) — draw_bitmap_lighted :

move.b  (a0,d1.w),d0      ; stride=1 byte, 1 pixel = 1 octet
beq.s   .skip_black        ; 0 = transparent
move.b  (a4,d0.w),(a6)    ; index DIRECT 8-bit dans globalPalette

Fix WadConverter

  • Ajout HQN_SPRITE_NAMES : guard, priest, insect, triclaw, ashnarg, robotright, worm, globe
  • renderFrameHqn() : stride=1 byte, index direct globalPalette
  • Detection automatique via baseName dans convertObject()

Pipeline

./gradlew convertWads buildScenes run

Bugs corriges dans WadConverter

Bug 1 — Palette globale (256pal.bin) :

// AVANT (faux) : lisait le LOW byte = 0x00 => toutes couleurs noires
int r = readShortBE(raw, i*6) & 0xFF;
// APRES (correct) : lit le HIGH byte = valeur reelle
int r = raw[i*6] & 0xFF;  // Peek semantics

Bug 2 — Palette objet (.256pal) :

// AVANT (faux) : readShortBE & 0xFF = LOW byte = 0x00 => index 0 = noir
int globalIdx = readShortBE(palData, i*2) & 0xFF;
// APRES (correct) : Peek(A*2) = premier byte du WORD
int globalIdx = palData[i*2] & 0xFF;

AMOS Peek(base + A*2) lit un BYTE à l’offset A*2 = premier octet de chaque WORD.

Bug 3 — Largeur HQN (GUARD, INSECT, PRIEST, TRICLAW, ASHNARG) :
Le LW du LNK couvre TOUTES les vues de rotation concaténées dans le WAD.
woff du header PTR donne la largeur d’UNE seule vue.

// renderFrame() utilise maintenant woff pour limiter LW
int width = (woff > 0 && woff < fd.lw()) ? woff : fd.lw();

Pipeline

./gradlew convertWads buildScenes run

Sprites trouvés dans ab3d2-tkg-original/media/

  • includes/ : alien2, pickups, bigbullet, explosion, keys, lamps, glare, rockets, splutch
  • hqn/ : guard, priest, insect, triclaw, ashnarg, robotright, worm

Implémentation

WadConverter : mise à jour gatherWadSearchPaths pour trouver automatiquement
les WAD dans ../ab3d2-tkg-original/media/includes et media/hqn.

LevelSceneBuilder :
tryLoadSprite(wadName, height) : charge Textures/objects/{name}/{name}_f0.png,
crée un Quad texturé avec BlendMode.Alpha + BillboardControl.Camera
– Fallback cube coloré si PNG absent
– Mapping gfxType alien → nom WAD (alien2, worm, robotright, guard, insect)
– Taille sprite depuis colHeight des definitions (en unités Amiga / 32)

Workflow

./gradlew convertWads convertLevels buildScenes run

Cause racine

Les coordonnées dans la table ObjectPoints sont stockées en fixed-point 16.16 :

LONG value = (coord_entier << 16) | partie_fractionnaire
coord_réelle = value >> 16  (= high SHORT)

C’est le format standard Amiga pour le mouvement fluide (fractionnaire).
Conformément à hires.s : move.w d0,ObjT_ZPos_l(a0) écrit un SHORT
dans le HIGH WORD du LONG — la partie entière est toujours dans le high word.

Fix

Dans LevelBinaryParser : lire getShort() + skip getShort() au lieu de getInt()
pour chaque composante X et Z dans la table ObjectPoints.

Résultat

Av ant : x=-160432128 (mauvais), Après : x=-2436 (dans zone 88 ✓)
Le medikit proche du joueur et tous les objets apparaissent maintenant aux bonnes positions.


Découverte capitale (hires.s lignes 2266-2270)

Les positions XPos/ZPos/YPos dans ObjT_XPos_l (+0..+11) sont marquées « To be confirmed »
dans defs.i et NE sont PAS stockées dans le fichier binaire.

La structure réelle de chaque ObjT dans twolev.bin :

+0..+1 : WORD = objPointIndex   INDEX dans la table ObjectPoints
+2..+3 : WORD   padding (0)
+4..+7 : LONG   0 (ZPos placeholder, initialisé runtime)
+8..+11: LONG   0 (YPos placeholder, initialisé runtime)
+12    : WORD   ObjT_ZoneID_w
+16    : BYTE   ObjT_TypeID_b
... (EntT overlay valide à partir de +18)

Les positions monde réelles sont dans la table ObjectPoints :
– Pointée par TLBT_ObjectPointsOffset_l (TLBT +42)
– 8 bytes/entrée : { xPos:int, zPos:int }
TLBT_NumObjects_w = nombre d’entrées
– Code runtime : move.w (a0),d0 (lit l’index) puis (a1,d0.w*8) (accède ObjectPoints)

Explication du bug : Java lisait le WORD d’index comme le mot fort d’un int
x = index * 65536, z = 0, y = 0 pour tous les objets.

Correction Y

Le y dans le JSON vaut maintenant zone.floorH (même valeur que les murs/zones).
Conversion JME : jy = -zone.floorH / 32 + 0.3f

Fichiers modifiés

  • LevelBinaryParser.java :
  • Parse la table ObjectPoints depuis objectPointsOffset
  • Lit le WORD +0 comme objPointIndex (pas XPos)
  • xPos/zPos = ObjectPoints[objPointIndex].{x,z}
  • ObjData : ajout du champ objPointIndex

  • LevelJsonExporter.java :

  • Construit zoneFloorH map depuis les zones parsées
  • Export y = zone.floorH au lieu de yPos = 0

  • LevelSceneBuilder.java :

  • jy = -y / SCALE + 0.3f (y = floorH, +0.3 au-dessus du sol)

Workflow

./gradlew convertLevels buildScenes run

Sources analysées

  • defs.i : STRUCTURE ObjT, STRUCTURE EntT, constantes OBJ_TYPE_, ENT_TYPE_
  • leved303.amos : ALIENSAVE / THINGSAVE (save format exact de chaque champ)
  • newaliencontrol.s : ODefT_GFXType_w usage (0=BITMAP, 1=VECTOR, 2=GLARE)

Catalogue complet ObjT_TypeID_b

TypeID Constante Signification
0 OBJ_TYPE_ALIEN Alien vivant, EntT_Type_b = alien def 0-19
1 OBJ_TYPE_OBJECT Objet, EntT_Type_b = objet def 0-29
2 OBJ_TYPE_PROJECTILE Bullet/projectile (runtime uniquement)
3 OBJ_TYPE_AUX Sprite auxiliaire attaché (runtime)
4 OBJ_TYPE_PLAYER1 Position spawn joueur 1
5 OBJ_TYPE_PLAYER2 Position spawn joueur 2 (coop)

EntT overlay (18 champs parsés)

+18  EntT_HitPoints_b        points de vie initiaux
+21  EntT_TeamNumber_b       équipe alien (0=aucune)
+24  EntT_DisplayText_w      index texte niveau (-1=aucun)
+30  EntT_CurrentAngle_w     angle initial 0-8191 = 360°
+32  EntT_TargetControlPoint_w  waypoint cible aliens
+34  EntT_Timer1_w           STRTANIM = frame de départ (objets)
+50  EntT_DoorsAndLiftsHeld_l  bits portes bloquées
+52  EntT_Timer3_w           bits lifts bloquées
+54  EntT_Type_b             INDEX def alien(0-19) OU objet(0-29)
+55  EntT_WhichAnim_b        frame courante (runtime)

ODefT_Behaviour_w (pour TypeID=1)

Valeur Constante Exemples
0 ENT_TYPE_COLLECTABLE health, ammo, armes, clés
1 ENT_TYPE_ACTIVATABLE switch, levier, terminal
2 ENT_TYPE_DESTRUCTABLE barils, caisses
3 ENT_TYPE_DECORATION lampes, décors

ODefT_GFXType_w (newaliencontrol.s)

Valeur Constante Rendu
0 OBJ_GFX_BITMAP sprite WAD (alien2.wad, pickups.wad…)
1 OBJ_GFX_VECTOR modèle vectoriel (vectobj/blaster…)
2 OBJ_GFX_GLARE effet glare/smoke additif

Fichiers modifiés

  • LevelBinaryParser.java : ObjData passe de 7 à 14 champs, parse tous les champs EntT.
    Constantes OBJ_TYPE_, ENT_TYPE_, OBJ_GFX_* ajoutées.
    Méthode makeObjDataCompat() pour rétrocompatibilité.
  • LevelJsonExporter.java : exporte defIndex, startAnim, angle, hitPoints,
    teamNumber, doorLocks, liftLocks au lieu de entType/whichAnim.
    TypeID PROJECTILE et AUX filtrés (runtime uniquement).
  • LevelSceneBuilder.java : addItems() utilise les nouveaux champs JSON.
    UserData : defIndex, startAnim, angle, hitPoints, teamNumber, doorLocks, liftLocks.
    Couleur des cubes par TypeID (rouge=alien, vert=collectible, jaune=activatable…).
  • LnkParser.java : parseur complet TEST.LNK (86268 bytes, GLF database).
    Extraction WAD names ($2C0), vector names ($13FE0), frame data ($39B0).
  • WadConverter.java : convertisseur WAD+PTR+256PAL → PNG (format extrait de leved303.amos).
  • AssetAnalyzer.java : intègre LnkParser, dump TEST.LNK.
  • build.gradle : tâches convertWads et analyzeAssets ajoutées.

Workflow

./gradlew convertLevels buildScenes run   # rebuild JSON + scènes
./gradlew analyzeAssets                   # dump TEST.LNK
./gradlew convertWads                     # si les .WAD du jeu sont disponibles

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *