If you have comments or questions concerning this source file, discuss them in the forum.
/*
Copyright (c) 2002 Nicolai Haehnle

See the license.txt for details. If that file was not included in the
source distributions, please email <prefect@rtts.org>
*/
// ge_level.cpp - contains the ELevel

#include "game.h"

#include "ff_level.h"

/*
==============================================================================

ETileTypes IMPLEMENTATION

==============================================================================
*/

/*
ETileTypes doesn't remove unused tiles.
This isn't a problem, since it will be destroyed after a level finishes,
and there's hardly a reason to load/unload tiles in the middle of a level.
*/

/*
==============
ETileTypes::ETileTypes

The tile types container starts out empty
==============
*/
ETileTypes::ETileTypes()
{
    m_iNumTT = 0;
    m_pTT = 0;
}

/*
==============
ETileTypes::~ETileTypes

Free all tile types
==============
*/
ETileTypes::~ETileTypes()
{
    unsigned i;

    if (m_iNumTT) {
        for(i = 0; i < m_iNumTT; i++)
            m_pTT[i].pic->Release();
        L_Free(m_pTT);
    }
}

/*
==============
ETileTypes::Load

Name is just the plain name, without a path or extension.
Returns ~0 if the tile couldn't be loaded.
==============
*/
unsigned ETileTypes::Load(const char *name)
{
    char path[MAX_OSPATH];
    HPicture *pic;
    tiletype_t *tt;
    unsigned i;

    snprintf(path, sizeof(path), "tiles/%s", name);

    for(i = 0, tt = m_pTT; i < m_iNumTT; i++, tt++) {
        if (!strcmp(path, tt->pic->m_szName))
            return i;
    }

    try {
        pic = HPicture::Get(path, pic_normal, 0);
    } catch(LError &err) {
        L_Printf("Couldn't load tile %s: %s\n", name, err.Get());
        return ~0;
    }

    i = m_iNumTT++;
    m_pTT = (tiletype_t *)L_Realloc(m_pTT, sizeof(tiletype_t)*m_iNumTT, TAG_MAP);
    m_pTT[i].pic = pic;
    return i;
}

/*
==============================================================================

airjob_t IMPLEMENTATION

==============================================================================
*/

airjob_t::airjob_t()
{
    x = y = 0;
    type = 0;
    level = 0;
}

airjob_t::airjob_t(airunit_type_t *ntype)
{
    x = y = 0;
    type = ntype;
    type->used++;
    level = 0;
}

airjob_t::airjob_t(const airjob_t &copy)
{
    x = copy.x;
    y = copy.y;
    type = copy.type;
    if (type)
        type->used++;
    level = copy.level;

    nodes = copy.nodes;
}

airjob_t::~airjob_t()
{
    if (type)
        Air_FreeType(type);
}

airjob_t &airjob_t::operator=(const airjob_t &copy)
{
    if (type)
        Air_FreeType(type);

    x = copy.x;
    y = copy.y;
    type = copy.type;
    if (type)
        type->used++;
    level = copy.level;
    nodes = copy.nodes;

    return *this;
}


/*
==============================================================================

ELevel IMPLEMENTATION

==============================================================================
*/

/*
==============
ELevel::ELevel

Creates an empty level of length 0
==============
*/
ELevel::ELevel(ETileTypes *pTileTypes)
{
    m_bEdit = false;

    m_iBaseLevel = -1;
    m_szName[0] = 0;
    m_szAuthor[0] = 0;
    m_iMapLength = 0;

    m_pTileTypes = pTileTypes;
    m_pTiles = 0;

    m_flLastDistance = m_flDistance = 0;

    m_iNumBosses = 0;
}

/*
==============
ELevel::~ELevel

Free all allocated resources
==============
*/
ELevel::~ELevel()
{
    Clear();
}

/*
==============
ELevel::Clear

Completely empties the level (length is reset to 0)
==============
*/
void ELevel::Clear()
{
    m_AirSched.clear();
    m_AirJobs.clear();

    if (m_pTiles) {
        L_Free(m_pTiles);
        m_pTiles = 0;
    }

    m_iMapLength = 0;

    m_flLastDistance = m_flDistance = 0;

    m_iBaseLevel = -1;
    m_szName[0] = 0;
    m_szAuthor[0] = 0;

    m_iNumBosses = 0;
}

