view src/liblocfile.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 99c6d0cea264
children
line wrap: on
line source

/*
 * liblocfile - Location file format handling
 * Programmed by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
 * (C) Copyright 2006-2022 Tecnic Software productions (TNSP)
 */
#include "liblocfile.h"


// Internal parsing context structure
typedef struct
{
    LocFileInfo *file;

    char *filename;
    FILE *fp;
    unsigned int line;
    bool versionSet;
    int ch, parseMode, prevMode, nextMode, field, subField, sep;
    char *fieldSep;
} LocFileParseContext;


const char * locGetAreaNameType(const int flags, const bool type)
{
    const char *pfx;
    const char *desc;

    switch (flags)
    {
        case NAME_ORIG       : pfx = "@"; desc = "original"; break;
        default              : pfx = "" ; desc = NULL; break;
    }

    return type ? desc : pfx;
}


static void locAddAreaName(LocName *dst, int *ndst, const LocName *src, const int nsrc)
{
    if (src != NULL && src[nsrc].name != NULL)
    {
        int flags = 0;
        int n = 0;
        switch (src[nsrc].name[0])
        {
            case '@': n++; flags |= NAME_ORIG; break;
        }
        dst[*ndst].name = th_strdup(&(src[nsrc].name[n]));
        dst[*ndst].flags = flags;
        (*ndst)++;
    }
}


const char * locGetAreaAuthorRole(const int flags, const bool type)
{
    const char *pfx;
    const char *desc;

    switch (flags)
    {
        case AUTHOR_ORIG       : pfx = "@"; desc = "original"; break;
        case AUTHOR_RECODER    : pfx = "!"; desc = "recoder"; break;
        case AUTHOR_MAINTAINER : pfx = "%"; desc = "maintainer"; break;
        case AUTHOR_EXPANDER   : pfx = "&"; desc = "expander"; break;
        default               : pfx = "" ; desc = "undefined"; break;
    }

    return type ? desc : pfx;
}


static void locAddAreaAuthor(LocName *dst, int *ndst, const LocName *src, const int nsrc)
{
    if (src != NULL && src[nsrc].name != NULL)
    {
        int flags = 0;
        int n = 0;
        switch (src[nsrc].name[0])
        {
            case '@': n++; flags |= AUTHOR_ORIG; break;
            case '!': n++; flags |= AUTHOR_RECODER; break;
            case '%': n++; flags |= AUTHOR_MAINTAINER; break;
            case '&': n++; flags |= AUTHOR_EXPANDER; break;
        }
        dst[*ndst].name = th_strdup(&(src[nsrc].name[n]));
        dst[*ndst].flags = flags;
        (*ndst)++;
    }
}


const char *locGetTypePrefix(const int flags)
{
    switch (flags & LOCF_M_MASK)
    {
        case LOCF_M_CITY:   return "CITY";
        case LOCF_M_PCITY:  return "PCITY";

        default:
            switch (flags & LOCF_T_MASK)
            {
                case LOCF_T_SHRINE:     return "SHRINE";
                case LOCF_T_GUILD:      return "GUILD";
                case LOCF_T_SS:         return "SS";
                case LOCF_T_MONSTER:    return "MOB";
                case LOCF_T_TRAINER:    return "TRAINER";
                case LOCF_T_FORT:       return "FORT";
            }
            break;
    }

    return NULL;
}


const char *locGetTypeName(const int flags)
{
    switch (flags & LOCF_M_MASK)
    {
        case LOCF_M_CITY:   return "city";
        case LOCF_M_PCITY:  return "pcity";

        default:
            switch (flags & LOCF_T_MASK)
            {
                case LOCF_T_SHRINE:  return "shrine";
                case LOCF_T_GUILD:   return "guild";
                case LOCF_T_SS:      return "ss";
                case LOCF_T_MONSTER: return "monster";
                case LOCF_T_TRAINER: return "trainer";
                case LOCF_T_FORT:    return "fort";
            }
            break;
    }

    return "default";
}


