/*
===========================================================================
Copyright (C) 2015 the OpenMoHAA team

This file is part of OpenMoHAA source code.

OpenMoHAA source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

OpenMoHAA source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// level.cpp : Global Level Info.

#include "g_local.h"
#include "g_spawn.h"
#include "g_bot.h"
#include "level.h"
#include "parm.h"
#include "navigate.h"
#include "gravpath.h"
#include "entity.h"
#include "dm_manager.h"
#include "player.h"
#include "Entities.h"
#include "health.h"

#include "scriptmaster.h"
#include "scriptthread.h"
#include "scriptvariable.h"
#include "scriptexception.h"

#include <cfloat>

Level level;

gclient_t *spawn_client = NULL;

Event EV_Level_GetTime
(
    "time",
    EV_DEFAULT,
    NULL,
    NULL,
    "current level time",
    EV_GETTER
);

Event EV_Level_GetTotalSecrets
(
    "total_secrets",
    EV_DEFAULT,
    NULL,
    NULL,
    "count of total secrets",
    EV_GETTER
);

Event EV_Level_GetFoundSecrets
(
    "found_secrets",
    EV_DEFAULT,
    NULL,
    NULL,
    "count of found secrets",
    EV_GETTER
);

Event EV_Level_PreSpawnSentient
(
    "prespawnsentient",
    EV_CODEONLY,
    NULL,
    NULL,
    "internal usage.",
    EV_NORMAL
);

Event EV_Level_GetAlarm
(
    "alarm",
    EV_DEFAULT,
    NULL,
    NULL,
    "zero = global level alarm off,"
    "non-zero = alarm on",
    EV_GETTER
);

Event EV_Level_SetAlarm
(
    "alarm",
    EV_DEFAULT,
    "i",
    "alarm_status",
    "zero = global level alarm off,"
    "non-zero = alarm on",
    EV_SETTER
);

Event EV_Level_SetNoDropHealth
(
    "nodrophealth",
    EV_DEFAULT,
    "i",
    "alarm_status",
    "zero = automatically drop health according to cvars, non-zero = don't autodrop health (like hard mode)",
    EV_SETTER
);

Event EV_Level_SetNoDropWeapons
(
    "nodropweapons",
    EV_DEFAULT,
    "i",
    "alarm_status",
    "zero = automatically drop weapons according to cvars, non-zero = don't autodrop weapons (like hard mode)",
    EV_SETTER
);

Event EV_Level_GetRoundStarted
(
    "roundstarted",
    EV_DEFAULT,
    NULL,
    NULL,
    "non-zero if round has started",
    EV_GETTER
);

Event EV_Level_GetLoopProtection
(
    "loop_protection",
    EV_DEFAULT,
    NULL,
    NULL,
    "says if infinite loop protection is enabled",
    EV_GETTER
);

Event EV_Level_SetLoopProtection
(
    "loop_protection",
    EV_DEFAULT,
    "i",
    "loop_protection",
    "specify if infinite loop protection is enabled",
    EV_SETTER
);

Event EV_Level_GetPapersLevel
(
    "papers",
    EV_DEFAULT,
    NULL,
    NULL,
    "the level of papers the player currently has",
    EV_GETTER
);

Event EV_Level_SetPapersLevel
(
    "papers",
    EV_DEFAULT,
    NULL,
    NULL,
    "the level of papers the player currently has",
    EV_SETTER
);

Event EV_Level_GetDMRespawning
(
    "dmrespawning",
    EV_DEFAULT,
    NULL,
    NULL,
    "returns 1 if wave-based DM, 0 if respawns are disabled within a round",
    EV_GETTER
);

Event EV_Level_SetDMRespawning
(
    "dmrespawning",
    EV_DEFAULT,
    "i",
    "allow_respawn",
    "set to 1 to turn on wave-based DM, to 0 to disable respawns within a round",
    EV_SETTER
);

Event EV_Level_SetDMRespawning2(
    "dmrespawning",
    EV_DEFAULT,
    "i",
    "allow_respawn",
    "set to 1 to turn on wave-based DM,"
    "to 0 to disable respawns within a round"
);

Event EV_Level_GetDMRoundLimit(
    "dmroundlimit",
    EV_DEFAULT,
    NULL,
    NULL,
    "gets the actual roundlimit, in minutes; may be 'roundlimit' cvar or the default round limit",
    EV_GETTER
);

Event EV_Level_SetDMRoundLimit(
    "dmroundlimit",
    EV_DEFAULT,
    "i",
    "roundlimit",
    "sets the default roundlimit, in minutes; can be overridden by 'roundlimit' cvar",
    EV_SETTER
);

Event EV_Level_SetDMRoundLimit2(
    "dmroundlimit",
    EV_DEFAULT,
    "i",
    "roundlimit",
    "sets the default roundlimit,"
    "in minutes; can be overridden by 'roundlimit' cvar"
);

Event EV_Level_GetClockSide
(
    "clockside",
    EV_DEFAULT,
    NULL,
    NULL,
    "Gets which side the clock is on... 'axis' or 'allies' win when time is up",
    EV_GETTER
);

Event EV_Level_SetClockSide
(
    "clockside",
    EV_DEFAULT,
    "s",
    "axis_or_allies",
    "Sets which side the clock is on... 'axis' or 'allies' win when time is up",
    EV_SETTER
);

Event EV_Level_SetClockSide2(
    "clockside",
    EV_DEFAULT,
    "s",
    "axis_allies_draw_kills",
    "Sets which side the clock is on... 'axis' or 'allies' win when time is up, 'kills' gives the win to the team with "
    "more live members,"
    "'draw' no one wins"
);

Event EV_Level_GetBombPlantTeam
(
    "planting_team",
    EV_DEFAULT,
    NULL,
    NULL,
    "Gets which is planting the bomb,"
    "'axis' or 'allies'",
    EV_GETTER
);

Event EV_Level_SetBombPlantTeam
(
    "planting_team",
    EV_DEFAULT,
    "s",
    "axis_or_allies",
    "Sets which is planting the bomb,"
    "'axis' or 'allies'",
    EV_SETTER
);

Event EV_Level_SetBombPlantTeam2(
    "planting_team",
    EV_DEFAULT,
    "s",
    "axis_allies_draw_kills",
    "which is planting the bomb,"
    "'axis' or 'allies'"
);

Event EV_Level_GetTargetsToDestroy
(
    "targets_to_destroy",
    EV_DEFAULT,
    NULL,
    NULL,
    "Gets the number of bomb targets that must be destroyed",
    EV_GETTER
);

Event EV_Level_SetTargetsToDestroy
(
    "targets_to_destroy",
    EV_DEFAULT,
    "i",
    "num",
    "Sets the number of bomb targets that must be destroyed",
    EV_SETTER
);

Event EV_Level_SetTargetsToDestroy2
(
    "targets_to_destroy",
    EV_DEFAULT,
    "i",
    "num",
    "the number of bomb targets that must be destroyed",
    EV_NORMAL
);

Event EV_Level_GetTargetsDestroyed
(
    "targets_destroyed",
    EV_DEFAULT,
    NULL,
    NULL,
    "Gets the number of bomb targets that have been destroyed",
    EV_GETTER
);

Event EV_Level_SetTargetsDestroyed
(
    "targets_destroyed",
    EV_DEFAULT,
    "i",
    "num",
    "Sets the number of bomb targets that have been destroyed",
    EV_SETTER
);

Event EV_Level_SetTargetsDestroyed2
(
    "targets_destroyed",
    EV_DEFAULT,
    "i",
    "num",
    "the number of bomb targets that have been destroyed",
    EV_NORMAL
);

Event EV_Level_GetBombsPlanted
(
    "bombs_planted",
    EV_DEFAULT,
    NULL,
    NULL,
    "Gets the number of bombs that are set",
    EV_GETTER
);

Event EV_Level_SetBombsPlanted
(
    "bombs_planted",
    EV_DEFAULT,
    "i",
    "num",
    "Sets the number of bombs that are set",
    EV_SETTER
);

Event EV_Level_SetBombsPlanted2
(
    "bombs_planted",
    EV_DEFAULT,
    "i",
    "num",
    "the number of bombs that are set",
    EV_NORMAL
);

Event EV_Level_GetRoundBased
(
    "roundbased",
    EV_DEFAULT,
    NULL,
    NULL,
    "Gets whether or not the game is currently round based or not",
    EV_GETTER
);

Event EV_Level_GetObjectiveBased
(
    "objectivebased",
    EV_DEFAULT,
    NULL,
    NULL,
    "Gets whether or not the game is currently objective based or not",
    EV_GETTER
);

Event EV_Level_Rain_Density_Set
(
    "rain_density",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain density",
    EV_SETTER
);

Event EV_Level_Rain_Density_Get
(
    "rain_density",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain density",
    EV_GETTER
);

Event EV_Level_Rain_Speed_Set
(
    "rain_speed",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain speed",
    3
);

Event EV_Level_Rain_Speed_Get
(
    "rain_speed",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain speed",
    2
);

Event EV_Level_Rain_Speed_Vary_Set
(
    "rain_speed_vary",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain speed variance",
    EV_SETTER
);

Event EV_Level_Rain_Speed_Vary_Get
(
    "rain_speed_vary",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain speed variance",
    EV_GETTER
);

Event EV_Level_Rain_Slant_Set
(
    "rain_slant",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain slant",
    EV_SETTER
);

Event EV_Level_Rain_Slant_Get
(
    "rain_slant",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain slant",
    EV_GETTER
);

Event EV_Level_Rain_Length_Set
(
    "rain_length",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain length",
    EV_SETTER
);

Event EV_Level_Rain_Length_Get
(
    "rain_length",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain length",
    EV_GETTER
);

Event EV_Level_Rain_Min_Dist_Set
(
    "rain_min_dist",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain min_dist",
    EV_SETTER
);

Event EV_Level_Rain_Min_Dist_Get
(
    "rain_min_dist",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain min_dist",
    EV_GETTER
);

Event EV_Level_Rain_Width_Set
(
    "rain_width",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain width",
    EV_SETTER
);

Event EV_Level_Rain_Width_Get
(
    "rain_width",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain width",
    EV_GETTER
);

Event EV_Level_Rain_Shader_Set
(
    "rain_shader",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain ",
    EV_SETTER
);

Event EV_Level_Rain_Shader_Get
(
    "rain_shader",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain ",
    EV_GETTER
);

Event EV_Level_Rain_NumShaders_Set
(
    "rain_numshaders",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain numshaders",
    EV_SETTER
);

Event EV_Level_Rain_NumShaders_Get
(
    "rain_numshaders",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets the rain numshaders",
    EV_GETTER
);

Event EV_Level_AddBadPlace
(
    "badplace",
    EV_DEFAULT,
    "svfSF",
    "name origin radius [team] [duration]",
    "Enables a 'bad place' for AI of team 'american', 'german', or (default) 'both' to avoid, and optionally gives it "
    "a duration",
    EV_NORMAL
);

Event EV_Level_RemoveBadPlace
(
    "removebadplace",
    EV_DEFAULT,
    "s",
    "name",
    "Removes a 'bad place'",
    EV_NORMAL
);

Event EV_Level_IgnoreClock
(
    "ignoreclock",
    EV_DEFAULT,
    "i",
    "ignoreclock",
    "Tells a level weather or not to ignore the clock",
    EV_NORMAL
);

//
// Added in OPM
//
Event EV_Level_SetForceTeamObjectiveLocation
(
    "force_team_objective",
    EV_DEFAULT,
    "i",
    "force",
    "Whether or not to force using a separate objective location for each team.\n"
    "1 (forced on TOW/Liberation match): different location for each team\n"
    "0 (default): teams use same location on non-TOW/non-Liberation match\n",
    EV_SETTER
);
Event EV_Level_GetForceTeamObjectiveLocation
(
    "force_team_objective",
    EV_DEFAULT,
    NULL,
    NULL,
    "Return 1 if each team has a separate objective location",
    EV_GETTER
);

extern Event EV_Entity_Start;

CLASS_DECLARATION(Listener, Level, NULL) {
    {&EV_Level_GetTime,                       &Level::GetTime                      },
    {&EV_Level_GetTotalSecrets,               &Level::GetTotalSecrets              },
    {&EV_Level_GetFoundSecrets,               &Level::GetFoundSecrets              },
    {&EV_Level_PreSpawnSentient,              &Level::PreSpawnSentient             },
    {&EV_Level_GetAlarm,                      &Level::GetAlarm                     },
    {&EV_Level_SetAlarm,                      &Level::SetAlarm                     },
    {&EV_Level_SetNoDropHealth,               &Level::SetNoDropHealth              },
    {&EV_Level_SetNoDropWeapons,              &Level::SetNoDropWeapons             },
    {&EV_Level_GetRoundStarted,               &Level::EventGetRoundStarted         },
    {&EV_Level_GetLoopProtection,             &Level::GetLoopProtection            },
    {&EV_Level_SetLoopProtection,             &Level::SetLoopProtection            },
    {&EV_Level_GetPapersLevel,                &Level::GetPapersLevel               },
    {&EV_Level_SetPapersLevel,                &Level::SetPapersLevel               },
    {&EV_Level_GetDMRespawning,               &Level::EventGetDMRespawning         },
    {&EV_Level_SetDMRespawning,               &Level::EventSetDMRespawning         },
    {&EV_Level_SetDMRespawning2,              &Level::EventSetDMRespawning         },
    {&EV_Level_GetDMRoundLimit,               &Level::EventGetDMRoundLimit         },
    {&EV_Level_SetDMRoundLimit,               &Level::EventSetDMRoundLimit         },
    {&EV_Level_SetDMRoundLimit2,              &Level::EventSetDMRoundLimit         },
    {&EV_Level_GetClockSide,                  &Level::EventGetClockSide            },
    {&EV_Level_SetClockSide,                  &Level::EventSetClockSide            },
    {&EV_Level_SetClockSide2,                 &Level::EventSetClockSide            },
    {&EV_Level_GetBombPlantTeam,              &Level::EventGetBombPlantTeam        },
    {&EV_Level_SetBombPlantTeam,              &Level::EventSetBombPlantTeam        },
    {&EV_Level_SetBombPlantTeam2,             &Level::EventSetBombPlantTeam        },
    {&EV_Level_GetTargetsToDestroy,           &Level::EventGetTargetsToDestroy     },
    {&EV_Level_SetTargetsToDestroy,           &Level::EventSetTargetsToDestroy     },
    {&EV_Level_SetTargetsToDestroy2,          &Level::EventSetTargetsToDestroy     },
    {&EV_Level_GetTargetsDestroyed,           &Level::EventGetTargetsDestroyed     },
    {&EV_Level_SetTargetsDestroyed,           &Level::EventSetTargetsDestroyed     },
    {&EV_Level_SetTargetsDestroyed2,          &Level::EventSetTargetsDestroyed     },
    {&EV_Level_GetBombsPlanted,               &Level::EventGetBombsPlanted         },
    {&EV_Level_SetBombsPlanted,               &Level::EventSetBombsPlanted         },
    {&EV_Level_SetBombsPlanted2,              &Level::EventSetBombsPlanted         },
    {&EV_Level_GetRoundBased,                 &Level::EventGetRoundBased           },
    {&EV_Level_GetObjectiveBased,             &Level::EventGetObjectiveBased       },
    {&EV_Level_Rain_Density_Set,              &Level::EventRainDensitySet          },
    {&EV_Level_Rain_Density_Get,              &Level::EventRainDensityGet          },
    {&EV_Level_Rain_Speed_Set,                &Level::EventRainSpeedSet            },
    {&EV_Level_Rain_Speed_Get,                &Level::EventRainSpeedGet            },
    {&EV_Level_Rain_Speed_Vary_Set,           &Level::EventRainSpeedVarySet        },
    {&EV_Level_Rain_Speed_Vary_Get,           &Level::EventRainSpeedVaryGet        },
    {&EV_Level_Rain_Slant_Set,                &Level::EventRainSlantSet            },
    {&EV_Level_Rain_Slant_Get,                &Level::EventRainSlantGet            },
    {&EV_Level_Rain_Length_Set,               &Level::EventRainLengthSet           },
    {&EV_Level_Rain_Length_Get,               &Level::EventRainLengthGet           },
    {&EV_Level_Rain_Min_Dist_Set,             &Level::EventRainMin_DistSet         },
    {&EV_Level_Rain_Min_Dist_Get,             &Level::EventRainMin_DistGet         },
    {&EV_Level_Rain_Width_Set,                &Level::EventRainWidthSet            },
    {&EV_Level_Rain_Width_Get,                &Level::EventRainWidthGet            },
    {&EV_Level_Rain_Shader_Set,               &Level::EventRainShaderSet           },
    {&EV_Level_Rain_Shader_Get,               &Level::EventRainShaderGet           },
    {&EV_Level_Rain_NumShaders_Set,           &Level::EventRainNumShadersSet       },
    {&EV_Level_Rain_NumShaders_Get,           &Level::EventRainNumShadersGet       },
    {&EV_Level_AddBadPlace,                   &Level::EventAddBadPlace             },
    {&EV_Level_RemoveBadPlace,                &Level::EventRemoveBadPlace          },
    {&EV_Level_IgnoreClock,                   &Level::EventIgnoreClock             },
    {&EV_Level_SetForceTeamObjectiveLocation, &Level::SetForceTeamObjectiveLocation},
    {&EV_Level_GetForceTeamObjectiveLocation, &Level::GetForceTeamObjectiveLocation},
    {NULL,                                    NULL                                 }
};

void Level::GetTime(Event *ev)
{
    ev->AddFloat(level.time);
}

void Level::GetTotalSecrets(Event *ev)
{
    ev->AddInteger(total_secrets);
}

void Level::GetFoundSecrets(Event *ev)
{
    ev->AddInteger(found_secrets);
}

Level::Level()
{
    Init();
}

Level::~Level() {}

void Level::Init(void)
{
    m_HeadSentient[1] = NULL;
    m_HeadSentient[0] = NULL;

    spawn_entnum = -1;

    current_map = NULL;

    framenum  = 0;
    time      = 0;
    frametime = 0;

    level_name = "";
    mapname    = "";
    spawnpoint = "";
    nextmap    = "";

    total_secrets = 0;
    found_secrets = 0;

    m_iCuriousVoiceTime    = 0;
    m_iAttackEntryAnimTime = 0;

    playerfrozen = false;

    intermissiontime = 0.0f;
    exitintermission = 0;

    memset(&impact_trace, 0, sizeof(trace_t));

    cinematic = false;
    ai_on     = true;

    near_exit      = false;
    mission_failed = false;

    m_bAlarm       = false;
    m_iPapersLevel = 0;

    died_already = false;

    water_color = vec_zero;
    lava_color  = vec_zero;

    lava_alpha  = 0.0f;
    water_alpha = 0.0f;

    saved_soundtrack   = "";
    current_soundtrack = "";

    automatic_cameras.ClearObjectList();

    m_fade_time_start = 0.0f;
    m_fade_time       = -1.0f;
    m_fade_color      = vec_zero;
    m_fade_style      = additive;

    m_fade_alpha = 0;

    m_letterbox_fraction   = 0;
    m_letterbox_time       = -1;
    m_letterbox_time_start = 0;
    m_letterbox_dir        = letterbox_out;

    m_numArenas = 1;

    m_voteTime     = 0;
    m_nextVoteTime = 0;
    m_voteYes      = 0;
    m_voteNo       = 0;
    m_numVoters    = 0;

    m_LoopProtection = true;
    m_LoopDrop       = true;

    m_letterbox_time = -1.0f;

    m_vObjectiveLocation          = vec_zero;
    m_vAlliedObjectiveLocation    = vec_zero;
    m_vAxisObjectiveLocation      = vec_zero;
    m_bForceTeamObjectiveLocation = false;

    svsEndTime = 0;

    earthquake_magnitude = 0;

    mHealthPopCount = 0;
    mbNoDropHealth  = false;
    mbNoDropWeapons = false;

    spawning          = false;
    m_bIgnoreClock    = false;
    svsStartFloatTime = 0;

    m_fLandmarkXDistMin = 0;
    m_fLandmarkXDistMax = 0;
    m_fLandmarkYDistMin = 0;
    m_fLandmarkYDistMax = 0;
    m_pLandmarks        = NULL;
    m_iMaxLandmarks     = 0;
    m_iLandmarksCount   = 0;

    m_badPlaces.ClearObjectList();

    m_pAIStats = NULL;

    m_bScriptSpawn = false;
    m_bRejectSpawn = false;
}

void Level::CleanUp(qboolean samemap, qboolean resetConfigStrings)
{
    int i;

    DisableListenerNotify++;

    if (g_gametype->integer != GT_SINGLE_PLAYER) {
        dmManager.Reset();
    }

    Director.Reset(samemap);

    ClearCachedStatemaps();

    // clear active current bots
    G_ResetBots();

    assert(active_edicts.next);
    assert(active_edicts.next->prev == &active_edicts);
    assert(active_edicts.prev);
    assert(active_edicts.prev->next == &active_edicts);
    assert(free_edicts.next);
    assert(free_edicts.next->prev == &free_edicts);
    assert(free_edicts.prev);
    assert(free_edicts.prev->next == &free_edicts);

    while (active_edicts.next != &active_edicts) {
        assert(active_edicts.next != &free_edicts);
        assert(active_edicts.prev != &free_edicts);

        assert(active_edicts.next);
        assert(active_edicts.next->prev == &active_edicts);
        assert(active_edicts.prev);
        assert(active_edicts.prev->next == &active_edicts);
        assert(free_edicts.next);
        assert(free_edicts.next->prev == &free_edicts);
        assert(free_edicts.prev);
        assert(free_edicts.prev->next == &free_edicts);

        if (active_edicts.next->entity) {
            delete active_edicts.next->entity;
        } else {
            FreeEdict(active_edicts.next);
        }
    }

    //
    // Remove all archived entities
    //
    for (i = m_SimpleArchivedEntities.NumObjects(); i > 0; i--) {
        delete m_SimpleArchivedEntities.ObjectAt(i);
    }

    cinematic = false;
    ai_on     = true;
    near_exit = false;

    mission_failed = false;
    died_already   = false;

    globals.num_entities = game.maxclients + 1;
    gi.LocateGameData(g_entities, game.maxclients + 1, sizeof(gentity_t), &game.clients[0].ps, sizeof(gclient_t));

    // clear up all AI node information
    PathManager.ResetNodes();

    // clear out automatic cameras
    automatic_cameras.ClearObjectList();

    // clear out level script variables
    level.Vars()->ClearList();

    // Clear out parm vars
    parm.Vars()->ClearList();

    // initialize the game variables
    // these get restored by the persistant data, so we can safely clear them here
    game.Vars()->ClearList();

    // clearout any waiting events
    L_ClearEventList();

    // reset all edicts
    ResetEdicts();

    // reset all grenade hints
    GrenadeHint::ResetHints();

    // reset projectile targets
    ClearProjectileTargets();

    // Reset the boss health cvar
    gi.cvar_set("bosshealth", "0");

    Actor::ResetBodyQueue();
    Health::ResetHealthQueue();

    if (world) {
        world->FreeTargetList();
    }

    num_earthquakes = 0;

    AddWaitTill(STRING_PRESPAWN);
    AddWaitTill(STRING_SPAWN);
    AddWaitTill(STRING_PLAYERSPAWN);
    AddWaitTill(STRING_SKIP);
    AddWaitTill(STRING_POSTTHINK);

    if (g_gametype->integer >= GT_TEAM_ROUNDS && g_gametype->integer <= GT_LIBERATION) {
        AddWaitTill(STRING_ROUNDSTART);
    }

    if (g_gametype->integer > GT_FFA) {
        AddWaitTill(STRING_ALLIESWIN);
        AddWaitTill(STRING_AXISWIN);
        AddWaitTill(STRING_DRAW);
    }

    if (resetConfigStrings) {
        gi.setConfigstring(CS_RAIN_DENSITY, "0");
        gi.setConfigstring(CS_RAIN_SPEED, "2048");
        gi.setConfigstring(CS_RAIN_SPEEDVARY, "512");
        gi.setConfigstring(CS_RAIN_SLANT, "50");
        gi.setConfigstring(CS_RAIN_LENGTH, "90");
        gi.setConfigstring(CS_RAIN_MINDIST, "512");
        gi.setConfigstring(CS_RAIN_WIDTH, "1");
        gi.setConfigstring(CS_RAIN_SHADER, "textures/rain");
        gi.setConfigstring(CS_RAIN_NUMSHADERS, "0");
        gi.setConfigstring(CS_CURRENT_OBJECTIVE, "");

        for (i = 0; i < MAX_OBJECTIVES; i++) {
            gi.setConfigstring(CS_OBJECTIVES + i, "");
        }

        gi.setConfigstring(CS_VOTE_TIME, "");
        gi.setConfigstring(CS_VOTE_STRING, "");
        gi.setConfigstring(CS_VOTE_YES, "");
        gi.setConfigstring(CS_VOTE_NO, "");
        gi.setConfigstring(CS_VOTE_UNDECIDED, "");
    }

    DisableListenerNotify--;

    svsStartFloatTime = svsFloatTime;

    FreeLandmarks();
}

/*
==============
ResetEdicts
==============
*/
void Level::ResetEdicts(void)
{
    int i;

    memset(g_entities, 0, game.maxentities * sizeof(g_entities[0]));

    // Add all the edicts to the free list
    LL_Reset(&free_edicts, next, prev);
    LL_Reset(&active_edicts, next, prev);
    for (i = 0; i < game.maxentities; i++) {
        LL_Add(&free_edicts, &g_entities[i], next, prev);
    }

    for (i = 0; i < game.maxclients; i++) {
        // set client fields on player ents
        g_entities[i].client = game.clients + i;

        G_InitClientPersistant(&game.clients[i]);
    }

    globals.num_entities = game.maxclients;
}