/*
==============
ELevel::SetEditMode

Potentially rearrange pack/unpack data structures (not done right now).
==============
*/
void ELevel::SetEditMode(bool bEdit)
{
    m_bEdit = bEdit;
}

/*
==============
ELevel::BeginPlay
ELevel::EndPlay

Called before and after the level is used for playing.
Used to prepare/finish airjob usage. level is the builtin
level (0..2)
==============
*/
struct airsched_compare {
    bool operator() (const airsched_t &s1, const airsched_t &s2)
    {
        return s1.line > s2.line;
    }
};

void ELevel::BeginPlay(int level)
{
    m_AirSched.clear();

    for(airjob_it job = m_AirJobs.begin(); job != m_AirJobs.end(); job++) {
        airsched_t sched;

        sched.job = &*job;
        sched.line = (job->y + FIRSTSHADOWY(job->type->sprite->maxs[1])) / 32.0;

        if (job->level <= level && sched.line >= m_flDistance+15)
            m_AirSched.push_back(sched);
    }

    airsched_compare compare;
    std::sort(m_AirSched.begin(), m_AirSched.end(), compare);
}

void ELevel::EndPlay()
{
}

/*
==============
ELevel::Logic

Perform in-game logic: Scroll the level, launched jobs. Called once
per game logic frame
==============
*/
void ELevel::Logic()
{
    lassert(!m_bEdit);

    m_flLastDistance = m_flDistance;
    m_flDistance += 1.5 * LOGICTIME / 1000.0;
    if (m_flDistance > m_iMapLength-15)
        m_flDistance = m_iMapLength-15;

    while(m_AirSched.size()) {
        airsched_it sched = m_AirSched.end()-1;

        if (sched->line > m_flDistance+15)
            break;

        ge->Air_Spawn(sched->job);
        m_AirSched.pop_back();
    }
}

/*
==============
ELevel::Draw

Draw the level. In edit mode, missing tiles are drawn in pink.
==============
*/
void ELevel::Draw()
{
    float distance;
    int x, y;
    float scry, scrx;
    tile_t *tile;
    HPicture *pl;

    // interpolate the effective distance
    if (ge && m_flLastDistance != m_flDistance)
        distance = LERP(m_flLastDistance, m_flDistance);
    else
        distance = m_flLastDistance;

    scry = 480 + 32 * (distance - (int)distance);

    y = (int)distance;
    tile = &m_pTiles[y*MAPWIDTH];
    while(y < m_iMapLength && scry >= 0)
    {
        scrx = 0;
        scry -= 32;

        if (y < 0) {
            if (m_bEdit)
                hal->FillRect(0, (int)scry, 640, 32, 0, 0, 0);
            tile += MAPWIDTH;
            y++;
            continue;
        }

        for(x = 0; x < MAPWIDTH; x++) {
            if (m_pTileTypes->ValidId(tile->gfx)) {
                pl = m_pTileTypes->m_pTT[tile->gfx].pic;
                pl->Draw(scrx, scry);
            } else if (m_bEdit)
                hal->FillRect((int)scrx, (int)scry, 32, 32, 255, 0, 255);

            tile++;
            scrx += 32;
        }

        y++;
    }

    if (scry > 0 && m_bEdit)
        hal->FillRect(0, 0, 640, (int)scry, 0, 0, 0);
}

/*
==============
ELevel::SetDistance

Override the scrolling distance
==============
*/
void ELevel::SetDistance(float dist)
{
    m_flLastDistance = m_flDistance = dist;
}

/*
==============
ELevel::TileAtPoint
ELevel::PointForTile

Determine which tile is below a certain coordinate and vice versa.
Note that coordinates are in screen coordinates.
==============
*/
void ELevel::TileAtPoint(float x, float y, int *ptx, int *pty)
{
    y = -y + 480;
    y = y / 32.0;
    y += m_flDistance;
    *pty = (int)y;

    x = x / 32.0;
    *ptx = (int)x;
}

void ELevel::PointForTile(int tx, int ty, float *px, float *py)
{
    float x, y;

    x = tx * 32;
    y = (ty+1) - m_flDistance;
    y = y * 32;
    y = -y + 480;

    *px = x;
    *py = y;
}

