1 /*
2 	C-Dogs SDL
3 	A port of the legendary (and fun) action/arcade cdogs.
4 	Copyright (C) 1995 Ronny Wester
5 	Copyright (C) 2003 Jeremy Chin
6 	Copyright (C) 2003-2007 Lucas Martin-King
7 
8 	This program is free software; you can redistribute it and/or modify
9 	it under the terms of the GNU General Public License as published by
10 	the Free Software Foundation; either version 2 of the License, or
11 	(at your option) any later version.
12 
13 	This program is distributed in the hope that it will be useful,
14 	but WITHOUT ANY WARRANTY; without even the implied warranty of
15 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 	GNU General Public License for more details.
17 
18 	You should have received a copy of the GNU General Public License
19 	along with this program; if not, write to the Free Software
20 	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21 
22 	This file incorporates work covered by the following copyright and
23 	permission notice:
24 
25 	Copyright (c) 2013-2021 Cong Xu
26 	All rights reserved.
27 
28 	Redistribution and use in source and binary forms, with or without
29 	modification, are permitted provided that the following conditions are met:
30 
31 	Redistributions of source code must retain the above copyright notice, this
32 	list of conditions and the following disclaimer.
33 	Redistributions in binary form must reproduce the above copyright notice,
34 	this list of conditions and the following disclaimer in the documentation
35 	and/or other materials provided with the distribution.
36 
37 	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
38 	AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
39 	IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
40 	ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
41 	LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
42 	CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
43 	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
44 	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
45 	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
46 	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
47 	POSSIBILITY OF SUCH DAMAGE.
48 */
49 #include "objs.h"
50 
51 #include <assert.h>
52 
53 #include "bullet_class.h"
54 #include "damage.h"
55 #include "gamedata.h"
56 #include "log.h"
57 #include "net_util.h"
58 #include "pickup.h"
59 
60 CArray gObjs;
61 CArray gMobObjs;
62 static unsigned int sObjUIDs = 0;
63 static unsigned int sMobObjUIDs = 0;
64 
65 // Draw functions
66 
MapObjectDraw(GraphicsDevice * g,const int id,const struct vec2i pos)67 static void MapObjectDraw(
68 	GraphicsDevice *g, const int id, const struct vec2i pos)
69 {
70 	const TObject *obj = CArrayGet(&gObjs, id);
71 	CASSERT(obj->isInUse, "Cannot draw non-existent map object");
72 	CPicDrawContext c = CPicDrawContextNew();
73 	c.Offset = obj->Class->Offset;
74 	CPicDraw(g, &obj->thing.CPic, pos, &c);
75 }
76 
77 #define NUM_SPALL_PARTICLES 3
78 #define SPALL_IMPULSE_FACTOR 1.0f
DamageObject(const NThingDamage d)79 void DamageObject(const NThingDamage d)
80 {
81 	TObject *o = ObjGetByUID(d.UID);
82 	// Don't bother if object already destroyed
83 	if (o->Health <= 0)
84 	{
85 		return;
86 	}
87 
88 	// Create damage spall
89 	Emitter em;
90 	EmitterInit(&em, NULL, svec2_zero(), -0.5f, 0.5f, 1, 4, 0, 0, 0);
91 	AddParticle ap;
92 	memset(&ap, 0, sizeof ap);
93 	ap.Pos = o->thing.Pos;
94 	ap.Angle = NAN;
95 	ap.Z = 10;
96 	ap.Vel =
97 		svec2_scale(svec2_normalize(NetToVec2(d.Vel)), SPALL_IMPULSE_FACTOR);
98 	// Generate spall
99 	for (int i = 0; i < MIN(o->Health, d.Power); i++)
100 	{
101 		char buf[256];
102 		sprintf(buf, "spall%d", rand() % NUM_SPALL_PARTICLES + 1);
103 		ap.Class = StrParticleClass(&gParticleClasses, buf);
104 		// Choose random colour from object
105 		ap.Mask = PicGetRandomColor(CPicGetPic(&o->Class->Pic, 0));
106 		EmitterStart(&em, &ap);
107 	}
108 
109 	o->Health -= d.Power;
110 
111 	// Destroying objects and all the wonderful things that happen
112 	if (o->Health <= 0)
113 	{
114 		if (!gCampaign.IsClient)
115 		{
116 			GameEvent e = GameEventNew(GAME_EVENT_MAP_OBJECT_REMOVE);
117 			e.u.MapObjectRemove.UID = o->uid;
118 			e.u.MapObjectRemove.ActorUID = d.SourceActorUID;
119 			e.u.MapObjectRemove.Flags = d.Flags;
120 			GameEventsEnqueue(&gGameEvents, e);
121 		}
122 
123 		// Exploding spall
124 		EmitterInit(&em, NULL, svec2_zero(), -1.0f, 1.0f, 1, 16, 0, 0, 0);
125 		ap.Vel = svec2_scale(ap.Vel, 0.5f);
126 		for (int i = 0; i < 20; i++)
127 		{
128 			char buf[256];
129 			sprintf(buf, "spall%d", rand() % NUM_SPALL_PARTICLES + 1);
130 			ap.Class = StrParticleClass(&gParticleClasses, buf);
131 			// Choose random colour from object
132 			ap.Mask = PicGetRandomColor(CPicGetPic(&o->Class->Pic, 0));
133 			EmitterStart(&em, &ap);
134 		}
135 	}
136 }
137 
AddPickupAtObject(const TObject * o,const PickupType type)138 static void AddPickupAtObject(const TObject *o, const PickupType type)
139 {
140 	GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP);
141 	switch (type)
142 	{
143 	case PICKUP_HEALTH:
144 		if (!ConfigGetBool(&gConfig, "Game.HealthPickups"))
145 		{
146 			return;
147 		}
148 		strcpy(e.u.AddPickup.PickupClass, "health");
149 		break;
150 	case PICKUP_AMMO:
151 		if (!gCampaign.Setting.Ammo)
152 		{
153 			return;
154 		}
155 		// Pick a random ammo type and spawn it
156 		{
157 			const int ammoId = rand() % AmmoGetNumClasses(&gAmmo);
158 			const Ammo *a = AmmoGetById(&gAmmo, ammoId);
159 			sprintf(e.u.AddPickup.PickupClass, "ammo_%s", a->Name);
160 		}
161 		break;
162 	case PICKUP_GUN:
163 		// Pick a random mission gun type and spawn it
164 		{
165 			const int gunId = (int)(rand() % gMission.Weapons.size);
166 			const WeaponClass **wc = CArrayGet(&gMission.Weapons, gunId);
167 			sprintf(e.u.AddPickup.PickupClass, "gun_%s", (*wc)->name);
168 		}
169 		break;
170 	default:
171 		CASSERT(false, "unexpected pickup type");
172 		break;
173 	}
174 	e.u.AddPickup.Pos = Vec2ToNet(o->thing.Pos);
175 	e.u.AddPickup.IsRandomSpawned = true;
176 	GameEventsEnqueue(&gGameEvents, e);
177 }
178 
179 static void PlaceWreck(const char *wreckClass, const Thing *ti);
ObjRemove(const NMapObjectRemove mor)180 void ObjRemove(const NMapObjectRemove mor)
181 {
182 	TObject *o = ObjGetByUID(mor.UID);
183 	o->Health = 0;
184 
185 	if (!gCampaign.IsClient)
186 	{
187 		// Update objective
188 		UpdateMissionObjective(
189 			&gMission, o->thing.flags, OBJECTIVE_DESTROY, 1);
190 		const TActor *a = ActorGetByUID(mor.ActorUID);
191 		const int playerUID = a != NULL ? a->PlayerUID : -1;
192 		// Extra score if objective
193 		if ((o->thing.flags & THING_OBJECTIVE) && playerUID >= 0)
194 		{
195 			GameEvent e = GameEventNew(GAME_EVENT_SCORE);
196 			e.u.Score.PlayerUID = playerUID;
197 			e.u.Score.Score = OBJECT_SCORE;
198 			GameEventsEnqueue(&gGameEvents, e);
199 		}
200 
201 		// Weapons that go off when this object is destroyed
202 		CA_FOREACH(const WeaponClass *, wc, o->Class->DestroyGuns)
203 		CASSERT((*wc)->Type != GUNTYPE_MULTI, "unexpected gun type");
204 		WeaponClassFire(
205 			*wc, o->thing.Pos, 0, 0, mor.Flags, mor.ActorUID, true, false);
206 		CA_FOREACH_END()
207 
208 		// Random chance to add pickups in single player modes
209 		if (!IsPVP(gCampaign.Entry.Mode))
210 		{
211 			CA_FOREACH(
212 				const MapObjectDestroySpawn, mods, o->Class->DestroySpawn)
213 			const double chance = (double)rand() / RAND_MAX;
214 			if (chance < mods->SpawnChance)
215 			{
216 				AddPickupAtObject(o, mods->Type);
217 			}
218 			CA_FOREACH_END()
219 		}
220 
221 		if (strlen(o->Class->Wreck.Bullet) > 0)
222 		{
223 			// A wreck left after the destruction of this object
224 			// TODO: doesn't need to be network event
225 			GameEvent e = GameEventNew(GAME_EVENT_ADD_BULLET);
226 			e.u.AddBullet.UID = MobObjsObjsGetNextUID();
227 			strcpy(e.u.AddBullet.BulletClass, o->Class->Wreck.Bullet);
228 			e.u.AddBullet.MuzzlePos = Vec2ToNet(o->thing.Pos);
229 			GameEventsEnqueue(&gGameEvents, e);
230 		}
231 	}
232 
233 	SoundPlayAt(&gSoundDevice, o->Class->Wreck.Sound, o->thing.Pos);
234 
235 	// If wreck is available spawn it in the exact same position
236 	PlaceWreck(o->Class->Wreck.MO, &o->thing);
237 
238 	ObjDestroy(o);
239 
240 	if (o->thing.flags & THING_IMPASSABLE)
241 	{
242 		// Update pathfinding cache if this object blocked a path before
243 		PathCacheClear(&gPathCache);
244 	}
245 }
PlaceWreck(const char * wreckClass,const Thing * ti)246 static void PlaceWreck(const char *wreckClass, const Thing *ti)
247 {
248 	if (wreckClass == NULL)
249 	{
250 		return;
251 	}
252 	GameEvent e = GameEventNew(GAME_EVENT_MAP_OBJECT_ADD);
253 	e.u.MapObjectAdd.UID = ObjsGetNextUID();
254 	const MapObject *mo = StrMapObject(wreckClass);
255 	CASSERT(mo != NULL, "cannot find wreck");
256 	if (mo == NULL)
257 	{
258 		LOG(LM_MAIN, LL_ERROR, "wreck (%s) not found", wreckClass);
259 		return;
260 	}
261 	strcpy(e.u.MapObjectAdd.MapObjectClass, mo->Name);
262 	e.u.MapObjectAdd.Pos = Vec2ToNet(ti->Pos);
263 	e.u.MapObjectAdd.ThingFlags = MapObjectGetFlags(mo);
264 	e.u.MapObjectAdd.Health = mo->Health;
265 	GameEventsEnqueue(&gGameEvents, e);
266 }
267 
CanHit(const BulletClass * b,const int flags,const int uid,const Thing * target)268 bool CanHit(
269 	const BulletClass *b, const int flags, const int uid, const Thing *target)
270 {
271 	switch (target->kind)
272 	{
273 	case KIND_CHARACTER:
274 		return b->Hit.Flesh.Hit &&
275 			   CanHitCharacter(flags, uid, CArrayGet(&gActors, target->id));
276 	case KIND_OBJECT:
277 		return b->Hit.Object.Hit;
278 	default:
279 		CASSERT(false, "cannot damage tile item kind");
280 		break;
281 	}
282 	return false;
283 }
HasHitSound(const ThingKind targetKind,const int targetUID,const special_damage_e special,const bool allowFriendlyHitSound)284 bool HasHitSound(
285 	const ThingKind targetKind, const int targetUID,
286 	const special_damage_e special, const bool allowFriendlyHitSound)
287 {
288 	switch (targetKind)
289 	{
290 	case KIND_CHARACTER: {
291 		const TActor *a = ActorGetByUID(targetUID);
292 		return allowFriendlyHitSound || ActorTakesDamage(a, special);
293 	}
294 	case KIND_OBJECT:
295 		return true;
296 	default:
297 		CASSERT(false, "cannot damage tile item kind");
298 		break;
299 	}
300 	return false;
301 }
302 
303 static void DoDamageThing(
304 	const ThingKind targetKind, const int targetUID, const TActor *source,
305 	const int flags, const BulletClass *bullet, const bool canDamage,
306 	const struct vec2 hitVector);
307 static void DoDamageCharacter(
308 	const TActor *actor, const TActor *source, const struct vec2 hitVector,
309 	const BulletClass *bullet, const int flags);
Damage(const struct vec2 hitVector,const BulletClass * bullet,const int flags,const TActor * source,const ThingKind targetKind,const int targetUID)310 void Damage(
311 	const struct vec2 hitVector, const BulletClass *bullet, const int flags,
312 	const TActor *source, const ThingKind targetKind, const int targetUID)
313 {
314 	switch (targetKind)
315 	{
316 	case KIND_CHARACTER: {
317 		const TActor *actor = ActorGetByUID(targetUID);
318 		DoDamageCharacter(actor, source, hitVector, bullet, flags);
319 	}
320 	break;
321 	case KIND_OBJECT:
322 		DoDamageThing(
323 			targetKind, targetUID, source, flags, bullet, true, hitVector);
324 		break;
325 	default:
326 		CASSERT(false, "cannot damage tile item kind");
327 		break;
328 	}
329 }
DoDamageThing(const ThingKind targetKind,const int targetUID,const TActor * source,const int flags,const BulletClass * bullet,const bool canDamage,const struct vec2 hitVector)330 static void DoDamageThing(
331 	const ThingKind targetKind, const int targetUID, const TActor *source,
332 	const int flags, const BulletClass *bullet, const bool canDamage,
333 	const struct vec2 hitVector)
334 {
335 	GameEvent e = GameEventNew(GAME_EVENT_THING_DAMAGE);
336 	e.u.ThingDamage.UID = targetUID;
337 	e.u.ThingDamage.Kind = targetKind;
338 	e.u.ThingDamage.SourceActorUID = source ? source->uid : -1;
339 	e.u.ThingDamage.Flags = flags;
340 	e.u.ThingDamage.Special = bullet->Special.Effect;
341 	e.u.ThingDamage.SpecialTicks = bullet->Special.Ticks;
342 	e.u.ThingDamage.Power = canDamage ? bullet->Power : 0;
343 	e.u.ThingDamage.Mass = bullet->Mass;
344 	e.u.ThingDamage.Vel = Vec2ToNet(hitVector);
345 	GameEventsEnqueue(&gGameEvents, e);
346 }
DoDamageCharacter(const TActor * actor,const TActor * source,const struct vec2 hitVector,const BulletClass * bullet,const int flags)347 static void DoDamageCharacter(
348 	const TActor *actor, const TActor *source, const struct vec2 hitVector,
349 	const BulletClass *bullet, const int flags)
350 {
351 	// Create events: hit, damage, score
352 	CASSERT(actor->isInUse, "Cannot damage nonexistent player");
353 	CASSERT(
354 		CanHitCharacter(flags, source ? source->uid : -1, actor),
355 		"damaging undamageable actor");
356 
357 	// Shot pushback, based on mass and velocity
358 	const float impulseFactor = bullet->Mass * SHOT_IMPULSE_FACTOR *
359 								CHARACTER_DEFAULT_MASS /
360 								ActorGetCharacter(actor)->Class->Mass;
361 	const struct vec2 vel = svec2_scale(hitVector, impulseFactor);
362 	if (!svec2_is_zero(vel))
363 	{
364 		GameEvent ei = GameEventNew(GAME_EVENT_ACTOR_IMPULSE);
365 		ei.u.ActorImpulse.UID = actor->uid;
366 		ei.u.ActorImpulse.Vel = Vec2ToNet(vel);
367 		ei.u.ActorImpulse.Pos = Vec2ToNet(actor->Pos);
368 		GameEventsEnqueue(&gGameEvents, ei);
369 	}
370 
371 	const bool canDamage =
372 		CanDamageCharacter(flags, source, actor, bullet->Special.Effect);
373 
374 	DoDamageThing(
375 		KIND_CHARACTER, actor->uid, source, flags, bullet, canDamage,
376 		hitVector);
377 
378 	if (canDamage)
379 	{
380 		// Don't score for friendly, unpiloted vehicle, or player hits
381 		const bool isFriendly =
382 			(actor->flags & FLAGS_GOOD_GUY) ||
383 			actor->pilotUID == -1 ||
384 			(!IsPVP(gCampaign.Entry.Mode) && actor->PlayerUID >= 0);
385 		if (source && source->PlayerUID >= 0 && bullet->Power != 0 &&
386 			!isFriendly)
387 		{
388 			// Calculate score based on
389 			// if they hit a penalty character
390 			GameEvent e = GameEventNew(GAME_EVENT_SCORE);
391 			e.u.Score.PlayerUID = source->PlayerUID;
392 			if (actor->flags & FLAGS_PENALTY)
393 			{
394 				e.u.Score.Score = PENALTY_MULTIPLIER * bullet->Power;
395 			}
396 			else
397 			{
398 				e.u.Score.Score = bullet->Power;
399 			}
400 			GameEventsEnqueue(&gGameEvents, e);
401 		}
402 	}
403 }
404 
UpdateMobileObjects(int ticks)405 void UpdateMobileObjects(int ticks)
406 {
407 	CA_FOREACH(TMobileObject, obj, gMobObjs)
408 	if (!obj->isInUse)
409 	{
410 		continue;
411 	}
412 	if (!BulletUpdate(obj, ticks) && !gCampaign.IsClient)
413 	{
414 		GameEvent e = GameEventNew(GAME_EVENT_REMOVE_BULLET);
415 		e.u.RemoveBullet.UID = obj->UID;
416 		GameEventsEnqueue(&gGameEvents, e);
417 		continue;
418 	}
419 	CA_FOREACH_END()
420 }
421 
ObjsInit(void)422 void ObjsInit(void)
423 {
424 	CArrayInit(&gObjs, sizeof(TObject));
425 	CArrayReserve(&gObjs, 1024);
426 	sObjUIDs = 0;
427 }
ObjsTerminate(void)428 void ObjsTerminate(void)
429 {
430 	CA_FOREACH(TObject, o, gObjs)
431 	if (o->isInUse)
432 	{
433 		ObjDestroy(o);
434 	}
435 	CA_FOREACH_END()
436 	CArrayTerminate(&gObjs);
437 }
ObjsGetNextUID(void)438 int ObjsGetNextUID(void)
439 {
440 	return sObjUIDs++;
441 }
442 
ObjAdd(const NMapObjectAdd amo)443 void ObjAdd(const NMapObjectAdd amo)
444 {
445 	// Don't add if UID exists
446 	if (ObjGetByUID(amo.UID) != NULL)
447 	{
448 		LOG(LM_MAIN, LL_DEBUG, "object uid(%d) already exists; not adding",
449 			(int)amo.UID);
450 		return;
451 	}
452 	// Find an empty slot in object list
453 	TObject *o = NULL;
454 	int i;
455 	for (i = 0; i < (int)gObjs.size; i++)
456 	{
457 		TObject *obj = CArrayGet(&gObjs, i);
458 		if (!obj->isInUse)
459 		{
460 			o = obj;
461 			break;
462 		}
463 	}
464 	if (o == NULL)
465 	{
466 		TObject obj;
467 		memset(&obj, 0, sizeof obj);
468 		CArrayPushBack(&gObjs, &obj);
469 		i = (int)gObjs.size - 1;
470 		o = CArrayGet(&gObjs, i);
471 	}
472 	memset(o, 0, sizeof *o);
473 	o->uid = amo.UID;
474 	o->Class = StrMapObject(amo.MapObjectClass);
475 	switch (o->Class->Type)
476 	{
477 	case MAP_OBJECT_TYPE_NORMAL:
478 		// do nothing
479 		break;
480 	case MAP_OBJECT_TYPE_PICKUP_SPAWNER:
481 		// do nothing
482 		break;
483 	case MAP_OBJECT_TYPE_ACTOR_SPAWNER:
484 		o->counter = o->Class->u.Character.Counter;
485 		break;
486 	default:
487 		CASSERT(false, "unknown map object type");
488 		break;
489 	}
490 	ThingInit(&o->thing, i, KIND_OBJECT, o->Class->Size, amo.ThingFlags);
491 	o->Health = amo.Health;
492 	o->thing.CPic = o->Class->Pic;
493 	o->thing.CPic.Mask = Net2Color(amo.Mask);
494 	if (ColorEquals(o->thing.CPic.Mask, colorTransparent))
495 	{
496 		o->thing.CPic.Mask = o->Class->Pic.Mask;
497 	}
498 	o->thing.CPicFunc = MapObjectDraw;
499 	MapTryMoveThing(&gMap, &o->thing, NetToVec2(amo.Pos));
500 	EmitterInit(
501 		&o->damageSmoke, StrParticleClass(&gParticleClasses, "smoke_big"),
502 		svec2_zero(), -0.05f, 0.05f, 3, 3, 0, 0, 20);
503 	o->isInUse = true;
504 	LOG(LM_MAIN, LL_DEBUG,
505 		"added object uid(%d) class(%s) health(%d) pos(%d, %d)", (int)amo.UID,
506 		amo.MapObjectClass, amo.Health, (int)amo.Pos.x, (int)amo.Pos.y);
507 
508 	if (o->thing.flags & THING_IMPASSABLE)
509 	{
510 		// Update pathfinding cache if this object blocked a path before
511 		PathCacheClear(&gPathCache);
512 	}
513 }
514 
ObjDestroy(TObject * o)515 void ObjDestroy(TObject *o)
516 {
517 	CASSERT(o->isInUse, "Destroying in-use object");
518 	MapRemoveThing(&gMap, &o->thing);
519 	o->isInUse = false;
520 }
521 
ObjIsDangerous(const TObject * o)522 bool ObjIsDangerous(const TObject *o)
523 {
524 	// TODO: something more sophisticated? Check if weapon is dangerous
525 	return o->Class->DestroyGuns.size > 0;
526 }
527 
UpdateObjects(const int ticks)528 void UpdateObjects(const int ticks)
529 {
530 	CA_FOREACH(TObject, obj, gObjs)
531 	if (!obj->isInUse)
532 	{
533 		continue;
534 	}
535 	ThingUpdate(&obj->thing, ticks);
536 	switch (obj->Class->Type)
537 	{
538 	case MAP_OBJECT_TYPE_NORMAL:
539 		// Emit smoke when damaged
540 		if (obj->Class->DamageSmoke.HealthThreshold >= 0 &&
541 			obj->Health <=
542 				obj->Class->Health * obj->Class->DamageSmoke.HealthThreshold)
543 		{
544 			AddParticle ap;
545 			memset(&ap, 0, sizeof ap);
546 			ap.Pos = svec2_add(
547 				obj->thing.Pos,
548 				svec2(
549 					RAND_FLOAT(-obj->thing.size.x / 4, obj->thing.size.x / 4),
550 					RAND_FLOAT(
551 						-obj->thing.size.y / 4, obj->thing.size.y / 4)));
552 			ap.Mask = colorWhite;
553 			EmitterUpdate(&obj->damageSmoke, &ap, ticks);
554 		}
555 		break;
556 	case MAP_OBJECT_TYPE_PICKUP_SPAWNER:
557 		if (gCampaign.IsClient)
558 			break;
559 		// If counter -1, it is inactive i.e. already spawned
560 		if (obj->counter == -1)
561 		{
562 			break;
563 		}
564 		obj->counter -= ticks;
565 		if (obj->counter <= 0)
566 		{
567 			// Deactivate spawner by setting counter to -1
568 			// Spawner reactivated only when ammo taken
569 			obj->counter = -1;
570 			GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP);
571 			strcpy(e.u.AddPickup.PickupClass, obj->Class->u.PickupClass->Name);
572 			e.u.AddPickup.SpawnerUID = obj->uid;
573 			e.u.AddPickup.Pos = Vec2ToNet(obj->thing.Pos);
574 			GameEventsEnqueue(&gGameEvents, e);
575 		}
576 		break;
577 	case MAP_OBJECT_TYPE_ACTOR_SPAWNER:
578 		if (gCampaign.IsClient)
579 			break;
580 		// If counter -1, it is inactive i.e. already spawned
581 		if (obj->counter == -1)
582 		{
583 			break;
584 		}
585 		obj->counter -= ticks;
586 		if (obj->counter <= 0)
587 		{
588 			// Deactivate spawner by setting counter to -1
589 			obj->counter = -1;
590 			GameEvent e = GameEventNewActorAdd(
591 				obj->thing.Pos,
592 				CArrayGet(
593 					&gCampaign.Setting.characters.OtherChars,
594 					obj->Class->u.Character.CharId),
595 				true);
596 			e.u.ActorAdd.CharId = obj->Class->u.Character.CharId;
597 			GameEventsEnqueue(&gGameEvents, e);
598 
599 			// Destroy object
600 			// TODO: persistent actor spawners
601 			e = GameEventNew(GAME_EVENT_MAP_OBJECT_REMOVE);
602 			e.u.MapObjectRemove.UID = obj->uid;
603 			GameEventsEnqueue(&gGameEvents, e);
604 		}
605 		break;
606 	default:
607 		// Do nothing
608 		break;
609 	}
610 	CA_FOREACH_END()
611 }
612 
ObjGetByUID(const int uid)613 TObject *ObjGetByUID(const int uid)
614 {
615 	CA_FOREACH(TObject, o, gObjs)
616 	if (o->uid == uid)
617 	{
618 		return o;
619 	}
620 	CA_FOREACH_END()
621 	return NULL;
622 }
623 
MobObjsInit(void)624 void MobObjsInit(void)
625 {
626 	CArrayInit(&gMobObjs, sizeof(TMobileObject));
627 	CArrayReserve(&gMobObjs, 1024);
628 	sMobObjUIDs = 0;
629 }
MobObjsTerminate(void)630 void MobObjsTerminate(void)
631 {
632 	CA_FOREACH(TMobileObject, m, gMobObjs)
633 	if (m->isInUse)
634 	{
635 		BulletDestroy(m);
636 	}
637 	CA_FOREACH_END()
638 	CArrayTerminate(&gMobObjs);
639 }
MobObjsObjsGetNextUID(void)640 int MobObjsObjsGetNextUID(void)
641 {
642 	return sMobObjUIDs++;
643 }
MobObjGetByUID(const int uid)644 TMobileObject *MobObjGetByUID(const int uid)
645 {
646 	CA_FOREACH(TMobileObject, o, gMobObjs)
647 	if (o->UID == uid)
648 	{
649 		return o;
650 	}
651 	CA_FOREACH_END()
652 	return NULL;
653 }
654