qboolean Level::inhibitEntity(int spawnflags)
{
    if (!detail->integer && (spawnflags & SPAWNFLAG_DETAIL)) {
        return qtrue;
    }

    if (g_gametype->integer != GT_SINGLE_PLAYER) {
        return (spawnflags & SPAWNFLAG_NOT_DEATHMATCH) ? qtrue : qfalse;
    }

    if (!developer->integer && (spawnflags & SPAWNFLAG_DEVELOPMENT)) {
        return qtrue;
    }

    if (!Q_stricmp(mapname, "t3l2")) {
        // Added in 2.0.
        //   FIXME: there should be a better way to handle
        //   specific maps
        return (spawnflags & SPAWNFLAG_NOT_EASY) ? qtrue : qfalse;
    }

    switch (skill->integer) {
    case 0:
        return (spawnflags & SPAWNFLAG_NOT_EASY) != 0;

    case 1:
        return (spawnflags & SPAWNFLAG_NOT_MEDIUM) != 0;

    case 2:
    case 3:
        return (spawnflags & SPAWNFLAG_NOT_HARD);
    }

    /*
#ifdef _CONSOLE
    if (spawnflags & SPAWNFLAG_NOCONSOLE)
#else
    if (spawnflags & SPAWNFLAG_NOPC)
#endif
    {
        return qtrue;
    }
    */

    return qfalse;
}

