/* * This software is licensed under the terms of the MIT License. * See COPYING for further information. * --- * Copyright (c) 2011-2024, Lukas Weber . * Copyright (c) 2012-2024, Andrei Alexeyev . */ #include "cli.h" #include "cutscenes/cutscene.h" #include "cutscenes/scenes.h" #include "difficulty.h" #include "log.h" #include "plrmodes.h" #include "stageinfo.h" #include "util.h" #include "util/env.h" #include "util/io.h" #include "version.h" #include struct TsOption { struct option opt; const char *help; const char *argname; }; enum { OPT_RENDERER = INT_MIN, OPT_CUTSCENE, OPT_CUTSCENE_LIST, OPT_FORCE_INTRO, OPT_REREPLAY, OPT_POPCACHE, OPT_UNLOCKALL, }; static void print_help(struct TsOption* opts) { tsfprintf(stdout, "Usage: taisei [OPTIONS]\nTaisei is an open source Tōhō Project fangame.\n\nOptions:\n"); int margin = 20; for(struct TsOption *opt = opts; opt->opt.name; opt++) { if(opt->opt.val > 0) { tsfprintf(stdout, " -%c, --%s ", opt->opt.val, opt->opt.name); } else { tsfprintf(stdout, " --%s ", opt->opt.name); } int length = margin-(int)strlen(opt->opt.name); if(opt->argname) { tsfprintf(stdout, "%s", opt->argname); length -= (int)strlen(opt->argname); } for(int i = 0; i < length; i++) { fputc(' ', stdout); } fputs(" ", stdout); if(opt->argname && strchr(opt->help, '%')) { tsfprintf(stdout, opt->help, opt->argname); } else { tsfprintf(stdout, "%s", opt->help); } tsfprintf(stdout, "\n"); } } int cli_args(int argc, char **argv, CLIAction *a) { const char *const _renderer_list = #define R(r) ","#r TAISEI_BUILDCONF_RENDERER_BACKENDS #undef R ; char renderer_list[strlen(_renderer_list) + 2]; snprintf(renderer_list, sizeof(renderer_list), "{%s}", _renderer_list+1); struct TsOption taisei_opts[] = { {{"replay", required_argument, 0, 'r'}, "Play a replay from %s", "FILE"}, {{"verify-replay", required_argument, 0, 'R'}, "Play a replay from %s in headless mode, crash as soon as it desyncs unless --rereplay is used", "FILE"}, {{"rereplay", required_argument, 0, OPT_REREPLAY}, "Re-record replay into %s; specify input with -r or -R", "OUTFILE"}, #ifdef DEBUG {{"play", no_argument, 0, 'p'}, "Play a specific stage"}, {{"sid", required_argument, 0, 'i'}, "Select stage by %s", "ID"}, {{"diff", required_argument, 0, 'd'}, "Select a difficulty (Easy/Normal/Hard/Lunatic)", "DIFF"}, {{"shotmode", required_argument, 0, 's'}, "Select a shotmode (marisaA/youmuA/marisaB/youmuB)", "SMODE"}, {{"dumpstages", no_argument, 0, 'u'}, "Print a list of all stages in the game"}, {{"vfs-tree", required_argument, 0, 't'}, "Print the virtual filesystem tree starting from %s", "PATH"}, {{"cutscene", required_argument, 0, OPT_CUTSCENE}, "Play cutscene by numeric %s and exit", "ID"}, {{"list-cutscenes", no_argument, 0, OPT_CUTSCENE_LIST}, "List all registered cutscenes with their numeric IDs and names, then exit" }, {{"intro", no_argument, 0, OPT_FORCE_INTRO}, "Play the intro cutscene even if already seen"}, {{"skip-to-bookmark", required_argument, 0, 'b'}, "Fast-forward stage to a specific STAGE_BOOKMARK call"}, {{"unlock-all", no_argument, 0, OPT_UNLOCKALL}, "Unlock all content"}, #endif {{"frameskip", optional_argument, 0, 'f'}, "Disable FPS limiter, render only every %s frame", "FRAME"}, {{"credits", no_argument, 0, 'c'}, "Show the credits scene and exit"}, {{"renderer", required_argument, 0, OPT_RENDERER}, "Choose the rendering backend", renderer_list}, {{"populate-cache", no_argument, 0, OPT_POPCACHE}, "Attempt to load all available resources, populating the cache, then exit"}, {{"width", required_argument, 0, 'W'}, "Set window width", "WIDTH"}, {{"height", required_argument, 0, 'H'}, "Set window height", "HEIGHT"}, {{"help", no_argument, 0, 'h'}, "Print help and exit"}, {{"version", no_argument, 0, 'v'}, "Print version and exit"}, { 0 } }; memset(a, 0, sizeof(*a)); int nopts = ARRAY_SIZE(taisei_opts); struct option opts[nopts]; char optc[2*nopts+1]; char *ptr = optc; for(int i = 0; i < nopts; i++) { opts[i] = taisei_opts[i].opt; if(opts[i].val <= 0) { continue; } *ptr = opts[i].val; ptr++; if(opts[i].has_arg != no_argument) { *ptr = ':'; ptr++; if(opts[i].has_arg == optional_argument) { *ptr = ':'; ptr++; } } } *ptr = 0; // on OS X, programs get passed some strange parameter when they are run from bundles. for(int i = 0; i < argc; i++) { if(strstartswith(argv[i],"-psn_")) argv[i][0] = 0; } int c; uint16_t stageid = 0; PlayerMode *plrmode = NULL; while((c = getopt_long(argc, argv, optc, opts, 0)) != -1) { char *endptr = NULL; switch(c) { case 'h': case '?': print_help(taisei_opts); // a->type = CLI_Quit; exit(0); break; case 'r': a->type = CLI_PlayReplay; stralloc(&a->filename, optarg); break; case 'R': a->type = CLI_VerifyReplay; stralloc(&a->filename, optarg); break; case OPT_REREPLAY: stralloc(&a->out_replay, optarg); env_set("TAISEI_REPLAY_DESYNC_CHECK_FREQUENCY", 1, false); break; case 'p': a->type = CLI_SelectStage; break; case 'i': stageid = strtol(optarg, &endptr, 16); if(!*optarg || endptr == optarg) log_fatal("Stage id '%s' is not a number", optarg); break; case 'u': a->type = CLI_DumpStages; break; case 'd': a->diff = D_Any; for(int i = D_Easy ; i <= NUM_SELECTABLE_DIFFICULTIES; i++) { if(strcasecmp(optarg, difficulty_name(i)) == 0) { a->diff = i; break; } } if(a->diff == D_Any) { char *end; int dval = strtol(optarg, &end, 10); if(dval >= D_Easy && dval <= D_Lunatic && end == optarg + strlen(optarg)) { a->diff = dval; } } if(a->diff == D_Any) { log_fatal("Invalid difficulty '%s'", optarg); } break; case 's': if(!(plrmode = plrmode_parse(optarg))) log_fatal("Invalid shotmode '%s'", optarg); break; case 'f': a->frameskip = 1; if(optarg) { a->frameskip = strtol(optarg, &endptr, 10); if(endptr == optarg) { log_fatal("Frameskip value '%s' is not a number", optarg); } if(a->frameskip < 0) { a->frameskip = INT_MAX; } } break; case 't': a->type = CLI_DumpVFSTree, stralloc(&a->filename, optarg ? optarg : ""); break; case 'c': a->type = CLI_Credits; break; case OPT_RENDERER: env_set("TAISEI_RENDERER", optarg, true); break; case OPT_CUTSCENE: a->type = CLI_Cutscene; a->cutscene = strtol(optarg, &endptr, 16); if(!*optarg || endptr == optarg || (uint)a->cutscene >= NUM_CUTSCENE_IDS) { log_fatal("Invalid cutscene ID '%s'", optarg); } break; case OPT_CUTSCENE_LIST: for(CutsceneID i = 0; i < NUM_CUTSCENE_IDS; ++i) { const Cutscene *cs = g_cutscenes + i; tsfprintf(stdout, "%2d : %s\n", i, cs->phases ? cs->name : "!! UNIMPLEMENTED !!"); } exit(0); case OPT_FORCE_INTRO: a->force_intro = true; break; case 'b': env_set("TAISEI_SKIP_TO_BOOKMARK", optarg, true); break; case 'v': tsfprintf(stdout, "%s %s\n", TAISEI_VERSION_FULL, TAISEI_VERSION_BUILD_TYPE); exit(0); case OPT_POPCACHE: env_set("TAISEI_AGGRESSIVE_PRELOAD", 1, true); a->type = CLI_QuitLate; break; case OPT_UNLOCKALL: a->unlock_all = true; break; case 'W': a->width = strtol(optarg, NULL, 10); break; case 'H': a->height = strtol(optarg, NULL, 10); break; default: UNREACHABLE; } } if(stageid) { switch(a->type) { case CLI_PlayReplay: case CLI_VerifyReplay: case CLI_SelectStage: if(stageinfo_get_by_id(stageid) == NULL) { log_fatal("Invalid stage id: %X", stageid); } break; default: log_warn("--sid was ignored"); break; } } if(plrmode) { if(a->type == CLI_SelectStage) { a->plrmode = plrmode; } else { log_warn("--shotmode was ignored"); } } a->stageid = stageid; if(a->type == CLI_SelectStage && !stageid) { log_fatal("StageSelect mode, but no stage id was given"); } if(a->out_replay && a->type != CLI_PlayReplay && a->type != CLI_VerifyReplay) { log_fatal("--rereplay requires --replay or --verify-replay"); } return 0; } void free_cli_action(CLIAction *a) { mem_free(a->filename); a->filename = NULL; mem_free(a->out_replay); a->out_replay = NULL; }