openmohaa/code/game/level.cpp

2118 lines
38 KiB
C++
Raw Normal View History

2016-03-27 11:49:47 +02:00
/*
===========================================================================
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.
2023-04-29 21:56:38 +02:00
#include "g_local.h"
#include "g_spawn.h"
2016-03-27 11:49:47 +02:00
#include "level.h"
#include "parm.h"
2023-04-29 21:56:38 +02:00
#include "navigate.h"
#include "gravpath.h"
#include "entity.h"
#include "dm_manager.h"
#include "player.h"
2016-03-27 11:49:47 +02:00
#include "scriptmaster.h"
#include "scriptthread.h"
2016-03-27 11:49:47 +02:00
#include "scriptvariable.h"
2023-04-29 21:56:38 +02:00
#include "scriptexception.h"
2016-03-27 11:49:47 +02:00
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."
);
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_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"
);
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"
);
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"
);
Event EV_Level_GetRoundBased
(
"roundbased",
EV_DEFAULT,
NULL,
NULL,
"Gets wether or not the game is currently round 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_GetFlags
(
"flag",
EV_DEFAULT,
NULL,
NULL,
"Initialized flag list",
EV_GETTER
);
2019-07-01 20:17:46 +02:00
extern Event EV_Entity_Start;
2016-03-27 11:49:47 +02:00
Level::Level()
{
Init();
}
Level::~Level()
{
}
void Level::Archive( Archiver& arc )
{
bool prespawn;
bool spawn;
Listener::Archive( arc );
if( arc.Saving() )
{
prespawn = classinfo()->WaitTillDefined( "prespawn" );
spawn = classinfo()->WaitTillDefined( "spawn" );
}
arc.ArchiveBool( &prespawn );
arc.ArchiveBool( &spawn );
if( arc.Loading() )
{
if( prespawn ) {
AddWaitTill( STRING_PRESPAWN );
}
if( spawn ) {
AddWaitTill( 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 );
arc.ArchiveInteger( &m_iCuriousVoiceTime );
arc.ArchiveInteger( &m_iAttackEntryAnimTime );
arc.ArchiveInteger( &mHealthPopCount );
arc.ArchiveBoolean( &m_bAlarm );
arc.ArchiveBoolean( &mbNoDropHealth );
arc.ArchiveInteger( &m_iPapersLevel );
arc.ArchiveInteger( &m_LoopProtection );
memset( skel_index, 255, 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 ] );
}
// FIXME: Archive Actor::mBodyQueue
// FIXME: Archive Actor::mCurBody
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 );
}
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_letterbox_dir = letterbox_out;
m_numArenas = 1;
m_fade_alpha = 0;
m_letterbox_fraction = 0;
m_letterbox_time_start = 0;
m_voteTime = 0;
m_numVoters = 0;
m_LoopProtection = true;
m_LoopDrop = true;
m_letterbox_time = -1.0f;
m_voteYes = 0;
m_voteNo = 0;
m_vObjectiveLocation = vec_zero;
svsEndTime = 0;
earthquake_magnitude = 0;
mHealthPopCount = 0;
mbNoDropHealth = false;
spawning = false;
m_bSpawnBot = false;
m_bScriptSpawn = false;
m_bRejectSpawn = false;
}
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 );
}
bool Level::PreSpawned( void )
{
return WaitTillDefined( "prespawn" );
}
bool Level::Spawned( void )
{
return WaitTillDefined( "spawn" );
}
void Level::AddEarthquake( earthquake_t *e )
{
if( num_earthquakes == MAX_EARTHQUAKES )
{
if( earthquakes[ 0 ].m_Thread ) {
earthquakes[ 0 ].m_Thread->Wait( ( float )( e->endtime - inttime ) / 1000.0f - 0.5f );
}
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::CleanUp( qboolean samemap, qboolean resetConfigStrings )
{
DisableListenerNotify++;
if( g_gametype->integer ) {
dmManager.Reset();
}
Director.Reset( samemap );
ClearCachedStatemaps();
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 );
}
}
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 the boss health cvar
gi.Cvar_Set( "bosshealth", "0" );
2018-09-05 16:55:10 +02:00
Actor::ResetBodyQueue();
2016-03-27 11:49:47 +02:00
if( world ) {
world->FreeTargetList();
}
num_earthquakes = 0;
2018-09-17 23:50:38 +02:00
AddWaitTill(STRING_PRESPAWN);
AddWaitTill(STRING_SPAWN);
AddWaitTill(STRING_PLAYERSPAWN);
AddWaitTill(STRING_SKIP);
AddWaitTill(STRING_POSTTHINK);
if (g_gametype->integer <= GT_OBJECTIVE)
{
AddWaitTill(STRING_ROUNDSTART);
}
if (g_gametype->integer > GT_FFA)
{
AddWaitTill(STRING_ALLIESWIN);
AddWaitTill(STRING_AXISWIN);
AddWaitTill(STRING_DRAW);
}
2016-03-27 11:49:47 +02:00
classinfo()->RemoveWaitTill( "prespawn" );
classinfo()->RemoveWaitTill( "spawn" );
classinfo()->RemoveWaitTill( "skip" );
classinfo()->RemoveWaitTill( "postthink" );
if( g_gametype->integer >= GT_TEAM_ROUNDS )
{
classinfo()->RemoveWaitTill( "roundstart" );
classinfo()->RemoveWaitTill( "allieswin" );
classinfo()->RemoveWaitTill( "axiswin" );
classinfo()->RemoveWaitTill( "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( int i = CS_OBJECTIVES; i < CS_OBJECTIVES + MAX_OBJECTIVES; i++ )
{
gi.SetConfigstring( i, "" );
}
}
DisableListenerNotify--;
}
/*
==============
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( !developer->integer && ( spawnflags & SPAWNFLAG_DEVELOPMENT ) )
{
return qtrue;
}
if( !detail->integer && ( spawnflags & SPAWNFLAG_DETAIL ) )
{
return qtrue;
}
#ifdef _CONSOLE
if( spawnflags & SPAWNFLAG_NOCONSOLE )
#else
if( spawnflags & SPAWNFLAG_NOPC )
#endif
{
return qtrue;
}
if( g_gametype->integer )
{
if( spawnflags & SPAWNFLAG_NOT_DEATHMATCH )
{
return qtrue;
}
return qfalse;
}
switch( skill->integer )
{
case 0:
return ( spawnflags & SPAWNFLAG_NOT_EASY ) != 0;
break;
case 1:
return ( spawnflags & SPAWNFLAG_NOT_MEDIUM ) != 0;
break;
case 2:
case 3:
return ( spawnflags & SPAWNFLAG_NOT_HARD );
break;
}
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::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 ) {
LoadAllScripts( "anim", ".scr" );
}
LoadAllScripts( "global", ".scr" );
}
void Level::SpawnEntities( char *entities, int svsTime )
{
2019-07-01 20:17:46 +02:00
int inhibit, radnum = 0, count = 0;
2016-03-27 11:49:47 +02:00
int enttime;
const char *value;
2019-07-01 20:17:46 +02:00
char name[128];
2016-03-27 11:49:47 +02:00
SpawnArgs args;
Listener *listener;
Entity *ent;
// Create the game variable list
game.CreateVars();
// Create the parm variable list
parm.CreateVars();
// Create the level variable list
CreateVars();
// set up time so functions still have valid times
setTime( svsTime );
setFrametime( 50 );
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();
// parse ents
inhibit = 0;
enttime = gi.Milliseconds();
// Set up for a new map
PathManager.LoadNodes();
Com_Printf( "-------------------- Actual Spawning Entities -----------------------\n" );
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 );
if( inhibitEntity( spawnflags ) )
{
inhibit++;
continue;
}
}
listener = args.SpawnInternal();
if( listener )
{
2019-07-01 20:17:46 +02:00
radnum++;
2016-03-27 11:49:47 +02:00
if( listener->isSubclassOf( Entity ) )
{
count++;
ent = ( Entity * )listener;
2019-07-01 20:17:46 +02:00
ent->radnum = radnum;
2016-03-27 11:49:47 +02:00
Q_strncpyz( ent->edict->entname, ent->getClassID(), sizeof( ent->edict->entname ) );
2019-07-01 20:17:46 +02:00
ent->PostEvent(EV_Entity_Start, -1.0, 0);
sprintf(name, "i%d", radnum);
gi.LoadResource(name);
2016-03-27 11:49:47 +02:00
}
}
}
Com_Printf( "-------------------- Actual Spawning Entities Done ------------------ %i ms\n", gi.Milliseconds() - enttime );
world->UpdateConfigStrings();
Event *ev = new Event( EV_Level_PreSpawnSentient );
PostEvent( ev, EV_SPAWNENTITIES );
L_ProcessPendingEvents();
if( g_gametype->integer ) {
dmManager.InitGame();
}
if( game.maxclients == 1 )
{
spawn_entnum = 0;
new Player;
}
m_LoopProtection = false;
AddWaitTill( STRING_PRESPAWN );
Unregister( STRING_PRESPAWN );
m_LoopProtection = true;
Com_Printf( "%i entities spawned\n", count );
2019-07-01 20:17:46 +02:00
Com_Printf( "%i simple entities spawned\n", radnum );
2016-03-27 11:49:47 +02:00
Com_Printf( "%i entities inhibited\n", inhibit );
Com_Printf( "-------------------- Spawning Entities Done ------------------ %i ms\n", gi.Milliseconds() - enttime );
}
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();
2018-09-17 23:50:38 +02:00
2016-03-27 11:49:47 +02:00
}
/*
================
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 );
}
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( !WaitTillDisabled( STRING_SPAWN ) )
{
AddWaitTill( STRING_SPAWN );
Director.iPaused++;
for( ent = active_edicts.next; ent != &active_edicts; ent = ent->next )
{
ent->entity->Unregister( STRING_SPAWN );
}
if( Director.iPaused-- == 1 ) {
Director.ExecuteRunning();
}
Unregister( STRING_SPAWN );
}
else
{
Director.LoadMenus();
}
spawning = false;
}
void Level::CheckVote
(
void
)
{
// FIXME: show the vote HUD like in SH and BT
if( !m_voteTime )
return;
if( time - m_voteTime >= 30000.0f )
{
dmManager.PrintAllClients( "Vote failed.\n" );
m_voteTime = 0;
return;
}
if( m_voteYes > m_numVoters / 2 )
{
dmManager.PrintAllClients( "Vote passed.\n" );
// Pass arguments to console
gi.SendConsoleCommand( va( "%s", level.m_voteString.c_str() ) );
m_voteTime = 0;
return;
}
if( m_voteNo >= m_numVoters / 2 )
{
dmManager.PrintAllClients( "Vote failed.\n" );
m_voteTime = 0;
return;
}
}
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 );
}
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::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::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)
2016-03-27 11:49:47 +02:00
{
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::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
)
{
gi.SetConfigstring( CS_RAIN_NUMSHADERS, ev->GetString( 1 ) );
}
void Level::EventRainNumShadersGet
(
Event *ev
)
{
ev->AddString( gi.GetConfigstring( CS_RAIN_NUMSHADERS ) );
}
void Level::EventGetFlags
(
Event *ev
)
{
}
2018-08-19 08:26:59 +02:00
str Level::GetRandomHeadModel(const char *model)
{
//FIXME: macros
char s[1024];
2018-08-29 14:41:48 +02:00
int num = random2() * gi.NumHeadModels(model);
2018-08-19 08:26:59 +02:00
gi.GetHeadModel(model, num, s);
return s;
}
str Level::GetRandomHeadSkin(const char *model)
{
//FIXME: macros
char s[1024];
2018-08-29 14:41:48 +02:00
int num = random2() * gi.NumHeadSkins(model);
2018-08-19 08:26:59 +02:00
gi.GetHeadSkin(model, num, s);
return s;
}
2016-03-27 11:49:47 +02:00
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_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_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::EventRainShaderSet },
{ &EV_Level_Rain_NumShaders_Get, &Level::EventRainShaderGet },
{ &EV_Level_GetFlags, &Level::EventGetFlags },
{ NULL, NULL }
};