void Level::setSkill(int value)
{
    int skill_level;

    skill_level = floor((float)value);
    skill_level = bound(skill_level, 0, 3);

    gi.cvar_set("skill", va("%d", skill_level));
}

void Level::setTime(int levelTime)
{
    svsTime      = levelTime;
    inttime      = levelTime - svsStartTime;
    svsFloatTime = levelTime / 1000.0f;
    time         = inttime / 1000.0f;
}

void Level::setFrametime(int frametime)
{
    intframetime    = frametime;
    this->frametime = frametime / 1000.0f;
}

void Level::SpawnEntities(char *entities, int svsTime)
{
    int         inhibit, radnum = 0, count = 0;
    const char *value;
    SpawnArgs   args;
    Listener   *listener;
    Entity     *ent;
    int         t1, t2;
    int         start, end;
    char        name[128];

    if (gi.Cvar_Get("g_invulnoverride", "0", 0)->integer == 1) {
        // Added in 2.30
        //  Clear the invulnerable override when loading
        gi.cvar_set("g_invulnoverride", "0");
    }

    Com_Printf("-------------------- Spawning Entities -----------------------\n");

    t1 = gi.Milliseconds();
    memset(skel_index, 0xff, sizeof(skel_index));

    // set up time so functions still have valid times
    setTime(svsTime);
    setFrametime(50);

    gi.LoadResource("*144");

    setSkill(skill->integer);

    // reset out count of the number of game traces
    sv_numtraces = 0;

    // parse world
    entities     = args.Parse(entities);
    spawn_entnum = ENTITYNUM_WORLD;
    args.SpawnInternal();

    gi.LoadResource("*147");

    // Set up for a new map
    PathManager.LoadNodes();

    gi.LoadResource("*147a");

    Com_Printf("-------------------- Actual Spawning Entities -----------------------\n");

    start = gi.Milliseconds();

    // parse ents
    inhibit = 0;

    for (entities = args.Parse(entities); entities != NULL; entities = args.Parse(entities)) {
        // remove things (except the world) from different skill levels or deathmatch
        spawnflags = 0;
        value      = args.getArg("spawnflags");

        if (value) {
            spawnflags = atoi(value);

            value = args.getArg("classname");
            if (!value || (Q_stricmp(value, "info_pathnode") && Q_stricmp(value, "info_patharea"))) {
                if (inhibitEntity(spawnflags)) {
                    inhibit++;
                    continue;
                }
            }
        }

        listener = args.SpawnInternal();

        if (listener) {
            radnum++;

            if (listener->isSubclassOf(Entity)) {
                count++;

                ent         = (Entity *)listener;
                ent->radnum = radnum;

                Q_strncpyz(ent->edict->entname, ent->getClassID(), sizeof(ent->edict->entname));

                ent->PostEvent(EV_Entity_Start, -1.0, 0);
                Com_sprintf(name, sizeof(name), "i%d", radnum);
                gi.LoadResource(name);
            }
        }
    }

    end = gi.Milliseconds();

    Com_Printf("-------------------- Actual Spawning Entities Done ------------------ %i ms\n", end - start);

    gi.LoadResource("*147b");

    world->UpdateConfigStrings();

    Event *ev = new Event(EV_Level_PreSpawnSentient);
    PostEvent(ev, EV_SPAWNENTITIES);

    L_ProcessPendingEvents();

    gi.LoadResource("*148");

    if (g_gametype->integer != GT_SINGLE_PLAYER) {
        dmManager.InitGame();
    }

    gi.LoadResource("*148a");

    // Added in OPM
    //  Check for single-player before pre-creating a player instance
    if (game.maxclients == 1 && g_gametype->integer == GT_SINGLE_PLAYER) {
        spawn_entnum = 0;
        new Player;
    }

    gi.LoadResource("*148b");

    m_LoopProtection = false;
    RemoveWaitTill(STRING_PRESPAWN);
    Unregister(STRING_PRESPAWN);
    m_LoopProtection = true;

    gi.LoadResource("*150");

    t2 = gi.Milliseconds();

    Com_Printf("%i entities spawned\n", count);
    Com_Printf("%i simple entities spawned\n", radnum);
    Com_Printf("%i entities inhibited\n", inhibit);

    Com_Printf("-------------------- Spawning Entities Done ------------------ %i ms\n", t2 - t1);

    //
    // create landmarks
    //
    ComputeDMWaypoints();
}

