Aller au contenu principal

Session 99 — Audit architectural : données ignorées du binaire de niveau

Le constat

Après avoir rapidement « réparé » la porte rouge zone 132 et le lift zone 104, il est apparu que l’LevelJsonExporter actuel ignore une grosse partie des données déjà parsées par LevelBinaryParser. C’est probablement la racine de plusieurs bugs subtils observés.

Inventaire des données manquantes

Données PARSÉES en mémoire mais NON EXPORTÉES :

  1. Messages texte (10 × 160 chars) – briefings/narratif/hints
  2. Control Points (TLBT_NumControlPoints) – points de patrouille des aliens
  3. Zones étendues : tous les champs après floor/roof/brightness sont ignorés :
    upperFloor, upperRoof, upperBrightness -> zones à 2 niveaux (mezzanines, ponts)
    water -> zones aquatiques (animation, dégâts par water touch)
    controlPoint -> control point assigné à cette zone
    backSFXMask -> sons d’ambiance (gouttes, vent, etc.)
    drawBackdrop -> skybox visible
    echo -> reverb audio par zone
    telZone, telX, telZ -> TÉLÉPORTEURS (entrer dans la zone téléporte)
    floorNoise, upperFloorNoise -> sons de pas (eau, métal, terre)
  4. PVS records (Potentially Visible Set) – optimisation rendu
  5. Edges étendus : Word_5, Byte_12, Byte_13, Flags_w (flags wall pour DoorRoutine)
  6. DisplayText des objets (texte affiché quand activé)

Données PAS DU TOUT PARSÉES :

  1. Switches (header +8 du TLGT) – leviers/boutons déclencheurs !
  2. TYPE_OBJECT/WATER/BACKDROP dans ZoneGraphAdds (decoration zone-bound, water anims, custom skybox)
  3. ZoneCrossing logic – comment passer d’un étage bas à étage haut dans une zone à 2 niveaux

Architecture clé manquée : ZONES À 2 NIVEAUX

Le vrai jeu supporte des zones avec deux étages superposés (floor/roof pour bas, upperFloor/upperRoof pour haut). Le joueur a un flag PlrT_StoodInTop_b qui détermine s’il est dans la moitié basse ou haute. Les passages entre zones utilisent ZoneCrossing (LOWER_TO_LOWER, LOWER_TO_UPPER, etc.).

Mon code JME suppose actuellement une seule épaisseur par zone -> impossible de modéliser correctement les mezzanines, ponts, escaliers à étage.

Étape 1 — Enrichissement du JSON (FAIT)

LevelJsonExporter.java exporte maintenant :

  • messages[] : 10 messages texte du niveau (briefings, hints, scene transitions)
  • controlPoints[] : points de patrouille pour aliens
  • Zones étendues : upperFloorH, upperRoofH, hasUpper, water, upperBrightness, controlPoint, backSFXMask, drawBackdrop, echo, telZone, telX, telZ, floorNoise, upperFloorNoise, pvs[]
  • Edges étendus : word5, byte12, byte13, flags
  • Objets : displayText, targetCP
  • switchesDataOffset : pointeur vers la table switches (pas encore parse)

Outil d’inspection — LevelInspector (FAIT)

Nouveau tool LevelInspector.java + task gradle levelInspect qui dumps un rapport synthetique pour chaque niveau : nombre de messages non vides, control points, teleporteurs, zones a 2 niveaux, zones water, doors/lifts/switches.

Usage : ./gradlew convertLevels levelInspect ou ./gradlew levelInspect -Plevel=A

Fichiers modifies session 99

  • tools/LevelJsonExporter.java : ajout de tous les champs etendus
  • tools/LevelInspector.java : NOUVEAU – rapport synthetique par niveau
  • build.gradle : ajout task levelInspect
  • gradle.properties : ajout action NetBeans pour levelInspect

Etapes suivantes

  • ~~Etape 2 : parser les SWITCHES depuis switchesDataOffset~~ (FAIT) – voir ci-dessous
  • Etape 3 : valider que le LIFT zone 104 a un floorH coherent en utilisant les nouvelles donnees
  • Etape 4 : architecture des ZONES A 2 NIVEAUX (refactor LevelSceneBuilder)
  • Etape 5 : utiliser les messages texte et displayText dans le HUD
  • Etape 6 : implementer les TELEPORTEURS dans le runtime
  • Etape 7 : sons d’ambiance par backSFXMask + reverb par echo

Etape 2 — Parsing des SWITCHES (FAIT)

Apres relecture de newanims.s::SwitchRoutine (lignes ~2200-2400), structure decryptee :

struct Switch {           // 14 bytes
    int16_t  active;         // +0  : -1 = slot vide, >=0 = actif
    uint8_t  timerActive;    // +2  : flag timer en cours
    uint8_t  timerCounter;   // +3  : decrement par Anim_TempFrames_w*4
    uint16_t pointIndex;     // +4  : index Lvl_PointsPtr_l (position du switch)
    uint32_t gfxOffset;      // +6  : offset Lvl_GraphicsPtr (graphique)
    uint8_t  pressed;        // +10 : 0/1 toggle (Plr active)
    uint8_t  padding;        // +11
    uint16_t reserved;       // +12
};