LocMarker * locCopyLocMarker(const LocMarker *src)
{
    LocMarker *res = th_malloc0(sizeof(LocMarker));
    if (res == NULL)
        return NULL;

    // Just copy the data, as most of it is "static"
    // and then replace the pointers etc. as necessary.
    memcpy(res, src, sizeof(LocMarker));
    res->file = NULL;
    res->uri = th_strdup(src->uri);
    res->freeform = th_strdup(src->freeform);

    for (int i = 0; i < res->nnames; i++)
        res->names[i].name = th_strdup(src->names[i].name);

    for (int i = 0; i < res->nauthors; i++)
        res->authors[i].name = th_strdup(src->authors[i].name);

    return res;
}


void locCopyLocations(MapLocations *dst, const MapLocations *src)
{
    if (dst == NULL || src == NULL)
        return;

    dst->nlocations = src->nlocations;
    dst->locations = (LocMarker **) th_malloc(dst->nlocations * sizeof(LocMarker *));

    for (int i = 0; i < dst->nlocations; i++)
        dst->locations[i] = locCopyLocMarker(src->locations[i]);
}


bool locAddNew(MapLocations *l, int xc, int yc, int dir, int flags,
    LocName *names, LocName *authors, LocDateStruct *added, bool valid,
    const char *uri, const char *freeform, LocFileInfo *file)
{
    LocMarker *tmp;
    int i;

    // Allocate location struct
    if ((tmp = th_malloc0(sizeof(LocMarker))) == NULL)
        return false;

    tmp->xc = tmp->ox = xc;
    tmp->yc = tmp->oy = yc;
    tmp->align = dir;
    tmp->flags = flags;
    tmp->file = file;

    for (i = 0; i < LOC_MAX_NAMES; i++)
    {
        locAddAreaName(tmp->names, &tmp->nnames, names, i);
        locAddAreaAuthor(tmp->authors, &tmp->nauthors, authors, i);
    }

    if (added != NULL)
    {
        memcpy(&(tmp->added), added, sizeof(tmp->added));
        tmp->valid = valid;
    }
    else
    {
        time_t stamp = time(NULL);
        struct tm *tmpTime = localtime(&stamp);
        tmp->added.day = tmpTime->tm_mday;
        tmp->added.month = tmpTime->tm_mon + 1;
        tmp->added.year = tmpTime->tm_year + 1900;
        tmp->valid = true;
    }
    tmp->uri = th_strdup(uri);
    tmp->freeform = th_strdup(freeform);

    // Add new location
    l->locations = (LocMarker **) th_realloc(l->locations,
        sizeof(LocMarker*) * (l->nlocations+1));

    if (l->locations == NULL)
        return false;

    l->locations[l->nlocations] = tmp;
    l->nlocations++;

    return true;
}


int locFindByCoords(const MapLocations *l, const int xc, const int yc, const bool locTrue)
{
    for (int i = 0; i < l->nlocations; i++)
    {
        LocMarker *tmp = l->locations[i];
        if (locTrue)
        {
            if (tmp->ox == xc && tmp->oy == yc)
                return i;
        }
        else
        {
            if (tmp->xc == xc && tmp->yc == yc)
                return i;
        }
    }

    return -1;
}


void locFreeMarkerData(LocMarker *marker)
{
    for (int i = 0; i < LOC_MAX_NAMES; i++)
    {
        th_free_r(&marker->names[i].name);
        th_free_r(&marker->authors[i].name);
    }

    th_free_r(&marker->uri);
    th_free_r(&marker->freeform);
}


void locFreeMapLocations(MapLocations *loc)
{
    if (loc->locations != NULL)
    {
        for (int i = 0; i < loc->nlocations; i++)
        if (loc->locations[i] != NULL)
        {
            locFreeMarkerData(loc->locations[i]);
            th_free(loc->locations[i]);
        }
        th_free(loc->locations);
    }
}


enum
{
    PM_IDLE = 0,
    PM_FIELD,
    PM_FIELD_SEP,
    PM_COMMENT,
    PM_NEXT,
    PM_EOF,
    PM_ERROR
};


static int locFGetc(LocFileParseContext *f)
{
    return fgetc(f->fp);
}


static void locPMSet(LocFileParseContext *f, int parseMode, int nextMode)
{
    f->prevMode = f->parseMode;

    if (parseMode != -1)
        f->parseMode = parseMode;

    if (nextMode != -1)
        f->nextMode = nextMode;
}


