1 /**
2  * @file
3  * @brief Client side entity spawning from map entity string
4  */
5 
6 /*
7 Copyright (C) 2002-2013 UFO: Alien Invasion.
8 
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
13 
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17 
18 See the GNU General Public License for more details.
19 
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
23 */
24 
25 #include "cl_spawn.h"
26 #include "../client.h"
27 #include "../cgame/cl_game.h"
28 #include "cl_particle.h"
29 #include "../../shared/parse.h"
30 
31 /* position in the spawnflags */
32 #define MISC_MODEL_GLOW 9
33 #define SPAWNFLAG_NO_DAY 8
34 
35 typedef struct localEntityParse_s {
36 	char classname[MAX_VAR];
37 	char target[MAX_VAR];
38 	char targetname[MAX_VAR];
39 	char tagname[MAX_VAR];
40 	char anim[MAX_VAR];
41 	char model[MAX_QPATH];
42 	char particle[MAX_VAR];
43 	char noise[MAX_QPATH];
44 	vec3_t origin;
45 	vec3_t angles;
46 	vec3_t scale;
47 	vec3_t color;
48 	vec3_t ambientNightColor;
49 	vec_t nightLight;
50 	vec2_t nightSunAngles;
51 	vec3_t nightSunColor;
52 	vec3_t ambientDayColor;
53 	vec_t dayLight;
54 	vec2_t daySunAngles;
55 	vec3_t daySunColor;
56 	vec2_t wait;
57 	int maxLevel;
58 	int maxMultiplayerTeams;
59 	int skin;
60 	int frame;
61 	int light;
62 	int spawnflags;
63 	float volume;
64 	float attenuation;
65 	float angle;
66 	int maxteams;
67 
68 	/* not filled from entity string */
69 	const char* entStringPos;
70 	int entnum;
71 } localEntityParse_t;
72 
73 static const value_t localEntityValues[] = {
74 	{"skin", V_INT, offsetof(localEntityParse_t, skin), MEMBER_SIZEOF(localEntityParse_t, skin)},
75 	{"maxteams", V_INT, offsetof(localEntityParse_t, maxteams), MEMBER_SIZEOF(localEntityParse_t, maxteams)},
76 	{"spawnflags", V_INT, offsetof(localEntityParse_t, spawnflags), MEMBER_SIZEOF(localEntityParse_t, spawnflags)},
77 	{"maxlevel", V_INT, offsetof(localEntityParse_t, maxLevel), MEMBER_SIZEOF(localEntityParse_t, maxLevel)},
78 	{"attenuation", V_FLOAT, offsetof(localEntityParse_t, attenuation), MEMBER_SIZEOF(localEntityParse_t, attenuation)},
79 	{"volume", V_FLOAT, offsetof(localEntityParse_t, volume), MEMBER_SIZEOF(localEntityParse_t, volume)},
80 	{"frame", V_INT, offsetof(localEntityParse_t, frame), MEMBER_SIZEOF(localEntityParse_t, frame)},
81 	{"angle", V_FLOAT, offsetof(localEntityParse_t, angle), MEMBER_SIZEOF(localEntityParse_t, angle)},
82 	{"wait", V_POS, offsetof(localEntityParse_t, wait), MEMBER_SIZEOF(localEntityParse_t, wait)},
83 	{"angles", V_VECTOR, offsetof(localEntityParse_t, angles), MEMBER_SIZEOF(localEntityParse_t, angles)},
84 	{"origin", V_VECTOR, offsetof(localEntityParse_t, origin), MEMBER_SIZEOF(localEntityParse_t, origin)},
85 	{"color", V_VECTOR, offsetof(localEntityParse_t, color), MEMBER_SIZEOF(localEntityParse_t, color)},
86 	{"_color", V_VECTOR, offsetof(localEntityParse_t, color), MEMBER_SIZEOF(localEntityParse_t, color)},
87 	{"modelscale_vec", V_VECTOR, offsetof(localEntityParse_t, scale), MEMBER_SIZEOF(localEntityParse_t, scale)},
88 	{"wait", V_POS, offsetof(localEntityParse_t, wait), MEMBER_SIZEOF(localEntityParse_t, wait)},
89 	{"classname", V_STRING, offsetof(localEntityParse_t, classname), 0},
90 	{"model", V_STRING, offsetof(localEntityParse_t, model), 0},
91 	{"anim", V_STRING, offsetof(localEntityParse_t, anim), 0},
92 	{"particle", V_STRING, offsetof(localEntityParse_t, particle), 0},
93 	{"noise", V_STRING, offsetof(localEntityParse_t, noise), 0},
94 	{"tag", V_STRING, offsetof(localEntityParse_t, tagname), 0},
95 	{"target", V_STRING, offsetof(localEntityParse_t, target), 0},
96 	{"targetname", V_STRING, offsetof(localEntityParse_t, targetname), 0},
97 	{"light", V_INT, offsetof(localEntityParse_t, light), MEMBER_SIZEOF(localEntityParse_t, light)},
98 	{"ambient_day", V_VECTOR, offsetof(localEntityParse_t, ambientDayColor), MEMBER_SIZEOF(localEntityParse_t, ambientDayColor)},
99 	{"light_day", V_FLOAT, offsetof(localEntityParse_t, dayLight), MEMBER_SIZEOF(localEntityParse_t, dayLight)},
100 	{"angles_day", V_POS, offsetof(localEntityParse_t, daySunAngles), MEMBER_SIZEOF(localEntityParse_t, daySunAngles)},
101 	{"color_day", V_VECTOR, offsetof(localEntityParse_t, daySunColor), MEMBER_SIZEOF(localEntityParse_t, daySunColor)},
102 	{"ambient_night", V_VECTOR, offsetof(localEntityParse_t, ambientNightColor), MEMBER_SIZEOF(localEntityParse_t, ambientNightColor)},
103 	{"light_night", V_FLOAT, offsetof(localEntityParse_t, nightLight), MEMBER_SIZEOF(localEntityParse_t, nightLight)},
104 	{"angles_night", V_POS, offsetof(localEntityParse_t, nightSunAngles), MEMBER_SIZEOF(localEntityParse_t, nightSunAngles)},
105 	{"color_night", V_VECTOR, offsetof(localEntityParse_t, nightSunColor), MEMBER_SIZEOF(localEntityParse_t, nightSunColor)},
106 
107 	{nullptr, V_NULL, 0, 0}
108 };
109 
110 static void SP_worldspawn(const localEntityParse_t* entData);
111 static void SP_misc_model(const localEntityParse_t* entData);
112 static void SP_misc_particle(const localEntityParse_t* entData);
113 static void SP_misc_sound(const localEntityParse_t* entData);
114 static void SP_light(const localEntityParse_t* entData);
115 
116 typedef struct {
117 	const char* name;
118 	void (*spawn) (const localEntityParse_t* entData);
119 } spawn_t;
120 
121 static const spawn_t spawns[] = {
122 	{"worldspawn", SP_worldspawn},
123 	{"misc_model", SP_misc_model},
124 	{"misc_particle", SP_misc_particle},
125 	{"misc_sound", SP_misc_sound},
126 	{"light", SP_light},
127 
128 	{nullptr, nullptr}
129 };
130 
131 /**
132  * @brief Finds the spawn function for the entity and calls it
133  */
CL_SpawnCall(const localEntityParse_t * entData)134 static void CL_SpawnCall (const localEntityParse_t* entData)
135 {
136 	const spawn_t* s;
137 
138 	if (entData->classname[0] == '\0')
139 		return;
140 
141 	/* check normal spawn functions */
142 	for (s = spawns; s->name; s++) {
143 		/* found it */
144 		if (Q_streq(s->name, entData->classname)) {
145 			s->spawn(entData);
146 			return;
147 		}
148 	}
149 }
150 
151 /**
152  * @brief Parse the map entity string and spawns those entities that are client-side only
153  * @sa G_SendEdictsAndBrushModels
154  * @sa CL_AddBrushModel
155  */
CL_SpawnParseEntitystring(void)156 void CL_SpawnParseEntitystring (void)
157 {
158 	const char* es = cl.mapData->mapEntityString;
159 	int entnum = 0, maxLevel;
160 
161 	if (cl.mapMaxLevel > 0 && cl.mapMaxLevel < PATHFINDING_HEIGHT)
162 		maxLevel = cl.mapMaxLevel;
163 	else
164 		maxLevel = PATHFINDING_HEIGHT;
165 
166 	/* vid restart? */
167 	if (cl.numMapParticles || cl.numLMs)
168 		return;
169 
170 	/* parse ents */
171 	while (1) {
172 		localEntityParse_t entData;
173 		/* parse the opening brace */
174 		const char* entityToken = Com_Parse(&es);
175 		/* memorize the start */
176 		if (!es)
177 			break;
178 
179 		if (entityToken[0] != '{')
180 			Com_Error(ERR_DROP, "V_ParseEntitystring: found %s when expecting {", entityToken);
181 
182 		/* initialize */
183 		OBJZERO(entData);
184 		VectorSet(entData.scale, 1, 1, 1);
185 		entData.volume = SND_VOLUME_DEFAULT;
186 		entData.maxLevel = maxLevel;
187 		entData.entStringPos = es;
188 		entData.entnum = entnum;
189 		entData.maxMultiplayerTeams = TEAM_MAX_HUMAN;
190 		entData.attenuation = SOUND_ATTN_IDLE;
191 
192 		/* go through all the dictionary pairs */
193 		while (1) {
194 			const value_t* v;
195 			/* parse key */
196 			entityToken = Com_Parse(&es);
197 			if (entityToken[0] == '}')
198 				break;
199 			if (!es)
200 				Com_Error(ERR_DROP, "V_ParseEntitystring: EOF without closing brace");
201 
202 			for (v = localEntityValues; v->string; v++)
203 				if (Q_streq(entityToken, v->string)) {
204 					/* found a definition */
205 					entityToken = Com_Parse(&es);
206 					if (!es)
207 						Com_Error(ERR_DROP, "V_ParseEntitystring: EOF without closing brace");
208 					Com_EParseValue(&entData, entityToken, v->type, v->ofs, v->size);
209 					break;
210 				}
211 		}
212 		CL_SpawnCall(&entData);
213 
214 		entnum++;
215 	}
216 
217 	/* after we have parsed all the entities we can resolve the target, targetname
218 	 * connections for the misc_model entities */
219 	LM_Think();
220 }
221 
222 #define MIN_AMBIENT_COMPONENT 0.1
223 #define MIN_AMBIENT_SUM 0.50
224 
225 /** @note Defaults should match those of ufo2map, or lighting will be inconsistent between world and models */
SP_worldspawn(const localEntityParse_t * entData)226 static void SP_worldspawn (const localEntityParse_t* entData)
227 {
228 	const int dayLightmap = CL_GetConfigStringInteger(CS_LIGHTMAP);
229 	int i;
230 	vec3_t sunAngles;
231 	vec4_t sunColor;
232 	vec_t sunIntensity;
233 
234 	/* maximum level */
235 	cl.mapMaxLevel = entData->maxLevel;
236 
237 	if (GAME_IsMultiplayer()) {
238 		if (cl_teamnum->integer > entData->maxMultiplayerTeams || cl_teamnum->integer <= TEAM_CIVILIAN) {
239 			Com_Printf("The selected team is not usable. "
240 				"The map doesn't support %i teams but only %i teams\n",
241 				cl_teamnum->integer, entData->maxMultiplayerTeams);
242 			Cvar_SetValue("cl_teamnum", TEAM_DEFAULT);
243 			Com_Printf("Set teamnum to %i\n", cl_teamnum->integer);
244 		}
245 	}
246 
247 	/** @todo - make sun position/color vary based on local time at location? */
248 
249 	/** @note Some vectors have exra elements to comply with mathlib and/or OpenGL conventions, but handled as shorter ones */
250 	if (dayLightmap) {
251 		/* set defaults for daylight */
252 		Vector4Set(refdef.ambientColor, 0.26, 0.26, 0.26, 1.0);
253 		sunIntensity = 280;
254 		VectorSet(sunAngles, -75, 100, 0);
255 		Vector4Set(sunColor, 0.90, 0.75, 0.65, 1.0);
256 
257 		/* override defaults with data from worldspawn entity, if any */
258 		if (VectorNotEmpty(entData->ambientDayColor))
259 			VectorCopy(entData->ambientDayColor, refdef.ambientColor);
260 
261 		if (entData->dayLight)
262 			sunIntensity = entData->dayLight;
263 
264 		if (Vector2NotEmpty(entData->daySunAngles))
265 			Vector2Copy(entData->daySunAngles, sunAngles);
266 
267 		if (VectorNotEmpty(entData->daySunColor))
268 			VectorCopy(entData->daySunColor, sunColor);
269 
270 		Vector4Set(refdef.sunSpecularColor, 1.0, 1.0, 0.9, 1);
271 	} else {
272 		/* set defaults for night light */
273 		Vector4Set(refdef.ambientColor, 0.16, 0.16, 0.17, 1.0);
274 		sunIntensity = 15;
275 		VectorSet(sunAngles, -80, 220, 0);
276 		Vector4Set(sunColor, 0.25, 0.25, 0.35, 1.0);
277 
278 		/* override defaults with data from worldspawn entity, if any */
279 		if (VectorNotEmpty(entData->ambientNightColor))
280 			VectorCopy(entData->ambientNightColor, refdef.ambientColor);
281 
282 		if (entData->nightLight)
283 			sunIntensity = entData->nightLight;
284 
285 		if (Vector2NotEmpty(entData->nightSunAngles))
286 			Vector2Copy(entData->nightSunAngles, sunAngles);
287 
288 		if (VectorNotEmpty(entData->nightSunColor))
289 			VectorCopy(entData->nightSunColor, sunColor);
290 
291 		Vector4Set(refdef.sunSpecularColor, 0.5, 0.5, 0.7, 1);
292 	}
293 
294 	ColorNormalize(sunColor, sunColor);
295 	VectorScale(sunColor, sunIntensity/255.0, sunColor);
296 	Vector4Copy(sunColor, refdef.sunDiffuseColor);
297 
298 	/* clamp ambient for models */
299 	Vector4Copy(refdef.ambientColor, refdef.modelAmbientColor);
300 	for (i = 0; i < 3; i++)
301 		if (refdef.modelAmbientColor[i] < MIN_AMBIENT_COMPONENT)
302 			refdef.modelAmbientColor[i] = MIN_AMBIENT_COMPONENT;
303 
304 	/* scale it into a reasonable range, the clamp above ensures this will work */
305 	while (VectorSum(refdef.modelAmbientColor) < MIN_AMBIENT_SUM)
306 		VectorScale(refdef.modelAmbientColor, 1.25, refdef.modelAmbientColor);
307 
308 	AngleVectors(sunAngles, refdef.sunVector, nullptr, nullptr);
309 	refdef.sunVector[3] = 0.0; /* to use as directional light source in OpenGL */
310 
311 	/** @todo Parse fog from worldspawn config */
312 	refdef.weather = WEATHER_NONE;
313 	refdef.fogColor[3] = 1.0;
314 	VectorSet(refdef.fogColor, 0.75, 0.75, 0.75);
315 }
316 
SP_misc_model(const localEntityParse_t * entData)317 static void SP_misc_model (const localEntityParse_t* entData)
318 {
319 	localModel_t* lm;
320 	int renderFlags = 0;
321 
322 	if (entData->model[0] == '\0') {
323 		Com_Printf("misc_model without \"model\" specified\n");
324 		return;
325 	}
326 
327 	if (entData->spawnflags & (1 << MISC_MODEL_GLOW))
328 		renderFlags |= RF_PULSE;
329 
330 	/* add it */
331 	lm = LM_AddModel(entData->model, entData->origin, entData->angles, entData->entnum, (entData->spawnflags & 0xFF), renderFlags, entData->scale);
332 	if (lm) {
333 		if (LM_GetByID(entData->targetname) != nullptr)
334 			Com_Error(ERR_DROP, "Ambiguous targetname '%s'", entData->targetname);
335 		Q_strncpyz(lm->id, entData->targetname, sizeof(lm->id));
336 		Q_strncpyz(lm->target, entData->target, sizeof(lm->target));
337 		Q_strncpyz(lm->tagname, entData->tagname, sizeof(lm->tagname));
338 
339 		if (lm->animname[0] != '\0' && lm->tagname[0] != '\0') {
340 			Com_Printf("Warning: Model has animation set, but also a tag - use the tag and skip the animation\n");
341 			lm->animname[0] = '\0';
342 		}
343 
344 		if (lm->tagname[0] != '\0' && lm->target[0] == '\0') {
345 			Com_Error(ERR_DROP, "Warning: Model has tag set, but no target given");
346 		}
347 
348 		lm->think = LMT_Init;
349 		lm->skin = entData->skin;
350 		lm->frame = entData->frame;
351 		if (!lm->frame)
352 			Q_strncpyz(lm->animname, entData->anim, sizeof(lm->animname));
353 		else
354 			Com_Printf("Warning: Model has frame and anim parameters - using frame (no animation)\n");
355 	}
356 }
357 
SP_misc_particle(const localEntityParse_t * entData)358 static void SP_misc_particle (const localEntityParse_t* entData)
359 {
360 	const int dayLightmap = CL_GetConfigStringInteger(CS_LIGHTMAP);
361 	if (!(dayLightmap && (entData->spawnflags & (1 << SPAWNFLAG_NO_DAY))))
362 		CL_AddMapParticle(entData->particle, entData->origin, entData->wait, entData->entStringPos, (entData->spawnflags & 0xFF));
363 }
364 
SP_misc_sound(const localEntityParse_t * entData)365 static void SP_misc_sound (const localEntityParse_t* entData)
366 {
367 	const int dayLightmap = CL_GetConfigStringInteger(CS_LIGHTMAP);
368 	if (!(dayLightmap && (entData->spawnflags & (1 << SPAWNFLAG_NO_DAY))))
369 		LE_AddAmbientSound(entData->noise, entData->origin, (entData->spawnflags & 0xFF), entData->volume, entData->attenuation);
370 }
371 
372 /**
373  * @param entData
374  */
SP_light(const localEntityParse_t * entData)375 static void SP_light (const localEntityParse_t* entData)
376 {
377 	const int dayLightmap = CL_GetConfigStringInteger(CS_LIGHTMAP);
378 
379 	if (entData->light < 1.0)
380 		return;
381 
382 	if (!(dayLightmap && (entData->spawnflags & (1 << SPAWNFLAG_NO_DAY)))) {
383 		R_AddStaticLight(entData->origin, entData->light, entData->color);
384 	}
385 }
386