view mkloc.c @ 1771:72adabd8e746

More cleanups.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 27 Oct 2017 05:10:08 +0300
parents cc59f80b0e78
children db7fdbc8f81b
line wrap: on
line source

/*
 * mkloc - Manipulate and convert BatMUD location data files
 *
 * Programmed by Matti 'ccr' Hämäläinen (Ggr Pupunen)
 * (C) Copyright 2006-2015 Tecnic Software productions (TNSP)
 */
#include "libmaputils.h"
#include "liblocfile.h"
#include "th_args.h"
#include "th_string.h"
#include "th_datastruct.h"

enum
{
    OUTFMT_MAP = 0,
    OUTFMT_LOCFILE,
    OUTFMT_SCRIPT,
    OUTFMT_MAPLOC,
    OUTFMT_GMAPS
};

enum
{
    GMAPS_XML = 0,
    GMAPS_JSON,
    GMAPS_LAST
};

// These must be in lower case
const char *gmapsModes[GMAPS_LAST] =
{
    "xml",
    "json",
};

/* Variables
 */
char    *srcFile = NULL,
        *destFile = NULL;

int     nlocFiles = -1;
LocFileInfo locFiles[LOC_MAX_FILES];

char    *optLocMarkers = LOC_MARKERS;
BOOL    optGetUpdateLoc = FALSE,
        optOutput = OUTFMT_MAP,
        optNoLabels = FALSE,
        optNoAdjust = FALSE,
        optLabelType = FALSE;
int     optGMapsMode = -1;
float   optScale = -1,
        optUnitSize = 1.0f,
        optFontScale = 1.0f;


/* Arguments
 */
static const th_optarg optList[] =
{
    { 0, '?', "help",       "Show this help", OPT_NONE },
    { 2, 'v', "verbose",    "Be more verbose", OPT_NONE },
    { 3, 'q', "quiet",      "Be quiet", OPT_NONE },
    { 1, 'o', "output",     "Output file (default stdout)", OPT_ARGREQ },
    { 5, 'm', "map",        "Input map file", OPT_ARGREQ },
    { 6, 'l', "locinfo",    "Input location info file", OPT_ARGREQ },
    { 4, 'g', "getloc",     "Generate/update location info", OPT_NONE },
    { 7, 'x', "offset-x",   "Location X offset", OPT_ARGREQ },
    { 8, 'y', "offset-y",   "Location Y offset", OPT_ARGREQ },
    { 9, 's', "scale",      "Scale coordinates by", OPT_ARGREQ },
    { 12,'f', "font-scale", "(-S) Font scale factor", OPT_ARGREQ },
    { 13,'u', "unit-size",  "(-S) Unit size", OPT_ARGREQ },
    { 10,'S', "out-script", "Output script for ImageMagick", OPT_NONE },
    { 11,'L', "out-locinfo","Output location info file", OPT_NONE },
    { 17,'M', "out-maploc", "Output MapLoc HTML", OPT_NONE },
    { 20,'G', "out-gmaps",  "Output GMaps data (xml, overlay, labels)", OPT_ARGREQ },
    { 21,'c', "continent",  "Continent for GMaps XML output", OPT_ARGREQ },
    { 15,'n', "no-labels",  "No labels, only markers", OPT_NONE },
    { 16,'N', "no-adjust",  "No label adjustment", OPT_NONE },
    { 18,'t', "type-prefix","Prepend labels with type prefix", OPT_NONE },
    { 19,'X', "loc-markers","Location markers ('" LOC_MARKERS "')", OPT_ARGREQ },
};

static const int optListN = sizeof(optList) / sizeof(optList[0]);


void argShowHelp()
{
    th_print_banner(stdout, th_prog_name,
        "[options]");

    th_args_help(stdout, optList, optListN, 0);
}


BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    int i;
    LocFileInfo *f;

    switch (optN)
    {
    case 0:
        argShowHelp();
        exit(0);
        break;

    case 2:
        th_verbosityLevel++;
        break;

    case 3:
        th_verbosityLevel = -1;
        break;

    case 1:
        destFile = optArg;
        THMSG(2, "Output file '%s'\n", destFile);
        break;

    case 5:
        srcFile = optArg;
        THMSG(2, "Map file '%s'\n", srcFile);
        break;

    case 6:
        nlocFiles++;
        if (nlocFiles < LOC_MAX_FILES)
        {
            setLocFileInfo(&locFiles[nlocFiles], optArg, NULL);
            THMSG(2, "Added location data file #%d '%s'\n", nlocFiles, optArg);
        }
        else
        {
            THERR("Too many location data files specified, maximum is %d.\n", LOC_MAX_FILES);
            return FALSE;
        }
        break;

    case 4:
        THMSG(2, "Location updating/generation mode\n");
        optGetUpdateLoc = TRUE;
        break;

    case 7:
        f = &locFiles[nlocFiles];
        f->x = atoi(optArg);
        THMSG(2, "Location file #%d X offset = %d\n", nlocFiles, f->x);
        break;

    case 8:
        f = &locFiles[nlocFiles];
        f->y = atoi(optArg);
        THMSG(2, "Location file #%d Y offset = %d\n", nlocFiles, f->y);
        break;

    case 21:
        f = &locFiles[nlocFiles];
        th_free(f->continent);
        f->continent = th_strdup(optArg);
        THMSG(2, "Using continent name '%s' for location file #%d\n", optArg, nlocFiles);
        break;

    case 9:
        optScale = atof(optArg);
        THMSG(2, "Location scale factor = %1.3f\n", optScale);
        break;

    case 10:
        THMSG(2, "Script output mode\n");
        optOutput = OUTFMT_SCRIPT;
        break;

    case 11:
        THMSG(2, "Location file output mode\n");
        optOutput = OUTFMT_LOCFILE;
        break;

    case 17:
        THMSG(2, "MapLoc HTML output mode\n");
        optOutput = OUTFMT_MAPLOC;
        break;

    case 12:
        optFontScale = atof(optArg);
        THMSG(2, "Font scale factor = %1.3f\n", optFontScale);
        break;

    case 13:
        optUnitSize = atof(optArg);
        THMSG(2, "Unit size = %1.3f\n", optUnitSize);
        break;

    case 15:
        THMSG(2, "Not adding labels to locations\n");
        optNoLabels = TRUE;
        break;

    case 16:
        THMSG(2, "Not adjusting data under labels\n");
        optNoAdjust = TRUE;
        break;

    case 18:
        THMSG(2, "Adding label type prefixes\n");
        optLabelType = TRUE;
        break;

    case 19:
        optLocMarkers = optArg;
        THMSG(2, "Using location markers = '%s'\n", optLocMarkers);
        break;

    case 20:
        optOutput = OUTFMT_GMAPS;
        optGMapsMode = -1;
        for (i = 0; i < GMAPS_LAST; i++)
        if (tolower(optArg[0]) == gmapsModes[i][0])
        {
            optGMapsMode = i;
            break;
        }

        if (optGMapsMode < 0)
        {
            THERR("Invalid GMaps mode '%s'.\n", optArg);
            return FALSE;
        }
        THMSG(2, "GMaps output mode '%s' selected\n", gmapsModes[optGMapsMode]);
        break;


    default:
        THERR("Unknown option '%s'.\n", currArg);
        return FALSE;
    }

    return TRUE;
}


int locPrintType(FILE *outFile, LocMarker *loc, BOOL adjust, int (*func)(const char *, FILE *), BOOL label)
{
    const char *type = locGetType(loc->flags);
    int len = 0;

    if (type != NULL && label)
    {
        len += strlen(type) + 1;
        if (outFile != NULL && func != NULL)
            fprintf(outFile, "%s ", type);
    }

    if (func != NULL)
    {
        char *name = (loc->names[0].name != NULL) ? loc->names[0].name : "UNKNOWN";
        if (outFile != NULL)
            func(name, outFile);
        len += strlen(name);

        if (label && (loc->flags & LOCF_CLOSED))
        {
            static const char *strClosed = " (CLOSED)";
            if (outFile != NULL)
                func(strClosed, outFile);
            len += strlen(strClosed);
        }
    }

    return adjust ? len : 0;
}


/* Adjust location label coordinates for the ASCII-CTRL map
 */
void adjustLocInfoCoords(MapBlock *map, MapLocations *l)
{
    int i;

    for (i = 0; i < l->n; i++)
    {
        int y, x0, x1, len;
        LocMarker *tmp = l->locations[i];

        len = strlen(tmp->names[0].name);
        if (optLabelType)
            len += locPrintType(NULL, tmp, TRUE, NULL, FALSE);

        // Compute text location
        switch (tmp->align)
        {
        case LOCD_LEFTDOWN:
            y = tmp->y + 1;
            x0 = tmp->x - len;
            break;

        case LOCD_LEFT:
            y = tmp->y - 1;
            x0 = tmp->x - len;
            break;

        case LOCD_DOWN:
            y = tmp->y + 1;
            x0 = tmp->x + 1;
            break;

        case LOCD_NONE:
        default:
            y = tmp->y - 1;
            x0 = tmp->x + 1;
            break;
        }

        x1 = x0 + len + 1;

        if (x1 > map->width)
            x0 -= (x1 - map->width - 1);
        if (x0 < 0) x0 = 0;

        if (y < 0) y += 2;
        if (y > map->height) y -= (y - map->height - 1);

        // Update location info
        tmp->x = x0;
        tmp->y = y;
    }
}


