An emulator, assembler, and disassembler for the Sega Game Gear
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

361 lines
11 KiB

  1. /* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
  2. Released under the terms of the MIT License. See LICENSE for details. */
  3. #include <dirent.h>
  4. #include <errno.h>
  5. #include <stdio.h>
  6. #include <stdlib.h>
  7. #include <string.h>
  8. #include <sys/types.h>
  9. #include "config.h"
  10. #include "logging.h"
  11. #include "util.h"
  12. #include "version.h"
  13. /*
  14. Print command-line help/usage.
  15. */
  16. static void print_help(const char *arg1)
  17. {
  18. printf("%s [-h] [-v] [-f] [-s <n>] [<rom_path>] ...\n"
  19. "\n"
  20. "basic options:\n"
  21. " -h, --help show this help message and exit\n"
  22. " -v, --version show crater's version number and exit\n"
  23. " -f, --fullscreen enable fullscreen mode\n"
  24. " <rom_path> path to the rom file to execute; if not given, will look\n"
  25. " in the roms/ directory and prompt the user\n"
  26. "\n"
  27. "advanced options:\n"
  28. " -g, --debug show logging information while running; add twice (-g -g)\n"
  29. " to show more detailed logs, including an emulator trace\n"
  30. " -s, --scale <n> scale the game screen by an integer factor\n"
  31. " (applies to windowed mode only)\n"
  32. " -a, --assemble <in> [<out>] convert z80 assembly source code into a\n"
  33. " binary file that can be run by crater\n"
  34. " -d, --disassemble <in> [<out>] convert a binary file into z80 assembly\n"
  35. " source code\n"
  36. " -r, --overwrite allow crater to write assembler output to the same\n"
  37. " filename as the input\n",
  38. arg1);
  39. }
  40. /*
  41. Print crater's version.
  42. */
  43. static void print_version()
  44. {
  45. printf("crater %s\n", CRATER_VERSION);
  46. }
  47. /*
  48. Return whether the given string ends with the given suffix.
  49. */
  50. static bool ends_with(const char *input, const char *suffix)
  51. {
  52. size_t ilen = strlen(input), slen = strlen(suffix);
  53. if (ilen < slen)
  54. return false;
  55. return strcmp(input + (ilen - slen), suffix) == 0;
  56. }
  57. /*
  58. Load all potential ROM files in roms/ into a data structure.
  59. */
  60. static int get_rom_paths(char ***path_ptr)
  61. {
  62. DIR *dirp;
  63. struct dirent *entry;
  64. char **paths = NULL, *path;
  65. int psize = 8, npaths = 0;
  66. dirp = opendir(ROMS_DIR);
  67. if (dirp) {
  68. paths = cr_malloc(sizeof(char*) * psize);
  69. while ((entry = readdir(dirp))) {
  70. path = entry->d_name;
  71. if (ends_with(path, ".gg") || ends_with(path, ".bin")) {
  72. if (npaths >= psize) {
  73. paths = cr_realloc(paths, sizeof(char*) * (psize *= 2));
  74. }
  75. paths[npaths] = cr_malloc(sizeof(char*) *
  76. (strlen(path) + strlen(ROMS_DIR) + 1));
  77. strcpy(paths[npaths], ROMS_DIR "/");
  78. strcat(paths[npaths], path);
  79. npaths++;
  80. }
  81. }
  82. closedir(dirp);
  83. } else {
  84. WARN_ERRNO("couldn't open '" ROMS_DIR "/'")
  85. }
  86. *path_ptr = paths;
  87. return npaths;
  88. }
  89. /*
  90. Find all potential ROM files in the roms/ directory, then ask the user
  91. which one they want to run.
  92. */
  93. static char* get_rom_path_from_user()
  94. {
  95. char **paths, *path, *input = NULL;
  96. int npaths, i;
  97. long int index;
  98. size_t size = 0;
  99. ssize_t len;
  100. npaths = get_rom_paths(&paths);
  101. for (i = 0; i < npaths; i++)
  102. printf("[%2d] %s\n", i + 1, paths[i]);
  103. if (npaths)
  104. printf("Enter a ROM number from above, or the path to a ROM image: ");
  105. else
  106. printf("Enter the path to a ROM image: ");
  107. len = getline(&input, &size, stdin);
  108. if (!input)
  109. OUT_OF_MEMORY()
  110. if (len > 0 && input[len - 1] == '\n')
  111. input[len - 1] = '\0';
  112. index = strtol(input, NULL, 10);
  113. if (index < 1 || index > npaths)
  114. path = input;
  115. else
  116. path = paths[index - 1];
  117. for (i = 0; i < npaths; i++) {
  118. if (paths[i] != path)
  119. free(paths[i]);
  120. }
  121. free(paths);
  122. return path;
  123. }
  124. /*
  125. Parse the command-line arguments for any special flags.
  126. */
  127. static int parse_args(Config *config, int argc, char *argv[])
  128. {
  129. char *arg, *path;
  130. int i, paths_read = 0;
  131. for (i = 1; i < argc; i++) {
  132. arg = argv[i];
  133. if (arg[0] != '-') {
  134. // Parsing a path or other variable:
  135. if (paths_read >= 2) {
  136. ERROR("too many arguments given - emulator mode accepts one "
  137. "ROM file,\nand assembler mode accepts one input file "
  138. "and one output file")
  139. return CONFIG_EXIT_FAILURE;
  140. }
  141. path = cr_malloc(sizeof(char) * (strlen(arg) + 1));
  142. strcpy(path, arg);
  143. if (paths_read == 1) {
  144. /* If this is the second path given, it can only be an output
  145. file for the assembler. If the assembler is not enabled by
  146. subsequent arguments, we'll throw an error. */
  147. config->dst_path = path;
  148. } else {
  149. /* Otherwise, put the argument in the expected place. If we put
  150. it in rom_path and the assembler is enabled by later
  151. arguments, we'll move it. */
  152. if (config->assemble || config->disassemble)
  153. config->src_path = path;
  154. else
  155. config->rom_path = path;
  156. }
  157. paths_read++;
  158. continue;
  159. }
  160. // Parsing an option:
  161. do
  162. arg++;
  163. while (arg[0] == '-');
  164. if (!strcmp(arg, "h") || !strcmp(arg, "help")) {
  165. print_help(argv[0]);
  166. return CONFIG_EXIT_SUCCESS;
  167. } else if (!strcmp(arg, "v") || !strcmp(arg, "version")) {
  168. print_version();
  169. return CONFIG_EXIT_SUCCESS;
  170. } else if (!strcmp(arg, "f") || !strcmp(arg, "fullscreen")) {
  171. config->fullscreen = true;
  172. } else if (!strcmp(arg, "s") || !strcmp(arg, "scale")) {
  173. if (i++ >= argc) {
  174. ERROR("the scale option requires an argument")
  175. return CONFIG_EXIT_FAILURE;
  176. }
  177. arg = argv[i];
  178. long scale = strtol(arg, NULL, 10);
  179. if (scale <= 0 || scale > SCALE_MAX) {
  180. ERROR("scale factor of %s is not an integer or is out of range", arg)
  181. return CONFIG_EXIT_FAILURE;
  182. }
  183. config->scale = scale;
  184. } else if (!strcmp(arg, "g") || !strcmp(arg, "debug")) {
  185. config->debug++;
  186. } else if (!strcmp(arg, "a") || !strcmp(arg, "assemble")) {
  187. if (paths_read >= 1) {
  188. config->src_path = config->rom_path;
  189. config->rom_path = NULL;
  190. }
  191. config->assemble = true;
  192. } else if (!strcmp(arg, "d") || !strcmp(arg, "disassemble")) {
  193. if (paths_read >= 1) {
  194. config->src_path = config->rom_path;
  195. config->rom_path = NULL;
  196. }
  197. config->disassemble = true;
  198. } else if (!strcmp(arg, "r") || !strcmp(arg, "overwrite")) {
  199. config->overwrite = true;
  200. } else {
  201. ERROR("unknown argument: %s", argv[i])
  202. return CONFIG_EXIT_FAILURE;
  203. }
  204. }
  205. if (!config->assemble && !config->disassemble && paths_read >= 2) {
  206. ERROR("too many arguments given - emulator mode accepts one ROM file")
  207. return CONFIG_EXIT_FAILURE;
  208. }
  209. if (!config->assemble && !config->disassemble && paths_read == 0) {
  210. path = get_rom_path_from_user();
  211. if (path[0] == '\0') {
  212. ERROR("no ROM image given")
  213. return CONFIG_EXIT_FAILURE;
  214. }
  215. config->rom_path = path;
  216. }
  217. return CONFIG_OK;
  218. }
  219. /*
  220. If no output file is specified for the assembler, this function picks a
  221. filename based on the input file, replacing its extension with ".gg" or
  222. ".asm" (or adding it, if none is present).
  223. */
  224. static void guess_assembler_output_file(Config* config)
  225. {
  226. char *src = config->src_path, *ptr = src + strlen(src) - 1,
  227. *ext = config->assemble ? ".gg" : ".asm";
  228. size_t until_ext = ptr - src + 1;
  229. do {
  230. if (*ptr == '.') {
  231. until_ext = ptr - src;
  232. break;
  233. }
  234. } while (ptr-- >= src);
  235. config->dst_path = cr_malloc(sizeof(char) * (until_ext + 5));
  236. strcpy(stpncpy(config->dst_path, src, until_ext), ext);
  237. }
  238. /*
  239. Ensure that the combination of arguments in a config object are valid.
  240. Some modifications are made in the case of missing arguments, like guessing
  241. the filenames for assembler output files.
  242. */
  243. static bool sanity_check(Config* config)
  244. {
  245. bool assembler = config->assemble || config->disassemble;
  246. if (config->fullscreen && config->scale > 1) {
  247. ERROR("cannot specify a scale in fullscreen mode")
  248. return false;
  249. } else if (config->assemble && config->disassemble) {
  250. ERROR("cannot assemble and disassemble at the same time")
  251. return false;
  252. } else if (assembler && (config->fullscreen || config->scale > 1)) {
  253. ERROR("cannot specify emulator options in assembler mode")
  254. return false;
  255. } else if (assembler && !config->src_path) {
  256. ERROR("assembler mode requires an input file")
  257. return false;
  258. }
  259. if (assembler && !config->dst_path) {
  260. guess_assembler_output_file(config);
  261. }
  262. if (assembler && !config->overwrite && !strcmp(config->src_path, config->dst_path)) {
  263. ERROR("refusing to overwrite the assembler input file; pass -r to override")
  264. return false;
  265. }
  266. return true;
  267. }
  268. /*
  269. Create a new config object and load default values into it.
  270. Return value is one of CONFIG_OK, CONFIG_EXIT_SUCCESS, CONFIG_EXIT_FAILURE
  271. and indicates how the caller should proceed. If the caller should exit,
  272. then the config object should *not* be freed; otherwise it should be freed
  273. with config_destroy() when the caller is ready.
  274. */
  275. int config_create(Config** config_ptr, int argc, char* argv[])
  276. {
  277. Config *config = cr_malloc(sizeof(Config));
  278. int retval;
  279. config->debug = 0;
  280. config->assemble = false;
  281. config->disassemble = false;
  282. config->fullscreen = false;
  283. config->scale = 1;
  284. config->rom_path = NULL;
  285. config->src_path = NULL;
  286. config->dst_path = NULL;
  287. config->overwrite = false;
  288. retval = parse_args(config, argc, argv);
  289. if (retval == CONFIG_OK && !sanity_check(config))
  290. retval = CONFIG_EXIT_FAILURE;
  291. if (retval != CONFIG_OK) {
  292. config_destroy(config);
  293. return retval;
  294. }
  295. *config_ptr = config;
  296. return CONFIG_OK;
  297. }
  298. /*
  299. Destroy a config object previously created with config_create().
  300. */
  301. void config_destroy(Config *config)
  302. {
  303. free(config->rom_path);
  304. free(config->src_path);
  305. free(config->dst_path);
  306. free(config);
  307. }
  308. /*
  309. @DEBUG_LEVEL
  310. Print out all config arguments to stdout.
  311. */
  312. void config_dump_args(const Config* config)
  313. {
  314. DEBUG("Dumping arguments:")
  315. DEBUG("- debug: %d", config->debug)
  316. DEBUG("- assemble: %s", config->assemble ? "true" : "false")
  317. DEBUG("- disassemble: %s", config->disassemble ? "true" : "false")
  318. DEBUG("- fullscreen: %s", config->fullscreen ? "true" : "false")
  319. DEBUG("- scale: %d", config->scale)
  320. DEBUG("- rom_path: %s", config->rom_path ? config->rom_path : "(null)")
  321. DEBUG("- src_path: %s", config->src_path ? config->src_path : "(null)")
  322. DEBUG("- dst_path: %s", config->dst_path ? config->dst_path : "(null)")
  323. DEBUG("- overwrite: %s", config->overwrite ? "true" : "false")
  324. }