Analyse ASM (objdrawhires.s)
cmp.b #$ff, 6(a0): byte +6 de l’objet = marqueur polygon modelmove.w 6(a0), d5(apresmove.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
isPolygonetpolyModelIndexdansObjData
LevelJsonExporter.java
- Export
isPolygonetpolyModelIndexdans le JSON par objet
LevelSceneBuilder.java
VECTOR_NAMES_TABLE[]: table exacte GLFT_VectorNames_l depuis LevelED.txt- Priorite 1 :
polyModelIndexdirect (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 vectobjALIEN_NAME_TO_VECTOBJ: table nom alien -> fichier vectobj (SnakeScanner, Wasp, Mantis, Crab)tryLoadVectObj(name, targetH): charge le .j3o depuisScenes/vectobj/, scale au colHeightaddItems: 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 SBPparseProject(): lit le manifest SBPProjV01 (liste des parties)loadProject(): assemble toutes les parties d’un dossier .prjamigaColorToJme(): 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
breaksur numLines invalide (remplace parcontinueimplicite)
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 brutbuildHqnPalsVl(hqnPal256, lightType=0, brightness=31): construit la table
draw_Pals_vl pour preview en plein eclairageconvertObject(..., 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, splutchhqn/: 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 champobjPointIndex -
LevelJsonExporter.java :
- Construit
zoneFloorHmap depuis les zones parsées -
Export
y = zone.floorHau lieu deyPos = 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 :
ObjDatapasse de 7 à 14 champs, parse tous les champs EntT.
Constantes OBJ_TYPE_, ENT_TYPE_, OBJ_GFX_* ajoutées.
MéthodemakeObjDataCompat()pour rétrocompatibilité. - LevelJsonExporter.java : exporte
defIndex,startAnim,angle,hitPoints,
teamNumber,doorLocks,liftLocksau lieu deentType/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
convertWadsetanalyzeAssetsajoutées.
Workflow
./gradlew convertLevels buildScenes run # rebuild JSON + scènes
./gradlew analyzeAssets # dump TEST.LNK
./gradlew convertWads # si les .WAD du jeu sont disponibles