view mkspecial.c @ 1822:892d5277f1ff

Remove note about the search pattern parser being not very tolerant, it's somewhat better now.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 30 Oct 2017 12:55:21 +0200
parents 72adabd8e746
children 79dd960610cb
line wrap: on
line source

/*
 * mkspecial - Compute complete ASCII map by stitching pieces
 * together based on a matcher and coordinates
 *
 * Programmed by Matti 'ccr' Hämäläinen (Ggr Pupunen)
 * (C) Copyright 2006-2015 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);
}


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_verbosityLevel++;
        break;

    case 3:
        th_verbosityLevel = -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, int ox, int oy, 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, char centerCh)
{
    for (int y = 0; y < block->height; y++)
    for (int x = 0; x < block->width; x++)
    {
        int c = block->data[(y * block->scansize) + x];
        if (c == centerCh)
        {
            block->x -= x;
            block->y -= y;
            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, size_t bufSize)
{
    if (feof(inFile) || !fgets(buf, bufSize, inFile))
    {
        THERR("Unexpected end of file.\n");
        return FALSE;
    }
    else
        return TRUE;
}

int getLineWidth(char *buf, size_t bufSize)
{
    size_t width;

    for (width = 0; width < bufSize && buf[width] && buf[width] != '\r' && buf[width] != '\n'; width++);
    buf[width] = 0;

    return width;
}


MapBlock * parseBlock(FILE *inFile)
{
    MapBlock *tmp;
    int x, y, tmpW, tmpH;
    uint_t i;
    BOOL isFound;
    char s[4096], *p = NULL;

    isFound = FALSE;
    while (!feof(inFile) && !isFound && fgets(s, sizeof(s), inFile))
    {
        if ((p = strstr(s, ": !!PLR: ")) != NULL && (strstr(s, "party") || strstr(s, "report")))
            isFound = TRUE;
    }

    if (!isFound)
    {
        THERR("No block start marker found.\n");
        return NULL;
    }

    for (i = 0; i < sizeof(s) && s[i] && s[i] != '\r' && s[i] != '\n'; i++);
    s[i] = 0;

    if (sscanf(p, ": !!PLR: %d,%d", &x, &y) < 2)
    {
        THERR("Could not get location coordinates:\n'%s'\n", s);
        return NULL;
    }

    // Check for discardable lines
    if (!getLineData(inFile, s, sizeof(s)))
        return NULL;

    if (!th_strcasecmp(s, "You glance around."))
    {
        if (!getLineData(inFile, s, sizeof(s)))
            return NULL;
    }

    // New 'map' output in city maps has an empty line before the data, ignore it
    if ((tmpW = getLineWidth(s, sizeof(s))) <= 9)
    {
        if (!getLineData(inFile, s, sizeof(s)))
            return NULL;
        tmpW = getLineWidth(s, sizeof(s));
    }

    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, x, y);
            return NULL;
            break;
        }
    }
    else
        tmpH = tmpW - 8;

    if ((tmp = mapBlockAlloc(tmpW, tmpH)) == NULL)
    {
        THERR("Could not allocate mapblock (%d, %d)\n", tmpW, tmpH);
        return NULL;
    }

    tmp->x = x;
    tmp->y = y;

    y = 0;
    do
    {
        i = getLineWidth(s, tmpW);

        if (i != (uint_t) tmp->width)
        {
            THERR("Broken block, line width %d != %d!\n",
                i, tmp->width, s);
            mapBlockFree(tmp);
            return NULL;
        }

        for (x = 0; x < tmp->width; x++)
        {
            tmp->data[(y * tmp->scansize) + x] = s[x];
        }

        y++;
    } while (!feof(inFile) && (y < tmp->height) && fgets(s, sizeof(s), inFile));


    if (y < tmp->height)
    {
        THERR("Broken block, height %d < %d\n",
            y, tmp->height);
        mapBlockFree(tmp);
        return NULL;
    }

    return tmp;
}


/* Find the min/max of given coordinates.
 */
void findWorldSize(MapBlock *tmp,
    int *worldX0, int *worldY0,
    int *worldX1, int *worldY1)
{
    if (tmp->x < *worldX0) *worldX0 = tmp->x;
    if ((tmp->x + tmp->width) > *worldX1) *worldX1 = (tmp->x + tmp->width);

    if (tmp->y < *worldY0) *worldY0 = tmp->y;
    if ((tmp->y + tmp->height) > *worldY1) *worldY1 = (tmp->y + tmp->height);
}


int main(int argc, char *argv[])
{
    BOOL isOK;
    int i, currRounds, nmapBlocks, currBlocks,
        worldX0, worldY0, worldX1, worldY1,
        offsetX = 0, offsetY = 0;
    MapBlock *worldMap = NULL, *initialMap = NULL;
    MapBlock **mapBlocks = NULL;

    th_init("mkspecial", "Yet Another ASCII Map Auto-Stitcher", "0.5", NULL, NULL);
    th_verbosityLevel = 1;

    // Parse arguments
    if (!th_args_process(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, OPTH_BAILOUT))
        exit(1);

    if (nsrcFiles < 1)
    {
        THERR("Nothing to do. (try --help)\n");
        exit(0);
    }

    // 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, " ");
        }
        else
        {
            THERR("Initial map could not be loaded!\n");
            exit(1);
        }
    }

    /* Read in continuous mapdata and parse it into map blocks
     */
    for (nmapBlocks = i = 0; i < nsrcFiles; i++)
    {
        FILE *tmpFile;
        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]);
            exit(1);
        }

        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)
                    {
                        fclose(tmpFile);
                        THERR("Could not allocate/extend mapblock pointer structure (#%d)\n", nmapBlocks);
                        exit(3);
                    }

                    mapBlockClean(tmp, 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");
        exit(11);
    }


    /* 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->x = optInitialX;
        initialMap->y = 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");
        exit(4);
    }

    // Place optional initial map
    if (initialMap)
    {
        if (mapBlockPut(&worldMap, initialMap, offsetX + initialMap->x, offsetY + initialMap->y) < 0)
        {
            THERR("Initial map mapBlockPut() failed!\n");
            exit(9);
        }
    }
    else
    {
        i = 0;
        if (mapBlockPut(&worldMap, mapBlocks[i], offsetX + mapBlocks[i]->x, offsetY + mapBlocks[i]->y) < 0)
        {
            THERR("Initial map mapBlockPut() failed!\n");
            exit(9);
        }
        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->x,
                    qy = offsetY + tmp->y;

                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);
                        exit(9);
                    }
                    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);
            exit(1);
        }

        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]->x, mapBlocks[i]->y,
                    mapBlocks[i]->x + offsetX,
                    mapBlocks[i]->y + offsetY);

                mapBlockPrint(stderr, mapBlocks[i]);
            }
            unusedBlocks++;
        }

        THMSG(1, "%d mapblocks unused/discarded\n", unusedBlocks);
    }
    else
    {
        THERR("No map generated?\n");
    }

    return 0;
}