/* Check for adjacent markers
 */
int checkForAdjacent(MapLocations *world, int cx, int cy, int mask)
{
    int x, y, n;

    for (y = -1; y <= 1; y++)
    for (x = -1; x <= 1; x++)
    {
        if (!(y == 0 && x == 0) &&
            (n = locFindByCoords(world, cx + x, cy + y, TRUE)) >= 0)
        {
            LocMarker *loc = world->locations[n];
            if ((loc->flags & mask) == mask)
                return n;
        }
    }

    return -1;
}


/* Scan given map and update location list with new locations,
 * if any are found.
 */
void updateLocations(MapBlock *worldMap, MapLocations *worldLoc)
{
    int x, y, n, numNewLoc = 0, numInvLoc = 0, numNoMarker = 0;
    unsigned char *dp;
    LocMarker *tmpl;

    THMSG(2, "Updating location information ..\n");

    // Find new, unknown locations
    for (y = 0; y < worldMap->height; y++)
    {
        dp = worldMap->data + (worldMap->scansize * y);

        for (x = 0; x < worldMap->width; x++)
        {
            if (strchr(optLocMarkers, dp[x]))
            {
                LocName tmpNames[LOC_MAX_NAMES];
                char tmpDesc[512];
                int tmpFlags;

                // Check for new locations
                if (locFindByCoords(worldLoc, x, y, TRUE) >= 0)
                    continue;


                if (dp[x] == 'C')
                {
                    n = checkForAdjacent(worldLoc, x, y, LOCF_M_PCITY);
                    if (n >= 0)
                    {
                        tmpl = worldLoc->locations[n];
                        tmpFlags = LOCF_M_PCITY | LOCF_INVIS;
                        snprintf(tmpDesc, sizeof(tmpDesc),
                        "%s", tmpl->names[0].name);

                    }
                    else
                    {
                        numNewLoc++;
                        tmpFlags = LOCF_M_PCITY;
                        snprintf(tmpDesc, sizeof(tmpDesc),
                        "%.3s-PCITY #%d",
                        srcFile, numNewLoc);
                    }
                }
                else
                {
                    numNewLoc++;
                    tmpFlags = LOCF_NONE;
                    snprintf(tmpDesc, sizeof(tmpDesc),
                        "%.3s-UNK #%d",
                        srcFile, numNewLoc);
                }
                memset(tmpNames, 0, sizeof(tmpNames));
                tmpNames[0].name = tmpDesc;
                locAddNew(worldLoc, x, y, LOCD_NONE, tmpFlags,
                    tmpNames, NULL, NULL, FALSE, NULL, NULL, NULL);

            }
            else
            {
                // Check for misplaced locations
                if ((n = locFindByCoords(worldLoc, x, y, TRUE)) >= 0)
                {
                    tmpl = worldLoc->locations[n];

                    if (tmpl->flags == LOCF_NONE)
                    {
                        // Mark as possibly invalid
                        tmpl->flags |= LOCF_INVALID;
                        numInvLoc++;
                    }

                    if ((tmpl->flags & LOCF_M_MASK) == 0)
                    {
                        // No apparent marker
                        tmpl->flags |= LOCF_NOMARKER;
                        numNoMarker++;
                    }
                }
            }
        }
    }

    THMSG(1, "%d new locations, %d invalid, %d missing marker.\n",
        numNewLoc, numInvLoc, numNoMarker);
}


/* Quicksort comparision function for location names
 */
int maplocCompare(const void *p1, const void *p2)
{
    LocMarker *vp1 = *(LocMarker **) p1, *vp2 = *(LocMarker **) p2;

    if (vp1->val == vp2->val)
        return strcmp(vp1->names[0].name, vp2->names[0].name);
    else
        return vp1->val - vp2->val;
}


/* Sort locations by name
 */
void maplocSort(MapLocations *l)
{
    int i;

    for (i = 0; i < l->n; i++)
    {
        LocMarker *loc = l->locations[i];
        loc->val = 2;

        switch (loc->flags & LOCF_M_MASK)
        {
        case LOCF_M_CITY:   loc->val = 1; break;
        case LOCF_M_PCITY:  loc->val = 10; break;
        default:
            switch (loc->flags & LOCF_T_MASK)
            {
            case LOCF_T_GUILD:   loc->val = 3; break;
            case LOCF_T_TRAINER: loc->val = 4; break;
            case LOCF_T_SHRINE:  loc->val = 5; break;
            case LOCF_T_SS:      loc->val = 6; break;
            case LOCF_T_MONSTER: loc->val = 7; break;
            case LOCF_T_FORT:    loc->val = 100; break;
            }
        }
    }

    qsort(l->locations, l->n, sizeof(LocMarker *), maplocCompare);
}


