Registrarse

[N64] Ingeniería Inversa de los modelos de Pokémon Stadium - Stadium2Unity - Importer para Unity Engine

Manurocker95

Doctorando en Ingeniería Biomédica & Game Dev
Miembro insignia
Buenas, os dejo por aquí un write up de ingeniería inversa de modelos y animaciones de Pokémon Stadium (N64) que he añadido en mi importer para Unity "Stadium2Unity" como .md. Este post separa las observaciones confirmadas de las hipótesis de trabajo y mencionar que no habría sido posible sin la ayuda del usuario DramaticShape (y su mod para PokémonGen1Recomp), así como todo el repositorio de Pret y aquellos que lo contribuyen, así que muchísimas gracias por la ayuda.

Nota: Los offsets hacen referencia a la ROM utilizada durante esta investigación, siendo ésta la versión USA recomendada por Pret para el proyecto de decompilación pokestadium, con md-5: 3a7324ce816d5891dea074055690750a.

1. Estructura general

El pipeline de modelos Pokémon de Pokémon Stadium es bastante autocontenido. El recorrido principal es:

Código:
ROM
|
+-- Archivo de modelos Pokémon @ 0x00920000
      |
      +-- entrada del archivo
           |
           +-- descompresión cuando sea necesaria
           |    +-- PERS-SZP
           |    +-- Yay0
           |
           +-- módulo FRAGMENT
                |
                +-- Species ID
                |
                +-- stream de comandos de modelo/layout
                |    +-- skeleton
                |    +-- materiales
                |    +-- texturas / TLUT
                |    +-- display lists / geometría
                |
                +-- lista de punteros a animaciones esqueléticas
                |
                +-- lista de punteros a animaciones
                     auxiliares/de materiales
Una característica importante de Stadium 1 es que el propio root del FRAGMENT permite acceder tanto a la lista de animaciones esqueléticas como a la lista de animaciones auxiliares/de materiales. Existe además información de Batalla independiente en la ROM que resulta útil para determinar qué animación auxiliar/material está asociada normalmente con cada animación esquelética.

2. Normalización de la ROM y revisión utilizada

El importer admite los tres byte orders habituales de Nintendo 64 y los normaliza al orden big-endian de la ROM.

Magic values reconocidos:

Código:
0x80371240  z64 / big-endian nativo
0x37804012  v64 / byte-swapped
0x40123780  n64 / word-swapped
La implementación se desarrolló utilizando la ROM USA .n64 con MD5:

Código:
3a7324ce816d5891dea074055690750a
Un MD5 diferente se trata actualmente como una advertencia. Por tanto, todos los offsets fijos de ROM documentados a continuación deben considerarse específicos de esta revisión mientras no se comprueben en otras versiones.

3. Archivo de modelos Pokémon

El archivo de modelos Pokémon comienza en:

Código:
ROM 0x00920000
La cabecera contiene el número de entradas en:

Código:
archive + 0x0C
Los descriptores empiezan en:

Código:
archive + 0x10
y tienen un stride de:

Código:
0x10 bytes
El parser actual utiliza los dos primeros words de cada descriptor:

Código:
struct ArchiveEntry
{
    u32 relativeOffset;
    u32 compressedSize;

    // El resto de campos del descriptor
    // no son necesarios para el parser actual.
};
Por tanto, el payload se obtiene desde:

Código:
0x00920000 + relativeOffset
y ocupa:

Código:
compressedSize
bytes.


3.1. Compresión

Una entrada puede estar:

Código:
sin comprimir

Yay0

PERS-SZP
    -> contiene un stream Yay0 interno
En `PERS-SZP`, el offset hacia el stream Yay0 real se encuentra en:

Código:
+0x08
El flujo utilizado es:

Código:
PERS-SZP
    -> leer offset del stream interno
    -> decodificar Yay0

Yay0
    -> decodificar Yay0

cualquier otro caso
    -> utilizar directamente el payload
Es conveniente mantener separadas la extracción del archivo y el parseo del FRAGMENT. No se debe asumir que cada entrada del archivo comienza directamente por `FRAGMENT`.

4. Módulos FRAGMENT

