From a35ebea72a4bd53df7eda06d6fae3845da2d23ad Mon Sep 17 00:00:00 2001 From: IntelOrca Date: Thu, 9 Oct 2014 01:36:59 +0100 Subject: [PATCH] improve command line parsing --- lib/argparse/argparse.c | 323 ++++++++++++++++++++++++++++++ lib/argparse/argparse.h | 132 ++++++++++++ projects/openrct2.vcxproj | 2 + projects/openrct2.vcxproj.filters | 9 + src/cmdline.c | 79 +++++--- src/config.c | 32 +-- src/config.h | 2 + src/openrct2.c | 2 - src/platform/osinterface.c | 8 +- src/rct2.c | 9 +- src/scenario.c | 2 + 11 files changed, 546 insertions(+), 54 deletions(-) create mode 100644 lib/argparse/argparse.c create mode 100644 lib/argparse/argparse.h diff --git a/lib/argparse/argparse.c b/lib/argparse/argparse.c new file mode 100644 index 0000000000..3ecd4d1f2e --- /dev/null +++ b/lib/argparse/argparse.c @@ -0,0 +1,323 @@ +#include "argparse.h" + +#define OPT_UNSET 1 + +static const char * +prefix_skip(const char *str, const char *prefix) +{ + size_t len = strlen(prefix); + return strncmp(str, prefix, len) ? NULL : str + len; +} + +int +prefix_cmp(const char *str, const char *prefix) +{ + for (;; str++, prefix++) + if (!*prefix) + return 0; + else if (*str != *prefix) + return (unsigned char)*prefix - (unsigned char)*str; +} + +static void +argparse_error(struct argparse *this, const struct argparse_option *opt, + const char *reason) +{ + if (!strncmp(this->argv[0], "--", 2)) { + fprintf(stderr, "error: option `%s` %s\n", opt->long_name, reason); + exit(1); + } else { + fprintf(stderr, "error: option `%c` %s\n", opt->short_name, reason); + exit(1); + } +} + +static int +argparse_getvalue(struct argparse *this, const struct argparse_option *opt, + int flags) +{ + const char *s = NULL; + if (!opt->value) + goto skipped; + switch (opt->type) { + case ARGPARSE_OPT_BOOLEAN: + if (flags & OPT_UNSET) { + *(int *)opt->value = *(int *)opt->value - 1; + } else { + *(int *)opt->value = *(int *)opt->value + 1; + } + if (*(int *)opt->value < 0) { + *(int *)opt->value = 0; + } + break; + case ARGPARSE_OPT_BIT: + if (flags & OPT_UNSET) { + *(int *)opt->value &= ~opt->data; + } else { + *(int *)opt->value |= opt->data; + } + break; + case ARGPARSE_OPT_STRING: + if (this->optvalue) { + *(const char **)opt->value = this->optvalue; + this->optvalue = NULL; + } else if (this->argc > 1) { + this->argc--; + *(const char **)opt->value = *++this->argv; + } else { + argparse_error(this, opt, "requires a value"); + } + break; + case ARGPARSE_OPT_INTEGER: + if (this->optvalue) { + *(int *)opt->value = strtol(this->optvalue, (char **)&s, 0); + this->optvalue = NULL; + } else if (this->argc > 1) { + this->argc--; + *(int *)opt->value = strtol(*++this->argv, (char **)&s, 0); + } else { + argparse_error(this, opt, "requires a value"); + } + if (s[0] != '\0') + argparse_error(this, opt, "expects a numerical value"); + break; + default: + assert(0); + } + +skipped: + if (opt->callback) { + return opt->callback(this, opt); + } + + return 0; +} + +static void +argparse_options_check(const struct argparse_option *options) +{ + for (; options->type != ARGPARSE_OPT_END; options++) { + switch (options->type) { + case ARGPARSE_OPT_END: + case ARGPARSE_OPT_BOOLEAN: + case ARGPARSE_OPT_BIT: + case ARGPARSE_OPT_INTEGER: + case ARGPARSE_OPT_STRING: + case ARGPARSE_OPT_GROUP: + continue; + default: + fprintf(stderr, "wrong option type: %d", options->type); + break; + } + } +} + +static int +argparse_short_opt(struct argparse *this, const struct argparse_option *options) +{ + for (; options->type != ARGPARSE_OPT_END; options++) { + if (options->short_name == *this->optvalue) { + this->optvalue = this->optvalue[1] ? this->optvalue + 1 : NULL; + return argparse_getvalue(this, options, 0); + } + } + return -2; +} + +static int +argparse_long_opt(struct argparse *this, const struct argparse_option *options) +{ + for (; options->type != ARGPARSE_OPT_END; options++) { + const char *rest; + int opt_flags = 0; + if (!options->long_name) + continue; + + rest = prefix_skip(this->argv[0] + 2, options->long_name); + if (!rest) { + // Negation allowed? + if (options->flags & OPT_NONEG) { + continue; + } + // Only boolean/bit allow negation. + if (options->type != ARGPARSE_OPT_BOOLEAN && options->type != ARGPARSE_OPT_BIT) { + continue; + } + + if (!prefix_cmp(this->argv[0] + 2, "no-")) { + rest = prefix_skip(this->argv[0] + 2 + 3, options->long_name); + if (!rest) + continue; + opt_flags |= OPT_UNSET; + } else { + continue; + } + } + if (*rest) { + if (*rest != '=') + continue; + this->optvalue = rest + 1; + } + return argparse_getvalue(this, options, opt_flags); + } + return -2; +} + +int +argparse_init(struct argparse *this, struct argparse_option *options, + const char *const *usage, int flags) +{ + memset(this, 0, sizeof(*this)); + this->options = options; + this->usage = usage; + this->flags = flags; + return 0; +} + +int +argparse_parse(struct argparse *this, int argc, const char **argv) +{ + this->argc = argc - 1; + this->argv = argv + 1; + this->out = argv; + + argparse_options_check(this->options); + + for (; this->argc; this->argc--, this->argv++) { + const char *arg = this->argv[0]; + if (arg[0] != '-' || !arg[1]) { + if (this->flags & ARGPARSE_STOP_AT_NON_OPTION) { + goto end; + } + // if it's not option or is a single char '-', copy verbatimly + this->out[this->cpidx++] = this->argv[0]; + continue; + } + // short option + if (arg[1] != '-') { + this->optvalue = arg + 1; + switch (argparse_short_opt(this, this->options)) { + case -1: + break; + case -2: + goto unknown; + } + while (this->optvalue) { + switch (argparse_short_opt(this, this->options)) { + case -1: + break; + case -2: + goto unknown; + } + } + continue; + } + // if '--' presents + if (!arg[2]) { + this->argc--; + this->argv++; + break; + } + // long option + switch (argparse_long_opt(this, this->options)) { + case -1: + break; + case -2: + goto unknown; + } + continue; + +unknown: + fprintf(stderr, "error: unknown option `%s`\n", this->argv[0]); + argparse_usage(this); + exit(1); + } + +end: + memmove(this->out + this->cpidx, this->argv, + this->argc * sizeof(*this->out)); + this->out[this->cpidx + this->argc] = NULL; + + return this->cpidx + this->argc; +} + +void +argparse_usage(struct argparse *this) +{ + fprintf(stdout, "Usage: %s\n", *this->usage++); + while (*this->usage && **this->usage) + fprintf(stdout, " or: %s\n", *this->usage++); + fputc('\n', stdout); + + const struct argparse_option *options; + + // figure out best width + size_t usage_opts_width = 0; + size_t len; + options = this->options; + for (; options->type != ARGPARSE_OPT_END; options++) { + len = 0; + if ((options)->short_name) { + len += 2; + } + if ((options)->short_name && (options)->long_name) { + len += 2; // separator ", " + } + if ((options)->long_name) { + len += strlen((options)->long_name) + 2; + } + if (options->type == ARGPARSE_OPT_INTEGER) { + len += strlen("="); + } else if (options->type == ARGPARSE_OPT_STRING) { + len += strlen("="); + } + len = ceil((float)len / 4) * 4; + if (usage_opts_width < len) { + usage_opts_width = len; + } + } + usage_opts_width += 4; // 4 spaces prefix + + options = this->options; + for (; options->type != ARGPARSE_OPT_END; options++) { + size_t pos = 0; + int pad = 0; + if (options->type == ARGPARSE_OPT_GROUP) { + fputc('\n', stdout); + pos += fprintf(stdout, "%s", options->help); + fputc('\n', stdout); + continue; + } + pos = fprintf(stdout, " "); + if (options->short_name) { + pos += fprintf(stdout, "-%c", options->short_name); + } + if (options->long_name && options->short_name) { + pos += fprintf(stdout, ", "); + } + if (options->long_name) { + pos += fprintf(stdout, "--%s", options->long_name); + } + if (options->type == ARGPARSE_OPT_INTEGER) { + pos += fprintf(stdout, "="); + } else if (options->type == ARGPARSE_OPT_STRING) { + pos += fprintf(stdout, "="); + } + if (pos <= usage_opts_width) { + pad = usage_opts_width - pos; + } else { + fputc('\n', stdout); + pad = usage_opts_width; + } + fprintf(stdout, "%*s%s\n", pad + 2, "", options->help); + } +} + +int +argparse_help_cb(struct argparse *this, const struct argparse_option *option) +{ + (void)option; + argparse_usage(this); + exit(0); + return 0; +} diff --git a/lib/argparse/argparse.h b/lib/argparse/argparse.h new file mode 100644 index 0000000000..350ee58df4 --- /dev/null +++ b/lib/argparse/argparse.h @@ -0,0 +1,132 @@ +#ifndef ARGPARSE_H +#define ARGPARSE_H +/** + * Command-line arguments parsing library. + * + * This module is inspired by parse-options.c (git) and python's argparse + * module. + * + * Arguments parsing is common task in cli program, but traditional `getopt` + * libraries are not easy to use. This library provides high-level arguments + * parsing solutions. + * + * The program defines what arguments it requires, and `argparse` will figure + * out how to parse those out of `argc` and `argv`, it also automatically + * generates help and usage messages and issues errors when users give the + * program invalid arguments. + * + * Reserved namespaces: + * argparse + * OPT + * Author: Yecheng Fu + */ + +#include +#include +#include +#include +#include +#include + +struct argparse; +struct argparse_option; + +typedef int argparse_callback(struct argparse *this, + const struct argparse_option *option); + +enum argparse_flag { + ARGPARSE_STOP_AT_NON_OPTION = 1, +}; + +enum argparse_option_type { + /* special */ + ARGPARSE_OPT_END, + ARGPARSE_OPT_GROUP, + /* options with no arguments */ + ARGPARSE_OPT_BOOLEAN, + ARGPARSE_OPT_BIT, + /* options with arguments (optional or required) */ + ARGPARSE_OPT_INTEGER, + ARGPARSE_OPT_STRING, +}; + +enum argparse_option_flags { + OPT_NONEG = 1, /* Negation disabled. */ +}; + +/* + * Argparse option struct. + * + * `type`: + * holds the type of the option, you must have an ARGPARSE_OPT_END last in your + * array. + * + * `short_name`: + * the character to use as a short option name, '\0' if none. + * + * `long_name`: + * the long option name, without the leading dash, NULL if none. + * + * `value`: + * stores pointer to the value to be filled. + * + * `help`: + * the short help message associated to what the option does. + * Must never be NULL (except for ARGPARSE_OPT_END). + * + * `callback`: + * function is called when corresponding argument is parsed. + * + * `data`: + * associated data. Callbacks can use it like they want. + * + * `flags`: + * option flags. + * + */ +struct argparse_option { + enum argparse_option_type type; + const char short_name; + const char *long_name; + void *value; + const char *help; + argparse_callback *callback; + intptr_t data; + int flags; +}; + +/* + * argpparse + */ +struct argparse { + // user supplied + const struct argparse_option *options; + const char *const *usage; + int flags; + // internal context + int argc; + const char **argv; + const char **out; + int cpidx; + const char *optvalue; // current option value +}; + +// builtin callbacks +int argparse_help_cb(struct argparse *this, + const struct argparse_option *option); + +// builtin option macros +#define OPT_END() { ARGPARSE_OPT_END } +#define OPT_BOOLEAN(...) { ARGPARSE_OPT_BOOLEAN, __VA_ARGS__ } +#define OPT_BIT(...) { ARGPARSE_OPT_BIT, __VA_ARGS__ } +#define OPT_INTEGER(...) { ARGPARSE_OPT_INTEGER, __VA_ARGS__ } +#define OPT_STRING(...) { ARGPARSE_OPT_STRING, __VA_ARGS__ } +#define OPT_GROUP(h) { ARGPARSE_OPT_GROUP, 0, NULL, NULL, h, NULL } +#define OPT_HELP() OPT_BOOLEAN('h', "help", NULL, "show this help message and exit", argparse_help_cb) + +int argparse_init(struct argparse *this, struct argparse_option *options, + const char *const *usage, int flags); +int argparse_parse(struct argparse *this, int argc, const char **argv); +void argparse_usage(struct argparse *this); + +#endif diff --git a/projects/openrct2.vcxproj b/projects/openrct2.vcxproj index dc4b8b876c..a9edfa7b23 100644 --- a/projects/openrct2.vcxproj +++ b/projects/openrct2.vcxproj @@ -28,6 +28,7 @@ + @@ -35,6 +36,7 @@ + TurnOffAllWarnings diff --git a/projects/openrct2.vcxproj.filters b/projects/openrct2.vcxproj.filters index f70267d16a..f8cb1164cc 100644 --- a/projects/openrct2.vcxproj.filters +++ b/projects/openrct2.vcxproj.filters @@ -59,6 +59,9 @@ {51e38783-5334-464c-8f90-61d725dc8013} + + {7e9587b2-333f-42ca-8a56-b77070828b17} + @@ -419,6 +422,9 @@ Source + + Libraries\argparse + @@ -610,5 +616,8 @@ Source + + Libraries\argparse + \ No newline at end of file diff --git a/src/cmdline.c b/src/cmdline.c index fca9191417..6c24be9d61 100644 --- a/src/cmdline.c +++ b/src/cmdline.c @@ -22,15 +22,28 @@ #ifdef _MSC_VER #include #endif +#include +#include "addresses.h" #include "cmdline.h" #include "openrct2.h" +#include "platform/osinterface.h" typedef struct tm tm_t; +typedef struct argparse_option argparse_option_t; +typedef struct argparse argparse_t; int gExitCode = 0; static void print_launch_information(); +static const char *const usage[] = { + "openrct2 [options] []", + "openrct2 [options]", + "openrct2 intro [options]", + "openrct2 edit [path] [options]", + NULL +}; + /** * A shared entry point to OpenRCT2. The command lines must be parsed before any further action is done. Invalid command lines * will then terminate before any initialisation has even been done. @@ -38,19 +51,49 @@ static void print_launch_information(); */ int cmdline_run(char *argv[], int argc) { - print_launch_information(); + // For argparse's sake, add virtual first argument process path + argc++; + argv--; - if (argc > 0) { - if (_stricmp(argv[0], "edit") == 0) { + // + int version = 0, width = 0, height = 0; + + argparse_option_t options[] = { + OPT_HELP(), + OPT_BOOLEAN('v', "version", &version, "show version information and exit"), + OPT_END() + }; + + argparse_t argparse; + argparse_init(&argparse, options, usage, 0); + argc = argparse_parse(&argparse, argc, argv); + + if (version) { + printf("%s v%s\n", OPENRCT2_NAME, OPENRCT2_VERSION); + printf("%s (%s)\n", OPENRCT2_PLATFORM, OPENRCT2_ARCHITECTURE); + printf("%s\n", OPENRCT2_TIMESTAMP); + return 0; + } + + if (argc != 0) { + if (_stricmp(argv[0], "intro") == 0) { + gOpenRCT2StartupAction = STARTUP_ACTION_INTRO; + } else if (_stricmp(argv[0], "edit") == 0) { gOpenRCT2StartupAction = STARTUP_ACTION_EDIT; if (argc >= 2) strcpy(gOpenRCT2StartupActionPath, argv[1]); } else { - gOpenRCT2StartupAction = STARTUP_ACTION_OPEN; - strcpy(gOpenRCT2StartupActionPath, argv[0]); + if (osinterface_file_exists(argv[0])) { + gOpenRCT2StartupAction = STARTUP_ACTION_OPEN; + strcpy(gOpenRCT2StartupActionPath, argv[0]); + } else { + fprintf(stderr, "error: %s does not exist\n", argv[0]); + return 0; + } } } + print_launch_information(); return 1; } @@ -72,28 +115,4 @@ static void print_launch_information() printf("Time: %s\n", buffer); // TODO Print other potential information (e.g. user, hardware) -} - -//void check_cmdline_arg() -//{ -// int argc; -// char **argv; -// char *args; -// -// args = RCT2_GLOBAL(0x009AC310, char*); -// if (args == (char*)0xFFFFFFFF) -// return; -// RCT2_GLOBAL(0x009AC310, char*) = (char*)0xFFFFFFFF; -// -// argv = CommandLineToArgvA(args, &argc); -// if (argc > 0) { -// if (_stricmp(argv[0], "edit") == 0) { -// if (argc >= 1) -// editor_load_landscape(argv[1]); -// } else { -// rct2_open_file(argv[0]); -// } -// } -// -// LocalFree(argv); -//} \ No newline at end of file +} \ No newline at end of file diff --git a/src/config.c b/src/config.c index d12a86bc26..4200d613c4 100644 --- a/src/config.c +++ b/src/config.c @@ -74,21 +74,23 @@ static const uint16 _defaultShortcutKeys[SHORTCUT_COUNT] = { general_configuration_t gGeneral_config; general_configuration_t gGeneral_config_default = { - 0, // play_intro - 1, // confirmation_prompt - SCREENSHOT_FORMAT_PNG, // screenshot_format - "", // game_path - MEASUREMENT_FORMAT_IMPERIAL, // measurement_format - TEMPERATURE_FORMAT_F, // temperature_format - CURRENCY_POUNDS, // currency_format - 0, // construction_marker_colour - 1, // edge_scrolling - 0, // always_show_gridlines - 1, // landscape_smoothing - 0, // show_height_as_units - 1, // save_plugin_data - 0, // fullscreen mode (default: windowed) - LANGUAGE_ENGLISH_UK + 0, // play_intro + 1, // confirmation_prompt + SCREENSHOT_FORMAT_PNG, // screenshot_format + "", // game_path + MEASUREMENT_FORMAT_IMPERIAL, // measurement_format + TEMPERATURE_FORMAT_F, // temperature_format + CURRENCY_POUNDS, // currency_format + 0, // construction_marker_colour + 1, // edge_scrolling + 0, // always_show_gridlines + 1, // landscape_smoothing + 0, // show_height_as_units + 1, // save_plugin_data + 0, // fullscreen mode (default: windowed) + -1, // window_width + -1, // window_height + LANGUAGE_ENGLISH_UK // language }; sound_configuration_t gSound_config; diff --git a/src/config.h b/src/config.h index 664d57eb58..6092481635 100644 --- a/src/config.h +++ b/src/config.h @@ -130,6 +130,8 @@ typedef struct general_configuration { //new uint8 fullscreen_mode; + sint16 window_width; + sint16 window_height; uint16 language; } general_configuration_t; diff --git a/src/openrct2.c b/src/openrct2.c index b6227145de..8e19f18543 100644 --- a/src/openrct2.c +++ b/src/openrct2.c @@ -59,8 +59,6 @@ void openrct2_launch() RCT2_GLOBAL(RCT2_ADDRESS_RUN_INTRO_TICK_PART, uint8) = 0; RCT2_GLOBAL(RCT2_ADDRESS_SCREEN_FLAGS, uint8) = SCREEN_FLAGS_PLAYING; - - // TODO fix, crashes on first game logic update break; case STARTUP_ACTION_EDIT: if (strlen(gOpenRCT2StartupActionPath) == 0) diff --git a/src/platform/osinterface.c b/src/platform/osinterface.c index ed0090b4d4..de2a92a862 100644 --- a/src/platform/osinterface.c +++ b/src/platform/osinterface.c @@ -178,11 +178,11 @@ static void osinterface_create_window() osinterface_load_cursors(); RCT2_CALLPROC_EBPSAFE(0x0068371D); - width = RCT2_GLOBAL(RCT2_ADDRESS_CONFIG_RESOLUTION_WIDTH, sint16); - height = RCT2_GLOBAL(RCT2_ADDRESS_CONFIG_RESOLUTION_HEIGHT, sint16); + width = gGeneral_config.window_width; + height = gGeneral_config.window_height; - width = 640; - height = 480; + if (width == -1) width = 640; + if (height == -1) height = 480; } RCT2_GLOBAL(0x009E2D8C, sint32) = 0; diff --git a/src/rct2.c b/src/rct2.c index 9a2e00ded7..d6c13feee2 100644 --- a/src/rct2.c +++ b/src/rct2.c @@ -226,16 +226,19 @@ int rct2_open_file(const char *path) return 0; extension++; - if (_stricmp(extension, "sv6")) { + if (_stricmp(extension, "sv6") == 0) { game_load_save(path); - } else if (!_stricmp(extension, "sc6")) { + return 1; + } else if (_stricmp(extension, "sc6") == 0) { // TODO scenario install rct_scenario_basic scenarioBasic; strcpy(scenarioBasic.path, path); scenario_load_and_play_from_path(scenarioBasic.path); - } else if (!_stricmp(extension, "td6") || !_stricmp(extension, "td4")) { + } else if (_stricmp(extension, "td6") == 0 || _stricmp(extension, "td4") == 0) { // TODO track design install } + + return 0; } // rct2: 0x00407DB0 diff --git a/src/scenario.c b/src/scenario.c index 41de056a7a..f1659be109 100644 --- a/src/scenario.c +++ b/src/scenario.c @@ -318,6 +318,8 @@ int scenario_load_and_play_from_path(const char *path) gfx_invalidate_screen(); RCT2_GLOBAL(0x009DEA66, uint16) = 0; RCT2_GLOBAL(0x009DEA5C, uint16) = 62000; // (doesn't appear to ever be read) + + return 1; }