/* Output the map with labels and location markers, etc. in
 * special ASCII-CTRL format understood by colormap utility.
 */
void outputMapCTRL(FILE *outFile, MapBlock *map, MapLocations *l)
{
    unsigned char *dp;
    int x, y, n;

    for (y = 0; y < map->height; y++)
    {
        dp = map->data + (map->scansize * y);

        for (x = 0; x < map->width; x++)
        {
            if ((n = locFindByCoords(l, x, y, TRUE)) >= 0)
            {
                LocMarker *tmp = l->locations[n];
                char chm = dp[x];

                switch (tmp->flags & LOCF_M_MASK)
                {
                case LOCF_M_SCENIC1: chm = '?'; break;
                case LOCF_M_SCENIC2: chm = '%'; break;
                case LOCF_M_PCITY: chm = 'C'; break;
                case LOCF_M_CITY: chm = 'c'; break;

                default:
                    if (tmp->flags & LOCF_INVALID)
                        chm = '$';
                    break;
                }

                fputc(0xfb, outFile);
                fprintf(outFile, "mloc%d_%d", tmp->ox + 1, tmp->oy + 1);
                fputc(0xfc, outFile);
                fputc(chm, outFile);
                fputc(0xfe, outFile);
            }
            else
            if (!optNoLabels && (n = locFindByCoords(l, x, y, FALSE)) >= 0)
            {
                LocMarker *tmp = l->locations[n];

                if ((tmp->flags & LOCF_INVIS) == 0)
                {
                    int col = col_light_white;

                    fputc(0xff, outFile);
                    fprintf(outFile, "loc%d_%d", tmp->ox + 1, tmp->oy + 1);
                    fputc(0xfc, outFile);

                    switch (tmp->flags & LOCF_M_MASK)
                    {
                    case LOCF_M_PCITY: col = col_light_green; break;
                    case LOCF_M_CITY: col = col_light_red; break;

                    default:
                        switch (tmp->flags & LOCF_T_MASK)
                        {
                        case LOCF_T_SHRINE: col = col_light_yellow; break;
                        case LOCF_T_GUILD: col = col_light_magenta; break;
                        case LOCF_T_MONSTER: col = col_light_cyan; break;
                        case LOCF_T_TRAINER: col = col_light_magenta; break;
                        case LOCF_T_FORT: col = col_light_cyan; break;
                        default: col = col_light_white; break;
                        }
                        break;
                    }

                    if (tmp->flags & LOCF_CLOSED)
                        col = col_light_red;

                    fputc(col, outFile);

                    if (optLabelType)
                    {
                        x += locPrintType(outFile, tmp, !optNoAdjust, NULL, FALSE);
                    }

                    fputs(tmp->names[0].name, outFile);
                    fputc(0xfe, outFile);

                    if (!optNoAdjust)
                        x += strlen(tmp->names[0].name) - 1;
                    else
                        fputc(dp[x], outFile);
                }
                else
                    fputc(dp[x], outFile);
            } else
                fputc(dp[x], outFile);
        }

        fprintf(outFile, "\n");
    }
}


/* Print out location list as HTML option list.
 */
void outputMapLocHTML(FILE *outFile, MapLocations *l)
{
    int i;
    assert(l != NULL);

    fprintf(outFile, "<option value=\"\">-</option>\n");

    for (i = 0; i < l->n; i++)
    {
        LocMarker *tmp = l->locations[i];

        if (tmp->flags & LOCF_INVIS)
            continue;

        fprintf(outFile, "<option value=\"loc%d_%d\">", tmp->ox + 1, tmp->oy + 1);
        locPrintType(outFile, tmp, FALSE, fputse, TRUE);
        fprintf(outFile, "</option>\n");
    }
}


/* Output generated locations into given file stream
 */
void printLocNameEsc(FILE *outFile, LocName *name)
{
    char *prefix = "";
    switch (name->flags)
    {
        case NAME_ORIG:       prefix = "@"; break;
        case NAME_RECODER:    prefix = "!"; break;
        case NAME_MAINTAINER: prefix = "%"; break;
        case NAME_EXPANDER:   prefix = "&"; break;
    }
    fputs(prefix, outFile);
    fputsesc2(name->name, outFile);
}