Después de la descompresión, los recursos Pokémon son módulos FRAGMENT. El parser comprueba:

Código:
offset +0x08: "FRAGMENT"
Los punteros internos utilizan direcciones virtuales relativas a:

Código:
FRAGMENT base = 0x8FF00000
Por tanto:

Código:
localOffset = pointer - 0x8FF00000
Un puntero con valor cero se interpreta como null.

4.1. Localización del root

El root no se obtiene mediante un offset local hardcodeado. El parser busca aproximadamente entre:

Código:
0x20 .. 0x80
una secuencia MIPS formada por un `LUI` seguido de un `ADDIU` que utilice el mismo registro.

Conceptualmente:

Código:
LUI   rX, hi(root)
ADDIU rX, rX, lo(root)
Combinando ambos inmediatos se obtiene la dirección virtual del root.

Después:

Código:
rootLocalOffset =
    rootVirtualAddress - 0x8FF00000
Esto evita asumir que la estructura root siempre se encuentra en la misma posición local dentro del FRAGMENT.


5. Estructura root

Los campos utilizados por el parser son:

Código:
root +0x00 : u16 species ID

root +0x08 : ptr
             -> lista de punteros a streams
                de modelo/layout

root +0x0C : ptr
             -> lista de punteros a
                animaciones esqueléticas

root +0x10 : ptr
             -> lista de punteros a
                animaciones auxiliares/materiales
Las listas terminan al encontrar:

Código:
0x00000000
El parser de modelos actual solo necesita el primer stream de layout.

Una de las observaciones más útiles de Stadium 1 es, por tanto:

Código:
FRAGMENT
|
+-- geometría
+-- skeleton
+-- animaciones esqueléticas
+-- animaciones auxiliares/materiales
Todo ello puede alcanzarse partiendo del mismo root.


6. Stream de comandos de Layout

El modelo/layout no es simplemente un array convencional de bones y meshes. Se trata de un stream/árbol de comandos que debe interpretarse recursivamente.

Durante el recorrido, el parser mantiene:

Código:
bone stack
textura actual
TLUT actual
material display list actual
canal de texture animation actual
estado de tiles de N64
vertex cache de 64 entradas

6.1. Comandos estructurales

Entre los comandos observados y utilizados están:

Código:
0x00 / 0x03
    recurse hacia un stream hijo

0x02
    jump / continuar en otro stream

0x05
    push del bone actual

0x06
    pop del bone
Para reconstruir el modelo, lo fundamental es respetar la jerarquía mientras se recorre el stream.

6.2. Model Header — comando 0x17

El comando `0x17` expone las tablas de texturas y TLUT.

Campos relevantes:

Código:
+0x02 : s16  textureCount
+0x04 : s16  tlutCount
+0x08 : ptr  textureTable
+0x0C : ptr  tlutTable
Los registros de textura tienen un stride de:

Código:
0x0C bytes
con:

Código:
+0x00 : u8   format
+0x01 : u8   size
+0x02 : s16  width
+0x04 : u16  height
+0x08 : ptr  texel data
Los registros TLUT también utilizan:

Código:
0x0C bytes
y contienen:

Código:
+0x02 : u16  count
+0x04 : ptr  palette data
+0x08 : ptr  palette display list
La display list de la TLUT puede modificar/sobrescribir el puntero o el número de colores de la paleta. Por ello, seguir esa display list es importante para reconstruir correctamente las texturas CI.

6.3. Root Scale — comando 0x1C

El parser lee tres valores signed 16.16 fixed-point:

Código:
+0x04 : X
+0x08 : Y
+0x0C : Z
que forman la escala root del modelo.

6.4. Bone — comando 0x1D

La estructura utilizada por el parser es:

Código:
+0x01 : u8   boneId
+0x02 : u8   flags
+0x03 : s8   animation channel

+0x04 : s16  translation X
+0x06 : s16  translation Y
+0x08 : s16  translation Z

+0x0A : s16  rotation X
+0x0C : s16  rotation Y
+0x0E : s16  rotation Z

+0x10 : s32  scale X, 16.16
+0x14 : s32  scale Y, 16.16
+0x18 : s32  scale Z, 16.16
El parent del bone es el bone que se encuentre en la parte superior del stack en ese momento.

