view src/mkloc.c @ 2712:ac63db65b917

Implement -Wextra for some extra warnings.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 04 Mar 2024 11:20:27 +0200
parents 99c6d0cea264
children
line wrap: on
line source

/*
 * Manipulate and convert BatMUD location data files
 * Programmed by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
 * (C) Copyright 2006-2024 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
};

enum
{
    WARN_NONE                = 0x0000,
    WARN_MARKER_INVALID      = 0x0001,
    WARN_MARKER_MISSING      = 0x0002,
    WARN_AUTHORS_MISSING     = 0x0004,
    WARN_TIMESTAMPS_MISSING  = 0x0008,

    WARN_EXTRA               = 0x1000,
    WARN_ALL                 = 0x0fff
};


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

/* Variables
 */
char    *optInFilename = NULL,
        *optOutFilename = NULL;

int     noptLocFiles = -1;
LocFileInfo optLocFiles[LOC_MAX_FILES];

char    *optLocMarkers = LOC_MARKERS;
bool    optGetUpdateLoc = false,
        optNoLabels = false,
        optNoAdjust = false,
        optLabelType = false,
        optWarningsToLoc = false;
int     optWarnings = WARN_NONE;
int     optOutput = OUTFMT_MAP,
        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 },
    { 21,'c', "continent",   "Location continent", 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 location data in Batclient XML or GMaps JSON format", 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', "markers",     "Location markers ('" LOC_MARKERS "')", OPT_ARGREQ },
    { 22,'W', "warnings",    "Output warnings about: "
                             "none, all, timestamps, authors, extra\n"
                             "(NOTE: 'all' does NOT include 'extra')", OPT_ARGREQ },
    { 23,  0, "warn-loc",    "Output warnings to loc file instead of stderr", OPT_NONE },
};

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, 80 - 2);
}


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

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

    case 2:
        th_verbosity++;
        break;

    case 3:
        th_verbosity = -1;
        break;

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

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

    case 6:
        noptLocFiles++;
        if (noptLocFiles < LOC_MAX_FILES)
        {
            locSetFileInfo(&optLocFiles[noptLocFiles], optArg, NULL);
            THMSG(2, "Added location data file #%d '%s'\n", noptLocFiles, 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:
    case 8:
    case 21:
        if (noptLocFiles >= 0)
        {
            f = &optLocFiles[noptLocFiles];
            switch (optN)
            {
                case 7:
                    f->xoffs = atoi(optArg);
                    THMSG(2, "Location file #%d X offset = %d\n", noptLocFiles, f->xoffs);
                    break;

                case 8:
                    f->yoffs = atoi(optArg);
                    THMSG(2, "Location file #%d Y offset = %d\n", noptLocFiles, f->yoffs);
                    break;

                case 21:
                    th_free(f->continent);
                    f->continent = th_strdup(optArg);
                    THMSG(2, "Using continent name '%s' for location file #%d\n", optArg, noptLocFiles);
                    break;
            }
        }
        else
        {
            THERR("No location files specified, but location option '-%s %s' given.\n", currArg, optArg);
            return false;
        }
        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 (int 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;

    case 22:
        if (th_strcasecmp(optArg, "none") == 0)
            optWarnings = WARN_NONE;
        else
        if (th_strcasecmp(optArg, "all") == 0)
            optWarnings = WARN_ALL;
        else
        switch (tolower(optArg[0]))
        {
            case 'a':
                optWarnings |= WARN_AUTHORS_MISSING;
                break;
            case 't':
                optWarnings |= WARN_TIMESTAMPS_MISSING;
                break;
            case 'e':
                optWarnings |= WARN_EXTRA;
                break;
            default:
                THERR("Invalid argument to -W: '%s'\n", optArg);
                return false;
        }
        break;

    case 23:
        THMSG(2, "Outputting warnings to loc file.\n");
        optWarningsToLoc = true;
        break;

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

    return true;
}


int locPrintType(FILE *outFile, const LocMarker *loc,
    const bool adjust, int (*func)(const char *, FILE *),
    const bool label)
{
    const char *type = locGetTypePrefix(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)
    {
        const 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 *lp)
{
    for (int i = 0; i < lp->nlocations; i++)
    {
        int yc, x0, x1, len;
        LocMarker *loc = lp->locations[i];

        len = strlen(loc->names[0].name);
        if (optLabelType)
            len += locPrintType(NULL, loc, true, NULL, false);

        // Compute text location
        switch (loc->align)
        {
        case LOCD_LEFTDOWN:
            yc = loc->yc + 1;
            x0 = loc->xc - len;
            break;

        case LOCD_LEFT:
            yc = loc->yc - 1;
            x0 = loc->xc - len;
            break;

        case LOCD_DOWN:
            yc = loc->yc + 1;
            x0 = loc->xc + 1;
            break;

        case LOCD_NONE:
        default:
            yc = loc->yc - 1;
            x0 = loc->xc + 1;
            break;
        }

        x1 = x0 + len + 1;

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

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

        // Update location info
        loc->xc = x0;
        loc->yc = yc;
    }
}


/* Check for adjacent markers
 */
int checkForAdjacent(const MapLocations *world, const int cx, const int cy, const int mask)
{
    for (int y = -1; y <= 1; y++)
    for (int x = -1; x <= 1; x++)
    {
        int n;
        if (!(y == 0 && x == 0) &&
            (n = locFindByCoords(world, cx + x, cy + y, true)) >= 0)
        {
            const 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(const MapBlock *worldMap, MapLocations *worldLoc)
{
    int n, numNewLoc = 0, numInvLoc = 0, numNoMarker = 0;
    size_t noptLocMarkers = strlen(optLocMarkers);
    LocMarker *tmpl;

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

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

        for (int xc = 0; xc < worldMap->width; xc++)
        {
            if (muStrChr((unsigned char *) optLocMarkers, noptLocMarkers, dp[xc]))
            {
                LocName tmpNames[LOC_MAX_NAMES];
                char tmpDesc[512];
                int tmpFlags;

                // Check for new locations
                if (locFindByCoords(worldLoc, xc, yc, true) >= 0)
                    continue;


                if (dp[xc] == 'C')
                {
                    // In case of player cities, check for existing adjacent blocks
                    // so we can match with an existing pcity ..
                    n = checkForAdjacent(worldLoc, xc, yc, LOCF_M_PCITY);
                    if (n >= 0)
                    {
                        // Found, use its name for this block
                        tmpl = worldLoc->locations[n];
                        tmpFlags = LOCF_M_PCITY | LOCF_INVIS;
                        snprintf(tmpDesc, sizeof(tmpDesc),
                        "%s", tmpl->names[0].name);

                    }
                    else
                    {
                        // Nope, it is a "new" pcity
                        numNewLoc++;
                        tmpFlags = LOCF_M_PCITY;
                        snprintf(tmpDesc, sizeof(tmpDesc),
                        "%.3s-PCITY #%d",
                        optInFilename, numNewLoc);
                    }
                }
                else
                {
                    // Non-pcities are handled here
                    numNewLoc++;
                    tmpFlags = LOCF_NONE;
                    snprintf(tmpDesc, sizeof(tmpDesc),
                        "%.3s-UNK #%d",
                        optInFilename, numNewLoc);
                }

                memset(tmpNames, 0, sizeof(tmpNames));
                tmpNames[0].name = tmpDesc;
                locAddNew(worldLoc, xc, yc, LOCD_NONE, tmpFlags,
                    tmpNames, NULL, NULL, false, NULL, NULL, NULL);
            }
            else
            {
                // Check for misplaced locations
                if ((n = locFindByCoords(worldLoc, xc, yc, true)) >= 0)
                {
                    tmpl = worldLoc->locations[n];

                    if (tmpl->flags == LOCF_NONE)
                    {
                        // Mark as possibly invalid
                        tmpl->flags |= LOCF_INVALID;
                        numInvLoc++;
                    }
                    else
                    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 *pp1, const void *pp2)
{
    const LocMarker
        *vp1 = *(const LocMarker **) pp1,
        *vp2 = *(const LocMarker **) pp2;

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


/* Sort locations by name
 */
void maplocSort(MapLocations *lp)
{
    for (int i = 0; i < lp->nlocations; i++)
    {
        LocMarker *loc = lp->locations[i];
        loc->vsort.v_int = 2;

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

    qsort(lp->locations, lp->nlocations, 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, const MapBlock *map, const MapLocations *lp)
{
    for (int yc = 0; yc < map->height; yc++)
    {
        unsigned char *dp = map->data + (map->scansize * yc);

        for (int xc = 0; xc < map->width; xc++)
        {
            int n;
            if ((n = locFindByCoords(lp, xc, yc, true)) >= 0)
            {
                LocMarker *loc = lp->locations[n];
                char chm = dp[xc];

                switch (loc->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 (loc->flags & LOCF_INVALID)
                        chm = '$';
                    break;
                }

                fputc(0xfb, outFile);
                fprintf(outFile, "mloc%d_%d", loc->ox + 1, loc->oy + 1);
                fputc(0xfc, outFile);
                fputc(chm, outFile);
                fputc(0xfe, outFile);
            }
            else
            if (!optNoLabels && (n = locFindByCoords(lp, xc, yc, false)) >= 0)
            {
                LocMarker *loc = lp->locations[n];

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

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

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

                    default:
                        switch (loc->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 (loc->flags & LOCF_CLOSED)
                        col = col_light_red;

                    fputc(col, outFile);

                    if (optLabelType)
                    {
                        xc += locPrintType(outFile, loc, !optNoAdjust, NULL, false);
                    }

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

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

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


/* Print out location list as HTML option list.
 */
void outputMapLocHTML(FILE *outFile, const MapLocations *lp)
{
    fprintf(outFile, "<option value=\"\">-</option>\n");

    for (int i = 0; i < lp->nlocations; i++)
    {
        const LocMarker *loc = lp->locations[i];

        if (loc->flags & LOCF_INVIS)
            continue;

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


/* Output generated locations into given file stream
 */
void printLocNameEsc(FILE *outFile, const LocName *name)
{
    fputs(locGetAreaAuthorRole(name->flags, false), outFile);
    fputsesc2(name->name, outFile);
}


void printLocWarning(bool *first, FILE *fh, const LocMarker *loc, const char *msg)
{
    FILE *outFH = optWarningsToLoc ? fh : stderr;
    if (*first)
    {
        // Get continent name
        char *csep, *continent = loc->file != NULL ?
            th_strdup((loc->file->continent != NULL) ?
            loc->file->continent : loc->file->filename) : NULL;

        // Remove filename extension, if found
        if (continent != NULL &&
            (csep = strstr(continent, ".loc")) != NULL &&
            csep[4] == 0)
            *csep = 0;

        fprintf(outFH, "\n# '%s' @ go %d,%d,%s\n",
            loc->names[0].name,
            loc->ox + 1, loc->oy + 1,
            continent);

        th_free(continent);
    }

    fprintf(outFH, "# - %s\n", msg);
    *first = false;
}


void outputLocationFile(FILE *outFile, MapLocations *lp)
{
    // 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 (int i = 0; i < lp->nlocations; i++)
    {
        LocMarker *loc = lp->locations[i];

        // Add comment in few cases
        if (optWarnings)
        {
            bool first = true;
            if ((optWarnings & WARN_MARKER_INVALID) &&
                (loc->flags & LOCF_INVALID))
                printLocWarning(&first, outFile, loc, "Possibly invalid location marker");

            if ((optWarnings & WARN_MARKER_MISSING) &&
                (loc->flags & LOCF_NOMARKER))
                printLocWarning(&first, outFile, loc, "Location missing marker");

            // The next warnings apply only to visible non-pcity / non-shrine
            if ((loc->flags & LOCF_INVIS) == 0 &&
                (loc->flags & LOCF_M_PCITY) == 0 &&
                (loc->flags & LOCF_T_SHRINE) == 0)
            {
                if ((optWarnings & WARN_TIMESTAMPS_MISSING) && !loc->valid)
                    printLocWarning(&first, outFile, loc, "No timestamp");

                if ((optWarnings & WARN_AUTHORS_MISSING) && loc->nauthors == 0)
                    printLocWarning(&first, outFile, loc, "No authors listed");

                if (optWarnings & WARN_EXTRA)
                {
                    if (loc->valid)
                    {
                        const char *msg = NULL;
                        switch (loc->added.accuracy)
                        {
                            case TS_ACC_DEFAULT:
                                msg = "Addition timestamp 'default'.";
                                break;
/*
                            case TS_ACC_GUESSTIMATE:
                                msg = "Addition timestamp 'approximate'.";
                                break;
*/
                        }
                        if (msg != NULL)
                            printLocWarning(&first, outFile, loc, msg);
                    }

                    if (loc->nauthors > 0)
                    {
                        int flags = 0;
                        for (int nauthor = 0; nauthor < loc->nauthors; nauthor++)
                        {
                            flags |= loc->authors[nauthor].flags;
                        }
                        if ((flags & AUTHOR_ORIG) == 0)
                        {
                            printLocWarning(&first, outFile, loc, "Primary author not set");
                        }
                    }
                }
            }
        }

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

        switch (loc->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 (loc->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 (loc->flags & LOCF_CLOSED)
            fputc('!', outFile);

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

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

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

        if (loc->nauthors > 0)
        {
            printLocNameEsc(outFile, &loc->authors[0]);
            for (int n = 1; n < loc->nauthors; n++)
            {
                fprintf(outFile, ",");
                printLocNameEsc(outFile, &loc->authors[n]);
            }
        }

        fprintf(outFile, ";");

        if (loc->valid)
        {
            const char *acc;
            switch (loc->added.accuracy)
            {
                case TS_ACC_DEFAULT: acc = ""; break;
                case TS_ACC_KNOWN  : acc = "!"; break;
                case TS_ACC_GUESSTIMATE : acc = "?"; break;
                case TS_ACC_APPROXIMATE : acc = "#"; break;
                default: acc = "ERROR! "; break;
            }

            fprintf(outFile, "%s" LOC_TIMEFMT,
                acc, loc->added.day, loc->added.month, loc->added.year);
        }

        fprintf(outFile, ";");

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

        fprintf(outFile, ";");

        if (loc->freeform)
            fputsesc2(loc->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 *lp)
{
    // Output script start
    fprintf(outFile,
        "#!/bin/sh\n"
        "convert \"$1\" @OPTS_START@ \\\n");

    // Output instructions for each visible location
    for (int i = 0; i < lp->nlocations; i++)
    {
        LocMarker *loc = lp->locations[i];
        int xc, yc, leftMove;
        char *cs;

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

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

        switch (loc->align)
        {
        case LOCD_LEFTDOWN:
            yc = loc->yc + optUnitSize*3.0f;
            xc = loc->xc - leftMove;
            break;

        case LOCD_LEFT:
            yc = loc->yc - optUnitSize;
            xc = loc->xc - leftMove;
            break;

        case LOCD_DOWN:
            yc = loc->yc + optUnitSize*3.0f;
            xc = loc->xc + optUnitSize;
            break;

        case LOCD_NONE:
        default:
            yc = loc->yc - optUnitSize;
            xc = loc->xc + optUnitSize;
            break;
        }

        // Determine colour
        switch (loc->flags & LOCF_M_MASK)
        {
        case LOCF_M_CITY:
            cs = "'#880000'";
            break;
        case LOCF_M_PCITY:
            cs = "'#00ff00'";
            break;
        default:
            switch (loc->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;
            }
            break;
        }

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

        // Location marker
        fprintf(outFile,
            "\t-fill black -draw \"circle %d,%d %d,%d\" ",
            loc->xc, loc->yc,
            (int) (loc->xc + optUnitSize), (int) (loc->yc + optUnitSize));

        fprintf(outFile,
            "-fill %s -draw \"circle %d,%d %d,%d\" ",
            cs, loc->xc, loc->yc,
            (int) (loc->xc + optUnitSize * 0.90f),
            (int) (loc->yc + optUnitSize * 0.90f));


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

            fputsesc3(loc->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);

    for (size_t 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 const char *addQuestLink(char **buf, size_t *bufSize, size_t *bufLen,
    const char *ptr, const char *start, const 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, const LocMarker *loc,
    int (*fpr)(FILE *, const char *fmt, ...),
    int (*fps)(const char *, FILE *))
{
    if (loc->uri != NULL)
    {
        fpr(outFile, "<b><a target=\"_blank\" href=\"%s\">", loc->uri);
        locPrintType(outFile, loc, false, fps, true);
        fpr(outFile, "</a></b><br>");
    }
    else
    {
        fpr(outFile, "<b>");
        locPrintType(outFile, loc, false, fps, true);
        fpr(outFile, "</b><br>");
    }

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

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

    // Author names or societies
    if (loc->nauthors > 0)
    {
        if (loc->flags & LOCF_M_PCITY)
        {
            fprintf(outFile, "Societies: ");
            for (int n = 0; n < loc->nauthors; n++)
            {
                fps(loc->authors[n].name, outFile);
                if (n < loc->nauthors - 1)
                    fprintf(outFile, ", ");
            }
        }
        else
        {
            fprintf(outFile, "Authors: ");
            for (int n = 0; n < loc->nauthors; n++)
            {
                char *info = "", *sinfo = "";
                switch (loc->authors[n].flags)
                {
                    case AUTHOR_ORIG: info = " (O)"; sinfo = "Original coder"; break;
                    case AUTHOR_RECODER: info = " (R)"; sinfo = "Re-coder"; break;
                    case AUTHOR_MAINTAINER: info = " (M)"; sinfo = "Maintainer"; break;
                    case AUTHOR_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>",
                    loc->authors[n].name, sinfo,
                    loc->authors[n].name, info);

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

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

        while (*ptr != 0)
        {
            const char *start;

            // Detect AQ and LQ strings
            if (ptr[0] == 'A' && ptr[1] == 'Q' && th_isspace(ptr[2]) &&
                (start = strchr(ptr + 3, '"')) != NULL)
            {
                ptr = addQuestLink(&buf, &bufSize, &bufLen, ptr, start, strchr(start + 1, '"'));
            }
            else
            if (ptr[0] == 'L' && ptr[1] == 'Q' && th_isdigit(ptr[2]) &&
                (start = strchr(ptr + 3, '"')) != NULL)
            {
                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 *lp)
{
    fprintf(outFile,
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    "<markers>\n");

    // Output each location entry
    for (int i = 0; i < lp->nlocations; i++)
    {
        LocMarker *loc = lp->locations[i];

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

        // Coordinates and label direction/alignment
        fprintf(outFile, "<marker x=\"%d\" y=\"%d\" labeldir=\"%d\"",
            loc->ox, loc->oy, loc->align);

        // Primary location name
        fprintf(outFile, " name=\"");
        locPrintType(outFile, loc, false, fputse, false);
        fprintf(outFile, "\"");

        // HTML
        fprintf(outFile, " html=\"");
        outputGMapsHTML(outFile, loc, fprintfe, fputse);
        fprintf(outFile, "\"");

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

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

        // Type of the marker
        fprintf(outFile, " type=\"%s\"",
            locGetTypeName(loc->flags));

        // Added to game timestamp
        if (loc->valid)
        {
            const char *acc;
            switch (loc->added.accuracy)
            {
                case TS_ACC_DEFAULT: acc = "default"; break;
                case TS_ACC_KNOWN  : acc = "known"; break;
                case TS_ACC_GUESSTIMATE : acc = "guesstimate"; break;
                case TS_ACC_APPROXIMATE : acc = "approximate"; break;
                default: acc = "ERROR"; break;
            }

            fprintf(outFile,
                " added=\"" LOC_TIMEFMT "\" added-accuracy=\"%s\"",
                loc->added.day, loc->added.month, loc->added.year,
                acc);
        }

        fprintf(outFile, ">");

        // Location alternative names
        if (loc->nnames > 1)
        {
            fprintf(outFile, "<altnames>");
            for (int n = 1; n < loc->nnames; n++)
            {
                const char *tmps = locGetAreaNameType(loc->names[n].flags, true);
                fprintf(outFile, "<name");
                if (tmps != NULL)
                    fprintf(outFile, " type=\"original\"");
                fprintf(outFile, ">");
                fputse(loc->names[n].name, outFile);
                fprintf(outFile, "</name>");
            }
            fprintf(outFile, "</altnames>");
        }

        // Authors or secret societies
        if (loc->nauthors > 0)
        {
            if (loc->flags & LOCF_M_PCITY)
            {
                fprintf(outFile, "<societies>");
                for (int n = 0; n < loc->nauthors; n++)
                {
                    fprintf(outFile, "<name>");
                    fputse(loc->authors[n].name, outFile);
                    fprintf(outFile, "</name>");
                }
                fprintf(outFile, "</societies>");
            }
            else
            {
                fprintf(outFile, "<authors>");
                for (int n = 0; n < loc->nauthors; n++)
                {
                    fprintf(outFile, "<author role=\"%s\">",
                        locGetAreaAuthorRole(loc->authors[n].flags, true));
                    fputse(loc->authors[n].name, outFile);
                    fprintf(outFile, "</author>");
                }
                fprintf(outFile, "</authors>");
            }
        }

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

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


void outputGMapsJSON(FILE *outFile, MapLocations *lp)
{
    fprintf(outFile, "[\n");

    // Output each location entry
    for (int i = 0; i < lp->nlocations; i++)
    {
        LocMarker *loc = lp->locations[i];

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

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

        // Location name
        locPrintType(outFile, loc, false, fputsesc1, false);
        fprintf(outFile, "\",\"html\":\"");

        outputGMapsHTML(outFile, loc, fprintfesc1, fputsesc1);

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

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

        fprintf(outFile, "}%s\n",
            (i < lp->nlocations - 1) ? "," : "");
    }

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


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

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

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

    // Parse arguments
    if (!th_args_process(argc, argv, optList, optListN,
        argHandleOpt, NULL, OPTH_BAILOUT))
    {
        res = 1;
        goto out;
    }

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

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

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

    if (optInFilename == NULL && optOutput == OUTFMT_MAP)
    {
        argShowHelp();
        THERR("Map file required for map generation.\n");
        res = 1;
        goto out;
    }

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

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

    // Read location info
    for (int i = 0; i <= noptLocFiles; i++)
    {
        LocFileInfo *fp = &optLocFiles[i];

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

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

        if (!locParseLocStream(inFile, fp, &worldLoc, fp->xoffs, fp->yoffs))
        {
            res = -4;
            goto out;
        }

        fclose(inFile);
        inFile = NULL;
    }

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

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

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

            loc->xc = ((float) loc->xc) * optScale;
            loc->yc = ((float) loc->yc) * optScale;
        }
    }

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

    // 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.nlocations);
            break;

        case OUTFMT_MAPLOC:
            maplocSort(&worldLoc);
            THMSG(1, "Outputting MapLoc HTML ...\n");
            outputMapLocHTML(outFile, &worldLoc);
            THMSG(2, "%d locations\n", worldLoc.nlocations);
            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.nlocations);
            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;
    }

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

    if (inFile != NULL)
        fclose(inFile);

    mapBlockFree(worldMap);
    locFreeMapLocations(&worldLoc);

    return res;
}