/*
==============
ELevel::Resize

Add or remove lines to/from the level
==============
*/
void ELevel::Resize(bool start, int lines)
{
    int x, y, dy;
    int i;
    int newlength;
    int tiles;
    tile_t *tile;

    lassert(m_bEdit);

    if (lines < -m_iMapLength)
        lines = -m_iMapLength;
    if (!lines)
        return;

    // Clear all the tiles properly to prevent leaks
    if (lines < 0) {
        if (start) {
            y = 0;
            dy = 1;
        } else {
            y = m_iMapLength - 1;
            dy = -1;
        }

        i = -lines;
        while(i--) {
            for(x = 0; x < MAPWIDTH; x++)
                SetTile(x, y, 0);
            y += dy;
        }
    }

    // Realloc tiles memory
    newlength = m_iMapLength + lines;
    tiles = lines * MAPWIDTH;

    if (start) {
        if (lines < 0)
            memmove(m_pTiles, &m_pTiles[-tiles], (m_iMapLength+lines)*sizeof(tile_t)*MAPWIDTH);

        m_pTiles = (tile_t *)L_Realloc(m_pTiles, sizeof(tile_t)*newlength*MAPWIDTH,
                        TAG_MAP);

        if (lines > 0) {
            memmove(&m_pTiles[tiles], m_pTiles, m_iMapLength*MAPWIDTH*sizeof(tile_t));
            for(i = 0, tile = m_pTiles; i < tiles; i++, tile++)
                tile->gfx = ~0;
        }

        SetDistance(m_flDistance + lines);
    } else {
        m_pTiles = (tile_t *)L_Realloc(m_pTiles, sizeof(tile_t)*(newlength*MAPWIDTH),
                        TAG_MAP);

        if (lines > 0) {
            for(i = 0, tile = &m_pTiles[m_iMapLength*MAPWIDTH]; i < tiles; i++, tile++)
                tile->gfx = ~0;
        }
    }

    m_iMapLength = newlength;
}

/*
==============
ELevel::SetTile

Sets a given tile to be of a specific type.
Ignore out-of-bounds coordinates.
==============
*/
void ELevel::SetTile(int x, int y, unsigned id)
{
    tile_t *tile;

    lassert(m_bEdit);

    if (x < 0 || x >= MAPWIDTH || y < 0 || y >= m_iMapLength)
        return;

    tile = &m_pTiles[y*MAPWIDTH + x];
    tile->gfx = id;
}

/*
==============
ELevel::GetTile

Retrieve the tiletype id.
==============
*/
unsigned ELevel::GetTile(int x, int y)
{
    tile_t *tile;

    lassert(m_bEdit);

    if (x < 0 || x >= MAPWIDTH || y < 0 || y >= m_iMapLength)
        return 0;

    tile = &m_pTiles[y*MAPWIDTH + x];
    return tile->gfx;
}

/*
==============
ELevel::SavePrepare

Optimize the level and check for problems
==============
*/
bool ELevel::SavePrepare()
{
    bool playable;
    tile_t *tile;
    int tiles, i;

    playable = true;

    // 1) Check baselevel, name, length for validity
    if (m_iMapLength < 30 || m_iBaseLevel < 0 || !m_szName[0] || !m_szAuthor[0])
        playable = false;

    // 2) Check for missing tiles
    tiles = m_iMapLength * MAPWIDTH;
    for(i = 0, tile = m_pTiles; i < tiles; i++, tile++) {
        if (!m_pTileTypes->ValidId(tile->gfx)) {
            playable = false;
            break;
        }
    }

    // 3) Map is invalid if there is no boss
    if (!m_iNumBosses)
        playable = false;

    return playable;
}