6.5. Bone.Channel: asociación con las animaciones

Un detalle especialmente importante es que no se debe asumir:

Código:
bone N = animation channel N
Cada bone contiene explícitamente:

Código:
s8 Channel
en:

Código:
bone command +0x03
Por tanto:

Código:
Bone
|
+-- Channel
      |
      +-- canal que debe utilizar
           el decoder de animación
Un valor negativo significa que el bone no tiene un track de animación muestreado y debe mantener su transformación de bind pose.

Para los bones animados, `Bone.Channel` es el índice utilizado por el decoder.

6.6. Estado de material/textura — comando 0x23

El parser extrae:

Código:
+0x02 : texture-animation channel
+0x04 : material display-list pointer
+0x08 : texture index
+0x0A : TLUT index
Después sigue la material display list para recuperar:

Código:
palette state
tile addressing
mirror S/T
clamp S/T
El `TextureAnimation` channel es el que posteriormente permite conectar una primitive con una entrada de:

Código:
AuxAnimationData.Channels[]

7. Display Lists de N64 y geometría

La geometría se reconstruye interpretando las display lists referenciadas desde el layout. El parser implementa el subconjunto necesario para los modelos Pokémon, incluyendo:

Código:
display lists anidadas
carga de vértices
emisión de triángulos
estado de render
estado de tiles

7.1. Formato de vértice

Para el formato observado, cada vértice ocupa:

Código:
0x10 bytes
Layout:

Código:
+0x00 : s16 position X
+0x02 : s16 position Y
+0x04 : s16 position Z

+0x08 : s16 texture S
+0x0A : s16 texture T

+0x0C..0x0F :
    normal + alpha
    o
    RGBA
Las coordenadas de textura se interpretan como:

Código:
S / 32.0
T / 32.0
La interpretación de `0x0C..0x0F` depende del geometry mode de iluminación de N64.

Con lighting activo:

Código:
s8 nx
s8 ny
s8 nz
u8 alpha
Sin lighting:

Código:
u8 red
u8 green
u8 blue
u8 alpha
Cada vértice cargado se asocia con el bone activo en el momento en que se procesa su display list.


7.2. Tile state

El estado equivalente a `G_SETTILE` permite recuperar:

Código:
palette index
mirror S
mirror T
clamp S
clamp T
Estos estados deben conservarse.

Una textura que visualmente parece estar mal decodificada puede en realidad tener una decodificación correcta y estar utilizando incorrectamente mirror/clamp. Si el motor de destino no admite directamente algún modo de wrap, puede ser necesario modificar/bakear la textura.

Eso es una solución del exporter y no forma parte del formato de Stadium.

8. Estructura de las animaciones esqueléticas

La cabecera de una animación esquelética se interpreta como:

Código:
+0x00 : u8   flags
+0x06 : u16  loopStart
+0x08 : u16  channelCount
+0x0A : u16  frameCount

+0x0C : ptr  channel descriptor table
+0x10 : ptr  scale data
+0x14 : ptr  rotation data
+0x18 : ptr  translation data
Cada descriptor de bajo nivel ocupa:

Código:
0x0A bytes
y contiene:

Código:
+0x00 : u8   número de valores/keys de scale
+0x01 : u8   número de valores/keys de rotation
+0x02 : u8   número de valores/keys de translation
+0x03 : u8   interpolation flags

+0x04 : u16  scale offset/index
+0x06 : u16  rotation offset/index
+0x08 : u16  translation offset/index

8.1. Tres descriptores por canal lógico

El `Channel` de un bone se convierte en:

Código:
baseIndex = Bone.Channel * 3
A continuación se leen tres descriptores consecutivos:

Código:
baseIndex + 0 -> X
baseIndex + 1 -> Y
baseIndex + 2 -> Z
Cada descriptor describe de forma independiente los datos de:

Código:
Scale
Rotation
Translation
para ese eje.

La asociación completa es:

Código:
Bone
|
+-- Bone.Channel
      |
      +-- Channel * 3
           |
           +-- descriptor X
           +-- descriptor Y
           +-- descriptor Z
Por tanto, la asociación correcta no es:

Código:
bone index -> track index
sino:

Código:
Bone.Channel -> grupo de tres descriptores

9. Modos de sampling de animación

Los flags de la animación seleccionan al menos dos familias de encoding en el decoder actual.

9.1. Modo Hermite / keyframes

Cuando:

Código:
flags & 0x08
está activo, Scale/Rotation/Translation pueden representarse mediante keyframes.

Una key contiene:

Código:
s16 frame
s16 value
s16 inTangent
[s16 outTangent]
El interpolation flag de cada componente determina si existe un `outTangent` independiente.

El mapping utilizado es:

Código:
interpolation & 0x01
    translation wide key

interpolation & 0x02
    rotation wide key

interpolation & 0x04
    scale wide key
La interpolación se evalúa mediante Hermite.

Conversiones observadas:

Código:
rotation value / 10.0
    -> grados

scale value / 100.0
Después, la rotación se convierte a las unidades angulares de 16 bits utilizadas por Stadium.


9.2. Modo packed / sampled

Sin el flag Hermite, un componente puede ser:

Código:
ausente
    -> conservar componente de bind pose

constante
    -> valor almacenado directamente
       en el descriptor

sampled
    -> valores leídos de los arrays
       de datos del componente
Los tamaños observados incluyen:

Código:
translation
    12-bit o 16-bit según flags

rotation
    samples packed de 12 bits
    promovidos a unidades angulares de 16 bits

scale
    s16 / 1000
Es especialmente importante inicializar cada componente con el valor de bind pose antes de aplicar el sampling.

Si un componente no está presente:

Código:
fallback correcto = bind pose
fallback incorrecto = 0

10. Representación de las rotaciones

Los bones y tracks decodificados conservan las rotaciones utilizando una unidad basada en una vuelta de 16 bits:

Código:
65536 unidades = 360 grados
Para las rotaciones Hermite, el valor almacenado en la key se interpreta primero como décimas de grado antes de convertirlo. Al exportar a otro motor conviene hacer esta conversión de forma explícita y consistente.

Errores pequeños en esta parte pueden provocar:

Código:
jitter
saltos al cruzar el wrap angular
rotaciones por el camino largo

11. Animaciones auxiliares / de materiales

Stadium 1 contiene una lista independiente de animaciones auxiliares en el root del FRAGMENT.

La cabecera parseada es:

Código:
+0x00 : u8   flags
+0x06 : u16  loopStart
+0x08 : u16  channelCount
+0x0A : u16  frameCount
+0x0C : ptr  channel table
+0x10 : ptr  data
Cada descriptor de canal auxiliar ocupa cuatro bytes:

Código:
+0x00 : u16 count
+0x02 : u16 baseIndex
Los datos son valores de un byte.

Para cada frame:

Código:
value =
    data[
        baseIndex
        + min(frame, count - 1)
    ]
Si:

Código:
count == 0
el parser devuelve:

Código:
-1
Estos valores funcionan como selecciones de textura/material para primitives cuyo comando de layout tenga asociado el mismo canal `TextureAnimation`. Así se representan, entre otras cosas, cambios animados como:

Código:
ojos
bocas
otras variaciones de material/textura

12. Asociación entre animaciones esqueléticas y de materiales

Aunque las dos listas de animaciones se encuentran dentro del FRAGMENT del modelo, la asociación preferente:

Código:
animación esquelética
        <->
animación auxiliar/material
se obtiene de los datos de Batalla de la ROM.

Las localizaciones fijas utilizadas para la ROM USA son:

Código:
Main ROM code/data base : 0x00001000
Main VRAM base          : 0x80000400
Pointer table VRAM      : 0x80075BD0
Battle data base        : 0x0070D3A0
La tabla de punteros contiene una entrada de 32 bits por especie.

Para especies:

Código:
1 .. 151
el offset del puntero se calcula como:

Código:
pointerOffset =
      MainRomOffset
    + (PointerTableVram - MainVram)
    + (species - 1) * 4
El importer conserva únicamente los 24 bits inferiores:

Código:
relative = pointer & 0x00FFFFFF
y obtiene la tabla de Batalla mediante:

Código:
battleTable =
    0x0070D3A0 + relative
La tabla de Batalla de cada Pokémon inspeccionada por el importer tiene:

Código:
stride/size = 0xB90
entry size  = 0x10
Para asociar las animaciones se utilizan los dos primeros bytes:

Código:
entry +0x00
    -> skeletal animation index

entry +0x01
    -> auxiliary animation index

0xFF
    -> sin animación auxiliar
Conceptualmente:

Código:
Datos de Batalla
|
+-- entrada
      |
      +-- animation index
      |
      +-- auxiliary animation index
El importer cuenta cuántas veces aparece cada pareja:

Código:
(animationIndex, auxIndex)
y asigna a cada animación esquelética la animación auxiliar que aparece asociada con mayor frecuencia.

Hay que distinguir aquí dos cosas:

El FRAGMENT indica qué animaciones existen.

Los datos de Batalla indican qué animaciones de material se utilizan junto a determinadas animaciones esqueléticas en contextos de Batalla.


Elegir únicamente la pareja más frecuente es una heurística del importer. Una reimplementación más estricta del juego podría conservar toda la tabla de Batalla en lugar de reducirla a una única `AuxAnimation` preferida.

13. Estrategias para exportar las animaciones de materiales

Los datos reconstruidos pueden representarse de distintas formas en el motor de destino.

El importer de Unity utilizado durante esta investigación permite dos estrategias.

13.1. Animation Curves

Se pueden generar object-reference keys dentro del AnimationClip que sustituyan materiales/texturas en los frames correspondientes. Esto representa de forma bastante directa el `AuxAnimationData` decodificado.

13.2. Callbacks / Animation Events

Otra posibilidad es generar eventos de animación y utilizar un componente runtime que seleccione la textura/material correspondiente de un conjunto previamente generado. Esto puede resultar útil en motores antiguos o plataformas con restricciones adicionales. Ambos métodos son decisiones del exporter/runtime. No representan dos formatos diferentes dentro de Pokémon Stadium.

14. Pokémon Shiny

La exportación de Pokémon Shiny debe considerarse una capa adicional sobre el parser principal del modelo.

El FRAGMENT proporciona los recursos normales de:

Código:
texturas
TLUT
geometría
skeleton
animaciones
La implementación utilizada en esta investigación añade conocimiento específico sobre recoloreado/LUT para reconstruir las variantes Shiny.

La distinción importante es:

Código:
Parsing del FRAGMENT
    = ingeniería inversa del formato del modelo

Reconstrucción Shiny
    = interpretación de color/LUT
      aplicada sobre el modelo ya parseado
No se debe asumir que junto a cada Pokémon normal exista un segundo modelo completo almacenado específicamente para su versión Shiny.

El exporter puede generar:

Código:
Shiny textures
Shiny materials
Shiny prefab
reutilizando:

Código:
geometría
skeleton
animaciones
del modelo normal.


15. Variantes de color HUE / Entrenador

Las variantes de color dependientes del entrenador son también una capa adicional sobre el modelo normal.

El importer utilizado permite:

Código:
HUE obtenido de datos del entrenador
HUE aleatorio
HUE manual
La variante reutiliza:

Código:
geometría
skeleton
timing de las animaciones esqueléticas
y sustituye los recursos correspondientes de:

Código:
texturas
materiales

15.1. Animaciones de materiales en las variantes

Al exportar una variante hay un detalle importante.

No basta con:

Código:
1. crear prefab variante
2. reemplazar materiales estáticos
si los clips de animación siguen haciendo referencia a los materiales normales.

En ese caso el Pokémon puede verse correctamente hasta que se reproduzca una animación de ojos, boca u otro cambio de material.

Entonces reaparecería la textura normal.

El orden correcto en el motor de destino es:

Código:
1. Crear/copiar texturas y materiales de la variante.

2. Clonar/remapear los clips de animación
   de materiales o los texture-swap sets.

3. Hacer que esas referencias apunten
   a los materiales/texturas de la variante.

4. Asignar los materiales estáticos
   de la variante a los renderers.

5. Guardar el prefab variante.
Esto no constituye una regla nueva del formato de ROM. Es una consecuencia de conservar correctamente las referencias representadas por los canales de animación auxiliar.