void Level::ComputeDMWaypoints()
{
    qboolean shouldSetDefaultLandmark;
    float    startXDistMin, startXDistMax;
    float    startYDistMin, startYDistMax;
    int      i;

    if (g_gametype->integer == GT_SINGLE_PLAYER) {
        m_fLandmarkXDistMax = 1;
        m_fLandmarkYDistMax = 1;
        m_fLandmarkXDistMin = 0;
        m_fLandmarkYDistMin = 0.0;
        return;
    }

    shouldSetDefaultLandmark = qfalse;

    //
    // calculate the world bounds from entities
    //
    if (m_fLandmarkXDistMin == m_fLandmarkXDistMax && m_fLandmarkYDistMin == m_fLandmarkYDistMax
        && m_fLandmarkXDistMax == m_fLandmarkYDistMax) {
        shouldSetDefaultLandmark = qtrue;

        for (i = 0; i < game.maxentities; i++) {
            gentity_t *ent = &g_entities[i];

            if (ent->entity) {
                AddLandmarkOrigin(ent->entity->origin);
                shouldSetDefaultLandmark = qfalse;
            }
        }
    }

    if (shouldSetDefaultLandmark) {
        startXDistMax = 1;
        startYDistMax = 1;
        startXDistMin = 0;
        startYDistMin = 0;
    } else {
        startYDistMax = m_fLandmarkYDistMax;
        startYDistMin = m_fLandmarkYDistMin;
        startXDistMax = m_fLandmarkXDistMax;
        startXDistMin = m_fLandmarkXDistMin;
    }

    m_fLandmarkXDistMin = startXDistMin + (startXDistMax - startXDistMin) / 3.f;
    m_fLandmarkXDistMax = startXDistMin + (startXDistMax - startXDistMin) * 2.f / 3.f;
    m_fLandmarkYDistMin = startYDistMin + (startYDistMax - startYDistMin) / 3.f;
    m_fLandmarkYDistMax = startYDistMin + (startYDistMax - startYDistMin) * 2.f / 3.f;
}

void Level::AddLandmarkOrigin(const Vector& origin)
{
    float  yaw;
    vec3_t angles;
    vec3_t dir;
    float  dist;

    yaw = origin.toYaw();

    angles[0] = angles[2] = 0;
    angles[1]             = yaw + 90 - world->m_fNorth;
    AngleVectors(angles, dir, NULL, NULL);

    dist = origin.lengthXY();
    VectorScale(dir, dist, dir);

    if (m_fLandmarkYDistMax == m_fLandmarkYDistMin && m_fLandmarkXDistMin == m_fLandmarkXDistMax
        && m_fLandmarkYDistMax == m_fLandmarkXDistMin) {
        m_fLandmarkYDistMin = dir[1];
        m_fLandmarkXDistMax = dir[0];
        m_fLandmarkYDistMax = dir[1] + 1.0;
        m_fLandmarkXDistMin = dir[0] - 1.0;
    } else {
        if (m_fLandmarkYDistMin > dir[1]) {
            m_fLandmarkYDistMin = dir[1];
        }
        if (m_fLandmarkYDistMax < dir[1]) {
            m_fLandmarkYDistMax = dir[1];
        }
        if (m_fLandmarkXDistMax < dir[0]) {
            m_fLandmarkXDistMax = dir[0];
        }
        if (m_fLandmarkXDistMin > dir[0]) {
            m_fLandmarkXDistMin = dir[0];
        }
    }
}

void Level::AddLandmarkName(const str& name, const Vector& origin)
{
    landmark_t *landmark;
    int         i;

    if (m_pLandmarks) {
        if (m_iLandmarksCount == m_iMaxLandmarks) {
            // reallocate the landmark list with twice the size
            landmark_t **oldLandmarks = m_pLandmarks;

            m_iMaxLandmarks *= 2;
            m_pLandmarks = new landmark_t *[m_iMaxLandmarks];

            for (i = 0; i < m_iLandmarksCount; i++) {
                m_pLandmarks[i] = oldLandmarks[i];
            }

            delete[] oldLandmarks;
        }
    } else {
        // create the landmark list for the first time
        m_iMaxLandmarks   = 8;
        m_iLandmarksCount = 0;
        m_pLandmarks      = new landmark_t *[8];
    }

    //
    // create a new landmark
    //
    landmark = m_pLandmarks[m_iLandmarksCount] = new landmark_t();
    landmark->m_sName                          = name;
    landmark->m_vOrigin                        = origin;
}

void Level::FreeLandmarks()
{
    landmark_t *landmark;

    if (m_pLandmarks) {
        int i;

        for (i = 0; i < m_iLandmarksCount; i++) {
            landmark = m_pLandmarks[i];

            if (landmark) {
                delete landmark;
            }
        }

        delete[] m_pLandmarks;
        m_pLandmarks      = NULL;
        m_iLandmarksCount = 0;
        m_iMaxLandmarks   = 0;
    }
}

str Level::GetDynamicDMLocations(const Vector& origin)
{
    str   name = "nothing";
    int   i;
    float shortestDistSqr = 0;

    for (i = 0; i < m_iLandmarksCount; i++) {
        landmark_t *landmark = m_pLandmarks[i];
        Vector      delta    = origin - landmark->m_vOrigin;
        float       distSqr  = delta.lengthSquared();

        if (i == 0 || distSqr < shortestDistSqr) {
            shortestDistSqr = distSqr;
            name            = landmark->m_sName;
        }
    }

    return name;
}

str Level::GetDMLocation(const Vector& origin)
{
    float  yaw;
    vec3_t angles;
    vec3_t dir;
    float  dist;

    if (m_pLandmarks) {
        //
        // use the dynamic dm locations
        //
        return GetDynamicDMLocations(origin);
    }

    yaw = origin.toYaw();

    angles[0] = angles[2] = 0;
    angles[1]             = yaw + 90 - world->m_fNorth;
    AngleVectors(angles, dir, NULL, NULL);

    dist = origin.lengthXY();
    VectorScale(dir, dist, dir);

    if (dir[0] >= m_fLandmarkXDistMin) {
        if (dir[0] > m_fLandmarkXDistMax) {
            if (dir[1] >= m_fLandmarkYDistMin) {
                if (dir[1] > m_fLandmarkYDistMax) {
                    return "North East corner";
                } else {
                    return "East side";
                }
            } else {
                return "South East corner";
            }
        } else if (dir[1] >= m_fLandmarkYDistMin) {
            if (dir[1] > m_fLandmarkYDistMax) {
                return "North side";
            } else {
                return "center";
            }
        } else {
            return "South side";
        }
    } else if (dir[1] >= m_fLandmarkYDistMin) {
        if (dir[1] > m_fLandmarkYDistMax) {
            return "North West corner";
        } else {
            return "West side";
        }
    } else {
        return "South West corner";
    }
}

