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;
}