16. Arquitectura recomendada para un parser

Una implementación puede separarse en:

Código:
RomReader
|
+-- normalizar byte order de N64
+-- localizar archivo en la revisión conocida
+-- extraer entradas

ArchiveDecoder
|
+-- raw
+-- PERS-SZP
+-- Yay0

FragmentReader
|
+-- virtual pointer -> local pointer
+-- localizar root mediante bootstrap MIPS
+-- lecturas endian

FragmentParser
|
+-- metadata del root
+-- layout command walker
+-- skeleton
+-- registros texture/TLUT
+-- display lists de N64
+-- animaciones esqueléticas
+-- animaciones auxiliares

BattleDataParser
|
+-- uso/asociación:
     skeletal animation
       <->
     auxiliary animation

EngineExporter
|
+-- meshes
+-- skeleton
+-- textures/materials
+-- skeletal clips
+-- material animation
+-- Shiny assets
+-- variantes HUE
Mantener el exporter del motor separado del código de lectura/parser de ROM permite diferenciar claramente:

Código:
dato original
interpretación del formato
decisión de exportación

17. Errores comunes durante la reconstrucción

17.1. Asociar las animaciones mediante el índice del bone

Incorrecto:

Código:
bone 0 -> animation channel 0
bone 1 -> animation channel 1
...
Correcto:

Código:
bone
-> Bone.Channel
-> channel descriptors
Los bones con:

Código:
Channel < 0
no se muestrean.


17.2. Suponer un offset fijo para el root del FRAGMENT

El parser funcional obtiene el root desde el bootstrap MIPS del propio módulo.


17.3. Ignorar la bind pose como fallback

Los componentes ausentes conservan su valor de bind pose. Utilizar cero como fallback corrompe la pose.

17.4. Decodificar CI sin el estado de la display list de TLUT

La display list del registro TLUT puede resolver o modificar la información de la paleta.


17.5. Ignorar mirror/clamp

Una textura puede estar correctamente decodificada y aun así visualizarse mal si se ignora el tile state.


17.6. Tratar material animation como skeletal animation

Son estructuras distintas:

Código:
AnimationData
    -> Scale / Rotation / Translation de bones

AuxAnimationData
    -> selección de material/textura

17.7. Cambiar los materiales de una variante sin cambiar sus referencias animadas

Esto puede hacer que aparezcan ojos/bocas normales sobre un Pokémon cuya variante HUE o Shiny está correctamente aplicada.


17.8. Suponer que todas las entradas están sin comprimir

Hay que comprobar:

Código:
PERS-SZP
Yay0

18. Qué está sólidamente establecido por la implementación

El importer funcional proporciona evidencia de trabajo para:

Código:
Archivo de modelos Pokémon:
    ROM 0x00920000

Número de entradas:
    archive +0x0C

Stride de descriptor:
    0x10

Descompresión:
    PERS-SZP / Yay0

Base virtual FRAGMENT:
    0x8FF00000

Root:
    obtenido mediante código MIPS

Root:
    Species ID
    model/layout pointer list
    skeletal animation pointer list
    auxiliary animation pointer list

Skeleton:
    command stream
    Bone.Channel explícito

Texturas:
    texture tables
    TLUT tables
    palette state
    mirror/clamp

Geometría:
    display lists de N64

Animaciones:
    Scale / Rotation / Translation
    Hermite
    packed/sample

Material animation:
    auxiliary channels

Datos de Batalla:
    asociación skeletal/Aux

Exportación:
    normal
    Shiny
    HUE variants

19. Aspectos que todavía deben tratarse con cautela

El importer actual es suficiente para reconstruir los modelos, pero no todos los bytes del formato tienen un significado semántico confirmado.

En particular:

  • Los campos no utilizados de los descriptores de archivo de `0x10` bytes no están documentados aquí.
  • Muchos IDs de comandos de layout solo se implementan hasta el nivel necesario para reconstruir los modelos utilizados.
  • No es necesario interpretar todos los opcodes posibles de las display lists de N64.
  • Varios bits de flags de animación se conocen por su comportamiento operativo, pero no necesariamente por sus nombres simbólicos originales.
  • Reducir las referencias de Batalla a una única `AuxAnimation` preferida es una decisión del importer.
  • La reconstrucción Shiny/HUE incluye conocimiento de comportamiento/color adicional al parser puro del FRAGMENT.
  • Los offsets fijos de ROM están confirmados para la revisión USA utilizada y no deben asumirse universales.