void Level::PreSpawnSentient(Event *ev)
{
    GameScript *script;

    // general initialization
    FindTeams();

    script = Director.GetScript(m_mapscript);
    if (script) {
        gi.DPrintf("Adding script: '%s'\n", m_mapscript.c_str());

        m_LoopProtection = false;
        Director.ExecuteThread(m_mapscript);
        m_LoopProtection = true;
    }

    PathManager.CreatePaths();
}

bool Level::RoundStarted()
{
    return !WaitTillDefined(STRING_ROUNDSTART);
}

bool Level::PreSpawned(void)
{
    return !WaitTillDefined(STRING_PRESPAWN);
}

bool Level::Spawned(void)
{
    return !WaitTillDefined(STRING_SPAWN);
}

void Level::ServerSpawned(void)
{
    int        i;
    gclient_t *client;
    gentity_t *ent;

    for (i = 0, client = game.clients; i < game.maxclients; i++, client++) {
        client->ps.commandTime = svsTime;
    }

    if (!Spawned()) {
        RemoveWaitTill(STRING_SPAWN);

        Director.Pause();

        for (ent = active_edicts.next; ent != &active_edicts; ent = ent->next) {
            ent->entity->Unregister(STRING_SPAWN);
        }

        Director.Unpause();

        Unregister(STRING_SPAWN);
    } else {
        Director.LoadMenus();
    }

    spawning = false;
}

void Level::SetMap(const char *themapname)
{
    char *spawnpos;
    int   i;
    str   text;

    Init();

    spawning = true;

    // set a specific spawnpoint if the map was started with a $
    spawnpos = strchr((char *)themapname, '$');
    if (spawnpos) {
        mapname    = (const char *)(spawnpos - themapname);
        spawnpoint = mapname;
    } else {
        mapname    = themapname;
        spawnpoint = "";
    }

    current_map = (char *)themapname;

    level_name = mapname;
    for (i = 0; i < level_name.length(); i++) {
        if (level_name[i] == '.') {
            level_name[i] = 0;
            break;
        }
    }

    m_mapscript      = "maps/" + level_name + ".scr";
    m_precachescript = "maps/" + level_name + "_precache.scr";
    m_pathfile       = "maps/" + level_name + ".pth";
    m_mapfile        = "maps/" + level_name + ".bsp";
}

void Level::LoadAllScripts(const char *name, const char *extension)
{
    char **scriptFiles;
    char   filename[MAX_QPATH];
    int    numScripts;

    scriptFiles = gi.FS_ListFiles(name, extension, qfalse, &numScripts);

    if (!scriptFiles || !numScripts) {
        return;
    }

    for (int i = 0; i < numScripts; i++) {
        Com_sprintf(filename, sizeof(filename), "%s/%s", name, scriptFiles[i]);

        // Compile the script
        Director.GetScript(filename);
    }

    gi.FS_FreeFileList(scriptFiles);
}

void Level::Precache(void)
{
    setTime(svsStartTime);
    setFrametime(50);

    if (gi.FS_ReadFile(m_precachescript, NULL, qtrue) != -1) {
        gi.DPrintf("Adding script: '%s'\n", m_precachescript.c_str());

        // temporarily disable the loop protection
        // because caching models require time
        m_LoopProtection = false;
        Director.ExecuteThread(m_precachescript);
        m_LoopProtection = true;
    }

    if (g_gametype->integer == GT_SINGLE_PLAYER) {
        LoadAllScripts("anim", ".scr");
    }

    LoadAllScripts("global", ".scr");
    InitVoteOptions();
}

/*
================
FindTeams

Chain together all entities with a matching team field.
Entity teams are used for item groups and multi-entity mover groups.

All but the first will have the FL_TEAMSLAVE flag set and teammaster field set
All but the last will have the teamchain field set to the next one
================
*/
void Level::FindTeams()
{
    gentity_t *ent, *ent2;
    Entity    *e, *e2;
    int        i, j;
    int        c, c2;

    c  = 0;
    c2 = 0;
    for (i = 1, ent = g_entities + i; i < globals.num_entities; i++, ent++) {
        if (!ent->inuse) {
            continue;
        }

        e = ent->entity;

        if (!e->moveteam.length()) {
            continue;
        }

        if (e->flags & FL_TEAMSLAVE) {
            continue;
        }

        e->teammaster = e;
        c++;
        c2++;

        for (j = i + 1, ent2 = ent + 1; j < globals.num_entities; j++, ent2++) {
            if (!ent2->inuse) {
                continue;
            }

            e2 = ent->entity;

            if (!e2->moveteam.length()) {
                continue;
            }

            if (e2->flags & FL_TEAMSLAVE) {
                continue;
            }

            if (!strcmp(e->moveteam, e2->moveteam)) {
                c2++;
                e2->teamchain  = e->teamchain;
                e->teamchain   = e2;
                e2->teammaster = e;
                e2->flags |= FL_TEAMSLAVE;

                // make sure that targets only point at the master
                if (e2->targetname) {
                    e->targetname  = e2->targetname;
                    e2->targetname = NULL;
                }
            }
        }
    }

    G_Printf("%i teams with %i entities\n", c, c2);
}

gentity_t *Level::AllocEdict(Entity *entity)
{
    int        i;
    gentity_t *edict;

    if (spawn_entnum >= 0) {
        edict        = &g_entities[spawn_entnum];
        spawn_entnum = -1;

        assert(!edict->inuse && !edict->entity);

        // free up the entity pointer in case we took one that still exists
        if (edict->inuse && edict->entity) {
            delete edict->entity;
        }
    } else {
        edict = &g_entities[game.maxclients];

        for (i = game.maxclients; i < globals.num_entities; i++, edict++) {
            // the first couple seconds of server time can involve a lot of
            // freeing and allocating, so relax the replacement policy
            if (!edict->inuse && ((edict->freetime < 2.0f) || (time - edict->freetime > 0.5f))) {
                break;
            }
        }

        // allow two spots for none and world
        if (i == globals.max_entities - 2.0f) {
            // Try one more time before failing, relax timing completely

            edict = &g_entities[game.maxclients];

            for (i = game.maxclients; i < globals.num_entities; i++, edict++) {
                if (!edict->inuse) {
                    break;
                }
            }

            if (i == globals.max_entities - 2.0f) {
                gi.Error(ERR_DROP, "Level::AllocEdict: no free edicts");
            }
        }
    }

    LL_Remove(edict, next, prev);

    InitEdict(edict);

    LL_Add(&active_edicts, edict, next, prev);

    // Tell the server about our data since we just spawned something
    if ((edict->s.number < ENTITYNUM_WORLD) && (globals.num_entities <= edict->s.number)) {
        globals.num_entities = edict->s.number + 1;
        gi.LocateGameData(
            g_entities, globals.num_entities, sizeof(gentity_t), &game.clients[0].ps, sizeof(game.clients[0])
        );
    }

    edict->entity = entity;

    return edict;
}

void Level::FreeEdict(gentity_t *ed)
{
    gclient_t *client;

    // unlink from world
    gi.unlinkentity(ed);

    LL_Remove(ed, next, prev);

    client = ed->client;

    memset(ed, 0, sizeof(*ed));

    ed->client   = client;
    ed->freetime = time;
    ed->inuse    = false;
    ed->s.number = ed - g_entities;

    LL_Add(&free_edicts, ed, next, prev);
}

void Level::InitEdict(gentity_t *e)
{
    int i;

    e->inuse = true;

    e->s.renderfx |= RF_FRAMELERP;
    e->s.number = e - g_entities;

    // make sure a default scale gets set
    e->s.scale = 1.0f;
    // make sure the default constantlight gets set, initalize to r 1.0, g 1.0, b 1.0, r 0
    e->s.constantLight = 0xffffff;

    e->s.wasframe = 0;
    e->spawntime  = level.time;

    for (i = 0; i < NUM_BONE_CONTROLLERS; i++) {
        e->s.bone_tag[i] = -1;
        VectorClear(e->s.bone_angles[i]);
        EulerToQuat(e->s.bone_angles[i], e->s.bone_quat[i]);
    }
}

void Level::AddAutomaticCamera(Camera *cam)
{
    automatic_cameras.AddUniqueObject(cam);
}

void Level::InitVoteOptions()
{
    if (g_gametype->integer == GT_SINGLE_PLAYER) {
        return;
    }

    m_voteOptions.SetupVoteOptions("callvote.cfg");
    // clear the vote time
    gi.setConfigstring(CS_VOTE_TIME, "");
}

void Level::SendVoteOptionsFile(gentity_t *ent)
{
    const char *voteBuffer;
    int         voteLength;
    int         clientNum;
    int         i, j;
    char        buffer[2068];

    clientNum = ent - g_entities;

    if (clientNum < 0 || clientNum >= game.maxclients) {
        return;
    }

    voteBuffer = m_voteOptions.GetVoteOptionsFile(&voteLength);
    if (voteLength < MAX_VOTEOPTIONS_UPLOAD_BUFFER_LENGTH) {
        Q_strncpyz(buffer, voteBuffer, voteLength + 1);

        for (i = 0; i < voteLength; i++) {
            if (buffer[i] == '"') {
                buffer[i] = 1;
            }
        }

        gi.SendServerCommand(clientNum, "vo0 \"\"\n");
        gi.SendServerCommand(clientNum, "vo2 \"%s\"\n", buffer);
    } else {
        const char *cmd;
        int         destLength;
        int         offset;

        offset = 0;
        for (i = voteLength; i > 0; i -= MAX_VOTEOPTIONS_UPLOAD_BUFFER_LENGTH - 1) {
            if (offset == 0) {
                cmd        = "vo0";
                destLength = MAX_VOTEOPTIONS_UPLOAD_BUFFER_LENGTH;
            } else if (i >= MAX_VOTEOPTIONS_UPLOAD_BUFFER_LENGTH) {
                cmd        = "vo1";
                destLength = MAX_VOTEOPTIONS_UPLOAD_BUFFER_LENGTH;
            } else {
                cmd        = "vo2";
                destLength = i;
            }

            Q_strncpyz(buffer, &voteBuffer[offset], MAX_VOTEOPTIONS_UPLOAD_BUFFER_LENGTH);

            for (j = 0; j < destLength; j++) {
                if (buffer[j] == '"') {
                    buffer[j] = 1;
                }
            }

            gi.SendServerCommand(clientNum, "%s \"%s\"\n", cmd, buffer);
            offset += MAX_VOTEOPTIONS_UPLOAD_BUFFER_LENGTH - 1;
        }
    }
}

