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