void outputLocationFile(FILE *outFile, MapLocations *l)
{
    int i, n;

    // Output header
    fprintf(outFile,
    "# " LOC_MAGIC " (version %d.%d)\n",
    LOC_VERSION_MAJOR, LOC_VERSION_MINOR
    );

    fprintf(outFile,
    "# Refer to README.loc for more information.\n"
    "#\n"
    );

    // Output each location entry
    for (i = 0; i < l->n; i++)
    {
        LocMarker *tmp = l->locations[i];

        // Add comment in few cases
        if (tmp->flags & LOCF_Q_MASK)
        {
            char *s = NULL;
            if (tmp->flags & LOCF_NOMARKER)
                s = "Location missing marker";
            else
            if (tmp->flags & LOCF_INVALID)
                s = "Possibly invalid location";

            if (s)
            {
                fprintf(outFile, "\n# %s #%d: %s\n",
                    s, i, tmp->names[0].name);
            }
        }

        fprintf(outFile, "%d\t; %d\t; %d",
            tmp->ox + 1, tmp->oy + 1, tmp->align);

        switch (tmp->flags & LOCF_M_MASK)
        {
        case LOCF_M_SCENIC1:    fputc('?', outFile); break;
        case LOCF_M_SCENIC2:    fputc('%', outFile); break;
        case LOCF_M_PCITY:      fputc('C', outFile); break;
        case LOCF_M_CITY:       fputc('c', outFile); break;
        }

        switch (tmp->flags & LOCF_T_MASK)
        {
        case LOCF_T_SHRINE:     fputc('S', outFile); break;
        case LOCF_T_GUILD:      fputc('G', outFile); break;
        case LOCF_T_SS:         fputc('P', outFile); break;
        case LOCF_T_MONSTER:    fputc('M', outFile); break;
        case LOCF_T_TRAINER:    fputc('T', outFile); break;
        case LOCF_T_FORT:       fputc('F', outFile); break;
        }

        if (tmp->flags & LOCF_CLOSED)
            fputc('!', outFile);

        if (tmp->flags & LOCF_INSTANCED)
            fputc('I', outFile);

        if (tmp->flags & LOCF_INVIS)
            fputc('-', outFile);

        fprintf(outFile, "\t;");
        printLocNameEsc(outFile, &tmp->names[0]);
        for (n = 1; n < tmp->nnames; n++)
        {
            fprintf(outFile, "|");
            printLocNameEsc(outFile, &tmp->names[n]);
        }
        fprintf(outFile, ";");

        if (tmp->ncoders > 0)
        {
            printLocNameEsc(outFile, &tmp->coders[0]);
            for (n = 1; n < tmp->ncoders; n++)
            {
                fprintf(outFile, ",");
                printLocNameEsc(outFile, &tmp->coders[n]);
            }
        }

        fprintf(outFile, ";");

        if (tmp->valid)
        {
            fprintf(outFile, LOC_TIMEFMT,
            tmp->added.day, tmp->added.month, tmp->added.year);
        }

        fprintf(outFile, ";");

        if (tmp->uri)
            fputsesc2(tmp->uri, outFile);

        fprintf(outFile, ";");

        if (tmp->freeform)
            fputsesc2(tmp->freeform, outFile);

        fprintf(outFile, "\n");
    }
}


/* Generate a shell-script for running ImageMagick to add
 * location and label information to an map image.
 */
void outputMagickScript(FILE *outFile, MapLocations *l)
{
    int i, numCity, numLoc, numTotal;

    // Output script start
    fprintf(outFile,
        "#!/bin/sh\n"
        "convert \"$1\" @OPTS_START@ \\\n");

    numCity = numLoc = numTotal = 0;

    // Output instructions for each visible location
    for (i = 0; i < l->n; i++)
    {
        LocMarker *tmp = l->locations[i];
        int x, y, leftMove;
        char *cs;

        // Is location visible?
        if (tmp->flags & LOCF_INVIS)
            continue;

        leftMove = ((float) strlen(tmp->names[0].name)) * optFontScale;

        switch (tmp->align)
        {
        case LOCD_LEFTDOWN:
            y = tmp->y + optUnitSize*3.0f;
            x = tmp->x - leftMove;
            break;

        case LOCD_LEFT:
            y = tmp->y - optUnitSize;
            x = tmp->x - leftMove;
            break;

        case LOCD_DOWN:
            y = tmp->y + optUnitSize*3.0f;
            x = tmp->x + optUnitSize;
            break;

        case LOCD_NONE:
        default:
            y = tmp->y - optUnitSize;
            x = tmp->x + optUnitSize;
            break;
        }

        // Determine colour
        switch (tmp->flags & LOCF_M_MASK)
        {
        case LOCF_M_CITY:
            cs = "'#880000'";
            numLoc++;
            break;
        case LOCF_M_PCITY:
            cs = "'#00ff00'";
            numCity++;
            break;
        default:
            switch (tmp->flags & LOCF_T_MASK)
            {
                case LOCF_T_SHRINE: cs = "yellow"; break;
                case LOCF_T_GUILD: cs = "magenta"; break;
                case LOCF_T_MONSTER: cs = "cyan"; break;
                case LOCF_T_TRAINER: cs = "magenta"; break;
                case LOCF_T_FORT: cs = "'#00ffff'"; break;
                default: cs = "white"; break;
            }
            numLoc++;
            break;
        }

        if (tmp->flags & LOCF_CLOSED)
            cs = "'#ff0000'";

        numTotal++;

        // Location marker
        fprintf(outFile,
            "\t-fill black -draw \"circle %d,%d %d,%d\" ",
            tmp->x, tmp->y,
            (int) (tmp->x + optUnitSize), (int) (tmp->y + optUnitSize));

        fprintf(outFile,
            "-fill %s -draw \"circle %d,%d %d,%d\" ",
            cs, tmp->x, tmp->y,
            (int) (tmp->x + optUnitSize * 0.90f),
            (int) (tmp->y + optUnitSize * 0.90f));


        // Location description text
        if (!optNoLabels)
        {
            fprintf(outFile,
                "-fill %s -box '#00000080' -draw \"text %d,%d '",
                cs, x, y);

            fputsesc3(tmp->names[0].name, outFile);
            fprintf(outFile, "'\" ");
        }

        fprintf(outFile, " \\\n");
    }

    fprintf(outFile,
        "@OPTS_END@"
        "'\" \\\n");

    fprintf(outFile, "\t\"$2\"\n");
}


