Registrarse
  • ¡Votaciones abiertas del Concurso de Sprites SP 2026!
    Mirá los sprites de los participantes y elegí tus favoritos.
    Ver sprites y votar →

[GC] Ingeniería Inversa de los modelos 3D de Pokémon Colosseum y Pokémon XD - GameCube2Unity - Game Cube

Manurocker95

Doctorando en Ingeniería Biomédica & Game Dev
Miembro insignia
Buenas, os dejo por aquí otro write up de ingeniería inversa. En este caso, de modelos y animaciones de Pokémon Colosseum y Pokémon XD (GC) que he ido utilizando para crear mi importer para Unity "GameCube2Unity". Este post separa las observaciones confirmadas de las hipótesis de trabajo y mencionar que no habría sido posible sin StarsMmd y todo el que ha contribuído al repositorio del addon para Blender. En este caso NO está limitado a una versión específica ni del motor ni de la ISO, ya que funciona para los archivos de todas las regiones. Se ha usado para esta investigación los archivos de la versión PAL de Pokémon Colosseum. Aunque se hable específicamente de Renderers de Unity Engine, es válido para toda representación visual de mallas 3D.

1. Resumen ejecutivo

Pokémon Colosseum y Pokémon XD empaquetan muchos de sus recursos de modelo dentro de contenedores FSYS. Para los modelos de Pokémon, el modelo conceptual útil no es "FSYS = un modelo", sino:

Código:
FSYS
|
+-- one or more embedded resources
       |
       +-- HSD model data
       |     +-- JObj hierarchy / skeleton
       |     +-- DObj geometry ownership
       |     +-- PObj mesh primitives
       |     +-- MObj materials
       |     +-- TObj textures / UV state
       |
       +-- skeletal animations
       |     +-- AObj
       |     +-- FObj channels
       |
       +-- material / texture animations
             +-- MatAnimJoint
             +-- MatAnim
             +-- texture/UV animation channels
El resultado de implementación más importante es que el binding de las animaciones debe seguir el grafo de objetos HSD, no el orden en el que se termina creando las mallas o los renderers.

Esto es especialmente importante para la animación de materiales:

Código:
MatAnimJoint
   -> MatAnim slot
      -> DObj
         -> MObj
            -> PObj / generated Unity Renderer(s)
Un único "DObj"/"MObj" de HSD puede producir más de una malla (en nuestro caso de Unity). Vincular la animación de materiales por índice de renderer hace que las pistas posteriores de ojos, boca o animación de textura terminen desplazándose hacia mallas incorrectas.

La parte esquelética sigue una regla similar: hay que conservar la jerarquía original de joints de HSD y evaluar los canales de animación HSD contra los transforms a los que realmente apuntan. Una vez que la bind pose, la conversión de coordenadas y la semántica de los canales de animación se manejan de forma consistente, el modelo original puede reconstruirse directamente en Unity Engine.

2. Qué es FSYS dentro de este pipeline

A efectos de extracción de modelos, un archivo ".fsys" debe tratarse como un contenedor de recursos, no como el propio formato de modelo.

Conceptualmente:

Código:
.fsys / FSYS container
    |
    +-- file/resource table
    |
    +-- embedded payload 0
    +-- embedded payload 1
    +-- ...
El importador debe resolver primero el recurso embebido deseado y después analiza las estructuras HSD contenidas en ese payload. Esta distinción es importante durante la ingeniería inversa:

- FSYS responde dónde están los recursos y cómo están empaquetados.
- HSD responde cómo se representan el modelo, el esqueleto, los materiales, las texturas y las animaciones.
- La política de exportación/importación responde cómo se reconstruyen esas estructuras como assets de Unity.

No se debe inferir la semántica de HSD únicamente a partir del orden de las entradas FSYS.

3. Grafo de objetos HSD de alto nivel

La jerarquía del lado del modelo reconstruida por el importador puede resumirse como:

Código:
JObj
|
+-- child JObj
|     |
|     +-- ...
|
+-- DObj
       |
       +-- MObj
       |     |
       |     +-- TObj
       |
       +-- PObj
       +-- PObj
       +-- ...
Los roles importantes son:

Código:
| Objeto HSD | Rol práctico |
|---|---|
| `JObj` | Jerarquía de joints/nodos y transform local. Se utiliza como jerarquía esquelética.|
| `DObj` | Objeto de visualización/modelo unido a un joint. Contiene el estado dibujable/del modelo. |
| `PObj` | Objeto de polígonos/primitivas. Proporciona geometría y datos relacionados con skinning. |
| `MObj` | Objeto de material utilizado por un "DObj". |
| `TObj` | Objeto de textura y estado/transformación de textura.|
| `AObj` | Objeto de animación / contenedor de temporización de animación. |
| `FObj` | Propiedad/canal animado individual. |
| `MatAnimJoint` | Jerarquía de animación de materiales asociada con los joints del modelo. |
| `MatAnim` |  Entrada de animación de material/textura asociada con slots de material del modelo. |
Aquí no se documentan completamente los layouts binarios exactos ni todos los flags; el objetivo son las relaciones necesarias para construir un importador funcional.

4. Esqueleto y bind pose

El árbol "JObj" se reconstruye como una jerarquía de "Transform" de Unity.

Cada joint aporta un transform local de bind aproximadamente equivalente a:

Código:
local translation
local rotation
local scale
El importador debe utilizar la misma conversión de coordenadas tanto para la bind pose como para los valores de animación. Mezclar convenciones es una de las formas más sencillas de obtener una animación que parece razonable en el frame cero pero que se deforma progresivamente.

Durante esta investigación, varios problemas que parecían ser "animaciones incorrectas" eran en realidad problemas de coordenadas/binding:

- Quaternions de Unity inválidos o no normalizados;
- Valores Euler que llegaban a Unity con valores inválidos/no finitos;
- Una animación evaluada respecto al bind transform incorrecto;
- Un eje/signo del transform convertido de forma diferente entre la importación estática del modelo y la importación de animaciones.

Una regla útil de validación es:

Código:
conversión del transform estático de bind == conversión del transform de animación
excepto cuando la semántica del canal HSD requiera explícitamente una operación diferente.

5. Animación esquelética

La animación esquelética se representa mediante objetos de animación HSD y canales por propiedad. El flujo práctico de decodificación es:

Código:
Animation
|
+-- target JObj
       |
       +-- AObj
             |
             +-- FObj: translation X
             +-- FObj: translation Y
             +-- FObj: translation Z
             +-- FObj: rotation X
             +-- FObj: rotation Y
             +-- FObj: rotation Z
             +-- FObj: scale X
             +-- FObj: scale Y
             +-- FObj: scale Z
No todos los joints o animaciones contienen todas las propiedades. Por tanto, la estrategia segura de reconstrucción es:

Código:
Para cada joint animado:
          Comenzar con su transform local de bind/default
          Evaluar únicamente los canales realmente presentes
          Conservar el valor por defecto para los canales ausentes
          Convertir el transform HSD resultante al espacio de Unity
          Generar AnimationCurves de Unity
No se deben inicializar a cero los canales de escala ausentes.
Una animación de escala ausente normalmente significa "conservar la escala existente", no CODE[/CODE].

6. Tratamiento de las rotaciones

La rotación fue una de las partes más sensibles del importador nativo. Las conversiones incorrectas producían errores de Unity como:

Código:
QuaternionToEuler: Input quaternion was not normalized
Assertion failed on expression: 'IsFinite(rot)'
Assertion failed on expression:
'CompareApproximately(SqrMagnitude(result), 1.0F)'
Estos errores deben tratarse como fallos del decoder, no simplemente como warnings del editor de Unity.

Antes de crear un quaternion de Unity:

1. Rechazar o sanear NaN/Infinity.
2. Aplicar la conversión angular HSD correcta.
3. Aplicar la convención de ejes GameCube -> Unity.
4. Normalizar el quaternion.
5. Evitar convertir de nuevo un quaternion malformado mediante "Quaternion.Euler".

Un modelo que empieza cerca de la pose esperada pero "explota" durante la reproducción suele indicar con más fuerza una interpretación incorrecta de los canales o una conversión incorrecta del espacio de transforms que pesos de skinning incorrectos.

7. Duración de la animación y frames adicionales

Una versión temprana del importador generaba keys adicionales más allá de la duración real de la animación HSD.

Esto provocaba dos problemas visibles:

Código:
skeletal clip:
    real animation
    + unintended tail frames
material animation:
    previous state
        -> interpolation through unintended tail
        -> next state
La duración correcta del clip debe proceder de la temporización de la propia animación HSD. Añadir keys para implementar animación de material stepped no debe extender el clip. Para un clip que termina en el frame "N", las keys auxiliares deben permanecer dentro del intervalo real de la animación.

8. La animación de materiales es independiente de la animación esquelética

La animación facial de los Pokémon depende frecuentemente de animación de material/textura en lugar de geometría adicional.

Ejemplos típicos:

- Estados de ojos;
- Estados de boca;
- Selección de atlas de texturas;
- Traslación UV;
- Sustitución de textura.

Por tanto, una animación esquelética puede ser geométricamente perfecta y aun así el Pokémon verse incorrecto porque sus pistas "MatAnim" faltan o están vinculadas al material equivocado. La animación de materiales debe decodificarse como un sistema independiente y asociarse con el mismo modelo importado.

9. La regla crítica de binding de MatAnim

Este fue uno de los fixes de ingeniería inversa más importantes. Una implementación incorrecta asociaba las entradas de animación de materiales con renderers de Unity según el orden de creación:

Código:
MatAnim 0 -> Renderer 0
MatAnim 1 -> Renderer 1
MatAnim 2 -> Renderer 2
Eso es incorrecto.

Un objeto de modelo HSD puede producir múltiples mallas/renderers de Unity. En cuanto ocurre esto, todos los objetivos de animación posteriores quedan desplazados. La relación correcta es:

Código:
MatAnimJoint
   |
   +-- MatAnim
          |
          +-- slot de material/modelo HSD
                 |
                 +-- DObj
                        |
                        +-- MObj
                               |
                               +-- todos los PObj/renderers generados a partir de él
El importador debe conservar suficientes metadatos de origen mientras construye el modelo para poder resolver posteriormente esta relación. Un mapa útil en el lado del importador sería conceptualmente:

Código:
HsdMObjAddress -> List<Renderer>
HsdDObjAddress -> List<Renderer>
HsdJObjAddress -> Transform
La key exacta puede ser un offset, identidad de objeto, índice parseado u otro identificador estable del origen. Lo importante es que proceda de la relación HSD, no del orden de enumeración de Unity.

10. Por qué falla el orden de renderers

Consideremos:

Código:
DObj A
  MObj Eyes
  PObj 0
  PObj 1
DObj B
  MObj Mouth
  PObj 0
Unity puede generar:

Código:
Mesh_0
Mesh_0_2
Mesh_1
Si el importador asume un renderer por slot de material:

Código:
Eyes -> Mesh_0
Mouth -> Mesh_0_2 // INCORRECTO
La animación de la boca se aplica ahora al segundo renderer relacionado con los ojos y todos los bindings posteriores también pueden desplazarse. En su lugar:

Código:
Eyes -> todos los renderers generados desde MObj
Eyes Mouth -> todos los renderers generados desde MObj Mouth
Este fix es independiente del modelo y no debe hardcodearse utilizando nombres concretos de malla.

11. Traslación de texturas HSD y "_MainTex_ST" de Unity

Unity representa la transformación estándar de textura como:

Código:
_MainTex_ST.x = Scale / Tiling X
_MainTex_ST.y = Scale / Tiling Y
_MainTex_ST.z = Offset X
_MainTex_ST.w = Offset Y
Este mapping debe permanecer explícito en el importador. Un bug anterior mezclaba valores de escala base y offset base, haciendo que valores como "1" apareciesen como offset. Otro bug animaba únicamente los componentes de offset dejando implícitamente a cero los componentes de escala, dando como resultado Tiling = (0, 0). La regla segura es: Cuando se genere cualquier animación UV: conservar/escribir la escala base para X e Y animar únicamente los componentes controlados realmente por HSD. Para materiales normales esto suele significar conservar Tiling = (1, 1), salvo que el "TObj" de origen especifique otra escala base.

12. Las traslaciones U y V no son intercambiables

La convención de coordenadas de textura de GameCube/HSD no se mapea de forma idéntica a Unity en ambos ejes. Para los modelos validados durante esta investigación, la conversión funcional es:

Código:
Unity Offset X = -HSD TRANS_U
Unity Offset Y = HSD TRANS_V"
Esto fue necesario para soportar ambos tipos de modelo observados, los que tienen texturas que cambian con Offset en Y hacia abajo (E.g: Zangoose) y en X hacia la derecha (Eg. Venusaur). Esta regla debe seguir considerándose parte del comportamiento actualmente validado del importador y no una especificación completa de todos los posibles modos de matriz de textura HSD.

13. No cuantizar globalmente los offsets UV

Durante el debugging, valores como "0 -0.166687 -0.333252 -0.666687 -0.833252" se interpretaron inicialmente como representación de un atlas universal de tres filas. Esa suposición era incorrecta. Diferentes materiales pueden utilizar diferentes layouts de atlas y diferentes valores de traslación HSD. Los materiales de ojos y boca de un mismo Pokémon no tienen por qué compartir el mismo conjunto de estados UV.

14. Confirmado / fuertemente validado

El siguiente comportamiento está fuertemente validado por el importador nativo funcional y la comparación visual realizada durante el desarrollo:

- FSYS es un contenedor de recursos; la semántica del modelo procede de las estructuras HSD embebidas.
- La jerarquía del modelo se basa en relaciones HSD "JObj/DObj/PObj/MObj/TObj".
- La animación esquelética nativa puede reconstruirse directamente en cualquier motor de render.
- Los canales esqueléticos deben evaluarse contra sus objetivos joint HSD reales.
- Los canales de transform ausentes deben conservar los componentes bind/default.
- Una conversión de rotación inválida/no normalizada puede provocar deformaciones graves en Unity.
- La animación de materiales es independiente de la animación esquelética.
- La animación de materiales debe vincularse mediante las relaciones HSD de origen, no mediante el orden de renderers de Unity.
- Un objeto material/modelo de origen puede generar múltiples renderers de Unity.
- "_MainTex_ST" utiliza escala en X/Y y offset en Z/W.
- El tiling base debe conservarse cuando únicamente se animan offsets.
- Para los casos actualmente validados, la traslación U requiere signo opuesto mientras que V conserva el signo HSD.
- Los valores UV no deben cuantizarse globalmente a un layout de atlas supuesto.
- Los cambios de atlas facial/material requieren comportamiento stepped en lugar de interpolación lineal.
- Las hold keys no deben extender la duración del clip de origen.
- El orden/significado de las acciones de animación difiere entre Pokémon; es más seguro utilizar nombres genéricos "Animation_N".
- La convención de coordenadas actual de Unity requiere una rotación de 90 grados en X en la raíz.
- Zangoose y Venusaur proporcionaron casos de validación complementarios: Zangoose reveló problemas de binding de objetivos de material y del atlas V, mientras que Venusaur reveló problemas del signo del eje U y de conservación del tiling.

15. Provisional / incompleto

Las siguientes áreas todavía requieren más ingeniería inversa antes de poder tratarlas como una especificación completa del formato:

- Semántica completa del header binario FSYS y de los campos de la tabla de entradas;
- Variantes de compresión/alineación entre todos los tipos FSYS de Colosseum/XD;
- significado semántico exacto de todos los flags HSD encontrados;
- Especificación completa de opcodes/interpolación "AObj/FObj";
- Todos los modos de matriz de textura / generación de coordenadas;
- Mapping exacto original del juego entre nombre de acción/índice por especie;
- Semántica de animación de visibilidad en todos los tipos de modelo;
- Rol exacto de todos los objetos geométricos sin textura/con apariencia de helper;
- Si todas las clases de modelos de Colosseum y XD utilizan las mismas suposiciones de conversión U/V;
- Relación completa de recursos de texturas shiny/alternativas;
- Familias de modelos FSYS que no sean Pokémon y objetos de casos especiales.

16. Casos de referencia actuales

Zangoose


Útil para validar:

Código:
Deformación esquelética / conversión de rotación duración de la animación binding del objetivo de material animación vertical de textura facial cambios stepped de estado UV múltiples mallas generadas desde relaciones material/modelo de origen.
Venusaur

Útil para validar:

Código:
Animación UV horizontal conversión de signo TRANS_U conservación de Tiling (1,1) offsets X positivos de Unity producidos a partir de traslaciones U HSD negativas.
En conjunto, estos dos modelos son significativamente más útiles que tratar cualquiera de ellos como una descripción universal del formato.

17. Arquitectura de implementación de referencia

Para un importador limpio y orientado a ingeniería inversa, hay que mantener las capas separadas:

Código:
FSYS reader
    |
    v
HSD binary/object parser
    |
    +-- model graph
    +-- texture decoder
    +-- skeletal animation decoder
    +-- material animation decoder
    |
    v
normalized intermediate representation
    |
    v
Unity exporter
    +-- meshes
    +-- materials
    +-- textures
    +-- AnimationClips
    +-- callbacks
    +-- Animator/Animation
    +-- prefab
Esto evita contaminar el parseo binario con suposiciones específicas de Unity y facilita considerablemente los diagnósticos. Una representación normalizada debería conservar explícitamente los identificadores de origen aunque esos valores nunca se expongan en el build runtime final:

Código:
SourceJointId
SourceDObjId
SourceMObjId
SourcePObjId
SourceTextureId
18. Lectura binaria de FSYS y extracción de entradas

El importador actual lee FSYS directamente desde el stream de bytes original de GameCube. Esta capa es intencionadamente pequeña: valida el contenedor, resuelve la tabla de entradas, extrae cada payload soportado y descomprime las entradas marcadas como LZSS antes de entregar los bytes resultantes a la capa PKX o HSD/DAT. Todos los valores multibyte de esta ruta se leen como Big-Endian, coincidiendo con la representación de GameCube utilizada por estos archivos. El helper binario implementa explícitamente las lecturas primitivas:

Código:
U16(offset):
    (data[offset] << 8) |
     data[offset + 1]

U32(offset):
    (data[offset]     << 24) |
    (data[offset + 1] << 16) |
    (data[offset + 2] << 8)  |
     data[offset + 3]
Cada lectura primitiva y cada extracción de bytes comprueba sus límites. Si el rango solicitado queda fuera del buffer de origen, la lectura se rechaza en lugar de consumir silenciosamente datos inválidos.

18.1 Identificación de FSYS

Un contenedor FSYS se identifica mediante sus primeros cuatro bytes:

Código:
offset 0x00:

46 53 59 53
 F  S  Y  S
Conceptualmente:

Código:
data[0] == 'F'
data[1] == 'S'
data[2] == 'Y'
data[3] == 'S'
El importador también utiliza la extensión ".fsys" como pista para enrutar el archivo, pero "ExtractFSYS" exige que el magic real "FSYS" esté presente.

18.2 Campos del header utilizados actualmente

El importador no necesita comprender el header FSYS completo para extraer los recursos de modelo requeridos por el pipeline actual. Los dos campos del header que se consumen actualmente son:

Código:
FSYS + 0x0C : U32 -> número de entradas
FSYS + 0x40 : U32 -> offset de la lista de punteros a entradas
Estos valores se interpretan como "U32" big-endian. El segundo valor no apunta directamente a un payload. Apunta a una lista de offsets de 32 bits:

Código:
FSYS
 |
 +-- header
 |    |
 |    +-- 0x0C -> entry count
 |    +-- 0x40 -> entry pointer list offset
 |
 +-- entry pointer list
      |
      +-- U32 -> entry 0 structure
      +-- U32 -> entry 1 structure
      +-- U32 -> entry 2 structure
      +-- ...
Por tanto, el importador resuelve la estructura de una entrada mediante:

Código:
entry = U32(entryListOffset + index * 4)
Esta indirección es importante. La tabla es una lista de punteros/offsets hacia metadatos de entradas, no un array empaquetado de descriptores de payload con un stride fijo asumido.

18.3 Campos de una entrada utilizados por el importador

Para cada estructura de entrada resuelta, el reader actual consume:

Código:
entry + 0x02 : U8  -> tipo de recurso
entry + 0x04 : U32 -> dirección del payload
entry + 0x0C : U32 -> flags
entry + 0x14 : U32 -> tamaño almacenado del payload
entry + 0x1C : U32 -> puntero al string del nombre completo
entry + 0x24 : U32 -> puntero al string del nombre corto
Los valores de tipo reconocidos actualmente son:

Código:
0x02 -> .dat
0x04 -> .dat
0x18 -> .cam
0x1E -> .pkx
Los tipos desconocidos se omiten en el importador actual en lugar de asignarles una semántica especulativa. La ruta de extracción puede resumirse así:

Código:
para cada entrada FSYS:
    resolver la dirección de sus metadatos

    leer type
    si el type no está soportado actualmente:
        omitir la entrada

    leer dirección del payload
    leer flags
    leer tamaño del payload

    copiar los bytes del payload desde:
        [payload address, payload address + payload size)

    si está presente el flag de compresión:
        descomprimir el payload LZSS

    enrutar los bytes resultantes según el tipo de recurso
18.4 Nombres de las entradas

El importador intenta recuperar el nombre original de la entrada utilizando dos punteros de sus metadatos. Primero comprueba:

Código:
entry + 0x1C -> full-name C string
Si el puntero es válido y el string ASCII terminado en null resultante no está vacío, se utiliza directamente ese nombre. En caso contrario comprueba:

Código:
entry + 0x24 -> short-name C string
Si existe un nombre corto pero no tiene extensión, se añade la extensión inferida a partir del tipo de recurso. Si ninguno de los dos punteros produce un nombre válido, el importador genera un fallback determinista:

Código:
<archive-name>_entry_<index><extension>
Por ejemplo:

Código:
pkx_001_entry_3.dat
Este comportamiento de naming es política del importador. La observación relevante de ingeniería inversa es que, para los archivos validados por la implementación actual, las estructuras de entrada contienen punteros de string utilizables en los offsets anteriores.

18.5 Extracción de los bytes del payload

Una vez resueltos la dirección y el tamaño del payload, la extracción es deliberadamente sencilla:

Código:
file = Slice(address, size)
"Slice" comprueba primero que el intervalo completo esté dentro del buffer FSYS y después realiza una copia directa de los bytes. Conceptualmente:

Código:
FSYS byte array

0x00000000
    ...
payload address
    |
    +-- byte 0
    +-- byte 1
    +-- ...
    +-- byte size-1
    |
payload address + size
En esta fase no se realiza ninguna interpretación HSD. El resultado sigue siendo el stream exacto de bytes del recurso embebido, salvo cuando la entrada FSYS declara compresión. Esta separación es útil durante el debugging:

Código:
FSYS reader
    -> extrae/descomprime los bytes del recurso

PKX reader
    -> localiza el DAT embebido cuando corresponde

HSD/DAT reader
    -> interpreta las estructuras del modelo
