diff --git a/include/system4/ini.h b/include/system4/ini.h index ee1e804..6726cd7 100644 --- a/include/system4/ini.h +++ b/include/system4/ini.h @@ -28,6 +28,7 @@ enum ini_value_type { INI_STRING, INI_LIST, _INI_LIST_ENTRY, + INI_FORMATION, INI_NR_TYPES }; @@ -37,14 +38,21 @@ struct ini_value { union { int i; struct string *s; + // list struct { size_t list_size; struct ini_value *list; }; + // list item struct { size_t _list_pos; struct ini_value *_list_value; }; + // formation + struct { + size_t nr_entries; + struct ini_entry *entries; + }; }; }; diff --git a/include/system4/string.h b/include/system4/string.h index de19cdf..d37d9bd 100644 --- a/include/system4/string.h +++ b/include/system4/string.h @@ -62,5 +62,6 @@ void string_set_char(struct string **s, int i, unsigned int c); // C strings int int_to_cstr(char *buf, size_t size, int v, int figures, bool zero_pad, bool zenkaku); int float_to_cstr(char *buf, size_t size, float v, int figures, bool zero_pad, int precision, bool zenkaku); +struct string *cstr_to_string(const char *str); #endif diff --git a/src/ini.c b/src/ini.c index dc3b566..9661f2c 100644 --- a/src/ini.c +++ b/src/ini.c @@ -121,6 +121,12 @@ static void ini_free_value(struct ini_value *value) ini_free_value(value->_list_value); free(value->_list_value); break; + case INI_FORMATION: + for (size_t i = 0; i < value->nr_entries; i++) { + ini_free_entry(&value->entries[i]); + } + free(value->entries); + break; default: break; } } diff --git a/src/ini_lexer.l b/src/ini_lexer.l index 5979f4c..71ebd9e 100644 --- a/src/ini_lexer.l +++ b/src/ini_lexer.l @@ -38,6 +38,7 @@ unsigned long yini_line = 1; %% +\/\/[^\n]*\n yini_line++; [ \t\r] ; \n yini_line++; ;[^\n]*\n yini_line++; @@ -49,6 +50,7 @@ unsigned long yini_line = 1; , return ','; true return TRUE; false return FALSE; +Formation return FORMATION; [0-9]+ yini_lval.i = atoi(yini_text); return INTEGER; [a-zA-Z]+[a-zA-Z0-9]* yini_lval.s = make_string(yini_text, yini_leng); return IDENTIFIER; @@ -64,15 +66,7 @@ false return FALSE; \n ERROR("Unterminated string literal at line %lu", yini_line); - \\n *string_buf_ptr++ = '\n'; - \\t *string_buf_ptr++ = '\t'; - \\r *string_buf_ptr++ = '\r'; - \\b *string_buf_ptr++ = '\b'; - \\f *string_buf_ptr++ = '\f'; - - \\(.|\n) *string_buf_ptr++ = yini_text[1]; - - [^\\\n\"]+ { + [^\n\"]+ { char *yptr = yini_text; while (*yptr) *string_buf_ptr++ = *yptr++; diff --git a/src/ini_parser.y b/src/ini_parser.y index e6b0beb..1ec0477 100644 --- a/src/ini_parser.y +++ b/src/ini_parser.y @@ -43,6 +43,8 @@ static void push_entry(entry_list *ini, struct ini_entry *entry) static struct ini_value make_list(value_list *values) { + if (!values) + return ini_make_list(NULL, 0); struct ini_value *vals = kv_data(*values); size_t nr_vals = kv_size(*values); free(values); @@ -74,9 +76,25 @@ static struct ini_entry *make_list_assign(struct string *name, int i, struct ini return ini_make_entry(name, wrapper); } +static struct ini_entry *make_formation(struct string *name, entry_list *entries) +{ + struct ini_value wrapper = { + .type = INI_FORMATION, + .nr_entries = kv_size(*entries), + .entries = xcalloc(kv_size(*entries), sizeof(struct ini_entry)), + }; + for (size_t i = 0; i < wrapper.nr_entries; i++) { + wrapper.entries[i] = *kv_A(*entries, i); + free(kv_A(*entries, i)); + } + kv_destroy(*entries); + free(entries); + return ini_make_entry(name, wrapper); +} + %} -%token TRUE FALSE +%token TRUE FALSE FORMATION %token INTEGER %token STRING IDENTIFIER @@ -99,12 +117,14 @@ entries : entry { $$ = make_ini(); push_entry($$, $1); } entry : IDENTIFIER '=' value { $$ = ini_make_entry($1, $3); } | IDENTIFIER '[' INTEGER ']' '=' value { $$ = make_list_assign($1, $3, $6); } + | FORMATION STRING '{' entries '}' { $$ = make_formation($2, $4); } ; value : INTEGER { $$ = ini_make_integer($1); } | TRUE { $$ = ini_make_boolean(true); } | FALSE { $$ = ini_make_boolean(false); } | STRING { $$ = ini_make_string($1); } + | '{' '}' { $$ = make_list(NULL); } | '{' list '}' { $$ = make_list($2); } | '{' list ',' '}' { $$ = make_list($2); } ; diff --git a/src/meson.build b/src/meson.build index 3e7e637..49f251c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,6 +12,7 @@ system4 = ['afa.c', 'flat.c', 'fnl.c', 'hashtable.c', + 'ini.c', 'instructions.c', 'pms.c', 'qnt.c', @@ -30,7 +31,6 @@ xsystem4 = ['audio.c', 'fnl_render.c', 'format.c', 'heap.c', - 'ini.c', 'input.c', 'page.c', 'resume.c', @@ -75,8 +75,8 @@ bisongen = generator(bison, output: ['@BASENAME@.tab.c', '@BASENAME@.tab.h'], arguments : ['@INPUT@', '--defines=@OUTPUT1@', '--output=@OUTPUT0@']) -xsystem4 += flexgen.process('ini_lexer.l') -xsystem4 += bisongen.process('ini_parser.y') +system4 += flexgen.process('ini_lexer.l') +system4 += bisongen.process('ini_parser.y') libsys4 = static_library('sys4', system4, dependencies : [libm, zlib, tj, webp, png], diff --git a/src/string.c b/src/string.c index 9b5b863..5092835 100644 --- a/src/string.c +++ b/src/string.c @@ -54,6 +54,11 @@ struct string *make_string(const char *str, unsigned int len) return s; } +struct string *cstr_to_string(const char *str) +{ + return make_string(str, strlen(str)); +} + struct string *string_ref(struct string *s) { s->cow = 1; diff --git a/src/tools/ainedit/ainedit.c b/src/tools/ainedit/ainedit.c index 2d15a01..b5c7c85 100644 --- a/src/tools/ainedit/ainedit.c +++ b/src/tools/ainedit/ainedit.c @@ -32,6 +32,7 @@ static void usage(void) puts(" Edit AIN files."); puts(""); puts(" -h, --help Display this message and exit"); + puts(" -p, --project Build AIN from project file"); puts(" -c, --code Update the CODE section (assemble .jam file)"); puts(" --jaf Update AIN file from JAF source code"); puts(" -j, --json Update AIN file from JSON data"); @@ -44,13 +45,13 @@ static void usage(void) puts(" --ain-encoding Specify the text encoding of the input AIN file"); puts(" --ain-version Specify the AIN version (when creating a new AIN file)"); puts(" --silent Don't write messages to stdout"); - //puts(" -p,--project Build AIN from project file"); } extern int text_parse(void); enum { LOPT_HELP = 256, + LOPT_PROJECT, LOPT_CODE, LOPT_JAF, LOPT_JSON, @@ -92,6 +93,7 @@ int main(int argc, char *argv[]) initialize_instructions(); struct ain *ain; int err = AIN_SUCCESS; + const char *project_file = NULL; const char *code_file = NULL; unsigned nr_jaf_files = 0; const char **jaf_files = NULL; @@ -107,6 +109,7 @@ int main(int argc, char *argv[]) while (1) { static struct option long_options[] = { { "help", no_argument, 0, LOPT_HELP }, + { "project", required_argument, 0, LOPT_PROJECT }, { "code", required_argument, 0, LOPT_CODE }, { "jaf", required_argument, 0, LOPT_JAF }, { "json", required_argument, 0, LOPT_JSON }, @@ -123,7 +126,7 @@ int main(int argc, char *argv[]) int option_index = 0; int c; - c = getopt_long(argc, argv, "hc:j:t:o:", long_options, &option_index); + c = getopt_long(argc, argv, "hp:c:j:t:o:", long_options, &option_index); if (c == -1) break; @@ -132,6 +135,10 @@ int main(int argc, char *argv[]) case LOPT_HELP: usage(); return 0; + case 'p': + case LOPT_PROJECT: + project_file = optarg; + break; case 'c': case LOPT_CODE: code_file = optarg; @@ -190,11 +197,6 @@ int main(int argc, char *argv[]) ERROR("Too many arguments."); } - if (!output_file) { - usage(); - ERROR("No output file given"); - } - if ((ain_conv = iconv_open(output_encoding, input_encoding)) == (iconv_t)-1) { ERROR("iconv_open: %s", strerror(errno)); } @@ -205,6 +207,15 @@ int main(int argc, char *argv[]) ERROR("iconv_open: %s", strerror(errno)); } + if (project_file) { + pje_build(project_file, ain_version); + return 0; + } + + if (!output_file) { + output_file = "out.ain"; + } + if (!argc) { ain = ain_new(ain_version); } else { diff --git a/src/tools/ainedit/ainedit.h b/src/tools/ainedit/ainedit.h index e6f6a66..a5c3226 100644 --- a/src/tools/ainedit/ainedit.h +++ b/src/tools/ainedit/ainedit.h @@ -66,6 +66,9 @@ enum asm_pseudo_opcode { const struct instruction asm_pseudo_ops[NR_PSEUDO_OPS - PSEUDO_OP_OFFSET]; +// pje.c +void pje_build(const char *path, int ain_version); + // asm.c void asm_assemble_jam(const char *filename, struct ain *ain, uint32_t flags); const struct instruction *asm_get_instruction(const char *name); diff --git a/src/tools/ainedit/jaf_parser.y b/src/tools/ainedit/jaf_parser.y index 1922b35..8523ff1 100644 --- a/src/tools/ainedit/jaf_parser.y +++ b/src/tools/ainedit/jaf_parser.y @@ -530,6 +530,7 @@ function_definition function_declarator : IDENTIFIER '(' parameter_list ')' { $$ = jaf_function_declarator($1, $3); } | IDENTIFIER '(' ')' { $$ = jaf_function_declarator($1, NULL); } + | IDENTIFIER '(' VOID ')' { $$ = jaf_function_declarator($1, NULL); } ; parameter_list diff --git a/src/tools/ainedit/meson.build b/src/tools/ainedit/meson.build index 471d3db..e31c6d0 100644 --- a/src/tools/ainedit/meson.build +++ b/src/tools/ainedit/meson.build @@ -9,6 +9,7 @@ ainedit = ['../../cJSON.c', 'jaf_static_analysis.c', 'jaf_types.c', 'json.c', + 'pje.c', 'repack.c', 'text.c', 'transcode.c', diff --git a/src/tools/ainedit/pje.c b/src/tools/ainedit/pje.c new file mode 100644 index 0000000..21ad565 --- /dev/null +++ b/src/tools/ainedit/pje.c @@ -0,0 +1,381 @@ +/* Copyright (C) 2019 Nunuhara Cabbage + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include +#include +#include +#include "file.h" +#include "system4.h" +#include "system4/ain.h" +#include "system4/ini.h" +#include "system4/string.h" +#include "ainedit.h" +#include "jaf.h" + +struct pje_formation { + struct string *name; + unsigned nr_defines; + struct string **defines; +}; + +struct string_list { + unsigned n; + struct string **items; +}; + +struct pje_config { + struct string *project_name; + struct string *code_name; + int game_version; + struct string *source_dir; + struct string *hll_dir; + struct string *obj_dir; + struct string *output_dir; + unsigned nr_formations; + struct pje_formation *formations; + struct string_list system_source; + struct string_list source; + struct string *mod_ain; + struct string *mod_text; +}; + +struct inc_config { + struct string_list system_source; + struct string_list source; + struct string_list copy_to_run; + struct string_list copy_to_dll; + struct string_list copy_to_dp; + struct string_list load_dll; +}; + +struct build_job { + struct string_list system_source; + struct string_list source; + struct string_list headers; +}; + +static void string_list_append(struct string_list *list, struct string *str) +{ + list->items = xrealloc_array(list->items, list->n, list->n+1, sizeof(struct string*)); + list->items[list->n++] = str; +} + +static void free_string_list(struct string_list *list) +{ + for (unsigned i = 0; i < list->n; i++) { + free_string(list->items[i]); + } + free(list->items); +} + +static struct string *directory_name(const char *path) +{ + char *tmp = strdup(path); + char *dir = dirname(tmp); + struct string *r = make_string(dir, strlen(dir)); + free(tmp); + return r; +} + +static struct string *directory_file(struct string *dir, struct string *file) +{ + char tmp[1024]; + if (dir->size + file->size + 2 >= 1024) + ERROR("path too long"); + + memcpy(tmp, dir->text, dir->size); + tmp[dir->size] = '/'; + memcpy(tmp+dir->size+1, file->text, file->size); + tmp[dir->size+file->size+1] = '\0'; + + for (int i = dir->size+1; i < dir->size + file->size + 1; i++) { + if (tmp[i] == '\\') + tmp[i] = '/'; + } + + return make_string(tmp, dir->size+file->size+1); +} + +static struct string *pje_string(struct ini_entry *entry) +{ + if (entry->value.type != INI_STRING) + ERROR("Invalid value for '%s': not a string", entry->name->text); + return string_dup(entry->value.s); +} + +static int pje_integer(struct ini_entry *entry) +{ + if (entry->value.type != INI_INTEGER) + ERROR("Invalid value for '%s': not an integer", entry->name->text); + return entry->value.i; +} + +static void pje_string_list(struct ini_entry *entry, struct string_list *out) +{ + if (entry->value.type != INI_LIST) + ERROR("Invalid value for '%s': not a list", entry->name->text); + + out->n = entry->value.list_size; + out->items = xcalloc(entry->value.list_size, sizeof(struct string*)); + + for (unsigned i = 0; i < out->n; i++) { + if (entry->value.list[i].type != INI_STRING) + ERROR("Invalid value in '%s': not a string", entry->name->text); + out->items[i] = string_dup(entry->value.list[i].s); + } +} + +static void pje_parse(const char *path, struct pje_config *config) +{ + size_t ini_size; + struct ini_entry *ini = ini_parse(path, &ini_size); + + for (size_t i = 0; i < ini_size; i++) { + if (!strcmp(ini[i].name->text, "ProjectName")) { + config->project_name = pje_string(&ini[i]); + } else if (!strcmp(ini[i].name->text, "CodeName")) { + config->code_name = pje_string(&ini[i]); + } else if (!strcmp(ini[i].name->text, "GameVersion")) { + config->game_version = pje_integer(&ini[i]); + } else if (!strcmp(ini[i].name->text, "SourceDir")) { + config->source_dir = pje_string(&ini[i]); + } else if (!strcmp(ini[i].name->text, "HLLDir")) { + config->hll_dir = pje_string(&ini[i]); + } else if (!strcmp(ini[i].name->text, "ObjDir")) { + config->obj_dir = pje_string(&ini[i]); + } else if (!strcmp(ini[i].name->text, "OutputDir")) { + config->output_dir = pje_string(&ini[i]); + } else if (!strcmp(ini[i].name->text, "SystemSource")) { + pje_string_list(&ini[i], &config->system_source); + } else if (!strcmp(ini[i].name->text, "Source")) { + pje_string_list(&ini[i], &config->source); + } else if (!strcmp(ini[i].name->text, "HLL")) { + WARNING("HLL renaming not supported"); + } else if (!strcmp(ini[i].name->text, "SyncFolder")) { + WARNING("SyncFolder not supported"); + } else if (!strcmp(ini[i].name->text, "SyncLock")) { + WARNING("SyncLock not supported"); + } else if (!strcmp(ini[i].name->text, "ModAin")) { + config->mod_ain = pje_string(&ini[i]); + } else if (!strcmp(ini[i].name->text, "ModText")) { + config->mod_text = pje_string(&ini[i]); + } else if (ini[i].value.type == INI_FORMATION) { + WARNING("Formations not supported"); + } else { + ERROR("Unhandled pje variable: %s", ini[i].name->text); + } + ini_free_entry(&ini[i]); + } + free(ini); + + // defaults + if (!config->project_name) + config->project_name = cstr_to_string("UntitledProject"); + if (!config->code_name) + config->code_name = cstr_to_string("out.ain"); + if (!config->source_dir) + config->source_dir = cstr_to_string("source"); + if (!config->hll_dir) + config->hll_dir = cstr_to_string("hll"); + if (!config->obj_dir) + config->obj_dir = cstr_to_string("obj"); + if (!config->output_dir) + config->output_dir = cstr_to_string("run"); +} + +static void pje_parse_inc(const char *path, struct inc_config *config) +{ + size_t ini_size; + struct ini_entry *ini = ini_parse(path, &ini_size); + + for (size_t i = 0; i < ini_size; i++) { + if (!strcmp(ini[i].name->text, "Source")) { + pje_string_list(&ini[i], &config->source); + } else if (!strcmp(ini[i].name->text, "SystemSource")) { + pje_string_list(&ini[i], &config->system_source); + } else if (!strcmp(ini[i].name->text, "CopyToRun")) { + pje_string_list(&ini[i], &config->copy_to_run); + } else if (!strcmp(ini[i].name->text, "CopyToDLL")) { + pje_string_list(&ini[i], &config->copy_to_dll); + } else if (!strcmp(ini[i].name->text, "CopyToDP")) { + pje_string_list(&ini[i], &config->copy_to_dp); + } else if (!strcmp(ini[i].name->text, "LoadDLL")) { + pje_string_list(&ini[i], &config->load_dll); + } else { + ERROR("Unhandled variable in file '%s': %s", path, ini[i].name->text); + } + ini_free_entry(&ini[i]); + } + free(ini); +} + +static void pje_free_inc(struct inc_config *config) +{ + free_string_list(&config->system_source); + free_string_list(&config->source); + free_string_list(&config->copy_to_run); + free_string_list(&config->copy_to_dll); + free_string_list(&config->copy_to_dp); + free_string_list(&config->load_dll); +} + +static const char *extname(const char *s) +{ + const char *e = strrchr(s, '.'); + return e ? e+1 : NULL; +} + +static void pje_read_source(struct build_job *job, struct string *dir, struct string_list *source, bool system); + +static void pje_read_inc(struct build_job *job, struct string *dir, struct string *inc) +{ + struct inc_config config = {0}; + struct string *file = directory_file(dir, inc); + struct string *file_dir = directory_name(file->text); + pje_parse_inc(file->text, &config); + + pje_read_source(job, file_dir, &config.system_source, true); + pje_read_source(job, file_dir, &config.source, false); + + pje_free_inc(&config); + free_string(file); + free_string(file_dir); +} + +static void pje_read_source(struct build_job *job, struct string *dir, struct string_list *source, bool system) +{ + for (unsigned i = 0; i < source->n; i++) { + const char *ext = extname(source->items[i]->text); + if (!strcmp(ext, "inc")) { + pje_read_inc(job, dir, source->items[i]); + } else if (!strcmp(ext, "jaf")) { + string_list_append(system ? &job->system_source : &job->source, directory_file(dir, source->items[i])); + } else if (!strcmp(ext, "hll")) { + if (i+1 >= source->n) + ERROR("Missing HLL name in source list: %s", source->items[i]->text); + if (strchr(source->items[i+1]->text, '.')) + ERROR("HLL name contains '.': %s", source->items[i+1]->text); + string_list_append(&job->headers, directory_file(dir, source->items[i])); + string_list_append(&job->headers, directory_file(dir, source->items[i+1])); + i++; + } else { + ERROR("Unhandled file extension in source list: '%s'", ext); + } + } +} + +static void pje_free(struct pje_config *config) +{ + if (config->project_name) + free_string(config->project_name); + if (config->code_name) + free_string(config->code_name); + if (config->source_dir) + free_string(config->source_dir); + if (config->hll_dir) + free_string(config->hll_dir); + if (config->obj_dir) + free_string(config->obj_dir); + if (config->output_dir) + free_string(config->output_dir); + free_string_list(&config->system_source); + free_string_list(&config->source); +} + +static void build_job_free(struct build_job *job) +{ + free_string_list(&job->system_source); + free_string_list(&job->source); + free_string_list(&job->headers); +} + +void pje_build(const char *path, int ain_version) +{ + struct build_job job = {0}; + struct pje_config config = {0}; + pje_parse(path, &config); + + struct string *pje_dir = directory_name(path); + struct string *src_dir = directory_file(pje_dir, config.source_dir); + struct string *out_dir = directory_file(pje_dir, config.output_dir); + if (mkdir_p(out_dir->text)) { + ERROR("Creating output directory '%s': %s", out_dir->text, strerror(errno)); + } + + // collect source file names + pje_read_source(&job, src_dir, &config.system_source, true); + pje_read_source(&job, src_dir, &config.source, false); + + // TODO: parse hll files + for (unsigned i = 0; i < job.headers.n; i++) { + WARNING("Ignoring header: %s", job.headers.items[i]->text); + } + + // consolidate files into list + // TODO: It's unclear from the docs if ALL SystemSource files should be compiled + // before ALL Source files. This becomes an issue if .inc files are used + // which contain both SystemSource and Source lists. E.g., should + // SystemSource files included from the top-level Source list be compiled + // before Source files included from the top-level SystemSource list? + // This needs to be confirmed before implementing global constructors. + unsigned nr_source_files = job.system_source.n + job.source.n; + const char **source_files = xcalloc(nr_source_files, sizeof(const char*)); + for (unsigned i = 0; i < job.system_source.n; i++) { + source_files[i] = job.system_source.items[i]->text; + } + for (unsigned i = 0; i < job.source.n; i++) { + source_files[job.system_source.n + i] = job.source.items[i]->text; + } + + // open/create ain object + struct ain *ain; + if (config.mod_ain) { + int err; + struct string *mod_ain = directory_file(pje_dir, config.mod_ain); + if (!(ain = ain_open(mod_ain->text, &err))) { + ERROR("Failed to open ain file: %s", ain_strerror); + } + free_string(mod_ain); + } else { + ain = ain_new(ain_version); + } + + // apply text substitution, if ModText given + if (config.mod_text) { + struct string *mod_text = directory_file(pje_dir, config.mod_text); + read_text(mod_text->text, ain); + free_string(mod_text); + } + + // build + jaf_build(ain, source_files, nr_source_files); + + // write to disk + NOTICE("Writing AIN file..."); + struct string *output_file = directory_file(out_dir, config.code_name); + ain_write(output_file->text, ain); + + free_string(pje_dir); + free_string(src_dir); + free_string(out_dir); + free_string(output_file); + free(source_files); + build_job_free(&job); + pje_free(&config); + ain_free(ain); +} diff --git a/src/tools/ainedit/test/local-ref-int.jaf b/src/tools/ainedit/test/local-ref-int.jaf index 4135c2a..1e9fc49 100644 --- a/src/tools/ainedit/test/local-ref-int.jaf +++ b/src/tools/ainedit/test/local-ref-int.jaf @@ -3,6 +3,6 @@ int main() int a; ref int ra = a; ref int rb = ra; - ra = 1; + rb = 1; return !(a == 1); }