/*
==============
ELevel::Save

Saves the level, returns true if the level is playable
==============
*/
bool ELevel::Save()
{
    char buf[MAX_OSPATH];
    LFileWrite fw;
    bool playable;
    tile_t *tile;
    unsigned *map;
    int offset, usedtypes;
    int tiles, i;

    playable = SavePrepare();

    // 1) Write header
    fw.Integer(LVL_MAGIC);
    fw.Integer(LVL_VERSION);

    fw.Integer(m_iBaseLevel);
    fw.CString(m_szName);
    fw.Byte(playable ? 1 : 0);
    fw.Integer(LVL_FEAT_AIRJOBS|LVL_FEAT_LEVELS|LVL_FEAT_AUTHOR); // features flag

    tiles = m_iMapLength * MAPWIDTH;

    // 2) Write tiletype names
    // The tiletype container may include many more tiletypes than are needed
    // for the map. However, only those actually needed are written out.
    // The map will later be used to translate between internal and on-disk
    // tiletype IDs
    offset = fw.filepos;
    fw.Integer(0); // fix up later

    map = (unsigned *)L_Malloc(sizeof(unsigned)*m_pTileTypes->m_iNumTT, TAG_MAP);
    memset(map, -1, sizeof(unsigned)*m_pTileTypes->m_iNumTT);
    usedtypes = 0;

    for(i = 0, tile = m_pTiles; i < tiles; i++, tile++) {
        if (!m_pTileTypes->ValidId(tile->gfx))
            continue;

        if (map[tile->gfx] == ~0) {
            map[tile->gfx] = usedtypes++;
            fw.CString(m_pTileTypes->m_pTT[tile->gfx].pic->m_szName + 6);
        }
    }

    fw.Integer(usedtypes, offset); // fix up tile type count

    // 3) Write actual tile info
    fw.Integer(m_iMapLength);
    for(i = 0, tile = m_pTiles; i < tiles; i++, tile++) {
        if (m_pTileTypes->ValidId(tile->gfx))
            fw.Integer(map[tile->gfx]);
        else
            fw.Integer(~0);
    }

    L_Free(map);

    // 4) Write author
    fw.CString(m_szAuthor);

    // 5) Write airjobs
    for(airjob_it aj = m_AirJobs.begin(); aj != m_AirJobs.end(); aj++) {
        fw.CString(aj->type->name);
        fw.Float(aj->x);
        fw.Float(aj->y);
        fw.Byte(aj->level);

        fw.Byte(aj->nodes.size());
        for(i = 0; i < aj->nodes.size(); i++) {
            fw.Float(aj->nodes[i].dx);
            fw.Float(aj->nodes[i].dy);
        }
    }
    fw.CString(""); // end of list

    // Write the map out to disk
    snprintf(buf, sizeof(buf), "levels/%04i.%s.lvl", m_iBaseLevel+1, m_szName);
    fw.Write(buf);

    return playable;
}

/*
==============
ELevel::LoadTiles

Load tiletypes and tile data
==============
*/
bool ELevel::LoadTiles(LFileRead *fr)
{
    bool playable;
    unsigned *map, id;
    unsigned numtypes;
    tile_t *tile;
    int tiles, i;

    playable = true;

    numtypes = fr->Integer();

    try {
        // Read tile types
        // A file -> internal gfx mapping is needed (like in Save, but the other way round)
        map = (unsigned *)L_Malloc(sizeof(unsigned)*numtypes, TAG_MAP);

        for(id = 0; id < numtypes; id++)
            map[id] = m_pTileTypes->Load(fr->CString());

        // Read map
        m_iMapLength = fr->Integer();
        tiles = m_iMapLength * MAPWIDTH;
        m_pTiles = (tile_t *)L_Malloc(sizeof(tile_t)*tiles, TAG_MAP);
        memset(m_pTiles, -1, sizeof(tile_t)*tiles);

        for(i = 0, tile = m_pTiles; i < tiles; i++, tile++) {
            id = fr->Integer();
            if (id >= numtypes) {
                tile->gfx = ~0;
                playable = false;
            } else
                tile->gfx = map[id];
        }
    } catch(...) {
        if (map)
            L_Free(map);
        throw;
    }

    if (map)
        L_Free(map);
    return playable;
}

/*
==============
ELevel::LoadAirjobs

Load airjobs from the given file
==============
*/
void ELevel::LoadAirjobs(LFileRead *fr, unsigned features)
{
    const char *tname;
    int count;
    int i;
    float x, y;
    airunit_type_t *type;
    airjob_t *aj;

    for(;;) {
        tname = fr->CString();
        if (!*tname)
            break;

        type = Air_GetType(tname);
        x = fr->Float();
        y = fr->Float();

        aj = AddAirJob(type, x, y);

        if (features & LVL_FEAT_LEVELS)
            aj->level = fr->Byte();
        else
            aj->level = 0;
        if (aj->level > 2)
            aj->level = 2;

        Air_FreeType(type);

        count = fr->Byte();
        for(i = 0; i < count; i++) {
            jobnode_t jn;
            jn.dx = fr->Float();
            jn.dy = fr->Float();
            if (fabs(jn.dx) > 0.1 || fabs(jn.dy) > 0.1)
                aj->nodes.push_back(jn);
        }
    }
}