Por tanto, un modelo malformado no debe atribuirse inmediatamente al parser HSD. Primero puede volcarse y compararse de forma independiente el payload extraído.

18.6 Flag de compresión

La implementación actual trata el bit alto de los flags de la entrada como indicador de compresión:

Código:
(flags & 0x80000000) != 0
Cuando este bit está activo, el payload extraído se pasa al decoder LZSS de FSYS. El decoder exige que el propio payload comprimido comience con:

Código:
4C 5A 53 53
 L  Z  S  S
o:

Código:
"LZSS"
Si la entrada FSYS está marcada como comprimida pero el payload extraído no contiene este magic, el importador la rechaza en lugar de intentar una descompresión heurística.

18.7 Header LZSS y rango comprimido

El reader LZSS actual comienza a decodificar en el byte "0x10". Lee:

Código:
LZSS + 0x08 : U32 -> tamaño del stream comprimido utilizado por el decoder
LZSS + 0x10 :      -> comienzo de los comandos/datos codificados
Si el tamaño leído en "0x08" es inválido, menor que el header o queda fuera del payload disponible, el importador utiliza de forma conservadora el tamaño completo de la entrada extraída. El decoder implementado utiliza un ring buffer deslizante de 4096 bytes:

Código:
ring size       = 4096
ring mask       = 0x0FFF
initial write r = 4078
length mask     = 0x0F
Inicialmente el ring está relleno con ceros.

18.8 Decodificación de los flag bytes de LZSS

Los comandos se controlan mediante flag bytes. Se carga un nuevo flag byte cuando se ha consumido el grupo actual:

Código:
flags = input[p++] | 0xFF00
Después los comandos se procesan desde el bit menos significativo hacia arriba. Para cada comando:

Código:
flag bit = 1:
    byte literal

flag bit = 0:
    back-reference
Después de procesar un comando:

Código:
flags >>= 1
Un literal consume un byte del stream comprimido:

Código:
input:
    [literal]

output:
    literal
El byte se escribe tanto en la salida como en la posición actual del ring buffer de 4096 bytes.

18.9 Back-references de LZSS

Una back-reference consume dos bytes:

Código:
i = input[p++]
j = input[p++]

i |= (j >> 4) << 8
j  = (j & 0x0F) + 2
Esto produce:

Código:
posición de origen:
    12 bits

componente de longitud codificado:
    4 bits bajos del segundo byte
La implementación copia después mediante:

Código:
for (k = 0; k <= j; k++)
por lo que el número efectivo de bytes copiados es:

Código:
(lowNibble + 2) + 1
= lowNibble + 3
es decir, un rango de:

Código:
3..18 bytes
Cada byte copiado se lee aplicando la máscara del ring:

Código:
ring[(source + k) & 0x0FFF]
y se vuelve a escribir inmediatamente en el ring en la posición actual de salida. Esto permite matches solapados, como corresponde a este estilo de stream LZSS.

18.10 Enrutado de los recursos extraídos

Después de la descompresión opcional, la capa de contenedor actual enruta los recursos según su extensión inferida. Para entradas ".pkx":

Código:
FSYS
 -> PKX payload
    -> ExtractPKX
       -> embedded DAT bytes
          -> model parser
Para recursos ".dat", ".fdat", ".rdat" y ".cam" aceptados por la capa de contenedor, los bytes extraídos se devuelven directamente como entradas del contenedor. El mapping de tipos FSYS actual únicamente produce ".dat", ".cam" y ".pkx" a partir de IDs de tipo FSYS; ".fdat" y ".rdat" siguen estando aceptados por la ruta común posterior para inputs directos/soportados.

18.11 Extracción PKX -> DAT

Como los recursos de Pokémon pasan frecuentemente por PKX, esta es la siguiente capa relevante a nivel de bytes después de la extracción FSYS. El importador actual distingue los dos layouts utilizados por sus archivos validados de Colosseum/XD comparando:

Código:
U32(0x00)
U32(0x40)
La implementación actual considera que valores diferentes indican el layout de tipo XD.

