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 "game.h"
50 
51 #include <assert.h>
52 
53 #include <cdogs/actor_placement.h>
54 #include <cdogs/actors.h>
55 #include <cdogs/ai.h>
56 #include <cdogs/ai_coop.h>
57 #include <cdogs/automap.h>
58 #include <cdogs/draw/drawtools.h>
59 #include <cdogs/events.h>
60 #include <cdogs/grafx_bg.h>
61 #include <cdogs/handle_game_events.h>
62 #include <cdogs/log.h>
63 #include <cdogs/los.h>
64 #include <cdogs/map_build.h>
65 #include <cdogs/music.h>
66 #include <cdogs/net_client.h>
67 #include <cdogs/net_server.h>
68 #include <cdogs/objs.h>
69 #include <cdogs/pickup.h>
70 
71 #include "briefing_screens.h"
72 #include "hiscores.h"
73 #include "screens_end.h"
74 
PlayerSpecialCommands(TActor * actor,const int cmd)75 static void PlayerSpecialCommands(TActor *actor, const int cmd)
76 {
77 	if ((cmd & CMD_BUTTON2) && CMD_HAS_DIRECTION(cmd))
78 	{
79 		if (ConfigGetEnum(&gConfig, "Game.SwitchMoveStyle") ==
80 				SWITCHMOVE_SLIDE &&
81 			actor->vehicleUID == -1)
82 		{
83 			SlideActor(actor, cmd);
84 		}
85 	}
86 	else if (
87 		!(actor->lastCmd & CMD_BUTTON2) && (cmd & CMD_BUTTON2) &&
88 		!actor->specialCmdDir && !actor->CanPickupSpecial &&
89 		!(ConfigGetEnum(&gConfig, "Game.SwitchMoveStyle") ==
90 			  SWITCHMOVE_SLIDE &&
91 		  CMD_HAS_DIRECTION(cmd)))
92 	{
93 		if (actor->vehicleUID >= 0)
94 		{
95 			// Dismount
96 			GameEvent e = GameEventNew(GAME_EVENT_ACTOR_PILOT);
97 			e.u.Pilot.On = false;
98 			e.u.Pilot.UID = actor->uid;
99 			e.u.Pilot.VehicleUID = actor->vehicleUID;
100 			GameEventsEnqueue(&gGameEvents, e);
101 		}
102 		else
103 		{
104 			const PlayerData *p = PlayerDataGetByUID(actor->PlayerUID);
105 			const bool allGuns = p == NULL || !PlayerHasGrenadeButton(p);
106 			ActorTrySwitchWeapon(actor, allGuns);
107 		}
108 	}
109 }
110 
111 // TODO: reimplement in camera
GetPlayerCenter(GraphicsDevice * device,const Camera * camera,const PlayerData * pData,const int playerIdx)112 struct vec2i GetPlayerCenter(
113 	GraphicsDevice *device, const Camera *camera, const PlayerData *pData,
114 	const int playerIdx)
115 {
116 	if (pData->ActorUID < 0)
117 	{
118 		// Player is dead
119 		return svec2i_zero();
120 	}
121 	struct vec2i center = svec2i_zero();
122 	int w = device->cachedConfig.Res.x;
123 	int h = device->cachedConfig.Res.y;
124 
125 	if (GetNumPlayers(PLAYER_ANY, true, true) == 1 ||
126 		GetNumPlayers(PLAYER_ANY, false, true) == 1 || CameraIsSingleScreen())
127 	{
128 		const struct vec2 pCenter = camera->lastPosition;
129 		const struct vec2i screenCenter =
130 			svec2i(w / 2, device->cachedConfig.Res.y / 2);
131 		const TActor *actor = ActorGetByUID(pData->ActorUID);
132 		const struct vec2 p = actor->thing.Pos;
133 		center = svec2i_add(
134 			svec2i_assign_vec2(svec2_subtract(p, pCenter)), screenCenter);
135 	}
136 	else
137 	{
138 		const int numLocalPlayers = GetNumPlayers(PLAYER_ANY, false, true);
139 		if (numLocalPlayers == 2)
140 		{
141 			center.x = playerIdx == 0 ? w / 4 : w * 3 / 4;
142 			center.y = h / 2;
143 		}
144 		else if (numLocalPlayers >= 3 && numLocalPlayers <= 4)
145 		{
146 			center.x = (playerIdx & 1) ? w * 3 / 4 : w / 4;
147 			center.y = (playerIdx >= 2) ? h * 3 / 4 : h / 4;
148 		}
149 		else
150 		{
151 			CASSERT(false, "invalid number of players");
152 		}
153 	}
154 	return center;
155 }
156 
157 static void RunGameTerminate(GameLoopData *data);
158 static void RunGameOnEnter(GameLoopData *data);
159 static void RunGameOnExit(GameLoopData *data);
160 static void RunGameInput(GameLoopData *data);
161 static GameLoopResult RunGameUpdate(GameLoopData *data, LoopRunner *l);
162 static void RunGameDraw(GameLoopData *data);
RunGame(Campaign * co,struct MissionOptions * m,Map * map)163 GameLoopData *RunGame(Campaign *co, struct MissionOptions *m, Map *map)
164 {
165 	RunGameData *data;
166 	CMALLOC(data, sizeof *data);
167 	GameInit(data, co, m, map);
168 	GameLoopData *g = GameLoopDataNew(
169 		data, RunGameTerminate, RunGameOnEnter, RunGameOnExit, RunGameInput,
170 		RunGameUpdate, RunGameDraw);
171 	g->FPS = ConfigGetInt(&gConfig, "Game.FPS");
172 	g->SuperhotMode = ConfigGetBool(&gConfig, "Game.Superhot(tm)Mode");
173 	g->InputEverySecondFrame = true;
174 	return g;
175 }
RunGameReset(RunGameData * rData)176 static void RunGameReset(RunGameData *rData)
177 {
178 	// Clear the background
179 	BlitFillBuf(&gGraphicsDevice, colorBlack);
180 	BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.bkg);
181 	CameraReset(&rData->Camera);
182 }
RunGameTerminate(GameLoopData * data)183 static void RunGameTerminate(GameLoopData *data)
184 {
185 	RunGameData *rData = data->Data;
186 
187 	CFREE(rData);
188 }
RunGameOnEnter(GameLoopData * data)189 static void RunGameOnEnter(GameLoopData *data)
190 {
191 	RunGameData *rData = data->Data;
192 
193 	RunGameReset(rData);
194 
195 	MapBuild(rData->map, rData->m->missionData, rData->co, rData->m->index);
196 
197 	// Seed random if PVP mode (otherwise players will always spawn in same
198 	// position)
199 	if (IsPVP(rData->co->Entry.Mode))
200 	{
201 		srand((unsigned int)time(NULL));
202 	}
203 
204 	if (!rData->co->IsClient)
205 	{
206 		// For PVP modes, mark all map as explored
207 		if (IsPVP(rData->co->Entry.Mode))
208 		{
209 			MapMarkAllAsVisited(rData->map);
210 		}
211 
212 		// Reset players for the mission
213 		CA_FOREACH(const PlayerData, p, gPlayerDatas)
214 		// Only reset for local players; for remote ones wait for the
215 		// client ready message
216 		if (!p->IsLocal)
217 			continue;
218 		GameEvent e = GameEventNew(GAME_EVENT_PLAYER_DATA);
219 		e.u.PlayerData = PlayerDataMissionReset(p);
220 		GameEventsEnqueue(&gGameEvents, e);
221 		CA_FOREACH_END()
222 		// Process the events to force add the players
223 		HandleGameEvents(&gGameEvents, NULL, NULL, NULL, NULL);
224 
225 		// Note: place players first,
226 		// as bad guys are placed away from players
227 		struct vec2 firstPos = svec2_zero();
228 		CA_FOREACH(const PlayerData, p, gPlayerDatas)
229 		if (!p->Ready)
230 			continue;
231 		firstPos = PlacePlayer(&gMap, p, firstPos, true);
232 		CA_FOREACH_END()
233 		if (!IsPVP(rData->co->Entry.Mode))
234 		{
235 			InitializeBadGuys();
236 			CreateEnemies();
237 		}
238 	}
239 
240 	CameraInit(&rData->Camera);
241 	// If there are no players, show the full map before starting
242 	if (GetNumPlayers(PLAYER_ANY, false, true) == 0)
243 	{
244 		LOSSetAllVisible(&rData->map->LOS);
245 		rData->Camera.lastPosition =
246 			Vec2CenterOfTile(svec2i_scale_divide(rData->map->Size, 2));
247 		rData->Camera.FollowNextPlayer = true;
248 	}
249 	if (rData->co->Setting.RandomPickups)
250 	{
251 		HealthSpawnerInit(&rData->healthSpawner, rData->map);
252 		CArrayInit(&rData->ammoSpawners, sizeof(PowerupSpawner));
253 		for (int i = 0; i < AmmoGetNumClasses(&gAmmo); i++)
254 		{
255 			PowerupSpawner ps;
256 			AmmoSpawnerInit(&ps, rData->map, i);
257 			CArrayPushBack(&rData->ammoSpawners, &ps);
258 		}
259 	}
260 
261 	rData->m->state = MISSION_STATE_WAITING;
262 	rData->m->isDone = false;
263 	rData->m->DoneCounter = 0;
264 	Pic *crosshair = PicManagerGetPic(&gPicManager, "crosshair");
265 	crosshair->offset.x = -crosshair->size.x / 2;
266 	crosshair->offset.y = -crosshair->size.y / 2;
267 	MouseSetPicCursor(
268 		&gEventHandlers.mouse, crosshair,
269 		PicManagerGetPic(&gPicManager, "crosshair_trail"));
270 
271 	NetServerSendGameStartMessages(&gNetServer, NET_SERVER_BCAST);
272 	GameEvent start = GameEventNew(GAME_EVENT_GAME_START);
273 	GameEventsEnqueue(&gGameEvents, start);
274 
275 	// Start of mission message
276 	GameEvent e = GameEventNew(GAME_EVENT_SET_MESSAGE);
277 	if (HasRounds(rData->co->Entry.Mode))
278 	{
279 		// Display which round it is
280 		int totalScores = 0;
281 		CA_FOREACH(const PlayerData, p, gPlayerDatas)
282 		totalScores += p->Totals.Score;
283 		CA_FOREACH_END()
284 		sprintf(e.u.SetMessage.Message, "Round %d", totalScores + 1);
285 	}
286 	else if (IsPVP(rData->co->Entry.Mode))
287 	{
288 		strcpy(e.u.SetMessage.Message, "Fight!");
289 	}
290 	else
291 	{
292 		// Show title of mission
293 		strncat(
294 			e.u.SetMessage.Message, rData->m->missionData->Title,
295 			sizeof e.u.SetMessage.Message - 1);
296 	}
297 	e.u.SetMessage.Ticks = 3000;
298 	GameEventsEnqueue(&gGameEvents, e);
299 }
RunGameOnExit(GameLoopData * data)300 static void RunGameOnExit(GameLoopData *data)
301 {
302 	RunGameData *rData = data->Data;
303 
304 	LOG(LM_MAIN, LL_INFO, "Game finished");
305 
306 	// Flush events
307 	HandleGameEvents(&gGameEvents, NULL, NULL, NULL, NULL);
308 
309 	PowerupSpawnerTerminate(&rData->healthSpawner);
310 	CA_FOREACH(PowerupSpawner, a, rData->ammoSpawners)
311 	PowerupSpawnerTerminate(a);
312 	CA_FOREACH_END()
313 	CArrayTerminate(&rData->ammoSpawners);
314 	CameraTerminate(&rData->Camera);
315 
316 	// Draw background
317 	GrafxRedrawBackground(&gGraphicsDevice, rData->Camera.lastPosition);
318 	// Clear other texures
319 	BlitClearBuf(&gGraphicsDevice);
320 	BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.hud);
321 	if (gGraphicsDevice.cachedConfig.SecondWindow)
322 	{
323 		BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.hud2);
324 	}
325 
326 	// Unready all the players
327 	CA_FOREACH(PlayerData, p, gPlayerDatas)
328 	p->Ready = false;
329 	CA_FOREACH_END()
330 	gNetClient.Ready = false;
331 
332 	// Calculate remaining health and survived
333 	CA_FOREACH(PlayerData, p, gPlayerDatas)
334 	p->survived = IsPlayerAlive(p);
335 	if (IsPlayerAlive(p))
336 	{
337 		const TActor *player = ActorGetByUID(p->ActorUID);
338 		p->hp = player->health;
339 	}
340 	CA_FOREACH_END()
341 }
RunGameInput(GameLoopData * data)342 static void RunGameInput(GameLoopData *data)
343 {
344 	RunGameData *rData = data->Data;
345 
346 	if (gEventHandlers.HasQuit)
347 	{
348 		GameEvent e = GameEventNew(GAME_EVENT_MISSION_END);
349 		e.u.MissionEnd.IsQuit = true;
350 		GameEventsEnqueue(&gGameEvents, e);
351 		return;
352 	}
353 
354 	int lastCmdAll = 0;
355 	for (int i = 0; i < MAX_LOCAL_PLAYERS; i++)
356 	{
357 		rData->lastCmds[i] = rData->cmds[i];
358 		lastCmdAll |= rData->lastCmds[i];
359 	}
360 	memset(rData->cmds, 0, sizeof rData->cmds);
361 	int cmdAll = 0;
362 	int idx = 0;
363 	input_device_e pausingDevice = INPUT_DEVICE_UNSET;
364 	input_device_e firstPausingDevice = INPUT_DEVICE_UNSET;
365 	if (GetNumPlayers(PLAYER_ANY, false, true) == 0)
366 	{
367 		// If no players, allow default keyboard to control camera
368 		rData->cmds[0] = GetKeyboardCmd(&gEventHandlers.keyboard, 0, false);
369 		firstPausingDevice = INPUT_DEVICE_KEYBOARD;
370 	}
371 	else
372 	{
373 		for (int i = 0; i < (int)gPlayerDatas.size; i++, idx++)
374 		{
375 			const PlayerData *p = CArrayGet(&gPlayerDatas, i);
376 			if (!p->IsLocal)
377 			{
378 				idx--;
379 				continue;
380 			}
381 			if (firstPausingDevice == INPUT_DEVICE_UNSET)
382 			{
383 				firstPausingDevice = p->inputDevice;
384 			}
385 			rData->cmds[idx] = GetGameCmd(
386 				&gEventHandlers, p,
387 				GetPlayerCenter(&gGraphicsDevice, &rData->Camera, p, idx));
388 			cmdAll |= rData->cmds[idx];
389 
390 			// Only allow the first player to escape
391 			// Use keypress otherwise the player will quit immediately
392 			if (idx == 0 && (rData->cmds[idx] & CMD_ESC) &&
393 				!(rData->lastCmds[idx] & CMD_ESC))
394 			{
395 				pausingDevice = p->inputDevice;
396 			}
397 		}
398 	}
399 	if (KeyIsPressed(&gEventHandlers.keyboard, SDL_SCANCODE_ESCAPE))
400 	{
401 		pausingDevice = INPUT_DEVICE_KEYBOARD;
402 	}
403 
404 	// Check if any controllers are unplugged
405 	rData->controllerUnplugged = false;
406 	CA_FOREACH(const PlayerData, p, gPlayerDatas)
407 	if (p->inputDevice == INPUT_DEVICE_UNSET && p->IsLocal)
408 	{
409 		rData->controllerUnplugged = true;
410 		break;
411 	}
412 	CA_FOREACH_END()
413 
414 	// If in Superhot(tm) Mode, don't update unless there was an input in this
415 	// or the last frame
416 	data->SkipNextFrame = data->SuperhotMode && !lastCmdAll;
417 
418 	// Check if:
419 	// - escape was pressed, or
420 	// - window lost focus
421 	// - controller unplugged
422 	// If the game is paused, unpause if a button is released
423 	// If the game was not paused, enter pause mode
424 	// If the game was paused and escape was pressed, exit the game
425 	if (rData->pausingDevice != INPUT_DEVICE_UNSET && AnyButton(lastCmdAll) &&
426 		!AnyButton(cmdAll))
427 	{
428 		rData->pausingDevice = INPUT_DEVICE_UNSET;
429 	}
430 	else if (rData->controllerUnplugged || gEventHandlers.HasLostFocus)
431 	{
432 		// Pause the game
433 		rData->pausingDevice = firstPausingDevice;
434 		rData->isMap = false;
435 		SoundPlay(&gSoundDevice, StrSound("menu_error"));
436 	}
437 	else if (pausingDevice != INPUT_DEVICE_UNSET)
438 	{
439 		if (rData->pausingDevice != INPUT_DEVICE_UNSET)
440 		{
441 			// Already paused; exit
442 			GameEvent e = GameEventNew(GAME_EVENT_MISSION_END);
443 			e.u.MissionEnd.IsQuit = true;
444 			GameEventsEnqueue(&gGameEvents, e);
445 			// Need to unpause to process the quit
446 			rData->pausingDevice = INPUT_DEVICE_UNSET;
447 			rData->controllerUnplugged = false;
448 			// Don't skip exiting the game
449 			data->SkipNextFrame = false;
450 			SoundPlay(&gSoundDevice, StrSound("menu_back"));
451 		}
452 		else
453 		{
454 			// Pause the game
455 			rData->pausingDevice = pausingDevice;
456 			rData->isMap = false;
457 			SoundPlay(&gSoundDevice, StrSound("menu_back"));
458 		}
459 	}
460 
461 	const bool paused = rData->pausingDevice != INPUT_DEVICE_UNSET ||
462 						rData->controllerUnplugged;
463 	if (!paused)
464 	{
465 		// Check if automap key is pressed by any player
466 		// Toggle
467 		if (IsAutoMapEnabled(gCampaign.Entry.Mode) &&
468 			(KeyIsPressed(
469 				 &gEventHandlers.keyboard,
470 				 ConfigGetInt(&gConfig, "Input.PlayerCodes0.map")) ||
471 			 ((cmdAll & CMD_MAP) && !(lastCmdAll & CMD_MAP))))
472 		{
473 			rData->isMap = !rData->isMap;
474 			SoundPlay(
475 				&gSoundDevice,
476 				StrSound(rData->isMap ? "map_open" : "map_close"));
477 		}
478 	}
479 
480 	CameraInput(&rData->Camera, rData->cmds[0], rData->lastCmds[0]);
481 }
482 static void NextLoop(RunGameData *rData, LoopRunner *l);
483 static void CheckMissionCompletion(const struct MissionOptions *mo);
RunGameUpdate(GameLoopData * data,LoopRunner * l)484 static GameLoopResult RunGameUpdate(GameLoopData *data, LoopRunner *l)
485 {
486 	RunGameData *rData = data->Data;
487 
488 	// Detect exit
489 	if (rData->m->isDone)
490 	{
491 		rData->m->DoneCounter--;
492 		if (rData->m->DoneCounter <= 0)
493 		{
494 			NextLoop(rData, l);
495 			return UPDATE_RESULT_OK;
496 		}
497 		else
498 		{
499 			return UPDATE_RESULT_DRAW;
500 		}
501 	}
502 
503 	// Check if game can begin
504 	if (!rData->m->HasBegun && MissionCanBegin())
505 	{
506 		GameEvent begin = GameEventNew(GAME_EVENT_GAME_BEGIN);
507 		begin.u.GameBegin.MissionTime = gMission.time;
508 		GameEventsEnqueue(&gGameEvents, begin);
509 	}
510 
511 	// Set mission complete and display exit if it is complete
512 	MissionSetMessageIfComplete(rData->m);
513 
514 	// If we're not hosting a net game,
515 	// don't update if the game has paused or has automap shown
516 	// Important: don't consider paused if we are trying to quit
517 	const bool paused = rData->pausingDevice != INPUT_DEVICE_UNSET ||
518 						rData->controllerUnplugged || rData->isMap;
519 	if (!gCampaign.IsClient && !ConfigGetBool(&gConfig, "StartServer") &&
520 		paused && !gEventHandlers.HasQuit)
521 	{
522 		return UPDATE_RESULT_DRAW;
523 	}
524 
525 	if (data->SkipNextFrame)
526 	{
527 		return UPDATE_RESULT_DRAW;
528 	}
529 
530 	// If split screen never and players are too close to the
531 	// edge of the screen, forcefully pull them towards the center
532 	if (ConfigGetEnum(&gConfig, "Interface.Splitscreen") ==
533 			SPLITSCREEN_NEVER &&
534 		GetNumPlayers(PLAYER_ALIVE_OR_DYING, true, true) > 1 &&
535 		!IsPVP(gCampaign.Entry.Mode))
536 	{
537 		const int w = gGraphicsDevice.cachedConfig.Res.x;
538 		const int h = gGraphicsDevice.cachedConfig.Res.y;
539 		const struct vec2i screen = svec2i_add(
540 			svec2i_assign_vec2(PlayersGetMidpoint()), svec2i(-w / 2, -h / 2));
541 		CA_FOREACH(const PlayerData, pd, gPlayerDatas)
542 		if (!pd->IsLocal || !IsPlayerAlive(pd))
543 		{
544 			continue;
545 		}
546 		const TActor *p = ActorGetByUID(pd->ActorUID);
547 		const int pad = CAMERA_SPLIT_PADDING;
548 		struct vec2 vel = svec2_zero();
549 		if (screen.x + pad > p->thing.Pos.x && p->thing.Vel.x < 1)
550 		{
551 			vel.x = screen.x + pad - p->thing.Pos.x;
552 		}
553 		else if (screen.x + w - pad < p->thing.Pos.x && p->thing.Vel.x > -1)
554 		{
555 			vel.x = screen.x + w - pad - p->thing.Pos.x;
556 		}
557 		if (screen.y + pad > p->thing.Pos.y && p->thing.Vel.y < 1)
558 		{
559 			vel.y = screen.y + pad - p->thing.Pos.y;
560 		}
561 		else if (screen.y + h - pad < p->thing.Pos.y && p->thing.Vel.y > -1)
562 		{
563 			vel.y = screen.y + h - pad - p->thing.Pos.y;
564 		}
565 		if (!svec2_is_zero(vel))
566 		{
567 			GameEvent ei = GameEventNew(GAME_EVENT_ACTOR_IMPULSE);
568 			ei.u.ActorImpulse.UID = p->uid;
569 			ei.u.ActorImpulse.Vel = Vec2ToNet(svec2_scale(vel, 0.25f));
570 			ei.u.ActorImpulse.Pos = Vec2ToNet(svec2_zero());
571 			GameEventsEnqueue(&gGameEvents, ei);
572 			LOG(LM_MAIN, LL_TRACE,
573 				"playerUID(%d) pos(%f, %f) screen(%d, %d) impulse(%f, %f)",
574 				p->uid, p->thing.Pos.x, p->thing.Pos.y, screen.x, screen.y,
575 				ei.u.ActorImpulse.Vel.x, ei.u.ActorImpulse.Vel.y);
576 		}
577 		CA_FOREACH_END()
578 	}
579 
580 	const int ticksPerFrame = 1;
581 
582 	if (gPlayerDatas.size > 0)
583 	{
584 		LOSReset(&gMap.LOS);
585 		for (int i = 0, idx = 0; i < (int)gPlayerDatas.size; i++, idx++)
586 		{
587 			const PlayerData *p = CArrayGet(&gPlayerDatas, i);
588 			if (p->ActorUID == -1)
589 				continue;
590 			TActor *player = ActorGetByUID(p->ActorUID);
591 
592 			// Calculate LOS for all players alive or dying
593 			LOSCalcFrom(
594 				&gMap, Vec2ToTile(player->thing.Pos), !gCampaign.IsClient);
595 
596 			if (player->dead)
597 				continue;
598 
599 			// Only handle inputs/commands for local players
600 			if (!p->IsLocal)
601 			{
602 				idx--;
603 				continue;
604 			}
605 			if (p->inputDevice == INPUT_DEVICE_AI)
606 			{
607 				rData->cmds[idx] = AICoopGetCmd(player, ticksPerFrame);
608 			}
609 			PlayerSpecialCommands(player, rData->cmds[idx]);
610 			CommandActor(player, rData->cmds[idx], ticksPerFrame);
611 		}
612 	}
613 
614 	// Disable sounds on the first frame
615 	GameUpdate(rData, ticksPerFrame, data->Frames == 0 ? NULL : &gSoundDevice);
616 
617 	CameraUpdate(&rData->Camera, ticksPerFrame, 1000 / data->FPS);
618 
619 	return UPDATE_RESULT_DRAW;
620 }
621 static void PersistPlayerWeaponsAndAmmo(PlayerData *p);
NextLoop(RunGameData * rData,LoopRunner * l)622 static void NextLoop(RunGameData *rData, LoopRunner *l)
623 {
624 	// Find the next screen to switch to
625 	const bool hasLocalPlayers = GetNumPlayers(PLAYER_ANY, false, true) > 0;
626 	const int survivingPlayers = GetNumPlayers(PLAYER_ALIVE, false, false);
627 	const bool survivedAndCompletedObjectives =
628 		survivingPlayers > 0 && MissionAllObjectivesComplete(&gMission);
629 	// Persist player weapons/ammo
630 	CA_FOREACH(PlayerData, p, gPlayerDatas)
631 	PersistPlayerWeaponsAndAmmo(p);
632 	CA_FOREACH_END()
633 
634 	// Switch to a score screen if there are local players and we haven't quit
635 	const bool showScores = !rData->co->IsQuit && hasLocalPlayers;
636 	if (showScores)
637 	{
638 		switch (rData->co->Entry.Mode)
639 		{
640 		case GAME_MODE_DOGFIGHT:
641 			LoopRunnerChange(l, ScreenDogfightScores());
642 			break;
643 		case GAME_MODE_DEATHMATCH:
644 			LoopRunnerChange(l, ScreenDeathmatchFinalScores());
645 			break;
646 		default:
647 			// In co-op (non-PVP) modes, at least one player must survive
648 			LoopRunnerChange(
649 				l, ScreenMissionSummary(
650 					   rData->co, rData->m, survivedAndCompletedObjectives));
651 			break;
652 		}
653 	}
654 	else
655 	{
656 		LoopRunnerChange(l, HighScoresScreen(rData->co, &gGraphicsDevice));
657 	}
658 	if (!HasRounds(rData->co->Entry.Mode) && !rData->co->IsComplete)
659 	{
660 		rData->co->MissionIndex = rData->m->NextMission;
661 	}
662 }
PersistPlayerWeaponsAndAmmo(PlayerData * p)663 static void PersistPlayerWeaponsAndAmmo(PlayerData *p)
664 {
665 	if (!IsPlayerAlive(p))
666 		return;
667 	const TActor *a = ActorGetByUID(p->ActorUID);
668 	for (int i = 0; i < MAX_WEAPONS; i++)
669 	{
670 		p->guns[i] = a->guns[i].Gun;
671 	}
672 	CArrayCopy(&p->ammo, &a->ammo);
673 }
CheckMissionCompletion(const struct MissionOptions * mo)674 static void CheckMissionCompletion(const struct MissionOptions *mo)
675 {
676 	// Check if we need to update explore objectives
677 	CA_FOREACH(const Objective, o, mo->missionData->Objectives)
678 	if (o->Type != OBJECTIVE_INVESTIGATE)
679 		continue;
680 	const int update = MapGetExploredPercentage(&gMap) - o->done;
681 	if (update > 0 && !gCampaign.IsClient)
682 	{
683 		GameEvent e = GameEventNew(GAME_EVENT_OBJECTIVE_UPDATE);
684 		e.u.ObjectiveUpdate.ObjectiveId = _ca_index;
685 		e.u.ObjectiveUpdate.Count = update;
686 		GameEventsEnqueue(&gGameEvents, e);
687 	}
688 	CA_FOREACH_END()
689 
690 	const bool complete =
691 		GetNumPlayers(PLAYER_ALIVE_OR_DYING, false, false) > 0 &&
692 		IsMissionComplete(mo);
693 	const int exitIndex = AllSurvivingPlayersInSameExit();
694 	const bool canExit = complete && (!MapHasExits(&gMap) || exitIndex >= 0);
695 	if (mo->state == MISSION_STATE_PLAY && canExit)
696 	{
697 		GameEvent e = GameEventNew(GAME_EVENT_MISSION_PICKUP);
698 		GameEventsEnqueue(&gGameEvents, e);
699 	}
700 	if (mo->state == MISSION_STATE_PICKUP && !canExit)
701 	{
702 		GameEvent e = GameEventNew(GAME_EVENT_MISSION_INCOMPLETE);
703 		GameEventsEnqueue(&gGameEvents, e);
704 	}
705 	if (mo->state == MISSION_STATE_PICKUP &&
706 		mo->pickupTime + PICKUP_LIMIT <= mo->time)
707 	{
708 		GameEvent e = GameEventNew(GAME_EVENT_MISSION_END);
709 		if (exitIndex >= 0)
710 		{
711 			const Exit *exit = CArrayGet(&gMap.exits, exitIndex);
712 			e.u.MissionEnd.Mission = exit->Mission;
713 		}
714 		else
715 		{
716 			e.u.MissionEnd.Mission = mo->index + 1;
717 		}
718 		GameEventsEnqueue(&gGameEvents, e);
719 	}
720 
721 	// Check that all players have been destroyed
722 	// If the server has no players at all, wait for a player to join
723 	if (gPlayerDatas.size > 0)
724 	{
725 		// Note: there's a period of time where players are dying
726 		// Wait until after this period before ending the game
727 		bool allPlayersDestroyed = true;
728 		CA_FOREACH(const PlayerData, p, gPlayerDatas)
729 		if (p->ActorUID != -1)
730 		{
731 			allPlayersDestroyed = false;
732 			break;
733 		}
734 		CA_FOREACH_END()
735 		if (allPlayersDestroyed && AreAllPlayersDeadAndNoLives())
736 		{
737 			GameEvent e = GameEventNew(GAME_EVENT_MISSION_END);
738 			e.u.MissionEnd.Delay = GAME_OVER_DELAY;
739 			e.u.MissionEnd.Mission = mo->index;
740 			GameEventsEnqueue(&gGameEvents, e);
741 		}
742 	}
743 }
RunGameDraw(GameLoopData * data)744 static void RunGameDraw(GameLoopData *data)
745 {
746 	RunGameData *rData = data->Data;
747 
748 	// Draw game layer
749 	BlitClearBuf(&gGraphicsDevice);
750 	CameraDraw(&rData->Camera, rData->Camera.HUD.DrawData);
751 	BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.screen);
752 
753 	// Draw HUD layer
754 	BlitClearBuf(&gGraphicsDevice);
755 	CameraDrawMode(&rData->Camera);
756 	HUDDraw(
757 		&rData->Camera.HUD, rData->pausingDevice, rData->controllerUnplugged,
758 		rData->Camera.NumViews);
759 	const bool isMouse = GameIsMouseUsed();
760 	if (isMouse)
761 	{
762 		MouseDraw(&gEventHandlers.mouse);
763 	}
764 	// Draw automap if enabled
765 	if (rData->isMap)
766 	{
767 		AutomapDraw(
768 			gGraphicsDevice.gameWindow.renderer, 0,
769 			rData->Camera.HUD.showExit);
770 	}
771 	BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.hud);
772 
773 	if (gGraphicsDevice.cachedConfig.SecondWindow)
774 	{
775 		BlitClearBuf(&gGraphicsDevice);
776 		if (IsAutoMapEnabled(gCampaign.Entry.Mode))
777 		{
778 			AutomapDraw(
779 				gGraphicsDevice.secondWindow.renderer, 0,
780 				rData->Camera.HUD.showExit);
781 		}
782 		BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.hud2);
783 	}
784 }
785 
GameInit(RunGameData * data,Campaign * co,struct MissionOptions * m,Map * map)786 void GameInit(
787 	RunGameData *data, Campaign *co, struct MissionOptions *m, Map *map)
788 {
789 	memset(data, 0, sizeof *data);
790 	data->co = co;
791 	data->m = m;
792 	data->map = map;
793 }
794 
GameUpdate(RunGameData * data,const int ticksPerFrame,SoundDevice * sd)795 void GameUpdate(RunGameData *data, const int ticksPerFrame, SoundDevice *sd)
796 {
797 	// Update all the things in the game
798 
799 	if (!gCampaign.IsClient)
800 	{
801 		data->aiUpdateCounter -= ticksPerFrame;
802 		if (data->aiUpdateCounter <= 0)
803 		{
804 			const int enemies = AICommand(ticksPerFrame);
805 			AIAddRandomEnemies(enemies, data->m->missionData);
806 			data->aiUpdateCounter = 4;
807 		}
808 		else
809 		{
810 			AICommandLast(ticksPerFrame);
811 		}
812 	}
813 
814 	UpdateAllActors(ticksPerFrame);
815 	UpdateObjects(ticksPerFrame);
816 	UpdateMobileObjects(ticksPerFrame);
817 	PickupsUpdate(&gPickups, ticksPerFrame);
818 	ParticlesUpdate(&gParticles, ticksPerFrame);
819 
820 	UpdateWatches(&data->map->triggers, ticksPerFrame);
821 
822 	PowerupSpawnerUpdate(&data->healthSpawner, ticksPerFrame);
823 	CA_FOREACH(PowerupSpawner, a, data->ammoSpawners)
824 	PowerupSpawnerUpdate(a, ticksPerFrame);
825 	CA_FOREACH_END()
826 
827 	if (!gCampaign.IsClient)
828 	{
829 		CheckMissionCompletion(data->m);
830 	}
831 	else if (!NetClientIsConnected(&gNetClient))
832 	{
833 		// Check if disconnected from server; end mission
834 		const NMissionEnd me = NMissionEnd_init_zero;
835 		MissionDone(&gMission, me);
836 	}
837 
838 	HandleGameEvents(
839 		&gGameEvents, &data->Camera, &data->healthSpawner, &data->ammoSpawners,
840 		sd);
841 
842 	data->m->time += ticksPerFrame;
843 
844 	if (gEventHandlers.HasResolutionChanged)
845 	{
846 		RunGameReset(data);
847 	}
848 }
849