nitrogfx/gfx.c

1847 lines
67 KiB
C

// Copyright (c) 2015 YamaArashi, 2021-2024 red031000
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include "global.h"
#include "gfx.h"
#include "util.h"
static unsigned int FindNitroDataBlock(const unsigned char *data, const char *ident, unsigned int fileSize, unsigned int *blockSize_out)
{
unsigned int offset = 0x10;
while (offset < fileSize)
{
unsigned int blockSize = data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) | (data[offset + 7] << 24);
if (offset + blockSize > fileSize)
{
FATAL_ERROR("corrupted NTR file");
}
if (memcmp(data + offset, ident, 4) == 0)
{
*blockSize_out = blockSize;
return offset;
}
offset += blockSize;
}
return -1u;
}
#define GET_GBA_PAL_RED(x) (((x) >> 0) & 0x1F)
#define GET_GBA_PAL_GREEN(x) (((x) >> 5) & 0x1F)
#define GET_GBA_PAL_BLUE(x) (((x) >> 10) & 0x1F)
#define SET_GBA_PAL(r, g, b) (((b) << 10) | ((g) << 5) | (r))
#define UPCONVERT_BIT_DEPTH(x) (((x) * 255) / 31)
#define DOWNCONVERT_BIT_DEPTH(x) ((x) / 8)
static void AdvanceTilePosition(int *tilesSoFar, int *rowsSoFar, int *chunkStartX, int *chunkStartY, int chunksWide, int colsPerChunk, int rowsPerChunk)
{
(*tilesSoFar)++;
if (*tilesSoFar == colsPerChunk) {
*tilesSoFar = 0;
(*rowsSoFar)++;
if (*rowsSoFar == rowsPerChunk) {
*rowsSoFar = 0;
(*chunkStartX)++;
if (*chunkStartX == chunksWide) {
*chunkStartX = 0;
(*chunkStartY)++;
}
}
}
}
static void ConvertFromTiles1Bpp(unsigned char *src, unsigned char *dest, int numTiles, int chunksWide, int colsPerChunk, int rowsPerChunk, bool invertColors)
{
int tilesSoFar = 0;
int rowsSoFar = 0;
int chunkStartX = 0;
int chunkStartY = 0;
int pitch = chunksWide * colsPerChunk;
for (int i = 0; i < numTiles; i++) {
for (int j = 0; j < 8; j++) {
int idxComponentY = (chunkStartY * rowsPerChunk + rowsSoFar) * 8 + j;
int idxComponentX = chunkStartX * colsPerChunk + tilesSoFar;
unsigned char srcPixelOctet = *src++;
unsigned char *destPixelOctet = &dest[idxComponentY * pitch + idxComponentX];
for (int k = 0; k < 8; k++) {
*destPixelOctet <<= 1;
*destPixelOctet |= (srcPixelOctet & 1) ^ invertColors;
srcPixelOctet >>= 1;
}
}
AdvanceTilePosition(&tilesSoFar, &rowsSoFar, &chunkStartX, &chunkStartY, chunksWide, colsPerChunk, rowsPerChunk);
}
}
static void ConvertFromTiles4Bpp(unsigned char *src, unsigned char *dest, int numTiles, int chunksWide, int colsPerChunk, int rowsPerChunk, bool invertColors)
{
int tilesSoFar = 0;
int rowsSoFar = 0;
int chunkStartX = 0;
int chunkStartY = 0;
int pitch = (chunksWide * colsPerChunk) * 4;
for (int i = 0; i < numTiles; i++) {
for (int j = 0; j < 8; j++) {
int idxComponentY = (chunkStartY * rowsPerChunk + rowsSoFar) * 8 + j;
for (int k = 0; k < 4; k++) {
int idxComponentX = (chunkStartX * colsPerChunk + tilesSoFar) * 4 + k;
unsigned char srcPixelPair = *src++;
unsigned char leftPixel = srcPixelPair & 0xF;
unsigned char rightPixel = srcPixelPair >> 4;
if (invertColors) {
leftPixel = 15 - leftPixel;
rightPixel = 15 - rightPixel;
}
dest[idxComponentY * pitch + idxComponentX] = (leftPixel << 4) | rightPixel;
}
}
AdvanceTilePosition(&tilesSoFar, &rowsSoFar, &chunkStartX, &chunkStartY, chunksWide, colsPerChunk, rowsPerChunk);
}
}
static uint32_t ConvertFromScanned4Bpp(unsigned char *src, unsigned char *dest, int fileSize, bool invertColours, bool scanFrontToBack)
{
uint32_t encValue = 0;
if (scanFrontToBack) {
encValue = (src[1] << 8) | src[0];
for (int i = 0; i < fileSize; i += 2)
{
uint16_t val = src[i] | (src[i + 1] << 8);
val ^= (encValue & 0xFFFF);
src[i] = val;
src[i + 1] = val >> 8;
encValue = encValue * 1103515245;
encValue = encValue + 24691;
}
} else {
encValue = (src[fileSize - 1] << 8) | src[fileSize - 2];
for (int i = fileSize; i > 0; i -= 2)
{
uint16_t val = (src[i - 1] << 8) | src[i - 2];
val ^= (encValue & 0xFFFF);
src[i - 1] = (val >> 8);
src[i - 2] = val;
encValue = encValue * 1103515245;
encValue = encValue + 24691;
}
}
for (int i = 0; i < fileSize; i++)
{
unsigned char srcPixelPair = src[i];
unsigned char leftPixel = srcPixelPair & 0xF;
unsigned char rightPixel = srcPixelPair >> 4;
if (invertColours) {
leftPixel = 15 - leftPixel;
rightPixel = 15 - rightPixel;
}
dest[i] = (leftPixel << 4) | rightPixel;
}
return encValue;
}
static void ConvertFromTiles8Bpp(unsigned char *src, unsigned char *dest, int numTiles, int chunksWide, int colsPerChunk, int rowsPerChunk, bool invertColors)
{
int tilesSoFar = 0;
int rowsSoFar = 0;
int chunkStartX = 0;
int chunkStartY = 0;
int pitch = (chunksWide * colsPerChunk) * 8;
for (int i = 0; i < numTiles; i++) {
for (int j = 0; j < 8; j++) {
int idxComponentY = (chunkStartY * rowsPerChunk + rowsSoFar) * 8 + j;
for (int k = 0; k < 8; k++) {
int idxComponentX = (chunkStartX * colsPerChunk + tilesSoFar) * 8 + k;
unsigned char srcPixel = *src++;
if (invertColors)
srcPixel = 255 - srcPixel;
dest[idxComponentY * pitch + idxComponentX] = srcPixel;
}
}
AdvanceTilePosition(&tilesSoFar, &rowsSoFar, &chunkStartX, &chunkStartY, chunksWide, colsPerChunk, rowsPerChunk);
}
}
static uint32_t ConvertFromScanned8Bpp(unsigned char *src, unsigned char *dest, int fileSize, bool invertColours, bool scanFrontToBack)
{
uint32_t encValue = 0;
if (scanFrontToBack) {
encValue = (src[1] << 8) | src[0];
for (int i = 0; i < fileSize; i += 2)
{
uint16_t val = src[i] | (src[i + 1] << 8);
val ^= (encValue & 0xFFFF);
src[i] = val;
src[i + 1] = val >> 8;
encValue = encValue * 1103515245;
encValue = encValue + 24691;
}
} else {
encValue = (src[fileSize - 1] << 8) | src[fileSize - 2];
for (int i = fileSize; i > 0; i -= 2)
{
uint16_t val = (src[i - 1] << 8) | src[i - 2];
val ^= (encValue & 0xFFFF);
src[i - 1] = (val >> 8);
src[i - 2] = val;
encValue = encValue * 1103515245;
encValue = encValue + 24691;
}
}
for (int i = 0; i < fileSize; i++)
{
unsigned char srcPixel = src[i];
if (invertColours) {
srcPixel = 255 - srcPixel;
}
dest[i] = srcPixel;
}
return encValue;
}
static void ConvertToTiles1Bpp(unsigned char *src, unsigned char *dest, int numTiles, int chunksWide, int colsPerChunk, int rowsPerChunk, bool invertColors)
{
int tilesSoFar = 0;
int rowsSoFar = 0;
int chunkStartX = 0;
int chunkStartY = 0;
int pitch = chunksWide * colsPerChunk;
for (int i = 0; i < numTiles; i++) {
for (int j = 0; j < 8; j++) {
int idxComponentY = (chunkStartY * rowsPerChunk + rowsSoFar) * 8 + j;
int idxComponentX = chunkStartX * colsPerChunk + tilesSoFar;
unsigned char srcPixelOctet = src[idxComponentY * pitch + idxComponentX];
unsigned char *destPixelOctet = dest++;
for (int k = 0; k < 8; k++) {
*destPixelOctet <<= 1;
*destPixelOctet |= (srcPixelOctet & 1) ^ invertColors;
srcPixelOctet >>= 1;
}
}
AdvanceTilePosition(&tilesSoFar, &rowsSoFar, &chunkStartX, &chunkStartY, chunksWide, colsPerChunk, rowsPerChunk);
}
}
static void ConvertToTiles4Bpp(unsigned char *src, unsigned char *dest, int numTiles, int chunksWide, int colsPerChunk, int rowsPerChunk, bool invertColors)
{
int tilesSoFar = 0;
int rowsSoFar = 0;
int chunkStartX = 0;
int chunkStartY = 0;
int pitch = (chunksWide * colsPerChunk) * 4;
for (int i = 0; i < numTiles; i++) {
for (int j = 0; j < 8; j++) {
int idxComponentY = (chunkStartY * rowsPerChunk + rowsSoFar) * 8 + j;
for (int k = 0; k < 4; k++) {
int idxComponentX = (chunkStartX * colsPerChunk + tilesSoFar) * 4 + k;
unsigned char srcPixelPair = src[idxComponentY * pitch + idxComponentX];
unsigned char leftPixel = srcPixelPair >> 4;
unsigned char rightPixel = srcPixelPair & 0xF;
if (invertColors) {
leftPixel = 15 - leftPixel;
rightPixel = 15 - rightPixel;
}
*dest++ = (rightPixel << 4) | leftPixel;
}
}
AdvanceTilePosition(&tilesSoFar, &rowsSoFar, &chunkStartX, &chunkStartY, chunksWide, colsPerChunk, rowsPerChunk);
}
}
static void ConvertToScanned4Bpp(unsigned char *src, unsigned char *dest, int fileSize, bool invertColours, uint32_t encValue, uint32_t scanMode)
{
for (int i = 0; i < fileSize; i++)
{
unsigned char srcPixelPair = src[i];
unsigned char leftPixel = srcPixelPair & 0xF;
unsigned char rightPixel = srcPixelPair >> 4;
if (invertColours) {
leftPixel = 15 - leftPixel;
rightPixel = 15 - rightPixel;
}
dest[i] = (leftPixel << 4) | rightPixel;
}
if (scanMode == 2) { // front to back
for (int i = fileSize - 1; i > 0; i -= 2)
{
uint16_t val = dest[i - 1] | (dest[i] << 8);
encValue = (encValue - 24691) * 4005161829;
val ^= (encValue & 0xFFFF);
dest[i] = (val >> 8);
dest[i - 1] = val;
}
}
else if (scanMode == 1) {
for (int i = 1; i < fileSize; i += 2)
{
uint16_t val = (dest[i] << 8) | dest[i - 1];
encValue = (encValue - 24691) * 4005161829;
val ^= (encValue & 0xFFFF);
dest[i] = (val >> 8);
dest[i - 1] = val;
}
}
}
static void ConvertToTiles8Bpp(unsigned char *src, unsigned char *dest, int numTiles, int chunksWide, int colsPerChunk, int rowsPerChunk, bool invertColors)
{
int tilesSoFar = 0;
int rowsSoFar = 0;
int chunkStartX = 0;
int chunkStartY = 0;
int pitch = (chunksWide * colsPerChunk) * 8;
for (int i = 0; i < numTiles; i++) {
for (int j = 0; j < 8; j++) {
int idxComponentY = (chunkStartY * rowsPerChunk + rowsSoFar) * 8 + j;
for (int k = 0; k < 8; k++) {
int idxComponentX = (chunkStartX * colsPerChunk + tilesSoFar) * 8 + k;
unsigned char srcPixel = src[idxComponentY * pitch + idxComponentX];
if (invertColors)
srcPixel = 255 - srcPixel;
*dest++ = srcPixel;
}
}
AdvanceTilePosition(&tilesSoFar, &rowsSoFar, &chunkStartX, &chunkStartY, chunksWide, colsPerChunk, rowsPerChunk);
}
}
void ReadImage(char *path, int tilesWide, int bitDepth, int colsPerChunk, int rowsPerChunk, struct Image *image, bool invertColors)
{
int tileSize = bitDepth * 8; // number of bytes per tile
int fileSize;
unsigned char *buffer = ReadWholeFile(path, &fileSize);
int numTiles = fileSize / tileSize;
int tilesTall = (numTiles + tilesWide - 1) / tilesWide;
if (tilesWide % colsPerChunk != 0)
FATAL_ERROR("The width in tiles (%d) isn't a multiple of the specified tiles per row (%d)", tilesWide, colsPerChunk);
if (tilesTall % rowsPerChunk != 0)
FATAL_ERROR("The height in tiles (%d) isn't a multiple of the specified rows per chunk (%d)", tilesTall, rowsPerChunk);
image->width = tilesWide * 8;
image->height = tilesTall * 8;
image->bitDepth = bitDepth;
image->pixels = calloc(tilesWide * tilesTall, tileSize);
if (image->pixels == NULL)
FATAL_ERROR("Failed to allocate memory for pixels.\n");
int chunksWide = tilesWide / colsPerChunk; // how many chunks side-by-side are needed for the full width of the image
switch (bitDepth) {
case 1:
ConvertFromTiles1Bpp(buffer, image->pixels, numTiles, chunksWide, colsPerChunk, rowsPerChunk, invertColors);
break;
case 4:
ConvertFromTiles4Bpp(buffer, image->pixels, numTiles, chunksWide, colsPerChunk, rowsPerChunk, invertColors);
break;
case 8:
ConvertFromTiles8Bpp(buffer, image->pixels, numTiles, chunksWide, colsPerChunk, rowsPerChunk, invertColors);
break;
}
free(buffer);
}
uint32_t ReadNtrImage(char *path, int tilesWide, int bitDepth, int colsPerChunk, int rowsPerChunk, struct Image *image, bool invertColors, bool scanFrontToBack)
{
int fileSize;
unsigned char *buffer = ReadWholeFile(path, &fileSize);
if (memcmp(buffer, "RGCN", 4) != 0)
{
FATAL_ERROR("Not a valid NCGR character file.\n");
}
unsigned char *charHeader = buffer + 0x10;
if (memcmp(charHeader, "RAHC", 4) != 0)
{
FATAL_ERROR("No valid CHAR file after NCLR header.\n");
}
bitDepth = bitDepth ? bitDepth : (charHeader[0xC] == 3 ? 4 : 8);
if (bitDepth == 4)
{
image->palette.numColors = 16;
}
unsigned char *imageData = charHeader + 0x20;
bool scanned = charHeader[0x14];
int tileSize = bitDepth * 8; // number of bytes per tile
if (tilesWide == 0) {
tilesWide = ReadS16(charHeader, 0xA);
if (tilesWide < 0) {
tilesWide = 1;
}
}
int numTiles = ReadS32(charHeader, 0x18) / (64 / (8 / bitDepth));
int tilesTall = ReadS16(charHeader, 0x8);
if (tilesTall < 0)
tilesTall = (numTiles + tilesWide - 1) / tilesWide;
if (tilesWide % colsPerChunk != 0)
FATAL_ERROR("The width in tiles (%d) isn't a multiple of the specified tiles per row (%d)", tilesWide, colsPerChunk);
if (tilesTall % rowsPerChunk != 0)
FATAL_ERROR("The height in tiles (%d) isn't a multiple of the specified rows per chunk (%d)", tilesTall, rowsPerChunk);
image->width = tilesWide * 8;
image->height = tilesTall * 8;
image->bitDepth = bitDepth;
image->pixels = calloc(tilesWide * tilesTall, tileSize);
if (image->pixels == NULL)
FATAL_ERROR("Failed to allocate memory for pixels.\n");
int chunksWide = tilesWide / colsPerChunk; // how many chunks side-by-side are needed for the full width of the image
uint32_t key = 0;
if (scanned)
{
switch (bitDepth)
{
case 4:
key = ConvertFromScanned4Bpp(imageData, image->pixels, fileSize - 0x30, invertColors, scanFrontToBack);
break;
case 8:
key = ConvertFromScanned8Bpp(imageData, image->pixels, fileSize - 0x30, invertColors, scanFrontToBack);
break;
}
}
else
{
switch (bitDepth)
{
case 4:
ConvertFromTiles4Bpp(imageData, image->pixels, numTiles, chunksWide, colsPerChunk, rowsPerChunk,
invertColors);
break;
case 8:
ConvertFromTiles8Bpp(imageData, image->pixels, numTiles, chunksWide, colsPerChunk, rowsPerChunk,
invertColors);
break;
}
}
free(buffer);
return key;
}
void WriteImage(char *path, int numTiles, int bitDepth, int colsPerChunk, int rowsPerChunk, struct Image *image, bool invertColors)
{
int tileSize = bitDepth * 8; // number of bytes per tile
if (image->width % 8 != 0)
FATAL_ERROR("The width in pixels (%d) isn't a multiple of 8.\n", image->width);
if (image->height % 8 != 0)
FATAL_ERROR("The height in pixels (%d) isn't a multiple of 8.\n", image->height);
int tilesWide = image->width / 8; // how many tiles wide the image is
int tilesTall = image->height / 8; // how many tiles tall the image is
if (tilesWide % colsPerChunk != 0)
FATAL_ERROR("The width in tiles (%d) isn't a multiple of the specified tiles per row (%d)", tilesWide, colsPerChunk);
if (tilesTall % rowsPerChunk != 0)
FATAL_ERROR("The height in tiles (%d) isn't a multiple of the specified rows per chunk (%d)", tilesTall, rowsPerChunk);
int maxNumTiles = tilesWide * tilesTall;
if (numTiles == 0)
numTiles = maxNumTiles;
else if (numTiles > maxNumTiles)
FATAL_ERROR("The specified number of tiles (%d) is greater than the maximum possible value (%d).\n", numTiles, maxNumTiles);
int bufferSize = numTiles * tileSize;
unsigned char *buffer = malloc(bufferSize);
if (buffer == NULL)
FATAL_ERROR("Failed to allocate memory for pixels.\n");
int chunksWide = tilesWide / colsPerChunk; // how many chunks side-by-side are needed for the full width of the image
switch (bitDepth) {
case 1:
ConvertToTiles1Bpp(image->pixels, buffer, numTiles, chunksWide, colsPerChunk, rowsPerChunk, invertColors);
break;
case 4:
ConvertToTiles4Bpp(image->pixels, buffer, numTiles, chunksWide, colsPerChunk, rowsPerChunk, invertColors);
break;
case 8:
ConvertToTiles8Bpp(image->pixels, buffer, numTiles, chunksWide, colsPerChunk, rowsPerChunk, invertColors);
break;
}
WriteWholeFile(path, buffer, bufferSize);
free(buffer);
}
void WriteNtrImage(char *path, int numTiles, int bitDepth, int colsPerChunk, int rowsPerChunk, struct Image *image,
bool invertColors, bool clobberSize, bool byteOrder, bool version101, bool sopc, bool vram, uint32_t scanMode,
uint32_t mappingType, uint32_t key, bool wrongSize)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
FATAL_ERROR("Failed to open \"%s\" for writing.\n", path);
int tileSize = bitDepth * 8; // number of bytes per tile
if (image->width % 8 != 0)
FATAL_ERROR("The width in pixels (%d) isn't a multiple of 8.\n", image->width);
if (image->height % 8 != 0)
FATAL_ERROR("The height in pixels (%d) isn't a multiple of 8.\n", image->height);
int tilesWide = image->width / 8; // how many tiles wide the image is
int tilesTall = image->height / 8; // how many tiles tall the image is
if (tilesWide % colsPerChunk != 0)
FATAL_ERROR("The width in tiles (%d) isn't a multiple of the specified tiles per row (%d)", tilesWide, colsPerChunk);
if (tilesTall % rowsPerChunk != 0)
FATAL_ERROR("The height in tiles (%d) isn't a multiple of the specified rows per chunk (%d)", tilesTall, rowsPerChunk);
int maxNumTiles = tilesWide * tilesTall;
if (numTiles == 0)
numTiles = maxNumTiles;
else if (numTiles > maxNumTiles)
FATAL_ERROR("The specified number of tiles (%d) is greater than the maximum possible value (%d).\n", numTiles, maxNumTiles);
int bufferSize = numTiles * tileSize;
unsigned char *pixelBuffer = malloc(bufferSize);
if (pixelBuffer == NULL)
FATAL_ERROR("Failed to allocate memory for pixels.\n");
int chunksWide = tilesWide / colsPerChunk; // how many chunks side-by-side are needed for the full width of the image
if (scanMode)
{
switch (bitDepth)
{
case 4:
ConvertToScanned4Bpp(image->pixels, pixelBuffer, bufferSize, invertColors, key, scanMode);
break;
case 8:
FATAL_ERROR("8Bpp not supported yet.\n");
break;
}
}
else
{
switch (bitDepth)
{
case 4:
ConvertToTiles4Bpp(image->pixels, pixelBuffer, numTiles, chunksWide, colsPerChunk, rowsPerChunk,
invertColors);
break;
case 8:
ConvertToTiles8Bpp(image->pixels, pixelBuffer, numTiles, chunksWide, colsPerChunk, rowsPerChunk,
invertColors);
break;
}
}
WriteGenericNtrHeader(fp, "RGCN", bufferSize + (sopc ? 0x30 : 0x20) + (wrongSize ? -8 : 0), byteOrder, version101, sopc ? 2 : 1);
unsigned char charHeader[0x20] = { 0x52, 0x41, 0x48, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00 };
charHeader[4] = (bufferSize + 0x20 + (wrongSize ? -8 : 0)) & 0xFF;
charHeader[5] = ((bufferSize + 0x20 + (wrongSize ? -8 : 0)) >> 8) & 0xFF;
charHeader[6] = ((bufferSize + 0x20 + (wrongSize ? -8 : 0)) >> 16) & 0xFF;
charHeader[7] = ((bufferSize + 0x20 + (wrongSize ? -8 : 0)) >> 24) & 0xFF;
if (!clobberSize)
{
charHeader[8] = tilesTall & 0xFF;
charHeader[9] = (tilesTall >> 8) & 0xFF;
charHeader[10] = tilesWide & 0xFF;
charHeader[11] = (tilesWide >> 8) & 0xFF;
}
else
{
charHeader[8] = 0xFF;
charHeader[9] = 0xFF;
charHeader[10] = 0xFF;
charHeader[11] = 0xFF;
charHeader[16] = 0x10; //size clobbering implies mapping type is some variant of 1d - *should* have a mapping type that's not 0
if (mappingType == 0)
{
mappingType = 32; // if not specified assume that it is 32k
}
}
charHeader[12] = bitDepth == 4 ? 3 : 4;
if (mappingType != 0) {
uint32_t val = 0;
switch (mappingType) {
case 32:
val = 0;
break;
case 64:
val = 0x10;
break;
case 128:
val = 0x20;
break;
case 256:
val = 0x30;
break;
default:
FATAL_ERROR("Invalid mapping type %d\n", mappingType);
break;
}
charHeader[18] = val;
}
if (scanMode)
{
charHeader[20] = 1; //implies BMP
}
if (vram)
{
charHeader[21] = 1; //implies VRAM transfer
}
charHeader[24] = bufferSize & 0xFF;
charHeader[25] = (bufferSize >> 8) & 0xFF;
charHeader[26] = (bufferSize >> 16) & 0xFF;
charHeader[27] = (bufferSize >> 24) & 0xFF;
fwrite(charHeader, 1, 0x20, fp);
fwrite(pixelBuffer, 1, bufferSize, fp);
if (sopc)
{
unsigned char sopcBuffer[0x10] = { 0x53, 0x4F, 0x50, 0x43, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
sopcBuffer[12] = tilesWide & 0xFF;
sopcBuffer[13] = (tilesWide >> 8) & 0xFF;
sopcBuffer[14] = tilesTall & 0xFF;
sopcBuffer[15] = (tilesTall >> 8) & 0xFF;
fwrite(sopcBuffer, 1, 0x10, fp);
}
free(pixelBuffer);
fclose(fp);
}
void FreeImage(struct Image *image)
{
free(image->pixels);
image->pixels = NULL;
}
void ReadGbaPalette(char *path, struct Palette *palette)
{
int fileSize;
unsigned char *data = ReadWholeFile(path, &fileSize);
if (fileSize % 2 != 0)
FATAL_ERROR("The file size (%d) is not a multiple of 2.\n", fileSize);
palette->numColors = fileSize / 2;
for (int i = 0; i < palette->numColors; i++) {
uint16_t paletteEntry = (data[i * 2 + 1] << 8) | data[i * 2];
palette->colors[i].red = UPCONVERT_BIT_DEPTH(GET_GBA_PAL_RED(paletteEntry));
palette->colors[i].green = UPCONVERT_BIT_DEPTH(GET_GBA_PAL_GREEN(paletteEntry));
palette->colors[i].blue = UPCONVERT_BIT_DEPTH(GET_GBA_PAL_BLUE(paletteEntry));
}
free(data);
}
void ReadNtrPalette(char *path, struct Palette *palette, int bitdepth, int palIndex, bool inverted)
{
int fileSize;
unsigned char *data = ReadWholeFile(path, &fileSize);
if (memcmp(data, "RLCN", 4) != 0 && memcmp(data, "RPCN", 4) != 0) //NCLR / NCPR
{
FATAL_ERROR("Not a valid NCLR or NCPR palette file.\n");
}
unsigned char *paletteHeader = data + 0x10;
if (memcmp(paletteHeader, "TTLP", 4) != 0)
{
FATAL_ERROR("No valid PLTT file after NCLR header.\n");
}
if ((fileSize - 0x28) % 2 != 0)
FATAL_ERROR("The file size (%d) is not a multiple of 2.\n", fileSize);
palette->bitDepth = paletteHeader[0x8] == 3 ? 4 : 8;
bitdepth = bitdepth ? bitdepth : palette->bitDepth;
size_t paletteSize = (paletteHeader[0x10]) | (paletteHeader[0x11] << 8) | (paletteHeader[0x12] << 16) | (paletteHeader[0x13] << 24);
if (inverted) paletteSize = 0x200 - paletteSize;
if (palIndex == 0) {
palette->numColors = paletteSize / 2;
} else {
palette->numColors = bitdepth == 4 ? 16 : 256; //remove header and divide by 2
--palIndex;
}
unsigned char *paletteData = paletteHeader + 0x18;
for (int i = 0; i < 256; i++)
{
if (i < palette->numColors)
{
uint16_t paletteEntry = (paletteData[(32 * palIndex) + i * 2 + 1] << 8) | paletteData[(32 * palIndex) + i * 2];
palette->colors[i].red = UPCONVERT_BIT_DEPTH(GET_GBA_PAL_RED(paletteEntry));
palette->colors[i].green = UPCONVERT_BIT_DEPTH(GET_GBA_PAL_GREEN(paletteEntry));
palette->colors[i].blue = UPCONVERT_BIT_DEPTH(GET_GBA_PAL_BLUE(paletteEntry));
}
else
{
palette->colors[i].red = 0;
palette->colors[i].green = 0;
palette->colors[i].blue = 0;
}
}
free(data);
}
void WriteGbaPalette(char *path, struct Palette *palette)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
FATAL_ERROR("Failed to open \"%s\" for writing.\n", path);
for (int i = 0; i < palette->numColors; i++) {
unsigned char red = DOWNCONVERT_BIT_DEPTH(palette->colors[i].red);
unsigned char green = DOWNCONVERT_BIT_DEPTH(palette->colors[i].green);
unsigned char blue = DOWNCONVERT_BIT_DEPTH(palette->colors[i].blue);
uint16_t paletteEntry = SET_GBA_PAL(red, green, blue);
fputc(paletteEntry & 0xFF, fp);
fputc(paletteEntry >> 8, fp);
}
fclose(fp);
}
void WriteNtrPalette(char *path, struct Palette *palette, bool ncpr, bool ir, int bitdepth, bool pad, int compNum, bool pcmp, bool inverted)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
FATAL_ERROR("Failed to open \"%s\" for writing.\n", path);
int colourNum = pad ? 256 : palette->numColors;
uint32_t size = colourNum * 2; //todo check if there's a better way to detect :/
uint32_t extSize = size + (ncpr ? 0x10 : 0x18);
int numSections = 1;
int pcmpColorNum = 0;
uint32_t pcmpSize = 0;
if (pcmp)
{
pcmpColorNum = colourNum / (bitdepth == 4 ? 16 : 256);
if (pcmpColorNum == 0) {
FATAL_ERROR("colourNum=%d palette->bitDepth=%d\n", colourNum, bitdepth);
}
pcmpSize = 16 + pcmpColorNum * 2;
++numSections;
}
//NCLR header
WriteGenericNtrHeader(fp, (ncpr ? "RPCN" : "RLCN"), extSize + pcmpSize, !ncpr, false, numSections);
unsigned char palHeader[0x18] =
{
0x54, 0x54, 0x4C, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00
};
//section size
palHeader[4] = extSize & 0xFF;
palHeader[5] = (extSize >> 8) & 0xFF;
palHeader[6] = (extSize >> 16) & 0xFF;
palHeader[7] = (extSize >> 24) & 0xFF;
if (!palette->bitDepth)
palette->bitDepth = 4;
bitdepth = bitdepth ? bitdepth : palette->bitDepth;
//bit depth
palHeader[8] = bitdepth == 4 ? 0x03: 0x04;
if (compNum)
{
palHeader[10] = compNum; //assuming this is an indicator of compression, literally no docs for it though
}
//size
int colorSize = inverted ? 0x200 - size : size;
palHeader[16] = colorSize & 0xFF;
palHeader[17] = (colorSize >> 8) & 0xFF;
palHeader[18] = (colorSize >> 16) & 0xFF;
palHeader[19] = (colorSize >> 24) & 0xFF;
fwrite(palHeader, 1, 0x18, fp);
unsigned char * colours = malloc(colourNum * 2);
//palette data
for (int i = 0; i < colourNum; i++)
{
if (i < palette->numColors)
{
unsigned char red = DOWNCONVERT_BIT_DEPTH(palette->colors[i].red);
unsigned char green = DOWNCONVERT_BIT_DEPTH(palette->colors[i].green);
unsigned char blue = DOWNCONVERT_BIT_DEPTH(palette->colors[i].blue);
uint16_t paletteEntry = SET_GBA_PAL(red, green, blue);
colours[i * 2] = paletteEntry & 0xFF;
colours[i * 2 + 1] = paletteEntry >> 8;
}
else
{
colours[i * 2] = 0x00;
colours[i * 2 + 1] = 0x00;
}
}
if (ir)
{
colours[colourNum * 2 - 2] = 'I';
colours[colourNum * 2 - 1] = 'R';
}
fwrite(colours, 1, colourNum * 2, fp);
free(colours);
if (pcmp)
{
uint8_t pcmp_header[16] = {0x50, 0x4D, 0x43, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF, 0xBE, 0x08, 0x00, 0x00, 0x00};
pcmp_header[4] = pcmpSize & 0xFF;
pcmp_header[5] = (pcmpSize >> 8) & 0xFF;
pcmp_header[6] = (pcmpSize >> 16) & 0xFF;
pcmp_header[7] = (pcmpSize >> 24) & 0xFF;
pcmp_header[8] = pcmpColorNum & 0xFF;
pcmp_header[9] = (pcmpColorNum >> 8) & 0xFF;
fwrite(pcmp_header, 1, 16, fp);
uint8_t *pcmp_data = malloc(2 * pcmpColorNum);
if (pcmp_data == NULL)
{
FATAL_ERROR("failed to alloc pcmp_data\n");
}
for (int i = 0; i < pcmpColorNum; ++i) {
pcmp_data[i * 2] = i & 0xFF;
pcmp_data[i * 2 + 1] = (i >> 8) & 0xFF;
}
fwrite(pcmp_data, 1, pcmpColorNum * 2, fp);
free(pcmp_data);
}
fclose(fp);
}
void ReadNtrCell_CEBK(unsigned char * restrict data, unsigned int blockOffset, unsigned int blockSize, struct JsonToCellOptions *options)
{
options->cellCount = data[blockOffset + 0x8] | (data[blockOffset + 0x9] << 8);
options->extended = data[blockOffset + 0xA] == 1;
int vramTransferOffset = (data[blockOffset + 0x14] | data[blockOffset + 0x15] << 8);
options->vramTransferEnabled = vramTransferOffset > 0;
/*if (!options->extended)
{
//in theory not extended should be implemented, however not 100% sure
FATAL_ERROR("Don't know how to deal with not extended yet, bug red031000.\n");
}*/
options->mappingType = data[blockOffset + 0x10];
options->cells = malloc(sizeof(struct Cell *) * options->cellCount);
int celSize = options->extended ? 0x10 : 0x8;
for (int i = 0; i < options->cellCount; i++)
{
int offset = blockOffset + 0x20 + (i * celSize);
if (offset + celSize > blockOffset + blockSize) {
FATAL_ERROR("corrupted CEBK block\n");
}
options->cells[i] = malloc(sizeof(struct Cell));
options->cells[i]->oamCount = data[offset] | (data[offset + 1] << 8);
short cellAttrs = data[offset + 2] | (data[offset + 3] << 8);
options->cells[i]->attributes.hFlip = (cellAttrs >> 8) & 1;
options->cells[i]->attributes.vFlip = (cellAttrs >> 9) & 1;
options->cells[i]->attributes.hvFlip = (cellAttrs >> 10) & 1;
options->cells[i]->attributes.boundingRect = (cellAttrs >> 11) & 1;
options->cells[i]->attributes.boundingSphereRadius = cellAttrs & 0x3F;
if (options->extended)
{
options->cells[i]->maxX = data[offset + 8] | (data[offset + 9] << 8);
options->cells[i]->maxY = data[offset + 10] | (data[offset + 11] << 8);
options->cells[i]->minX = data[offset + 12] | (data[offset + 13] << 8);
options->cells[i]->minY = data[offset + 14] | (data[offset + 15] << 8);
}
}
int offset = blockOffset + 0x20 + (options->cellCount * celSize);
for (int i = 0; i < options->cellCount; i++)
{
options->cells[i]->oam = malloc(sizeof(struct OAM) * options->cells[i]->oamCount);
for (int j = 0; j < options->cells[i]->oamCount; j++)
{
//Attr0
//bits 0-7 Y coordinate
options->cells[i]->oam[j].attr0.YCoordinate = data[offset];
//bit 8 rotation
options->cells[i]->oam[j].attr0.Rotation = data[offset + 1] & 1;
//bit 9 Obj Size (if rotation) or Obj Disable (if not rotation)
options->cells[i]->oam[j].attr0.SizeDisable = (data[offset + 1] >> 1) & 1;
//bits 10-11 Obj Mode
options->cells[i]->oam[j].attr0.Mode = (data[offset + 1] >> 2) & 3;
//bit 12 Obj Mosaic
options->cells[i]->oam[j].attr0.Mosaic = (data[offset + 1] >> 4) & 1;
//bit 13 Colours
options->cells[i]->oam[j].attr0.Colours = ((data[offset + 1] >> 5) & 1) == 0 ? 16 : 256;
//bits 14-15 Obj Shape
options->cells[i]->oam[j].attr0.Shape = (data[offset + 1] >> 6) & 3;
//Attr1
//bits 0-8 X coordinate
options->cells[i]->oam[j].attr1.XCoordinate = data[offset + 2] | ((data[offset + 3] & 1) << 8);
//bits 9-13 Rotation and scaling (if rotation) bit 12 Horizontal flip, bit 13 Vertical flip (if not rotation)
options->cells[i]->oam[j].attr1.RotationScaling = (data[offset + 3] >> 1) & 0x1F;
//bits 14-15 Obj Size
options->cells[i]->oam[j].attr1.Size = (data[offset + 3] >> 6) & 3;
//Attr2
//bits 0-9 Character Name?
options->cells[i]->oam[j].attr2.CharName = data[offset + 4] | ((data[offset + 5] & 3) << 8);
//bits 10-11 Priority
options->cells[i]->oam[j].attr2.Priority = (data[offset + 5] >> 2) & 3;
//bits 12-15 Palette Number
options->cells[i]->oam[j].attr2.Palette = (data[offset + 5] >> 4) & 0xF;
offset += 6;
}
}
if (options->vramTransferEnabled)
{
offset = blockOffset + 0x08 + vramTransferOffset;
// first 2 dwords are max size and offset, offset *should* always be 0x08 since the transfer data list immediately follows this
options->vramTransferMaxSize = data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
offset += 0x08;
// read 1 VRAM transfer data block for each cell (this is an assumption based on the NCERs I looked at)
options->transferData = malloc(sizeof(struct CellVramTransferData *) * options->cellCount);
for (int idx = 0; idx < options->cellCount; idx++)
{
options->transferData[idx] = malloc(sizeof(struct CellVramTransferData));
options->transferData[idx]->sourceDataOffset = data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
options->transferData[idx]->size = data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) | (data[offset + 7] << 24);
offset += 8;
}
}
}
void ReadNtrCell_LABL(unsigned char * restrict data, unsigned int blockOffset, unsigned int blockSize, struct JsonToCellOptions *options)
{
int count = 0;
unsigned int textStart = blockOffset + 8;
while (textStart < blockOffset + blockSize)
{
unsigned int labelOffset = data[textStart] | (data[textStart + 1] << 8) | (data[textStart + 2] << 16) | (data[textStart + 3] << 24);
if (labelOffset > blockSize)
{
break;
}
else {
++count;
textStart += 4;
}
}
options->labelCount = count;
options->labels = malloc(sizeof(char *) * count);
for (int i = 0; i < count; ++i)
{
int offset = textStart + (data[blockOffset + 4 * i + 8] | (data[blockOffset + 4 * i + 9] << 8) | (data[blockOffset + 4 * i + 10] << 16) | (data[blockOffset + 4 * i + 11] << 24));
if (offset > blockOffset + blockSize)
{
FATAL_ERROR("corrupted LABL block\n");
}
unsigned long slen = strnlen((char *)data + offset, blockSize - offset);
options->labels[i] = malloc(slen + 1);
strncpy(options->labels[i], (char *)data + offset, slen + 1);
}
}
void ReadNtrCell(char *path, struct JsonToCellOptions *options)
{
int fileSize;
unsigned char *data = ReadWholeFile(path, &fileSize);
unsigned int offset = 0x10;
if (memcmp(data, "RECN", 4) != 0) //NCER
{
FATAL_ERROR("Not a valid NCER cell file.\n");
}
options->labelEnabled = false;
unsigned int blockSize;
offset = FindNitroDataBlock(data, "KBEC", fileSize, &blockSize);
if (offset != -1u)
{
ReadNtrCell_CEBK(data, offset, blockSize, options);
}
else {
FATAL_ERROR("missing CEBK block");
}
offset = FindNitroDataBlock(data, "LBAL", fileSize, &blockSize);
if (offset != -1u)
{
options->labelEnabled = true;
ReadNtrCell_LABL(data, offset, blockSize, options);
}
free(data);
}
void WriteNtrCell(char *path, struct JsonToCellOptions *options)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
FATAL_ERROR("Failed to open \"%s\" for writing.\n", path);
int iterNum = (options->extended ? 0x10 : 0x8);
// KBEC base size: 0x08 per bank, or 0x10 per extended bank
unsigned int kbecSize = options->cellCount * (options->extended ? 0x10 : 0x08);
// if VRAM transfer is enabled, add 0x08 for the header and 0x08 for each cell
if (options->vramTransferEnabled)
{
kbecSize += 0x08 + (0x08 * options->cellCount);
}
// add 0x06 for number of OAMs - can be more than 1
for (int idx = 0; idx < options->cellCount * iterNum; idx += iterNum)
{
kbecSize += options->cells[idx / iterNum]->oamCount * 0x06;
}
// KBEC size is padded to be 4-byte aligned
kbecSize += kbecSize % 4;
unsigned int totalSize = (options->labelEnabled > 0 ? 0x34 : 0x20) + kbecSize;
if (options->labelEnabled)
{
for (int j = 0; j < options->labelCount; j++)
{
totalSize += (unsigned)strlen(options->labels[j]) + 5; //strlen + terminator + pointer
}
}
WriteGenericNtrHeader(fp, "RECN", totalSize, true, false, options->labelEnabled ? 3 : 1);
unsigned char KBECHeader[0x20] =
{
0x4B, 0x42, 0x45, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
KBECHeader[8] = options->cellCount; //cell count
if (options->extended)
{
KBECHeader[10] = 1; //extended
}
KBECHeader[4] = (kbecSize + 0x20) & 0xFF; //size
KBECHeader[5] = (kbecSize + 0x20) >> 8; //unlikely to be more than 16 bits, but there are 32 allocated, change if necessary
KBECHeader[16] = (options->mappingType & 0xFF); //not possible to be more than 8 bits, though 32 are allocated
// offset to VRAM transfer data within KBEC section (offset from KBEC start + 0x08)
if (options->vramTransferEnabled)
{
unsigned int vramTransferLength = 0x08 + (0x08 * options->cellCount);
unsigned int vramTransferOffset = (kbecSize + 0x20) - vramTransferLength - 0x08;
KBECHeader[20] = vramTransferOffset & 0xFF;
KBECHeader[21] = (vramTransferOffset >> 8) & 0xFF;
KBECHeader[22] = (vramTransferOffset >> 16) & 0xFF;
KBECHeader[23] = (vramTransferOffset >> 24) & 0xFF;
}
fwrite(KBECHeader, 1, 0x20, fp);
unsigned char *KBECContents = malloc(kbecSize);
memset(KBECContents, 0, kbecSize);
/*if (!options->extended)
{
//in theory not extended should be implemented, however not 100% sure
FATAL_ERROR("Don't know how to deal with not extended yet, bug red031000.\n");
}*/
int i;
int totalOam = 0;
for (i = 0; i < options->cellCount * iterNum; i += iterNum)
{
KBECContents[i] = options->cells[i / iterNum]->oamCount; //number of OAM entries
short cellAttrs = (options->cells[i / iterNum]->attributes.hFlip << 8) | (options->cells[i / iterNum]->attributes.vFlip << 9)
| (options->cells[i / iterNum]->attributes.hvFlip << 10) | (options->cells[i / iterNum]->attributes.boundingRect << 11)
| (options->cells[i / iterNum]->attributes.boundingSphereRadius & 0x3F);
KBECContents[i + 2] = cellAttrs & 0xff; //cell attributes
KBECContents[i + 3] = cellAttrs >> 8;
KBECContents[i + 4] = (totalOam * 6) & 0xff; //pointer to OAM data
KBECContents[i + 5] = (totalOam * 6) >> 8; //unlikely to be more than 16 bits, but there are 32 allocated, change if necessary
if (options->extended)
{
KBECContents[i + 8] = options->cells[i / iterNum]->maxX & 0xff; //maxX
KBECContents[i + 9] = options->cells[i / iterNum]->maxX >> 8;
KBECContents[i + 10] = options->cells[i / iterNum]->maxY & 0xff; //maxY
KBECContents[i + 11] = options->cells[i / iterNum]->maxY >> 8;
KBECContents[i + 12] = options->cells[i / iterNum]->minX & 0xff; //minX
KBECContents[i + 13] = options->cells[i / iterNum]->minX >> 8;
KBECContents[i + 14] = options->cells[i / iterNum]->minY & 0xff; //minY
KBECContents[i + 15] = options->cells[i / iterNum]->minY >> 8;
}
totalOam += options->cells[i / iterNum]->oamCount;
}
//OAM data
int offset = i;
for (int j = 0; j < options->cellCount; j++)
{
for (int k = 0; k < options->cells[j]->oamCount; k++)
{
//Attr0
//bits 0-7 Y coordinate
KBECContents[offset] = options->cells[j]->oam[k].attr0.YCoordinate & 0xff;
//bit 8 rotation
KBECContents[offset + 1] = options->cells[j]->oam[k].attr0.Rotation;
//bit 9 Obj Size (if rotation) or Obj Disable (if not rotation)
KBECContents[offset + 1] |= options->cells[j]->oam[k].attr0.SizeDisable << 1;
//bits 10-11 Obj Mode
KBECContents[offset + 1] |= options->cells[j]->oam[k].attr0.Mode << 2;
//bit 12 Obj Mosaic
KBECContents[offset + 1] |= options->cells[j]->oam[k].attr0.Mosaic << 4;
//bit 13 Colours
KBECContents[offset + 1] |= (options->cells[j]->oam[k].attr0.Colours == 16 ? 0 : 1) << 5;
//bits 14-15 Obj Shape
KBECContents[offset + 1] |= options->cells[j]->oam[k].attr0.Shape << 6;
//Attr1
//bits 0-8 X coordinate
KBECContents[offset + 2] = options->cells[j]->oam[k].attr1.XCoordinate & 0xff;
KBECContents[offset + 3] = options->cells[j]->oam[k].attr1.XCoordinate >> 8;
//bits 9-13 Rotation and scaling (if rotation) bit 12 Horizontal flip, bit 13 Vertical flip (if not rotation)
KBECContents[offset + 3] |= options->cells[j]->oam[k].attr1.RotationScaling << 1;
//bits 14-15 Obj Size
KBECContents[offset + 3] |= options->cells[j]->oam[k].attr1.Size << 6;
//Attr2
//bits 0-9 Character Name?
KBECContents[offset + 4] = options->cells[j]->oam[k].attr2.CharName & 0xff;
KBECContents[offset + 5] = options->cells[j]->oam[k].attr2.CharName >> 8;
//bits 10-11 Priority
KBECContents[offset + 5] |= options->cells[j]->oam[k].attr2.Priority << 2;
//bits 12-15 Palette Number
KBECContents[offset + 5] |= options->cells[j]->oam[k].attr2.Palette << 4;
offset += 6;
}
}
// VRAM transfer data
if (options->vramTransferEnabled)
{
// max transfer size + fixed offset 0x08
KBECContents[offset] = options->vramTransferMaxSize & 0xFF;
KBECContents[offset + 1] = (options->vramTransferMaxSize >> 8) & 0xFF;
KBECContents[offset + 2] = (options->vramTransferMaxSize >> 16) & 0xFF;
KBECContents[offset + 3] = (options->vramTransferMaxSize >> 24) & 0xFF;
KBECContents[offset + 4] = 0x08;
offset += 8;
// write a VRAM transfer block for each cell
for (int idx = 0; idx < options->cellCount; idx++)
{
// offset
KBECContents[offset] = options->transferData[idx]->sourceDataOffset & 0xFF;
KBECContents[offset + 1] = (options->transferData[idx]->sourceDataOffset >> 8) & 0xFF;
KBECContents[offset + 2] = (options->transferData[idx]->sourceDataOffset >> 16) & 0xFF;
KBECContents[offset + 3] = (options->transferData[idx]->sourceDataOffset >> 24) & 0xFF;
// size
KBECContents[offset + 4] = options->transferData[idx]->size & 0xFF;
KBECContents[offset + 5] = (options->transferData[idx]->size >> 8) & 0xFF;
KBECContents[offset + 6] = (options->transferData[idx]->size >> 16) & 0xFF;
KBECContents[offset + 7] = (options->transferData[idx]->size >> 24) & 0xFF;
offset += 8;
}
}
fwrite(KBECContents, 1, kbecSize, fp);
free(KBECContents);
if (options->labelEnabled)
{
unsigned int lablSize = 8;
for (int j = 0; j < options->labelCount; j++)
{
lablSize += (unsigned)strlen(options->labels[j]) + 5;
}
unsigned char *labl = malloc(lablSize);
memset(labl, 0, lablSize);
strcpy((char *) labl, "LBAL");
labl[4] = lablSize & 0xff;
labl[5] = lablSize >> 8;
unsigned int position = 0;
i = 0;
for (int j = 0; j < options->labelCount; j++)
{
labl[i + 8] = position & 0xff;
labl[i + 9] = position >> 8;
position += (unsigned)strlen(options->labels[j]) + 1;
i += 4;
}
for (int j = 0; j < options->labelCount; j++)
{
strcpy((char *) (labl + (i + 8)), options->labels[j]);
i += (int)strlen(options->labels[j]) + 1;
}
fwrite(labl, 1, lablSize, fp);
free(labl);
unsigned char txeu[0xc] = {0x54, 0x58, 0x45, 0x55, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
fwrite(txeu, 1, 0xc, fp);
}
fclose(fp);
}
void WriteNtrScreen(char *path, struct JsonToScreenOptions *options)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
FATAL_ERROR("Failed to open \"%s\" for writing.\n", path);
int totalSize = options->width * options->height * 2 + 0x14;
WriteGenericNtrHeader(fp, "RCSN", totalSize, true, false, 1);
unsigned char NSCRHeader[0x14] = { 0x4E, 0x52, 0x43, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00 };
NSCRHeader[0x4] = totalSize & 0xff;
NSCRHeader[0x5] = (totalSize >> 8) & 0xff;
NSCRHeader[0x6] = (totalSize >> 16) & 0xff;
NSCRHeader[0x7] = totalSize >> 24;
NSCRHeader[0x8] = (options->width * 8) & 0xff;
NSCRHeader[0x9] = (options->width * 8) >> 8;
NSCRHeader[0xA] = (options->height * 8) & 0xff;
NSCRHeader[0xB] = (options->height * 8) >> 8;
NSCRHeader[0xC] = options->bitdepth == 4 ? 0 : 1;
NSCRHeader[0x10] = (totalSize - 0x14) & 0xff;
NSCRHeader[0x11] = ((totalSize - 0x14) >> 8) & 0xff;
NSCRHeader[0x12] = ((totalSize - 0x14) >> 16) & 0xff;
NSCRHeader[0x13] = (totalSize - 0x14) >> 24;
fwrite(NSCRHeader, 1, 0x14, fp);
fwrite(options->data, 1, totalSize - 0x14, fp);
fclose(fp);
}
void ReadNtrAnimation(char *path, struct JsonToAnimationOptions *options)
{
int fileSize;
unsigned char *data = ReadWholeFile(path, &fileSize);
if (memcmp(data, "RNAN", 4) != 0 && memcmp(data, "RAMN", 4) != 0) //NANR/NMAR
{
FATAL_ERROR("Not a valid NANR/NMAR animation file.\n");
}
options->labelEnabled = data[0xE] != 1;
if (memcmp(data + 0x10, "KNBA", 4) != 0 ) //ABNK
{
FATAL_ERROR("Not a valid ABNK animation file.\n");
}
options->sequenceCount = data[0x18] | (data[0x19] << 8);
options->frameCount = data[0x1A] | (data[0x1B] << 8);
options->sequenceData = malloc(sizeof(struct SequenceData *) * options->sequenceCount);
for (int i = 0; i < options->sequenceCount; i++)
{
options->sequenceData[i] = malloc(sizeof(struct SequenceData));
}
int offset = 0x30;
unsigned int *frameOffsets = malloc(sizeof(unsigned int) * options->sequenceCount);
for (int i = 0; i < options->sequenceCount; i++, offset += 0x10)
{
options->sequenceData[i]->frameCount = data[offset] | (data[offset + 1] << 8);
options->sequenceData[i]->loopStartFrame = data[offset + 2] | (data[offset + 3] << 8);
options->sequenceData[i]->animationElement = data[offset + 4] | (data[offset + 5] << 8);
options->sequenceData[i]->animationType = data[offset + 6] | (data[offset + 7] << 8);
options->sequenceData[i]->playbackMode = data[offset + 8] | (data[offset + 9] << 8) | (data[offset + 10] << 16) | (data[offset + 11] << 24);
frameOffsets[i] = data[offset + 12] | (data[offset + 13] << 8) | (data[offset + 14] << 16) | (data[offset + 15] << 24);
options->sequenceData[i]->frameData = malloc(sizeof(struct FrameData *) * options->sequenceData[i]->frameCount);
for (int j = 0; j < options->sequenceData[i]->frameCount; j++)
{
options->sequenceData[i]->frameData[j] = malloc(sizeof(struct FrameData));
}
}
int *resultOffsets = malloc(sizeof(int) * options->frameCount);
memset(resultOffsets, -1, sizeof(int) * options->frameCount);
for (int i = 0; i < options->sequenceCount; i++)
{
for (int j = 0; j < options->sequenceData[i]->frameCount; j++)
{
int frameOffset = offset + frameOffsets[i] + j * 0x8;
options->sequenceData[i]->frameData[j]->resultOffset = data[frameOffset] | (data[frameOffset + 1] << 8) | (data[frameOffset + 2] << 16) | (data[frameOffset + 3] << 24);
options->sequenceData[i]->frameData[j]->frameDelay = data[frameOffset + 4] | (data[frameOffset + 5] << 8);
//0xBEEF
//the following is messy
bool present = false;
//check for offset in array
for (int k = 0; k < options->frameCount; k++)
{
if (resultOffsets[k] == options->sequenceData[i]->frameData[j]->resultOffset)
{
options->sequenceData[i]->frameData[j]->resultId = k;
present = true;
break;
}
}
//add data if not present
if (!present)
{
for (int k = 0; i < options->frameCount; k++)
{
if (resultOffsets[k] == -1)
{
options->sequenceData[i]->frameData[j]->resultId = k;
resultOffsets[k] = options->sequenceData[i]->frameData[j]->resultOffset;
break;
}
}
}
}
}
free(frameOffsets);
offset = 0x18 + (data[0x24] | (data[0x25] << 8) | (data[0x26] << 16) | (data[0x27] << 24)); //start of animation results
int k;
for (k = 0; k < options->frameCount; k++)
{
if (resultOffsets[k] == -1)
break;
}
options->resultCount = k;
free(resultOffsets);
options->animationResults = malloc(sizeof(struct AnimationResults *) * options->resultCount);
for (int i = 0; i < options->resultCount; i++)
{
options->animationResults[i] = malloc(sizeof(struct AnimationResults));
}
// store the animationElement of the corresponding sequence as this result's resultType
for (int i = 0; i < options->sequenceCount; i++)
{
for (int j = 0; j < options->sequenceData[i]->frameCount; j++)
{
options->animationResults[options->sequenceData[i]->frameData[j]->resultId]->resultType = options->sequenceData[i]->animationElement;
}
}
int resultOffset = 0;
int lastSequence = 0;
for (int i = 0; i < options->resultCount; i++)
{
// find the earliest sequence matching this animation result,
// and add padding if the sequence changes + the total offset is not 4-byte aligned.
bool found = false;
for (int j = 0; j < options->sequenceCount; j++)
{
for (int k = 0; k < options->sequenceData[j]->frameCount; k++)
{
if (options->sequenceData[j]->frameData[k]->resultId == i)
{
if (lastSequence != j)
{
lastSequence = j;
if (resultOffset % 4 != 0)
{
resultOffset += 0x2;
offset += 0x2;
}
}
found = true;
break;
}
}
if (found) break;
}
switch (options->animationResults[i]->resultType)
{
case 0: //index
options->animationResults[i]->index = data[offset] | (data[offset + 1] << 8);
resultOffset += 0x2;
offset += 0x2;
break;
case 1: //SRT
options->animationResults[i]->dataSrt.index = data[offset] | (data[offset + 1] << 8);
options->animationResults[i]->dataSrt.rotation = data[offset + 2] | (data[offset + 3] << 8);
options->animationResults[i]->dataSrt.scaleX = data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) | (data[offset + 7] << 24);
options->animationResults[i]->dataSrt.scaleY = data[offset + 8] | (data[offset + 9] << 8) | (data[offset + 10] << 16) | (data[offset + 11] << 24);
options->animationResults[i]->dataSrt.positionX = data[offset + 12] | (data[offset + 13] << 8);
options->animationResults[i]->dataSrt.positionY = data[offset + 14] | (data[offset + 15] << 8);
resultOffset += 0x10;
offset += 0x10;
break;
case 2: //T
options->animationResults[i]->dataT.index = data[offset] | (data[offset + 1] << 8);
options->animationResults[i]->dataT.positionX = data[offset + 4] | (data[offset + 5] << 8);
options->animationResults[i]->dataT.positionY = data[offset + 6] | (data[offset + 7] << 8);
resultOffset += 0x8;
offset += 0x8;
break;
}
}
// add any missed padding from the final frame before processing labels
if (offset % 4 != 0) offset += 2;
if (options->labelEnabled)
{
options->labelCount = options->sequenceCount; //*should* be the same
options->labels = malloc(sizeof(char *) * options->labelCount);
offset += 0x8 + options->labelCount * 0x4; //skip to label data
for (int i = 0; i < options->labelCount; i++)
{
options->labels[i] = malloc(strlen((char *)data + offset) + 1);
strcpy(options->labels[i], (char *)data + offset);
offset += strlen((char *)data + offset) + 1;
}
}
free(data);
}
void WriteNtrAnimation(char *path, struct JsonToAnimationOptions *options)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
FATAL_ERROR("Failed to open \"%s\" for writing.\n", path);
unsigned int totalSize = 0x20 + options->sequenceCount * 0x10 + options->frameCount * 0x8;
for (int i = 0; i < options->resultCount; i++)
{
if (options->animationResults[i]->resultType == 0)
totalSize += 0x2;
else if (options->animationResults[i]->resultType == 1)
totalSize += 0x10;
else if (options->animationResults[i]->resultType == 2)
totalSize += 0x8;
}
// foreach sequence, need to check whether padding is applied for its results
// then add 0x02 to totalSize if padding exists.
// padding exists if the animation results for that sequence are not 4-byte aligned.
// also flag the last result for the sequence with `padded` to save having to redo this same step later.
int *usedResults = malloc(sizeof(int) * options->frameCount);
memset(usedResults, -1, sizeof(int) * options->frameCount);
for (int i = 0; i < options->sequenceCount; i++)
{
int sequenceLen = 0;
int resultIndex = 0;
int lastNewResultIndex = -1;
for (int j = 0; j < options->sequenceData[i]->frameCount; j++)
{
// check if the result has already been used
bool isUsed = false;
for (resultIndex = 0; resultIndex < options->resultCount; resultIndex++)
{
if (usedResults[resultIndex] == options->sequenceData[i]->frameData[j]->resultId)
{
isUsed = true;
break;
}
// if not already used, add it to the list
if (usedResults[resultIndex] == -1)
{
usedResults[resultIndex] = options->sequenceData[i]->frameData[j]->resultId;
lastNewResultIndex = options->sequenceData[i]->frameData[j]->resultId;
break;
}
}
// if not already used, add it to the result size for the sequence
if (!isUsed)
{
if (options->animationResults[resultIndex]->resultType == 0)
sequenceLen += 0x2;
else if (options->animationResults[resultIndex]->resultType == 1)
sequenceLen += 0x10;
else if (options->animationResults[resultIndex]->resultType == 2)
sequenceLen += 0x8;
}
}
if (sequenceLen % 4 != 0 && lastNewResultIndex != -1)
{
totalSize += 0x02;
// mark the last new animationResult index for the sequence as padded, this saves needing to check this again later
options->animationResults[lastNewResultIndex]->padded = true;
}
}
free(usedResults);
unsigned int KNBASize = totalSize;
if (options->labelEnabled)
{
totalSize += options->multiCell ? 0x8 : 0x14;
for (int j = 0; j < options->labelCount; j++)
{
totalSize += (unsigned)strlen(options->labels[j]) + 5; //strlen + terminator + pointer
}
}
WriteGenericNtrHeader(fp, options->multiCell ? "RAMN" : "RNAN", totalSize, true, false, options->labelEnabled ? (options->multiCell ? 2 : 3) : 1);
unsigned char KBNAHeader[0x20] =
{
0x4B, 0x4E, 0x42, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
KBNAHeader[4] = KNBASize & 0xff;
KBNAHeader[5] = (KNBASize >> 8) & 0xff;
KBNAHeader[6] = (KNBASize >> 16) & 0xff;
KBNAHeader[7] = KNBASize >> 24;
KBNAHeader[8] = options->sequenceCount & 0xff;
KBNAHeader[9] = options->sequenceCount >> 8;
KBNAHeader[10] = options->frameCount & 0xff;
KBNAHeader[11] = options->frameCount >> 8;
unsigned int frameOffset = 0x18 + options->sequenceCount * 0x10;
KBNAHeader[16] = frameOffset & 0xff;
KBNAHeader[17] = (frameOffset >> 8) & 0xff;
KBNAHeader[18] = (frameOffset >> 16) & 0xff;
KBNAHeader[19] = frameOffset >> 24;
unsigned int resultsOffset = frameOffset + options->frameCount * 0x8;
KBNAHeader[20] = resultsOffset & 0xff;
KBNAHeader[21] = (resultsOffset >> 8) & 0xff;
KBNAHeader[22] = (resultsOffset >> 16) & 0xff;
KBNAHeader[23] = resultsOffset >> 24;
fwrite(KBNAHeader, 1, 0x20, fp);
int contentsSize = KNBASize - 0x20;
unsigned char *KBNAContents = malloc(contentsSize);
int i;
int framePtrCounter = 0;
for (i = 0; i < options->sequenceCount * 0x10; i += 0x10)
{
KBNAContents[i] = options->sequenceData[i / 0x10]->frameCount & 0xff;
KBNAContents[i + 1] = options->sequenceData[i / 0x10]->frameCount >> 8;
KBNAContents[i + 2] = options->sequenceData[i / 0x10]->loopStartFrame & 0xff;
KBNAContents[i + 3] = options->sequenceData[i / 0x10]->loopStartFrame >> 8;
KBNAContents[i + 4] = options->sequenceData[i / 0x10]->animationElement & 0xff;
KBNAContents[i + 5] = (options->sequenceData[i / 0x10]->animationElement >> 8) & 0xff;
KBNAContents[i + 6] = options->sequenceData[i / 0x10]->animationType & 0xff;
KBNAContents[i + 7] = (options->sequenceData[i / 0x10]->animationType >> 8) & 0xff;
KBNAContents[i + 8] = options->sequenceData[i / 0x10]->playbackMode & 0xff;
KBNAContents[i + 9] = (options->sequenceData[i / 0x10]->playbackMode >> 8) & 0xff;
KBNAContents[i + 10] = (options->sequenceData[i / 0x10]->playbackMode >> 16) & 0xff;
KBNAContents[i + 11] = options->sequenceData[i / 0x10]->playbackMode >> 24;
KBNAContents[i + 12] = framePtrCounter & 0xff;
KBNAContents[i + 13] = (framePtrCounter >> 8) & 0xff;
KBNAContents[i + 14] = (framePtrCounter >> 16) & 0xff;
KBNAContents[i + 15] = framePtrCounter >> 24;
framePtrCounter += options->sequenceData[i / 0x10]->frameCount * 8;
}
int j;
int m;
for (j = i, m = 0; m < options->sequenceCount; m++)
{
for (int k = 0; k < options->sequenceData[m]->frameCount; k++) {
int resPtr = 0;
for (int l = 0; l < options->sequenceData[m]->frameData[k]->resultId; l++) {
if (options->animationResults[l]->resultType == 0)
resPtr += 0x2;
else if (options->animationResults[l]->resultType == 1)
resPtr += 0x10;
else if (options->animationResults[l]->resultType == 2)
resPtr += 0x8;
if (options->animationResults[l]->padded) resPtr += 0x02;
}
KBNAContents[j + (k * 8)] = resPtr & 0xff;
KBNAContents[j + (k * 8) + 1] = (resPtr >> 8) & 0xff;
KBNAContents[j + (k * 8) + 2] = (resPtr >> 16) & 0xff;
KBNAContents[j + (k * 8) + 3] = resPtr >> 24;
KBNAContents[j + (k * 8) + 4] = options->sequenceData[m]->frameData[k]->frameDelay & 0xff;
KBNAContents[j + (k * 8) + 5] = options->sequenceData[m]->frameData[k]->frameDelay >> 8;
KBNAContents[j + (k * 8) + 6] = 0xEF;
KBNAContents[j + (k * 8) + 7] = 0xBE;
}
j += options->sequenceData[m]->frameCount * 8;
}
int resPtrCounter = j;
for (int k = 0; k < options->resultCount; k++)
{
switch (options->animationResults[k]->resultType)
{
case 0:
KBNAContents[resPtrCounter] = options->animationResults[k]->index & 0xff;
KBNAContents[resPtrCounter + 1] = options->animationResults[k]->index >> 8;
resPtrCounter += 0x2;
break;
case 1:
KBNAContents[resPtrCounter] = options->animationResults[k]->dataSrt.index & 0xff;
KBNAContents[resPtrCounter + 1] = options->animationResults[k]->dataSrt.index >> 8;
KBNAContents[resPtrCounter + 2] = options->animationResults[k]->dataSrt.rotation & 0xff;
KBNAContents[resPtrCounter + 3] = options->animationResults[k]->dataSrt.rotation >> 8;
KBNAContents[resPtrCounter + 4] = options->animationResults[k]->dataSrt.scaleX & 0xff;
KBNAContents[resPtrCounter + 5] = (options->animationResults[k]->dataSrt.scaleX >> 8) & 0xff;
KBNAContents[resPtrCounter + 6] = (options->animationResults[k]->dataSrt.scaleX >> 16) & 0xff;
KBNAContents[resPtrCounter + 7] = options->animationResults[k]->dataSrt.scaleX >> 24;
KBNAContents[resPtrCounter + 8] = options->animationResults[k]->dataSrt.scaleY & 0xff;
KBNAContents[resPtrCounter + 9] = (options->animationResults[k]->dataSrt.scaleY >> 8) & 0xff;
KBNAContents[resPtrCounter + 10] = (options->animationResults[k]->dataSrt.scaleY >> 16) & 0xff;
KBNAContents[resPtrCounter + 11] = options->animationResults[k]->dataSrt.scaleY >> 24;
KBNAContents[resPtrCounter + 12] = options->animationResults[k]->dataSrt.positionX & 0xff;
KBNAContents[resPtrCounter + 13] = options->animationResults[k]->dataSrt.positionX >> 8;
KBNAContents[resPtrCounter + 14] = options->animationResults[k]->dataSrt.positionY & 0xff;
KBNAContents[resPtrCounter + 15] = options->animationResults[k]->dataSrt.positionY >> 8;
resPtrCounter += 0x10;
break;
case 2:
KBNAContents[resPtrCounter] = options->animationResults[k]->dataT.index & 0xff;
KBNAContents[resPtrCounter + 1] = options->animationResults[k]->dataT.index >> 8;
KBNAContents[resPtrCounter + 2] = 0xEF;
KBNAContents[resPtrCounter + 3] = 0xBE;
KBNAContents[resPtrCounter + 4] = options->animationResults[k]->dataT.positionX & 0xff;
KBNAContents[resPtrCounter + 5] = options->animationResults[k]->dataT.positionX >> 8;
KBNAContents[resPtrCounter + 6] = options->animationResults[k]->dataT.positionY & 0xff;
KBNAContents[resPtrCounter + 7] = options->animationResults[k]->dataT.positionY >> 8;
resPtrCounter += 0x8;
break;
}
// use the `padded` flag which was stored earlier to inject padding
if (options->animationResults[k]->padded)
{
KBNAContents[resPtrCounter] = 0xCC;
KBNAContents[resPtrCounter + 1] = 0xCC;
resPtrCounter += 0x2;
}
}
fwrite(KBNAContents, 1, contentsSize, fp);
free(KBNAContents);
if (options->labelEnabled)
{
unsigned int lablSize = 8;
for (int j = 0; j < options->labelCount; j++)
{
lablSize += (unsigned)strlen(options->labels[j]) + 5;
}
unsigned char *labl = malloc(lablSize);
memset(labl, 0, lablSize);
strcpy((char *) labl, "LBAL");
labl[4] = lablSize & 0xff;
labl[5] = lablSize >> 8;
unsigned int position = 0;
i = 0;
for (int j = 0; j < options->labelCount; j++)
{
labl[i + 8] = position & 0xff;
labl[i + 9] = position >> 8;
position += (unsigned)strlen(options->labels[j]) + 1;
i += 4;
}
for (int j = 0; j < options->labelCount; j++)
{
strcpy((char *) (labl + (i + 8)), options->labels[j]);
i += (int)strlen(options->labels[j]) + 1;
}
fwrite(labl, 1, lablSize, fp);
free(labl);
if(!options->multiCell)
{
unsigned char txeu[0xc] = {0x54, 0x58, 0x45, 0x55, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
fwrite(txeu, 1, 0xc, fp);
}
}
fclose(fp);
}