static void locPMErr(LocFileParseContext *ctx, const char *fmt, ...)
{
    va_list ap;

    fprintf(stderr, "[%s:%d @ %d]: ", ctx->file->filename, ctx->line, ctx->field);
    ctx->parseMode = PM_ERROR;

    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
}


static bool locCheckFlag(int flags, int mask, int flag)
{
    if (mask)
        return (flags & mask) == flag;
    else
        return (flags & flag);
}


static bool locCheckMutex(LocFileParseContext *f, int *flags, int mask, int flag)
{
    if (!locCheckFlag(*flags, mask, 0) &&
        !locCheckFlag(*flags, mask, flag))
    {
        locPMErr(f, "Invalid flags setting.\n");
        return false;
    }
    else
    {
        *flags |= flag;
        return true;
    }
}


static bool parseFieldInt(LocFileParseContext *f, int *val)
{
    int res = 0;

    if (!isdigit(f->ch))
        return false;

    while (isdigit(f->ch))
    {
        res *= 10;
        res += f->ch - '0';
        f->ch = locFGetc(f);
    }

    *val = res;
    return true;
}


static char * parseFieldString(LocFileParseContext *f, const char *endch)
{
    char res[4096];
    bool end = false;
    size_t pos = 0;
    int i;

    if (strchr(endch, f->ch))
        return NULL;

    while (!end && !strchr(endch, f->ch) && pos < sizeof(res) - 1)
    {
        switch (f->ch)
        {
        case '\n':
        case '\r':
            locPMErr(f, "Unexpected EOL inside text field.\n");
            return NULL;

        case EOF:
            locPMErr(f, "Unexpected EOF inside text field.\n");
            return NULL;

        case '\\':
            // Enable continuation via '\' at EOL
            i = locFGetc(f);
            if (i == EOF)
            {
                locPMErr(f, "Unexpected EOF inside text field.\n");
                return NULL;
            }
            else
            if (i == '\n' || i == '\r')
            {
                f->ch = locFGetc(f);
                if (i == '\r' && f->ch == '\n')
                    f->ch = locFGetc(f);
            }
            else
            {
                res[pos++] = i;
                f->ch = locFGetc(f);
            }
            break;

        default:
            res[pos++] = f->ch;
            f->ch = locFGetc(f);
            break;
        }
    }

    while (pos > 0 && isspace(res[pos - 1]))
        res[--pos] = 0;

    res[pos] = 0;

    return (pos > 0) ? th_strdup(res) : NULL;
}


static bool locParseFlags(LocFileParseContext *ctx, int *flags)
{
    bool endFlags;

    *flags = LOCF_NONE;
    endFlags = false;
    while (!endFlags)
    {
        switch (ctx->ch)
        {
            // Scenic marker flags
        case '?':
            if (!locCheckMutex(ctx, flags, LOCF_M_MASK, LOCF_M_SCENIC1))
                return false;
            break;
        case '%':
            if (!locCheckMutex(ctx, flags, LOCF_M_MASK, LOCF_M_SCENIC2))
                return false;
            break;
        case 'C':
            if (!locCheckMutex(ctx, flags, LOCF_M_MASK, LOCF_M_PCITY))
                return false;
            break;
        case 'c':
            if (!locCheckMutex(ctx, flags, LOCF_M_MASK, LOCF_M_CITY))
                return false;
            break;

            // Marker type flags
        case 'S':
            if (!locCheckMutex(ctx, flags, LOCF_T_MASK, LOCF_T_SHRINE))
                return false;
            break;
        case 'G':
            if (!locCheckMutex(ctx, flags, LOCF_T_MASK, LOCF_T_GUILD))
                return false;
            break;
        case 'P':
            if (!locCheckMutex(ctx, flags, LOCF_T_MASK, LOCF_T_SS))
                return false;
            break;
        case 'M':
            if (!locCheckMutex(ctx, flags, LOCF_T_MASK, LOCF_T_MONSTER))
                return false;
            break;
        case 'T':
            if (!locCheckMutex(ctx, flags, LOCF_T_MASK, LOCF_T_TRAINER))
                return false;
            break;
        case 'F':
            if (!locCheckMutex(ctx, flags, LOCF_T_MASK, LOCF_T_FORT))
                return false;
            break;

            // Additional flags
        case '-':
            *flags |= LOCF_INVIS;
            break;

        case '!':
            *flags |= LOCF_CLOSED;
            break;

        case 'I':
            *flags |= LOCF_INSTANCED;
            break;

        default:
            endFlags = true;
            break;
        }

        ctx->ch = locFGetc(ctx);
    }

    return true;
}