/* Output locations in GMaps XML format. Character set conversion
 * is not performed, caller must take care of it via iconv or similar.
 */
static char *getQuestLink(const char *name, const char *desc)
{
    char *str, *tmp = th_strdup(name);
    size_t i;
    for (i = 0; i < strlen(tmp); i++)
        tmp[i] = th_isspace(tmp[i]) ? '+' : th_tolower(tmp[i]);

    str = th_strdup_printf("<a target=\"_blank\" href=\"http://www.bat.org/help/quests?str=%s\">%s</a>", tmp, desc);
    th_free(tmp);
    return str;
}


static char *addQuestLink(char **buf, size_t *bufSize, size_t *bufLen, char *ptr, char *start, char *end)
{
    if (start != NULL && end != NULL)
    {
        char *name = th_strndup(start + 1, end - start - 1);
        char *desc = th_strndup(ptr, end - ptr + 1);
        char *tmp = getQuestLink(name, desc);
        th_strbuf_puts(buf, bufSize, bufLen, tmp);
        th_free(name);
        th_free(desc);
        th_free(tmp);
        return end + 1;
    }
    else
        return ptr;
}

void outputGMapsHTML(FILE *outFile, LocMarker *tmp,
    int (*fpr)(FILE *, const char *fmt, ...), int (*fps)(const char *, FILE *))
{
    int n;

    if (tmp->uri != NULL)
    {
        fpr(outFile, "<b><a target=\"_blank\" href=\"%s\">", tmp->uri);
        locPrintType(outFile, tmp, FALSE, fps, TRUE);
        fpr(outFile, "</a></b><br>");
    }
    else
    {
        fpr(outFile, "<b>");
        locPrintType(outFile, tmp, FALSE, fps, TRUE);
        fpr(outFile, "</b><br>");
    }

    // Alternative names, if any
    if (tmp->nnames > 1)
    {
        fpr(outFile, "Also known as <i>");
        for (n = 1; n < tmp->nnames; n++)
        {
            fps(tmp->names[n].name, outFile);
            if (tmp->names[n].flags & NAME_ORIG)
                fprintf(outFile, " (*)");
            if (n < tmp->nnames - 1)
                fprintf(outFile, " ; ");
        }
        fpr(outFile, "</i>.<br>");
    }

    // Added to game timestamp
    if (tmp->valid)
    {
        fpr(outFile, "Added " LOC_TIMEFMT ".<br>",
            tmp->added.day, tmp->added.month, tmp->added.year);
    }

    // Coder names or societies
    if (tmp->ncoders > 0)
    {
        if (tmp->flags & LOCF_M_PCITY)
        {
            fprintf(outFile, "Societies: ");
            for (n = 0; n < tmp->ncoders; n++)
            {
                fps(tmp->coders[n].name, outFile);
                if (n < tmp->ncoders - 1)
                    fprintf(outFile, ", ");
            }
        }
        else
        {
            fprintf(outFile, "Coders: ");
            for (n = 0; n < tmp->ncoders; n++)
            {
                char *info = "", *sinfo = "";
                switch (tmp->coders[n].flags)
                {
                    case NAME_ORIG: info = " (O)"; sinfo = "Original coder"; break;
                    case NAME_RECODER: info = " (R)"; sinfo = "Re-coder"; break;
                    case NAME_MAINTAINER: info = " (M)"; sinfo = "Maintainer"; break;
                    case NAME_EXPANDER: info = " (E)"; sinfo = "Expander"; break;
                }
                //fpr(outFile, "<a target=\"_blank\" href=\"http://www.bat.org/char/%s\">%s%s</a>",
                fpr(outFile, "<a target=\"_blank\" href=\"https://tnsp.org/maps/loc.php?a=%s\" title=\"%s\">%s%s</a>",
                    tmp->coders[n].name, sinfo,
                    tmp->coders[n].name, info);

                if (n < tmp->ncoders - 1)
                    fprintf(outFile, ", ");
            }
        }
        fps(".<br>", outFile);
    }

    // Print out freeform information field
    if (tmp->freeform)
    {
        char *ptr = tmp->freeform;
        char *buf = NULL;
        size_t bufLen = 0, bufSize = 0;

        while (*ptr != 0)
        {
            if (ptr[0] == 'A' && ptr[1] == 'Q' && th_isspace(ptr[2]))
            {
                char *start = strchr(ptr + 3, '"');
                ptr = addQuestLink(&buf, &bufSize, &bufLen, ptr, start, strchr(start + 1, '"'));
            }
            else
            if (ptr[0] == 'L' && ptr[1] == 'Q' && th_isdigit(ptr[2]))
            {
                char *start = strchr(ptr + 3, '"');
                ptr = addQuestLink(&buf, &bufSize, &bufLen, ptr, start, start ? strchr(start + 1, '"') : NULL);
            }

            if (*ptr != 0)
                th_strbuf_putch(&buf, &bufSize, &bufLen, *ptr++);
        }

        th_strbuf_putch(&buf, &bufSize, &bufLen, 0);

        fpr(outFile, "<br>%s<br>", buf);
        th_free(buf);
    }
}