/*
==============
ELevel::Load

Loads the level by filename. Returns true if the level is playable.
Throws an exception if the level file is corrupt.
==============
*/
bool ELevel::Load(const char *fname)
{
    LFileRead fr;
    bool playable;
    unsigned features;

    Clear(); // clear the current level, if any

    try
    {
        fr.Open(fname);

        // Process header
        if (fr.Integer() != LVL_MAGIC)
            throw LError("bad magic number");
        if (fr.Integer() != LVL_VERSION)
            throw LError("unsupported version");

        m_iBaseLevel = fr.Integer();
        xstrcpy(m_szName, sizeof(m_szName), fr.CString());
        playable = fr.Byte() ? true : false;
        features = fr.Integer();
        if (features & ~(LVL_FEAT_AIRJOBS|LVL_FEAT_LEVELS|LVL_FEAT_AUTHOR))
            throw LError("unsupported features: %08X", features);

        // Read tile types and tile data
        if (!LoadTiles(&fr))
            playable = false;

        // Read author
        if (features & LVL_FEAT_AUTHOR)
            xstrcpy(m_szAuthor, sizeof(m_szAuthor), fr.CString());

        // Read airjobs
        if (features & LVL_FEAT_AIRJOBS)
            LoadAirjobs(&fr, features);
    }
    catch(...) {
        Clear();
        throw;
    }

    return playable;
}

/*
==============
ELevel::ScreenToLevel
ELevel::LevelToScreen

Convert screen-based coordinates to level-based coordinates,
where 0 is at the bottom of the level, and vice versa
==============
*/
void ELevel::ScreenToLevel(float sx, float sy, float *plx, float *ply)
{
    *plx = sx;
    *ply = -sy+480 + m_flDistance*32;
}

void ELevel::LevelToScreen(float lx, float ly, float *psx, float *psy)
{
    *psx = lx;
    *psy = -ly+480 + m_flDistance*32;
}

//============================================================================
// AIRJOB EDITING

/*
==============
ELevel::AddAirJob

Add an airjob at the given location and return it.
x/y is in level coordinates.
Returns 0 if the airjob wasn't created (e.g. due to invalid coords)
Type ownership isn't transferred.
==============
*/
airjob_t *ELevel::AddAirJob(airunit_type_t *type, float x, float y)
{
    if (x < 0 || x > 640 || y < 0 || y > m_iMapLength*32)
        return 0;

    airjob_t aj(type);
    aj.x = x;
    aj.y = y;
    aj.level = 0;

    if (type->bBoss)
        m_iNumBosses++;

    m_AirJobs.push_back(aj);

    return &m_AirJobs.back();
}

/*
===============
ELevel::AddAirjobs

Add the airjobs from the given list to the airjobs in this level
===============
*/
void ELevel::AddAirjobs(const std::vector<airjob_t> &jobs, float x, float y)
{
    for(airjob_cit job = jobs.begin(); job != jobs.end(); job++) {
        airjob_t aj = *job;
        aj.x += x;
        aj.y += y;

        if (aj.x < 0 || aj.x > 640 || aj.y < 0 || aj.y > m_iMapLength*32)
            continue;

        if (aj.type->bBoss)
            m_iNumBosses++;

        m_AirJobs.push_back(aj);
    }
}

/*
===============
ELevel::DelAirJob

Deletes an airjob from a level
===============
*/
void ELevel::DelAirJob(airjob_t *job)
{
    lassert(job);

    if (job->type->bBoss)
        m_iNumBosses--;

    m_AirJobs.erase(airjob_it(job));
}