bool Level::GetVoteOptionMain(int index, str *outOptionCommand, voteoptiontype_t *outOptionType)
{
    return m_voteOptions.GetVoteOptionsMain(index, outOptionCommand, outOptionType);
}

bool Level::GetVoteOptionSub(int index, int listIndex, str *outCommand)
{
    return m_voteOptions.GetVoteOptionSub(index, listIndex, outCommand);
}

bool Level::GetVoteOptionMainName(int index, str *outVoteName)
{
    return m_voteOptions.GetVoteOptionMainName(index, outVoteName);
}

bool Level::GetVoteOptionSubName(int index, int listIndex, str *outName)
{
    return m_voteOptions.GetVoteOptionSubName(index, listIndex, outName);
}

void Level::CheckVote(void)
{
    gentity_t *ent;
    int        i;
    int        numVoters;
    int        oldVoteYes, oldVoteNo, oldNumVoters;

    if (m_nextVoteTime && m_nextVoteTime < inttime) {
        m_nextVoteTime = 0;

        gi.SendConsoleCommand(va("%s\n", m_voteString.c_str()));

        if (sv_sprinton->integer == 1) {
            if (sv_runspeed->integer == 250) {
                gi.cvar_set("sv_runspeed", "287");
            }
        } else if (sv_runspeed->integer == 287) {
            gi.cvar_set("sv_runspeed", "250");
        }

        SetupMaplist();
    }

    if (!m_voteTime) {
        return;
    }

    oldVoteYes   = level.m_voteYes;
    oldVoteNo    = level.m_voteNo;
    oldNumVoters = level.m_numVoters;

    level.m_voteYes = 0;
    level.m_voteNo  = 0;
    numVoters       = 0;

    for (i = 0; i < game.maxclients; i++) {
        Player *p;

        ent = &g_entities[i];
        if (!ent->inuse || !ent->client || !ent->entity) {
            continue;
        }

        p = static_cast<Player *>(ent->entity);
        if (p->client->ps.voted) {
            if (p->HasVotedYes()) {
                level.m_voteYes++;
            } else {
                level.m_voteNo++;
            }
        }
    }

    level.m_numVoters = numVoters;

    if ((svsFloatTime - svsStartFloatTime) * 1000 - m_voteTime >= 30000) {
        G_PrintToAllClients(va("%s: %s\n", gi.CL_LV_ConvertString("Vote Failed"), m_voteName.c_str()));
        m_voteTime = 0;
        gi.setConfigstring(CS_VOTE_TIME, "");
    } else if (m_voteYes > m_numVoters / 2) {
        // Pass arguments to console
        G_PrintToAllClients(va("%s: %s\n", gi.CL_LV_ConvertString("Vote Passed"), m_voteName.c_str()));
        m_nextVoteTime = level.inttime + 3000;
        m_voteTime     = 0;
        gi.setConfigstring(CS_VOTE_TIME, "");
    } else if (m_voteNo >= m_numVoters / 2) {
        G_PrintToAllClients(va("%s: %s\n", gi.CL_LV_ConvertString("Vote Failed"), m_voteName.c_str()));
        m_voteTime = 0;
        gi.setConfigstring(CS_VOTE_TIME, "");
    } else {
        if (oldVoteYes != level.m_voteYes) {
            gi.setConfigstring(CS_VOTE_YES, va("%i", level.m_voteYes));
        }

        if (oldVoteNo != level.m_voteNo) {
            gi.setConfigstring(CS_VOTE_NO, va("%i", level.m_voteNo));
        }

        if (oldNumVoters != level.m_numVoters || oldVoteYes != level.m_voteYes || oldVoteNo != level.m_voteNo) {
            gi.setConfigstring(CS_VOTE_UNDECIDED, va("%i", level.m_numVoters - (level.m_voteYes + level.m_voteNo)));
        }
    }
}

void Level::SetupMaplist()
{
    const char *p;
    const char  delim[2] = {';', 0};
    cvar_t     *maplistVar;
    int         gameTypeNum;
    char        buffer[1024];
    char        gameTypeBuffer[12];

    if (!strstr(m_voteString.c_str(), "g_gametype")) {
        return;
    }

    Q_strncpyz(buffer, m_voteString.c_str(), sizeof(buffer));

    for (p = strtok(buffer, delim); p; p = strtok(NULL, delim)) {
        if (strstr(p, "g_gametype")) {
            gameTypeBuffer[0] = p[strlen(p) - 1];
            gameTypeBuffer[1] = 0;
            gameTypeNum       = atoi(gameTypeBuffer);
        }
    }

    switch (gameTypeNum) {
    case GT_FFA:
        gi.ExecuteConsoleCommand(EXEC_NOW, "exec maplist_ffa.cfg");
        maplistVar = gi.Cvar_Get("ui_maplist_ffa", "", 0);
        break;
    case GT_TEAM:
        gi.ExecuteConsoleCommand(EXEC_NOW, "exec maplist_team.cfg");
        maplistVar = gi.Cvar_Get("ui_maplist_team", "", 0);
        break;
    case GT_TEAM_ROUNDS:
        gi.ExecuteConsoleCommand(EXEC_NOW, "exec maplist_round.cfg");
        maplistVar = gi.Cvar_Get("ui_maplist_round", "", 0);
        break;
    case GT_OBJECTIVE:
        gi.ExecuteConsoleCommand(EXEC_NOW, "exec maplist_obj.cfg");
        maplistVar = gi.Cvar_Get("ui_maplist_obj", "", 0);
        break;
    case GT_TOW:
        gi.ExecuteConsoleCommand(EXEC_NOW, "exec maplist_tow.cfg");
        maplistVar = gi.Cvar_Get("ui_maplist_tow", "", 0);
        break;
    case GT_LIBERATION:
        gi.ExecuteConsoleCommand(EXEC_NOW, "exec maplist_lib.cfg");
        maplistVar = gi.Cvar_Get("ui_maplist_lib", "", 0);
        break;
    }

    gi.cvar_set("sv_maplist", maplistVar->string);
}

void Level::GetAlarm(Event *ev)
{
    ev->AddInteger(m_bAlarm);
}

void Level::SetAlarm(Event *ev)
{
    m_bAlarm = ev->GetInteger(1);
}

void Level::SetNoDropHealth(Event *ev)
{
    mbNoDropHealth = ev->GetInteger(1);
}

void Level::SetNoDropWeapons(Event *ev)
{
    mbNoDropWeapons = ev->GetInteger(1);
}

void Level::GetLoopProtection(Event *ev)
{
    ev->AddInteger(m_LoopProtection);
}

void Level::SetLoopProtection(Event *ev)
{
    m_LoopProtection = ev->GetInteger(1);
}

void Level::GetPapersLevel(Event *ev)
{
    ev->AddInteger(m_iPapersLevel);
}

void Level::SetPapersLevel(Event *ev)
{
    m_iPapersLevel = ev->GetInteger(1);
}

void Level::EventGetRoundStarted(Event *ev)
{
    ev->AddInteger(RoundStarted());
}

void Level::EventGetDMRespawning(Event *ev)
{
    ev->AddInteger(dmManager.GameAllowsRespawns());
}

void Level::EventSetDMRespawning(Event *ev)
{
    dmManager.SetGameAllowsRespawns(ev->GetBoolean(1));
}

void Level::EventGetDMRoundLimit(Event *ev)
{
    ev->AddInteger(dmManager.GetRoundLimit());
}

void Level::EventSetDMRoundLimit(Event *ev)
{
    int round_limit = ev->GetInteger(1);

    if (round_limit < 0) {
        ScriptError("round limit must be greater than 0");
    }

    dmManager.SetDefaultRoundLimit(round_limit);
}

void Level::EventGetClockSide(Event *ev)
{
    ev->AddConstString(dmManager.GetClockSide());
}

void Level::EventSetClockSide(Event *ev)
{
    const_str clockside = ev->GetConstString(1);

    if (clockside < STRING_ALLIES || clockside > STRING_KILLS) {
        ScriptError("clockside must be 'axis', 'allies', 'kills', or 'draw'");
    }

    dmManager.SetClockSide(clockside);
}

void Level::EventGetBombPlantTeam(Event *ev)
{
    ev->AddConstString(dmManager.GetBombPlantTeam());
}

void Level::EventSetBombPlantTeam(Event *ev)
{
    const_str plant_team = ev->GetConstString(1);

    if (plant_team < STRING_ALLIES || plant_team > STRING_AXIS) {
        ScriptError("bombplantteam must be 'axis' or 'allies'");
    }

    dmManager.SetBombPlantTeam(plant_team);
}

void Level::EventGetTargetsToDestroy(Event *ev)
{
    ev->AddInteger(dmManager.GetTargetsToDestroy());
}

void Level::EventSetTargetsToDestroy(Event *ev)
{
    dmManager.SetTargetsToDestroy(ev->GetInteger(1));
}

void Level::EventGetTargetsDestroyed(Event *ev)
{
    ev->AddInteger(dmManager.GetTargetsDestroyed());
}