Para la ruta más sencilla no-XD:

Código:
DAT offset = 0x40
Para la ruta de tipo XD lee:

Código:
0x08 -> GPT1 length
0x10 -> animation count
y calcula:

Código:
DAT offset =
    Align32(0x84 + animationCount * 0xD0)
    + Align32(gpt1Length)
donde:

Código:
Align32(value) = (value + 0x1F) & ~0x1F
El primer "U32" del PKX se trata inicialmente como tamaño del DAT. Si ese tamaño declarado no cabe dentro del PKX, el importador utiliza como fallback el primer "U32" del propio DAT embebido como tamaño. Este fallback es intencionadamente conservador y existe porque algunos archivos validados declaran un tamaño externo padded o que no resulta utilizable directamente.

18.12 Mapa FSYS actual a nivel de bytes

El subconjunto del formato requerido por el importador actual puede documentarse así:

Código:
FSYS HEADER
-----------

0x00  4 bytes   "FSYS"
0x0C  U32 BE    entry count
0x40  U32 BE    entry pointer list offset


ENTRY POINTER LIST
------------------

+0x00  U32 BE   entry metadata offset
+0x04  U32 BE   next entry metadata offset
+0x08  U32 BE   next entry metadata offset
...


ENTRY METADATA — fields currently used
--------------------------------------

+0x02  U8       resource type
+0x04  U32 BE   payload address
+0x0C  U32 BE   flags
+0x14  U32 BE   stored payload size
+0x1C  U32 BE   full-name string pointer
+0x24  U32 BE   short-name string pointer


KNOWN TYPE VALUES IN CURRENT IMPORTER
-------------------------------------

0x02           .dat
0x04           .dat
0x18           .cam
0x1E           .pkx


KNOWN FLAG BEHAVIOR IN CURRENT IMPORTER
---------------------------------------

0x80000000     payload is LZSS-compressed
Este mapa debe entenderse como el subconjunto confirmado y consumido por el importador nativo actual, no como una especificación completa de FSYS. Otros campos del header, campos de entrada, flags y valores de tipo de recurso quedan fuera de lo que necesita actualmente esta implementación.

18.13 Reglas de parseo defensivo

Conviene conservar varias reglas defensivas del reader:

1. Cada lectura numérica comprueba sus límites.
2. Cada slice de payload debe caber completamente dentro del buffer de origen.
3. Los punteros a strings deben estar dentro del FSYS antes de desreferenciarse.
4. Los strings se leen como C strings ASCII y terminan en el primer byte null.
5. Los tipos de recurso no soportados se omiten en lugar de adivinarse.
6. Una entrada comprimida debe contener un magic "LZSS" válido.
7. Los tamaños DAT externos inválidos de PKX se contrastan con el rango real de bytes disponible.
8. Los cálculos de alineamiento son explícitos y no se infieren a partir de la posición actual del stream.

19. Resultado

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


Créditos / contexto de la investigación

El addon de Blender Blender-Addon-Gamecube-Models se utilizó como un importante punto de referencia de comportamiento durante la investigación, y un pipeline anterior Blender -> FBX -> Unity proporcionó una salida visual conocida como correcta para realizar comparaciones. Sin embargo, el importador final descrito aquí reconstruye los datos relevantes de modelo y animación de forma nativa en lugar de depender de Blender ni de ningún otro motor de render.
 
Última edición:

Manurocker95

Doctorando en Ingeniería Biomédica & Game Dev
Miembro insignia
En este write up se ha intentado dar una de cal y una de arena. Una vista más arriba de cómo es el parseador y otra más abajo de cómo tratar los archivos fsys.
 

Manurocker95

Doctorando en Ingeniería Biomédica & Game Dev
Miembro insignia
Y btw, si alguien quiere sacar los FSYS de una ISO de uno de estos dos juegos:

En dolphin (el emulador)-> Click derecho > Propiedades > Export all data
 
Arriba