Mercurial > hg > batmud > maputils
view src/stitchmap.c @ 2833:d0e186348cb2 default tip
Add mention of soft level limitation to 'Eightleg woods'.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Sun, 26 May 2024 20:33:53 +0300 |
parents | e96e757ab01e |
children |
line wrap: on
line source
/* * stitchmap - Compute complete ASCII map by stitching pieces * together based on a matcher and coordinates * * Programmed by Matti 'ccr' Hämäläinen <ccr@tnsp.org> * (C) Copyright 2006-2022 Tecnic Software productions (TNSP) */ #include "libmaputils.h" #include "th_args.h" #include "th_string.h" #define MAX_FILES (256) /* Variables */ int nsrcFiles = 0; char *srcFiles[MAX_FILES], *destFile = NULL; int optInitialX = 0, optInitialY = 0, optMapFactor = 10, optRounds = 100, optReset = 50; bool optHardDrop = false, optDumpRejected = false, optAdjustBlocks = false, optWalkMode = false; float optMatch = 40.0; char *optInitialMap = NULL, *optCleanChars = " *@", optCenterCh = -1; /* Arguments */ static const th_optarg optList[] = { { 0, '?', "help", "Show this help", OPT_NONE }, { 1, 'o', "output", "Specify output file", OPT_ARGREQ }, { 2, 'v', "verbose", "Be more verbose", OPT_NONE }, { 3, 'q', "quiet", "Be quiet", OPT_NONE }, { 4, 'm', "match", "Match percentage", OPT_ARGREQ }, { 5, 'r', "rounds", "Processing timeout # rounds", OPT_ARGREQ }, { 12,'R', "reset", "Round reset after", OPT_ARGREQ }, { 11,'d', "drop", "Use hard dropping (bailout on first mismatch)", OPT_NONE }, { 10,'I', "initial", "Initial map file", OPT_ARGREQ }, { 14,'x', "initial-x", "Initial map X offset", OPT_ARGREQ }, { 15,'y', "initial-y", "Initial map Y offset", OPT_ARGREQ }, { 16,'D', "dump", "Dump rejected pieces", OPT_NONE }, { 17,'a', "adjust-size","Adjust to variable size blocks", OPT_NONE }, { 18,'W', "walk-mode", "Walk mode (instead of ship mode)", OPT_NONE }, { 19,'c', "clean-chars","Characters to filter from input", OPT_ARGREQ }, { 20,'C', "center-char","Center character symbol (for -a)", OPT_ARGREQ }, }; static const int optListN = sizeof(optList) / sizeof(optList[0]); void argShowHelp() { th_print_banner(stdout, th_prog_name, "[options] <inputfile> [inputfile#2..]"); th_args_help(stdout, optList, optListN, 0, 80 - 2); } bool argHandleOpt(const int optN, char *optArg, char *currArg) { switch (optN) { case 0: argShowHelp(); exit(0); break; case 1: destFile = optArg; THMSG(1, "Output file '%s'\n", destFile); break; case 2: th_verbosity++; break; case 3: th_verbosity = -1; break; case 4: optMatch = atof(optArg); THMSG(1, "Match at %1.4f%%\n", optMatch); break; case 5: optRounds = atoi(optArg); THMSG(1, "Processing rounds timeout %d\n", optRounds); break; case 12: optReset = atoi(optArg); THMSG(1, "Round reset after %d blocks\n", optReset); break; case 10: optInitialMap = optArg; THMSG(1, "Using initial map file = '%s'\n", optArg); break; case 11: optHardDrop = true; THMSG(1, "Using hard dropping (instant bailout on mismatch).\n"); break; case 14: optInitialX = atoi(optArg); THMSG(1, "Initial map X offset = %d\n", optInitialX); break; case 15: optInitialY = atoi(optArg); THMSG(1, "Initial map Y offset = %d\n", optInitialY); break; case 16: THMSG(1, "Dumping rejected blocks.\n"); optDumpRejected = true; break; case 17: THMSG(1, "Handling variable size blocks via adjustment.\n"); optAdjustBlocks = true; break; case 18: THMSG(1, "Walk mode enabled.\n"); optWalkMode = true; break; case 19: optCleanChars = optArg; THMSG(1, "Removing '%s' from input blocks\n", optArg); break; case 20: if (strlen(optArg) != 1) { THERR("Invalid argument '%s' for -C, only one character symbol can be specified.\n", optArg); return false; } else { optCenterCh = optArg[0]; THMSG(1, "Using '%c' as centering character symbol\n", optCenterCh); } break; default: THERR("Unknown option '%s'.\n", currArg); return false; break; } return true; } bool argHandleFile(char *currArg) { if (nsrcFiles < MAX_FILES) { srcFiles[nsrcFiles] = currArg; nsrcFiles++; } else { THERR("Too many input files specified (%d max)!\n", MAX_FILES); return false; } return true; } /* Calculate matching percentage of given block against a map, * with using specified x/y offsets. */ float matchBlock(MapBlock *map, MapBlock *match, const int ox, const int oy, const bool hardDrop) { int n, k; n = k = 0; for (int y = 0; y < match->height; y++) for (int x = 0; x < match->width; x++) { const int dx = ox + x, dy = oy + y; int c1, c2; k++; if (dx >= 0 && dx < map->width && dy >= 0 && dy < map->height) c2 = map->data[(dy * map->scansize) + dx]; else c2 = 0; c1 = match->data[(y * match->scansize) + x]; if (c1 == 0 || c2 == 0) { // Both empty ... } else if (c1 != 0 && c1 == c2) { // Exact match, increase % n++; } else if (hardDrop) { // Mismatch, return failure return -1; } } if (k > 0) return ((float) n * 100.0f) / (float) k; else return 0.0f; } bool adjustBlockCenter(MapBlock *block, const char centerCh) { for (int yc = 0; yc < block->height; yc++) for (int xc = 0; xc < block->width; xc++) { int ch = block->data[(yc * block->scansize) + xc]; if (ch == centerCh) { block->xc -= xc; block->yc -= yc; return true; } } return false; } /* Parse next block (marked by string optPattern) from * input stream into a mapblock, return NULL if not found or error. */ bool getLineData(FILE *inFile, char *buf, const size_t bufSize) { if (feof(inFile) || !fgets(buf, bufSize, inFile)) { THERR("Unexpected end of file.\n"); return false; } else return true; } size_t getLineWidth(char *buf, const size_t bufSize) { size_t width; for (width = 0; width + 1 < bufSize && buf[width] != 0 && buf[width] != '\r' && buf[width] != '\n'; width++); buf[width] = 0; return width; } MapBlock * parseBlock(FILE *inFile) { MapBlock *tmp = NULL; int xc, yc, tmpW, tmpH; char str[4096], *pos = NULL; bool isFound; size_t i; isFound = false; while (!feof(inFile) && !isFound && fgets(str, sizeof(str), inFile)) { if ((pos = strstr(str, ": !!PLR: ")) != NULL && (strstr(str, "party") || strstr(str, "report"))) isFound = true; } if (!isFound) { THERR("No block start marker found.\n"); goto err; } // Trim end for (i = 0; i < sizeof(str) && str[i] && str[i] != '\r' && str[i] != '\n'; i++); str[i] = 0; if (sscanf(pos, ": !!PLR: %d,%d", &xc, &yc) < 2) { THERR("Could not get location coordinates:\n'%s'\n", str); goto err; } // Check for discardable lines if (!getLineData(inFile, str, sizeof(str))) goto err; if (!th_strcasecmp(str, "You glance around.")) { if (!getLineData(inFile, str, sizeof(str))) goto err; } // New 'map' output in city maps has an empty line before the data, ignore it if ((tmpW = getLineWidth(str, sizeof(str))) <= 9) { if (!getLineData(inFile, str, sizeof(str))) goto err; tmpW = getLineWidth(str, sizeof(str)); } if (optWalkMode) { switch (tmpW) { case 31: tmpH = 17; break; case 13: tmpH = 7; break; case 9: tmpH = 9; break; default: THERR("Invalid block width %d [%d, %d], rejecting.\n", tmpW, xc, yc); return NULL; break; } } else tmpH = tmpW - 8; if ((tmp = mapBlockAlloc(tmpW, tmpH)) == NULL) { THERR("Could not allocate mapblock (%d, %d)\n", tmpW, tmpH); goto err; } tmp->xc = xc; tmp->yc = yc; yc = 0; do { i = getLineWidth(str, tmpW); if (i != (size_t) tmp->width) { THERR("Broken block, line width %" PRIu_SIZE_T " != %d!\n", i, tmp->width); goto err; } for (xc = 0; xc < tmp->width; xc++) { tmp->data[(yc * tmp->scansize) + xc] = str[xc]; } yc++; } while (!feof(inFile) && (yc < tmp->height) && fgets(str, sizeof(str), inFile)); if (yc < tmp->height) { THERR("Broken block, height %d < %d\n", yc, tmp->height); goto err; } return tmp; err: mapBlockFree(tmp); return NULL; } /* Find the min/max of given coordinates. */ void findWorldSize(MapBlock *tmp, int *worldX0, int *worldY0, int *worldX1, int *worldY1) { if (tmp->xc < *worldX0) *worldX0 = tmp->xc; if ((tmp->xc + tmp->width) > *worldX1) *worldX1 = (tmp->xc + tmp->width); if (tmp->yc < *worldY0) *worldY0 = tmp->yc; if ((tmp->yc + tmp->height) > *worldY1) *worldY1 = (tmp->yc + tmp->height); } int main(int argc, char *argv[]) { int res = 0, i, currRounds, nmapBlocks = 0, currBlocks, worldX0, worldY0, worldX1, worldY1, offsetX, offsetY; bool isOK; MapBlock *worldMap = NULL, *initialMap = NULL; MapBlock **mapBlocks = NULL; th_init("stitchmap", "Yet Another ASCII Map Auto-Stitcher", "0.5", NULL, NULL); th_verbosity = 1; // Parse arguments if (!th_args_process(argc, argv, optList, optListN, argHandleOpt, argHandleFile, OPTH_BAILOUT)) { res = 1; goto out; } if (nsrcFiles < 1) { THERR("Nothing to do. (try --help)\n"); res = 1; goto out; } // Read initial map if (optInitialMap) { THMSG(1, "Reading initial map '%s'\n", optInitialMap); initialMap = mapBlockParseFile(optInitialMap, false); if (initialMap) { THMSG(2, "Initial dimensions %d x %d\n", initialMap->width, initialMap->height); mapBlockClean(initialMap, (unsigned char*) " ", 1); } else { THERR("Initial map could not be loaded!\n"); res = -1; goto out; } } // Read in continuous mapdata and parse it into map blocks for (nmapBlocks = i = 0; i < nsrcFiles; i++) { FILE *tmpFile = NULL; char centerCh; if (optCenterCh > 0) { centerCh = optCenterCh; } else { if (optWalkMode) centerCh = '@'; else centerCh = '*'; } if ((tmpFile = fopen(srcFiles[i], "rb")) == NULL) { THERR("Error opening input file '%s'!\n", srcFiles[i]); res = -16; goto out; } while (!feof(tmpFile)) { MapBlock *tmp; if ((tmp = parseBlock(tmpFile)) != NULL) { if (optAdjustBlocks && !adjustBlockCenter(tmp, centerCh)) { THERR("Block center not found, rejected.\n"); mapBlockFree(tmp); } else { nmapBlocks++; mapBlocks = (MapBlock **) th_realloc(mapBlocks, sizeof(MapBlock *) * nmapBlocks); if (mapBlocks == NULL) { fclose(tmpFile); THERR("Could not allocate/extend mapblock pointer structure (#%d)\n", nmapBlocks); res = -18; goto out; } mapBlockClean(tmp, (unsigned char *) optCleanChars, strlen(optCleanChars)); mapBlocks[nmapBlocks - 1] = tmp; } } } fclose(tmpFile); } THMSG(1, "Total of %d mapblocks read.\n", nmapBlocks); if (nmapBlocks <= 0) { THERR("No mapblocks, nothing to do.\n"); res = -11; goto out; } /* ALGORITHM * --------- find dimensions of the world map: for (each block in list) { if (block->x < mapx0) mapx0 = block->x; else if (block->x+block->w > mapx1) mapx1 = block->x+block->w; ... } allocate map start placing blocks: for (n = 0; n < rounds; n++) { for (each block in list) { if (check match % at given coordinates > threshold) place block } if (all blocks placed) break } */ // If initial map is available, find a match and determine coords if (initialMap) { initialMap->xc = optInitialX; initialMap->yc = optInitialY; } // Clear marks for (i = 0; i < nmapBlocks; i++) mapBlocks[i]->mark = false; // Get world dimensions worldX0 = worldY0 = worldX1 = worldY1 = 0; for (i = 0; i < nmapBlocks; i++) findWorldSize(mapBlocks[i], &worldX0, &worldY0, &worldX1, &worldY1); if (initialMap) findWorldSize(initialMap, &worldX0, &worldY0, &worldX1, &worldY1); // Compute offsets, allocate world map // FIXME: check dimensions offsetX = -worldX0; offsetY = -worldY0; if ((worldMap = mapBlockAlloc(worldX1 - worldX0 + 1, worldY1 - worldY0 + 1)) == NULL) { THERR("Error allocating world map!\n"); res = -4; goto out; } // Place optional initial map if (initialMap) { if (mapBlockPut(&worldMap, initialMap, offsetX + initialMap->xc, offsetY + initialMap->yc) < 0) { THERR("Initial map mapBlockPut() failed!\n"); res = -9; goto out; } } else { i = 0; if (mapBlockPut(&worldMap, mapBlocks[i], offsetX + mapBlocks[i]->xc, offsetY + mapBlocks[i]->yc) < 0) { THERR("Initial map mapBlockPut() failed!\n"); res = -9; goto out; } mapBlocks[i]->mark = true; } THMSG(1, "Initialized world map of (%d x %d), offset (%d, %d)\n", worldMap->width, worldMap->height, offsetX, offsetY); // Start placing blocks currRounds = 0; isOK = false; while (currRounds++ < optRounds && !isOK) { int usedBlocks; // Get number of used blocks for (usedBlocks = i = 0; i < nmapBlocks; i++) if (mapBlocks[i]->mark) usedBlocks++; // Print out status information THPRINT(2, "#%d [%d/%d]: ", currRounds, usedBlocks, nmapBlocks); // Place and match blocks isOK = true; currBlocks = 0; for (i = 0; i < nmapBlocks && currBlocks < optReset; i++) { MapBlock *tmp = mapBlocks[i]; if (!tmp->mark) { int qx = offsetX + tmp->xc, qy = offsetY + tmp->yc; isOK = false; if (matchBlock(worldMap, tmp, qx, qy, optHardDrop) >= optMatch) { if (mapBlockPut(&worldMap, tmp, qx, qy) < 0) { THERR("mapBlockPut(%d, %d, %d) failed!\n", offsetX, offsetY, i); res = -9; goto out; } tmp->mark = true; currBlocks++; THPRINT(2, "X"); } else { THPRINT(2, "."); #if 0 // Debug unmatching blocks char mysti[512]; snprintf(mysti, sizeof(mysti), "[%d]: %d,%d (%d,%d)", i, qx, qy, tmp->x, tmp->y); fprintf(stderr, "\n--- %.30s ---\n", mysti); mapBlockPrint(stderr, tmp); fprintf(stderr, "---------\n"); mapBlockPrint(stderr, worldMap); #endif } } } THPRINT(2, "\n"); } // Output generated map if (worldMap) { int unusedBlocks; FILE *tmpFile; THMSG(1, "Outputting generated map of (%d x %d) ...\n", worldMap->width, worldMap->height); if (destFile == NULL) tmpFile = stdout; else if ((tmpFile = fopen(destFile, "wb")) == NULL) { THERR("Error opening output file '%s'!\n", destFile); res = -1; goto out; } mapBlockPrint(tmpFile, worldMap); fclose(tmpFile); // Compute number of unused blocks for (unusedBlocks = i = 0; i < nmapBlocks; i++) if (!mapBlocks[i]->mark) { if (optDumpRejected) { fprintf(stderr, "\n#%d: %d,%d (%d,%d)\n", i, mapBlocks[i]->xc, mapBlocks[i]->yc, mapBlocks[i]->xc + offsetX, mapBlocks[i]->yc + offsetY); mapBlockPrint(stderr, mapBlocks[i]); } unusedBlocks++; } THMSG(1, "%d mapblocks unused/discarded\n", unusedBlocks); } else { THERR("No map generated?\n"); res = -6; } out: mapBlockFree(worldMap); mapBlockFree(initialMap); if (mapBlocks != NULL) { for (i = 0; i < nmapBlocks; i++) mapBlockFree(mapBlocks[i]); th_free(mapBlocks); } return res; }