Para un trabajo orientado a decompilación es preferible conservar los campos desconocidos y los registros raw, aunque un exporter de assets concreto no los necesite.


20. Stadium 1 frente a Stadium 2: diferencia principal

Stadium 1 resultó especialmente útil como referencia al estudiar Stadium 2 porque buena parte del modelo conceptual se mantiene:

Código:
FRAGMENT
Bone.Channel
descriptores S/R/T
canales Aux/material
estado de materiales/display lists de N64
La diferencia importante de almacenamiento es:

Código:
Stadium 1:

    model FRAGMENT
        -> lista de animaciones esqueléticas
           accesible directamente


Stadium 2:

    entrada N del archivo de modelos de Batalla
        +
    entrada N del archivo separado
    de animaciones de Batalla
        ->
    animation bank
Por tanto, un decoder derivado de Stadium 1 puede interpretar correctamente los canales S/R/T y, aun así, no encontrar animaciones en Stadium 2 si está buscando los recursos en el lugar equivocado. Una vez localizado el animation bank separado de Stadium 2, la semántica de canales observada en Stadium 1 sirve como referencia para interpretar las animaciones de Batalla. Si quieres ver el write-up completo de Stadium 2, lo tienes en este mismo foro: https://whackahack.com/foro/threads/n64-ingenieria-inversa-de-los-modelos-de-pokemon-stadium-2-stadiumgs2unity-importer-para-unity-engine.69322/

21. Pseudocódigo mínimo

Una importación simplificada de un Pokémon de Stadium 1 sería:

Código:
rom = normalize_n64_rom(file);

archive = rom + 0x00920000;
count = be32(archive + 0x0C);

entry = archive + 0x10 + index * 0x10;

payload =
    archive + be32(entry + 0x00);

size =
    be32(entry + 0x04);

fragmentBytes =
    decompress_if_needed(payload, size);

fragment =
    FragmentReader(
        fragmentBytes,
        0x8FF00000);

root =
    find_root_from_mips_bootstrap(fragment);

model.species =
    be16(root + 0x00);

layouts =
    read_ptr_list(
        ptr(root + 0x08));

animations =
    read_ptr_list(
        ptr(root + 0x0C));

auxAnims =
    read_ptr_list(
        ptr(root + 0x10));

walk_layout(
    layouts[0],
    model);

for each animation:

    for each bone:

        if bone.channel >= 0:

            sample animation channel
            bone.channel
            into S/R/T track;

for each aux animation:

    decode byte-valued
    material channels;

read battle table
for model.species;

associate skeletal animation indices
with auxiliary animation indices;
Ese es el modelo mental mínimo del formato que implementa actualmente el parser.

22. Base utilizada para este write-up

La intención es documentar las estructuras y comportamientos soportados por esa implementación sin inventar nombres para campos cuyo significado todavía no está confirmado. Los descubrimientos más reutilizables para ingeniería inversa son:

Código:
estructura root del FRAGMENT

binding mediante Bone.Channel

decodificación de los
descriptores de animación

representación de las animaciones
auxiliares/de materiales

asociación independiente mediante
los datos de Batalla
23. Resultado

Como una imagen vale más que mil palabras, aquí está el resultado de toda esta investigación:



24. Front-End

Al utilizar Unity Engine como Front-End para hacer ingeniería inversa, obtenemos una forma directa y completamente user-friendly de importar modelos para Fangames.

25. Créditos

La investigación se realizó de forma independiente durante el desarrollo de Stadium2Unity, comparando con la implementación de DramaticShape para Pokemon Stadium 1 para PokemonGen1Recomp y con la decompilación "pret/pokestadium". Una vez más, mil gracias a todo el equipo que se dedica horas y horas a hacer ingeniería inversa de todas nuestras ROMs favoritas.
 
Última edición:
Arriba