void Level::EventSetTargetsDestroyed(Event *ev)
{
    dmManager.SetTargetsDestroyed(ev->GetInteger(1));
}

void Level::EventGetBombsPlanted(Event *ev)
{
    ev->AddInteger(dmManager.GetBombsPlanted());
}

void Level::EventSetBombsPlanted(Event *ev)
{
    dmManager.SetBombsPlanted(ev->GetInteger(1));
}

void Level::EventGetRoundBased(Event *ev)
{
    ev->AddInteger(g_gametype->integer >= GT_TEAM_ROUNDS);
}

void Level::EventGetObjectiveBased(Event *ev)
{
    ev->AddInteger(g_gametype->integer >= GT_OBJECTIVE);
}

str Level::GetRandomHeadModel(const char *model)
{
    char s[MAX_STRING_TOKENS];

    gi.GetHeadModel(model, G_Random(gi.NumHeadModels(model)), s);
    return s;
}

str Level::GetRandomHeadSkin(const char *model)
{
    char s[MAX_STRING_TOKENS];

    gi.GetHeadSkin(model, G_Random(gi.NumHeadSkins(model)), s);
    return s;
}

void Level::AddEarthquake(earthquake_t *e)
{
    if (num_earthquakes == MAX_EARTHQUAKES) {
        if (earthquakes[0].m_Thread) {
            earthquakes[0].m_Thread->Wait(e->endtime - inttime);
        }

        num_earthquakes--;

        for (int i = 0; i < num_earthquakes; i++) {
            earthquakes[i] = earthquakes[i + 1];
        }
    }

    earthquakes[num_earthquakes] = *e;
    num_earthquakes++;

    e->m_Thread->Pause();
}

void Level::DoEarthquakes(void)
{
    int           i, j;
    earthquake_t *e;
    int           timedelta;
    int           rampuptime;
    int           rampdowntime;
    float         test_magnitude;

    if (num_earthquakes <= 0) {
        return;
    }

    earthquake_magnitude = 0.0f;

    for (i = num_earthquakes; i > 0; i--) {
        e = &earthquakes[i - 1];

        if (inttime >= e->endtime || !e->m_Thread) {
            if (e->m_Thread) {
                e->m_Thread->Wait(0);
            }

            num_earthquakes--;

            for (j = 0; j < num_earthquakes; j++) {
                earthquakes[j] = earthquakes[j + 1];
            }
        } else {
            test_magnitude = e->magnitude;

            timedelta = inttime - e->starttime;

            if (timedelta >= e->duration / 2) {
                rampdowntime = 2 * e->duration / 3 + e->starttime;

                if (!e->no_rampdown && inttime > rampdowntime) {
                    test_magnitude *= 1.0f - (inttime - rampdowntime) * 3.0f / e->duration;
                }
            } else {
                rampuptime = e->duration / 3 + e->starttime;

                if (!e->no_rampup && inttime < rampuptime) {
                    test_magnitude *= (inttime - e->starttime) * 3.0f / e->duration;
                }
            }

            if (test_magnitude > earthquake_magnitude) {
                earthquake_magnitude = test_magnitude;
            }
        }
    }
}

void Level::EventRainDensitySet(Event *ev)
{
    gi.setConfigstring(CS_RAIN_DENSITY, ev->GetString(1));
}

void Level::EventRainDensityGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_DENSITY));
}

void Level::EventRainSpeedSet(Event *ev)
{
    gi.setConfigstring(CS_RAIN_SPEED, ev->GetString(1));
}

void Level::EventRainSpeedGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_SPEED));
}

void Level::EventRainSpeedVarySet(Event *ev)
{
    gi.setConfigstring(CS_RAIN_SPEEDVARY, ev->GetString(1));
}

void Level::EventRainSpeedVaryGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_SPEEDVARY));
}

void Level::EventRainSlantSet(Event *ev)
{
    gi.setConfigstring(CS_RAIN_SLANT, ev->GetString(1));
}

void Level::EventRainSlantGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_SLANT));
}

void Level::EventRainLengthSet(Event *ev)
{
    gi.setConfigstring(CS_RAIN_LENGTH, ev->GetString(1));
}

void Level::EventRainLengthGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_LENGTH));
}

void Level::EventRainMin_DistSet(Event *ev)
{
    gi.setConfigstring(CS_RAIN_MINDIST, ev->GetString(1));
}

void Level::EventRainMin_DistGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_MINDIST));
}

void Level::EventRainWidthSet(Event *ev)
{
    gi.setConfigstring(CS_RAIN_WIDTH, ev->GetString(1));
}

void Level::EventRainWidthGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_WIDTH));
}

void Level::EventRainShaderSet(Event *ev)
{
    gi.setConfigstring(CS_RAIN_SHADER, ev->GetString(1));
}

void Level::EventRainShaderGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_SHADER));
}

void Level::EventRainNumShadersSet(Event *ev)
{
    if (g_protocol <= protocol_e::PROTOCOL_MOH) {
        // There is a mistake in 1.11 and below where the NumShader event doesn't work
        // Because the response points to EventRainShaderSet
        return;
    }

    gi.setConfigstring(CS_RAIN_NUMSHADERS, ev->GetString(1));
}

void Level::EventRainNumShadersGet(Event *ev)
{
    ev->AddString(gi.getConfigstring(CS_RAIN_NUMSHADERS));
}

void Level::EventAddBadPlace(Event *ev)
{
    badplace_t bp;
    int        nArgs;

    nArgs = ev->NumArgs();
    if (nArgs != 3 && nArgs != 4 && nArgs != 5) {
        throw ScriptException("badplace requires 3, 4, or 5 arguments");
    }

    if (ev->NumArgs() >= 5) {
        bp.m_fLifespan = ev->GetFloat(5);
        if (bp.m_fLifespan <= 0) {
            throw ScriptException("life span must be greater than 0");
        }
    }

    if (ev->NumArgs() >= 4) {
        switch (ev->GetConstString(4)) {
        case STRING_ALLIES:
        case STRING_AMERICAN:
            bp.m_iTeamSide = TEAM_ALLIES;
            break;
        case STRING_AXIS:
        case STRING_GERMAN:
            bp.m_iTeamSide = TEAM_AXIS;
            break;
        default:
            throw ScriptException("badplace must belong to 'american' or 'german' or 'both'");
        }
    }

    bp.m_fRadius = ev->GetFloat(3);
    bp.m_vOrigin = ev->GetVector(2);
    bp.m_name    = ev->GetConstString(1);

    if (bp.m_name == STRING_EMPTY && bp.m_fLifespan == FLT_MAX) {
        throw ScriptException("unnamed badplaces must have a specified duration");
    }

    if (bp.m_name != STRING_EMPTY) {
        int i;

        // replace badplaces with the same name
        for (i = 1; i <= m_badPlaces.NumObjects(); i++) {
            const badplace_t& existing = m_badPlaces.ObjectAt(i);

            if (existing.m_name == bp.m_name) {
                // remove the existing bad place at the pathway
                PathSearch::UpdatePathwaysForBadPlace(existing.m_vOrigin, existing.m_fRadius, -1, existing.m_iTeamSide);
                m_badPlaces.SetObjectAt(i, bp);
                // add the new bad place
                PathSearch::UpdatePathwaysForBadPlace(bp.m_vOrigin, bp.m_fRadius, 1, bp.m_iTeamSide);
                G_BroadcastAIEvent(NULL, vec_zero, 12, -1);
                return;
            }
        }
    }

    // add the new bad place
    m_badPlaces.AddObject(bp);
    PathSearch::UpdatePathwaysForBadPlace(bp.m_vOrigin, bp.m_fRadius, 1, bp.m_iTeamSide);
    // notify AI
    G_BroadcastAIEvent(NULL, vec_zero, 12, -1);
}

void Level::EventRemoveBadPlace(Event *ev)
{
    const_str name;
    int       i;

    if (ev->NumArgs() != 1) {
        throw ScriptException("removebadplace requires exactly 1 argument");
    }

    name = ev->GetConstString(1);
    for (i = 1; i <= m_badPlaces.NumObjects(); i++) {
        badplace_t& bp = m_badPlaces.ObjectAt(i);

        if (bp.m_name == name) {
            PathSearch::UpdatePathwaysForBadPlace(bp.m_vOrigin, bp.m_fRadius, -1, bp.m_iTeamSide);
            m_badPlaces.RemoveObjectAt(i);
            G_BroadcastAIEvent(NULL, vec_zero, 12, -1);
            return;
        }
    }

    Com_Printf("removebadplace: name '%s' not found", ev->GetString(1).c_str());
}

void Level::EventIgnoreClock(Event *ev)
{
    m_bIgnoreClock = ev->GetBoolean(1);
}

void Level::UpdateBadPlaces()
{
    qboolean removed;
    int      i;

    removed = qfalse;

    for (i = 1; i <= m_badPlaces.NumObjects(); i++) {
        badplace_t& bp = m_badPlaces.ObjectAt(i);
        if (level.time < bp.m_fLifespan) {
            continue;
        } else {
            PathSearch::UpdatePathwaysForBadPlace(bp.m_vOrigin, bp.m_fRadius, -1, bp.m_iTeamSide);
            m_badPlaces.RemoveObjectAt(i);
            removed = qtrue;
            i       = 0;
        }
    }

    if (removed) {
        // tell AI that a bad place was removed
        G_BroadcastAIEvent(NULL, vec_zero, 12, -1);
    }
}

