1 /** @file d_net.cpp  Common code related to net games.
2  *
3  * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2006-2014 Daniel Swanson <danij@dengine.net>
5  * @authors Copyright © 2007 Jamie Jones <jamie_jones_au@yahoo.com.au>
6  *
7  * @par License
8  * GPL: http://www.gnu.org/licenses/gpl.html
9  *
10  * <small>This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by the
12  * Free Software Foundation; either version 2 of the License, or (at your
13  * option) any later version. This program is distributed in the hope that it
14  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
15  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
16  * Public License for more details. You should have received a copy of the GNU
17  * General Public License along with this program; if not, see:
18  * http://www.gnu.org/licenses</small>
19  */
20 
21 #include "common.h"
22 #include "d_net.h"
23 
24 #include <de/RecordValue>
25 #include "d_netcl.h"
26 #include "d_netsv.h"
27 #include "fi_lib.h"
28 #include "g_common.h"
29 #include "g_defs.h"
30 #include "gamesession.h"
31 #include "hu_menu.h"
32 #include "p_mapsetup.h"
33 #include "p_start.h"
34 #include "player.h"
35 
36 using namespace de;
37 using namespace common;
38 
39 D_CMD(SetColor);
40 #if __JHEXEN__
41 D_CMD(SetClass);
42 #endif
43 D_CMD(LocalMessage);
44 
45 static void D_NetMessageEx(int player, char const *msg, dd_bool playSound);
46 
47 float netJumpPower = 9;
48 
49 static writer_s *netWriter;
50 static reader_s *netReader;
51 
notifyAllowCheatsChange()52 static void notifyAllowCheatsChange()
53 {
54     if(IS_NETGAME && IS_NETWORK_SERVER && G_GameState() != GS_STARTUP)
55     {
56         String const msg = String("--- CHEATS NOW %1 ON THIS SERVER ---")
57                                .arg(netSvAllowCheats? "ENABLED" : "DISABLED");
58         NetSv_SendMessage(DDSP_ALL_PLAYERS, msg.toUtf8().constData());
59     }
60 }
61 
D_NetDefaultEpisode()62 String D_NetDefaultEpisode()
63 {
64     return FirstPlayableEpisodeId();
65 }
66 
D_NetDefaultMap()67 de::Uri D_NetDefaultMap()
68 {
69     String const episodeId = D_NetDefaultEpisode();
70 
71     de::Uri map("Maps:", RC_NULL);
72     if(!episodeId.isEmpty())
73     {
74         map = de::makeUri(Defs().episodes.find("id", episodeId).gets("startMap"));
75         DENG2_ASSERT(!map.isEmpty());
76     }
77     return map;
78 }
79 
D_NetConsoleRegister()80 void D_NetConsoleRegister()
81 {
82     C_VAR_CHARPTR("mapcycle",           &mapCycle,  CVF_HIDE | CVF_NO_ARCHIVE, 0, 0);
83 
84     C_CMD        ("setcolor",   "i",    SetColor);
85 #if __JHEXEN__
86     C_CMD_FLAGS  ("setclass",   "i",    SetClass,   CMDF_NO_DEDICATED);
87 #endif
88     C_CMD        ("startcycle", "",     MapCycle);
89     C_CMD        ("endcycle",   "",     MapCycle);
90     C_CMD        ("message",    "s",    LocalMessage);
91 
92     if(IS_DEDICATED)
93     {
94         C_VAR_CHARPTR("server-game-episode",    &cfg.common.netEpisode,    0, 0, 0);
95         C_VAR_URIPTR ("server-game-map",        &cfg.common.netMap,        0, 0, 0);
96 
97         // Use the first playable map as the default.
98         String episodeId = D_NetDefaultEpisode();
99         de::Uri map      = D_NetDefaultMap();
100 
101         Con_SetString("server-game-episode", episodeId.toUtf8().constData());
102         Con_SetUri   ("server-game-map",     reinterpret_cast<uri_s *>(&map));
103     }
104 
105     /// @todo "server-*" cvars should only be registered by dedicated servers.
106     //if(IS_DEDICATED) return;
107 
108 #if !__JHEXEN__
109     C_VAR_BYTE   ("server-game-announce-secret",            &cfg.secretMsg,                         0, 0, 1);
110 #endif
111 #if __JDOOM__ || __JDOOM64__
112     C_VAR_BYTE   ("server-game-bfg-freeaim",                &cfg.netBFGFreeLook,                    0, 0, 1);
113 #endif
114     C_VAR_INT2   ("server-game-cheat",                      &netSvAllowCheats,                      0, 0, 1, notifyAllowCheatsChange);
115 #if __JDOOM__ || __JDOOM64__
116     C_VAR_BYTE   ("server-game-deathmatch",                 &cfg.common.netDeathmatch,              0, 0, 2);
117 #else
118     C_VAR_BYTE   ("server-game-deathmatch",                 &cfg.common.netDeathmatch,              0, 0, 1);
119 #endif
120     C_VAR_BYTE   ("server-game-jump",                       &cfg.common.netJumping,                 0, 0, 1);
121     C_VAR_CHARPTR("server-game-mapcycle",                   &mapCycle,                              0, 0, 0);
122     C_VAR_BYTE   ("server-game-mapcycle-noexit",            &mapCycleNoExit,                        0, 0, 1);
123 #if __JHERETIC__
124     C_VAR_BYTE   ("server-game-maulotaur-fixfloorfire",     &cfg.fixFloorFire,                      0, 0, 1);
125 #endif
126     C_VAR_BYTE   ("server-game-monster-meleeattack-nomaxz", &cfg.common.netNoMaxZMonsterMeleeAttack,0, 0, 1);
127 #if __JDOOM__ || __JDOOM64__
128     C_VAR_BYTE   ("server-game-nobfg",                      &cfg.noNetBFG,                          0, 0, 1);
129 #endif
130     C_VAR_BYTE   ("server-game-nomonsters",                 &cfg.common.netNoMonsters,              0, 0, 1);
131 #if !__JHEXEN__
132     C_VAR_BYTE   ("server-game-noteamdamage",               &cfg.noTeamDamage,                      0, 0, 1);
133 #endif
134 #if __JHERETIC__
135     C_VAR_BYTE   ("server-game-plane-fixmaterialscroll",    &cfg.fixPlaneScrollMaterialsEastOnly,   0, 0, 1);
136 #endif
137     C_VAR_BYTE   ("server-game-radiusattack-nomaxz",        &cfg.common.netNoMaxZRadiusAttack,      0, 0, 1);
138 #if __JHEXEN__
139     C_VAR_BYTE   ("server-game-randclass",                  &cfg.netRandomClass,                    0, 0, 1);
140 #endif
141 #if !__JHEXEN__
142     C_VAR_BYTE   ("server-game-respawn",                    &cfg.netRespawn,                        0, 0, 1);
143 #endif
144 #if __JDOOM__ || __JHERETIC__
145     C_VAR_BYTE   ("server-game-respawn-monsters-nightmare", &cfg.respawnMonstersNightmare,          0, 0, 1);
146 #endif
147     C_VAR_BYTE   ("server-game-skill",                      &cfg.common.netSkill,                   0, 0, 4);
148 
149     // Modifiers:
150     C_VAR_BYTE   ("server-game-mod-damage",                 &cfg.common.netMobDamageModifier,       0, 1, 100);
151     C_VAR_INT    ("server-game-mod-gravity",                &cfg.common.netGravity,                 0, -1, 100);
152     C_VAR_BYTE   ("server-game-mod-health",                 &cfg.common.netMobHealthModifier,       0, 1, 20);
153 
154     // Coop:
155 #if !__JHEXEN__
156     C_VAR_BYTE   ("server-game-coop-nodamage",              &cfg.noCoopDamage,                      0, 0, 1);
157 #endif
158 #if __JDOOM__ || __JDOOM64__
159     //C_VAR_BYTE   ("server-game-coop-nothing",               &cfg.noCoopAnything,                    0, 0, 1); // not implemented atm, see P_SpawnMobjXYZ
160     C_VAR_BYTE   ("server-game-coop-noweapons",             &cfg.noCoopWeapons,                     0, 0, 1);
161     C_VAR_BYTE   ("server-game-coop-respawn-items",         &cfg.coopRespawnItems,                  0, 0, 1);
162 #endif
163 
164     // Deathmatch:
165 #if __JDOOM__ || __JDOOM64__
166     C_VAR_BYTE   ("server-game-deathmatch-killmsg",         &cfg.killMessages,                      0, 0, 1);
167 #endif
168 }
169 
D_NetWrite()170 writer_s *D_NetWrite()
171 {
172     if(netWriter)
173     {
174         Writer_Delete(netWriter);
175     }
176     netWriter = Writer_NewWithDynamicBuffer(0 /*unlimited*/);
177     return netWriter;
178 }
179 
D_NetRead(byte const * buffer,size_t len)180 reader_s *D_NetRead(byte const *buffer, size_t len)
181 {
182     // Get rid of the old reader.
183     if(netReader)
184     {
185         Reader_Delete(netReader);
186     }
187     netReader = Reader_NewWithBuffer(buffer, len);
188     return netReader;
189 }
190 
D_NetClearBuffer()191 void D_NetClearBuffer()
192 {
193     if(netReader) Reader_Delete(netReader);
194     if(netWriter) Writer_Delete(netWriter);
195 
196     netReader = 0;
197     netWriter = 0;
198 }
199 
D_NetServerStarted(int before)200 int D_NetServerStarted(int before)
201 {
202     if(before) return true;
203 
204     // We're the server, so...
205     ::cfg.playerColor[0] = PLR_COLOR(0, ::cfg.common.netColor);
206 
207 #if __JHEXEN__
208     ::cfg.playerClass[0] = playerclass_t(::cfg.netClass);
209 #elif __JHERETIC__
210     ::cfg.playerClass[0] = PCLASS_PLAYER;
211 #endif
212     P_ResetPlayerRespawnClasses();
213 
214     String episodeId = Con_GetString("server-game-episode");
215     de::Uri mapUri = *reinterpret_cast<de::Uri const *>(Con_GetUri("server-game-map"));
216     if(mapUri.scheme().isEmpty()) mapUri.setScheme("Maps");
217 
218     GameRules rules(gfw_Session()->rules()); // Make a copy of the current rules.
219     GameRules_Set(rules, skill, skillmode_t(cfg.common.netSkill));
220 
221     gfw_Session()->end();
222 
223     try
224     {
225         // First try the configured map.
226         gfw_Session()->begin(rules, episodeId, mapUri);
227     }
228     catch(Error const &er)
229     {
230         LOGDEV_ERROR("Failed to start server: %s") << er.asText();
231         episodeId = D_NetDefaultEpisode();
232         mapUri    = D_NetDefaultMap();
233         LOG_INFO("Using the default map (%s) to start the server due to failure to load the configured map")
234                 << mapUri;
235 
236         gfw_Session()->begin(rules, episodeId, mapUri);
237     }
238 
239     G_SetGameAction(GA_NONE); /// @todo Necessary?
240 
241     return true;
242 }
243 
D_NetServerClose(int before)244 int D_NetServerClose(int before)
245 {
246     if (!before)
247     {
248         P_ResetPlayerRespawnClasses();
249 
250         // Restore normal game state.
251         /// @todo fixme: "normal" is defined by the game rules config which may
252         /// be changed from the command line (e.g., -fast, -nomonsters).
253         /// In order to "restore normal" this logic is insufficient.
254         GameRules newRules(gfw_Session()->rules());
255         GameRules_Set(newRules, deathmatch, 0);
256         GameRules_Set(newRules, noMonsters, false);
257 #if __JHEXEN__
258         GameRules_Set(newRules, randomClasses, false);
259 #endif
260         gfw_Session()->applyNewRules(newRules);
261 
262         D_NetMessage(CONSOLEPLAYER, "NETGAME ENDS");
263         D_NetClearBuffer();
264     }
265     return true;
266 }
267 
D_NetConnect(int before)268 int D_NetConnect(int before)
269 {
270     if(before)
271     {
272         BusyMode_FreezeGameForBusyMode();
273         return true;
274     }
275 
276     // After connecting we tell the server a bit about ourselves.
277     NetCl_SendPlayerInfo();
278 
279     // Close the menu, the game begins!!
280     //  advancedemo = false;
281     Hu_MenuCommand(MCMD_CLOSE);
282     return true;
283 }
284 
D_NetDisconnect(int before)285 int D_NetDisconnect(int before)
286 {
287     if(before)
288     {
289         // Free PU_MAP, Zone-allocated storage for the local world state.
290         P_ResetWorldState();
291         return true;
292     }
293 
294     D_NetClearBuffer();
295 
296     // Start demo.
297     gfw_Session()->endAndBeginTitle();
298 
299     /*GameRules newRules(gfw_Session()->rules());
300     newRules.deathmatch    = false;
301     newRules.noMonsters    = false;
302 #if __JHEXEN__
303     newRules.randomClasses = false;
304 #endif
305     gfw_Session()->applyNewRules(newRules);*/
306 
307     return true;
308 }
309 
D_NetPlayerEvent(int plrNumber,int peType,void * data)310 long int D_NetPlayerEvent(int plrNumber, int peType, void *data)
311 {
312     // If this isn't a netgame, we won't react.
313     if(!IS_NETGAME)
314         return true;
315 
316     if(peType == DDPE_ARRIVAL)
317     {
318         dd_bool showmsg = true;
319 
320         if(IS_SERVER)
321         {
322             NetSv_NewPlayerEnters(plrNumber);
323         }
324         else if(plrNumber == CONSOLEPLAYER)
325         {
326             // We have arrived, the game should be begun.
327             App_Log(DE2_NET_NOTE, "Arrived in netgame, waiting for data...");
328             G_ChangeGameState(GS_WAITING);
329             showmsg = false;
330         }
331         else
332         {
333             // Client responds to new player?
334             App_Log(DE2_LOG_NOTE, "Player %i has arrived in the game", plrNumber);
335             P_RebornPlayerInMultiplayer(plrNumber);
336             //players[plrNumber].playerstate = PST_REBORN;
337         }
338         if(showmsg)
339         {
340             // Print a notification.
341             AutoStr *str = AutoStr_New();
342             Str_Appendf(str, "%s joined the game", Net_GetPlayerName(plrNumber));
343             D_NetMessage(CONSOLEPLAYER, Str_Text(str));
344         }
345     }
346     else if(peType == DDPE_EXIT)
347     {
348         AutoStr *str = AutoStr_New();
349 
350         App_Log(DE2_LOG_NOTE, "Player %i has left the game", plrNumber);
351 
352         players[plrNumber].playerState = playerstate_t(PST_GONE);
353 
354         // Print a notification.
355         Str_Appendf(str, "%s left the game", Net_GetPlayerName(plrNumber));
356         D_NetMessage(CONSOLEPLAYER, Str_Text(str));
357 
358         if(IS_SERVER)
359         {
360             P_DealPlayerStarts(0);
361         }
362     }
363     // DDPE_CHAT_MESSAGE occurs when a PKT_CHAT is received.
364     // Here we will only display the message.
365     else if(peType == DDPE_CHAT_MESSAGE)
366     {
367         int oldecho = cfg.common.echoMsg;
368         AutoStr *msg = AutoStr_New();
369 
370         if(plrNumber > 0)
371         {
372             Str_Appendf(msg, "%s: %s", Net_GetPlayerName(plrNumber), (char const *) data);
373         }
374         else
375         {
376             Str_Appendf(msg, "[sysop] %s", (char const *) data);
377         }
378         Str_Truncate(msg, NETBUFFER_MAXMESSAGE); // not overly long, please
379 
380         // The chat message is already echoed by the console.
381         cfg.common.echoMsg = false;
382         D_NetMessageEx(CONSOLEPLAYER, Str_Text(msg), (cfg.common.chatBeep? true : false));
383         cfg.common.echoMsg = oldecho;
384     }
385 
386     return true;
387 }
388 
D_NetWorldEvent(int type,int parm,void * data)389 int D_NetWorldEvent(int type, int parm, void *data)
390 {
391     switch(type)
392     {
393     //
394     // Server events:
395     //
396     case DDWE_HANDSHAKE: {
397         dd_bool newPlayer = *((dd_bool*) data);
398 
399         // A new player is entering the game. We as a server should send him
400         // the handshake packet(s) to update his world.
401         // If 'data' is zero, this is a re-handshake that's used to
402         // begin demos.
403         App_Log(DE2_DEV_NET_MSG, "Sending a game state %shandshake to player %i",
404                 newPlayer? "" : "(re)", parm);
405 
406         // Mark new player for update.
407         players[parm].update |= PSF_REBORN;
408 
409         // First, the game state.
410         NetSv_SendGameState(GSF_CHANGE_MAP | GSF_CAMERA_INIT | (newPlayer ? 0 : GSF_DEMO), parm);
411 
412         // Send info about all players to the new one.
413         for(int i = 0; i < MAXPLAYERS; ++i)
414         {
415             if(players[i].plr->inGame && i != parm)
416                 NetSv_SendPlayerInfo(i, parm);
417         }
418 
419         // Send info about our jump power.
420         NetSv_SendJumpPower(parm, cfg.common.jumpEnabled? cfg.common.jumpPower : 0);
421         NetSv_Paused(paused);
422         break; }
423 
424     //
425     // Client events:
426     //
427 #if 0
428     case DDWE_SECTOR_SOUND:
429         // High word: sector number, low word: sound id.
430         if(parm & 0xffff)
431             S_SectorSound(P_ToPtr(DMU_SECTOR, parm >> 16), parm & 0xffff);
432         else
433             S_SectorStopSounds(P_ToPtr(DMU_SECTOR, parm >> 16));
434 
435         break;
436 
437     case DDWE_DEMO_END:
438         // Demo playback has ended. Advance demo sequence.
439         if(parm)                // Playback was aborted.
440             G_DemoAborted();
441         else                    // Playback ended normally.
442             G_DemoEnds();
443 
444         // Restore normal game state.
445         gfw_Rule(deathmatch) = false;
446         gfw_Session()->rules().noMonsters = false;
447 #if __JDOOM__ || __JHERETIC__ || __JDOOM64__
448         gfw_Rule(espawnMonsters) = false;
449 #endif
450 
451 #if __JHEXEN__
452         gfw_Session()->rules().randomClasses = false;
453 #endif
454         break;
455 #endif
456 
457     default:
458         return false;
459     }
460 
461     return true;
462 }
463 
D_HandlePacket(int fromplayer,int type,void * data,size_t length)464 void D_HandlePacket(int fromplayer, int type, void *data, size_t length)
465 {
466     reader_s *reader = D_NetRead((byte *)data, length);
467 
468     //
469     // Server events.
470     //
471     if(IS_SERVER)
472     {
473         switch (type)
474         {
475         case GPT_PLAYER_INFO:
476             // A player has changed color or other settings.
477             NetSv_ChangePlayerInfo(fromplayer, reader);
478             break;
479 
480         case GPT_CHEAT_REQUEST:
481             NetSv_DoCheat(fromplayer, reader);
482             break;
483 
484         case GPT_FLOOR_HIT_REQUEST:
485             NetSv_DoFloorHit(fromplayer, reader);
486             break;
487 
488         case GPT_ACTION_REQUEST:
489             NetSv_DoAction(fromplayer, reader);
490             break;
491 
492         case GPT_DAMAGE_REQUEST:
493             NetSv_DoDamage(fromplayer, reader);
494             break;
495         }
496         return;
497     }
498     //
499     // Client events.
500     //
501     switch(type)
502     {
503     case GPT_GAME_STATE:
504         App_Log(DE2_DEV_NET_MSG, "Received GTP_GAME_STATE");
505         NetCl_UpdateGameState(reader);
506 
507         // Tell the engine we're ready to proceed. It'll start handling
508         // the world updates after this variable is set.
509         Set(DD_GAME_READY, true);
510         break;
511 
512     case GPT_PLAYER_SPAWN_POSITION:
513         NetCl_PlayerSpawnPosition(reader);
514         break;
515 
516     case GPT_TOTAL_COUNTS:
517         NetCl_UpdateTotalCounts(reader);
518         break;
519 
520     case GPT_MOBJ_IMPULSE:
521         NetCl_MobjImpulse(reader);
522         break;
523 
524     case GPT_LOCAL_MOBJ_STATE:
525         NetCl_LocalMobjState(reader);
526         break;
527 
528     case GPT_MESSAGE:
529 #if __JHEXEN__
530     case GPT_YELLOW_MESSAGE:
531 #endif
532     {
533         size_t len = Reader_ReadUInt16(reader);
534         char *msg = (char *)Z_Malloc(len + 1, PU_GAMESTATIC, 0);
535         Reader_Read(reader, msg, len); msg[len] = 0;
536 
537 #if __JHEXEN__
538         if(type == GPT_YELLOW_MESSAGE)
539         {
540             P_SetYellowMessage(&players[CONSOLEPLAYER], msg);
541         }
542         else
543 #endif
544         {
545             P_SetMessage(&players[CONSOLEPLAYER], msg);
546         }
547         Z_Free(msg);
548         break; }
549 
550     case GPT_MAYBE_CHANGE_WEAPON: {
551         weapontype_t wt = (weapontype_t) Reader_ReadInt16(reader);
552         ammotype_t at   = (ammotype_t) Reader_ReadInt16(reader);
553         dd_bool force   = (Reader_ReadByte(reader) != 0);
554         P_MaybeChangeWeapon(&players[CONSOLEPLAYER], wt, at, force);
555         break; }
556 
557     case GPT_CONSOLEPLAYER_STATE:
558         NetCl_UpdatePlayerState(reader, CONSOLEPLAYER);
559         break;
560 
561     case GPT_CONSOLEPLAYER_STATE2:
562         NetCl_UpdatePlayerState2(reader, CONSOLEPLAYER);
563         break;
564 
565     case GPT_PLAYER_STATE:
566         NetCl_UpdatePlayerState(reader, -1);
567         break;
568 
569     case GPT_PLAYER_STATE2:
570         NetCl_UpdatePlayerState2(reader, -1);
571         break;
572 
573     case GPT_PSPRITE_STATE:
574         NetCl_UpdatePSpriteState(reader);
575         break;
576 
577     case GPT_INTERMISSION:
578         NetCl_Intermission(reader);
579         break;
580 
581     case GPT_FINALE_STATE:
582         NetCl_UpdateFinaleState(reader);
583         break;
584 
585     case GPT_PLAYER_INFO:
586         NetCl_UpdatePlayerInfo(reader);
587         break;
588 
589 #if __JHERETIC__ || __JHEXEN__ || __JSTRIFE__
590     case GPT_CLASS: {
591         player_t *plr = &players[CONSOLEPLAYER];
592         playerclass_t newClass = playerclass_t(Reader_ReadByte(reader));
593 # if __JHERETIC__
594         playerclass_t oldClass = plr->class_;
595 # endif
596         plr->class_ = newClass;
597         App_Log(DE2_DEV_MAP_MSG, "Player %i class changed to %i", CONSOLEPLAYER, plr->class_);
598 
599 # if __JHERETIC__
600         if(oldClass != newClass)
601         {
602             if(newClass == PCLASS_CHICKEN)
603             {
604                 App_Log(DE2_DEV_MAP_VERBOSE, "Player %i activating morph", CONSOLEPLAYER);
605 
606                 P_ActivateMorphWeapon(plr);
607             }
608             else if(oldClass == PCLASS_CHICKEN)
609             {
610                 App_Log(DE2_DEV_MAP_VERBOSE, "Player %i post-morph weapon %i", CONSOLEPLAYER, plr->readyWeapon);
611 
612                 // The morph has ended.
613                 P_PostMorphWeapon(plr, plr->readyWeapon);
614             }
615         }
616 # endif
617         break; }
618 #endif
619 
620     case GPT_SAVE:
621         NetCl_SaveGame(reader);
622         break;
623 
624     case GPT_LOAD:
625         NetCl_LoadGame(reader);
626         break;
627 
628     case GPT_PAUSE:
629         NetCl_Paused(reader);
630         break;
631 
632     case GPT_JUMP_POWER:
633         NetCl_UpdateJumpPower(reader);
634         break;
635 
636     case GPT_DISMISS_HUDS:
637         NetCl_DismissHUDs(reader);
638         break;
639 
640     default:
641         App_Log(DE2_NET_WARNING, "Game received unknown packet (type:%i)", type);
642     }
643 }
644 
645 /**
646  * Plays a (local) chat sound.
647  */
D_ChatSound()648 void D_ChatSound()
649 {
650 #if __JHEXEN__
651     S_LocalSound(SFX_CHAT, NULL);
652 #elif __JSTRIFE__
653     S_LocalSound(SFX_CHAT, NULL);
654 #elif __JHERETIC__
655     S_LocalSound(SFX_CHAT, NULL);
656 #else
657 # if __JDOOM__
658     if(gameModeBits & GM_ANY_DOOM2)
659         S_LocalSound(SFX_RADIO, NULL);
660     else
661 # endif
662         S_LocalSound(SFX_TINK, NULL);
663 #endif
664 }
665 
666 /**
667  * Show a message on screen, optionally accompanied by Chat sound effect.
668  *
669  * @param player     Player number to send the message to.
670  * @param playSound  @c true = play the chat sound.
671  */
D_NetMessageEx(int player,char const * msg,dd_bool playSound)672 static void D_NetMessageEx(int player, char const *msg, dd_bool playSound)
673 {
674     if(player < 0 || player > MAXPLAYERS) return;
675     player_t *plr = &players[player];
676 
677     if(!plr->plr->inGame)
678         return;
679 
680     // This is intended to be a local message.
681     // Let's make sure P_SetMessageWithFlags doesn't forward it anywhere.
682     netSvAllowSendMsg = false;
683     P_SetMessage(plr, msg);
684 
685     if(playSound)
686     {
687         D_ChatSound();
688     }
689 
690     netSvAllowSendMsg = true;
691 }
692 
D_NetMessage(int player,char const * msg)693 void D_NetMessage(int player, char const *msg)
694 {
695     D_NetMessageEx(player, msg, true);
696 }
697 
D_NetMessageNoSound(int player,char const * msg)698 void D_NetMessageNoSound(int player, char const *msg)
699 {
700     D_NetMessageEx(player, msg, false);
701 }
702 
D_NetDamageMobj(mobj_t * target,mobj_t * inflictor,mobj_t * source,int damage)703 dd_bool D_NetDamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, int damage)
704 {
705     int sourcePlrNum = -1;
706     if(source && source->player)
707     {
708         sourcePlrNum = source->player - players;
709     }
710 
711     if(source && !source->player)
712     {
713         // Not applicable: only damage from players.
714         return false;
715     }
716 
717     if(IS_SERVER && sourcePlrNum > 0)
718     {
719         /*
720          * A client is trying to do damage. However, it is not guaranteed that the server is 100%
721          * accurately aware of the gameplay situation in which the damage is being inflicted (due
722          * to network latency), so instead of applying the damage now we will wait for the client
723          * to request it separately.
724          */
725         return false;
726     }
727     else if(IS_CLIENT)
728     {
729         if((sourcePlrNum < 0 || sourcePlrNum == CONSOLEPLAYER) &&
730            target && target->player && target->player - players == CONSOLEPLAYER)
731         {
732             // Clients are allowed to damage themselves.
733             NetCl_DamageRequest(ClPlayer_ClMobj(CONSOLEPLAYER), inflictor, source, damage);
734 
735             // No further processing of this damage is needed.
736             return true;
737         }
738     }
739     return false;
740 }
741 
742 /**
743  * Console command to change the players' colors.
744  */
D_CMD(SetColor)745 D_CMD(SetColor)
746 {
747     DENG2_UNUSED2(src, argc);
748 
749     cfg.common.netColor = atoi(argv[1]);
750     if(IS_SERVER) // A local player?
751     {
752         if(IS_DEDICATED) return false;
753 
754         int player = CONSOLEPLAYER;
755 
756         // Server players, must be treated as a special case because this is
757         // a local mobj we're dealing with. We'll change the color translation
758         // bits directly.
759 
760         cfg.playerColor[player] = PLR_COLOR(player, cfg.common.netColor);
761         players[player].colorMap = cfg.playerColor[player];
762 
763         if(players[player].plr->mo)
764         {
765             // Change the color of the mobj (translation flags).
766             players[player].plr->mo->flags &= ~MF_TRANSLATION;
767             players[player].plr->mo->flags |= (cfg.playerColor[player] << MF_TRANSSHIFT);
768         }
769 
770         // Tell the clients about the change.
771         NetSv_SendPlayerInfo(player, DDSP_ALL_PLAYERS);
772     }
773     else
774     {
775         // Tell the server about the change.
776         NetCl_SendPlayerInfo();
777     }
778 
779     return true;
780 }
781 
782 /**
783  * Console command to change the players' class.
784  */
785 #if __JHEXEN__
D_CMD(SetClass)786 D_CMD(SetClass)
787 {
788     DENG2_UNUSED2(src, argc);
789 
790     playerclass_t newClass = playerclass_t(atoi(argv[1]));
791 
792     if(!(newClass < NUM_PLAYER_CLASSES))
793     {
794         return false;
795     }
796 
797     if(!PCLASS_INFO(newClass)->userSelectable)
798     {
799         return false;
800     }
801 
802     cfg.netClass = newClass; // Stored as a cvar.
803 
804     if(IS_CLIENT)
805     {
806         // Tell the server that we want to change our class.
807         NetCl_SendPlayerInfo();
808     }
809     else
810     {
811         // On the server (or singleplayer) we can do an immediate change.
812         P_PlayerChangeClass(&players[CONSOLEPLAYER], playerclass_t(cfg.netClass));
813     }
814 
815     return true;
816 }
817 #endif
818 
819 /**
820  * Post a local game message.
821  */
D_CMD(LocalMessage)822 D_CMD(LocalMessage)
823 {
824     DENG2_UNUSED2(src, argc);
825     D_NetMessageNoSound(CONSOLEPLAYER, argv[1]);
826     return true;
827 }
828