2021-10-24 21:17:09 +02:00
# include "vk_common.h"
# include "vk_mapents.h"
2021-11-27 08:59:48 +01:00
# include "vk_core.h" // TODO we need only pool from there, not the entire vulkan garbage
# include "vk_textures.h"
2021-10-24 21:17:09 +02:00
2021-10-24 22:21:00 +02:00
# include "eiface.h" // ARRAYSIZE
# include "xash3d_mathlib.h"
2021-11-28 22:38:01 +01:00
# include <string.h>
2021-12-11 21:40:57 +01:00
# include <ctype.h>
2021-10-24 22:21:00 +02:00
2021-10-24 21:17:09 +02:00
xvk_map_entities_t g_map_entities ;
static unsigned parseEntPropWadList ( const char * value , string * out , unsigned bit ) {
int dst_left = sizeof ( string ) - 2 ; // ; \0
char * dst = * out ;
* dst = ' \0 ' ;
gEngine . Con_Reportf ( " WADS: %s \n " , value ) ;
for ( ; * value ; ) {
const char * file_begin = value ;
for ( ; * value & & * value ! = ' ; ' ; + + value ) {
if ( * value = = ' \\ ' | | * value = = ' / ' )
file_begin = value + 1 ;
}
{
const int len = value - file_begin ;
gEngine . Con_Reportf ( " WAD: %.*s \n " , len , file_begin ) ;
if ( len < dst_left ) {
Q_strncpy ( dst , file_begin , len + 1 ) ;
dst + = len ;
dst [ 0 ] = ' ; ' ;
dst + + ;
dst [ 0 ] = ' \0 ' ;
dst_left - = len ;
}
}
if ( * value ) value + + ;
}
gEngine . Con_Reportf ( " wad list: %s \n " , * out ) ;
return bit ;
}
static unsigned parseEntPropFloat ( const char * value , float * out , unsigned bit ) {
return ( 1 = = sscanf ( value , " %f " , out ) ) ? bit : 0 ;
}
static unsigned parseEntPropInt ( const char * value , int * out , unsigned bit ) {
return ( 1 = = sscanf ( value , " %d " , out ) ) ? bit : 0 ;
}
2021-12-11 21:40:57 +01:00
static unsigned parseEntPropIntArray ( const char * value , int_array_t * out , unsigned bit ) {
unsigned retval = 0 ;
out - > num = 0 ;
while ( * value ) {
int i = 0 ;
if ( 0 = = sscanf ( value , " %d " , & i ) )
break ;
if ( out - > num = = MAX_INT_ARRAY_SIZE )
break ;
retval | = bit ;
out - > values [ out - > num + + ] = i ;
while ( * value & & isdigit ( * value ) ) + + value ;
while ( * value & & isspace ( * value ) ) + + value ;
}
if ( * value ) {
gEngine . Con_Printf ( S_ERROR " Error parsing mapents patch IntArray (wrong format? too many entries (max=%d)), portion not parsed: %s \n " , MAX_INT_ARRAY_SIZE , value ) ;
}
return retval ;
}
2021-10-24 22:17:13 +02:00
static unsigned parseEntPropString ( const char * value , string * out , unsigned bit ) {
const int len = Q_strlen ( value ) ;
if ( len > = sizeof ( string ) )
2021-12-11 21:40:57 +01:00
gEngine . Con_Printf ( S_ERROR " Map entity value '%s' is too long, max length is %d \n " ,
2021-10-24 22:17:13 +02:00
value , sizeof ( string ) ) ;
Q_strncpy ( * out , value , sizeof ( * out ) ) ;
return bit ;
}
2021-10-24 21:17:09 +02:00
static unsigned parseEntPropVec3 ( const char * value , vec3_t * out , unsigned bit ) {
return ( 3 = = sscanf ( value , " %f %f %f " , & ( * out ) [ 0 ] , & ( * out ) [ 1 ] , & ( * out ) [ 2 ] ) ) ? bit : 0 ;
}
static unsigned parseEntPropRgbav ( const char * value , vec3_t * out , unsigned bit ) {
float scale = 1.f ;
const int components = sscanf ( value , " %f %f %f %f " , & ( * out ) [ 0 ] , & ( * out ) [ 1 ] , & ( * out ) [ 2 ] , & scale ) ;
if ( components = = 1 ) {
( * out ) [ 2 ] = ( * out ) [ 1 ] = ( * out ) [ 0 ] = ( * out ) [ 0 ] ;
return bit ;
} else if ( components = = 4 ) {
scale / = 255.f ;
( * out ) [ 0 ] * = scale ;
( * out ) [ 1 ] * = scale ;
( * out ) [ 2 ] * = scale ;
return bit ;
} else if ( components = = 3 ) {
( * out ) [ 0 ] * = scale ;
( * out ) [ 1 ] * = scale ;
( * out ) [ 2 ] * = scale ;
return bit ;
}
return 0 ;
}
static unsigned parseEntPropClassname ( const char * value , class_name_e * out , unsigned bit ) {
if ( Q_strcmp ( value , " light " ) = = 0 ) {
* out = Light ;
} else if ( Q_strcmp ( value , " light_spot " ) = = 0 ) {
* out = LightSpot ;
} else if ( Q_strcmp ( value , " light_environment " ) = = 0 ) {
* out = LightEnvironment ;
} else if ( Q_strcmp ( value , " worldspawn " ) = = 0 ) {
* out = Worldspawn ;
} else {
* out = Ignored ;
}
return bit ;
}
static void weirdGoldsrcLightScaling ( vec3_t intensity ) {
float l1 = Q_max ( intensity [ 0 ] , Q_max ( intensity [ 1 ] , intensity [ 2 ] ) ) ;
l1 = l1 * l1 / 10 ;
VectorScale ( intensity , l1 , intensity ) ;
}
static void parseAngles ( const entity_props_t * props , vk_light_entity_t * le ) {
float angle = props - > angle ;
VectorSet ( le - > dir , 0 , 0 , 0 ) ;
if ( angle = = - 1 ) { // UP
le - > dir [ 0 ] = le - > dir [ 1 ] = 0 ;
le - > dir [ 2 ] = 1 ;
} else if ( angle = = - 2 ) { // DOWN
le - > dir [ 0 ] = le - > dir [ 1 ] = 0 ;
le - > dir [ 2 ] = - 1 ;
} else {
if ( angle = = 0 ) {
angle = props - > angles [ 1 ] ;
}
angle * = M_PI / 180.f ;
le - > dir [ 2 ] = 0 ;
le - > dir [ 0 ] = cosf ( angle ) ;
le - > dir [ 1 ] = sinf ( angle ) ;
}
angle = props - > pitch ? props - > pitch : props - > angles [ 0 ] ;
angle * = M_PI / 180.f ;
le - > dir [ 2 ] = sinf ( angle ) ;
le - > dir [ 0 ] * = cosf ( angle ) ;
le - > dir [ 1 ] * = cosf ( angle ) ;
}
static void parseStopDot ( const entity_props_t * props , vk_light_entity_t * le ) {
le - > stopdot = props - > _cone ? props - > _cone : 10 ;
le - > stopdot2 = Q_max ( le - > stopdot , props - > _cone2 ) ;
le - > stopdot = cosf ( le - > stopdot * M_PI / 180.f ) ;
le - > stopdot2 = cosf ( le - > stopdot2 * M_PI / 180.f ) ;
}
2021-11-28 22:38:01 +01:00
static void fillLightFromProps ( vk_light_entity_t * le , const entity_props_t * props , unsigned have_fields , qboolean patch , int entity_index ) {
switch ( le - > type ) {
case LightTypePoint :
break ;
case LightTypeSpot :
case LightTypeEnvironment :
if ( ! patch | | ( have_fields & Field_pitch ) | | ( have_fields & Field_angles ) | | ( have_fields & Field_angle ) ) {
parseAngles ( props , le ) ;
}
if ( ! patch | | ( have_fields & Field__cone ) | | ( have_fields & Field__cone2 ) ) {
parseStopDot ( props , le ) ;
}
break ;
default :
ASSERT ( false ) ;
}
if ( have_fields & Field_target )
Q_strcpy ( le - > target_entity , props - > target ) ;
if ( have_fields & Field_origin )
VectorCopy ( props - > origin , le - > origin ) ;
if ( have_fields & Field__light )
{
VectorCopy ( props - > _light , le - > color ) ;
} else if ( ! patch ) {
// same as qrad
VectorSet ( le - > color , 300 , 300 , 300 ) ;
}
2021-12-08 20:25:39 +01:00
if ( have_fields & Field__xvk_radius ) {
le - > radius = props - > _xvk_radius ;
2021-11-28 23:05:54 +01:00
}
if ( have_fields & Field_style ) {
2021-11-28 22:38:01 +01:00
le - > style = props - > style ;
}
if ( le - > type ! = LightEnvironment & & ( ! patch | | ( have_fields & Field__light ) ) ) {
weirdGoldsrcLightScaling ( le - > color ) ;
}
2021-12-05 22:38:16 +01:00
gEngine . Con_Reportf ( " %s light %d (ent=%d): %s targetname=%s color=(%f %f %f) origin=(%f %f %f) style=%d R=%f dir=(%f %f %f) stopdot=(%f %f) \n " ,
2021-11-28 22:38:01 +01:00
patch ? " Patch " : " Added " ,
g_map_entities . num_lights , entity_index ,
le - > type = = LightTypeEnvironment ? " environment " : le - > type = = LightTypeSpot ? " spot " : " point " ,
props - > targetname ,
le - > color [ 0 ] , le - > color [ 1 ] , le - > color [ 2 ] ,
le - > origin [ 0 ] , le - > origin [ 1 ] , le - > origin [ 2 ] ,
le - > style ,
2021-12-05 22:38:16 +01:00
le - > radius ,
2021-11-28 22:38:01 +01:00
le - > dir [ 0 ] , le - > dir [ 1 ] , le - > dir [ 2 ] ,
le - > stopdot , le - > stopdot2 ) ;
}
2021-12-22 08:30:48 +01:00
static void addLightEntity ( const entity_props_t * props , unsigned have_fields ) {
2021-10-24 21:17:09 +02:00
const int index = g_map_entities . num_lights ;
vk_light_entity_t * le = g_map_entities . lights + index ;
unsigned expected_fields = 0 ;
if ( g_map_entities . num_lights = = ARRAYSIZE ( g_map_entities . lights ) ) {
gEngine . Con_Printf ( S_ERROR " Too many lights entities in map \n " ) ;
return ;
}
* le = ( vk_light_entity_t ) { 0 } ;
switch ( props - > classname ) {
case Light :
le - > type = LightTypePoint ;
expected_fields = Field_origin ;
break ;
case LightSpot :
if ( ( have_fields & Field__sky ) & & props - > _sky ! = 0 ) {
le - > type = LightTypeEnvironment ;
expected_fields = Field__cone | Field__cone2 ;
} else {
le - > type = LightTypeSpot ;
expected_fields = Field_origin | Field__cone | Field__cone2 ;
}
break ;
case LightEnvironment :
le - > type = LightTypeEnvironment ;
break ;
2021-11-28 22:38:01 +01:00
default :
ASSERT ( false ) ;
}
2021-10-24 21:17:09 +02:00
if ( ( have_fields & expected_fields ) ! = expected_fields ) {
gEngine . Con_Printf ( S_ERROR " Missing some fields for light entity \n " ) ;
return ;
}
2021-11-28 22:38:01 +01:00
if ( le - > type = = LightTypeEnvironment ) {
if ( g_map_entities . single_environment_index = = NoEnvironmentLights ) {
g_map_entities . single_environment_index = index ;
} else {
g_map_entities . single_environment_index = MoreThanOneEnvironmentLight ;
}
2021-10-26 18:56:16 +02:00
}
2021-12-22 08:30:48 +01:00
fillLightFromProps ( le , props , have_fields , false , g_map_entities . entity_count ) ;
2021-10-24 21:17:09 +02:00
2021-12-22 08:30:48 +01:00
le - > entity_index = g_map_entities . entity_count ;
2021-10-24 21:17:09 +02:00
g_map_entities . num_lights + + ;
}
2021-10-24 22:17:13 +02:00
static void addTargetEntity ( const entity_props_t * props ) {
xvk_mapent_target_t * target = g_map_entities . targets + g_map_entities . num_targets ;
gEngine . Con_Reportf ( " Adding target entity %s at (%f, %f, %f) \n " ,
props - > targetname , props - > origin [ 0 ] , props - > origin [ 1 ] , props - > origin [ 2 ] ) ;
if ( g_map_entities . num_targets = = MAX_MAPENT_TARGETS ) {
gEngine . Con_Printf ( S_ERROR " Too many map target entities \n " ) ;
return ;
}
Q_strcpy ( target - > targetname , props - > targetname ) ;
VectorCopy ( props - > origin , target - > origin ) ;
+ + g_map_entities . num_targets ;
}
2021-10-24 21:17:09 +02:00
static void readWorldspawn ( const entity_props_t * props ) {
Q_strcpy ( g_map_entities . wadlist , props - > wad ) ;
}
2021-11-28 20:15:02 +01:00
static void addPatchSurface ( const entity_props_t * props , uint32_t have_fields ) {
2021-10-24 21:17:09 +02:00
const model_t * const map = gEngine . pfnGetModelByIndex ( 1 ) ;
2021-11-27 08:59:48 +01:00
const int num_surfaces = map - > numsurfaces ;
2021-12-20 23:05:08 +01:00
const qboolean should_remove = ( have_fields = = Field__xvk_surface_id ) | | ( have_fields & Field__xvk_texture & & props - > _xvk_texture [ 0 ] = = ' \0 ' ) ;
2021-10-24 21:17:09 +02:00
2021-12-11 21:40:57 +01:00
for ( int i = 0 ; i < props - > _xvk_surface_id . num ; + + i ) {
const int index = props - > _xvk_surface_id . values [ i ] ;
xvk_patch_surface_t * psurf = NULL ;
if ( index < 0 | | index > = num_surfaces ) {
gEngine . Con_Printf ( S_ERROR " Incorrect patch for surface_index %d where numsurfaces=%d \n " , index , num_surfaces ) ;
2021-12-20 23:05:08 +01:00
continue ;
2021-12-11 21:40:57 +01:00
}
if ( ! g_map_entities . patch . surfaces ) {
g_map_entities . patch . surfaces = Mem_Malloc ( vk_core . pool , num_surfaces * sizeof ( xvk_patch_surface_t ) ) ;
for ( int i = 0 ; i < num_surfaces ; + + i ) {
g_map_entities . patch . surfaces [ i ] . flags = Patch_Surface_NoPatch ;
g_map_entities . patch . surfaces [ i ] . tex_id = - 1 ;
g_map_entities . patch . surfaces [ i ] . tex = NULL ;
}
2021-11-27 08:59:48 +01:00
}
2021-10-24 21:17:09 +02:00
2021-12-11 21:40:57 +01:00
psurf = g_map_entities . patch . surfaces + index ;
2021-11-28 20:15:02 +01:00
2021-12-20 23:05:08 +01:00
if ( should_remove ) {
gEngine . Con_Reportf ( " Patch: surface %d removed \n " , index ) ;
psurf - > flags = Patch_Surface_Delete ;
continue ;
}
2021-12-11 21:40:57 +01:00
if ( have_fields & Field__xvk_texture ) {
2021-12-22 20:02:34 +01:00
const int tex_id = XVK_FindTextureNamedLike ( props - > _xvk_texture ) ;
2021-12-20 23:05:08 +01:00
gEngine . Con_Reportf ( " Patch for surface %d with texture \" %s \" -> %d \n " , index , props - > _xvk_texture , tex_id ) ;
psurf - > tex_id = tex_id ;
// Find texture_t for this index
for ( int i = 0 ; i < map - > numtextures ; + + i ) {
const texture_t * const tex = map - > textures [ i ] ;
if ( tex - > gl_texturenum = = tex_id ) {
psurf - > tex = tex ;
2021-12-22 20:55:35 +01:00
psurf - > tex_id = - 1 ;
2021-12-20 23:05:08 +01:00
break ;
2021-11-28 20:15:02 +01:00
}
2021-12-11 21:40:57 +01:00
}
2021-12-20 23:05:08 +01:00
psurf - > flags | = Patch_Surface_Texture ;
2021-11-28 20:15:02 +01:00
}
2021-11-27 08:59:48 +01:00
2021-12-11 21:40:57 +01:00
if ( have_fields & Field__light ) {
2022-03-21 20:00:02 +01:00
VectorScale ( props - > _light , 0.1f , psurf - > emissive ) ;
2021-12-11 21:40:57 +01:00
psurf - > flags | = Patch_Surface_Emissive ;
gEngine . Con_Reportf ( " Patch for surface %d: assign emissive %f %f %f \n " , index ,
psurf - > emissive [ 0 ] ,
psurf - > emissive [ 1 ] ,
psurf - > emissive [ 2 ]
) ;
}
2021-11-28 20:15:02 +01:00
}
2021-11-27 08:59:48 +01:00
}
2021-11-28 22:38:01 +01:00
int findLightEntityWithIndex ( int index ) {
// TODO could do binary search (entities are sorted by index) but why
for ( int i = 0 ; i < g_map_entities . num_lights ; + + i ) {
if ( g_map_entities . lights [ i ] . entity_index = = index )
return i ;
}
return - 1 ;
}
static void addPatchEntity ( const entity_props_t * props , uint32_t have_fields ) {
2021-12-11 21:40:57 +01:00
for ( int i = 0 ; i < props - > _xvk_ent_id . num ; + + i ) {
const int light_index = findLightEntityWithIndex ( props - > _xvk_ent_id . values [ i ] ) ;
if ( light_index < 0 ) {
gEngine . Con_Printf ( S_ERROR " Patch light entity with index=%d not found \n " , props - > _xvk_ent_id ) ;
2021-12-20 23:05:08 +01:00
continue ;
2021-12-11 21:40:57 +01:00
}
2021-11-28 22:38:01 +01:00
2021-12-11 21:40:57 +01:00
if ( have_fields = = Field__xvk_ent_id ) {
gEngine . Con_Reportf ( " Deleting light entity (%d of %d) with index=%d \n " , light_index , g_map_entities . num_lights , props - > _xvk_ent_id ) ;
g_map_entities . num_lights - - ;
memmove ( g_map_entities . lights + light_index , g_map_entities . lights + light_index + 1 , sizeof ( * g_map_entities . lights ) * g_map_entities . num_lights - light_index ) ;
2021-12-20 23:05:08 +01:00
continue ;
2021-12-11 21:40:57 +01:00
}
2021-11-28 22:38:01 +01:00
2021-12-11 21:40:57 +01:00
fillLightFromProps ( g_map_entities . lights + light_index , props , have_fields , true , props - > _xvk_ent_id . values [ i ] ) ;
}
2021-11-28 22:38:01 +01:00
}
2021-12-22 08:30:48 +01:00
static void parseEntities ( char * string ) {
2021-11-27 08:59:48 +01:00
unsigned have_fields = 0 ;
entity_props_t values ;
char * pos = string ;
2021-10-24 21:17:09 +02:00
//gEngine.Con_Reportf("ENTITIES: %s\n", pos);
for ( ; ; ) {
char key [ 1024 ] ;
char value [ 1024 ] ;
pos = COM_ParseFile ( pos , key , sizeof ( key ) ) ;
ASSERT ( Q_strlen ( key ) < sizeof ( key ) ) ;
if ( ! pos )
break ;
if ( key [ 0 ] = = ' { ' ) {
have_fields = None ;
values = ( entity_props_t ) { 0 } ;
continue ;
} else if ( key [ 0 ] = = ' } ' ) {
2021-10-24 22:17:13 +02:00
const int target_fields = Field_targetname | Field_origin ;
if ( ( have_fields & target_fields ) = = target_fields )
addTargetEntity ( & values ) ;
2021-10-24 21:17:09 +02:00
switch ( values . classname ) {
case Light :
case LightSpot :
case LightEnvironment :
2021-12-22 08:30:48 +01:00
addLightEntity ( & values , have_fields ) ;
2021-10-24 21:17:09 +02:00
break ;
case Worldspawn :
readWorldspawn ( & values ) ;
break ;
case Unknown :
2021-11-28 20:15:02 +01:00
if ( have_fields & Field__xvk_surface_id ) {
addPatchSurface ( & values , have_fields ) ;
2021-11-28 22:38:01 +01:00
} else if ( have_fields & Field__xvk_ent_id ) {
addPatchEntity ( & values , have_fields ) ;
2021-11-27 08:59:48 +01:00
}
break ;
2021-10-24 21:17:09 +02:00
case Ignored :
// Skip
break ;
}
2021-11-28 22:38:01 +01:00
2021-12-22 08:30:48 +01:00
g_map_entities . entity_count + + ;
2021-10-24 21:17:09 +02:00
continue ;
}
pos = COM_ParseFile ( pos , value , sizeof ( value ) ) ;
ASSERT ( Q_strlen ( value ) < sizeof ( value ) ) ;
if ( ! pos )
break ;
# define READ_FIELD(num, type, name, kind) \
if ( Q_strcmp ( key , # name ) = = 0 ) { \
const unsigned bit = parseEntProp # # kind ( value , & values . name , Field_ # # name ) ; \
if ( bit = = 0 ) { \
gEngine . Con_Printf ( S_ERROR " Error parsing entity property " # name " , invalid value: %s \n " , value ) ; \
} else have_fields | = bit ; \
} else
ENT_PROP_LIST ( READ_FIELD )
{
//gEngine.Con_Reportf("Unknown field %s with value %s\n", key, value);
}
# undef CHECK_FIELD
}
2021-11-27 08:59:48 +01:00
}
const xvk_mapent_target_t * findTargetByName ( const char * name ) {
for ( int i = 0 ; i < g_map_entities . num_targets ; + + i ) {
const xvk_mapent_target_t * target = g_map_entities . targets + i ;
if ( Q_strcmp ( name , target - > targetname ) = = 0 )
return target ;
}
2021-10-24 22:17:13 +02:00
2021-11-27 08:59:48 +01:00
return NULL ;
}
static void orientSpotlights ( void ) {
2021-10-24 22:17:13 +02:00
// Patch spotlight directions based on target entities
for ( int i = 0 ; i < g_map_entities . num_lights ; + + i ) {
vk_light_entity_t * const light = g_map_entities . lights + i ;
const xvk_mapent_target_t * target ;
2021-10-26 17:57:43 +02:00
if ( light - > type ! = LightSpot & & light - > type ! = LightTypeEnvironment )
2021-10-24 22:17:13 +02:00
continue ;
if ( light - > target_entity [ 0 ] = = ' \0 ' )
continue ;
target = findTargetByName ( light - > target_entity ) ;
if ( ! target ) {
gEngine . Con_Printf ( S_ERROR " Couldn't find target entity '%s' for spot light %d \n " , light - > target_entity , i ) ;
continue ;
}
VectorSubtract ( target - > origin , light - > origin , light - > dir ) ;
VectorNormalize ( light - > dir ) ;
gEngine . Con_Reportf ( " Light %d patched direction towards '%s': %f %f %f \n " , i , target - > targetname ,
light - > dir [ 0 ] , light - > dir [ 1 ] , light - > dir [ 2 ] ) ;
}
2021-10-24 21:17:09 +02:00
}
2021-11-27 08:59:48 +01:00
2021-12-22 08:30:48 +01:00
static void parsePatches ( const model_t * const map ) {
2021-11-27 08:59:48 +01:00
char filename [ 256 ] ;
byte * data ;
if ( g_map_entities . patch . surfaces ) {
Mem_Free ( g_map_entities . patch . surfaces ) ;
g_map_entities . patch . surfaces = NULL ;
}
Q_snprintf ( filename , sizeof ( filename ) , " luchiki/%s.patch " , map - > name ) ;
gEngine . Con_Reportf ( " Loading patches from file \" %s \" \n " , filename ) ;
data = gEngine . COM_LoadFile ( filename , 0 , false ) ;
if ( ! data ) {
gEngine . Con_Reportf ( " No patch file \" %s \" \n " , filename ) ;
return ;
}
2021-12-22 08:30:48 +01:00
parseEntities ( ( char * ) data ) ;
2021-11-27 08:59:48 +01:00
Mem_Free ( data ) ;
}
void XVK_ParseMapEntities ( void ) {
const model_t * const map = gEngine . pfnGetModelByIndex ( 1 ) ;
ASSERT ( map ) ;
g_map_entities . num_targets = 0 ;
g_map_entities . num_lights = 0 ;
g_map_entities . single_environment_index = NoEnvironmentLights ;
2021-12-22 08:30:48 +01:00
g_map_entities . entity_count = 0 ;
2021-11-27 08:59:48 +01:00
2021-12-22 08:30:48 +01:00
parseEntities ( map - > entities ) ;
orientSpotlights ( ) ;
}
void XVK_ParseMapPatches ( void ) {
const model_t * const map = gEngine . pfnGetModelByIndex ( 1 ) ;
2021-11-27 08:59:48 +01:00
2021-12-22 08:30:48 +01:00
parsePatches ( map ) ;
2021-11-27 08:59:48 +01:00
orientSpotlights ( ) ;
}