/*
===============
ELevel::DelAirjobs

Delete a list of airjobs safely from a level.
===============
*/
void ELevel::DelAirjobs(const std::vector<airjob_t*> &jobs)
{
    // This is a stupid hack:
    // Mark all airjobs in the list as "to delete" by setting their y-coordinate
    // to -1 (which is invalid)
    for(airjobp_cit p = jobs.begin(); p != jobs.end(); p++)
        (*p)->y = -1;

    int i = 0;
    while(i < m_AirJobs.size()) {
        if (m_AirJobs[i].y < 0)
            m_AirJobs.erase(m_AirJobs.begin() + i);
        else
            i++;
    }
}

/*
===============
ELevel::SetAirjobPos

Move an airjob to a new position. Point is in level coordinates
===============
*/
void ELevel::SetAirjobPos(airjob_t *aj, float x, float y)
{
    if (x < 0 || x > 640 || y < 0 || y > m_iMapLength*32)
        return;

    aj->x = x;
    aj->y = y;
}

/*
===============
ELevel::AirJobAtPoint

Walk the linked list in search of an airjob (bbox match)
Point is in level coordinates.
===============
*/
airjob_t *ELevel::AirJobAtPoint(float x, float y)
{
    for(airjob_it aj = m_AirJobs.begin(); aj != m_AirJobs.end(); aj++) {
        sprite_t *spr = aj->type->sprite;

        if (x <= aj->x-spr->mins[0] && x >= aj->x-spr->maxs[0] &&
            y <= aj->y-spr->mins[1] && y >= aj->y-spr->maxs[1])
            return &*aj;
    }

    return 0;
}

/*
===============
ELevel::FindAirJobs

Look for airjobs in the given rectangle in level coordinates.
If list is not null, pointers to all airjobs are stored in that vector.

Returns true if airjobs were found.
===============
*/
bool ELevel::FindAirJobs(float x1, float y1, float x2, float y2, std::vector<airjob_t*> *list)
{
    bool found = false;

    if (x1 > x2)
        std::swap(x1, x2);
    if (y1 > y2)
        std::swap(y1, y2);

    for(airjob_it aj = m_AirJobs.begin(); aj != m_AirJobs.end(); aj++)
    {
        sprite_t *spr = aj->type->sprite;

        if (aj->x+spr->mins[0] > x2 || aj->x+spr->maxs[0] < x1 ||
            aj->y-spr->maxs[1] > y2 || aj->y-spr->mins[1] < y1) // y-axis is reversed!
            continue;

        if (list) {
            list->push_back(&*aj);
            found = true;
        } else
            return true;
    }

    return found;
}

/*
==============
ELevel::NodeAtPoint

Try to find a node at the given location (level coordinates).
This also returns the root node, which is not represented with
a structure. pidx is set to -1 for the root node
==============
*/
airjob_t *ELevel::NodeAtPoint(float x, float y, int *pidx)
{
    jobnode_t *jn;
    float tx, ty;
    int i;

    for(airjob_it aj = m_AirJobs.begin(); aj != m_AirJobs.end(); aj++) {
        tx = aj->x;
        ty = aj->y;

        i = -1;
        for(;;) {
            if (x >= tx-4 && x <= tx+4 && y >= ty-4 && y <= ty+4) {
                *pidx = i;
                return &*aj;
            }

            i++;
            if (i >= aj->nodes.size())
                break;

            jn = &aj->nodes[i];
            tx += jn->dx;
            ty -= jn->dy; // node dy is in screen coordinates!
        }
    }

    return 0;
}


/*
==============
ELevel::NodeAtPoint

Try to find a node at the given location (level coordinates).
This also returns the root node, which is not represented with
a structure. pidx is set to -1 for the root node

Only looks at units in the list.
==============
*/
airjob_t *ELevel::NodeAtPoint(const std::vector<airjob_t*> &list, float x, float y, int *pidx)
{
    jobnode_t *jn;
    float tx, ty;
    int i;

    for(airjobp_cit aj = list.begin(); aj != list.end(); aj++) {
        tx = (*aj)->x;
        ty = (*aj)->y;

        i = -1;
        for(;;) {
            if (x >= tx-4 && x <= tx+4 && y >= ty-4 && y <= ty+4) {
                *pidx = i;
                return *aj;
            }

            i++;
            if (i >= (*aj)->nodes.size())
                break;

            jn = &(*aj)->nodes[i];
            tx += jn->dx;
            ty -= jn->dy; // node dy is in screen coordinates!
        }
    }

    return 0;
}