Maximum 8 switches par niveau (boucle move.w #7,d0).
Le bit affecte dans Conditions par switch d’index i (0..7) est bit (11 - i) (cf asm addq #4,d3 apres sub.w d0,d3 ou d3 part de 7).
Distance d’activation : 60 unites Amiga^2 (~1.9 unites JME).

Exporte dans level_*.json sous la section "switches": [...].
L’offset brut reste expose via switchesDataOffset pour reference.

Audit complet 16 niveaux (LevelInspector)

Les 16 niveaux du jeu (A-P) ont chacun un caractere distinct :

Niveau Telep 2-Niv Eau Echo Caractere
A 0 0 0 Intro classique (1 message: cle blast doors)
B 0 0 0 0 doors/lifts – bug ou open space ?
C 3 3 0 Premier niveau « complexe »
D 8 0 0 Labyrinthe (50 zones, 8 telep, 8 lifts, 3 obj)
E 0 0 0
F 3 0 1
G 1 0 0 33 Scenarise (10 messages narratifs !)
H 0 0 0 64
I 0 2 0 56
J 5 1 7 17 Aquatique (zones 0-4 spawn dans eau)
K 6 0 0
L 0 0 0 54 Niveau « spectacle » (2 objets)
M 0 0 0 122
N 1 2 1 Test/debug (2x « hello »)
O 22 2 6 138 Hub massif (22 telep !)
P 0 12 0 76 Vertical (mezzanines partout)

Totaux : 49 teleporteurs, 22 zones a 2 niveaux, 22 zones aquatiques.

Switches : table inutilisee dans le jeu de base

0 switches actifs sur 16 niveaux. La table SwitchRoutine existe (offset present) mais est vide partout. Donc cette routine est du code mort dans la version finale du jeu. Les leviers/boutons sont implementes via ENT_TYPE_ACTIVATABLE qui utilise le mecanisme EntT_DoorsAndLiftsHeld_l.

Niveau G : narration scenarisee

Le niveau G est le plus narratif, les 10 messages forment l’histoire complete :
1. Energy weapon intro (collecte arme)
2. Ventilation duct discovery (passage cache)
3. Fan mechanism puzzle (obstacle)
4. Pillar objective (objectif vise)
5. Get out of here ! (urgence apres trigger)
6. Sky/planet surface transition (sortie outdoor)
7. Strike one generator ! (boss/objectif)
8. Magnetic seal puzzle (sortie verrouillee)
9. Way out (chemin trouve)
10. « That was a really bad idea » (game over scripte)

Mecanisme cle->unlock confirme (newaliencontrol.s::Collectable + JUMPALIEN)

Collectable:
    move.w  ObjT_ZoneID_w(a0),d0
    bge.s   .ok_in_room
    rts                              ; deja ramasse -> exclu
.ok_in_room:
    move.l  EntT_DoorsAndLiftsHeld_l(a0),d1
    or.l    d1,Anim_DoorAndLiftLocks_l   ; OR ses bits dans les locks globaux

Anim_DoorAndLiftLocks_l = OR de tous les EntT_DoorsAndLiftsHeld_l des objets vivants/non collectes (aliens ET pickups). Quand un pickup est ramasse, son ObjT_ZoneID_w = -1 -> exclu de l’OR au prochain frame -> la porte n’est plus bloquee.

DoorRoutine lit ensuite ces bits :

move.w  Anim_DoorAndLiftLocks_l,d5
btst    d2,d5         ; d2 = anim_CurrentLiftable_w (index porte)
beq.s   satisfied     ; bit non set -> porte peut s'ouvrir

Note IMPORTANTE : la ligne ; and.w Conditions,d2 est COMMENTEE dans DoorRoutine. Le mecanisme utilise Anim_DoorAndLiftLocks_l, pas la variable Conditions (qui semble inutilisee finalement, malgre les references dans SwitchRoutine code mort).

Donnees deja disponibles dans le JSON pour implementer ca

  • objects[].doorLocks (low word de EntT_DoorsAndLiftsHeld_l)
  • objects[].liftLocks (high word)
  • objects[].displayText (index dans messages[] pour hint au pickup)
  • Index de la porte = position dans doors[]
  • Index du lift = position dans lifts[]

Pas besoin de plus de parsing, il faut juste exploiter en runtime.

Etape 3 – Bug du LIFT zone 104 : trou dans le sol (FAIT)

Symptome : le joueur tombe dans un trou JME -43 au lieu d’arriver a -10.75 dans la zone du lift.

Cause racine : depuis la session 92 fix 4, le sol statique de la zone-lift est explicitement retire dans LevelSceneBuilder (pour eviter le double-rendering avec le sol dynamique). Mais l’ajout de la collision physique sur le sol dynamique avait ete oublie.

Resultat : le mesh dynamique est genere et rendu correctement a yRest (-10.75 JME), mais sans RigidBodyControl -> aucune collision Bullet -> le joueur traverse le sol et tombe dans le trou (jusqu’a la prochaine collision en dessous, soit yBot=-43).

Fix session 99 :

  1. GameAppState.setupPhysics() : pour chaque lift_<zid> Node, on cree un RigidBodyControl kinematic (mass=0 + setKinematic=true) sur le Geometry lift_floor_*, et on l’ajoute au PhysicsSpace.

  2. LiftControl.updateFloorMesh() : refactor de l’animation pour etre simple-et-correcte :
    Avant : on modifiait le buffer Position du Mesh chaque frame (couteux, ne suit pas la collision)
    Apres : on garde le Mesh STATIC genere a yBot, et on translate le Geometry.setLocalTranslation(0, currentFloorYDelta, 0). Le RigidBody kinematic suit via setPhysicsLocation.

Cette approche est plus naturelle (un seul LocalTranslation au lieu de N vertices a recalculer), plus rapide, et fait suivre la collision.

Fichiers : app/GameAppState.java (creation RigidBody kinematic), world/LiftControl.java (refactor updateFloorMesh).

A tester : entree dans la zone 104 (lift bas du level A) – le joueur doit etre stable au sol a -10.75 JME, et monter avec le lift quand il bouge.

Etape 3 (suite) – MeshCollisionShape ne fonctionne pas en kinematic

Symptome 2 : le joueur arrive sur la plateforme du lift (collision fonctionne quand il atterrit) mais des qu’il bouge il tombe a travers.

Cause : CollisionShapeFactory.createMeshShape() cree un MeshCollisionShape (concave). Bullet ne supporte pas de deplacer un MeshCollisionShape, meme en kinematic. Les contacts disparaissent des qu’on bouge.

Fix : remplacer par une BoxCollisionShape (convexe). Pour un sol de lift, une boite plate (extent.y = 0.1) suffit, dimensionnee depuis la BoundingBox du Geometry. Le RigidBody est positionne au centre du polygone (physCenterX/Y/Z stocke en UserData), et LiftControl deplace verticalement de currentFloorYDelta autour de ce centre.

Limitation reconnue : le CharacterControl Bullet ne suit pas automatiquement les kinematic platforms en mouvement. Quand le lift montera, le joueur restera sur place pendant que la plateforme passe a travers lui. Solution future : LiftControl.setPlayerStoodOnLift(true) -> pousser manuellement la position Y du joueur dans GameAppState.update().

Etape 3 (refonte) – Approche « sol statique + push Y » (FAIT)

Symptome 3 : avec BoxCollisionShape kinematic, le joueur tombe quand il bouge sur le lift. Investigation des donnees zone 104 :

  • Geometrie : un losange (4 edges, sommets a 45 degres) — pas un rectangle
  • floorH=344 (-10.75 JME), roofH=-128 (+4 JME)
  • bottom=1376 (-43 JME, jamais utilise car lowerCondition=NEVER)
  • top=-64 (+2 JME, niveau atteint quand le lift est en haut)
  • 2 zones voisines : zone 103 (couloir bas, floorH=344) et zone 105 (couloir haut, floorH=-8)
  • Le lift connecte le couloir bas au couloir haut, course de ~12.75 unites JME

Probleme architectural : le CharacterControl Bullet ne suit pas les MeshCollisionShape ni les BoxCollisionShape kinematic. La box deborde du losange (plus large dans les coins) et le joueur « glisse hors » quand il marche.

Refonte session 99 :

  1. LevelSceneBuilder.java : ANNULE le retrait du sol statique pour les zones-lifts (sess 92 fix 4 etait une erreur). Le sol de la zone-lift est maintenant present dans geometry/ (= collision Bullet globale OK).

  2. LiftControl.java : simplifie – se contente de translater le Geometry visuel localement de currentFloorYDelta. Pas de gestion de RigidBody. Expose getCurrentFloorY() et isPlayerOver(liftNode) pour utilisation externe.

  3. GameAppState.java : nouvelle methode applyLiftPushY() appelee chaque frame. Si le joueur est dans le polygone XZ d’un lift et que le sol visuel du lift est plus haut que ses pieds, on teleporte le joueur (via setPhysicsLocation) pour le poser sur le sol. Comme ca le joueur monte avec le lift sans casser le moteur Bullet.

Approche fidele a l’ASM Amiga : LiftRoutine modifie ZoneT_Floor_l, et le code physique du jeu original re-positionne le joueur en consequence (pas de kinematic platforms – tout etait calcule manuellement).

Avantages :
– Au repos : sol statique = collision Bullet OK, joueur stable comme dans toute autre zone
– Visuellement : le sol du lift remonte (Geometry localTranslation)
– En mouvement : le joueur suit le sol via push Y manuel
– Le sol statique sous le sol visuel reste un « filet de securite » si jamais le push Y rate un frame

Laisser un commentaire

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