
Reverse engineering del motore che alimenta i giochi PriPara su arcade.
PriPara (nome interno Prineo) è un gioco ritmico arcade sviluppato da SynSophia.
L'obiettivo di questo progetto è stato quello di fare reverse engineering del formato dei modelli della serie arcade per poter portare i personaggi esclusivi sulla versione più recente del gioco per Nintendo Switch.
Alcuni screenshot interessanti scattati durante il processo di reverse engineering.
Versione iniziale del plugin Noesis — primo import parziale del modello (14-09-2025)
Lettore di modelli in C# utilizzato per testare le implementazioni di deserializzazione (06-12-2025)
Esecuzione di un dump decrittato in una VM e hooking della funzione di hashing tramite DLL injection usando MinHook e Xenos, sfruttando l'intrinseco ReturnAddress per tracciare le chiamate alla funzione di hash (08-12-2025)
Scena importata dal gioco tramite il plugin Noesis (27-12-2025)
Modello skinnato importato da una risorsa crittata tramite il plugin Noesis

Vestito portato da PriPara arcade alla versione Nintendo Switch, modello esportato utilizzando il plugin Noesis
Il gioco implementa versioni modificate di primitive crittografiche standard.
Implementazione: https://github.com/ExIfDev/Prineo_RE/blob/main/Noesis/fmt_prineo.py#L1130-L1140
Implementazione: https://github.com/ExIfDev/Prineo_RE/blob/main/Noesis/fmt_prineo.py#L1074-L1128
La documentazione seguente si riferisce ai file dopo la decompressione SSZL.
const byte[] ident = {0x00, 0x13, 0x10, 0x09};
struct Header
{
uint32 VERSION;//1
uint32 FILE_SIZE;
byte[4] Ident;
int32 unk1;
int32 unk2;
int32 unk3;
uint32 DATA_OFFSET; //offset assoluto
uint32 DATA_SIZE;
};
//il file MDJ può contenere vari buffer relativi a geometria e scene; non
//ha un header, si assume che inizi con uno dei buffer seguenti
//e termina sempre con il marcatore "end/0/"
struct Pose //posa applicata a uno scheletro (=restPose in un contenitore di modello)
//di solito questa posa è uguale a quella nel buffer "base".
//se sono presenti AnimParams, ciò che segue sono le curve di animazione.
//AnimParams può essere nullo quando non ci sono keyframe.
{
char[4] ident = "pose";
uint16 strLen;
char[strLen] POSE_NAME;
byte[2] unk;
uint16 BONE_COUNT;
byte[8] unkdata;
struct Bone[BONE_COUNT]
{
uint16 serializer_id;//518
uint16 strLen;
string[strLen] BONE_NAME;
//Trasformazione base
float sclX, sclY, sclZ;
float rotX, rotY, rotZ;
float tslX, tslY, tslZ;
//per tutti i canali (9) "Sx", "Sy", "Sz", "Rx", "Ry", "Rz", "Tx", "Ty", "Tz"
struct AnimationParams //6 byte
{
uint16 unk2; //possibilmente un uint32 dato che unk3 è sempre 0
uint16 unk3;
uint16 CHANNEL_MASK;
};
//se i parametri di animazione non sono nulli allora....
uint32 KEYFRAME_COUNT;
struct Keyframe[KEYFRAME_COUNT]
{
float VALUE;//radianti
float TIME;//centisecondi
};
};
};
struct TextureDefs //definisce i collegamenti nome e id delle texture
{
string[4] ident ="imag";
uint16 TEX_COUNT;
struct TexDef[TEX_COUNT]
{
uint16 serializer_id;//1029
uint16 unk;
uint16 TEX_ID;
uint16 strLen;
string[strLen] texName;
};
};
struct MaterialChunk //definisce i parametri dei materiali e degli shader
{
string[4] ident ="mate";
uint16 MAT_COUNT;
struct Material[MAT_COUNT]
{
uint16 serializer_id;//1024
uint16 strLen;
string[strLen] MatName;
uint32 TYPE;//..forse hash? cercato in un registro
//Slot texture
//ambientale
uint16 aCount;
for (int i = 0; i < aCount; i++)
{
uint16 serializer_id;
if (serializer_id == 1029)
{
uint32 unk;
}
else
{
uint32 unk2;
}
}
//diffuso
uint16 dCount;
for (int i = 0; i < dCount; i++)
{
uint16 serializer_id;
if (serializer_id == 1029)
{
uint32 unk;
}
else
{
uint32 unk2;
}
}
//speculare
uint16 sCount;
for (int i = 0; i < sCount; i++)
{
uint16 serializer_id;
if (serializer_id == 1029)
{
uint32 unk;
}
else
{
uint32 unk2;
}
}
//possibili slot di parametri
uint16 unk1Count;
for (int i = 0; i < unk1Count; i++)
{
uint16 serializer_id;
float unk12;
}
uint16 unk2Count;
for (int i = 0; i < unk2Count; i++)
{
uint16 serializer_id;
float unk22;
}
byte HAS_TEXTURE;
if (HAS_TEXTURE == 1)
{
uint16 TEX_COUNT;
struct TexureMap[TEX_COUNT]
{
uint16 serializer_id;
uint32 TEX_ID;
}
}
uint16 strLen;
string[strLen] unkNullString;
};
//due possibili chunk potrebbero seguire:
struct MPSS_Subchunk //uso chiaro sconosciuto
{
string header = "MPSS";
uint16 count;
struct MPSS_Entry[count]
{
uint16 unk32;
byte[16] payload;
}
}
struct MPDS_Subchunk //chunk parametri shader stringa-valore
{
string header = "MPDS";
uint16 count;
struct MPDS_Entry[count]
{
uint16 strLen;
string[strLen] Name;
byte[32] value;
}
}
};
struct LightChunk //definisce i parametri delle luci in una scena
{
char[4] ident = "ligh";
int16 LIGHT_COUNT;
struct light[LIGHT_COUNT]
{
uint16 serializer_id;
uint16 strLen;
char[strLen] light_name;
byte[49] unk40; //matrice di trasformazione insieme a valori di colore
}
}
struct ModeBuffer//marca l'inizio di una lista di submesh e dei suoi buffer
{
char[4] ident = "mode";
uint16 serializer_id;//513
uint16 strLen;
char[strLen] unkName; //possibilmente il nodo radice
byte[4] unk;
uint16 unkC;
};
struct MeshMeta//una submesh inizia con alcuni metadati sulla sua dimensione di bounding, nome e la sua boneMap
{
uint16 serializer_id;//770 0x0302
uint16 strLen;
char[strLen] MESH_NAME;
byte[4] unk;
uint16 boneMapCount;
struct HashedBoneMap[boneMapCount]
{
uint32 boneHash;//nome osseo hashato con SSFNV1a
}
float unk3;
byte unk2;
byte[24] AABB;
}
struct MeshBegin //indica l'inizio di una submesh
{
string[4] ident ="verb";
};
struct FaceBuffer
{
string[4] ident = "surf";
int16 FACE_COUNT;
struct Face [FACE_COUNT]//stride: 6 //tipo: lista triangoli // avvolgimento: orario
{
uint16 A;
uint16 B;
uint16 C;
};
};
struct VertexBuffer
{
string[4] ident = "coor";
uint16 VERTEX_COUNT;
struct Vertex [VERTEX_COUNT] //stride 12
{
float x;
float y;
float z;
};
};
struct NormalBuffer
{
string[4] ident ="norm";
uint16 NORM_COUNT; //dovrebbe essere uguale a VERTEX_COUNT
struct Normal[NORM_COUNT]//stride 12
{
float nx;
float ny;
float nz;
};
};
struct BinormalBuffer//buffer binormale (non presente in nessun file)
{
string[4] ident ="bino";
uint16 BINO_COUNT; //dovrebbe essere uguale a VERTEX_COUNT
struct BiNormal[NORM_COUNT]//stride 12
{
float bx;
float by;
float bz;
};
};
struct UV0Buffer
{
string[4] ident ="tex0";
uint16 UV0_COUNT; //dovrebbe essere uguale a VERTEX_COUNT
struct UV[UV0_COUNT] //stride 8
{
float u;
float v;
};
};
struct UV1Buffer
{
string[4] ident ="tex1";
uint16 UV1_COUNT; //dovrebbe essere uguale a VERTEX_COUNT
struct UV[UV1_COUNT] //stride 8
{
float u;
float v;
};
};
struct VertexColors
{
string[4] ident = "colo";
uint16 VERTEX_COUNT; //dovrebbe essere uguale a VERTEX_COUNT
struct UV[UV1_COUNT] //stride 4
{
byte r,g,b,a;
};
}
struct TangentBuffer //vettore tangente senza segno
{
char[4] ident ="tan ";
uint16 TANG_COUNT; //dovrebbe essere uguale a VERTEX_COUNT
struct Tang[TANG_COUNT]//stride 12
{
float tx;
float ty;
float tz;
};
};
struct WeightBuffer //Valori di peso per ogni osso assegnato a un vertice
{
char[4] ident ="weig";
uint16 WEIGHT_COUNT;
struct VertexWeight[WEIGHT_COUNT] //stride 16
{
float w1;
float w2;
float w3;
float w4;
};
};
struct VBIBuffer //Indici ossei del vertice, in ordine, indici ossei che influenzano un vertice
//questa tabella è locale al buffer del vertice corrente
{
char[4] ident ="bone";
uint16 VERTEX_COUNT;
struct VBI[VERTEX_COUNT]
{
byte boneIDX1;
byte boneIDX2;
byte boneIDX3;
byte boneIDX4;
};
};
//
struct Skeleton //Albero dei nodi ossei, definisce le relazioni tra le ossa
{
char[4] ident ="skel";
struct BoneName[BONE_COUNT] //Numero di ossa letto dal chunk POSE
{
uint16 serializer_id; //257=radice 258=figlio
uint16 strLen;
char[strLen] BONE_NAME;
uint32 unk2;
uint16 CHILD_COUNT;
};
};
struct RestPose //definisce la posa a riposo dello scheletro
{
char[4] ident ="base";
uint16 strLen;
char[strLen] POSE_NAME;
int16 BONE_COUNT;
struct BoneTrs[BONE_COUNT]
{
uint16 serializer_id;//1537
uint16 strLen;
char[strLen] BONE_NAME;
float sx, sy, sz; //scala
float rx, ry, rz; //rotazione
float tx, ty, tz; //traslazione
};
};
struct MeshEnd //indica la fine di una mesh
{
string[4] ident ="vere";
uint16 strLen;
char[strLen] NAME;
byte[2] padding;
};
struct EndFlag //indica la fine del file
{
string[4] ident ="end "
}
//le informazioni sono relative al file decompresso
struct Header
{
uint32 VERSION;
uint32 FILE_SIZE;
uint32 tex_ident;
uint32 unk;
uint32 TEXTURE_COUNT;
};
struct TextureDecl[TEXTURE_COUNT]
{
uint32 HASH;//nome texture hashato senza estensione
uint32 OFFSET;//offset assoluto
uint32 SIZE;
};
//seguono i buffer delle texture a OFFSET con SIZE
//TGA o DDS