int Level::GetNearestBadPlace(const Vector& org, float radius, int team) const
{
    float bestDistSqr;
    int   bestBpIndex;
    int   i;

    bestDistSqr = FLT_MAX;
    bestBpIndex = 0;

    for (i = 1; i <= m_badPlaces.NumObjects(); i++) {
        badplace_t& bp = m_badPlaces.ObjectAt(i);

        if (team & bp.m_iTeamSide) {
            const Vector delta   = bp.m_vOrigin - org;
            float        distSqr = delta.lengthSquared();
            float        rad     = bp.m_fRadius + radius;

            if (distSqr < bestDistSqr && distSqr < rad * rad) {
                bestDistSqr = distSqr;
                bestBpIndex = i;
            }
        }
    }

    return bestBpIndex;
}

static void ArchiveBadPlace(Archiver& arc, badplace_t *bp)
{
    Director.ArchiveString(arc, bp->m_name);
    arc.ArchiveVector(&bp->m_vOrigin);
    arc.ArchiveFloat(&bp->m_fRadius);
    arc.ArchiveFloat(&bp->m_fLifespan);
}

void Level::Archive(Archiver& arc)
{
    bool prespawn;
    bool spawn;

    Listener::Archive(arc);

    if (arc.Saving()) {
        prespawn = WaitTillDefined(STRING_PRESPAWN);
        spawn    = WaitTillDefined(STRING_SPAWN);
    }

    arc.ArchiveBool(&prespawn);
    arc.ArchiveBool(&spawn);

    if (arc.Loading()) {
        if (!prespawn) {
            RemoveWaitTill(STRING_PRESPAWN);
        }

        if (!spawn) {
            RemoveWaitTill(STRING_SPAWN);
        }
    }

    arc.ArchiveInteger(&framenum);

    arc.ArchiveString(&level_name);
    arc.ArchiveString(&mapname);
    arc.ArchiveString(&spawnpoint);
    arc.ArchiveString(&nextmap);

    arc.ArchiveBoolean(&playerfrozen);

    arc.ArchiveFloat(&intermissiontime);
    ArchiveEnum(intermissiontype, INTTYPE_e);
    arc.ArchiveInteger(&exitintermission);

    arc.ArchiveInteger(&total_secrets);
    arc.ArchiveInteger(&found_secrets);

    arc.ArchiveFloat(&earthquake_magnitude);
    arc.ArchiveInteger(&num_earthquakes);

    for (int i = 0; i < num_earthquakes; i++) {
        arc.ArchiveInteger(&earthquakes[i].duration);
        arc.ArchiveFloat(&earthquakes[i].magnitude);
        arc.ArchiveBool(&earthquakes[i].no_rampup);
        arc.ArchiveBool(&earthquakes[i].no_rampdown);

        arc.ArchiveInteger(&earthquakes[i].starttime);
        arc.ArchiveInteger(&earthquakes[i].endtime);

        arc.ArchiveSafePointer(&earthquakes[i].m_Thread);
    }

    arc.ArchiveBoolean(&cinematic);
    arc.ArchiveBoolean(&ai_on);
    arc.ArchiveBoolean(&mission_failed);
    arc.ArchiveBoolean(&died_already);

    arc.ArchiveVector(&water_color);
    arc.ArchiveVector(&lava_color);

    arc.ArchiveFloat(&water_alpha);
    arc.ArchiveFloat(&lava_alpha);

    arc.ArchiveString(&current_soundtrack);
    arc.ArchiveString(&saved_soundtrack);

    arc.ArchiveVector(&m_fade_color);
    arc.ArchiveFloat(&m_fade_alpha);
    arc.ArchiveFloat(&m_fade_time);
    arc.ArchiveFloat(&m_fade_time_start);
    ArchiveEnum(m_fade_style, fadestyle_t);
    ArchiveEnum(m_fade_type, fadetype_t);

    arc.ArchiveFloat(&m_letterbox_fraction);
    arc.ArchiveFloat(&m_letterbox_time);
    arc.ArchiveFloat(&m_letterbox_time_start);

    ArchiveEnum(m_letterbox_dir, letterboxdir_t);
    m_badPlaces.Archive(arc, &ArchiveBadPlace);
    arc.ArchiveInteger(&m_iCuriousVoiceTime);
    arc.ArchiveInteger(&m_iAttackEntryAnimTime);
    arc.ArchiveInteger(&mHealthPopCount);

    arc.ArchiveBoolean(&m_bAlarm);
    arc.ArchiveBoolean(&mbNoDropHealth);
    arc.ArchiveBoolean(&mbNoDropWeapons);
    arc.ArchiveInteger(&m_iPapersLevel);
    arc.ArchiveInteger(&m_LoopProtection);

    // clear skel indexes
    memset(skel_index, 0xff, sizeof(skel_index));

    if (arc.Loading()) {
        str saved = saved_soundtrack;
        ChangeSoundtrack(current_soundtrack);
        saved_soundtrack = saved;

        memset(&impact_trace, 0, sizeof(trace_t));
    }

    for (int i = 0; i < MAX_HEAD_SENTIENTS; i++) {
        arc.ArchiveObjectPointer((Class **)&m_HeadSentient[i]);
    }

    arc.ArchiveVector(&m_vObjectiveLocation);
    arc.ArchiveVector(&m_vAlliedObjectiveLocation);
    arc.ArchiveVector(&m_vAxisObjectiveLocation);

    for (int i = 0; i < MAX_BODYQUEUE; i++) {
        arc.ArchiveSafePointer(&Actor::mBodyQueue[i]);
    }

    arc.ArchiveInteger(&Actor::mCurBody);
    Health::ArchiveStatic(arc);

    arc.ArchiveConfigString(CS_CURRENT_OBJECTIVE);

    for (int i = CS_OBJECTIVES; i < CS_OBJECTIVES + MAX_OBJECTIVES; i++) {
        arc.ArchiveConfigString(i);
    }

    arc.ArchiveConfigString(CS_RAIN_DENSITY);
    arc.ArchiveConfigString(CS_RAIN_SPEED);
    arc.ArchiveConfigString(CS_RAIN_SPEEDVARY);
    arc.ArchiveConfigString(CS_RAIN_SLANT);
    arc.ArchiveConfigString(CS_RAIN_LENGTH);
    arc.ArchiveConfigString(CS_RAIN_MINDIST);
    arc.ArchiveConfigString(CS_RAIN_WIDTH);
    arc.ArchiveConfigString(CS_RAIN_SHADER);
    arc.ArchiveConfigString(CS_RAIN_NUMSHADERS);

    arc.ArchiveFloat(&svsStartFloatTime);
    arc.ArchiveFloat(&m_fLandmarkYDistMax);
    arc.ArchiveFloat(&m_fLandmarkYDistMin);
    arc.ArchiveFloat(&m_fLandmarkXDistMin);
    arc.ArchiveFloat(&m_fLandmarkXDistMax);
}

void WriteStatS(FILE *pFile, const char *value)
{
    fprintf(pFile, "%s", value);
}

void WriteStatI(FILE *pFile, int value)
{
    fprintf(pFile, "%d", value);
}

void WriteStatF(FILE *pFile, float value)
{
    fprintf(pFile, "%.2f", value);
}

void Level::OpenActorStats()
{
    str filename;

    if (!g_aistats->integer) {
        return;
    }

    filename   = "aistats_" + mapname + ".csv";
    m_pAIStats = fopen(filename, "wt");

    if (m_pAIStats) {
        fprintf(
            (FILE *)m_pAIStats,
            "targetname,weapon,grenade ammo,health,accuracy,hearing,sight,fov,mindist,maxdist,leash,sound "
            "awareness,noticescale,enemyshare,grenade aware,model\n"
        );
    }
}

void Level::WriteActorStats(Actor *actor)
{
    const char *name;
    str         itemName;
    Weapon     *weapon;
    Ammo       *ammo;
    int         ammoAmount;
    FILE       *pFile;

    if (!g_aistats->integer) {
        return;
    }

    if (!m_pAIStats) {
        OpenActorStats();
    }

    pFile = (FILE *)m_pAIStats;
    if (!pFile) {
        return;
    }

    name     = actor->TargetName().c_str();
    itemName = "none";

    weapon = actor->GetActiveWeapon(WEAPON_MAIN);
    if (weapon) {
        itemName = weapon->GetItemName();
    }

    ammoAmount = 0;
    ammo       = actor->FindAmmoByName("grenade");
    if (ammo) {
        ammoAmount = ammo->getAmount();
    }

    WriteStatS(pFile, name);
    WriteStatS(pFile, itemName);
    WriteStatI(pFile, ammoAmount);
    WriteStatF(pFile, actor->health);
    WriteStatF(pFile, actor->mAccuracy * 100);
    WriteStatF(pFile, actor->m_fHearing);
    WriteStatF(pFile, actor->m_fSight);
    WriteStatF(pFile, actor->m_fFov);
    WriteStatF(pFile, actor->m_fMinDistance);
    WriteStatF(pFile, actor->m_fMaxDistance);
    WriteStatF(pFile, actor->m_fLeash);
    WriteStatF(pFile, actor->m_fSoundAwareness);
    WriteStatF(pFile, actor->m_fMaxNoticeTimeScale * 100);

    WriteStatI(pFile, sqrt(actor->m_fMaxShareDistSquared));
    WriteStatF(pFile, actor->m_fGrenadeAwareness);
    WriteStatS(pFile, actor->model);

    fprintf(pFile, "\n");
}

void Level::SetForceTeamObjectiveLocation(Event *ev)
{
    m_bForceTeamObjectiveLocation = ev->GetBoolean(1);
}

void Level::GetForceTeamObjectiveLocation(Event *ev)
{
    ev->AddInteger(g_gametype->integer >= GT_TOW || m_bForceTeamObjectiveLocation);
}

badplace_t::badplace_t()
    : m_fLifespan(FLT_MAX)
    , m_iTeamSide(TEAM_ALLIES)
{}