void outputGMapsXML(FILE *outFile, MapLocations *l)
{
    int i;

    fprintf(outFile,
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    "<markers>\n");

    // Output each location entry
    for (i = 0; i < l->n; i++)
    {
        LocMarker *tmp = l->locations[i];

        // Skip disabled / invisible locations
        if (tmp->flags & (LOCF_INVIS | LOCF_INVALID)) continue;

        // Print out coordinates etc.
        fprintf(outFile, "<marker x=\"%d\" y=\"%d\" labeldir=\"%d\" name=\"",
            tmp->ox, tmp->oy, tmp->align);

        // Location name
        locPrintType(outFile, tmp, FALSE, fputse, FALSE);

        fprintf(outFile, "\" html=\"");

        outputGMapsHTML(outFile, tmp, fprintfe, fputse);

        // Flags
        fprintf(outFile, "\" flags=\"%d\"",
            tmp->flags);

        // Continent name
        if (tmp->file != NULL && tmp->file->continent != NULL)
        {
            fprintf(outFile, " continent=\"");
            fputse(tmp->file->continent, outFile);
            fprintf(outFile, "\"");
        }

        // Type of the marker
        fprintf(outFile, " type=\"%s\"/>\n", locGetLocationType(tmp->flags));
    }

    fprintf(outFile, "</markers>\n");
}


void outputGMapsJSON(FILE *outFile, MapLocations *l)
{
    int i;

    fprintf(outFile, "[\n");

    // Output each location entry
    for (i = 0; i < l->n; i++)
    {
        LocMarker *tmp = l->locations[i];

        // Skip disabled / invisible locations
        if (tmp->flags & (LOCF_INVIS | LOCF_INVALID)) continue;

        // Print out coordinates etc.
        fprintf(outFile, "{\"x\":%d,\"y\":%d,\"labeldir\":%d,\"name\":\"",
            tmp->ox, tmp->oy, tmp->align);

        // Location name
        locPrintType(outFile, tmp, FALSE, fputsesc1, FALSE);
        fprintf(outFile, "\",\"html\":\"");

        outputGMapsHTML(outFile, tmp, fprintfesc1, fputsesc1);

        // Flags
        fprintf(outFile, "\",\"flags\":%d",
            tmp->flags);

        // Continent name
        if (tmp->file != NULL && tmp->file->continent != NULL)
        {
            fprintf(outFile, ",\"continent\":\"");
            fputsesc1(tmp->file->continent, outFile);
            fprintf(outFile, "\"");
        }

        // Type of the marker
        fprintf(outFile, "}");

        if (i < l->n - 1)
            fprintf(outFile, ",");

        fprintf(outFile, "\n");
    }

    fprintf(outFile, "]\n");
}