static void locParseMultiField(LocFileParseContext *ctx, char *fieldsep, char sep, const char *desc, LocName *data)
{
    if (ctx->subField < 0)
    {
        ctx->subField = 0;
        ctx->fieldSep = fieldsep;
        ctx->sep = sep;
    }

    if (ctx->sep == sep)
    {
        if (ctx->subField < LOC_MAX_NAMES)
        {
            th_free(data[ctx->subField].name);
            data[ctx->subField++].name = parseFieldString(ctx, ctx->fieldSep);
            locPMSet(ctx, PM_NEXT, PM_FIELD_SEP);
            if (!strchr(ctx->fieldSep, ctx->ch))
                locPMErr(ctx, "Expected field separator '%s' after %s.\n", ctx->fieldSep, desc);
        }
        else
            locPMErr(ctx, "Too many %s (max %d).\n", desc, LOC_MAX_NAMES);
    }
    else
    {
        ctx->fieldSep = ";";
        ctx->subField = -1;
        ctx->field++;
        locPMSet(ctx, PM_FIELD, -1);
    }
}


static void locParseLocField(LocFileParseContext *ctx, MapLocations *l, LocMarker *marker)
{
    bool res = false;
    char *tmpStr;
    int i;

    if ((ctx->ch == '\n' || ctx->ch == '\r') && ctx->field < 8)
    {
        locPMErr(ctx, "Unexpected end of line.\n");
        return;
    }

    switch (ctx->field)
    {
    case 1:            // X-coordinate
        res = parseFieldInt(ctx, &marker->xc);
        ctx->fieldSep = ";";
        if (res)
        {
            ctx->field++;
            locPMSet(ctx, PM_NEXT, PM_FIELD_SEP);
        }
        else
            locPMErr(ctx, "Error parsing X-coordinate.\n");
        break;

    case 2:            // Y-coordinate
        res = parseFieldInt(ctx, &marker->yc);
        if (res)
        {
            ctx->field++;
            locPMSet(ctx, PM_NEXT, PM_FIELD_SEP);
        }
        else
            locPMErr(ctx, "Error parsing Y-coordinate.\n");
        break;

    case 3:            // Label orientation and flags
        res = parseFieldInt(ctx, &marker->align);
        if (res)
            res = locParseFlags(ctx, &marker->flags);

        if (res)
        {
            ctx->field++;
            locPMSet(ctx, PM_NEXT, PM_FIELD_SEP);
        }
        else
            locPMErr(ctx, "Error parsing orientation and flags field.\n");
        break;

    case 4: // Location name(s)
        locParseMultiField(ctx, "|;", '|', "location names", marker->names);
        break;

    case 5:            // Authors
        locParseMultiField(ctx, ",;", ',', "author names", marker->authors);
        break;

    case 6:            // Date
        marker->valid = false;
        marker->added.accuracy = TS_ACC_DEFAULT;
        tmpStr = parseFieldString(ctx, ctx->fieldSep);
        if (tmpStr && tmpStr[0])
        {
            char *stamp;
            switch (tmpStr[0])
            {
                case TS_ACC_KNOWN:
                case TS_ACC_GUESSTIMATE:
                case TS_ACC_APPROXIMATE:
                    marker->added.accuracy = (int) tmpStr[0];
                    stamp = tmpStr + 1;
                    break;

                default:
                    stamp = tmpStr;
            }

            if (sscanf(stamp, LOC_TIMEFMT, &marker->added.day, &marker->added.month, &marker->added.year) == 3)
                marker->valid = true;
            else
            {
                locPMErr(ctx, "Warning, invalid timestamp '%s' in '%s'.\n",
                    tmpStr, marker->names[0].name);
            }
        }
        th_free(tmpStr);
        ctx->field++;
        locPMSet(ctx, PM_NEXT, PM_FIELD_SEP);
        break;

    case 7:            // URI
        th_free(marker->uri);
        marker->uri = parseFieldString(ctx, ctx->fieldSep);
        ctx->field++;
        locPMSet(ctx, PM_NEXT, PM_FIELD_SEP);
        break;

    case 8:            // Freeform
        tmpStr = parseFieldString(ctx, "\r\n");

        // Check coordinates
        if (marker->xc < 1 || marker->yc < 1)
        {
            locPMErr(ctx, "Invalid X or Y coordinate (%d, %d), for location '%s'. Must be > 0.\n",
                marker->xc, marker->yc, marker->names[0].name);
        }

        // Check if location already exists
        marker->xc = marker->xc + marker->ox - 1;
        marker->yc = marker->yc + marker->oy - 1;

        i = locFindByCoords(l, marker->xc, marker->yc, true);
        if (i >= 0)
        {
            LocMarker *tloc = l->locations[i];
            locPMErr(ctx, "Warning, location already in list! (%d,%d) '%s' <-> (%d,%d) '%s'\n",
                tloc->xc + 1, tloc->yc + 1, tloc->names[0].name,
                marker->xc + 1, marker->yc + 1, marker->names[0].name);
        }
        else
        {
            // Add new location to our list
            locAddNew(l, marker->xc, marker->yc, marker->align, marker->flags,
                      marker->names, marker->authors, &marker->added,
                      marker->valid, marker->uri, tmpStr, ctx->file);
            locPMSet(ctx, PM_IDLE, -1);
        }

        locFreeMarkerData(marker);
        th_free(tmpStr);
        break;

    default:
        locPMErr(ctx, "FATAL ERROR! Invalid state=%d!\n", ctx->parseMode);
    }
}


