tools: Implement processor for resdat files

This commit is contained in:
Rachel
2026-03-18 21:01:04 -07:00
parent 2c06aacabb
commit d6bb75e803
3 changed files with 911 additions and 0 deletions

View File

@@ -0,0 +1,333 @@
{
"characters": [
{
"id": "PartyMenu_MemberPokeBall_Chars",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "member_ball_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_Cursor_Chars",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "cursor_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_Button_Chars",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "button_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_StatusIcons_Chars",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PST_GRA",
"from_file": "status_icons_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_MemberIcon0_Chars",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "icon_00000_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_MemberIcon1_Chars",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "icon_00000_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_MemberIcon2_Chars",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "icon_00000_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_MemberIcon3_Chars",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "icon_00000_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_MemberIcon4_Chars",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "icon_00000_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_MemberIcon5_Chars",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "icon_00000_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_HeldIcons_Chars",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "icons_NCGR",
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_TouchButtonEffect_Chars",
"from_narc": "NARC_INDEX_GRAPHIC__PL_BAG_GRA",
"from_file": "button_shockwave_NCGR",
"compressed": false,
"on_main_screen": false,
"on_sub_screen": true
}
],
"palettes": [
{
"id": "PartyMenu_Shared_Palette",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "shared_NCLR",
"load_in_slot": 2,
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_StatusIcons_Palette",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PST_GRA",
"from_file": "status_icons_NCLR",
"load_in_slot": 1,
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_MemberIcons_Palette",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "shared_pals_NCLR",
"load_in_slot": 3,
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_HeldIcons_Palette",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "icons_NCLR",
"load_in_slot": 1,
"compressed": false,
"on_main_screen": true,
"on_sub_screen": false
},
{
"id": "PartyMenu_TouchButtonEffect_Palette",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "touch_button_shockwave_NCLR",
"load_in_slot": 1,
"compressed": false,
"on_main_screen": false,
"on_sub_screen": true
}
],
"cells": [
{
"id": "PartyMenu_MemberPokeBall_Cells",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "member_ball_cell_NCER",
"compressed": false
},
{
"id": "PartyMenu_Cursor_Cells",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "cursor_cell_NCER",
"compressed": false
},
{
"id": "PartyMenu_Button_Cells",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "button_cell_NCER",
"compressed": false
},
{
"id": "PartyMenu_StatusIcons_Cells",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PST_GRA",
"from_file": "status_icons_cell_NCER",
"compressed": false
},
{
"id": "PartyMenu_MemberIcons_Cells",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "shared_cell_32k_NCER",
"compressed": false
},
{
"id": "PartyMenu_HeldIcons_Cells",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "icons_cell_NCER",
"compressed": false
},
{
"id": "PartyMenu_TouchButtonEffect_Cells",
"from_narc": "NARC_INDEX_GRAPHIC__PL_BAG_GRA",
"from_file": "button_shockwave_cell_NCER",
"compressed": false
}
],
"animations": [
{
"id": "PartyMenu_MemberPokeBall_Anims",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "member_ball_anim_NANR",
"compressed": false
},
{
"id": "PartyMenu_Cursor_Anims",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "cursor_anim_NANR",
"compressed": false
},
{
"id": "PartyMenu_Button_Anims",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "button_anim_NANR",
"compressed": false
},
{
"id": "PartyMenu_StatusIcons_Anims",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PST_GRA",
"from_file": "status_icons_anim_NANR",
"compressed": false
},
{
"id": "PartyMenu_MemberIcons_Anims",
"from_narc": "NARC_INDEX_POKETOOL__ICONGRA__PL_POKE_ICON",
"from_file": "shared_anim_32k_NANR",
"compressed": false
},
{
"id": "PartyMenu_HeldIcons_Anims",
"from_narc": "NARC_INDEX_GRAPHIC__PL_PLIST_GRA",
"from_file": "icons_anim_NANR",
"compressed": false
},
{
"id": "PartyMenu_TouchButtonEffect_Anims",
"from_narc": "NARC_INDEX_GRAPHIC__PL_BAG_GRA",
"from_file": "button_shockwave_anim_NANR",
"compressed": false
}
],
"templates": [
{
"characters": "PartyMenu_MemberPokeBall_Chars",
"palette": "PartyMenu_Shared_Palette",
"cells": "PartyMenu_MemberPokeBall_Cells",
"animations": "PartyMenu_MemberPokeBall_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_Cursor_Chars",
"palette": "PartyMenu_Shared_Palette",
"cells": "PartyMenu_Cursor_Cells",
"animations": "PartyMenu_Cursor_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_Button_Chars",
"palette": "PartyMenu_Shared_Palette",
"cells": "PartyMenu_Button_Cells",
"animations": "PartyMenu_Button_Anims",
"vram_transfer": false,
"priority": 2
},
{
"characters": "PartyMenu_StatusIcons_Chars",
"palette": "PartyMenu_StatusIcons_Palette",
"cells": "PartyMenu_StatusIcons_Cells",
"animations": "PartyMenu_StatusIcons_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_MemberIcon0_Chars",
"palette": "PartyMenu_MemberIcons_Palette",
"cells": "PartyMenu_MemberIcons_Cells",
"animations": "PartyMenu_MemberIcons_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_MemberIcon1_Chars",
"palette": "PartyMenu_MemberIcons_Palette",
"cells": "PartyMenu_MemberIcons_Cells",
"animations": "PartyMenu_MemberIcons_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_MemberIcon2_Chars",
"palette": "PartyMenu_MemberIcons_Palette",
"cells": "PartyMenu_MemberIcons_Cells",
"animations": "PartyMenu_MemberIcons_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_MemberIcon3_Chars",
"palette": "PartyMenu_MemberIcons_Palette",
"cells": "PartyMenu_MemberIcons_Cells",
"animations": "PartyMenu_MemberIcons_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_MemberIcon4_Chars",
"palette": "PartyMenu_MemberIcons_Palette",
"cells": "PartyMenu_MemberIcons_Cells",
"animations": "PartyMenu_MemberIcons_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_MemberIcon5_Chars",
"palette": "PartyMenu_MemberIcons_Palette",
"cells": "PartyMenu_MemberIcons_Cells",
"animations": "PartyMenu_MemberIcons_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_HeldIcons_Chars",
"palette": "PartyMenu_HeldIcons_Palette",
"cells": "PartyMenu_HeldIcons_Cells",
"animations": "PartyMenu_HeldIcons_Anims",
"vram_transfer": false,
"priority": 1
},
{
"characters": "PartyMenu_TouchButtonEffect_Chars",
"palette": "PartyMenu_TouchButtonEffect_Palette",
"cells": "PartyMenu_TouchButtonEffect_Cells",
"animations": "PartyMenu_TouchButtonEffect_Anims",
"vram_transfer": false,
"priority": 0
}
]
}

