summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile42
-rw-r--r--spec/ability.fspec11
-rw-r--r--spec/ftable.fspec3
-rw-r--r--spec/item.fspec28
-rw-r--r--spec/model.fspec25
-rw-r--r--spec/name.fspec5
-rw-r--r--spec/spell.fspec18
-rw-r--r--spec/vtable.fspec3
-rw-r--r--src/dump.c310
-rw-r--r--src/ragel/fspec.h77
-rw-r--r--src/ragel/fspec.rl329
-rw-r--r--src/ragel/ragel.h236
-rw-r--r--src/utils/dec2bin.c40
-rw-r--r--src/xi/xi2path.c16
-rw-r--r--src/xi/xi2path.h35
-rw-r--r--src/xi/xidec.c136
-rw-r--r--src/xi/xifile.c169
-rw-r--r--src/xi/xils.c94
-rw-r--r--vim/filespec.vim56
19 files changed, 1633 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0a8aaf8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,42 @@
+PREFIX ?= /usr/local
+bindir ?= /bin
+
+WARNINGS := -Wall -Wextra -Wformat=2 -Winit-self -Wfloat-equal -Wcast-align -Wpointer-arith
+CFLAGS += -std=c11 $(WARNINGS)
+
+all: fspec-dump dec2bin xidec xi2path xils xifile
+
+%.c: %.rl
+ ragel $^
+
+fspec-dump: src/ragel/ragel.h src/ragel/fspec.h src/ragel/fspec.c src/dump.c
+ $(LINK.c) $(filter %.c,$^) $(LDLIBS) -o $@
+
+dec2bin: src/utils/dec2bin.c
+ $(LINK.c) $(filter %.c,$^) $(LDLIBS) -o $@
+
+xidec: src/xi/xidec.c
+ $(LINK.c) $(filter %.c,$^) $(LDLIBS) -o $@
+
+xi2path: src/xi/xi2path.c
+ $(LINK.c) $(filter %.c,$^) $(LDLIBS) -o $@
+
+xils: src/xi/xils.c
+ $(LINK.c) $(filter %.c,$^) $(LDLIBS) -o $@
+
+xifile: src/xi/xifile.c
+ $(LINK.c) $(filter %.c,$^) $(LDLIBS) -o $@
+
+install:
+ install -Dm755 $(DESTDIR)$(PREFIX)$(bindir)/fspec-dump
+ install -Dm755 $(DESTDIR)$(PREFIX)$(bindir)/dec2bin
+ install -Dm755 $(DESTDIR)$(PREFIX)$(bindir)/xidec
+ install -Dm755 $(DESTDIR)$(PREFIX)$(bindir)/xi2path
+ install -Dm755 $(DESTDIR)$(PREFIX)$(bindir)/xils
+ install -Dm755 $(DESTDIR)$(PREFIX)$(bindir)/xifile
+
+clean:
+ $(RM) src/ragel/fspec.c
+ $(RM) fspec-dump dec2bin xidec xi2path xils xifile
+
+.PHONY: all clean install
diff --git a/spec/ability.fspec b/spec/ability.fspec
new file mode 100644
index 0000000..3c2c890
--- /dev/null
+++ b/spec/ability.fspec
@@ -0,0 +1,11 @@
+// Abilities
+struct ability {
+ u16 index;
+ u16 icon_id;
+ u16 mp_cost;
+ u16 unknown;
+ u16 targets;
+ u8 name[32] = sjis; // The kind actually depends on ROM section
+ u8 description[256] = sjis; // ^ Ditto, we probably can't express this
+ u8 padding[726] = pad;
+};
diff --git a/spec/ftable.fspec b/spec/ftable.fspec
new file mode 100644
index 0000000..615b7b3
--- /dev/null
+++ b/spec/ftable.fspec
@@ -0,0 +1,3 @@
+struct ftable {
+ u16 id;
+};
diff --git a/spec/item.fspec b/spec/item.fspec
new file mode 100644
index 0000000..c4d1767
--- /dev/null
+++ b/spec/item.fspec
@@ -0,0 +1,28 @@
+struct string_info {
+ u32 offset;
+ u32 flags;
+};
+
+struct strings {
+ u32 nmemb;
+ struct string_info info[nmemb];
+};
+
+struct item {
+ u32 id;
+ u16 flags;
+ u16 stack;
+ u16 type;
+ u16 resource;
+ u16 targets;
+
+ union data (type) {
+ 4 => struct weapon weapon;
+ 5 => struct armor armor;
+ 7 => struct usable usable;
+ 12 => struct puppet puppet;
+ * => struct general general;
+ };
+
+ struct strings strings;
+};
diff --git a/spec/model.fspec b/spec/model.fspec
new file mode 100644
index 0000000..afa8281
--- /dev/null
+++ b/spec/model.fspec
@@ -0,0 +1,25 @@
+struct texture {
+ u8 type;
+ u8 name[16] = ascii;
+ u32 version;
+ u32 width;
+ u32 height;
+ u32 unknown[6];
+ u32 bits_per_pal_clr;
+};
+
+struct geometry {
+ u16 unknown;
+ u16 vert_and_bone_ref_flag;
+ u16 mirror;
+ u32 draw_data_offset;
+ u16 draw_data_size;
+ u32 bone_ref_offset;
+ u16 bone_ref_count;
+ u32 weighted_vert_count_offset;
+ u16 max_weights_per_vertex;
+ u32 weighted_data_offset;
+ u16 weighted_data_count;
+ u32 vertex_data_offset;
+ u16 vertex_data_size;
+};
diff --git a/spec/name.fspec b/spec/name.fspec
new file mode 100644
index 0000000..69f75de
--- /dev/null
+++ b/spec/name.fspec
@@ -0,0 +1,5 @@
+// NPC IDs
+struct name = {
+ u8 name[28] = ascii; // The kind actually depends on ROM section
+ u32 id;
+};
diff --git a/spec/spell.fspec b/spec/spell.fspec
new file mode 100644
index 0000000..f65b5ad
--- /dev/null
+++ b/spec/spell.fspec
@@ -0,0 +1,18 @@
+struct spell {
+ u16 index;
+ u16 type; // 1-6 for White/Black/Summon/Ninja/Bard/Blue
+ u16 element;
+ u16 targets;
+ u16 skill;
+ u16 mp_cost;
+ u8 casting_time; // in quarter of seconds
+ u8 recast_delay; // in quarter of seconds
+ u8 level[24]; // 1 byte per job, 0xxFF if not learnable, first slot is NONE job so always 0xFF
+ u16 id; // 0 for "unused" spells; often, but not always, equal to index
+ u8 unknown;
+ u8 jp_name[20] = sjis;
+ u8 en_name[20] = ascii;
+ u8 jp_description[128] = sjis;
+ u8 en_description[128] = ascii;
+ u8 padding[687] = pad;
+};
diff --git a/spec/vtable.fspec b/spec/vtable.fspec
new file mode 100644
index 0000000..0fc8701
--- /dev/null
+++ b/spec/vtable.fspec
@@ -0,0 +1,3 @@
+struct vtable {
+ u8 exist;
+};
diff --git a/src/dump.c b/src/dump.c
new file mode 100644
index 0000000..641bb55
--- /dev/null
+++ b/src/dump.c
@@ -0,0 +1,310 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <limits.h>
+#include <string.h>
+#include <assert.h>
+#include <err.h>
+
+#include <iconv.h>
+#include <locale.h>
+#include <langinfo.h>
+
+#include "ragel/fspec.h"
+
+static size_t
+to_hex(const char *buf, const size_t buf_sz, char *out, const size_t out_sz, const bool reverse)
+{
+ assert(out);
+ const char nibble[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+ const uint8_t nbs = sizeof(nibble) - 1;
+
+ size_t w = 0;
+ for (size_t i = 0; i < buf_sz && out_sz > 2 && w < out_sz - 2; ++i) {
+ for (uint8_t c = 0; c < CHAR_BIT / 8 && w < out_sz; ++c) {
+ const size_t idx = (reverse ? (buf_sz - 1) - i : i);
+ const uint8_t hi = (buf[idx] >> (4 * (c + 1))) & nbs;
+ const uint8_t lo = (buf[idx] >> (8 * c)) & nbs;
+ out[w++] = nibble[hi];
+ out[w++] = nibble[lo];
+ }
+ }
+
+ assert(w < out_sz);
+ out[w] = 0;
+ return w;
+}
+
+static void
+print_decimal(const char *buf, const bool is_signed, const size_t size, const size_t nmemb)
+{
+ if (nmemb > 1)
+ printf("{ ");
+
+ for (size_t n = 0; n < nmemb; ++n) {
+ char hex[2 * sizeof(uint64_t) + 1];
+ to_hex(buf + size * n, size, hex, sizeof(hex), true);
+ const char *delim = (nmemb > 1 && n + 1 < nmemb ? ", " : "");
+
+ if (is_signed) {
+ printf("%ld%s", (int64_t)strtoll(hex, NULL, 16), delim);
+ } else {
+ printf("%lu%s", (uint64_t)strtoull(hex, NULL, 16), delim);
+ }
+ }
+
+ printf("%s\n", (nmemb > 1 ? " }" : ""));
+}
+
+static void
+print_hex(const char *buf, const size_t size, const size_t nmemb)
+{
+ if (nmemb > 1)
+ printf("{ ");
+
+ for (size_t n = 0; n < nmemb; ++n) {
+ char hex[2 * sizeof(uint64_t) + 1];
+ to_hex(buf + size * n, size, hex, sizeof(hex), false);
+ printf("%s%s", hex, (nmemb > 1 && n + 1 < nmemb ? ", " : ""));
+ }
+
+ printf("%s\n", (nmemb > 1 ? " }" : ""));
+}
+
+static void
+print_chars(const char *buf, const size_t size, const size_t nmemb)
+{
+ assert(size == sizeof(char));
+
+ for (size_t n = 0; n < nmemb && buf[n] != 0; ++n)
+ printf("%c", buf[n]);
+}
+
+static void
+print_encoded(const char *buf, const char *from, const char *to, const size_t size, const size_t nmemb)
+{
+ assert(from && size == sizeof(char));
+
+ if (!to) {
+ static const char *sys_encoding;
+ if (!sys_encoding) {
+ setlocale(LC_ALL, "");
+ sys_encoding = nl_langinfo(CODESET);
+ }
+
+ to = sys_encoding;
+ }
+
+ iconv_t iv;
+ if ((iv = iconv_open(to, from)) == (iconv_t)-1)
+ err(EXIT_FAILURE, "iconv_open(%s, %s)", to, from);
+
+ const char *in = buf;
+ size_t in_left = nmemb;
+ do {
+ char enc[1024], *out = enc;
+ size_t out_left = sizeof(enc);
+
+ if (iconv(iv, (char**)&in, &in_left, &out, &out_left) == (size_t)-1)
+ err(EXIT_FAILURE, "iconv(%s, %s)", to, from);
+
+ print_chars(enc, 1, sizeof(enc) - out_left);
+ } while (in_left > 0);
+
+ iconv_close(iv);
+ puts("");
+}
+
+struct container;
+struct field {
+ struct fspec_field f;
+ struct container *c, *link;
+ uint64_t value;
+};
+
+struct container {
+ struct fspec_container c;
+ struct field fields[255];
+ size_t num_fields;
+};
+
+static size_t
+field_get_buffer(const struct field *field, FILE *f, char **buf)
+{
+ assert(field && f && buf);
+
+ switch (field->f.array.type) {
+ case FSPEC_ARRAY_FIXED:
+ if (!(*buf = calloc(field->f.array.nmemb, field->f.type.size)))
+ err(EXIT_FAILURE, "calloc(%zu, %zu)", field->f.array.nmemb, field->f.type.size);
+
+ if (fread(*buf, field->f.type.size, field->f.array.nmemb, f) != field->f.array.nmemb)
+ return 0;
+
+ return field->f.array.nmemb;
+
+ case FSPEC_ARRAY_MATCH:
+ {
+ size_t off = 0;
+ const size_t msz = field->f.array.match.size;
+ for (size_t len = 0;; ++off) {
+ if (off >= (len ? len - 1 : len) && !(*buf = realloc(*buf, (len += 1024))))
+ err(EXIT_FAILURE, "realloc(%zu)", len);
+
+ assert(off < len);
+ if (fread(*buf + off, 1, 1, f) != 1)
+ return 0;
+
+ if (off >= msz && !memcmp(field->f.array.match.data, *buf + off - msz, msz))
+ break;
+ }
+
+ (*buf)[off] = 0;
+ return off;
+ }
+ break;
+
+ case FSPEC_ARRAY_VAR:
+ for (size_t i = 0; i < field->c->num_fields; ++i) {
+ if (!strcmp(field->c->fields[i].f.name, field->f.array.var))
+ return field->c->fields[i].value;
+ }
+ break;
+ }
+
+ return 0;
+}
+
+static void
+container_process(struct container *container, FILE *f);
+
+static void
+field_process(struct field *field, FILE *f)
+{
+ assert(field && f);
+
+ char *buf = NULL;
+ const size_t nmemb = field_get_buffer(field, f, &buf);
+
+ if (field->link) {
+ for (size_t i = 0; i < nmemb; ++i)
+ container_process(field->link, f);
+ } else {
+ printf("%s(%zu) %s[%zu] = ", field->f.type.name, field->f.type.size, field->f.name, nmemb);
+
+ if (field->f.kind.flags & FSPEC_KIND_IGNORE) {
+ puts("...");
+ } else if (field->f.kind.flags & FSPEC_KIND_ENCODING) {
+ print_encoded(buf, field->f.kind.name, NULL, field->f.type.size, nmemb);
+ } else if (field->f.kind.flags & FSPEC_KIND_HEXADECIMAL) {
+ print_hex(buf, field->f.type.size, nmemb);
+ } else {
+ print_decimal(buf, (field->f.type.flags & FSPEC_TYPE_SIGNED), field->f.type.size, nmemb);
+ }
+
+ if (nmemb == 1) {
+ char hex[2 * sizeof(uint64_t) + 1];
+ to_hex(buf, field->f.type.size, hex, sizeof(hex), true);
+ field->value = strtoull(hex, NULL, 16);
+ }
+ }
+
+ free(buf);
+}
+
+static void
+container_process(struct container *container, FILE *f)
+{
+ assert(container && f);
+
+ for (size_t i = 0; i < container->num_fields; ++i)
+ field_process(&container->fields[i], f);
+}
+
+#define container_of(ptr, type, member) ((type *)((char *)(1 ? (ptr) : &((type *)0)->member) - offsetof(type, member)))
+
+struct fspec_file {
+ // TODO: Rethink container/field
+ // I think I want just flat structure of key / value pairs in the end
+ // Especially if I want to express members of struct members (e.g. struct a { struct b b; u8 c[b.x]; };)
+ struct container containers[32];
+ struct fspec fspec;
+ FILE *handle;
+ size_t num_containers;
+};
+
+static void
+fspec_field(struct fspec *fspec, const struct fspec_container *container, const struct fspec_field *field)
+{
+ assert(fspec && container);
+ struct fspec_file *f = container_of(fspec, struct fspec_file, fspec);
+
+ if (!f->num_containers || memcmp(container, &f->containers[f->num_containers - 1].c, sizeof(*container)))
+ f->containers[f->num_containers++].c = *container;
+
+ struct container *c = &f->containers[f->num_containers - 1];
+
+ if (field->type.flags & FSPEC_TYPE_CONTAINER) {
+ for (size_t i = 0; i < f->num_containers - 1; ++i) {
+ if (strcmp(field->type.name, f->containers[i].c.name))
+ continue;
+
+ c->fields[c->num_fields].link = &f->containers[i];
+ break;
+ }
+ }
+
+ c->fields[c->num_fields].c = c;
+ c->fields[c->num_fields++].f = *field;
+}
+
+static size_t
+fspec_read(struct fspec *fspec, char *buf, const size_t size, const size_t nmemb)
+{
+ assert(fspec && buf);
+ struct fspec_file *f = container_of(fspec, struct fspec_file, fspec);
+ return fread(buf, size, nmemb, f->handle);
+}
+
+static FILE*
+fopen_or_die(const char *path, const char *mode)
+{
+ assert(path && mode);
+
+ FILE *f;
+ if (!(f = fopen(path, mode)))
+ err(EXIT_FAILURE, "fopen(%s, %s)", path, mode);
+
+ return f;
+}
+
+int
+main(int argc, const char *argv[])
+{
+ if (argc < 2)
+ errx(EXIT_FAILURE, "usage: %s file.spec < data\n", argv[0]);
+
+ uint8_t data[4096] = {0};
+
+ struct fspec_file file = {
+ .fspec = {
+ .ops = {
+ .read = fspec_read,
+ .field = fspec_field,
+ },
+ .mem = {
+ .data = data,
+ .size = sizeof(data),
+ },
+ },
+ .handle = fopen_or_die(argv[1], "rb"),
+ };
+
+ fspec_parse(&file.fspec);
+
+ if (!file.num_containers)
+ errx(EXIT_FAILURE, "'%s' contains no containers", argv[1]);
+
+ container_process(&file.containers[file.num_containers - 1], stdin);
+ fclose(file.handle);
+ return EXIT_SUCCESS;
+}
diff --git a/src/ragel/fspec.h b/src/ragel/fspec.h
new file mode 100644
index 0000000..68998f4
--- /dev/null
+++ b/src/ragel/fspec.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+struct fspec_bytes {
+ const uint8_t *data;
+ size_t size;
+};
+
+enum fspec_kind_bits {
+ FSPEC_KIND_IGNORE = 1<<0,
+ FSPEC_KIND_HEXADECIMAL = 1<<1,
+ FSPEC_KIND_ENCODING = 1<<2,
+};
+
+struct fspec_kind {
+ const char *name;
+ uint32_t flags;
+};
+
+enum fspec_array_type {
+ FSPEC_ARRAY_FIXED,
+ FSPEC_ARRAY_MATCH,
+ FSPEC_ARRAY_VAR,
+};
+
+struct fspec_array {
+ enum fspec_array_type type;
+
+ union {
+ struct fspec_bytes match;
+ const char *var;
+ size_t nmemb;
+ };
+};
+
+enum fspec_type_bits {
+ FSPEC_TYPE_SIGNED = 1<<0,
+ FSPEC_TYPE_CONTAINER = 1<<1,
+};
+
+struct fspec_type {
+ const char *name;
+ size_t size;
+ uint32_t flags;
+};
+
+struct fspec_field {
+ struct fspec_type type;
+ struct fspec_array array;
+ struct fspec_kind kind;
+ const char *name;
+};
+
+struct fspec_container {
+ const char *name;
+};
+
+struct fspec;
+struct fspec {
+ struct {
+ void (*field)(struct fspec *fspec, const struct fspec_container *container, const struct fspec_field *field);
+ size_t (*read)(struct fspec *fspec, char *buf, const size_t size, const size_t nmemb);
+ } ops;
+
+ struct {
+ // XXX: replace with ops.alloc, ops.free
+ // on dump.c we can then just provide implementation that still uses reasonable amount of static memory
+ // but we don't limit the code from working with regular dynamic memory
+ uint8_t *data;
+ size_t size;
+ } mem;
+};
+
+void fspec_parse(struct fspec *fspec);
diff --git a/src/ragel/fspec.rl b/src/ragel/fspec.rl
new file mode 100644
index 0000000..8493cf1
--- /dev/null
+++ b/src/ragel/fspec.rl
@@ -0,0 +1,329 @@
+#include "fspec.h"
+#include "ragel.h"
+
+// It's pretty good base so far.
+// ragel_search_str for typechecking variable delcaration is hack.
+// State should have hashmap for fields/containers.
+//
+// XXX: Maybe drop whole container thing and just give field const char *parent; that points to keypath of container.
+// Then we would have flat structure like, "foo, foo.var, foo.b, ..."
+
+static const struct fspec_container default_container = {0};
+static const struct fspec_field default_field = { .array.nmemb = 1 };
+
+enum stack_type {
+ STACK_VAR,
+ STACK_STR,
+ STACK_NUM,
+};
+
+struct stack {
+ enum stack_type type;
+
+ union {
+ struct fspec_bytes str;
+ const char *var;
+ uint64_t num;
+ };
+};
+
+struct state {
+ struct ragel ragel;
+ struct stack stack;
+ struct fspec_field field;
+ struct fspec_container container;
+ size_t container_data_offset;
+};
+
+static const char*
+stack_type_to_str(const enum stack_type type)
+{
+ switch (type) {
+ case STACK_VAR: return "var";
+ case STACK_STR: return "str";
+ case STACK_NUM: return "num";
+ };
+
+ assert(0 && "should not happen");
+ return "unknown";
+}
+
+static void
+stack_check_type(const struct ragel *ragel, const struct stack *stack, const enum stack_type type)
+{
+ assert(ragel && stack);
+
+ if (stack->type != type)
+ ragel_throw_error(ragel, "tried to get '%s' from stack, but the last pushed type was '%s'", stack_type_to_str(type), stack_type_to_str(stack->type));
+}
+
+static const char*
+stack_get_var(const struct ragel *ragel, const struct stack *stack)
+{
+ assert(ragel && stack);
+ stack_check_type(ragel, stack, STACK_VAR);
+ return stack->var;
+}
+
+static const struct fspec_bytes*
+stack_get_str(const struct ragel *ragel, const struct stack *stack)
+{
+ assert(ragel && stack);
+ stack_check_type(ragel, stack, STACK_STR);
+ return &stack->str;
+}
+
+static uint64_t
+stack_get_num(const struct ragel *ragel, const struct stack *stack)
+{
+ assert(ragel && stack);
+ stack_check_type(ragel, stack, STACK_NUM);
+ return stack->num;
+}
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
+
+static void
+fspec_type_from_str(const struct ragel *ragel, const char *str, struct fspec_type *out_type)
+{
+ assert(ragel && str);
+
+ const struct fspec_type types[] = {
+ { .name = "u8", .size = sizeof(uint8_t) },
+ { .name = "u16", .size = sizeof(uint16_t) },
+ { .name = "u32", .size = sizeof(uint32_t) },
+ { .name = "u64", .size = sizeof(uint64_t) },
+ { .name = "s8", .size = sizeof(int8_t), .flags = FSPEC_TYPE_SIGNED },
+ { .name = "s16", .size = sizeof(int16_t), .flags = FSPEC_TYPE_SIGNED },
+ { .name = "s32", .size = sizeof(int32_t), .flags = FSPEC_TYPE_SIGNED },
+ { .name = "s64", .size = sizeof(int64_t), .flags = FSPEC_TYPE_SIGNED },
+ };
+
+ for (size_t i = 0; i < ARRAY_SIZE(types); ++i) {
+ if (strcmp(str, types[i].name))
+ continue;
+
+ *out_type = types[i];
+ return;
+ }
+
+ if (ragel_search_str(ragel, 0, str)) {
+ *out_type = (struct fspec_type){ .name = str, .flags = FSPEC_TYPE_CONTAINER };
+ return;
+ }
+
+ ragel_throw_error(ragel, "invalid type");
+}
+
+static void
+fspec_kind_from_str(const struct ragel *ragel, const char *str, struct fspec_kind *out_kind)
+{
+ assert(ragel && str);
+
+ const struct fspec_kind kinds[] = {
+ { .name = "pad", .flags = FSPEC_KIND_IGNORE },
+ { .name = "hex", .flags = FSPEC_KIND_HEXADECIMAL },
+ { .name = "ascii", .flags = FSPEC_KIND_ENCODING },
+ { .name = "utf8", .flags = FSPEC_KIND_ENCODING },
+ { .name = "sjis", .flags = FSPEC_KIND_ENCODING },
+ };
+
+ for (size_t i = 0; i < ARRAY_SIZE(kinds); ++i) {
+ if (strcmp(str, kinds[i].name))
+ continue;
+
+ *out_kind = kinds[i];
+ return;
+ }
+
+ ragel_throw_error(ragel, "invalid kind");
+}
+
+static void
+check_field_kind(const struct ragel *ragel, const struct fspec_field *field)
+{
+ assert(ragel && field);
+
+ if ((field->kind.flags & FSPEC_KIND_ENCODING) && field->type.size != sizeof(uint8_t))
+ ragel_throw_error(ragel, "invalid kind: %s kind only allowed for u8 and s8 types", field->kind.name);
+}
+
+%%{
+ # File specification parser.
+
+ machine fspec;
+ variable p state.ragel.p;
+ variable pe state.ragel.pe;
+ variable eof state.ragel.eof;
+ write data noerror nofinal;
+
+ action field {
+ fspec->ops.field(fspec, &state.container, &state.field);
+ }
+
+ action field_kind {
+ fspec_kind_from_str(&state.ragel, stack_get_var(&state.ragel, &state.stack), &state.field.kind);
+ check_field_kind(&state.ragel, &state.field);
+ }
+
+ action field_array {
+ switch (state.stack.type) {
+ case STACK_NUM:
+ state.field.array.type = FSPEC_ARRAY_FIXED;
+ state.field.array.nmemb = stack_get_num(&state.ragel, &state.stack);
+ break;
+
+ case STACK_STR:
+ state.field.array.type = FSPEC_ARRAY_MATCH;
+ state.field.array.match = *stack_get_str(&state.ragel, &state.stack);
+ break;
+
+ case STACK_VAR:
+ state.field.array.type = FSPEC_ARRAY_VAR;
+ state.field.array.var = stack_get_var(&state.ragel, &state.stack);
+
+ if (!ragel_search_str(&state.ragel, state.container_data_offset, state.field.array.var))
+ ragel_throw_error(&state.ragel, "undeclared variable '%s'", state.field.array.var);
+ break;
+
+ default:
+ ragel_throw_error(&state.ragel, "array can't contain the stack type of '%s'", stack_type_to_str(state.stack.type));
+ break;
+ }
+ }
+
+ action field_name {
+ state.field.name = stack_get_var(&state.ragel, &state.stack);
+ }
+
+ action field_type {
+ state.field = default_field;
+ fspec_type_from_str(&state.ragel, stack_get_var(&state.ragel, &state.stack), &state.field.type);
+ }
+
+ action container_name {
+ state.container = default_container;
+ state.container.name = stack_get_var(&state.ragel, &state.stack);
+ state.container_data_offset = state.ragel.mem.cur - state.ragel.mem.data;
+ }
+
+ action push_var {
+ state.stack.type = STACK_VAR;
+ state.stack.var = (char*)state.ragel.mem.cur;
+ }
+
+ action push_hex {
+ state.stack.type = STACK_NUM;
+ state.stack.num = strtoll((char*)state.ragel.mem.cur, NULL, 16);
+ }
+
+ action push_dec {
+ state.stack.type = STACK_NUM;
+ state.stack.num = strtoll((char*)state.ragel.mem.cur, NULL, 10);
+ }
+
+ action push_str {
+ state.stack.type = STACK_STR;
+ state.stack.str.data = state.ragel.mem.cur;
+ state.stack.str.size = (state.ragel.mem.data + state.ragel.mem.written) - state.ragel.mem.cur;
+ }
+
+ action convert_escape {
+ ragel_convert_escape(&state.ragel);
+ }
+
+ action remove {
+ ragel_remove_last_data(&state.ragel);
+ }
+
+ action finish {
+ ragel_finish_data(&state.ragel);
+ }
+
+ action store {
+ ragel_store_data(&state.ragel);
+ }
+
+ action begin {
+ ragel_begin_data(&state.ragel);
+ }
+
+ action invalid_kind {
+ ragel_throw_error(&state.ragel, "invalid kind");
+ }
+
+ action invalid_type {
+ ragel_throw_error(&state.ragel, "invalid type");
+ }
+
+ action error {
+ ragel_throw_error(&state.ragel, "malformed input (machine failed here or in previous or next expression)");
+ }
+
+ action line {
+ ragel_advance_line(&state.ragel);
+ }
+
+ # Semantic
+ ws = space;
+ valid = ^cntrl;
+ es = '\\';
+ delim = ';';
+ quote = ['"];
+ bopen = '{';
+ bclose = '}';
+ newline = '\n';
+ octal = [0-7];
+ hex = '0x' <: xdigit+;
+ decimal = ([1-9] <: digit*) | '0';
+ comment = '//' <: valid* :>> newline;
+ escape = es <: ('x' <: xdigit+ | [abfnrtv\\'"e] | octal{1,3});
+ type = 'u8' | 'u16' | 'u32' | 'u64' | 's8' | 's16' | 's32' | 's64';
+ kind = 'ascii' | 'utf8' | 'sjis' | 'hex' | 'pad';
+ reserved = 'struct' | type | kind;
+ var = ((alpha | '_') <: (alnum | '_')*) - reserved;
+
+ # Catchers
+ catch_var = var >begin $store %finish %push_var;
+ catch_struct = ('struct' $store ws+ >store <: var $store) >begin %finish %push_var;
+ catch_type = (catch_struct | type >begin $store %push_var %remove) $!invalid_type;
+ catch_hex = hex >begin $store %push_hex %remove;
+ catch_decimal = decimal >begin $store %push_dec %remove;
+ catch_string = quote <: (escape %convert_escape | print)* >begin $store %finish %push_str :>> quote;
+ catch_array = '[' <: (catch_hex | catch_decimal | catch_string | catch_var) :>> ']';
+ catch_kind = '=' ws* <: kind >begin $store %push_var %remove $!invalid_kind;
+
+ # Actions
+ field = catch_type %field_type ws+ <: catch_var %field_name ws* <: (catch_array %field_array ws*)? <: (catch_kind %field_kind ws*)? :>> delim %field;
+ container = catch_struct %container_name ws* :>> bopen <: (ws | comment | field)* :>> bclose ws* delim;
+ line = valid* :>> newline @line;
+ main := (ws | comment | container)* & line* $!error;
+}%%
+
+void
+fspec_parse(struct fspec *fspec)
+{
+ int cs;
+ %% write init;
+
+ (void)fspec_en_main;
+ assert(fspec);
+ assert(fspec->ops.read);
+ assert(fspec->ops.field);
+
+ struct state state = {
+ .ragel = {
+ .lineno = 1,
+ .mem = {
+ .data = fspec->mem.data,
+ .size = fspec->mem.size,
+ },
+ },
+ };
+
+ for (bool ok = true; ok;) {
+ const size_t bytes = fspec->ops.read(fspec, state.ragel.buf, 1, sizeof(state.ragel.buf));
+ ok = ragel_confirm_input(&state.ragel, bytes);
+ %% write exec;
+ }
+}
diff --git a/src/ragel/ragel.h b/src/ragel/ragel.h
new file mode 100644
index 0000000..af06f4a
--- /dev/null
+++ b/src/ragel/ragel.h
@@ -0,0 +1,236 @@
+#pragma once
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <limits.h>
+#include <err.h>
+
+struct ragel {
+ struct {
+ uint8_t *data; // data\0another_data\0
+ const uint8_t *cur; // data\0another_data\0cursor
+ size_t written, size; // amount of data written / size of data
+ } mem;
+
+ char buf[4096]; // block of input data
+ const char *p, *pe, *eof; // see ragel doc
+ size_t lineno; // current line
+};
+
+static inline void
+ragel_get_current_line(const struct ragel *ragel, size_t *out_lineno, size_t *out_ls, size_t *out_le, size_t *out_ws, size_t *out_we)
+{
+ assert(out_ls && out_le && out_ws && out_we);
+ assert(ragel->p >= ragel->buf && ragel->pe >= ragel->p);
+
+ size_t ls, le, ws, we;
+ size_t off = ragel->p - ragel->buf;
+ size_t lineno = ragel->lineno;
+ const size_t end = ragel->pe - ragel->buf;
+
+ // rewind to first non-space
+ for (; off > 0 && (isspace(ragel->buf[off]) || !ragel->buf[off]); --off) {
+ if (lineno > 0 && ragel->buf[off] == '\n')
+ --lineno;
+ }
+
+ for (ls = off; ls > 0 && ragel->buf[ls] != '\n'; --ls); // beginning of line
+ for (le = off; le < end && ragel->buf[le] != '\n'; ++le); // end of line
+ for (; ls < le && isspace(ragel->buf[ls]); ++ls); // strip leading whitespace
+ for (ws = off; ws > ls && isspace(ragel->buf[ws]); --ws); // rewind to first non-space
+ for (; ws > 0 && ws > ls && !isspace(ragel->buf[ws - 1]); --ws); // find word start
+ for (we = ws; we < le && !isspace(ragel->buf[we]); ++we); // find word ending
+
+ assert(we >= ws && ws >= ls && le >= ls && le >= we);
+ *out_lineno = lineno;
+ *out_ls = ls;
+ *out_le = le;
+ *out_ws = ws;
+ *out_we = we;
+}
+
+__attribute__((format(printf, 2, 3)))
+static inline void
+ragel_throw_error(const struct ragel *ragel, const char *fmt, ...)
+{
+ assert(ragel && fmt);
+
+ size_t lineno, ls, le, ws, we;
+ ragel_get_current_line(ragel, &lineno, &ls, &le, &ws, &we);
+ assert(le - ls <= INT_MAX && ws - ls <= INT_MAX);
+
+ char msg[255];
+ va_list args;
+ va_start(args, fmt);
+ vsnprintf(msg, sizeof(msg), fmt, args);
+ va_end(args);
+
+ const int indent = 8;
+ const size_t mark = (we - ws ? we - ws : 1), cur = (ragel->p - ragel->buf) - ws;
+ warnx("\x1b[37m%zu: \x1b[31merror: \x1b[0m%s\n%*s%.*s", lineno, msg, indent, "", (int)(le - ls), ragel->buf + ls);
+ fprintf(stderr, "%*s%*s\x1b[31m", indent, "", (int)(ws - ls), "");
+ for (size_t i = 0; i < mark; ++i) fputs((i == cur ? "^" : "~"), stderr);
+ fputs("\x1b[0m\n", stderr);
+
+ exit(EXIT_FAILURE);
+}
+
+static inline void
+ragel_bounds_check_data(const struct ragel *ragel, const size_t nmemb)
+{
+ assert(ragel);
+
+ if (ragel->mem.size < nmemb || ragel->mem.written >= ragel->mem.size - nmemb)
+ ragel_throw_error(ragel, "data storage limit exceeded: %zu bytes exceeds the maximum store size of %zu bytes", ragel->mem.written, ragel->mem.size);
+}
+
+static inline void
+ragel_replace_data(struct ragel *ragel, const size_t nmemb, char replacement)
+{
+ assert(ragel);
+
+ if (ragel->mem.written < nmemb)
+ ragel_throw_error(ragel, "parse error: received escape conversion with mem.written of %zu, expected >= %zu", ragel->mem.written, nmemb);
+
+ ragel->mem.data[(ragel->mem.written -= nmemb)] = replacement;
+ ragel->mem.data[++ragel->mem.written] = 0;
+}
+
+static inline void
+ragel_convert_escape(struct ragel *ragel)
+{
+ assert(ragel);
+
+ if (ragel->mem.written < 2)
+ ragel_throw_error(ragel, "parse error: received escape conversion with mem.written of %zu, expected >= 2", ragel->mem.written);
+
+ const struct {
+ const char *e;
+ const char v, b;
+ } map[] = {
+ { .e = "\\a", .v = '\a' },
+ { .e = "\\b", .v = '\b' },
+ { .e = "\\f", .v = '\f' },
+ { .e = "\\n", .v = '\n' },
+ { .e = "\\r", .v = '\r' },
+ { .e = "\\t", .v = '\t' },
+ { .e = "\\v", .v = '\v' },
+ { .e = "\\\\", .v = '\\' },
+ { .e = "\\'", .v = '\'' },
+ { .e = "\\\"", .v = '"' },
+ { .e = "\\e", .v = '\e' },
+ { .e = "\\x", .b = 16 },
+ { .e = "\\", .b = 8 },
+ };
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
+ const char *cur = (char*)ragel->mem.cur;
+ const size_t cur_sz = strlen(cur);
+ for (size_t i = 0; i < ARRAY_SIZE(map); ++i) {
+ if (!strncmp(cur, map[i].e, strlen(map[i].e))) {
+ const char v = (!map[i].b ? map[i].v : strtol(cur + strlen(map[i].e), NULL, map[i].b));
+ assert((map[i].b == 8 && cur_sz >= 2) || (map[i].b == 16 && cur_sz >= 2) || (map[i].b == 0 && cur_sz == 2));
+ assert(map[i].b != 8 || isdigit(cur[1]));
+ ragel_replace_data(ragel, cur_sz, v);
+ return;
+ }
+ }
+#undef ARRAY_SIZE
+
+ ragel_throw_error(ragel, "parse error: received unknown escape conversion");
+}
+
+static inline void
+ragel_dump_data(struct ragel *ragel, const size_t offset)
+{
+ const uint8_t *end = ragel->mem.data + ragel->mem.written;
+ for (const uint8_t *p = ragel->mem.data + offset; p && p < end; p = (uint8_t*)memchr(p, 0, end - p), p += !!p)
+ printf("%s\n", p);
+}
+
+static inline const uint8_t*
+ragel_search_data(const struct ragel *ragel, const size_t offset, const uint8_t *data, const size_t size)
+{
+ assert(ragel && data);
+
+ const uint8_t *end = ragel->mem.data + ragel->mem.written;
+ for (const uint8_t *p = ragel->mem.data + offset; p && p < end && (size_t)(end - p) >= size; p = (uint8_t*)memchr(p, 0, end - p), p += !!p) {
+ if (!memcmp(data, p, size))
+ return p;
+ }
+
+ return NULL;
+}
+
+static inline const uint8_t*
+ragel_search_str(const struct ragel *ragel, const size_t offset, const char *str)
+{
+ return ragel_search_data(ragel, offset, (const uint8_t*)str, strlen(str) + 1);
+}
+
+static inline void
+ragel_remove_last_data(struct ragel *ragel)
+{
+ assert(ragel);
+ const uint8_t *end = ragel->mem.data + ragel->mem.written;
+ const size_t size = end - ragel->mem.cur + 1;
+ assert(ragel->mem.written >= size);
+ ragel->mem.written -= size;
+ ragel->mem.data[ragel->mem.written] = 0;
+}
+
+static inline void
+ragel_finish_data(struct ragel *ragel)
+{
+ assert(ragel);
+
+ const uint8_t *end = ragel->mem.data + ragel->mem.written, *p;
+ if ((p = ragel_search_data(ragel, 0, ragel->mem.cur, end - ragel->mem.cur + 1))) {
+ ragel_remove_last_data(ragel);
+ ragel->mem.cur = p;
+ }
+}
+
+static inline void
+ragel_store_data(struct ragel *ragel)
+{
+ ragel_bounds_check_data(ragel, 1);
+ ragel->mem.data[ragel->mem.written++] = *ragel->p;
+ ragel->mem.data[ragel->mem.written] = 0;
+}
+
+static inline void
+ragel_begin_data(struct ragel *ragel)
+{
+ ragel_bounds_check_data(ragel, 1);
+ ragel->mem.written += (ragel->mem.written > 0);
+ ragel->mem.cur = ragel->mem.data + ragel->mem.written;
+}
+
+static inline void
+ragel_advance_line(struct ragel *ragel)
+{
+ assert(ragel);
+ ++ragel->lineno;
+}
+
+static inline bool
+ragel_confirm_input(struct ragel *ragel, const size_t bytes)
+{
+ assert(ragel);
+
+ if (bytes > sizeof(ragel->buf))
+ errx(EXIT_FAILURE, "%s: gave larger buffer than %zu", __func__, sizeof(ragel->buf));
+
+ const bool in_eof = (bytes < sizeof(ragel->buf));
+ ragel->p = ragel->buf;
+ ragel->pe = ragel->p + bytes;
+ ragel->eof = (in_eof ? ragel->pe : NULL);
+ return !in_eof;
+}
diff --git a/src/utils/dec2bin.c b/src/utils/dec2bin.c
new file mode 100644
index 0000000..97e59bb
--- /dev/null
+++ b/src/utils/dec2bin.c
@@ -0,0 +1,40 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <err.h>
+
+int
+main(int argc, char *argv[])
+{
+ if (argc < 3)
+ errx(EXIT_FAILURE, "usage: %s <u8|u16|u32|u64> number\n", argv[0]);
+
+ const struct {
+ const char *t;
+ size_t sz;
+ } map[] = {
+ { .t = "u8", .sz = sizeof(uint8_t) },
+ { .t = "u16", .sz = sizeof(uint16_t) },
+ { .t = "u32", .sz = sizeof(uint32_t) },
+ { .t = "u64", .sz = sizeof(uint64_t) },
+ };
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
+
+ size_t sz = 0;
+ for (size_t i = 0; i < ARRAY_SIZE(map); ++i) {
+ if (strcmp(argv[1], map[i].t))
+ continue;
+
+ sz = map[i].sz;
+ break;
+ }
+
+ if (!sz)
+ errx(EXIT_FAILURE, "unknown type: %s", argv[1]);
+
+ const uint64_t v = strtoll(argv[2], NULL, 10);
+ fwrite(&v, sz, 1, stdout);
+ return EXIT_SUCCESS;
+}
diff --git a/src/xi/xi2path.c b/src/xi/xi2path.c
new file mode 100644
index 0000000..bd9c702
--- /dev/null
+++ b/src/xi/xi2path.c
@@ -0,0 +1,16 @@
+#include <stdlib.h>
+#include <err.h>
+
+#include "xi2path.h"
+
+int
+main(int argc, char *argv[])
+{
+ if (argc < 2)
+ errx(EXIT_FAILURE, "usage: %s id\n", argv[0]);
+
+ char path[12];
+ xi2path(path, strtol(argv[1], NULL, 10));
+ printf("%s\n", path);
+ return EXIT_SUCCESS;
+}
diff --git a/src/xi/xi2path.h b/src/xi/xi2path.h
new file mode 100644
index 0000000..954c554
--- /dev/null
+++ b/src/xi/xi2path.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <stdio.h>
+#include <assert.h>
+#include <stdint.h>
+
+static inline void
+xi2path(char out[12], const uint16_t id)
+{
+ // Forms path like: section/id.DAT, from 16bit id.
+ // First 7 bits are used for DAT and rest of the bits for ROM section.
+ //
+ // e.g. ID of 1000 would look like 11101000 00000011 in binary.
+ // RDDDDDDD RRRRRRRR
+ //
+ // In the above graph, 'R' bits form the ROM section (7) and 'D' bits form the DAT (104).
+ // Thus maximum DAT and ROM section IDs are 127 and 511 respectively (65535 as decimal).
+
+ snprintf(out, 12, "%u/%u.DAT", id >> 7, id & 0x7F);
+}
+
+static inline void
+xi2rompath(char out[18], const uint8_t rom, const uint16_t id)
+{
+ assert(rom <= 9);
+
+ char path[12];
+ xi2path(path, id);
+
+ if (rom > 1) {
+ snprintf(out, 18, "ROM%u/%s", rom, path);
+ } else {
+ snprintf(out, 18, "ROM/%s", path);
+ }
+}
diff --git a/src/xi/xidec.c b/src/xi/xidec.c
new file mode 100644
index 0000000..3df917f
--- /dev/null
+++ b/src/xi/xidec.c
@@ -0,0 +1,136 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <string.h>
+#include <assert.h>
+#include <err.h>
+
+static uint8_t
+rotate_right(uint8_t b, const uint8_t count)
+{
+ for (size_t i = 0; i < count; ++i) {
+ const bool drop = ((b & 0x01) == 0x01);
+ b = (b >> 1) | (drop ? 0x80 : 0);
+ }
+ return b;
+}
+
+static void
+decode(uint8_t *data, const size_t size, const uint8_t count)
+{
+ assert(data);
+
+ for (size_t i = 0; i < size; ++i)
+ data[i] = rotate_right(data[i], count);
+}
+
+static size_t
+count_bits(const uint8_t byte)
+{
+ static const uint8_t lut[16] = {
+ 0, 1, 1, 2, 1, 2, 2, 3,
+ 1, 2, 2, 3, 2, 3, 3, 4
+ };
+
+ return lut[byte & 0x0F] + lut[byte >> 4];
+}
+
+static uint8_t
+text_rotation(const uint8_t *data, const size_t size)
+{
+ assert(data);
+
+ if (size < 2 || (data[0] == 0 && data[1] == 0))
+ return 0;
+
+ const int seed = count_bits(data[1]) - count_bits(data[0]);
+ switch (abs(seed) % 5) {
+ case 0: return 1;
+ case 1: return 7;
+ case 2: return 2;
+ case 3: return 6;
+ case 4: return 3;
+ default:break;
+ }
+
+ assert(0 && "failed to detect rotation");
+ return 0;
+}
+
+static uint8_t
+other_rotation(const uint8_t *data, const size_t size)
+{
+ assert(data);
+
+ if (size < 13)
+ return 0;
+
+ const int seed = count_bits(data[2]) - count_bits(data[11]) + count_bits(data[12]);
+ switch (abs(seed) % 5) {
+ case 0: return 7;
+ case 1: return 1;
+ case 2: return 6;
+ case 3: return 2;
+ case 4: return 5;
+ default:break;
+ }
+
+ assert(0 && "failed to detect rotation");
+ return 0;
+}
+
+static uint8_t
+item_rotation(const uint8_t *data, const size_t size)
+{
+ assert(data);
+ (void)data, (void)size;
+ return 5;
+}
+
+int
+main(int argc, char *argv[])
+{
+ if (argc < 2)
+ errx(EXIT_FAILURE, "usage: %s (name | ability | spell | item | text) < data\n", argv[0]);
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
+
+ const struct info {
+ const char *name;
+ size_t chunk;
+ uint8_t (*rotation)(const uint8_t *data, const size_t size);
+ } map[] = {
+ { .name = "name", .chunk = 32 },
+ { .name = "ability", .chunk = 1024, .rotation = other_rotation },
+ { .name = "spell", .chunk = 1024, .rotation = other_rotation },
+ { .name = "item", .chunk = 3072, .rotation = item_rotation },
+ { .name = "text", .chunk = 255, .rotation = text_rotation },
+ };
+
+ const struct info *info = NULL;
+ for (size_t i = 0; i < ARRAY_SIZE(map); ++i) {
+ if (strcmp(map[i].name, argv[1]))
+ continue;
+
+ info = &map[i];
+ break;
+ }
+
+ if (!info)
+ errx(EXIT_FAILURE, "unknown file type '%s'", argv[1]);
+
+ uint8_t buf[4096];
+ assert(sizeof(buf) >= info->chunk);
+
+ size_t bytes;
+ while ((bytes = fread(buf, 1, info->chunk, stdin)) > 0) {
+ if (info->rotation)
+ decode(buf, bytes, info->rotation(buf, bytes));
+
+ fwrite(buf, 1, bytes, stdout);
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/xi/xifile.c b/src/xi/xifile.c
new file mode 100644
index 0000000..f1b2111
--- /dev/null
+++ b/src/xi/xifile.c
@@ -0,0 +1,169 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <err.h>
+
+static const char *stdin_name = "/dev/stdin";
+
+static FILE*
+fopen_or_die(const char *path, const char *mode)
+{
+ assert(path && mode);
+
+ FILE *f;
+ if (!(f = fopen(path, mode)))
+ err(EXIT_FAILURE, "fopen(%s, %s)", path, mode);
+
+ return f;
+}
+
+static void
+detect(const char *path)
+{
+ assert(path);
+
+ uint8_t buf[32] = {0};
+ const char *name = (!strcmp(path, "-") ? stdin_name : path);
+ FILE *f = (name == stdin_name ? stdin : fopen_or_die(name, "rb"));
+ fread(buf, 1, sizeof(buf), f);
+
+ const struct info {
+ const char *name;
+ const uint8_t *header;
+ size_t chunk;
+ } map[] = {
+#define HDR(...) .header = (const uint8_t[]){__VA_ARGS__}, .chunk = sizeof((const uint8_t[]){__VA_ARGS__})
+ {
+ .name = "BGMStream",
+ HDR('B', 'G', 'M', 'S', 't', 'r', 'e', 'a', 'm')
+ }, {
+ .name = "SeWave",
+ HDR('S', 'e', 'W', 'a', 'v', 'e')
+ }, {
+ .name = "PMUS",
+ HDR('P', 'M', 'U', 'S')
+ }, {
+ .name = "RIFF/WAVE",
+ HDR('R', 'I', 'F', 'F', 0x24, 0xB3, 0xCF, 0x04, 'W', 'A', 'V', 'E', 'f', 'm', 't')
+ }, {
+ .name = "RIFF/ACON",
+ HDR('R', 'I', 'F', 'F', 0x58, 0x23, 0, 0, 'A', 'C', 'O', 'N', 'a', 'n', 'i', 'h')
+ }, {
+ .name = "name",
+ HDR('n', 'o', 'n', 'e', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
+ }, {
+ .name = "syst",
+ HDR('s', 'y', 's', 't')
+ }, {
+ .name = "menu",
+ HDR('m', 'e', 'n', 'u')
+ }, {
+ .name = "lobb",
+ HDR('l', 'o', 'b', 'b')
+ }, {
+ .name = "wave",
+ HDR('w', 'a', 'v', 'e')
+ }, {
+ .name = "ability",
+ HDR(0, 0, 0, 0x17, 0, 0, 0, 0, 0x80)
+ }, {
+ .name = "spell",
+ HDR(0, 0, 0, 0, 0x03, 0, 0x9F, 0, 0x10)
+ }, {
+ .name = "mgc_",
+ HDR('m', 'g', 'c', '_')
+ }, {
+ .name = "win0",
+ HDR('w', 'i', 'n', '0')
+ }, {
+ .name = "titl",
+ HDR('t', 'i', 't', 'l')
+ }, {
+ .name = "sel_",
+ HDR('s', 'e', 'l', '_')
+ }, {
+ .name = "damv",
+ HDR('d', 'a', 'm', 'v')
+ }, {
+ .name = "XISTRING",
+ HDR('X', 'I', 'S', 'T', 'R', 'I', 'N', 'G')
+ }, {
+ .name = "prvd",
+ HDR('p', 'r', 'v', 'd')
+ }, {
+ .name = "selp",
+ HDR('s', 'e', 'l', 'p')
+ }, {
+ .name = "dun",
+ HDR('d', 'u', 'n')
+ }, {
+ .name = "town",
+ HDR('t', 'o', 'w', 'n')
+ }, {
+ .name = "dese",
+ HDR('d', 'e', 's', 'e')
+ }, {
+ .name = "fore",
+ HDR('f', 'o', 'r', 'e')
+ }, {
+ .name = "tree",
+ HDR('t', 'r', 'e', 'e')
+ }, {
+ .name = "unka",
+ HDR('u', 'n', 'k', 'a')
+ }, {
+ .name = "moun",
+ HDR('m', 'o', 'u', 'n')
+ }, {
+ .name = "cast",
+ HDR('c', 'a', 's', 't')
+ }, {
+ .name = "fuji",
+ HDR('f', 'u', 'j', 'i')
+ }, {
+ .name = "view",
+ HDR('v', 'i', 'e', 'w')
+ }
+#undef HDR
+ };
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
+
+ const struct info *info = NULL;
+ for (size_t i = 0; i < ARRAY_SIZE(map); ++i) {
+ assert(map[i].chunk <= sizeof(buf));
+ if (memcmp(buf, map[i].header, map[i].chunk))
+ continue;
+
+ info = &map[i];
+ break;
+ }
+
+ if (info) {
+ printf("%s: %s\n", name, info->name);
+ } else {
+ int i;
+ for (i = 0; i < 32 && isprint(buf[i]); ++i);
+ if (i > 0) {
+ printf("%s: unknown (%.*s)\n", name, i, buf);
+ } else {
+ printf("%s: unknown\n", name);
+ }
+ }
+ fclose(f);
+}
+
+int
+main(int argc, char *argv[])
+{
+ if (argc < 2)
+ errx(EXIT_FAILURE, "usage: %s file\n", argv[0]);
+
+ for (int i = 1; i < argc; ++i)
+ detect(argv[i]);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/xi/xils.c b/src/xi/xils.c
new file mode 100644
index 0000000..9c9a75e
--- /dev/null
+++ b/src/xi/xils.c
@@ -0,0 +1,94 @@
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <assert.h>
+#include <err.h>
+
+#include "xi2path.h"
+
+static FILE*
+fopen_or_die(const char *gamedir, const char *file, const char *mode)
+{
+ assert(gamedir && file && mode);
+
+ char path[4096];
+ snprintf(path, sizeof(path), "%s/%s", gamedir, file);
+
+ FILE *f;
+ if (!(f = fopen(path, mode)))
+ err(EXIT_FAILURE, "fopen(%s, %s)", path, mode);
+
+ return f;
+}
+
+static void
+dump_tables(const char *dir, const char *names[2], const uint8_t rom, const bool print_all, const bool verbose)
+{
+ assert(dir && names);
+
+ // FTABLE.DAT contains list of IDs.
+ // VTABLE.DAT contains number of ROM for each entry or 0 if the entry is not used.
+ FILE *f = fopen_or_die(dir, names[0], "rb");
+ FILE *v = fopen_or_die(dir, names[1], "rb");
+
+#define ELEM_SIZE(x) (sizeof(x[0]))
+#define ARRAY_SIZE(x) (sizeof(x) / ELEM_SIZE(x))
+
+ {
+ size_t read[2];
+ uint16_t id[255];
+ uint8_t exist[255];
+ while ((read[0] = fread(id, ELEM_SIZE(id), ARRAY_SIZE(id), f)) > 0 &&
+ (read[1] = fread(exist, ELEM_SIZE(exist), ARRAY_SIZE(exist), v)) > 0) {
+ assert(read[0] == read[1]);
+
+ for (size_t i = 0; i < read[0]; ++i) {
+ if (!print_all && !exist[i])
+ continue;
+
+ if (verbose)
+ printf("%u: ", exist[i]);
+
+ char path[18];
+ xi2rompath(path, rom, id[i]);
+ printf("%s\n", path);
+ }
+ }
+ }
+
+ fclose(v);
+ fclose(f);
+}
+
+int
+main(int argc, char *argv[])
+{
+ bool verbose = false;
+ bool print_all = false;
+ const char *gamedir = NULL;
+ for (int i = 1; i < argc; ++i) {
+ if (!strcmp(argv[i], "-a")) {
+ print_all = true;
+ } else if (!strcmp(argv[i], "-v")) {
+ verbose = true;
+ } else {
+ gamedir = argv[i];
+ break;
+ }
+ }
+
+ if (!gamedir)
+ errx(EXIT_FAILURE, "usage: %s [-a|-v] gamedir\n", argv[0]);
+
+ dump_tables(gamedir, (const char*[]){ "FTABLE.DAT", "VTABLE.DAT" }, 1, print_all, verbose);
+
+ for (uint8_t i = 2; i <= 9; ++i) {
+ char dir[4096], f[12], v[12];
+ snprintf(dir, sizeof(dir), "%s/ROM%u", gamedir, i);
+ snprintf(f, sizeof(f), "FTABLE%u.DAT", i);
+ snprintf(v, sizeof(v), "VTABLE%u.DAT", i);
+ dump_tables(dir, (const char*[]){ f, v }, i, print_all, verbose);
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/vim/filespec.vim b/vim/filespec.vim
new file mode 100644
index 0000000..5e43fc5
--- /dev/null
+++ b/vim/filespec.vim
@@ -0,0 +1,56 @@
+" Vim syntax file
+" Language: Filespec
+" Author: Jari Vetoniemi
+
+syntax clear
+
+syn match fsBadContinuation contained "\\\s\+$"
+syn keyword fsTodo contained TODO FIXME XXX
+syn cluster fsCommentGroup contains=fsTodo,fsBadContinuation
+syn region fsComment start="//" skip="\\$" end="$" keepend contains=@fsCommentGroup,@Spell
+
+syn keyword fsStructure struct union
+syn keyword fsType s8 s16 s32 s64
+syn keyword fsType u8 u16 u32 u64
+syn keyword fsKind ascii utf8 sjis pad hex
+
+syn case ignore
+syn match fsNumbers display transparent "\<\d\|\.\d" contains=fsNumber,fsFloat,fsOctalError,fsOctal
+syn match fsNumbersCom display contained transparent "\<\d\|\.\d" contains=fsNumber,fsFloat,fsOctal
+syn match fsNumber display contained "\d\+\(u\=l\{0,2}\|ll\=u\)\>"
+syn match fsNumber display contained "0x\x\+\(u\=l\{0,2}\|ll\=u\)\>"
+syn match fsOctal display contained "0\o\+\(u\=l\{0,2}\|ll\=u\)\>" contains=fsOctalZero
+syn match fsOctalZero display contained "\<0"
+syn match fsFloat display contained "\d\+f"
+syn match fsFloat display contained "\d\+\.\d*\(e[-+]\=\d\+\)\=[fl]\="
+syn match fsFloat display contained "\.\d\+\(e[-+]\=\d\+\)\=[fl]\=\>"
+syn match fsFloat display contained "\d\+e[-+]\=\d\+[fl]\=\>"
+syn match fsOctalError display contained "0\o*[89]\d*"
+syn case match
+
+syn match fsSpecial display contained "\\\(x\x\+\|\o\{1,3}\|.\|$\)"
+syn region fsString start=+"+ skip=+\\\\\|\\"+ end=+"+ contains=fsSpecial,@Spell extend
+syn match fsCharacter "'[^']*'" contains=fsSpecial
+
+syn match fsBlock "[{}]"
+syn match fsBracket "[\[\]]"
+syn match fsOperator display "[-+&|<>=!*\/~.,;:%&^?()]" contains=fsComment,fsKind
+
+" Define the default highlighting.
+" Only used when an item doesn't have highlighting yet
+hi def link fsTodo Todo
+hi def link fsComment Comment
+hi def link fsStructure Structure
+hi def link fsType Type
+hi def link fsKind Constant
+hi def link fsNumber Number
+hi def link fsOctal Number
+hi def link fsOctalZero PreProc
+hi def link fsFloat Float
+hi def link fsOctalError Error
+hi def link fsString Constant
+hi def link fsCharacter Character
+hi def link fsSpecial SpecialChar
+hi def link fsBlock Constant
+hi def link fsBracket Constant
+hi def link fsOperator Operator