bool locParseLocStream(FILE *fp, LocFileInfo *file, MapLocations *l, const int offX, const int offY)
{
    LocFileParseContext ctx;
    LocMarker marker;
    int i;

    memset(&ctx, 0, sizeof(ctx));
    ctx.fp = fp;
    ctx.line = 1;
    ctx.ch = -1;
    ctx.file = file;

    memset(&marker, 0, sizeof(marker));
    marker.ox = offX;
    marker.oy = offY;

    ctx.parseMode = PM_IDLE;
    ctx.nextMode = ctx.prevMode = PM_ERROR;
    ctx.field = ctx.subField = ctx.sep = -1;

    ctx.ch = locFGetc(&ctx);
    do
    {
        switch (ctx.parseMode)
        {
        case PM_IDLE:
            if (ctx.ch == EOF)
                locPMSet(&ctx, PM_EOF, -1);
            else
            if (ctx.ch == '\r')
            {
                ctx.line++;
                ctx.ch = locFGetc(&ctx);
                if (ctx.ch == '\n')
                    ctx.ch = locFGetc(&ctx);
            }
            else
            if (ctx.ch == '\n')
            {
                ctx.line++;
                ctx.ch = locFGetc(&ctx);
            }
            else
            if (ctx.ch == '#')
            {
                locPMSet(&ctx, PM_COMMENT, PM_IDLE);
            }
            else
            if (isdigit(ctx.ch))
            {
                // Start of a record
                locPMSet(&ctx, PM_FIELD, -1);
                ctx.field = 1;
            }
            else
            if (isspace(ctx.ch))
            {
                ctx.ch = locFGetc(&ctx);
            }
            else
            {
                // Syntax error
                locPMErr(&ctx, "Syntax error in '%s' line #%d.\n",
                    ctx.filename, ctx.line);
            }
            break;

        case PM_COMMENT:
            switch (ctx.ch)
            {
            case '\r':
                ctx.ch = locFGetc(&ctx);
                if (ctx.ch == '\n')
                    ctx.ch = locFGetc(&ctx);
                ctx.line++;
                ctx.prevMode = ctx.parseMode;
                ctx.parseMode = ctx.nextMode;
                break;
            case '\n':
                ctx.ch = locFGetc(&ctx);
                ctx.line++;
                ctx.prevMode = ctx.parseMode;
                ctx.parseMode = ctx.nextMode;
                break;
            case EOF:
                ctx.parseMode = PM_EOF;
                break;
            default:
                ctx.ch = locFGetc(&ctx);

                /* Because loc file identification should be the first
                 * comment line, we check it here.
                 */
                if (ctx.versionSet || !isalpha(ctx.ch))
                    break;

                char *tmp = parseFieldString(&ctx, "(\n\r");
                if (tmp != NULL && !strcmp(tmp, LOC_MAGIC))
                {
                    // ID found, check version
                    char *verStr = parseFieldString(&ctx, ")\n\r");
                    int verMajor, verMinor;
                    if (verStr != NULL && sscanf(verStr, "(version %d.%d", &verMajor, &verMinor) == 2)
                    {
                        if (verMajor != LOC_VERSION_MAJOR)
                        {
                            // Major version mismatch, bail out with informative message
                            THERR(
                                "LOC file format version %d.%d detected, internal version is %d.%d. "
                                "Refusing to read due to potential incompatibilities. If you neverthless "
                                "wish to proceed, change the loc file's version to match internal version.\n",
                                verMajor, verMinor, LOC_VERSION_MAJOR, LOC_VERSION_MINOR);
                            ctx.parseMode = PM_ERROR;
                        }
                        else
                        if (verMinor != LOC_VERSION_MINOR)
                        {
                            // Minor version mismatch, just inform about it
                            THERR("LOC file format version %d.%d detected, internal version is %d.%d, proceeding.\n",
                                verMajor, verMinor, LOC_VERSION_MAJOR, LOC_VERSION_MINOR);
                        }
                    }
                    else
                    {
                        THERR("Invalid or malformed LOC file, version not found (%s).\n",
                             verStr);
                        ctx.parseMode = PM_ERROR;
                    }
                    th_free(verStr);
                }
                else
                {
                    THERR("Invalid LOC file, the file ID is missing ('# %s (version %d.%d)' should be the first line.)\n",
                        LOC_MAGIC, LOC_VERSION_MAJOR, LOC_VERSION_MINOR);
                    ctx.parseMode = PM_ERROR;
                }
                th_free(tmp);
                ctx.versionSet = true;
                break;
            }
            break;

        case PM_NEXT:
            switch (ctx.ch)
            {
            case EOF:
                locPMErr(&ctx, "Unexpected end of file.\n");
                break;
            case 32:
            case 9:
                ctx.ch = locFGetc(&ctx);
                break;
            case '\\':
                // Enable continuation via '\' at EOL
                i = locFGetc(&ctx);
                if (i != '\n' && i != '\r')
                {
                    locPMErr(&ctx, "Expected end of line.\n");
                }
                else
                {
                    ctx.line++;
                    ctx.ch = locFGetc(&ctx);
                    if (i == '\r' && ctx.ch == '\n')
                        ctx.ch = locFGetc(&ctx);
                }
                break;
            default:
                ctx.prevMode = ctx.parseMode;
                ctx.parseMode = ctx.nextMode;
                break;
            }
            break;

        case PM_FIELD_SEP:
            if (strchr(ctx.fieldSep, ctx.ch) != NULL)
            {
                ctx.sep = ctx.ch;
                ctx.ch = locFGetc(&ctx);
                locPMSet(&ctx, PM_NEXT, PM_FIELD);
            }
            else
            {
                locPMErr(&ctx, "Expected field separator '%s', got '%c' (%d).\n",
                    ctx.fieldSep, ctx.ch, ctx.ch);
            }
            break;

        case PM_FIELD:
            locParseLocField(&ctx, l, &marker);
            break;

        default:
            locPMErr(&ctx, "Invalid state in loc-file parser - mode=%d, prev=%d, next=%d.\n",
                ctx.parseMode, ctx.prevMode, ctx.nextMode);
            break;
        }
    }
    while (ctx.parseMode != PM_ERROR && ctx.parseMode != PM_EOF);

    locFreeMarkerData(&marker);

    return (ctx.parseMode == PM_EOF);
}


void locSetFileInfo(LocFileInfo *file, const char *filename, const char *continent)
{
    file->filename = th_strdup(filename);
    file->continent = th_strdup(continent);
    file->xoffs = file->yoffs = 0;
}


void locFreeFileInfo(LocFileInfo *file)
{
    th_free(file->filename);
    th_free(file->continent);
}