int main(int argc, char *argv[])
{
    FILE *outFile = NULL;
    MapBlock *worldMap = NULL;
    MapLocations worldLoc;

    memset(&worldLoc, 0, sizeof(worldLoc));

    // Initialize
    th_init("mkloc", "Manipulate and convert location files and ASCII map data", "1.6", NULL, NULL);
    th_verbosityLevel = 0;

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

    // Check the mode and arguments
    if (srcFile == NULL && optGetUpdateLoc && optOutput == OUTFMT_LOCFILE)
    {
        THERR("Map file required for location update mode!\n");
        goto exit;
    }

    if (optOutput == OUTFMT_LOCFILE && nlocFiles < 0 && !optGetUpdateLoc)
    {
        THERR("Location file or location update mode required for location file output!\n");
        goto exit;
    }

    if ((optOutput == OUTFMT_SCRIPT || optOutput == OUTFMT_MAPLOC) && nlocFiles < 0)
    {
        THERR("Location file required for script or MapLoc HTML output!\n");
        goto exit;
    }

    if (srcFile == NULL && optOutput == OUTFMT_MAP)
    {
        THERR("Mapfile required for map generation.\n");
        goto exit;
    }

    // Read initial map
    if (srcFile)
    {
        THMSG(2, "Reading map '%s'\n", srcFile);
        worldMap = mapBlockParseFile(srcFile, FALSE);
        if (worldMap == NULL)
        {
            THERR("World map could not be loaded!\n");
            goto exit;
        }

        THMSG(2, "Initial dimensions (%d x %d)\n", worldMap->width, worldMap->height);
    }

    // Read location info
    for (int i = 0; i <= nlocFiles; i++)
    {
        LocFileInfo *f = &locFiles[i];
        FILE *inFile;

        if (optOutput == OUTFMT_GMAPS && f->continent == NULL)
        {
            THERR("Required continent name not set for #%d '%s'.\n",
                i, f->filename);
            goto exit;
        }

        THMSG(2, "Reading location info '%s'\n", f->filename);
        if ((inFile = fopen(f->filename, "rb")) == NULL)
        {
            THERR("Could not open location file '%s' for reading.\n",
                f->filename);
            goto exit;
        }

        if (!locParseLocStream(inFile, f, &worldLoc, f->x, f->y))
            goto exit;

        fclose(inFile);
    }

    // Update locations
    if (optGetUpdateLoc)
        updateLocations(worldMap, &worldLoc);

    // Scale locations
    if (optScale > 0)
    {
        THMSG(1, "Scaling locations ..\n");

        for (int i = 0; i < worldLoc.n; i++)
        {
            LocMarker *tmp = worldLoc.locations[i];

            tmp->x = ((float) tmp->x) * optScale;
            tmp->y = ((float) tmp->y) * optScale;
        }
    }

    // Open output file
    if (destFile == NULL)
        outFile = stdout;
    else
    if ((outFile = fopen(destFile, "wb")) == NULL)
    {
        THERR("Error opening output file '%s'!\n",
            destFile);
        goto exit;
    }

    // Output results
    switch (optOutput)
    {
        case OUTFMT_SCRIPT:
            THMSG(1, "Generating ImageMagick script ...\n");
            outputMagickScript(outFile, &worldLoc);
            break;

        case OUTFMT_LOCFILE:
            THMSG(1, "Outputting generated location list ...\n");
            outputLocationFile(outFile, &worldLoc);
            THMSG(2, "%d locations\n", worldLoc.n);
            break;

        case OUTFMT_MAPLOC:
            maplocSort(&worldLoc);
            THMSG(1, "Outputting MapLoc HTML ...\n");
            outputMapLocHTML(outFile, &worldLoc);
            THMSG(2, "%d locations\n", worldLoc.n);
            break;

        case OUTFMT_GMAPS:
            maplocSort(&worldLoc);
            THMSG(1, "Outputting GMaps data (%s) ...\n", gmapsModes[optGMapsMode]);
            switch (optGMapsMode)
            {
                case GMAPS_XML: outputGMapsXML(outFile, &worldLoc); break;
                case GMAPS_JSON: outputGMapsJSON(outFile, &worldLoc); break;
            }
            THMSG(2, "%d locations\n", worldLoc.n);
            break;

        case OUTFMT_MAP:
            THMSG(1, "Outputting generated map of (%d x %d) ...\n",
                worldMap->width, worldMap->height);

            THMSG(2, "Adjusting location labels ..\n");
            adjustLocInfoCoords(worldMap, &worldLoc);

            outputMapCTRL(outFile, worldMap, &worldLoc);
            break;
    }

exit:
    if (outFile != NULL)
        fclose(outFile);

    mapBlockFree(worldMap);
    locFreeMapLocations(&worldLoc);

    return 0;
}