View File

@@ -57,3 +57,14 @@ speciesproc_exe = executable(
dependencies: commonproc_dep,
native: true,
)
resdatproc_exe = executable(
'resdatproc',
sources: [ files('src/resdatproc.c') ],
c_args: dataproc_cflags,
include_directories: [ public_includes, toplevel_includes ],
dependencies: commonproc_dep,
native: true,
)

View File

@@ -0,0 +1,567 @@
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "common.h"
#include "dataproc.h"
#define MAX_LOADED_NAIX 16
static enum_template_t enums[MAX_LOADED_NAIX + 1] = {
{ .from_file = "constants/narc.h", .for_type = "enum NarcID" },
};
enum restype {
RESDAT_T_CHARS,
RESDAT_T_PLTTS,
RESDAT_T_CELLS,
RESDAT_T_ANIMS,
RESDAT_T_TEMPLATES,
};
typedef struct resource resource_t;
struct resource {
const char *id;
const char *from_narc;
unsigned from_file_id;
unsigned from_narc_id;
unsigned palette_slot;
bool dummy;
bool compressed;
bool on_main;
bool on_sub;
};
typedef struct resources resources_t;
struct resources {
resource_t *arr;
lookup_t *map;
size_t len;
enum restype type;
};
typedef struct template template_t;
struct template {
const char *id;
u32 char_id;
u32 pltt_id;
u32 cell_id;
u32 anim_id;
bool transfer;
unsigned priority;
};
typedef struct templates templates_t;
struct templates {
template_t *arr;
size_t len;
};
static void parse_args(int *pargc, char ***pargv);
static resources_t proc_resources(datafile_t *df, enum restype type);
static templates_t proc_templates(
datafile_t *df,
resources_t *chars,
resources_t *pltts,
resources_t *cells,
resources_t *anims
);
static int dump_resources(resources_t *resources, enum restype type);
static int dump_templates(templates_t *templates);
static int dump_header(templates_t *templates);
static void free_resources(resources_t *resources);
static char *program_name = NULL;
static char *filename = NULL;
static char *depfile_path = NULL;
static char *output_prefix = "";
static char *output_dir = ".";
static size_t arr_index = 0;
static size_t len_prefix = 0;
static bool create_index = false;
static bool free_depfile = false;
static enum restype filter = 0;
#define SOURCE_NAME "tools/dataproc/src/resdatproc.c"
int main(int argc, char **argv) {
parse_args(&argc, &argv);
common_init(DATAPROC_F_JSON, enums, NULL, NULL, SOURCE_NAME, depfile_path, output_dir, NULL, NULL);
datafile_t df = { 0 };
unsigned errc = EXIT_SUCCESS;
if (dp_load(&df, filename) == 0) {
resources_t chars = proc_resources(&df, RESDAT_T_CHARS);
resources_t pltts = proc_resources(&df, RESDAT_T_PLTTS);
resources_t cells = proc_resources(&df, RESDAT_T_CELLS);
resources_t anims = proc_resources(&df, RESDAT_T_ANIMS);
templates_t temps = proc_templates(&df, &chars, &pltts, &cells, &anims);
if (df.diag_head == NULL) {
errc = dump_resources(&chars, RESDAT_T_CHARS)
|| dump_resources(&pltts, RESDAT_T_PLTTS)
|| dump_resources(&cells, RESDAT_T_CELLS)
|| dump_resources(&anims, RESDAT_T_ANIMS)
|| dump_templates(&temps)
|| (create_index ? dump_header(&temps) : EXIT_SUCCESS);
}
free_resources(&chars);
free_resources(&pltts);
free_resources(&cells);
free_resources(&anims);
free(temps.arr);
}
if (dp_report(&df) == DIAG_ERROR) errc = EXIT_FAILURE;
if (free_depfile) free(depfile_path);
dp_free(&df);
return common_done(errc, NULL);
}
int sort_strcmp(const void *lhs, const void *rhs) {
const lookup_t *l = lhs;
const lookup_t *r = rhs;
if (l->def == NULL || r->def == NULL) return 0;
return strcmp(l->def, r->def);
}
void free_resources(resources_t *res) {
free(res->arr);
free(res->map);
}
static inline bool has_vram(enum restype type) {
return type == RESDAT_T_PLTTS || type == RESDAT_T_CHARS;
}
static inline bool has_slot(enum restype type) {
return type == RESDAT_T_PLTTS;
}
static resources_t proc_resources(datafile_t *df, enum restype type) {
datanode_t array = { 0 };
switch (type) {
case RESDAT_T_PLTTS: array = dp_get(df, ".palettes"); break;
case RESDAT_T_CHARS: array = dp_get(df, ".characters"); break;
case RESDAT_T_CELLS: array = dp_get(df, ".cells"); break;
case RESDAT_T_ANIMS: array = dp_get(df, ".animations"); break;
default: abort(); // unreachable
}
const size_t array_len = dp_arrlen(array);
if (array_len > INT32_MAX) {
dp_error(&array, "number of elements exceeds INT32_MAX = %d", INT32_MAX);
return (resources_t){ 0 };
}
resources_t result = {
.arr = calloc(array_len, sizeof(*result.arr)),
.map = calloc(array_len, sizeof(*result.map)),
.len = array_len,
.type = type,
};
for (size_t i = 0; i < array_len; i++) {
datanode_t node = dp_arrelem(array, i);
if (dp_hasmemb(node, "dummy") && dp_bool(dp_objmemb(node, "dummy"))) {
result.arr[i].dummy = true;
continue;
}
datanode_t node_narc = dp_objmemb(node, "from_narc");
datanode_t node_file = dp_objmemb(node, "from_file");
datanode_t node_narc_i = dp_lookup(node_narc, "enum NarcID");
result.arr[i].id = dp_string(dp_objmemb(node, "id"));
result.arr[i].compressed = dp_bool(dp_objmemb(node, "compressed"));
result.arr[i].from_narc = dp_string(node_narc);
result.arr[i].from_narc_id = (unsigned)node_narc_i.mapped;
if (result.arr[i].from_narc && node_narc_i.type == DATAPROC_T_MAPPED) {
if (node_file.type == DATAPROC_T_ARRAY) node_file = dp_arrelem(node_file, arr_index);
datanode_t node_file_i = dp_lookup(node_file, result.arr[i].from_narc);
if (node_file_i.type == DATAPROC_T_MAPPED) {
result.arr[i].from_file_id = (unsigned)node_file_i.mapped;
}
else {
dp_note(&node_narc, "type '%s' defined here",
result.arr[i].from_narc);
}
}
if (has_vram(type)) {
result.arr[i].on_main = dp_bool(dp_objmemb(node, "on_main_screen"));
result.arr[i].on_sub = dp_bool(dp_objmemb(node, "on_sub_screen"));
}
if (has_slot(type)) {
result.arr[i].palette_slot = dp_u8range(dp_objmemb(node, "load_in_slot"), 0, 15);
}
result.map[i].def = result.arr[i].id;
result.map[i].val = i;
}
qsort(result.map, result.len, sizeof(*result.map), sort_strcmp);
return result;
}
static inline void putleu32(void *p, u32 u) {
u8 *b = p;
b[0] = (u8)((u >> 0) & 0xFF);
b[1] = (u8)((u >> 8) & 0xFF);
b[2] = (u8)((u >> 16) & 0xFF);
b[3] = (u8)((u >> 24) & 0xFF);
}
static inline char* prefix(const char *base) {
size_t len_base = strlen(base);
char *prefixed = calloc(len_prefix + 1 + len_base + 1, sizeof(*prefixed));
memcpy(prefixed, output_prefix, len_prefix);
memcpy(prefixed + len_prefix + 1, base, len_base);
prefixed[len_prefix] = '_';
return prefixed;
}
static inline bool is_disabled(enum restype type) {
return filter != 0 && (filter & (1 << type)) == 0;
}
#define SENTINEL 0xFFFFFFFE
#define SENTINEL_RES (serial_t){ SENTINEL, SENTINEL, SENTINEL, SENTINEL, SENTINEL, SENTINEL }
#define SENTINEL_TMP (serial_t){ SENTINEL, SENTINEL, SENTINEL, SENTINEL, SENTINEL, SENTINEL, SENTINEL, SENTINEL }
static int dump_resources(resources_t *resources, enum restype type) {
if (is_disabled(type)) return EXIT_SUCCESS;
typedef struct serial serial_t;
struct serial {
u32 narc;
u32 member;
u32 is_compressed;
u32 id;
u32 screen_mask;
u32 palette_slot;
};
char *output_path = NULL;
switch (type) {
case RESDAT_T_CHARS: output_path = prefix("chars.resdat"); break;
case RESDAT_T_PLTTS: output_path = prefix("pltts.resdat"); break;
case RESDAT_T_CELLS: output_path = prefix("cells.resdat"); break;
case RESDAT_T_ANIMS: output_path = prefix("anims.resdat"); break;
default: abort(); // unreachable
}
size_t size = sizeof(u32) + (sizeof(serial_t) * (resources->len + 1));
void *data = calloc(size, 1);
if (data == NULL) return EXIT_FAILURE;
putleu32(data, type);
serial_t *ser = (void *)(((u8 *)data) + sizeof(u32));
size_t ser_count = 0; // Only serialize non-dummy entries
for (size_t i = 0; i < resources->len; i++) {
resource_t *res = &resources->arr[i];
if (res->dummy) continue;
ser[ser_count++] = (serial_t){
.narc = res->from_narc_id,
.member = res->from_file_id,
.is_compressed = res->compressed ? 1 : 0,
.id = (u32)i,
.screen_mask = has_vram(type) ? (res->on_main ? 1 : 0) | (res->on_sub ? 2 : 0) : res->from_narc_id,
.palette_slot = has_slot(type) ? res->palette_slot : res->from_narc_id,
};
}
ser[ser_count++] = SENTINEL_RES;
size = sizeof(u32) + (sizeof(serial_t) * ser_count);
int errc = fdump_blob(data, size, output_path);
free(data);
free(output_path);
return errc;
}
static int check_lookup(const void *key_v, const void *memb_v) {
const char *key = key_v;
const lookup_t *memb = memb_v;
return strcmp(key, memb->def);
}
static lookup_t* find_resource(datanode_t parent, const char *key, resources_t *resources) {
datanode_t node = dp_objmemb(parent, key);
const char *str = dp_string(node);
if (str == NULL) return NULL;
lookup_t *found = bsearch(str, resources->map, resources->len, sizeof(*resources->map), check_lookup);
if (!found) {
dp_error(&node, "undefined %s-resource identifier", key);
return NULL;
}
return found;
}
static templates_t proc_templates(
datafile_t *df,
resources_t *chars,
resources_t *pltts,
resources_t *cells,
resources_t *anims
) {
datanode_t array = dp_get(df, ".templates");
const size_t array_len = dp_arrlen(array);
templates_t result = {
.arr = calloc(array_len, sizeof(*result.arr)),
.len = array_len,
};
for (size_t i = 0; i < array_len; i++) {
datanode_t node = dp_arrelem(array, i);
lookup_t *match_char = find_resource(node, "characters", chars);
lookup_t *match_pltt = find_resource(node, "palette", pltts);
lookup_t *match_cell = find_resource(node, "cells", cells);
lookup_t *match_anim = find_resource(node, "animations", anims);
// We want to assert that these keys are present even if a resource-identifier is undefined.
const char *id = dp_string(dp_objmemb(node, "id"));
bool transfer = dp_bool(dp_objmemb(node, "vram_transfer"));
unsigned priority = (unsigned)dp_int(dp_objmemb(node, "priority"));
if (match_char && match_pltt && match_cell && match_anim) {
result.arr[i] = (template_t){
.id = id,
.char_id = (u32)match_char->val,
.pltt_id = (u32)match_pltt->val,
.cell_id = (u32)match_cell->val,
.anim_id = (u32)match_anim->val,
.transfer = transfer,
.priority = priority,
};
}
}
return result;
}
static int dump_templates(templates_t *templates) {
if (is_disabled(RESDAT_T_TEMPLATES)) return EXIT_SUCCESS;
typedef struct serial serial_t;
struct serial {
s32 char_id;
s32 pltt_id;
s32 cell_id;
s32 anim_id;
s32 mcel_id; // unused by gen4 games
s32 mani_id; // unused by gen4 games
s32 transfer;
s32 priority;
};
serial_t *ser = calloc(templates->len + 1, sizeof(*ser));
if (ser == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < templates->len; i++) {
template_t *tmp = &templates->arr[i];
ser[i] = (serial_t){
.char_id = tmp->char_id,
.pltt_id = tmp->pltt_id,
.cell_id = tmp->cell_id,
.anim_id = tmp->anim_id,
.mcel_id = -1,
.mani_id = -1,
.transfer = tmp->transfer ? 1 : 0,
.priority = tmp->priority,
};
}
ser[templates->len] = SENTINEL_TMP;
char *output_path = "templates.cldat";
if (*output_prefix != 0) {
size_t size = len_prefix + sizeof(".cldat") + 1;
output_path = calloc(size, sizeof(*output_path));
snprintf(output_path, size, "%s.cldat", output_prefix);
}
int errc = fdump_blob(ser, sizeof(serial_t) * (templates->len + 1), output_path);
free(ser);
if (*output_prefix != 0) free(output_path);
return errc;
}
static int dump_header(templates_t *templates) {
if (is_disabled(RESDAT_T_TEMPLATES)) return EXIT_SUCCESS;
char *output_path = "templates.h";
if (*output_prefix != 0) {
size_t size = len_prefix + sizeof(".h") + 1;
output_path = calloc(size, sizeof(*output_path));
snprintf(output_path, size, "%s.h", output_prefix);
}
int errc = EXIT_SUCCESS;
char *h_path = pathjoin(output_dir, NULL, output_path);
char *guard = guardify(h_path);
FILE *header = fopen(h_path, "wb");
if (header == NULL) {
fprintf(stderr, "could not open output file '%s': %s\n", h_path, strerror(errno));
errc = EXIT_FAILURE;
goto cleanup;
}
fprintf(header, "#ifndef %s\n", guard);
fprintf(header, "#define %s\n", guard);
fputc('\n', header);
for (size_t i = 0; i < templates->len; i++) {
fprintf(header, "#define %s %zu\n", templates->arr[i].id, i);
}
fputc('\n', header);
fprintf(header, "#endif // %s\n", guard);
fclose(header);
cleanup:
free(h_path);
free(guard);
if (*output_prefix != 0) free(output_path);
return errc;
}
static void usage(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void parse_args(int *pargc, char ***pargv) {
program_name = (*pargv)[0];
opterr = 0;
int num_naix = 0, c = 0;
while ((c = getopt(*pargc, *pargv, "D:f:n:o:p:M:Nh")) != -1) {
switch (c) {
case 'D':
if (num_naix == MAX_LOADED_NAIX) {
usage("too many '-D' options; max is %d", MAX_LOADED_NAIX);
}
char *pargsep = strchr(optarg, ':');
if (!pargsep) {
usage("missing ':' separator in argument '%s'", optarg);
}
// NOTE: ideally this would produce an error if the NARC constant is
// unknown, but that requires loading `constants/narc.h` independently
// of the common init routines, which is a big scope increase.
*pargsep = 0;
enums[++num_naix] = (enum_template_t){
.from_file = pargsep + 1,
.with_prefix = NULL,
.for_type = optarg,
.from_defs = true,
};
break;
case 'f':
if (strcmp(optarg, "characters") == 0) { filter |= (1 << RESDAT_T_CHARS); break; }
if (strcmp(optarg, "palettes") == 0) { filter |= (1 << RESDAT_T_PLTTS); break; }
if (strcmp(optarg, "cells") == 0) { filter |= (1 << RESDAT_T_CELLS); break; }
if (strcmp(optarg, "animations") == 0) { filter |= (1 << RESDAT_T_ANIMS); break; }
if (strcmp(optarg, "templates") == 0) { filter |= (1 << RESDAT_T_TEMPLATES); break; }
usage("invalid argument '%s' for option '-%c'", optarg, optopt);
break;
case 'n':
if (optarg[0] == '-') usage("argument for option '-%c' must be a nonnegative integer", optopt);
char *endp = NULL;
arr_index = strtol(optarg, &endp, 0);
if (*endp) usage("argument for option '-%c' must be a nonnegative integer", optopt);
break;
case 'o': output_dir = optarg; break;
case 'p': output_prefix = optarg; break;
case 'M': depfile_path = optarg; break;
case 'N': create_index = true; break;
case 'h': usage(NULL); break;
case ':': usage("missing argument for '-%c'", optopt); break;
default: usage("unknown option '-%c'", optopt); break;
}
}
*pargc -= optind;
*pargv += optind;
if (*pargc < 1) usage("missing required argument RESDAT_JSON");
filename = (*pargv)[0];
if (depfile_path == NULL) {
char *tmp = prefix("resdat.d");
depfile_path = pathjoin(output_dir, NULL, tmp);
free_depfile = true;
free(tmp);
}
len_prefix = strlen(output_prefix);
}
static void usage(const char *fmt, ...) {
FILE *f = stdout;
if (fmt != NULL) {
f = stderr;
va_list args;
va_start(args, fmt);
fputs(program_name, f);
fputs(": ", f);
vfprintf(f, fmt, args);
fputc('\n', f);
va_end(args);
}
#define fputf(fmt, ...) fprintf(f, fmt, __VA_ARGS__)
fputf("usage: %s [-D NARC_ID:PATH] [-o OUTDIR] [-N] RESDAT_JSON\n", program_name);
fputs("\n", f);
fputs("options:\n", f);
fputs(" -D NARC_ID:PATH Load PATH as a set of enum-constants for the members of\n", f);
fputf(" NARC_ID. Up to %d instances may be given.\n", MAX_LOADED_NAIX);
fputs(" -o OUTDIR Write output files to OUTDIR.\n", f);
fputs(" -p PREFIX Prefix all output '.resdat' files with PREFIX, followed by\n", f);
fputs(" an underscore '_'. PREFIX will also be used as the filename\n", f);
fputs(" stem of the output '.cldat' file and -- when the '-N' option\n", f);
fputs(" is present -- the output '.h' file.\n", f);
fputs(" -f TYPE Filter the output-types that will generated. By default,\n", f);
fputs(" all possible outputs are generated.\n", f);
fputs(" TYPE must be one of 'characters', 'palettes', 'cells',\n", f);
fputs(" 'animations', or 'templates'.\n", f);
fputs(" -n INDEX Specify the index of any array-type 'from_file' properties\n", f);
fputs(" in the input JSON which will be used for the output.\n", f);
fputs(" Defaults to 0.\n", f);
fputs(" -N Create a C-style index-header for defined templates.\n", f);
fputs(" -M DEPFILE Specify the full path to an output dependency file.\n", f);
fputs(" Defaults to 'resdat.d'.\n", f);
#undef fputf
exit(f == stdout ? EXIT_SUCCESS : EXIT_FAILURE);
}