From 22b20b5877f5549b01c10c9588b744637ee197eb Mon Sep 17 00:00:00 2001 From: Eidolon Date: Fri, 13 Dec 2024 17:12:14 -0600 Subject: [PATCH] Add netgame voice chat Implemented using libopus for the Opus codec, same as is used in Discord. This adds the following cvars: - `voice_chat` On/Off, triggers self-deafen state on server via weaponprefs - `voice_mode` Activity/PTT - `voice_selfmute` On/Off, triggers self-mute state on server via weaponprefs - `voice_inputamp` -30 to 30, scales input by value in decibels - `voice_activationthreshold` -30 to 0, if any peak in a frame is higher, activates voice - `voice_loopback` On/Off, plays back local transcoded voice - `voice_proximity` On/Off, enables proximity effects for server - `voice_distanceattenuation_distance` distance in fracunits to scale voice volume over - `voice_distanceattenuation_factor` distance in logarithmic factor to scale voice volume by distance to. e.g. 0.5 for "half as loud" at or above max distance - `voice_stereopanning_factor` at 1.0, player voices are panned to left or right speaker, scaling to no effect at 0.0 - `voice_concurrentattenuation_factor` the logarithmic factor to attenuate player voices with concurrent speakers - `voice_concurrentattenuation_min` the minimum concurrent speakers before global concurrent speaker attenuation - `voice_concurrentattenuation_max` the maximum concurrent speakers for full global concurrent speaker attenuation - `voice_servermute` whether voice chat is enabled on this server. visible from MS via bitflag - `voicevolume` local volume of all voice playback A Voice Options menu is added with a subset of these options, and Server Options has server mute. --- CMakeLists.txt | 1 + cmake/Modules/FindOpus.cmake | 7 + src/CMakeLists.txt | 1 + src/cvars.cpp | 64 + src/d_clisrv.c | 14796 ++++++++++--------- src/d_clisrv.h | 22 + src/d_main.cpp | 11 +- src/d_netcmd.c | 30 + src/d_netcmd.h | 3 + src/d_player.h | 11 +- src/doomstat.h | 1 + src/dummy/i_sound.c | 27 + src/g_game.c | 5 +- src/g_input.c | 3 +- src/g_input.h | 1 + src/hu_stuff.c | 31 +- src/hu_stuff.h | 3 + src/hud/spectator.cpp | 1 + src/i_sound.h | 18 + src/k_hud.cpp | 14088 +++++++++--------- src/k_menu.h | 4 + src/k_menudraw.c | 8 + src/k_profiles.cpp | 21 +- src/k_profiles.h | 2 +- src/k_vote.c | 17 + src/k_zvote.c | 31 + src/k_zvote.h | 1 + src/m_swap.h | 21 + src/menus/CMakeLists.txt | 1 + src/menus/options-1.c | 3 + src/menus/options-profiles-edit-controls.c | 7 +- src/menus/options-server-1.c | 7 +- src/menus/options-sound.cpp | 17 + src/menus/options-voice.cpp | 91 + src/s_sound.c | 234 + src/s_sound.h | 30 + src/sdl/new_sound.cpp | 260 +- src/typedef.h | 1 + src/y_inter.cpp | 33 + vcpkg.json | 1 + 40 files changed, 15772 insertions(+), 14142 deletions(-) create mode 100644 cmake/Modules/FindOpus.cmake create mode 100644 src/menus/options-voice.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f852c981..0ff3bd28e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ find_package(ZLIB REQUIRED) find_package(PNG REQUIRED) find_package(SDL2 CONFIG REQUIRED) find_package(CURL REQUIRED) +find_package(Opus REQUIRED) # Use the one in thirdparty/fmt to guarantee a minimum version #find_package(FMT CONFIG REQUIRED) diff --git a/cmake/Modules/FindOpus.cmake b/cmake/Modules/FindOpus.cmake new file mode 100644 index 000000000..6c739d5cb --- /dev/null +++ b/cmake/Modules/FindOpus.cmake @@ -0,0 +1,7 @@ +find_package(Opus CONFIG) +if(NOT TARGET Opus::opus) + find_package(PkgConfig REQUIRED) + pkg_check_modules(Opus REQUIRED IMPORTED_TARGET opus) + set_target_properties(PkgConfig::Opus PROPERTIES IMPORTED_GLOBAL TRUE) + add_library(Opus::opus ALIAS PkgConfig::Opus) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 498bd4fbd..eb20b7088 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -258,6 +258,7 @@ endif() target_link_libraries(SRB2SDL2 PRIVATE ZLIB::ZLIB) target_link_libraries(SRB2SDL2 PRIVATE PNG::PNG) target_link_libraries(SRB2SDL2 PRIVATE CURL::libcurl) +target_link_libraries(SRB2SDL2 PRIVATE Opus::opus) if("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD") target_link_libraries(SRB2SDL2 PRIVATE -lexecinfo) target_link_libraries(SRB2SDL2 PRIVATE -lpthread) diff --git a/src/cvars.cpp b/src/cvars.cpp index 233fa1d0e..1d7b53c35 100644 --- a/src/cvars.cpp +++ b/src/cvars.cpp @@ -322,6 +322,7 @@ consvar_t cv_controlperkey = Player("controlperkey", "One").values({{1, "One"}, consvar_t cv_mastervolume = Player("volume", "80").min_max(0, 100); consvar_t cv_digmusicvolume = Player("musicvolume", "80").min_max(0, 100); consvar_t cv_soundvolume = Player("soundvolume", "80").min_max(0, 100); +consvar_t cv_voicevolume = Player("voicevolume", "100").min_max(0, 100); #ifdef HAVE_DISCORDRPC void DRPC_UpdatePresence(void); @@ -1351,8 +1352,71 @@ consvar_t cv_chatwidth = Player("chatwidth", "150").min_max(64, 150); // old shit console chat. (mostly exists for stuff like terminal, not because I cared if anyone liked the old chat.) consvar_t cv_consolechat = Player("chatmode", "Yes").values({{0, "Yes"}, {2, "No"}}); +// When off, inbound voice packets are ignored +void VoiceChat_OnChange(void); +consvar_t cv_voice_chat = Player("voice_chat", "Off") + .on_off() + .onchange(VoiceChat_OnChange) + .description("Whether voice chat is played or not. Shown as self-deafen to others."); + +// When on, local player won't transmit voice +consvar_t cv_voice_mode = Player("voice_mode", "Activity") + .values({{0, "Activity"}, {1, "PTT"}}) + .description("How to activate voice transmission"); + +consvar_t cv_voice_selfmute = Player("voice_selfmute", "Off") + .on_off() + .onchange(weaponPrefChange) + .description("Whether the local microphone is muted. Shown as self-mute to others."); + +consvar_t cv_voice_inputamp = Player("voice_inputamp", "14") + .min_max(-30, 30) + .description("How much louder or quieter to make voice input, in decibels."); + +consvar_t cv_voice_activationthreshold = Player("voice_activationthreshold", "-20") + .min_max(-30, 0) + .description("The voice activation threshold, in decibels from maximum amplitude."); + +// When on, local voice is played back out +consvar_t cv_voice_loopback = Player("voice_loopback", "Off") + .on_off() + .dont_save() + .description("When on, plays the local player's voice"); + +consvar_t cv_voice_proximity = NetVar("voice_proximity", "On") + .on_off() + .description("Whether proximity effects for voice chat are enabled on the server."); + +// The relative distance for maximum voice attenuation +consvar_t cv_voice_distanceattenuation_distance = NetVar("voice_distanceattenuation_distance", "4096") + .floating_point() + .description("Voice speaker's distance from listener at which positional voice is fully attenuated"); + +// The volume factor (scaled logarithmically, i.e. 0.5 = "half as loud") for voice distance attenuation +consvar_t cv_voice_distanceattenuation_factor = NetVar("voice_distanceattenuation_factor", "0.2") + .floating_point() + .description("Maximum attenuation, in perceived loudness, when a voice speaker is at voice_distanceattenuation_distance units or further from the listener"); + +// The scale factor applied to stereo separation for voice panning +consvar_t cv_voice_stereopanning_factor = NetVar("voice_stereopanning_factor", "1.0") + .floating_point() + .description("Scale of stereo panning applied to a voice speaker relative to their in-game position, from 0.0-1.0"); + +consvar_t cv_voice_concurrentattenuation_factor = NetVar("voice_concurrentattenuation_factor", "0.6") + .floating_point() + .description("The maximum attenuation factor, in perceived loudness, when at or exceeding voice_concurrentattenuation_max speakers"); +consvar_t cv_voice_concurrentattenuation_min = NetVar("voice_concurrentattenuation_min", "3") + .description("Minimum concurrent speakers before global attenuation starts"); +consvar_t cv_voice_concurrentattenuation_max = NetVar("voice_concurrentattenuation_max", "8") + .description("Maximum concurrent speakers at which full global attenuation is applied"); + void Mute_OnChange(void); +void VoiceMute_OnChange(void); consvar_t cv_mute = UnsavedNetVar("mute", "Off").on_off().onchange(Mute_OnChange); +consvar_t cv_voice_servermute = NetVar("voice_servermute", "On") + .on_off() + .onchange(VoiceMute_OnChange) + .description("If On, the server will not broadcast voice chat to clients"); // diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 8251404d0..ebcd68d55 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -1,7135 +1,7661 @@ -// DR. ROBOTNIK'S RING RACERS -//----------------------------------------------------------------------------- -// Copyright (C) 2024 by Kart Krew. -// Copyright (C) 2020 by Sonic Team Junior. -// Copyright (C) 2000 by DooM Legacy Team. -// -// This program is free software distributed under the -// terms of the GNU General Public License, version 2. -// See the 'LICENSE' file for more details. -//----------------------------------------------------------------------------- -/// \file d_clisrv.c -/// \brief SRB2 Network game communication and protocol, all OS independent parts. - -#include -#ifdef __GNUC__ -#include //for unlink -#endif - -#include "i_time.h" -#include "i_net.h" -#include "i_system.h" -#include "i_video.h" -#include "d_net.h" -#include "d_netfil.h" // fileneedednum -#include "d_main.h" -#include "g_game.h" -#include "st_stuff.h" -#include "hu_stuff.h" -#include "keys.h" -#include "g_input.h" // JOY1 -#include "k_menu.h" -#include "console.h" -#include "d_netfil.h" -#include "byteptr.h" -#include "p_saveg.h" -#include "z_zone.h" -#include "p_local.h" -#include "m_misc.h" -#include "am_map.h" -#include "m_random.h" -#include "mserv.h" -#include "y_inter.h" -#include "r_local.h" -#include "m_argv.h" -#include "p_setup.h" -#include "lzf.h" -#include "lua_script.h" -#include "lua_hook.h" -#include "md5.h" -#include "m_perfstats.h" -#include "monocypher/monocypher.h" -#include "stun.h" - -// SRB2Kart -#include "k_credits.h" -#include "k_kart.h" -#include "k_battle.h" -#include "k_pwrlv.h" -#include "k_bot.h" -#include "k_grandprix.h" -#include "doomstat.h" -#include "s_sound.h" // sfx_syfail -#include "m_cond.h" // netUnlocked -#include "g_party.h" -#include "k_vote.h" -#include "k_serverstats.h" -#include "k_zvote.h" -#include "music.h" -#include "k_bans.h" -#include "sanitize.h" -#include "r_fps.h" -#include "filesrch.h" // refreshdirmenu -#include "k_objects.h" - -// cl loading screen -#include "v_video.h" -#include "f_finale.h" -#include "k_hud.h" - -#ifdef HAVE_DISCORDRPC -#include "discord.h" -#endif - -// -// NETWORKING -// -// gametic is the tic about to (or currently being) run -// Server: -// maketic is the tic that hasn't had control made for it yet -// nettics is the tic for each node -// firstticstosend is the lowest value of nettics -// Client: -// neededtic is the tic needed by the client to run the game -// firstticstosend is used to optimize a condition -// Normally maketic >= gametic > 0 - -#define MAX_REASONLENGTH 30 -#define FORCECLOSE 0x8000 - -boolean server = true; // true or false but !server == client -#define client (!server) -boolean nodownload = false; -boolean serverrunning = false; -INT32 serverplayer = 0; -char motd[254], server_context[8]; // Message of the Day, Unique Context (even without Mumble support) - -UINT8 playerconsole[MAXPLAYERS]; - -// Server specific vars -UINT8 playernode[MAXPLAYERS]; - -// Minimum timeout for sending the savegame -// The actual timeout will be longer depending on the savegame length -tic_t jointimeout = (3*TICRATE); -static boolean sendingsavegame[MAXNETNODES]; // Are we sending the savegame? -static boolean resendingsavegame[MAXNETNODES]; // Are we resending the savegame? -static tic_t savegameresendcooldown[MAXNETNODES]; // How long before we can resend again? -static tic_t freezetimeout[MAXNETNODES]; // Until when can this node freeze the server before getting a timeout? - -// Incremented by cv_joindelay when a client joins, decremented each tic. -// If higher than cv_joindelay * 2 (3 joins in a short timespan), joins are temporarily disabled. -static tic_t joindelay = 0; - -UINT16 pingmeasurecount = 1; -UINT32 realpingtable[MAXPLAYERS]; //the base table of ping where an average will be sent to everyone. -UINT32 playerpingtable[MAXPLAYERS]; //table of player latency values. -UINT32 playerpacketlosstable[MAXPLAYERS]; -UINT32 playerdelaytable[MAXPLAYERS]; // mindelay values. - -#define GENTLEMANSMOOTHING (TICRATE) -static tic_t reference_lag; -static UINT8 spike_time; -static tic_t lowest_lag; -boolean server_lagless; - -SINT8 nodetoplayer[MAXNETNODES]; -SINT8 nodetoplayer2[MAXNETNODES]; // say the numplayer for this node if any (splitscreen) -SINT8 nodetoplayer3[MAXNETNODES]; // say the numplayer for this node if any (splitscreen == 2) -SINT8 nodetoplayer4[MAXNETNODES]; // say the numplayer for this node if any (splitscreen == 3) -UINT8 playerpernode[MAXNETNODES]; // used specialy for splitscreen -boolean nodeingame[MAXNETNODES]; // set false as nodes leave game -boolean nodeneedsauth[MAXNETNODES]; - -tic_t servermaxping = 20; // server's max delay, in frames. Defaults to 20 -static tic_t nettics[MAXNETNODES]; // what tic the client have received -static tic_t supposedtics[MAXNETNODES]; // nettics prevision for smaller packet -static UINT8 nodewaiting[MAXNETNODES]; -static tic_t firstticstosend; // min of the nettics -static tic_t tictoclear = 0; // optimize d_clearticcmd -static tic_t maketic; - -static INT16 consistancy[BACKUPTICS]; - -static UINT8 player_joining = false; -UINT8 hu_redownloadinggamestate = 0; - -// kart, true when a player is connecting or disconnecting so that the gameplay has stopped in its tracks -boolean hu_stopped = false; - -UINT8 adminpassmd5[16]; -boolean adminpasswordset = false; - -// Client specific -static ticcmd_t localcmds[MAXSPLITSCREENPLAYERS][MAXGENTLEMENDELAY]; -static boolean cl_packetmissed; -// here it is for the secondary local player (splitscreen) -static UINT8 mynode; // my address pointofview server -static boolean cl_redownloadinggamestate = false; - -static UINT8 localtextcmd[MAXSPLITSCREENPLAYERS][MAXTEXTCMD]; -static tic_t neededtic; -SINT8 servernode = 0; // the number of the server node -char connectedservername[MAXSERVERNAME]; -char connectedservercontact[MAXSERVERCONTACT]; -/// \brief do we accept new players? -/// \todo WORK! -boolean acceptnewnode = true; - -UINT32 ourIP; // Used when populating PT_SERVERCHALLENGE (guards against signature reuse) -uint8_t lastReceivedKey[MAXNETNODES][MAXSPLITSCREENPLAYERS][PUBKEYLENGTH]; // Player's public key (join process only! active players have it on player_t) -uint8_t lastSentChallenge[MAXNETNODES][CHALLENGELENGTH]; // The random message we asked them to sign in PT_SERVERCHALLENGE, check it in PT_CLIENTJOIN -uint8_t awaitingChallenge[CHALLENGELENGTH]; // The message the server asked our client to sign when joining -uint8_t lastChallengeAll[CHALLENGELENGTH]; // The message we asked EVERYONE to sign for client-to-client identity proofs -uint8_t lastReceivedSignature[MAXPLAYERS][SIGNATURELENGTH]; // Everyone's response to lastChallengeAll -uint8_t knownWhenChallenged[MAXPLAYERS][PUBKEYLENGTH]; // Everyone a client saw at the moment a challenge should be initiated -boolean expectChallenge = false; // Were we in-game before a client-to-client challenge should have been sent? - -uint8_t priorKeys[MAXPLAYERS][PUBKEYLENGTH]; // Make a note of keys before consuming a new gamestate, and if the server tries to send us a gamestate where keys differ, assume shenanigans - -boolean serverisfull = false; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not -tic_t firstconnectattempttime = 0; - -// engine - -// Must be a power of two -#define TEXTCMD_HASH_SIZE 4 - -typedef struct textcmdplayer_s -{ - INT32 playernum; - UINT8 cmd[MAXTEXTCMD]; - struct textcmdplayer_s *next; -} textcmdplayer_t; - -typedef struct textcmdtic_s -{ - tic_t tic; - textcmdplayer_t *playercmds[TEXTCMD_HASH_SIZE]; - struct textcmdtic_s *next; -} textcmdtic_t; - -ticcmd_t netcmds[BACKUPTICS][MAXPLAYERS]; -static textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE] = {NULL}; - - -static tic_t stop_spamming[MAXPLAYERS]; - -// Generate a message for an authenticating client to sign, with some guarantees about who we are. -void GenerateChallenge(uint8_t *buf) -{ - #ifndef SRB2_LITTLE_ENDIAN - #error "FIXME: 64-bit timestamp field is not supported on Big Endian" - #endif - - UINT64 now = time(NULL); - csprng(buf, CHALLENGELENGTH); // Random noise as a baseline, but... - memcpy(buf, &now, sizeof(now)); // Timestamp limits the reuse window. - memcpy(buf + sizeof(now), &ourIP, sizeof(ourIP)); // IP prevents captured signatures from being used elsewhere. - - #ifdef DEVELOP - if (cv_badtime.value) - { - CV_AddValue(&cv_badtime, -1); - CONS_Alert(CONS_WARNING, "cv_badtime enabled, trashing time in auth message\n"); - memset(buf, 0, sizeof(now)); - } - - if (cv_badip.value) - { - CV_AddValue(&cv_badip, -1); - CONS_Alert(CONS_WARNING, "cv_badip enabled, trashing IP in auth message\n"); - memset(buf + sizeof(now), 0, sizeof(ourIP)); - } - #endif -} - -// Modified servers can throw softballs or reuse challenges. -// Don't sign anything that wasn't generated just for us! -shouldsign_t ShouldSignChallenge(uint8_t *message) -{ - #ifndef SRB2_LITTLE_ENDIAN - #error "FIXME: 64-bit timestamp field is not supported on Big Endian" - #endif - - UINT64 then, now; - UINT32 claimedIP, realIP; - - now = time(NULL); - memcpy(&then, message, sizeof(then)); - memcpy(&claimedIP, message + sizeof(then), sizeof(claimedIP)); - realIP = I_GetNodeAddressInt(servernode); - - if ((max(now, then) - min(now, then)) > 60*15) - return SIGN_BADTIME; - - if (realIP != claimedIP && I_IsExternalAddress(&realIP)) - return SIGN_BADIP; - - return SIGN_OK; -} - -static inline void *G_DcpyTiccmd(void* dest, const ticcmd_t* src, const size_t n) -{ - const size_t d = n / sizeof(ticcmd_t); - const size_t r = n % sizeof(ticcmd_t); - UINT8 *ret = dest; - - if (r) - M_Memcpy(dest, src, n); - else if (d) - G_MoveTiccmd(dest, src, d); - return ret+n; -} - -static inline void *G_ScpyTiccmd(ticcmd_t* dest, void* src, const size_t n) -{ - const size_t d = n / sizeof(ticcmd_t); - const size_t r = n % sizeof(ticcmd_t); - UINT8 *ret = src; - - if (r) - M_Memcpy(dest, src, n); - else if (d) - G_MoveTiccmd(dest, src, d); - return ret+n; -} - - - -// Some software don't support largest packet -// (original sersetup, not exactely, but the probability of sending a packet -// of 512 bytes is like 0.1) -UINT16 software_MAXPACKETLENGTH; - -/** Guesses the full value of a tic from its lowest byte, for a specific node - * - * \param low The lowest byte of the tic value - * \param basetic The last full tic value to compare against - * \return The full tic value - * - */ -tic_t ExpandTics(INT32 low, tic_t basetic) -{ - INT32 delta; - - delta = low - (basetic & UINT8_MAX); - - if (delta >= -64 && delta <= 64) - return (basetic & ~UINT8_MAX) + low; - else if (delta > 64) - return (basetic & ~UINT8_MAX) - 256 + low; - else //if (delta < -64) - return (basetic & ~UINT8_MAX) + 256 + low; -} - -// ----------------------------------------------------------------- -// Some extra data function for handle textcmd buffer -// ----------------------------------------------------------------- - -static void (*listnetxcmd[MAXNETXCMD])(const UINT8 **p, INT32 playernum); - -void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(const UINT8 **p, INT32 playernum)) -{ -#ifdef PARANOIA - if (id >= MAXNETXCMD) - I_Error("Command id %d too big", id); - if (listnetxcmd[id] != 0) - I_Error("Command id %d already used", id); -#endif - listnetxcmd[id] = cmd_f; -} - -void SendNetXCmdForPlayer(UINT8 playerid, netxcmd_t id, const void *param, size_t nparam) -{ - if (((UINT16*)localtextcmd[playerid])[0]+3+nparam > MAXTEXTCMD) - { - // for future reference: if (cht_debug) != debug disabled. - CONS_Alert(CONS_ERROR, M_GetText("NetXCmd buffer full, cannot add netcmd %d! (size: %d, needed: %s)\n"), id, ((UINT16*)localtextcmd[playerid])[0], sizeu1(nparam)); - return; - } - - ((UINT16*)localtextcmd[playerid])[0]++; - localtextcmd[playerid][((UINT16*)localtextcmd[playerid])[0] + 1] = (UINT8)id; - - if (param && nparam) - { - M_Memcpy(&localtextcmd[playerid][((UINT16*)localtextcmd[playerid])[0] + 2], param, nparam); - ((UINT16*)localtextcmd[playerid])[0] = ((UINT16*)localtextcmd[playerid])[0] + (UINT8)nparam; - } -} - -UINT8 GetFreeXCmdSize(UINT8 playerid) -{ - // -2 for the size and another -1 for the ID. - return (UINT8)(localtextcmd[playerid][0] - 3); -} - -// Frees all textcmd memory for the specified tic -static void D_FreeTextcmd(tic_t tic) -{ - textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)]; - textcmdtic_t *textcmdtic = *tctprev; - - while (textcmdtic && textcmdtic->tic != tic) - { - tctprev = &textcmdtic->next; - textcmdtic = textcmdtic->next; - } - - if (textcmdtic) - { - INT32 i; - - // Remove this tic from the list. - *tctprev = textcmdtic->next; - - // Free all players. - for (i = 0; i < TEXTCMD_HASH_SIZE; i++) - { - textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[i]; - - while (textcmdplayer) - { - textcmdplayer_t *tcpnext = textcmdplayer->next; - Z_Free(textcmdplayer); - textcmdplayer = tcpnext; - } - } - - // Free this tic's own memory. - Z_Free(textcmdtic); - } -} - -// Gets the buffer for the specified ticcmd, or NULL if there isn't one -static UINT8* D_GetExistingTextcmd(tic_t tic, INT32 playernum) -{ - textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)]; - while (textcmdtic && textcmdtic->tic != tic) textcmdtic = textcmdtic->next; - - // Do we have an entry for the tic? If so, look for player. - if (textcmdtic) - { - textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)]; - while (textcmdplayer && textcmdplayer->playernum != playernum) textcmdplayer = textcmdplayer->next; - - if (textcmdplayer) return textcmdplayer->cmd; - } - - return NULL; -} - -// Gets the buffer for the specified ticcmd, creating one if necessary -static UINT8* D_GetTextcmd(tic_t tic, INT32 playernum) -{ - textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)]; - textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)]; - textcmdplayer_t *textcmdplayer, **tcpprev; - - // Look for the tic. - while (textcmdtic && textcmdtic->tic != tic) - { - tctprev = &textcmdtic->next; - textcmdtic = textcmdtic->next; - } - - // If we don't have an entry for the tic, make it. - if (!textcmdtic) - { - textcmdtic = *tctprev = Z_Calloc(sizeof (textcmdtic_t), PU_STATIC, NULL); - textcmdtic->tic = tic; - } - - tcpprev = &textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)]; - textcmdplayer = *tcpprev; - - // Look for the player. - while (textcmdplayer && textcmdplayer->playernum != playernum) - { - tcpprev = &textcmdplayer->next; - textcmdplayer = textcmdplayer->next; - } - - // If we don't have an entry for the player, make it. - if (!textcmdplayer) - { - textcmdplayer = *tcpprev = Z_Calloc(sizeof (textcmdplayer_t), PU_STATIC, NULL); - textcmdplayer->playernum = playernum; - } - - return textcmdplayer->cmd; -} - -static boolean ExtraDataTicker(void) -{ - boolean anyNetCmd = false; - INT32 i; - - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i] || i == 0) - { - const UINT8 *bufferstart = D_GetExistingTextcmd(gametic, i); - - if (bufferstart) - { - const UINT8 *curpos = bufferstart; - const UINT8 *bufferend = &curpos[((const UINT16*)curpos)[0]+2]; - - curpos += 2; - while (curpos < bufferend) - { - if (*curpos < MAXNETXCMD && listnetxcmd[*curpos]) - { - const UINT8 id = *curpos; - curpos++; - DEBFILE(va("executing x_cmd %s ply %u ", netxcmdnames[id - 1], i)); - (listnetxcmd[id])(&curpos, i); - DEBFILE("done\n"); - anyNetCmd = true; - } - else - { - if (server) - { - SendKick(i, KICK_MSG_CON_FAIL); - DEBFILE(va("player %d kicked [gametic=%u] reason as follows:\n", i, gametic)); - } - CONS_Alert(CONS_WARNING, M_GetText("Got unknown net command [%s]=%d (max %d)\n"), sizeu1(curpos - bufferstart), *curpos, bufferstart[0]); - break; - } - } - } - } - } - - // If you are a client, you can safely forget the net commands for this tic - // If you are the server, you need to remember them until every client has been acknowledged, - // because if you need to resend a PT_SERVERTICS packet, you will need to put the commands in it - if (client) - { - D_FreeTextcmd(gametic); - } - - return anyNetCmd; -} - -static void D_Clearticcmd(tic_t tic) -{ - INT32 i; - - D_FreeTextcmd(tic); - - for (i = 0; i < MAXPLAYERS; i++) - netcmds[tic%BACKUPTICS][i].flags = 0; - - DEBFILE(va("clear tic %5u (%2u)\n", tic, tic%BACKUPTICS)); -} - -void D_ResetTiccmds(void) -{ - INT32 i, j; - - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - { - for (j = 0; j < MAXGENTLEMENDELAY; j++) - { - memset(&localcmds[i][j], 0, sizeof(ticcmd_t)); - } - } - - // Reset the net command list - for (i = 0; i < TEXTCMD_HASH_SIZE; i++) - while (textcmds[i]) - D_Clearticcmd(textcmds[i]->tic); -} - -void D_ResetTiccmdAngle(UINT8 ss, angle_t angle) -{ - INT32 i; - - for (i = 0; i < MAXGENTLEMENDELAY; ++i) - { - localcmds[ss][i].angle = angle >> TICCMD_REDUCE; - } -} - -ticcmd_t *D_LocalTiccmd(UINT8 ss) -{ - return &localcmds[ss][0]; -} - -void SendKick(UINT8 playernum, UINT8 msg) -{ - UINT8 buf[2]; - - buf[0] = playernum; - buf[1] = msg; - SendNetXCmd(XD_KICK, &buf, 2); -} - -// ----------------------------------------------------------------- -// end of extra data function -// ----------------------------------------------------------------- - -// ----------------------------------------------------------------- -// extra data function for lmps -// ----------------------------------------------------------------- - -// if extradatabit is set, after the ziped tic you find this: -// -// type | description -// ---------+-------------- -// byte | size of the extradata -// byte | the extradata (xd) bits: see XD_... -// with this byte you know what parameter folow -// if (xd & XDNAMEANDCOLOR) -// byte | color -// char[MAXPLAYERNAME] | name of the player -// endif -// if (xd & XD_WEAPON_PREF) -// byte | original weapon switch: boolean, true if use the old -// | weapon switch methode -// char[NUMWEAPONS] | the weapon switch priority -// byte | autoaim: true if use the old autoaim system -// endif -/*boolean AddLmpExtradata(UINT8 **demo_point, INT32 playernum) -{ - UINT8 *textcmd = D_GetExistingTextcmd(gametic, playernum); - - if (!textcmd) - return false; - - M_Memcpy(*demo_point, textcmd, textcmd[0]+1); - *demo_point += textcmd[0]+1; - return true; -} - -void ReadLmpExtraData(UINT8 **demo_pointer, INT32 playernum) -{ - UINT8 nextra; - UINT8 *textcmd; - - if (!demo_pointer) - return; - - textcmd = D_GetTextcmd(gametic, playernum); - nextra = **demo_pointer; - M_Memcpy(textcmd, *demo_pointer, nextra + 1); - // increment demo pointer - *demo_pointer += nextra + 1; -}*/ - -// ----------------------------------------------------------------- -// end extra data function for lmps -// ----------------------------------------------------------------- - -static INT16 Consistancy(void); - -typedef enum -{ - CL_SEARCHING, - CL_CHECKFILES, - CL_DOWNLOADFILES, - CL_DOWNLOADFAILED, - CL_ASKJOIN, - CL_LOADFILES, - CL_SETUPFILES, - CL_WAITJOINRESPONSE, - CL_DOWNLOADSAVEGAME, - CL_CONNECTED, - CL_ABORTED, - CL_ASKFULLFILELIST, - CL_CONFIRMCONNECT, -#ifdef HAVE_CURL - CL_PREPAREHTTPFILES, - CL_DOWNLOADHTTPFILES, -#endif - CL_SENDKEY, - CL_WAITCHALLENGE, -} cl_mode_t; - -static void GetPackets(void); - -static cl_mode_t cl_mode = CL_SEARCHING; - -#ifdef HAVE_CURL -char http_source[MAX_MIRROR_LENGTH]; -#endif - -static UINT16 cl_lastcheckedfilecount = 0; // used for full file list - -// -// CL_DrawConnectionStatus -// -// Keep the local client informed of our status. -// -static inline void CL_DrawConnectionStatus(void) -{ - INT32 ccstime = I_GetTime(); - - // Draw background fade - if (!menuactive) // menu already draws its own fade - V_DrawFadeScreen(0xFF00, 16); // force default - - if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES && cl_mode != CL_CHECKFILES -#ifdef HAVE_CURL - && cl_mode != CL_DOWNLOADHTTPFILES -#endif - ) - { - INT32 i, animtime = ((ccstime / 4) & 15) + 16; - UINT8 palstart = (cl_mode == CL_SEARCHING) ? 32 : 96; - // 15 pal entries total. - const char *cltext; - - // Draw bottom box - M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); - K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, 0, "Press or to abort", 1, 2, V_YELLOWMAP); - - for (i = 0; i < 16; ++i) - V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-24, 16, 8, palstart + ((animtime - i) & 15)); - - switch (cl_mode) - { - case CL_DOWNLOADSAVEGAME: - if (lastfilenum != -1) - { - UINT32 currentsize = fileneeded[lastfilenum].currentsize; - UINT32 totalsize = fileneeded[lastfilenum].totalsize; - INT32 dldlength; - - cltext = M_GetText("Downloading game state..."); - Net_GetNetStat(); - - dldlength = (INT32)((currentsize/(double)totalsize) * 256); - if (dldlength > 256) - dldlength = 256; - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111); - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96); - - V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE, - va(" %4uK/%4uK",currentsize>>10,totalsize>>10)); - - V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE, - va("%3.1fK/s ", ((double)getbps)/1024)); - } - else - cltext = M_GetText("Waiting to download game state..."); - break; - case CL_ASKFULLFILELIST: - case CL_CONFIRMCONNECT: - case CL_DOWNLOADFAILED: - cltext = ""; - break; - case CL_SETUPFILES: - cltext = M_GetText("Configuring addons..."); - break; - case CL_ASKJOIN: - case CL_WAITJOINRESPONSE: - if (serverisfull) - cltext = M_GetText("Server full, waiting for a slot..."); - else - cltext = M_GetText("Requesting to join..."); - - break; -#ifdef HAVE_CURL - case CL_PREPAREHTTPFILES: - cltext = M_GetText("Waiting to download files..."); - break; -#endif - default: - cltext = M_GetText("Attempting to connect..."); - if (I_GetTime() - firstconnectattempttime > 15*TICRATE) - { - V_DrawCenteredString(BASEVIDWIDTH/2, 16, V_YELLOWMAP, "This is taking much longer than usual."); - V_DrawCenteredString(BASEVIDWIDTH/2, 16+8, V_YELLOWMAP, "Are you sure you've got the right IP?"); - V_DrawCenteredString(BASEVIDWIDTH/2, 16+16, V_YELLOWMAP, "The host may need to forward port 5029 UDP."); - } - break; - } - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, cltext); - } - else - { - if (cl_mode == CL_CHECKFILES) - { - INT32 totalfileslength; - INT32 checkednum = 0; - INT32 i; - - K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, 0, "Press or to abort", 1, 2, V_YELLOWMAP); - - //ima just count files here - for (i = 0; i < fileneedednum; i++) - if (fileneeded[i].status != FS_NOTCHECKED) - checkednum++; - - // Loading progress - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP, "Checking server addons..."); - totalfileslength = (INT32)((checkednum/(double)(fileneedednum)) * 256); - M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, 256, 8, 111); - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, totalfileslength, 8, 96); - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, - va(" %2u/%2u Files",checkednum,fileneedednum)); - } - else if (cl_mode == CL_LOADFILES) - { - INT32 totalfileslength; - INT32 loadcompletednum = 0; - INT32 i; - - K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, 0, "Press or to abort", 1, 2, V_YELLOWMAP); - - //ima just count files here - for (i = 0; i < fileneedednum; i++) - if (fileneeded[i].status == FS_OPEN) - loadcompletednum++; - - // Loading progress - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP, "Loading server addons..."); - totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256); - M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, 256, 8, 111); - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, totalfileslength, 8, 96); - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, - va(" %2u/%2u Files",loadcompletednum,fileneedednum)); - } - else if (lastfilenum != -1) - { - INT32 dldlength; - INT32 totalfileslength; - UINT32 totaldldsize; - static char tempname[28]; - fileneeded_t *file = &fileneeded[lastfilenum]; - char *filename = file->filename; - - // Draw the bottom box. - M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-58-8, 32, 1); - K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-58-14, 0, "Press or to abort", 1, 2, V_YELLOWMAP); - - Net_GetNetStat(); - dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256); - if (dldlength > 256) - dldlength = 256; - - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-58, 256, 8, 111); - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-58, dldlength, 8, 96); - - memset(tempname, 0, sizeof(tempname)); - // offset filename to just the name only part - filename += strlen(filename) - nameonlylength(filename); - - if (strlen(filename) > sizeof(tempname)-1) // too long to display fully - { - size_t endhalfpos = strlen(filename)-10; - // display as first 14 chars + ... + last 10 chars - // which should add up to 27 if our math(s) is correct - snprintf(tempname, sizeof(tempname), "%.14s...%.10s", filename, filename+endhalfpos); - } - else // we can copy the whole thing in safely - { - strncpy(tempname, filename, sizeof(tempname)-1); - } - - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-58-30, 0, - va(M_GetText("%s downloading"), ((cl_mode == CL_DOWNLOADHTTPFILES) ? "\x82""HTTP" : "\x85""Direct"))); - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-58-22, V_YELLOWMAP, - va(M_GetText("\"%s\""), tempname)); - V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-58, V_20TRANS|V_MONOSPACE, - va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10)); - V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-58, V_20TRANS|V_MONOSPACE, - va("%3.1fK/s ", ((double)getbps)/1024)); - - // Download progress - - if (fileneeded[lastfilenum].currentsize != fileneeded[lastfilenum].totalsize) - totaldldsize = downloadcompletedsize+fileneeded[lastfilenum].currentsize; //Add in single file progress download if applicable - else - totaldldsize = downloadcompletedsize; - - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-14, V_YELLOWMAP, "Overall Download Progress"); - totalfileslength = (INT32)((totaldldsize/(double)totalfilesrequestedsize) * 256); - M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, 256, 8, 111); - V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, totalfileslength, 8, 96); - - if (totalfilesrequestedsize>>20 >= 10) //display in MB if over 10MB - V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, - va(" %4uM/%4uM",totaldldsize>>20,totalfilesrequestedsize>>20)); - else - V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, - va(" %4uK/%4uK",totaldldsize>>10,totalfilesrequestedsize>>10)); - - V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, - va("%2u/%2u Files ",downloadcompletednum,totalfilesrequestednum)); - } - else - { - INT32 i, animtime = ((ccstime / 4) & 15) + 16; - UINT8 palstart = (cl_mode == CL_SEARCHING) ? 128 : 160; - // 15 pal entries total. - - //Draw bottom box - M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); - K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, 0, "Press or to abort", 1, 2, V_YELLOWMAP); - - for (i = 0; i < 16; ++i) - V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-24, 16, 8, palstart + ((animtime - i) & 15)); - - V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP, - M_GetText("Waiting to download files...")); - } - } -} - -static boolean CL_AskFileList(INT32 firstfile) -{ - netbuffer->packettype = PT_TELLFILESNEEDED; - netbuffer->u.filesneedednum = firstfile; - - return HSendPacket(servernode, false, 0, sizeof (INT32)); -} - -/** Sends a special packet to declare how many players in local - * Used only in arbitratrenetstart() - * Sends a PT_CLIENTJOIN packet to the server - * - * \return True if the packet was successfully sent - * \todo Improve the description... - * Because to be honest, I have no idea what arbitratrenetstart is... - * Is it even used...? - * - */ -static boolean CL_SendJoin(void) -{ - UINT8 localplayers = 1; - UINT8 i; - - if (netgame) - CONS_Printf(M_GetText("Sending join request...\n")); - netbuffer->packettype = PT_CLIENTJOIN; - - if (splitscreen) - localplayers += splitscreen; - - netbuffer->u.clientcfg.localplayers = localplayers; - netbuffer->u.clientcfg._255 = 255; - netbuffer->u.clientcfg.packetversion = PACKETVERSION; - netbuffer->u.clientcfg.version = VERSION; - netbuffer->u.clientcfg.subversion = SUBVERSION; - strncpy(netbuffer->u.clientcfg.application, SRB2APPLICATION, - sizeof netbuffer->u.clientcfg.application); - - for (i = 0; i <= splitscreen; i++) - { - // the MAXPLAYERS addition is necessary to communicate that g_localplayers is not yet safe to reference - CleanupPlayerName(MAXPLAYERS+i, cv_playername[i].zstring); - strncpy(netbuffer->u.clientcfg.names[i], cv_playername[i].zstring, MAXPLAYERNAME); - } - // privacy shield for the local players not joining this session - for (; i < MAXSPLITSCREENPLAYERS; i++) - strncpy(netbuffer->u.clientcfg.names[i], va("Player %c", 'A' + i), MAXPLAYERNAME); - - memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false, -1), MAXAVAILABILITY*sizeof(UINT8)); - - // Don't leak old signatures from prior sessions. - memset(&netbuffer->u.clientcfg.challengeResponse, 0, sizeof(((clientconfig_pak *)0)->challengeResponse)); - - if (client && netgame) - { - shouldsign_t safe = ShouldSignChallenge(awaitingChallenge); - - if (safe != SIGN_OK) - { - if (safe == SIGN_BADIP) - { - I_Error("External server IP didn't match the message it sent."); - } - else if (safe == SIGN_BADTIME) - { - I_Error("External server sent a message with an unusual timestamp.\nMake sure your system time is set correctly."); - } - else - { - I_Error("External server asked for a signature on something strange.\nPlease notify a developer if you've seen this more than once."); - } - return false; - } - } - - for (i = 0; i <= splitscreen; i++) - { - uint8_t signature[SIGNATURELENGTH]; - profile_t *localProfile = PR_GetLocalPlayerProfile(i); - - if (PR_IsLocalPlayerGuest(i)) // GUESTS don't have keys - { - memset(signature, 0, sizeof(signature)); - } - else - { - // If our keys are garbage (corrupted profile?), fail here instead of when the server boots us, so the player knows what's going on. - crypto_eddsa_sign(signature, localProfile->secret_key, awaitingChallenge, sizeof(awaitingChallenge)); - if (crypto_eddsa_check(signature, localProfile->public_key, awaitingChallenge, sizeof(awaitingChallenge)) != 0) - I_Error("Couldn't self-verify key associated with player %d, profile %d.\nProfile data may be corrupted.", i, cv_lastprofile[i].value); // I guess this is the most reasonable way to catch a malformed key. - } - - #ifdef DEVELOP - if (cv_badjoin.value) - { - CV_AddValue(&cv_badjoin, -1); - CONS_Alert(CONS_WARNING, "cv_badjoin enabled, scrubbing signature from CL_SendJoin\n"); - memset(signature, 0, sizeof(signature)); - } - #endif - - // Testing - // memset(signature, 0, sizeof(signature)); - - memcpy(&netbuffer->u.clientcfg.challengeResponse[i], signature, sizeof(signature)); - } - - return HSendPacket(servernode, false, 0, sizeof (clientconfig_pak)); -} - -static boolean CL_SendKey(void) -{ - int i; - netbuffer->packettype = PT_CLIENTKEY; - - memset(netbuffer->u.clientkey.key, 0, sizeof(((clientkey_pak *)0)->key)); - for (i = 0; i <= splitscreen; i++) - { - // GUEST profiles have all-zero keys. This will be handled at the end of the challenge process, don't worry about it. - memcpy(netbuffer->u.clientkey.key[i], PR_GetProfile(cv_lastprofile[i].value)->public_key, PUBKEYLENGTH); - } - return HSendPacket(servernode, false, 0, sizeof (clientkey_pak) ); -} - -static void SV_SendServerInfo(INT32 node, tic_t servertime) -{ - UINT8 *p; - size_t mirror_length; - const char *httpurl = cv_httpsource.string; - - netbuffer->packettype = PT_SERVERINFO; - netbuffer->u.serverinfo._255 = 255; - netbuffer->u.serverinfo.packetversion = PACKETVERSION; - - netbuffer->u.serverinfo.version = VERSION; - netbuffer->u.serverinfo.subversion = SUBVERSION; - - memcpy(netbuffer->u.serverinfo.commit, - comprevision_abbrev_bin, GIT_SHA_ABBREV); - - strncpy(netbuffer->u.serverinfo.application, SRB2APPLICATION, - sizeof netbuffer->u.serverinfo.application); - // return back the time value so client can compute their ping - netbuffer->u.serverinfo.time = (tic_t)LONG(servertime); - netbuffer->u.serverinfo.leveltime = (tic_t)LONG(leveltime); - - netbuffer->u.serverinfo.numberofplayer = (UINT8)D_NumPlayers(); - netbuffer->u.serverinfo.maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value)); - - if (!node) - netbuffer->u.serverinfo.refusereason = 0; - else if (!cv_allownewplayer.value) - netbuffer->u.serverinfo.refusereason = 1; - else if (D_NumPlayers() >= cv_maxconnections.value) - netbuffer->u.serverinfo.refusereason = 2; - else - netbuffer->u.serverinfo.refusereason = 0; - - strncpy(netbuffer->u.serverinfo.gametypename, gametypes[gametype]->name, - sizeof netbuffer->u.serverinfo.gametypename); - netbuffer->u.serverinfo.modifiedgame = (UINT8)modifiedgame; - netbuffer->u.serverinfo.cheatsenabled = CV_CheatsEnabled(); - - netbuffer->u.serverinfo.kartvars = (UINT8) ( - (gamespeed & SV_SPEEDMASK) | - (dedicated ? SV_DEDICATED : 0) - ); - - D_ParseCarets(netbuffer->u.serverinfo.servername, cv_servername.string, MAXSERVERNAME); - - M_Memcpy(netbuffer->u.serverinfo.mapmd5, mapmd5, 16); - - if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) && !(mapheaderinfo[gamemap-1]->zonttl[0])) - netbuffer->u.serverinfo.iszone = 1; - else - netbuffer->u.serverinfo.iszone = 0; - - memset(netbuffer->u.serverinfo.maptitle, 0, sizeof netbuffer->u.serverinfo.maptitle); - - if (!(mapheaderinfo[gamemap-1]->menuflags & LF2_HIDEINMENU) && mapheaderinfo[gamemap-1]->lvlttl[0]) - { - //strncpy(netbuffer->u.serverinfo.maptitle, (char *)mapheaderinfo[gamemap-1]->lvlttl, sizeof netbuffer->u.serverinfo.maptitle); - // set up the levelstring - if (netbuffer->u.serverinfo.iszone || (mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE)) - { - if (snprintf(netbuffer->u.serverinfo.maptitle, - sizeof netbuffer->u.serverinfo.maptitle, - "%s", - mapheaderinfo[gamemap-1]->lvlttl) < 0) - { - // If there's an encoding error, send "Unknown", we accept that the above may be truncated - strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", sizeof netbuffer->u.serverinfo.maptitle); - } - } - else - { - if (snprintf(netbuffer->u.serverinfo.maptitle, - sizeof netbuffer->u.serverinfo.maptitle, - "%s %s", - mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->zonttl) < 0) - { - // If there's an encoding error, send "Unknown", we accept that the above may be truncated - strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", sizeof netbuffer->u.serverinfo.maptitle); - } - } - } - else - strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", sizeof netbuffer->u.serverinfo.maptitle); - - netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum; - - memset(netbuffer->u.serverinfo.httpsource, 0, MAX_MIRROR_LENGTH); - - mirror_length = strlen(httpurl); - if (mirror_length > MAX_MIRROR_LENGTH) - mirror_length = MAX_MIRROR_LENGTH; - - if (snprintf(netbuffer->u.serverinfo.httpsource, mirror_length+1, "%s", httpurl) < 0) - // If there's an encoding error, send nothing, we accept that the above may be truncated - strncpy(netbuffer->u.serverinfo.httpsource, "", mirror_length); - - netbuffer->u.serverinfo.httpsource[MAX_MIRROR_LENGTH-1] = '\0'; - - if (K_UsingPowerLevels() != PWRLV_DISABLED) - netbuffer->u.serverinfo.avgpwrlv = K_CalculatePowerLevelAvg(); - else - netbuffer->u.serverinfo.avgpwrlv = -1; - - p = PutFileNeeded(0); - - HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u)); -} - -static void SV_SendPlayerInfo(INT32 node) -{ - UINT8 i; - netbuffer->packettype = PT_PLAYERINFO; - - for (i = 0; i < MSCOMPAT_MAXPLAYERS; i++) - { - if (i >= MAXPLAYERS) - { - netbuffer->u.playerinfo[i].num = 255; // Master Server compat - continue; - } - - if (!playeringame[i]) - { - netbuffer->u.playerinfo[i].num = 255; // This slot is empty. - continue; - } - - netbuffer->u.playerinfo[i].num = i; - strncpy(netbuffer->u.playerinfo[i].name, (const char *)&player_names[i], MAXPLAYERNAME+1); - netbuffer->u.playerinfo[i].name[MAXPLAYERNAME] = '\0'; - - //fetch IP address - //No, don't do that, you fuckface. - memset(netbuffer->u.playerinfo[i].address, 0, 4); - - if (players[i].spectator) - { - netbuffer->u.playerinfo[i].team = 255; - } - else - { - if (G_GametypeHasTeams()) - { - if (players[i].team == TEAM_UNASSIGNED) - { - netbuffer->u.playerinfo[i].team = 255; - } - else - { - netbuffer->u.playerinfo[i].team = players[i].team; - } - } - else - { - netbuffer->u.playerinfo[i].team = 0; - } - } - - netbuffer->u.playerinfo[i].score = LONG(players[i].score); - netbuffer->u.playerinfo[i].timeinserver = SHORT((UINT16)(players[i].jointime / TICRATE)); - netbuffer->u.playerinfo[i].skin = (UINT8)(players[i].skin); - - // Extra data - netbuffer->u.playerinfo[i].data = 0; //players[i].skincolor; - } - - HSendPacket(node, false, 0, sizeof(plrinfo) * MSCOMPAT_MAXPLAYERS); -} - -/** Sends a PT_SERVERCFG packet - * - * \param node The destination - * \return True if the packet was successfully sent - * - */ -static boolean SV_SendServerConfig(INT32 node) -{ - boolean waspacketsent; - - memset(&netbuffer->u.servercfg, 0, sizeof netbuffer->u.servercfg); - - netbuffer->packettype = PT_SERVERCFG; - - netbuffer->u.servercfg.version = VERSION; - netbuffer->u.servercfg.subversion = SUBVERSION; - - netbuffer->u.servercfg.serverplayer = (UINT8)serverplayer; - netbuffer->u.servercfg.totalslotnum = (UINT8)(doomcom->numslots); - netbuffer->u.servercfg.gametic = (tic_t)LONG(gametic); - netbuffer->u.servercfg.clientnode = (UINT8)node; - netbuffer->u.servercfg.gamestate = (UINT8)gamestate; - netbuffer->u.servercfg.gametype = (UINT8)gametype; - netbuffer->u.servercfg.modifiedgame = (UINT8)modifiedgame; - - netbuffer->u.servercfg.maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value)); - netbuffer->u.servercfg.allownewplayer = cv_allownewplayer.value; - netbuffer->u.servercfg.discordinvites = (boolean)cv_discordinvites.value; - - memcpy(netbuffer->u.servercfg.server_context, server_context, 8); - - D_ParseCarets(netbuffer->u.servercfg.server_name, cv_servername.string, MAXSERVERNAME); - D_ParseCarets(netbuffer->u.servercfg.server_contact, cv_server_contact.string, MAXSERVERCONTACT); - - { - const size_t len = sizeof (serverconfig_pak); - -#ifdef DEBUGFILE - if (debugfile) - { - fprintf(debugfile, "ServerConfig Packet about to be sent, size of packet:%s to node:%d\n", - sizeu1(len), node); - } -#endif - - waspacketsent = HSendPacket(node, true, 0, len); - } - -#ifdef DEBUGFILE - if (debugfile) - { - if (waspacketsent) - { - fprintf(debugfile, "ServerConfig Packet was sent\n"); - } - else - { - fprintf(debugfile, "ServerConfig Packet could not be sent right now\n"); - } - } -#endif - - return waspacketsent; -} - -static boolean SV_ResendingSavegameToAnyone(void) -{ - INT32 i; - - for (i = 0; i < MAXNETNODES; i++) - if (resendingsavegame[i]) - return true; - return false; -} - -static void SV_SendSaveGame(INT32 node, boolean resending) -{ - size_t length, compressedlen; - savebuffer_t save = {0}; - UINT8 *compressedsave; - UINT8 *buffertosend; - - // first save it in a malloced buffer - if (P_SaveBufferAlloc(&save, NETSAVEGAMESIZE) == false) - { - CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n")); - return; - } - - // Leave room for the uncompressed length. - save.p += sizeof(UINT32); - - P_SaveNetGame(&save, resending); - - length = save.p - save.buffer; - if (length > NETSAVEGAMESIZE) - { - P_SaveBufferFree(&save); - I_Error("Savegame buffer overrun"); - } - - // Allocate space for compressed save: one byte fewer than for the - // uncompressed data to ensure that the compression is worthwhile. - compressedsave = Z_Malloc(length - 1, PU_STATIC, NULL); - if (!compressedsave) - { - CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n")); - return; - } - - // Attempt to compress it. - if ((compressedlen = lzf_compress(save.buffer + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1))) - { - // Compressing succeeded; send compressed data - P_SaveBufferFree(&save); - - // State that we're compressed. - buffertosend = compressedsave; - WRITEUINT32(compressedsave, length - sizeof(UINT32)); - length = compressedlen + sizeof(UINT32); - } - else - { - // Compression failed to make it smaller; send original - Z_Free(compressedsave); - - // State that we're not compressed - buffertosend = save.buffer; - WRITEUINT32(save.buffer, 0); - } - - AddRamToSendQueue(node, buffertosend, length, SF_Z_RAM, 0); - - // Remember when we started sending the savegame so we can handle timeouts - sendingsavegame[node] = true; - freezetimeout[node] = I_GetTime() + jointimeout + length / 1024; // 1 extra tic for each kilobyte -} - -static void CL_DumpConsistency(const char *file_name) -{ - size_t length; - savebuffer_t save = {0}; - char tmpsave[1024]; - - snprintf(tmpsave, sizeof(tmpsave), "%s" PATHSEP "%s", srb2home, file_name); - - // first save it in a malloced buffer - if (P_SaveBufferAlloc(&save, NETSAVEGAMESIZE) == false) - { - CONS_Alert(CONS_ERROR, M_GetText("No more free memory for consistency dump\n")); - return; - } - - P_SaveNetGame(&save, false); - - length = save.p - save.buffer; - if (length > NETSAVEGAMESIZE) - { - P_SaveBufferFree(&save); - I_Error("Consistency dump buffer overrun"); - } - - // then save it! - if (!FIL_WriteFile(tmpsave, save.buffer, length)) - CONS_Printf(M_GetText("Didn't save %s for consistency dump"), tmpsave); - - P_SaveBufferFree(&save); -} - -#define TMPSAVENAME "$$$.sav" - -static void CL_LoadReceivedSavegame(boolean reloading) -{ - savebuffer_t save = {0}; - size_t length, decompressedlen; - char tmpsave[256]; - - sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home); - - if (P_SaveBufferFromFile(&save, tmpsave) == false) - { - I_Error("Can't read savegame sent"); - return; - } - - length = save.size; - CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length)); - - // Decompress saved game if necessary. - decompressedlen = READUINT32(save.p); - if (decompressedlen > 0) - { - UINT8 *decompressedbuffer = Z_Malloc(decompressedlen, PU_STATIC, NULL); - - lzf_decompress(save.p, length - sizeof(UINT32), decompressedbuffer, decompressedlen); - - P_SaveBufferFree(&save); - P_SaveBufferFromExisting(&save, decompressedbuffer, decompressedlen); - } - - paused = false; - demo.playback = false; - demo.attract = DEMO_ATTRACT_OFF; - titlemapinaction = false; - tutorialchallenge = TUTORIALSKIP_NONE; - automapactive = false; - - // load a base level - if (P_LoadNetGame(&save, reloading)) - { - if (!reloading) - { - CON_LogMessage(va(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap))); - - if (strlen(mapheaderinfo[gamemap-1]->lvlttl) > 0) - { - CON_LogMessage(va(": %s", mapheaderinfo[gamemap-1]->lvlttl)); - if (strlen(mapheaderinfo[gamemap-1]->zonttl) > 0) - CON_LogMessage(va(" %s", mapheaderinfo[gamemap-1]->zonttl)); - else if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE)) - CON_LogMessage(M_GetText(" Zone")); - if (mapheaderinfo[gamemap-1]->actnum > 0) - CON_LogMessage(va(" %d", mapheaderinfo[gamemap-1]->actnum)); - } - - CON_LogMessage("\"\n"); - } - } - - // done - P_SaveBufferFree(&save); - - if (unlink(tmpsave) == -1) - { - CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave); - } - - consistancy[gametic%BACKUPTICS] = Consistancy(); - CON_ToggleOff(); - - // Tell the server we have received and reloaded the gamestate - // so they know they can resume the game - netbuffer->packettype = PT_RECEIVEDGAMESTATE; - HSendPacket(servernode, true, 0, 0); - - if (reloading) - { - int i; - for (i = 0; i < MAXPLAYERS; i++) - { - if (memcmp(priorKeys[i], players[i].public_key, sizeof(priorKeys[i])) != 0) - { - HandleSigfail("Gamestate reload contained new keys"); - break; - } - } - } -} - -static void CL_ReloadReceivedSavegame(void) -{ - extern consvar_t cv_dumpconsistency; - if (cv_dumpconsistency.value) - { - CL_DumpConsistency("TEMP.consdump"); - } - - INT32 i; - for (i = 0; i < MAXPLAYERS; i++) - { - LUA_InvalidatePlayer(&players[i]); - sprintf(player_names[i], "Player %c", 'A' + i); - } - - CL_LoadReceivedSavegame(true); - - if (neededtic < gametic) - neededtic = gametic; - maketic = neededtic; - - for (i = 0; i <= r_splitscreen; i++) - { - P_ForceLocalAngle(&players[displayplayers[i]], players[displayplayers[i]].angleturn); - } - - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - { - camera[i].subsector = R_PointInSubsector(camera[i].x, camera[i].y); - } - - cl_redownloadinggamestate = false; - - CONS_Printf(M_GetText("Game state reloaded\n")); - - if (cv_dumpconsistency.value) - { - // This is dumb, but we want the file names - // to be pairable together with the server's - // version, and gametic being randomly off - // is a deal breaker. - char dump_name[1024]; - snprintf( - dump_name, sizeof(dump_name), - "%s_%u_%s-client.consdump", - server_context, - gametic, - player_names[consoleplayer] - ); - if (FIL_RenameFile("TEMP.consdump", dump_name) == false) - { - CONS_Alert(CONS_WARNING, "Failed to rename temporary consdump file.\n"); - } - } -} - -static void SendAskInfo(INT32 node) -{ - tic_t asktime; - - if (node != 0 && node != BROADCASTADDR && - cv_rendezvousserver.string[0]) - { - I_NetRequestHolePunch(node); - } - - asktime = I_GetTime(); - - netbuffer->packettype = PT_ASKINFO; - netbuffer->u.askinfo.version = VERSION; - netbuffer->u.askinfo.time = (tic_t)LONG(asktime); - - // Even if this never arrives due to the host being firewalled, we've - // now allowed traffic from the host to us in, so once the MS relays - // our address to the host, it'll be able to speak to us. - HSendPacket(node, false, 0, sizeof (askinfo_pak)); -} - -serverelem_t serverlist[MAXSERVERLIST]; -UINT32 serverlistcount = 0; -UINT32 serverlistultimatecount = 0; - -static boolean resendserverlistnode[MAXNETNODES]; -static tic_t serverlistepoch; - -static void SL_ClearServerList(INT32 connectedserver) -{ - UINT32 i; - - for (i = 0; i < serverlistcount; i++) - if (connectedserver != serverlist[i].node) - { - Net_CloseConnection(serverlist[i].node|FORCECLOSE); - serverlist[i].node = 0; - } - serverlistcount = 0; - - memset(resendserverlistnode, 0, sizeof resendserverlistnode); -} - -static UINT32 SL_SearchServer(INT32 node) -{ - UINT32 i; - for (i = 0; i < serverlistcount; i++) - if (serverlist[i].node == node) - return i; - - return UINT32_MAX; -} - -static boolean SL_InsertServer(serverinfo_pak* info, SINT8 node) -{ - UINT32 i; - - resendserverlistnode[node] = false; - - // search if not already on it - i = SL_SearchServer(node); - if (i == UINT32_MAX) - { - // not found, check for packet format rejections - - if (serverlistcount >= MAXSERVERLIST) - return false; // list full - - if (info->_255 != 255) - return false; // old packet format - - if (info->packetversion != PACKETVERSION) - return false; // old new packet format - - if (info->version != VERSION) - return false; // Not same version. - - if (info->subversion != SUBVERSION) - return false; // Close, but no cigar. - - if (strcmp(info->application, SRB2APPLICATION)) - return false; // that's a different mod - } - - const INT32 gtidentifier = G_GetGametypeByName(info->gametypename); - UINT8 gtcalc = GTCALC_RACE; - if (gtidentifier != GT_RACE) - { - gtcalc = (gtidentifier == GT_BATTLE) ? GTCALC_BATTLE : GTCALC_CUSTOM; - } - - if (i == UINT32_MAX) - { - // Still not added to list... check for modifiedgame rejections - if (serverlistultimatecount) - { - // We're on the server browser page. We can reject based on our room. - if ( - ( - info->modifiedgame != false // self-declared - || (gtcalc == GTCALC_CUSTOM) // not a main two gametype - ) != (mpmenu.room == 1) - ) - { - return false; // CORE vs MODDED! - } - } - - // Ok, FINALLY now we can confirm - i = serverlistcount++; - } - - serverlist[i].info = *info; - serverlist[i].node = node; - serverlist[i].cachedgtcalc = gtcalc; - - // resort server list - M_SortServerList(); - - return true; -} - -void CL_QueryServerList (msg_server_t *server_list) -{ - INT32 i; - - CL_UpdateServerList(); - - serverlistepoch = I_GetTime(); - - for (i = 0; server_list[i].header.buffer[0]; i++) - { - // Make sure MS version matches our own, to - // thwart nefarious servers who lie to the MS. - - /* lol bruh, that version COMES from the servers */ - //if (strcmp(version, server_list[i].version) == 0) - { - INT32 node = I_NetMakeNodewPort(server_list[i].ip, server_list[i].port); - if (node == -1) - break; // no more node free - SendAskInfo(node); - - resendserverlistnode[node] = true; - // Leave this node open. It'll be closed if the - // request times out (CL_TimeoutServerList). - } - } - - serverlistultimatecount = i; -} - -#define SERVERLISTRESENDRATE NEWTICRATE - -void CL_TimeoutServerList(void) -{ - if (netgame && serverlistultimatecount > serverlistcount) - { - const tic_t timediff = I_GetTime() - serverlistepoch; - const tic_t timetoresend = timediff % SERVERLISTRESENDRATE; - const boolean timedout = timediff > connectiontimeout; - - if (timedout || (timediff > 0 && timetoresend == 0)) - { - INT32 node; - - for (node = 1; node < MAXNETNODES; ++node) - { - if (resendserverlistnode[node]) - { - if (timedout) - Net_CloseConnection(node|FORCECLOSE); - else - SendAskInfo(node); - } - } - - if (timedout) - serverlistultimatecount = serverlistcount; - } - } -} - -void CL_UpdateServerList (void) -{ - SL_ClearServerList(0); - - if (!netgame && I_NetOpenSocket) - { - if (I_NetOpenSocket()) - { - netgame = true; - multiplayer = true; - } - } - - // search for local servers - if (netgame) - SendAskInfo(BROADCASTADDR); -} - -static void M_ConfirmConnect(INT32 choice) -{ - if (choice == MA_YES) - { - if (totalfilesrequestednum > 0) - { - #ifdef HAVE_CURL - if (http_source[0] == '\0' || curl_failedwebdownload) - #endif - { - if (CL_SendFileRequest()) - { - cl_mode = CL_DOWNLOADFILES; - } - else - { - cl_mode = CL_DOWNLOADFAILED; - } - } - #ifdef HAVE_CURL - else - cl_mode = CL_PREPAREHTTPFILES; - #endif - } - else - cl_mode = CL_LOADFILES; - - return; - } - - cl_mode = CL_ABORTED; -} - -static boolean CL_FinishedFileList(void) -{ - INT32 i; - char *downloadsize = NULL; - //CONS_Printf(M_GetText("Checking files...\n")); - i = CL_CheckFiles(); - if (i == 4) // still checking ... - { - return true; - } - else if (i == 3) // too many files - { - Command_ExitGame_f(); - M_StartMessage("Server Connection Failure", - M_GetText( - "You have too many WAD files loaded\n" - "to add ones the server is using.\n" - "Please restart Ring Racers before connecting.\n" - ), NULL, MM_NOTHING, NULL, "Back to Menu"); - return false; - } - else if (i == 2) // cannot join for some reason - { - Command_ExitGame_f(); - M_StartMessage("Server Connection Failure", - M_GetText( - "You have the wrong addons loaded.\n\n" - "To play on this server, restart\n" - "the game and don't load any addons.\n" - "Ring Racers will automatically add\n" - "everything you need when you join.\n" - ), NULL, MM_NOTHING, NULL, "Back to Menu"); - return false; - } - else if (i == 1) - { - if (serverisfull) - { - M_StartMessage("Server Connection Failure", - M_GetText( - "This server is full!\n" - "\n" - "You may load server addons (if any), and wait for a slot.\n" - ), &M_ConfirmConnect, MM_YESNO, "Continue", "Back to Menu"); - cl_mode = CL_CONFIRMCONNECT; - } - else - cl_mode = CL_LOADFILES; - } - else - { - // must download something - // can we, though? -#ifdef HAVE_CURL - if (http_source[0] == '\0' || curl_failedwebdownload) -#endif - { - if (!CL_CheckDownloadable()) // nope! - { - Command_ExitGame_f(); - M_StartMessage("Server Connection Failure", - M_GetText( - "An error occured when trying to\n" - "download missing addons.\n" - "(This is almost always a problem\n" - "with the server, not your game.)\n\n" - "See the console or log file\n" - "for additional details.\n" - ), NULL, MM_NOTHING, NULL, "Back to Menu"); - return false; - } - } - -#ifdef HAVE_CURL - if (!curl_failedwebdownload) -#endif - { - downloadcompletednum = 0; - downloadcompletedsize = 0; - totalfilesrequestednum = 0; - totalfilesrequestedsize = 0; - - for (i = 0; i < fileneedednum; i++) - if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD) - { - totalfilesrequestednum++; - totalfilesrequestedsize += fileneeded[i].totalsize; - } - - if (totalfilesrequestedsize>>20 >= 10) - downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20)); - else - downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10)); - - if (serverisfull) - M_StartMessage("Server Connection", - va(M_GetText( - "This server is full!\n" - "Download of %s additional content\n" - "is required to join.\n" - "\n" - "You may download, load server addons,\n" - "and wait for a slot.\n" - ), downloadsize), &M_ConfirmConnect, MM_YESNO, "Continue", "Back to Menu"); - else - M_StartMessage("Server Connection", - va(M_GetText( - "Download of %s additional content\n" - "is required to join.\n" - ), downloadsize), &M_ConfirmConnect, MM_YESNO, "Continue", "Back to Menu"); - - Z_Free(downloadsize); - cl_mode = CL_CONFIRMCONNECT; - } -#ifdef HAVE_CURL - else - { - if (CL_SendFileRequest()) - { - cl_mode = CL_DOWNLOADFILES; - } - else - { - cl_mode = CL_DOWNLOADFAILED; - } - } -#endif - } - return true; -} - -/** Called by CL_ServerConnectionTicker - * - * \param asksent The last time we asked the server to join. We re-ask every second in case our request got lost in transmit. - * \return False if the connection was aborted - * \sa CL_ServerConnectionTicker - * \sa CL_ConnectToServer - * - */ -static boolean CL_ServerConnectionSearchTicker(tic_t *asksent) -{ - INT32 i; - - // serverlist is updated by GetPacket function - if (serverlistcount > 0) - { - // this can be a responce to our broadcast request - if (servernode == -1 || servernode >= MAXNETNODES) - { - i = 0; - servernode = serverlist[i].node; - CONS_Printf(M_GetText("Found, ")); - } - else - { - i = SL_SearchServer(servernode); - if (i < 0) - return true; - } - - // Quit here rather than downloading files and being refused later. - if (serverlist[i].info.refusereason) - { - serverisfull = true; - } - - if (client) - { -#ifdef DEVELOP - // Commits do not match? Do not connect! - if (memcmp(serverlist[i].info.commit, - comprevision_abbrev_bin, - GIT_SHA_ABBREV)) - { - char theirs[GIT_SHA_ABBREV * 2 + 1]; - UINT8 n; - - for (n = 0; n < GIT_SHA_ABBREV; ++n) - { - sprintf(&theirs[n * 2], "%02hhx", - serverlist[i].info.commit[n]); - } - - Command_ExitGame_f(); - - M_StartMessage("Server Connection Failure", - va( - "Your EXE differs from the server.\n" - " Yours: %.*s\n" - "Theirs: %s\n\n", - GIT_SHA_ABBREV * 2, comprevision, theirs), NULL, MM_NOTHING, NULL, "Back to Menu"); - return false; - } -#endif - -#ifdef HAVE_CURL - if (serverlist[i].info.httpsource[0]) - strncpy(http_source, serverlist[i].info.httpsource, MAX_MIRROR_LENGTH); - else - http_source[0] = '\0'; -#else - if (serverlist[i].info.httpsource[0]) - CONS_Printf("We received a http url from the server, however it will not be used as this build lacks curl support (%s)\n", serverlist[i].info.httpsource); -#endif - D_ParseFileneeded(serverlist[i].info.fileneedednum, serverlist[i].info.fileneeded, 0); - if (serverlist[i].info.kartvars & SV_LOTSOFADDONS) - { - cl_mode = CL_ASKFULLFILELIST; - cl_lastcheckedfilecount = 0; - return true; - } - - cl_mode = CL_CHECKFILES; - } - else - { - cl_mode = CL_ASKJOIN; // files need not be checked for the server. - *asksent = 0; - } - - return true; - } - - // Ask the info to the server (askinfo packet) - if (I_GetTime() >= *asksent) - { - SendAskInfo(servernode); - *asksent = I_GetTime() + NEWTICRATE; - } - - return true; -} - -/** Called by CL_ConnectToServer - * - * \param tmpsave The name of the gamestate file??? - * \param oldtic Used for knowing when to poll events and redraw - * \param asksent The last time we asked the server to join. We re-ask every second in case our request got lost in transmit. - * \return False if the connection was aborted - * \sa CL_ServerConnectionSearchTicker - * \sa CL_ConnectToServer - * - */ -static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic_t *asksent) -{ - boolean waitmore; - INT32 i; - const UINT8 pid = 0; - - switch (cl_mode) - { - case CL_SEARCHING: - if (!CL_ServerConnectionSearchTicker(asksent)) - return false; - break; - - case CL_ASKFULLFILELIST: - if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved - cl_mode = CL_CHECKFILES; - else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent) - { - if (CL_AskFileList(fileneedednum)) - { - cl_lastcheckedfilecount = fileneedednum; - *asksent = I_GetTime() + NEWTICRATE; - } - } - break; - case CL_CHECKFILES: - if (!CL_FinishedFileList()) - return false; - break; -#ifdef HAVE_CURL - case CL_PREPAREHTTPFILES: - if (http_source[0]) - { - for (i = 0; i < fileneedednum; i++) - if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD) - { - curl_transfers++; - } - - cl_mode = CL_DOWNLOADHTTPFILES; - } - break; - - case CL_DOWNLOADHTTPFILES: - waitmore = false; - for (i = 0; i < fileneedednum; i++) - if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD) - { - if (!curl_running) - CURLPrepareFile(http_source, i); - waitmore = true; - break; - } - - if (curl_running) - CURLGetFile(); - - if (waitmore) - break; // exit the case - - if (curl_failedwebdownload && !curl_transfers) - { - CONS_Printf("One or more files failed to download, falling back to internal downloader\n"); - cl_mode = CL_CHECKFILES; - break; - } - - if (!curl_transfers) - cl_mode = CL_LOADFILES; - - break; -#endif - case CL_DOWNLOADFILES: - waitmore = false; - for (i = 0; i < fileneedednum; i++) - if (fileneeded[i].status == FS_DOWNLOADING - || fileneeded[i].status == FS_REQUESTED) - { - waitmore = true; - break; - } - if (waitmore) - break; // exit the case - - cl_mode = CL_LOADFILES; - break; - case CL_DOWNLOADFAILED: - { - CONS_Printf(M_GetText("Legacy downloader request packet failed.\n")); - CONS_Printf(M_GetText("Network game synchronization aborted.\n")); - Command_ExitGame_f(); - M_StartMessage("Server Connection Failure", - M_GetText( - "The direct download encountered an error.\n" - "See the logfile for more info.\n" - ), NULL, MM_NOTHING, NULL, "Back to Menu"); - return false; - } - case CL_LOADFILES: - if (CL_LoadServerFiles()) - cl_mode = CL_SETUPFILES; - - break; - case CL_SETUPFILES: - if (P_PartialAddGetStage() < 0 || P_MultiSetupWadFiles(false)) - { - *asksent = 0; //This ensure the first join ask is right away - firstconnectattempttime = I_GetTime(); - cl_mode = CL_SENDKEY; - } - break; - case CL_ASKJOIN: - if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server) - { - CONS_Printf(M_GetText("5 minute wait time exceeded.\n")); - CONS_Printf(M_GetText("Network game synchronization aborted.\n")); - Command_ExitGame_f(); - M_StartMessage("Server Connection Failure", - M_GetText( - "5 minute wait time exceeded.\n" - "You may retry connection.\n" - ), NULL, MM_NOTHING, NULL, "Return to Menu"); - return false; - } - // prepare structures to save the file - // WARNING: this can be useless in case of server not in GS_LEVEL - // but since the network layer doesn't provide ordered packets... - CL_PrepareDownloadSaveGame(tmpsave); - if (I_GetTime() >= *asksent && CL_SendJoin()) - { - *asksent = I_GetTime() + NEWTICRATE*3; - cl_mode = CL_WAITJOINRESPONSE; - } - break; - case CL_WAITJOINRESPONSE: - if (I_GetTime() >= *asksent) - { - cl_mode = CL_ASKJOIN; - } - break; - case CL_SENDKEY: - if (I_GetTime() >= *asksent && CL_SendKey()) - { - *asksent = I_GetTime() + NEWTICRATE*3; - cl_mode = CL_WAITCHALLENGE; - } - break; - case CL_WAITCHALLENGE: - if (I_GetTime() >= *asksent) - { - cl_mode = CL_SENDKEY; - } - break; - case CL_DOWNLOADSAVEGAME: - // At this state, the first (and only) needed file is the gamestate - if (fileneeded[0].status == FS_FOUND) - { - // Gamestate is now handled within CL_LoadReceivedSavegame() - CL_LoadReceivedSavegame(false); - cl_mode = CL_CONNECTED; - break; - } // don't break case continue to CL_CONNECTED - else - break; - case CL_CONNECTED: - case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect - default: - break; - - // Connection closed by cancel, timeout or refusal. - case CL_ABORTED: - cl_mode = CL_SEARCHING; - return false; - - } - - GetPackets(); - Net_AckTicker(); - - // Call it only once by tic - if (*oldtic != I_GetTime()) - { - I_OsPolling(); - - // Needs to be updated here for M_DrawEggaChannelAlignable - renderdeltatics = FRACUNIT; - rendertimefrac = FRACUNIT; - - G_ResetAllDeviceResponding(); - - if (netgame) - { - for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1)) - { - HandleGamepadDeviceEvents(&events[eventtail]); - G_MapEventsToControls(&events[eventtail]); - } - -#ifdef HAVE_THREADS - I_lock_mutex(&k_menu_mutex); -#endif - M_UpdateMenuCMD(0, true); - - if (cl_mode == CL_CONFIRMCONNECT) - { - if (menumessage.active) - M_HandleMenuMessage(); - } - else - { - if (M_MenuBackPressed(pid)) - cl_mode = CL_ABORTED; - } - - M_ScreenshotTicker(); - -#ifdef HAVE_THREADS - I_unlock_mutex(k_menu_mutex); -#endif - } - - if (cl_mode == CL_ABORTED) - { - CONS_Printf(M_GetText("Network game synchronization aborted.\n")); -// M_StartMessage("Server Connection", M_GetText("Network game synchronization aborted.\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - - Command_ExitGame_f(); - return false; - } - - if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME)) - FileReceiveTicker(); - - // why are these here? this is for servers, we're a client - //if (key == 's' && server) - // doomcom->numnodes = (INT16)pnumnodes; - //FileSendTicker(); - *oldtic = I_GetTime(); - - if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED) - { - if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADSAVEGAME) - { - M_DrawEggaChannelAlignable(true); - } - CL_DrawConnectionStatus(); - - if (cl_mode == CL_CONFIRMCONNECT) - { -#ifdef HAVE_THREADS - I_lock_mutex(&k_menu_mutex); -#endif - M_DrawMenuMessage(); -#ifdef HAVE_THREADS - I_unlock_mutex(k_menu_mutex); -#endif - } - I_UpdateNoVsync(); // page flip or blit buffer - -#ifdef HWRENDER - // Only take screenshots after drawing. - if (moviemode && rendermode == render_opengl) - M_LegacySaveFrame(); - if (rendermode == render_opengl && takescreenshot) - M_DoLegacyGLScreenShot(); -#endif - - if ((moviemode || takescreenshot) && rendermode == render_soft) - I_CaptureVideoFrame(); - S_UpdateSounds(); - S_UpdateClosedCaptions(); - } - } - else - { - I_Sleep(cv_sleep.value); - I_UpdateTime(); - } - - return true; -} - -/** Use adaptive send using net_bandwidth and stat.sendbytes - * - * \todo Better description... - * - */ -static void CL_ConnectToServer(void) -{ - INT32 pnumnodes, nodewaited = doomcom->numnodes, i; - tic_t oldtic; - tic_t asksent; - char tmpsave[256]; - - sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home); - - lastfilenum = -1; - - cl_mode = CL_SEARCHING; - - // Don't get a corrupt savegame error because tmpsave already exists - if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1) - I_Error("Can't delete %s\n", tmpsave); - - if (netgame) - { - if (servernode < 0 || servernode >= MAXNETNODES) - CONS_Printf(M_GetText("Searching for a server...\n")); - else - CONS_Printf(M_GetText("Contacting the server...\n")); - } - - if (cv_currprofile.value == -1 && !demo.playback) - { - PR_ApplyProfilePretend(cv_ttlprofilen.value, 0); - for (i = 1; i < cv_splitplayers.value; i++) - { - PR_ApplyProfile(cv_lastprofile[i].value, i); - } - - // Slightly sucks that we have to duplicate these from d_main.c, but - // the change to cv_lastprofile doesn't take in time for this codepath. - if (M_CheckParm("-profile")) - { - UINT8 num = atoi(M_GetNextParm()); - PR_ApplyProfile(num, 0); - } - if (M_CheckParm("-profile2")) - { - UINT8 num = atoi(M_GetNextParm()); - PR_ApplyProfile(num, 1); - } - if (M_CheckParm("-profile3")) - { - UINT8 num = atoi(M_GetNextParm()); - PR_ApplyProfile(num, 2); - } - if (M_CheckParm("-profile4")) - { - UINT8 num = atoi(M_GetNextParm()); - PR_ApplyProfile(num, 3); - } - } - if (gamestate == GS_INTERMISSION) - Y_EndIntermission(); // clean up intermission graphics etc - if (gamestate == GS_VOTING) - Y_EndVote(); - - DEBFILE(va("waiting %d nodes\n", doomcom->numnodes)); - G_SetGamestate(GS_WAITINGPLAYERS); - if (wipegamestate == GS_MENU) - M_ClearMenus(true); - wipegamestate = GS_WAITINGPLAYERS; - - ClearAdminPlayers(); - Schedule_Clear(); - Automate_Clear(); - K_ClearClientPowerLevels(); - K_ResetMidVote(); - - pnumnodes = 1; - oldtic = 0; - asksent = 0; - firstconnectattempttime = I_GetTime(); - - i = SL_SearchServer(servernode); - - if (i != -1) - { - char *gametypestr = serverlist[i].info.gametypename; - - CON_LogMessage(va(M_GetText("Connecting to: %s\n"), serverlist[i].info.servername)); - - gametypestr[sizeof serverlist[i].info.gametypename - 1] = '\0'; - CON_LogMessage(va(M_GetText("Gametype: %s\n"), gametypestr)); - - CON_LogMessage(va(M_GetText("Version: %d.%d\n"), - serverlist[i].info.version, serverlist[i].info.subversion)); - } - SL_ClearServerList(servernode); - - do - { - // If the connection was aborted for some reason, leave - if (!CL_ServerConnectionTicker(tmpsave, &oldtic, &asksent)) - { - if (P_PartialAddGetStage() >= 0) - P_MultiSetupWadFiles(true); // in case any partial adds were done - - return; - } - - if (server) - { - pnumnodes = 0; - for (i = 0; i < MAXNETNODES; i++) - if (nodeingame[i]) - pnumnodes++; - } - } - while (!(cl_mode == CL_CONNECTED && (client || (server && nodewaited <= pnumnodes)))); - - if (netgame) - F_StartWaitingPlayers(); - DEBFILE(va("Synchronisation Finished\n")); - - displayplayers[0] = consoleplayer; - - // At this point we've succesfully joined the server, if we joined by IP (ie: a valid joinedIP string), save it! - // @TODO: Save the proper server name, right now it doesn't seem like we can consistently retrieve it from the serverlist....? - // It works... sometimes but not always which is weird. - - tmpsave[0] = '\0'; // TEMPORARY -- connectedservername is currently only set for YOUR server - if (joinedIP[0]) // false if we have "" which is \0 - M_AddToJoinedIPs(joinedIP, tmpsave); //connectedservername); -- as above - - joinedIP[0] = '\0'; // And empty this for good measure regardless of whether or not we actually used it. - -} - -static void Command_connect(void) -{ - - // By default, clear the saved address that we'd save after succesfully joining just to be sure: - joinedIP[0] = '\0'; - - if (COM_Argc() < 2 || *COM_Argv(1) == 0) - { - CONS_Printf(M_GetText( - "Connect (port): connect to a server\n" - "Connect ANY: connect to the first lan server found\n" - //"Connect SELF: connect to your own server.\n" - )); - return; - } - - if (Playing() || demo.attract) - { - CONS_Printf(M_GetText("You cannot connect while in a game. End this game first.\n")); - return; - } - - // modified game check: no longer handled - // we don't request a restart unless the filelist differs - - server = false; - - // Get the server node. - if (netgame) - { - // used in menu to connect to a server in the list - if (stricmp(COM_Argv(1), "node") != 0) - { - CONS_Printf(M_GetText("You cannot connect via address while joining a server.\n")); - return; - } - servernode = (SINT8)atoi(COM_Argv(2)); - } - else - { - // Standard behaviour - if (I_NetOpenSocket) - { - I_NetOpenSocket(); - netgame = true; - multiplayer = true; - - if (!stricmp(COM_Argv(1), "any")) - servernode = BROADCASTADDR; - else if (I_NetMakeNodewPort) - { - if (COM_Argc() >= 3) // address AND port - servernode = I_NetMakeNodewPort(COM_Argv(1), COM_Argv(2)); - else // address only, or address:port - servernode = I_NetMakeNode(COM_Argv(1)); - - // Last IPs joined: - // Keep the address we typed in memory so that we can save it if we *succesfully* join the server - strlcpy(joinedIP, COM_Argv(1), MAX_LOGIP); - } - else - { - CONS_Alert(CONS_ERROR, M_GetText("There is no server identification with this network driver\n")); - D_CloseConnection(); - return; - } - } - else - { - CONS_Alert(CONS_ERROR, M_GetText("There is no network driver\n")); - return; - } - } - - if (splitscreen != cv_splitplayers.value-1) - { - splitscreen = cv_splitplayers.value-1; - SplitScreen_OnChange(); - } - - // Menu restore state. - restoreMenu = &PLAY_MP_OptSelectDef; - currentMenu->lastOn = itemOn; - - Music_Remap("menu", "NETMD2"); - - if (stricmp(Music_CurrentSong(), "NETMD2")) - { - Music_Play("menu"); - } - - if (setup_numplayers == 0) - { - setup_numplayers = 1; - } - - CL_ConnectToServer(); -} - -static void ResetNode(INT32 node); - -// -// CL_ClearPlayer -// -// Clears the player data so that a future client can use this slot -// -void CL_ClearPlayer(INT32 playernum) -{ - int i; - - // Handle mobj_t pointers. - if (G_GamestateUsesLevel() == true) - { - if (players[playernum].follower) - { - K_RemoveFollower(&players[playernum]); - } - -#define PlayerPointerRemove(field) \ - if (P_MobjWasRemoved(field) == false) \ - { \ - P_RemoveMobj(field); \ - P_SetTarget(&field, NULL); \ - } - - // These are mostly subservient to the player, and may not clean themselves up. - PlayerPointerRemove(players[playernum].mo); - PlayerPointerRemove(players[playernum].followmobj); - PlayerPointerRemove(players[playernum].stumbleIndicator); - PlayerPointerRemove(players[playernum].wavedashIndicator); - PlayerPointerRemove(players[playernum].trickIndicator); - -#undef PlayerPointerRemove - - // These have thinkers of their own. - P_SetTarget(&players[playernum].whip, NULL); - P_SetTarget(&players[playernum].hand, NULL); - P_SetTarget(&players[playernum].hoverhyudoro, NULL); - P_SetTarget(&players[playernum].ballhogreticule, NULL); - P_SetTarget(&players[playernum].ringShooter, NULL); - - // TODO: Any better handling in store? - P_SetTarget(&players[playernum].flickyAttacker, NULL); - P_SetTarget(&players[playernum].powerup.flickyController, NULL); - P_SetTarget(&players[playernum].powerup.barrier, NULL); - - // These are camera items and possibly belong to multiple players. - P_SetTarget(&players[playernum].skybox.viewpoint, NULL); - P_SetTarget(&players[playernum].skybox.centerpoint, NULL); - P_SetTarget(&players[playernum].awayview.mobj, NULL); - - } - - // Handle parties. - for (i = 0; i < MAXPLAYERS; ++i) - { - if (splitscreen_invitations[i] == playernum) - splitscreen_invitations[i] = -1; - } - splitscreen_invitations[playernum] = -1; - - playerconsole[playernum] = playernum; - - // Wipe the struct. - memset(&players[playernum], 0, sizeof (player_t)); - - // Handle values which should not be initialised to 0. - players[playernum].followerskin = -1; // don't have a ghost follower - players[playernum].fakeskin = players[playernum].lastfakeskin = MAXSKINS; // don't avoid eggman - - // Handle post-cleanup. - RemoveAdminPlayer(playernum); // don't stay admin after you're gone -} - -// -// CL_RemovePlayer -// -// Removes a player from the current game -// -void CL_RemovePlayer(INT32 playernum, kickreason_t reason) -{ - // Sanity check: exceptional cases (i.e. c-fails) can cause multiple - // kick commands to be issued for the same player. - if (!playeringame[playernum]) - return; - - if (server && !demo.playback && playernode[playernum] != UINT8_MAX && !players[playernum].bot) - { - INT32 node = playernode[playernum]; - //playerpernode[node] = 0; // It'd be better to remove them all at once, but ghosting happened, so continue to let CL_RemovePlayer do it one-by-one - playerpernode[node]--; - if (playerpernode[node] <= 0) - { - nodeingame[node] = false; - Net_CloseConnection(node); - ResetNode(node); - } - } - - K_CalculateBattleWanted(); - - LUA_HookPlayerQuit(&players[playernum], reason); // Lua hook for player quitting - - G_LeaveParty(playernum); - - // Reset player data - CL_ClearPlayer(playernum); - - // remove avatar of player - playeringame[playernum] = false; - demo_extradata[playernum] |= DXD_PLAYSTATE; - playernode[playernum] = UINT8_MAX; - while (!playeringame[doomcom->numslots-1] && doomcom->numslots > 1) - doomcom->numslots--; - - // Reset the name - sprintf(player_names[playernum], "Player %c", 'A' + playernum); - - player_name_changes[playernum] = 0; - - LUA_InvalidatePlayer(&players[playernum]); - - // don't look through someone's view who isn't there - G_ResetViews(); - - K_CheckBumpers(); - P_CheckRacers(); -} - -void CL_Reset(void) -{ - if (demo.recording) - G_CheckDemoStatus(); - - // reset client/server code - DEBFILE(va("\n-=-=-=-=-=-=-= Client reset =-=-=-=-=-=-=-\n\n")); - - if (servernode > 0 && servernode < MAXNETNODES) - { - nodeingame[(UINT8)servernode] = false; - Net_CloseConnection(servernode); - } - D_CloseConnection(); // netgame = false - multiplayer = false; - servernode = 0; - server = true; - doomcom->numnodes = 1; - doomcom->numslots = 1; - SV_StopServer(); - SV_ResetServer(); - - // make sure we don't leave any fileneeded gunk over from a failed join - fileneedednum = 0; - memset(fileneeded, 0, sizeof(fileneeded)); - - totalfilesrequestednum = 0; - totalfilesrequestedsize = 0; - firstconnectattempttime = 0; - serverisfull = false; - connectiontimeout = (tic_t)cv_nettimeout.value; //reset this temporary hack - - expectChallenge = false; - -#ifdef HAVE_CURL - curl_failedwebdownload = false; - curl_transfers = 0; - curl_running = false; - http_source[0] = '\0'; -#endif - - G_ResetAllDeviceRumbles(); - - // D_StartTitle should get done now, but the calling function will handle it -} - -static void Command_GetPlayerNum(void) -{ - INT32 i; - - for (i = 0; i < MAXPLAYERS; i++) - if (playeringame[i]) - { - if (serverplayer == i) - CONS_Printf(M_GetText("num:%2d node:%2d %s\n"), i, playernode[i], player_names[i]); - else - CONS_Printf(M_GetText("\x82num:%2d node:%2d %s\n"), i, playernode[i], player_names[i]); - } -} - -SINT8 nametonum(const char *name) -{ - INT32 playernum, i; - - if (!strcmp(name, "0")) - return 0; - - playernum = (SINT8)atoi(name); - - if (playernum < 0 || playernum >= MAXPLAYERS) - return -1; - - if (playernum) - { - if (playeringame[playernum]) - return (SINT8)playernum; - else - return -1; - } - - for (i = 0; i < MAXPLAYERS; i++) - if (playeringame[i] && !stricmp(player_names[i], name)) - return (SINT8)i; - - CONS_Printf(M_GetText("There is no player named \"%s\"\n"), name); - - return -1; -} - -/** Lists all players and their player numbers. - * - * \sa Command_GetPlayerNum - */ -static void Command_Nodes(void) -{ - INT32 i; - size_t maxlen = 0; - const char *address; - - for (i = 0; i < MAXPLAYERS; i++) - { - const size_t plen = strlen(player_names[i]); - if (playeringame[i] && plen > maxlen) - maxlen = plen; - } - - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i]) - { - CONS_Printf("%.2u: %*s", i, (int)maxlen, player_names[i]); - - if (playernode[i] != UINT8_MAX) - { - CONS_Printf(" [node %.2d]", playernode[i]); - if (I_GetNodeAddress && (address = I_GetNodeAddress(playernode[i])) != NULL) - CONS_Printf(" - %s", address); - } - - if (K_UsingPowerLevels() != PWRLV_DISABLED) // No power type?! - { - CONS_Printf(" [%.4d PWR]", clientpowerlevels[i][K_UsingPowerLevels()]); - } - - CONS_Printf(" [%d games]", SV_GetStatsByPlayerIndex(i)->finishedrounds); - - - CONS_Printf(" [RRID-%s]", GetPrettyRRID(players[i].public_key, true)); - - if (IsPlayerAdmin(i)) - CONS_Printf(M_GetText(" (verified admin)")); - - if (players[i].spectator) - CONS_Printf(M_GetText(" (spectator)")); - - CONS_Printf("\n"); - } - } -} - -static void Command_Ban(void) -{ - if (COM_Argc() < 2) - { - CONS_Printf(M_GetText("ban : ban and kick a player\n")); - return; - } - - if (!netgame) // Don't kick Tails in splitscreen! - { - CONS_Printf(M_GetText("This only works in a netgame.\n")); - return; - } - - if (server || IsPlayerAdmin(consoleplayer)) - { - UINT8 buf[3 + MAX_REASONLENGTH]; - UINT8 *p = buf; - const SINT8 pn = nametonum(COM_Argv(1)); - - if (pn == -1 || pn == 0) - return; - - WRITEUINT8(p, pn); - - if (COM_Argc() == 2) - { - WRITEUINT8(p, KICK_MSG_BANNED); - SendNetXCmd(XD_KICK, &buf, 2); - } - else - { - size_t i, j = COM_Argc(); - char message[MAX_REASONLENGTH]; - - //Steal from the motd code so you don't have to put the reason in quotes. - strlcpy(message, COM_Argv(2), sizeof message); - for (i = 3; i < j; i++) - { - strlcat(message, " ", sizeof message); - strlcat(message, COM_Argv(i), sizeof message); - } - - WRITEUINT8(p, KICK_MSG_CUSTOM_BAN); - WRITESTRINGN(p, message, MAX_REASONLENGTH); - SendNetXCmd(XD_KICK, &buf, p - buf); - } - } - else - CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n")); -} - -static void Command_Kick(void) -{ - if (COM_Argc() < 2) - { - CONS_Printf(M_GetText("kick : kick a player\n")); - return; - } - - if (!netgame) // Don't kick Tails in splitscreen! - { - CONS_Printf(M_GetText("This only works in a netgame.\n")); - return; - } - - if (server || IsPlayerAdmin(consoleplayer)) - { - UINT8 buf[3 + MAX_REASONLENGTH]; - UINT8 *p = buf; - const SINT8 pn = nametonum(COM_Argv(1)); - - if (pn == -1 || pn == 0) - return; - - // Special case if we are trying to kick a player who is downloading the game state: - // trigger a timeout instead of kicking them, because a kick would only - // take effect after they have finished downloading - if (server && playernode[pn] != UINT8_MAX && sendingsavegame[playernode[pn]]) - { - Net_ConnectionTimeout(playernode[pn]); - return; - } - - WRITESINT8(p, pn); - - if (COM_Argc() == 2) - { - WRITEUINT8(p, KICK_MSG_KICKED); - SendNetXCmd(XD_KICK, &buf, 2); - } - else - { - size_t i, j = COM_Argc(); - char message[MAX_REASONLENGTH]; - - //Steal from the motd code so you don't have to put the reason in quotes. - strlcpy(message, COM_Argv(2), sizeof message); - for (i = 3; i < j; i++) - { - strlcat(message, " ", sizeof message); - strlcat(message, COM_Argv(i), sizeof message); - } - - WRITEUINT8(p, KICK_MSG_CUSTOM_KICK); - WRITESTRINGN(p, message, MAX_REASONLENGTH); - SendNetXCmd(XD_KICK, &buf, p - buf); - } - } - else - CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n")); -} - -static void Got_KickCmd(const UINT8 **p, INT32 playernum) -{ - INT32 pnum, msg; - char buf[3 + MAX_REASONLENGTH]; - char *reason = buf; - kickreason_t kickreason = KR_KICK; - UINT32 banMinutes = 0; - - pnum = READUINT8(*p); - msg = READUINT8(*p); - - if (msg == KICK_MSG_CUSTOM_BAN || msg == KICK_MSG_CUSTOM_KICK) - { - READSTRINGN(*p, reason, MAX_REASONLENGTH+1); - } - else - { - memset(reason, 0, sizeof(buf)); - } - - // Is playernum authorized to make this kick? - if (playernum != serverplayer && !IsPlayerAdmin(playernum) - /*&& !(playernode[playernum] != UINT8_MAX && playerpernode[playernode[playernum]] == 2 - && nodetoplayer2[playernode[playernum]] == pnum)*/) - { - // We received a kick command from someone who isn't the - // server or admin, and who isn't in splitscreen removing - // player 2. Thus, it must be someone with a modified - // binary, trying to kick someone but without having - // authorization. - - // We deal with this by changing the kick reason to - // "consistency failure" and kicking the offending user - // instead. - - CONS_Alert(CONS_WARNING, M_GetText("Illegal kick command received from %s for player %d\n"), player_names[playernum], pnum); - - // In debug, print a longer message with more details. - // TODO Callum: Should we translate this? -/* - CONS_Debug(DBG_NETPLAY, - "So, you must be asking, why is this an illegal kick?\n" - "Well, let's take a look at the facts, shall we?\n" - "\n" - "playernum (this is the guy who did it), he's %d.\n" - "pnum (the guy he's trying to kick) is %d.\n" - "playernum's node is %d.\n" - "That node has %d players.\n" - "Player 2 on that node is %d.\n" - "pnum's node is %d.\n" - "That node has %d players.\n" - "Player 2 on that node is %d.\n" - "\n" - "If you think this is a bug, please report it, including all of the details above.\n", - playernum, pnum, - playernode[playernum], playerpernode[playernode[playernum]], - nodetoplayer2[playernode[playernum]], - playernode[pnum], playerpernode[playernode[pnum]], - nodetoplayer2[playernode[pnum]]); -*/ - pnum = playernum; - msg = KICK_MSG_CON_FAIL; - } - - if (g_midVote.active == true && g_midVote.victim == &players[pnum]) - { - if (g_midVote.type == MVT_KICK) - { - // Running the callback here would mean a very dumb infinite loop. - // We'll manually handle this here by changing the msg type. - if (msg != KICK_MSG_BANNED && msg != KICK_MSG_CUSTOM_BAN) - { - // of course, don't take the teeth out of a ban - msg = KICK_MSG_VOTE_KICK; - } - K_MidVoteFinalize(FRACUNIT); // Vote succeeded, so the delay is normal. - } - else - { - // It should be safe to run the vote callback directly. - K_MidVoteSuccess(); - } - } - - if (playernode[pnum] == servernode) - { - CONS_Printf(M_GetText("Ignoring kick attempt from %s on node %d (it's the server)\n"), player_names[playernum], servernode); - return; - } - - //CONS_Printf("\x82%s ", player_names[pnum]); - - // Save bans here. Used to be split between here and the actual command, depending on - // whenever the server did it or a remote admin did it, but it's simply more convenient - // to keep it all in one place. - if (server) - { - if (msg == KICK_MSG_KICKED || msg == KICK_MSG_VOTE_KICK || msg == KICK_MSG_CUSTOM_KICK) - { - // Kick as a temporary ban. - banMinutes = cv_kicktime.value; - } - - if (msg == KICK_MSG_BANNED || msg == KICK_MSG_CUSTOM_BAN || banMinutes) - { - SV_BanPlayer(pnum, banMinutes, reason); - } - } - - if (msg == KICK_MSG_PLAYER_QUIT) - S_StartSound(NULL, sfx_leave); // intended leave - else - S_StartSound(NULL, sfx_syfail); // he he he - - switch (msg) - { - case KICK_MSG_KICKED: - HU_AddChatText(va("\x82*%s has been kicked (No reason given)", player_names[pnum]), false); - kickreason = KR_KICK; - break; - case KICK_MSG_VOTE_KICK: - HU_AddChatText(va("\x82*%s has been kicked (Popular demand)", player_names[pnum]), false); - kickreason = KR_KICK; - break; - case KICK_MSG_PING_HIGH: - HU_AddChatText(va("\x82*%s left the game (Broke delay limit)", player_names[pnum]), false); - kickreason = KR_PINGLIMIT; - break; - case KICK_MSG_CON_FAIL: - HU_AddChatText(va("\x82*%s left the game (Synch failure)", player_names[pnum]), false); - kickreason = KR_SYNCH; - - if (M_CheckParm("-consisdump")) // Helps debugging some problems - { - INT32 i; - - CONS_Printf(M_GetText("Player kicked is #%d, dumping consistency...\n"), pnum); - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i]) - continue; - CONS_Printf("-------------------------------------\n"); - CONS_Printf("Player %d: %s\n", i, player_names[i]); - CONS_Printf("Skin: %d\n", players[i].skin); - CONS_Printf("Color: %d\n", players[i].skincolor); - CONS_Printf("Speed: %d\n",players[i].speed>>FRACBITS); - if (players[i].mo) - { - if (!players[i].mo->skin) - CONS_Printf("Mobj skin: NULL!\n"); - else - CONS_Printf("Mobj skin: %s\n", ((skin_t *)players[i].mo->skin)->name); - CONS_Printf("Position: %d, %d, %d\n", players[i].mo->x, players[i].mo->y, players[i].mo->z); - if (!players[i].mo->state) - CONS_Printf("State: S_NULL\n"); - else - CONS_Printf("State: %d\n", (statenum_t)(players[i].mo->state-states)); - } - else - CONS_Printf("Mobj: NULL\n"); - CONS_Printf("-------------------------------------\n"); - } - } - break; - case KICK_MSG_TIMEOUT: - HU_AddChatText(va("\x82*%s left the game (Connection timeout)", player_names[pnum]), false); - kickreason = KR_TIMEOUT; - break; - case KICK_MSG_SIGFAIL: - HU_AddChatText(va("\x82*%s left the game (Invalid signature)", player_names[pnum]), false); - kickreason = KR_TIMEOUT; - break; - case KICK_MSG_PLAYER_QUIT: - if (netgame) // not splitscreen/bots - HU_AddChatText(va("\x82*%s left the game", player_names[pnum]), false); - kickreason = KR_LEAVE; - break; - case KICK_MSG_GRIEF: - S_StartSound(NULL, sfx_cftbl1); - HU_AddChatText(va("\x82*%s has been kicked (Automatic grief detection)", player_names[pnum]), false); - kickreason = KR_KICK; - break; - case KICK_MSG_BANNED: - HU_AddChatText(va("\x82*%s has been banned (No reason given)", player_names[pnum]), false); - kickreason = KR_BAN; - break; - case KICK_MSG_CUSTOM_KICK: - HU_AddChatText(va("\x82*%s has been kicked (%s)", player_names[pnum], reason), false); - kickreason = KR_KICK; - break; - case KICK_MSG_CUSTOM_BAN: - HU_AddChatText(va("\x82*%s has been banned (%s)", player_names[pnum], reason), false); - kickreason = KR_BAN; - break; - } - - // SRB2Kart: kicks count as forfeit - switch (kickreason) - { - case KR_KICK: - case KR_BAN: - case KR_LEAVE: - // Intentional removals should be hit with a true forfeit. - K_PlayerForfeit(pnum, true); - break; - default: - // Otherwise, give remaining players the point compensation, but doesn't penalize who left. - K_PlayerForfeit(pnum, false); - break; - } - - if (playernode[pnum] == playernode[consoleplayer]) - { - LUA_HookBool(false, HOOK(GameQuit)); //Lua hooks handled differently now - - Command_ExitGame_f(); - - if (msg == KICK_MSG_CON_FAIL) - M_StartMessage("Server Disconnected", M_GetText("Server closed connection\n(Synch failure)\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - else if (msg == KICK_MSG_PING_HIGH) - M_StartMessage("Server Disconnected", M_GetText("Server closed connection\n(Broke delay limit)\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - else if (msg == KICK_MSG_TIMEOUT) // this one will probably never be seen? - M_StartMessage("Server Disconnected", M_GetText("Connection timed out\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - else if (msg == KICK_MSG_BANNED) - M_StartMessage("Server Disconnected", M_GetText("You have been banned by the server\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - else if (msg == KICK_MSG_CUSTOM_KICK) - M_StartMessage("Server Disconnected", M_GetText("You have been kicked\n(Automatic grief detection)\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - else if (msg == KICK_MSG_CUSTOM_KICK) - M_StartMessage("Server Disconnected", va(M_GetText("You have been kicked\n(%s)\n"), reason), NULL, MM_NOTHING, NULL, "Back to Menu"); - else if (msg == KICK_MSG_CUSTOM_BAN) - M_StartMessage("Server Disconnected", va(M_GetText("You have been banned\n(%s)\n"), reason), NULL, MM_NOTHING, NULL, "Back to Menu"); - else if (msg == KICK_MSG_SIGFAIL) - M_StartMessage("Server Disconnected", M_GetText("Server closed connection\n(Invalid signature)\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - else if (msg == KICK_MSG_VOTE_KICK) - M_StartMessage("Server Disconnected", M_GetText("You have been kicked by popular demand\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - else - M_StartMessage("Server Disconnected", M_GetText("You have been kicked by the server\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); - } - else if (server) - { - // Sal: Because kicks (and a lot of other commands) are player-based, we can't tell which player pnum is on the node from a glance. - // When we want to remove everyone from a node, we have to get the kicked player's node, then remove everyone on that node manually so we don't miss any. - // This avoids the bugs with older SRB2 version's online splitscreen kicks, specifically ghosting. - // On top of this, it can't just be a CL_RemovePlayer call; it has to be a server-sided. - // Clients don't bother setting any nodes for anything but THE server player (even ignoring the server's extra players!), so it'll often remove everyone because they all have node -1/255, insta-desync! - // And yes. This is a netxcmd wrap for just CL_RemovePlayer! :V - -#define removethisplayer(otherp) \ - if (otherp >= 0) \ - { \ - buf[0] = (UINT8)otherp; \ - if (otherp != pnum) \ - { \ - HU_AddChatText(va("\x82*%s left the game (Joined with %s)", player_names[otherp], player_names[pnum]), false); \ - buf[1] = KR_LEAVE; \ - } \ - else \ - buf[1] = (UINT8)kickreason; \ - SendNetXCmd(XD_REMOVEPLAYER, &buf, 2); \ - otherp = -1; \ - } - removethisplayer(nodetoplayer[playernode[pnum]]) - removethisplayer(nodetoplayer2[playernode[pnum]]) - removethisplayer(nodetoplayer3[playernode[pnum]]) - removethisplayer(nodetoplayer4[playernode[pnum]]) -#undef removethisplayer - } -} - -#ifdef HAVE_CURL -/** Add a login for HTTP downloads. If the - * user/password is missing, remove it. - * - * \sa Command_list_http_logins - */ -static void Command_set_http_login (void) -{ - HTTP_login *login; - HTTP_login **prev_next; - - if (COM_Argc() < 2) - { - CONS_Printf( - "set_http_login [user:password]: Set or remove a login to " - "authenticate HTTP downloads.\n" - ); - return; - } - - login = CURLGetLogin(COM_Argv(1), &prev_next); - - if (COM_Argc() == 2) - { - if (login) - { - (*prev_next) = login->next; - CONS_Printf("Login for '%s' removed.\n", login->url); - Z_Free(login); - } - } - else - { - if (login) - Z_Free(login->auth); - else - { - login = ZZ_Alloc(sizeof *login); - login->url = Z_StrDup(COM_Argv(1)); - } - - login->auth = Z_StrDup(COM_Argv(2)); - - login->next = curl_logins; - curl_logins = login; - } -} - -/** List logins for HTTP downloads. - * - * \sa Command_set_http_login - */ -static void Command_list_http_logins (void) -{ - HTTP_login *login; - - for ( - login = curl_logins; - login; - login = login->next - ){ - CONS_Printf( - "'%s' -> '%s'\n", - login->url, - login->auth - ); - } -} -#endif/*HAVE_CURL*/ - -static void Command_ResendGamestate(void) -{ - SINT8 playernum; - - if (COM_Argc() == 1) - { - CONS_Printf(M_GetText("resendgamestate : resend the game state to a player\n")); - return; - } - else if (client) - { - CONS_Printf(M_GetText("Only the server can use this.\n")); - return; - } - - playernum = nametonum(COM_Argv(1)); - if (playernum == -1 || playernum == 0) - return; - - // Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on - netbuffer->packettype = PT_WILLRESENDGAMESTATE; - if (!HSendPacket(playernode[playernum], true, 0, 0)) - { - CONS_Alert(CONS_ERROR, M_GetText("A problem occured, please try again.\n")); - return; - } -} - -static void Got_AddPlayer(const UINT8 **p, INT32 playernum); -static void Got_RemovePlayer(const UINT8 **p, INT32 playernum); -static void Got_AddBot(const UINT8 **p, INT32 playernum); - -void Joinable_OnChange(void); -void Joinable_OnChange(void) -{ - UINT8 buf[3]; - UINT8 *p = buf; - UINT8 maxplayer; - - if (!server) - return; - - maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value)); - - WRITEUINT8(p, maxplayer); - WRITEUINT8(p, cv_allownewplayer.value); - WRITEUINT8(p, cv_discordinvites.value); - - SendNetXCmd(XD_DISCORD, &buf, 3); -} - -// called one time at init -void D_ClientServerInit(void) -{ - DEBFILE(va("- - -== Ring Racers v%d.%d "VERSIONSTRING" debugfile ==- - -\n", - VERSION, SUBVERSION)); - - COM_AddCommand("getplayernum", Command_GetPlayerNum); - COM_AddCommand("kick", Command_Kick); - COM_AddCommand("ban", Command_Ban); - COM_AddCommand("listbans", Command_Listbans); - COM_AddCommand("unban", Command_Unban); - COM_AddCommand("banip", Command_BanIP); - COM_AddCommand("connect", Command_connect); - COM_AddCommand("nodes", Command_Nodes); -#ifdef HAVE_CURL - COM_AddCommand("set_http_login", Command_set_http_login); - COM_AddCommand("list_http_logins", Command_list_http_logins); -#endif - COM_AddCommand("resendgamestate", Command_ResendGamestate); -#ifdef PACKETDROP - COM_AddCommand("drop", Command_Drop); - COM_AddCommand("droprate", Command_Droprate); -#endif - COM_AddCommand("numnodes", Command_Numnodes); - - RegisterNetXCmd(XD_KICK, Got_KickCmd); - RegisterNetXCmd(XD_ADDPLAYER, Got_AddPlayer); - RegisterNetXCmd(XD_REMOVEPLAYER, Got_RemovePlayer); - RegisterNetXCmd(XD_ADDBOT, Got_AddBot); - - gametic = 0; - localgametic = 0; - - // do not send anything before the real begin - SV_StopServer(); - SV_ResetServer(); - if (dedicated) - SV_SpawnServer(); -} - -static void ResetNode(INT32 node) -{ - nodeingame[node] = false; - nodewaiting[node] = 0; - nodeneedsauth[node] = false; - - nettics[node] = gametic; - supposedtics[node] = gametic; - - nodetoplayer[node] = -1; - nodetoplayer2[node] = -1; - nodetoplayer3[node] = -1; - nodetoplayer4[node] = -1; - playerpernode[node] = 0; - - sendingsavegame[node] = false; - resendingsavegame[node] = false; - savegameresendcooldown[node] = 0; - - bannednode[node].banid = SIZE_MAX; - bannednode[node].timeleft = NO_BAN_TIME; -} - -void SV_ResetServer(void) -{ - INT32 i; - - // +1 because this command will be executed in com_executebuffer in - // tryruntic so gametic will be incremented, anyway maketic > gametic - // is not an issue - - maketic = gametic + 1; - neededtic = maketic; - tictoclear = maketic; - - joindelay = 0; - - for (i = 0; i < MAXNETNODES; i++) - ResetNode(i); - - for (i = 0; i < MAXPLAYERS; i++) - { - LUA_InvalidatePlayer(&players[i]); - sprintf(player_names[i], "Player %c", 'A' + i); - } - - memset(playeringame, false, sizeof playeringame); - memset(playernode, UINT8_MAX, sizeof playernode); - - pingmeasurecount = 1; - memset(realpingtable, 0, sizeof realpingtable); - memset(playerpingtable, 0, sizeof playerpingtable); - memset(playerpacketlosstable, 0, sizeof playerpacketlosstable); - memset(playerdelaytable, 0, sizeof playerdelaytable); - - ClearAdminPlayers(); - Schedule_Clear(); - Automate_Clear(); - K_ClearClientPowerLevels(); - G_ObliterateParties(); - K_ResetMidVote(); - - memset(splitscreen_invitations, -1, sizeof splitscreen_invitations); - memset(player_name_changes, 0, sizeof player_name_changes); - - mynode = 0; - cl_packetmissed = false; - cl_redownloadinggamestate = false; - - if (dedicated) - { - nodeingame[0] = true; - serverplayer = 0; - } - else - serverplayer = consoleplayer; - - if (server) - servernode = 0; - - doomcom->numslots = 0; - - // clear server_context - memset(server_context, '-', 8); - - strlcpy(connectedservername, "\0", MAXSERVERNAME); - strlcpy(connectedservercontact, "\0", MAXSERVERCONTACT); - - CV_RevertNetVars(); - - // Copy our unlocks to a place where net material can grab at/overwrite them safely. - // (permits all unlocks in dedicated) - M_SetNetUnlocked(); - - expectChallenge = false; - - DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n"); -} - -#ifndef TESTERS -static void SV_GenContext(void) -{ - UINT8 i; - - // generate server_context, as exactly 8 bytes of randomly mixed A-Z and a-z - // (hopefully M_Random is initialized!! if not this will be awfully silly!) - for (i = 0; i < 8; i++) - { - const char a = M_RandomKey(26*2); - if (a < 26) // uppercase - server_context[i] = 'A'+a; - else // lowercase - server_context[i] = 'a'+(a-26); - } - - D_ParseCarets(connectedservername, cv_servername.string, MAXSERVERNAME); - D_ParseCarets(connectedservercontact, cv_server_contact.string, MAXSERVERCONTACT); -} -#endif // TESTERS - -// -// D_QuitNetGame -// Called before quitting to leave a net game -// without hanging the other players -// -void D_QuitNetGame(void) -{ - if (!netgame || !netbuffer) - return; - - DEBFILE("===========================================================================\n" - " Quitting Game, closing connection\n" - "===========================================================================\n"); - - // abort send/receive of files - CloseNetFile(); - RemoveAllLuaFileTransfers(); - waitingforluafiletransfer = false; - waitingforluafilecommand = false; - - if (server) - { - INT32 i; - - netbuffer->packettype = PT_SERVERSHUTDOWN; - for (i = 0; i < MAXNETNODES; i++) - if (nodeingame[i]) - HSendPacket(i, true, 0, 0); -#ifdef MASTERSERVER - if (serverrunning && netgame && cv_advertise.value) // see mserv.c Online() - UnregisterServer(); -#endif - } - else if (servernode > 0 && servernode < MAXNETNODES && nodeingame[(UINT8)servernode]) - { - netbuffer->packettype = PT_CLIENTQUIT; - HSendPacket(servernode, true, 0, 0); - } - - D_CloseConnection(); - ClearAdminPlayers(); - Schedule_Clear(); - Automate_Clear(); - K_ClearClientPowerLevels(); - G_ObliterateParties(); - K_ResetMidVote(); - - DEBFILE("===========================================================================\n" - " Log finish\n" - "===========================================================================\n"); -#ifdef DEBUGFILE - if (debugfile) - { - fclose(debugfile); - debugfile = NULL; - } -#endif -} - -// Adds a node to the game (player will follow at map change or at savegame....) -static inline void SV_AddNode(INT32 node) -{ - nettics[node] = gametic; - supposedtics[node] = gametic; - // little hack because the server connects to itself and puts - // nodeingame when connected not here - if (node) - nodeingame[node] = true; - - nodeneedsauth[node] = false; -} - -// Xcmd XD_ADDPLAYER -static void Got_AddPlayer(const UINT8 **p, INT32 playernum) -{ - INT16 node, newplayernum; - UINT8 console; - UINT8 splitscreenplayer = 0; - UINT8 i; - player_t *newplayer; - uint8_t public_key[PUBKEYLENGTH]; - - if (playernum != serverplayer && !IsPlayerAdmin(playernum)) - { - // protect against hacked/buggy client - CONS_Alert(CONS_WARNING, M_GetText("Illegal add player command received from %s\n"), player_names[playernum]); - if (server) - SendKick(playernum, KICK_MSG_CON_FAIL); - return; - } - - node = READUINT8(*p); - newplayernum = READUINT8(*p); - - CONS_Debug(DBG_NETPLAY, "addplayer: %d %d\n", node, newplayernum); - - //G_SpectatePlayerOnJoin(newplayernum); -- caused desyncs in this spot :( - - if (newplayernum+1 > doomcom->numslots) - doomcom->numslots = (INT16)(newplayernum+1); - - newplayer = &players[newplayernum]; - - READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME); - READMEM(*p, public_key, PUBKEYLENGTH); - READMEM(*p, clientpowerlevels[newplayernum], sizeof(((serverplayer_t *)0)->powerlevels)); - - console = READUINT8(*p); - splitscreenplayer = READUINT8(*p); - - G_AddPlayer(newplayernum, console); - memcpy(players[newplayernum].public_key, public_key, PUBKEYLENGTH); - - for (i = 0; i < MAXAVAILABILITY; i++) - { - newplayer->availabilities[i] = READUINT8(*p); - } - - // the server is creating my player - if (node == mynode) - { - playernode[newplayernum] = 0; // for information only - - if (splitscreenplayer) - { - displayplayers[splitscreenplayer] = newplayernum; - g_localplayers[splitscreenplayer] = newplayernum; - DEBFILE(va("spawning sister # %d\n", splitscreenplayer)); - } - else - { - consoleplayer = newplayernum; - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - { - displayplayers[i] = newplayernum; - g_localplayers[i] = newplayernum; - } - DEBFILE("spawning me\n"); - } - - P_ForceLocalAngle(newplayer, newplayer->angleturn); - - D_SendPlayerConfig(splitscreenplayer); - addedtogame = true; - } - - players[newplayernum].splitscreenindex = splitscreenplayer; - players[newplayernum].bot = false; - - if (node == mynode && splitscreenplayer == 0) - S_AttemptToRestoreMusic(); // Earliest viable point - - if (netgame) - { - char joinmsg[256]; - - strcpy(joinmsg, M_GetText("\x82*%s has joined the game (player %d)")); - strcpy(joinmsg, va(joinmsg, player_names[newplayernum], newplayernum)); - - if (node != mynode) - S_StartSound(NULL, sfx_join); - - // Merge join notification + IP to avoid clogging console/chat - if (server && cv_showjoinaddress.value && I_GetNodeAddress) - { - const char *address = I_GetNodeAddress(node); - if (address) - strcat(joinmsg, va(" (%s)", address)); - } - - HU_AddChatText(joinmsg, false); - } - - if (server && multiplayer && motd[0] != '\0') - COM_BufAddText(va("sayto %d %s\n", newplayernum, motd)); - - LUA_HookInt(newplayernum, HOOK(PlayerJoin)); - -#ifdef HAVE_DISCORDRPC - DRPC_UpdatePresence(); -#endif -} - -// Xcmd XD_REMOVEPLAYER -static void Got_RemovePlayer(const UINT8 **p, INT32 playernum) -{ - SINT8 pnum, reason; - - if (playernum != serverplayer && !IsPlayerAdmin(playernum)) - { - // protect against hacked/buggy client - CONS_Alert(CONS_WARNING, M_GetText("Illegal remove player command received from %s\n"), player_names[playernum]); - if (server) - { - SendKick(playernum, KICK_MSG_CON_FAIL); - } - return; - } - - pnum = READUINT8(*p); - reason = READUINT8(*p); - - CL_RemovePlayer(pnum, reason); - -#ifdef HAVE_DISCORDRPC - DRPC_UpdatePresence(); -#endif -} - -// Xcmd XD_ADDBOT -// Compacted version of XD_ADDPLAYER for simplicity -static void Got_AddBot(const UINT8 **p, INT32 playernum) -{ - INT16 newplayernum; - UINT8 skinnum = 0; - UINT8 difficulty = DIFFICULTBOT; - botStyle_e style = BOT_STYLE_NORMAL; - - if (playernum != serverplayer && !IsPlayerAdmin(playernum)) - { - // protect against hacked/buggy client - CONS_Alert(CONS_WARNING, M_GetText("Illegal add player command received from %s\n"), player_names[playernum]); - if (server) - { - SendKick(playernum, KICK_MSG_CON_FAIL); - } - return; - } - - newplayernum = READUINT8(*p); - skinnum = READUINT8(*p); - difficulty = READUINT8(*p); - style = READUINT8(*p); - - K_SetBot(newplayernum, skinnum, difficulty, style); -} - -static boolean SV_AddWaitingPlayers(SINT8 node, UINT8 *availabilities, - const char *name, uint8_t *key, UINT16 *pwr, - const char *name2, uint8_t *key2, UINT16 *pwr2, - const char *name3, uint8_t *key3, UINT16 *pwr3, - const char *name4, uint8_t *key4, UINT16 *pwr4) -{ - INT32 n, newplayernum, i; - UINT8 buf[4 + MAXPLAYERNAME + PUBKEYLENGTH + MAXAVAILABILITY + sizeof(((serverplayer_t *)0)->powerlevels)]; - UINT8 *buf_p = buf; - boolean newplayer = false; - - { - // splitscreen can allow 2+ players in one node - for (; nodewaiting[node] > 0; nodewaiting[node]--) - { - newplayer = true; - - { - UINT8 nobotoverwrite; - - // search for a free playernum - // we can't solely use playeringame since it is not updated here - for (newplayernum = dedicated ? 1 : 0; newplayernum < MAXPLAYERS; newplayernum++) - { - if (playeringame[newplayernum]) - continue; - - for (n = 0; n < MAXNETNODES; n++) - if (nodetoplayer[n] == newplayernum - || nodetoplayer2[n] == newplayernum - || nodetoplayer3[n] == newplayernum - || nodetoplayer4[n] == newplayernum) - break; - - if (n == MAXNETNODES) - break; - } - - nobotoverwrite = newplayernum; - - while (playeringame[nobotoverwrite] - && players[nobotoverwrite].bot - && nobotoverwrite < MAXPLAYERS) - { - // Overwrite bots if there are NO other slots available. - nobotoverwrite++; - } - - if (nobotoverwrite < MAXPLAYERS) - { - newplayernum = nobotoverwrite; - } - } - - // should never happen since we check the playernum - // before accepting the join - I_Assert(newplayernum < MAXPLAYERS); - - playernode[newplayernum] = (UINT8)node; - - // Reset the buffer to the start for multiple joiners - buf_p = buf; - - WRITEUINT8(buf_p, (UINT8)node); - WRITEUINT8(buf_p, newplayernum); - - if (playerpernode[node] < 1) - { - nodetoplayer[node] = newplayernum; - WRITESTRINGN(buf_p, name, MAXPLAYERNAME); - WRITEMEM(buf_p, key, PUBKEYLENGTH); - WRITEMEM(buf_p, pwr, sizeof(((serverplayer_t *)0)->powerlevels)); - } - else if (playerpernode[node] < 2) - { - nodetoplayer2[node] = newplayernum; - WRITESTRINGN(buf_p, name2, MAXPLAYERNAME); - WRITEMEM(buf_p, key2, PUBKEYLENGTH); - WRITEMEM(buf_p, pwr2, sizeof(((serverplayer_t *)0)->powerlevels)); - } - else if (playerpernode[node] < 3) - { - nodetoplayer3[node] = newplayernum; - WRITESTRINGN(buf_p, name3, MAXPLAYERNAME); - WRITEMEM(buf_p, key3, PUBKEYLENGTH); - WRITEMEM(buf_p, pwr3, sizeof(((serverplayer_t *)0)->powerlevels)); - } - else if (playerpernode[node] < 4) - { - nodetoplayer4[node] = newplayernum; - WRITESTRINGN(buf_p, name4, MAXPLAYERNAME); - WRITEMEM(buf_p, key4, PUBKEYLENGTH); - WRITEMEM(buf_p, pwr4, sizeof(((serverplayer_t *)0)->powerlevels)); - } - - WRITEUINT8(buf_p, nodetoplayer[node]); // consoleplayer - WRITEUINT8(buf_p, playerpernode[node]); // splitscreen num - - for (i = 0; i < MAXAVAILABILITY; i++) - { - WRITEUINT8(buf_p, availabilities[i]); - } - - playerpernode[node]++; - - SendNetXCmd(XD_ADDPLAYER, buf, buf_p - buf); - DEBFILE(va("Server added player %d node %d\n", newplayernum, node)); - } - } - - return newplayer; -} - -/*-------------------------------------------------- - boolean K_AddBotFromServer(UINT8 skin, UINT8 difficulty, botStyle_e style, UINT8 *p) - - See header file for description. ---------------------------------------------------*/ -boolean K_AddBotFromServer(UINT8 skin, UINT8 difficulty, botStyle_e style, UINT8 *p) -{ - UINT8 newplayernum = *p; - - // search for a free playernum - // we can't use playeringame since it is not updated here - for (; newplayernum < MAXPLAYERS; newplayernum++) - { - UINT8 n; - - for (n = 0; n < MAXNETNODES; n++) - { - if (nodetoplayer[n] == newplayernum - || nodetoplayer2[n] == newplayernum - || nodetoplayer3[n] == newplayernum - || nodetoplayer4[n] == newplayernum) - break; - } - - if (n == MAXNETNODES) - break; - } - - for (; newplayernum < MAXPLAYERS; newplayernum++) - { - if (playeringame[newplayernum] == false) - { - // free player slot - break; - } - } - - if (newplayernum >= MAXPLAYERS) - { - // nothing is free - *p = MAXPLAYERS; - return false; - } - - if (server) - { - UINT8 buf[4]; - UINT8 *buf_p = buf; - - WRITEUINT8(buf_p, newplayernum); - - if (skin > numskins) - { - skin = numskins; - } - - WRITEUINT8(buf_p, skin); - - if (difficulty < 1) - { - difficulty = 1; - } - else if (difficulty > MAXBOTDIFFICULTY) - { - difficulty = MAXBOTDIFFICULTY; - } - - WRITEUINT8(buf_p, difficulty); - WRITEUINT8(buf_p, style); - - SendNetXCmd(XD_ADDBOT, buf, buf_p - buf); - DEBFILE(va("Server added bot %d\n", newplayernum)); - } - - // use the next free slot (we can't put playeringame[newplayernum] = true here) - *p = newplayernum+1; - return true; -} - -void CL_AddSplitscreenPlayer(void) -{ - if (cl_mode == CL_CONNECTED) - CL_SendJoin(); -} - -void CL_RemoveSplitscreenPlayer(UINT8 p) -{ - if (cl_mode != CL_CONNECTED) - return; - - SendKick(p, KICK_MSG_PLAYER_QUIT); -} - -#ifndef TESTERS -static void GotOurIP(UINT32 address) -{ - #ifdef DEVELOP - { - const unsigned char * p = (const unsigned char *)&address; - CONS_Printf("Got IP of %u.%u.%u.%u\n", p[0], p[1], p[2], p[3]); - } - #endif - ourIP = address; -} -#endif - -// is there a game running -boolean Playing(void) -{ - return (server && serverrunning) || (client && cl_mode == CL_CONNECTED); -} - -boolean SV_SpawnServer(void) -{ -#ifdef TESTERS - /* Just don't let the testers play. Easy. */ - I_Error("What do you think you're doing?"); - return false; -#else - boolean result = false; - if (demo.playback) - G_StopDemo(); // reset engine parameter - - if (!serverrunning) - { - CON_LogMessage(M_GetText("Starting Server....\n")); - serverrunning = true; - SV_ResetServer(); - SV_GenContext(); - if (netgame) - { - if (I_NetOpenSocket) - { - I_NetOpenSocket(); - } - - if (cv_advertise.value) - { - RegisterServer(); - } - - ourIP = 0; - STUN_bind(GotOurIP); - } - - // non dedicated server just connect to itself - if (!dedicated) - CL_ConnectToServer(); - else doomcom->numslots = 1; - } - - - - // strictly speaking, i'm not convinced the following is necessary - // but I'm not confident enough to remove it entirely in case it breaks something - { - UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false, -1); - SINT8 node = 0; - for (; node < MAXNETNODES; node++) - result |= SV_AddWaitingPlayers(node, availabilitiesbuffer, - cv_playername[0].zstring, PR_GetLocalPlayerProfile(0)->public_key, SV_GetStatsByKey(PR_GetLocalPlayerProfile(0)->public_key)->powerlevels, - cv_playername[1].zstring, PR_GetLocalPlayerProfile(1)->public_key, SV_GetStatsByKey(PR_GetLocalPlayerProfile(1)->public_key)->powerlevels, - cv_playername[2].zstring, PR_GetLocalPlayerProfile(2)->public_key, SV_GetStatsByKey(PR_GetLocalPlayerProfile(2)->public_key)->powerlevels, - cv_playername[3].zstring, PR_GetLocalPlayerProfile(3)->public_key, SV_GetStatsByKey(PR_GetLocalPlayerProfile(3)->public_key)->powerlevels); - } - return result; -#endif -} - -void SV_StopServer(void) -{ - tic_t i; - - if (gamestate == GS_INTERMISSION) - Y_EndIntermission(); - if (gamestate == GS_VOTING) - Y_EndVote(); - - G_SetGamestate(GS_NULL); - wipegamestate = GS_NULL; - - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - ((UINT16*)localtextcmd[i])[0] = 0; - - for (i = firstticstosend; i < firstticstosend + BACKUPTICS; i++) - D_Clearticcmd(i); - - consoleplayer = 0; - cl_mode = CL_ABORTED; - maketic = gametic+1; - neededtic = maketic; - serverrunning = false; - titlemapinaction = false; -} - -// called at singleplayer start and stopdemo -void SV_StartSinglePlayerServer(INT32 dogametype, boolean donetgame) -{ - INT32 lastgametype = gametype; - server = true; - multiplayer = (modeattacking == ATTACKING_NONE); - joinedIP[0] = '\0'; // Make sure to empty this so that we don't save garbage when we start our own game. (because yes we use this for netgames too....) - - netgame = false; // so setting timelimit works... (XD_NETVAR doesn't play nice with SV_StopServer) - - G_SetGametype(dogametype); - if (gametype != lastgametype) - D_GameTypeChanged(lastgametype); - - netgame = donetgame; - - // no more tic the game with this settings! - SV_StopServer(); -} - -static void SV_SendRefuse(INT32 node, const char *reason) -{ - strcpy(netbuffer->u.serverrefuse.reason, reason); - - netbuffer->packettype = PT_SERVERREFUSE; - HSendPacket(node, false, 0, strlen(netbuffer->u.serverrefuse.reason) + 1); - Net_CloseConnection(node); -} - -// used at txtcmds received to check packetsize bound -static size_t TotalTextCmdPerTic(tic_t tic) -{ - INT32 i; - size_t total = 1; // num of textcmds in the tic (ntextcmd byte) - - for (i = 0; i < MAXPLAYERS; i++) - { - UINT8 *textcmd = D_GetExistingTextcmd(tic, i); - if ((!i || playeringame[i]) && textcmd) - total += 3 + ((UINT16*)textcmd)[0]; // "+2" for size and playernum - } - - return total; -} - -#ifdef SIGNGAMETRAFFIC - static boolean IsSplitPlayerOnNodeGuest(int node, int split) - { - char allZero[PUBKEYLENGTH]; - memset(allZero, 0, PUBKEYLENGTH); - - if (split == 0) - return PR_IsKeyGuest(players[nodetoplayer[node]].public_key); - else if (split == 1) - return PR_IsKeyGuest(players[nodetoplayer2[node]].public_key); - else if (split == 2) - return PR_IsKeyGuest(players[nodetoplayer3[node]].public_key); - else if (split == 3) - return PR_IsKeyGuest(players[nodetoplayer4[node]].public_key); - else - I_Error("IsSplitPlayerOnNodeGuest: Out of bounds"); - return false; // unreachable - } -#endif - -static boolean IsPlayerGuest(int player) -{ - return PR_IsKeyGuest(players[player].public_key); -} - -/** Called when a PT_CLIENTJOIN packet is received - * - * \param node The packet sender - * - */ -static void HandleConnect(SINT8 node) -{ - char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1]; - INT32 i, j; - UINT8 availabilitiesbuffer[MAXAVAILABILITY]; - - // Sal: Dedicated mode is INCREDIBLY hacked together. - // If a server filled out, then it'd overwrite the host and turn everyone into weird husks..... - // It's too much effort to legimately fix right now. Just prevent it from reaching that state. - UINT8 maxplayers = min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value); - UINT8 connectedplayers = 0; - - for (i = dedicated ? 1 : 0; i < MAXPLAYERS; i++) - { - // We use this to count players because it is affected by SV_AddWaitingPlayers when - // more than one client joins on the same tic, unlike playeringame and D_NumPlayers. - // UINT8_MAX denotes no node for that player. - - if (playernode[i] != UINT8_MAX) - { - // Sal: This hack sucks, but it should be safe. - // playeringame is set for bots immediately; they are deterministic instead of a netxcmd. - // If a bot is added with netxcmd in the future, then the node check is still here too. - // So at worst, a theoretical netxcmd added bot will block real joiners for the time - // it takes for the command to process, but not cause any horrifying player overwriting. - if (playeringame[i] && players[i].bot) - { - continue; - } - - connectedplayers++; - } - } - - banrecord_t *ban = SV_GetBanByAddress(node); - if (ban == NULL) - { - for (i = 0; i < netbuffer->u.clientcfg.localplayers - playerpernode[node]; i++) - { - if (ban == NULL) - ban = SV_GetBanByKey(lastReceivedKey[node][i]); - } - } - - if (ban != NULL && node != 0) - { - UINT32 timeremaining = 0; - if (ban->expires > time(NULL)) - { - timeremaining = ban->expires - time(NULL); - int minutes = (timeremaining + 30) / 60; - int hours = (minutes + 1) / 60; - int days = (hours + 1) / 24; - - if (days) - { - SV_SendRefuse(node, va("K|%s\n(Time remaining: %d day%s)", ban->reason, days, days > 1 ? "s" : "")); - } - else if (hours) - { - SV_SendRefuse(node, va("K|%s\n(Time remaining: %d hour%s)", ban->reason, hours, hours > 1 ? "s" : "")); - } - else if (minutes) - { - SV_SendRefuse(node, va("K|%s\n(Time remaining: %d minute%s)", ban->reason, minutes, minutes > 1 ? "s" : "")); - } - else - { - SV_SendRefuse(node, va("K|%s\n(Time remaining: <1 minute)", ban->reason)); - } - } - else - { - SV_SendRefuse(node, va("B|%s", ban->reason)); - } - } - else if (netbuffer->u.clientcfg._255 != 255 || - netbuffer->u.clientcfg.packetversion != PACKETVERSION) - { - SV_SendRefuse(node, "Incompatible packet formats."); - } - else if (strncmp(netbuffer->u.clientcfg.application, SRB2APPLICATION, - sizeof netbuffer->u.clientcfg.application)) - { - SV_SendRefuse(node, "Different Ring Racers modifications\nare not compatible."); - } - else if (netbuffer->u.clientcfg.version != VERSION - || netbuffer->u.clientcfg.subversion != SUBVERSION) - { - SV_SendRefuse(node, va(M_GetText("Different Ring Racers versions cannot\nplay a netgame!\n(server version %d.%d)"), VERSION, SUBVERSION)); - } - else if (!cv_allownewplayer.value && node) - { - SV_SendRefuse(node, M_GetText("The server is not accepting\njoins for the moment.")); - } - else if (connectedplayers >= maxplayers) - { - SV_SendRefuse(node, va(M_GetText("Maximum players reached: %d"), maxplayers)); - } - else if (netgame && netbuffer->u.clientcfg.localplayers > MAXSPLITSCREENPLAYERS) // Hacked client? - { - SV_SendRefuse(node, M_GetText("Too many players from\nthis node.")); - } - else if (netgame && connectedplayers + netbuffer->u.clientcfg.localplayers > maxplayers) - { - SV_SendRefuse(node, va(M_GetText("Number of local players\nwould exceed maximum: %d"), maxplayers)); - } - else if (netgame && !netbuffer->u.clientcfg.localplayers) // Stealth join? - { - SV_SendRefuse(node, M_GetText("No players from\nthis node.")); - } - else if (luafiletransfers) - { - SV_SendRefuse(node, M_GetText("The server is broadcasting a file\nrequested by a Lua script.\nPlease wait a bit and then\ntry rejoining.")); - } - else if (netgame && joindelay > 2 * (tic_t)cv_joindelay.value * TICRATE) - { - SV_SendRefuse(node, va(M_GetText("Too many people are connecting.\nPlease wait %d seconds and then\ntry rejoining."), - (joindelay - 2 * cv_joindelay.value * TICRATE) / TICRATE)); - } - else - { - int sigcheck; - boolean newnode = false; - - for (i = 0; i < netbuffer->u.clientcfg.localplayers - playerpernode[node]; i++) - { - strlcpy(names[i], netbuffer->u.clientcfg.names[i], MAXPLAYERNAME + 1); - if (!EnsurePlayerNameIsGood(names[i], -1)) - { - SV_SendRefuse(node, "Bad player name"); - return; - } - - if (node == 0) // Hey, that's us. We're always allowed to do what we want. - { - memcpy(lastReceivedKey[node][i], PR_GetLocalPlayerProfile(i)->public_key, sizeof(lastReceivedKey[node][i])); - } - else // Remote player, gotta check their signature. - { - if (PR_IsKeyGuest(lastReceivedKey[node][i])) // IsSplitPlayerOnNodeGuest isn't appropriate here, they're not in-game yet! - { - if (!cv_allowguests.value) - { - SV_SendRefuse(node, M_GetText("The server doesn't allow GUESTs.\nCreate a profile to join!")); - return; - } - - sigcheck = 0; // Always succeeds. Yes, this is a success response. C R Y P T O - } - else - { - sigcheck = crypto_eddsa_check(netbuffer->u.clientcfg.challengeResponse[i], lastReceivedKey[node][i], lastSentChallenge[node], CHALLENGELENGTH); - } - - if (netgame && sigcheck != 0) - { - SV_SendRefuse(node, M_GetText("Signature verification failed.")); - return; - } - } - - // Check non-GUESTS for duplicate pubkeys, they'll create nonsense stats - if (!PR_IsKeyGuest(lastReceivedKey[node][i])) - { - // Players already here - for (j = 0; j < MAXPLAYERS; j++) - { - if (!playeringame[j]) - continue; - if (memcmp(lastReceivedKey[node][i], players[j].public_key, PUBKEYLENGTH) == 0) - { - #ifdef DEVELOP - CONS_Alert(CONS_WARNING, "Joining player's pubkey matches existing player, stat updates will be nonsense!\n"); - #else - SV_SendRefuse(node, M_GetText("Duplicate pubkey already on server.\n(Did you share your profile?)")); - return; - #endif - } - } - - // Players we're trying to add - for (j = 0; j < netbuffer->u.clientcfg.localplayers - playerpernode[node]; j++) - { - if (PR_IsKeyGuest(lastReceivedKey[node][j])) - continue; - if (i == j) - continue; - if (memcmp(lastReceivedKey[node][i], lastReceivedKey[node][j], PUBKEYLENGTH) == 0) - { - #ifdef DEVELOP - CONS_Alert(CONS_WARNING, "Players with same pubkey in the joning party, stat updates will be nonsense!\n"); - #else - SV_SendRefuse(node, M_GetText("Duplicate pubkey in local party.\n(How did you even do this?)")); - return; - #endif - } - } - } - } - - memcpy(availabilitiesbuffer, netbuffer->u.clientcfg.availabilities, sizeof(availabilitiesbuffer)); - - // client authorised to join - nodewaiting[node] = (UINT8)(netbuffer->u.clientcfg.localplayers - playerpernode[node]); - if (!nodeingame[node]) - { - gamestate_t backupstate = gamestate; - newnode = true; - - SV_AddNode(node); - -#ifdef VANILLAJOINNEXTROUND - if (cv_joinnextround.value && gameaction == ga_nothing) - G_SetGamestate(GS_WAITINGPLAYERS); -#endif - if (!SV_SendServerConfig(node)) - { - G_SetGamestate(backupstate); - /// \note Shouldn't SV_SendRefuse be called before ResetNode? - SV_SendRefuse(node, M_GetText("Server couldn't send info, please try again")); - ResetNode(node); // Yeah, lets try it! - /// \todo fix this !!! - return; // restart the while - } - //if (gamestate != GS_LEVEL) // GS_INTERMISSION, etc? - // SV_SendPlayerConfigs(node); // send bare minimum player info - G_SetGamestate(backupstate); - DEBFILE("new node joined\n"); - } - if (nodewaiting[node]) - { - if (node && newnode) - { - SV_SendSaveGame(node, false); // send a complete game state - DEBFILE("send savegame\n"); - } - - SV_AddWaitingPlayers(node, availabilitiesbuffer, - names[0], lastReceivedKey[node][0], SV_GetStatsByKey(lastReceivedKey[node][0])->powerlevels, - names[1], lastReceivedKey[node][1], SV_GetStatsByKey(lastReceivedKey[node][1])->powerlevels, - names[2], lastReceivedKey[node][2], SV_GetStatsByKey(lastReceivedKey[node][2])->powerlevels, - names[3], lastReceivedKey[node][3], SV_GetStatsByKey(lastReceivedKey[node][3])->powerlevels); - joindelay += cv_joindelay.value * TICRATE; - player_joining = true; - } - } -} - -/** Called when a PT_SERVERSHUTDOWN packet is received - * - * \param node The packet sender (should be the server) - * - */ -static void HandleShutdown(SINT8 node) -{ - (void)node; - LUA_HookBool(false, HOOK(GameQuit)); - Command_ExitGame_f(); - M_StartMessage("Server Disconnected", M_GetText("Server has shutdown\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); -} - -/** Called when a PT_NODETIMEOUT packet is received - * - * \param node The packet sender (should be the server) - * - */ -static void HandleTimeout(SINT8 node) -{ - (void)node; - LUA_HookBool(false, HOOK(GameQuit)); - Command_ExitGame_f(); - M_StartMessage("Server Disconnected", M_GetText("Server Timeout\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); -} - -// Called when a signature check fails and we suspect the server is playing games. -void HandleSigfail(const char *string) -{ - if (server) // This situation is basically guaranteed to be nonsense. - { - CONS_Alert(CONS_ERROR, "Auth error! %s\n", string); - return; // Keep the game running, you're probably testing. - } - - LUA_HookBool(false, HOOK(GameQuit)); - Command_ExitGame_f(); - M_StartMessage("Server Disconnected", va(M_GetText("Signature check failed.\n(%s)\n"), string), NULL, MM_NOTHING, NULL, "Back to Menu"); -} - -/** Called when a PT_SERVERINFO packet is received - * - * \param node The packet sender - * \note What happens if the packet comes from a client or something like that? - * - */ -static void HandleServerInfo(SINT8 node) -{ - // compute ping in ms - const tic_t ticnow = I_GetTime(); - const tic_t ticthen = (tic_t)LONG(netbuffer->u.serverinfo.time); - const tic_t ticdiff = (ticnow - ticthen)*1000/NEWTICRATE; - netbuffer->u.serverinfo.time = (tic_t)LONG(ticdiff); - netbuffer->u.serverinfo.servername[MAXSERVERNAME-1] = 0; - netbuffer->u.serverinfo.application - [sizeof netbuffer->u.serverinfo.application - 1] = '\0'; - netbuffer->u.serverinfo.gametypename - [sizeof netbuffer->u.serverinfo.gametypename - 1] = '\0'; - D_SanitizeKeepColors(netbuffer->u.serverinfo.servername, netbuffer->u.serverinfo.servername, MAXSERVERNAME); - - // If we have cause to reject it, it's not worth observing. - if ( - SL_InsertServer(&netbuffer->u.serverinfo, node) == false - && serverlistultimatecount - ) - { - serverlistultimatecount--; - } -} - -static void PT_WillResendGamestate(void) -{ - char tmpsave[1024]; - - if (server || cl_redownloadinggamestate) - return; - - // Don't let the server pull a fast one with everyone's identity! - // Save the public keys we see, so if the server tries to swap one, we'll know. - int i; - for (i = 0; i < MAXPLAYERS; i++) - { - memcpy(priorKeys[i], players[i].public_key, sizeof(priorKeys[i])); - } - - // Send back a PT_CANRECEIVEGAMESTATE packet to the server - // so they know they can start sending the game state - netbuffer->packettype = PT_CANRECEIVEGAMESTATE; - if (!HSendPacket(servernode, true, 0, 0)) - return; - - CONS_Printf(M_GetText("Reloading game state...\n")); - - sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home); - - // Don't get a corrupt savegame error because tmpsave already exists - if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1) - I_Error("Can't delete %s\n", tmpsave); - - CL_PrepareDownloadSaveGame(tmpsave); - - cl_redownloadinggamestate = true; -} - -static void PT_CanReceiveGamestate(SINT8 node) -{ - if (client || sendingsavegame[node]) - return; - - CONS_Printf(M_GetText("Resending game state to %s...\n"), player_names[nodetoplayer[node]]); - - extern consvar_t cv_dumpconsistency; - if (cv_dumpconsistency.value) - { - char dump_name[1024]; - snprintf( - dump_name, sizeof(dump_name), - "%s_%u_%s-server.consdump", - server_context, - gametic, - player_names[nodetoplayer[node]] - ); - CL_DumpConsistency(dump_name); - } - - SV_SendSaveGame(node, true); // Resend a complete game state - resendingsavegame[node] = true; -} - -/** Handles a packet received from a node that isn't in game - * - * \param node The packet sender - * \todo Choose a better name, as the packet can also come from the server apparently? - * \sa HandlePacketFromPlayer - * \sa GetPackets - * - */ -static void HandlePacketFromAwayNode(SINT8 node) -{ - if (node != servernode) - DEBFILE(va("Received packet from unknown host %d\n", node)); - -// macro for packets that should only be sent by the server -// if it is NOT from the server, bail out and close the connection! -#define SERVERONLY \ - if (node != servernode) \ - { \ - Net_CloseConnection(node); \ - break; \ - } - switch (netbuffer->packettype) - { - case PT_ASKINFOVIAMS: -#if 0 - if (server && serverrunning) - { - INT32 clientnode; - if (ms_RoomId < 0) // ignore if we're not actually on the MS right now - { - Net_CloseConnection(node); // and yes, close connection - return; - } - clientnode = I_NetMakeNode(netbuffer->u.msaskinfo.clientaddr); - if (clientnode != -1) - { - SV_SendServerInfo(clientnode, (tic_t)LONG(netbuffer->u.msaskinfo.time)); - SV_SendPlayerInfo(clientnode); // Send extra info - Net_CloseConnection(clientnode); - // Don't close connection to MS... - } - else - Net_CloseConnection(node); // ...unless the IP address is not valid - } - else - Net_CloseConnection(node); // you're not supposed to get it, so ignore it -#else - Net_CloseConnection(node); -#endif - break; - - case PT_TELLFILESNEEDED: - if (server && serverrunning) - { - UINT8 *p; - INT32 firstfile = netbuffer->u.filesneedednum; - - netbuffer->packettype = PT_MOREFILESNEEDED; - netbuffer->u.filesneededcfg.first = firstfile; - netbuffer->u.filesneededcfg.more = 0; - - p = PutFileNeeded(firstfile); - - HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u)); - } - else // Shouldn't get this if you aren't the server...? - Net_CloseConnection(node); - break; - - case PT_MOREFILESNEEDED: - if (server && serverrunning) - { // But wait I thought I'm the server? - Net_CloseConnection(node); - break; - } - SERVERONLY - if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum) - { - D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first); - if (!netbuffer->u.filesneededcfg.more) - cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list - } - break; - - case PT_ASKINFO: - if (server && serverrunning) - { - SV_SendServerInfo(node, (tic_t)LONG(netbuffer->u.askinfo.time)); - SV_SendPlayerInfo(node); // Send extra info - } - Net_CloseConnection(node); - break; - - case PT_SERVERREFUSE: // Negative response of client join request - if (server && serverrunning) - { // But wait I thought I'm the server? - Net_CloseConnection(node); - break; - } - SERVERONLY - if (cl_mode == CL_WAITJOINRESPONSE) - { - // Save the reason so it can be displayed after quitting the netgame - char *reason = strdup(netbuffer->u.serverrefuse.reason); - if (!reason) - I_Error("Out of memory!\n"); - - if (strstr(reason, "Maximum players reached")) - { - serverisfull = true; - //Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer - //We set it back to the value of cv_nettimeout.value in CL_Reset - connectiontimeout = NEWTICRATE*7; - cl_mode = CL_ASKJOIN; - free(reason); - break; - } - - Command_ExitGame_f(); - - if (reason[1] == '|') - { - M_StartMessage("Server Connection Failure", - va("You have been %sfrom the server\n\nReason:\n%s", - (reason[0] == 'B') ? "banned\n" : "temporarily\nkicked ", - reason+2), NULL, MM_NOTHING, NULL, "Back to Menu"); - } - else - { - M_StartMessage("Server Connection Failure", - va(M_GetText("Server refuses connection\n\nReason:\n%s"), - reason), NULL, MM_NOTHING, NULL, "Back to Menu"); - } - - free(reason); - - // Will be reset by caller. Signals refusal. - cl_mode = CL_ABORTED; - } - break; - - case PT_SERVERCFG: // Positive response of client join request - { - if (server && serverrunning && node != servernode) - { // but wait I thought I'm the server? - Net_CloseConnection(node); - break; - } - SERVERONLY - /// \note how would this happen? and is it doing the right thing if it does? - if (!(cl_mode == CL_WAITJOINRESPONSE || cl_mode == CL_ASKJOIN)) - break; - - if (client) - { - maketic = gametic = neededtic = (tic_t)LONG(netbuffer->u.servercfg.gametic); - - G_SetGametype(netbuffer->u.servercfg.gametype); - - modifiedgame = netbuffer->u.servercfg.modifiedgame; - - memcpy(server_context, netbuffer->u.servercfg.server_context, 8); - - D_SanitizeKeepColors(connectedservername, netbuffer->u.servercfg.server_name, MAXSERVERNAME); - D_SanitizeKeepColors(connectedservercontact, netbuffer->u.servercfg.server_contact, MAXSERVERCONTACT); - } - -#ifdef HAVE_DISCORDRPC - discordInfo.maxPlayers = netbuffer->u.servercfg.maxplayer; - discordInfo.joinsAllowed = netbuffer->u.servercfg.allownewplayer; - discordInfo.everyoneCanInvite = netbuffer->u.servercfg.discordinvites; -#endif - - nodeingame[(UINT8)servernode] = true; - serverplayer = netbuffer->u.servercfg.serverplayer; - doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum); - mynode = netbuffer->u.servercfg.clientnode; - if (serverplayer >= 0) - playernode[(UINT8)serverplayer] = servernode; - - if (netgame) - CONS_Printf(M_GetText("Join accepted, waiting for complete game state...\n")); - DEBFILE(va("Server accept join gametic=%u mynode=%d\n", gametic, mynode)); - - /// \note Wait. What if a Lua script uses some global custom variables synched with the NetVars hook? - /// Shouldn't them be downloaded even at intermission time? - /// Also, according to HandleConnect, the server will send the savegame even during intermission... - /// Sryder 2018-07-05: If we don't want to send the player config another way we need to send the gamestate - /// At almost any gamestate there could be joiners... So just always send gamestate? - cl_mode = ((server) ? CL_CONNECTED : CL_DOWNLOADSAVEGAME); - break; - } - - // Handled in d_netfil.c - case PT_FILEFRAGMENT: - if (server) - { // But wait I thought I'm the server? - Net_CloseConnection(node); - break; - } - SERVERONLY - PT_FileFragment(); - break; - - case PT_FILEACK: - if (server) - PT_FileAck(); - break; - - case PT_FILERECEIVED: - if (server) - PT_FileReceived(); - break; - - case PT_REQUESTFILE: - if (server) - { - if (!cv_downloading.value || !PT_RequestFile(node)) - Net_CloseConnection(node); // close connection if one of the requested files could not be sent, or you disabled downloading anyway - } - else - Net_CloseConnection(node); // nope - break; - - case PT_NODETIMEOUT: - case PT_CLIENTQUIT: - if (server) - { - Net_CloseConnection(node); - nodeneedsauth[node] = false; - } - break; - - case PT_CLIENTCMD: - break; // This is not an "unknown packet" - - case PT_SERVERTICS: - // Do not remove my own server (we have just get a out of order packet) - if (node == servernode) - break; - /* FALLTHRU */ - case PT_CLIENTKEY: - if (server) - { - PT_ClientKey(node); - - // Client's not in the server yet, but we still need to lock up the node. - // Otherwise, someone else could request a challenge on the same node and trash it. - nodeneedsauth[node] = true; - freezetimeout[node] = I_GetTime() + jointimeout; - } - break; - case PT_SERVERCHALLENGE: - if (server && serverrunning && node != servernode) - { - Net_CloseConnection(node); - break; - } - if (cl_mode != CL_WAITCHALLENGE) - break; - memcpy(awaitingChallenge, netbuffer->u.serverchallenge.secret, sizeof(awaitingChallenge)); - cl_mode = CL_ASKJOIN; - break; - default: - DEBFILE(va("unknown packet received (%d) from unknown host\n",netbuffer->packettype)); - Net_CloseConnection(node); - break; // Ignore it - - } -#undef SERVERONLY -} - -/** Checks ticcmd for "speed hacks" - * - * \param p Which player - * \return True if player is hacking - * \sa HandlePacketFromPlayer - * - */ -static boolean CheckForSpeedHacks(UINT8 p) -{ - if (netcmds[maketic%BACKUPTICS][p].forwardmove > MAXPLMOVE || netcmds[maketic%BACKUPTICS][p].forwardmove < -MAXPLMOVE - || netcmds[maketic%BACKUPTICS][p].turning > KART_FULLTURN || netcmds[maketic%BACKUPTICS][p].turning < -KART_FULLTURN - || netcmds[maketic%BACKUPTICS][p].throwdir > KART_FULLTURN || netcmds[maketic%BACKUPTICS][p].throwdir < -KART_FULLTURN) - { - CONS_Alert(CONS_WARNING, M_GetText("Illegal movement value received from node %d\n"), playernode[p]); - //D_Clearticcmd(k); - - SendKick(p, KICK_MSG_CON_FAIL); - return true; - } - - return false; -} - -static void PT_Say(int node) -{ - if (client) - return; // Only sent to servers, why are we receiving this? - - say_pak say = netbuffer->u.say; - - // Check for a spoofed source. - if (say.source == serverplayer) - { - // Servers aren't guaranteed to have a playernode, dedis exist. - if (node != servernode) - return; - } - else - { - if (playernode[say.source] != node) - return; - } - - if ((cv_mute.value || say.flags & (HU_CSAY|HU_SHOUT)) && say.source != serverplayer && !(IsPlayerAdmin(say.source))) - { - CONS_Debug(DBG_NETPLAY,"Received SAY cmd from Player %d (%s), but cv_mute is on.\n", say.source+1, player_names[say.source]); - return; - } - - if ((say.flags & HU_PRIVNOTICE) && !(IsPlayerAdmin(say.source))) - { - CONS_Debug(DBG_NETPLAY,"Received SAY cmd from Player %d (%s) with an illegal HU_PRIVNOTICE flag.\n", say.source+1, player_names[say.source]); - SendKick(say.source, KICK_MSG_CON_FAIL); - return; - } - - { - size_t i; - for (i = 0; i < sizeof say.message && say.message[i]; i++) - { - if (say.message[i] & 0x80) - { - CONS_Alert(CONS_WARNING, M_GetText("Illegal say command received from %s containing invalid characters\n"), player_names[say.source]); - SendKick(say.source, KICK_MSG_CON_FAIL); - return; - } - } - } - - if (stop_spamming[say.source] != 0 && consoleplayer != say.source && cv_chatspamprotection.value && !(say.flags & (HU_CSAY|HU_SHOUT))) - { - CONS_Debug(DBG_NETPLAY,"Received SAY cmd too quickly from Player %d (%s), assuming as spam and blocking message.\n", say.source+1, player_names[say.source]); - stop_spamming[say.source] = 4; - return; - } - - stop_spamming[say.source] = 4; - - serverplayer_t *stats = SV_GetStatsByPlayerIndex(say.source); - - if (stats->finishedrounds < (uint32_t)cv_gamestochat.value && !(consoleplayer == say.source || IsPlayerAdmin(say.source))) - { - CONS_Debug(DBG_NETPLAY,"Received SAY cmd from Player %d (%s), but they aren't permitted to chat yet.\n", say.source+1, player_names[say.source]); - - char rejectmsg[256]; - strlcpy(rejectmsg, va("Please finish in %d more games to use chat.", cv_gamestochat.value - stats->finishedrounds), 256); - if (IsPlayerGuest(say.source)) - strlcpy(rejectmsg, va("GUESTs can't chat on this server. Rejoin with a profile to track your playtime."), 256); - SendServerNotice(say.source, rejectmsg); - - return; - } - - DoSayCommand(say.message, say.target, say.flags, say.source); -} - -static void PT_ReqMapQueue(int node) -{ - if (client) - return; // Only sent to servers, why are we receiving this? - - reqmapqueue_pak reqmapqueue = netbuffer->u.reqmapqueue; - - // Check for a spoofed source. - if (reqmapqueue.source == serverplayer) - { - // Servers aren't guaranteed to have a playernode, dedis exist. - if (node != servernode) - return; - } - else - { - if (playernode[reqmapqueue.source] != node) - return; - - if (!IsPlayerAdmin(reqmapqueue.source)) - { - CONS_Debug(DBG_NETPLAY,"Received illegal request map queue cmd from Player %d (%s).\n", reqmapqueue.source+1, player_names[reqmapqueue.source]); - SendKick(reqmapqueue.source, KICK_MSG_CON_FAIL); - return; - } - } - - const boolean doclear = (reqmapqueue.newgametype == ROUNDQUEUE_CMD_CLEAR); - - // The following prints will only appear when multiple clients - // attempt to affect the round queue at similar time increments - if (doclear == true) - { - if (roundqueue.size == 0) - { - // therefore this one doesn't really need a error print - // because what both players wanted was done anyways - //CONS_Alert(CONS_ERROR, "queuemap: Queue is already empty!\n"); - return; - } - } - else if (reqmapqueue.newgametype == ROUNDQUEUE_CMD_SHOW) - { - char maprevealmsg[256]; - if (roundqueue.size == 0) - { - strlcpy(maprevealmsg, "There are no Rounds queued.", 256); - } - else if (roundqueue.position >= roundqueue.size) - { - strlcpy(maprevealmsg, "There are no more Rounds queued!", 256); - } - else - { - char *title = G_BuildMapTitle(roundqueue.entries[roundqueue.position].mapnum + 1); - - strlcpy( - maprevealmsg, - va("The next Round will be on \"%s\".", title), - 256 - ); - - Z_Free(title); - } - DoSayCommand(maprevealmsg, 0, HU_SHOUT, servernode); - - return; - } - else if (roundqueue.size >= ROUNDQUEUE_MAX) - { - CONS_Alert(CONS_ERROR, "Recieved REQMAPQUEUE, but unable to add map beyond %u\n", roundqueue.size); - - // But this one does, because otherwise it's silent failure! - char rejectmsg[256]; - strlcpy(rejectmsg, "The server couldn't queue your chosen map.", 256); - SendServerNotice(reqmapqueue.source, rejectmsg); - - return; - } - - if (reqmapqueue.newmapnum == NEXTMAP_VOTING) - { - UINT8 numPlayers = 0, i; - for (i = 0; i < MAXPLAYERS; ++i) - { - if (!playeringame[i] || players[i].spectator) - { - continue; - } - - extern consvar_t cv_forcebots; // debug - - if (!(gametypes[reqmapqueue.newgametype]->rules & GTR_BOTS) && players[i].bot && !cv_forcebots.value) - { - // Gametype doesn't support bots - continue; - } - - numPlayers++; - } - - reqmapqueue.newmapnum = G_RandMapPerPlayerCount(G_TOLFlag(reqmapqueue.newgametype), UINT16_MAX, false, false, NULL, numPlayers); - } - - if (reqmapqueue.newmapnum >= nummapheaders) - { - CONS_Alert(CONS_ERROR, "Recieved REQMAPQUEUE, but unable to add map of invalid ID (%u)\n", reqmapqueue.newmapnum); - - char rejectmsg[256]; - strlcpy(rejectmsg, "The server couldn't queue your chosen map.", 256); - SendServerNotice(reqmapqueue.source, rejectmsg); - - return; - } - - G_AddMapToBuffer(reqmapqueue.newmapnum); - - UINT8 buf[1+2+1]; - UINT8 *buf_p = buf; - - WRITEUINT8(buf_p, reqmapqueue.flags); - WRITEUINT16(buf_p, reqmapqueue.newgametype); - - WRITEUINT8(buf_p, roundqueue.size); - - // Match Got_MapQueuecmd, but with the addition of reqmapqueue.newmapnum available to us - if (doclear == true) - { - memset(&roundqueue, 0, sizeof(struct roundqueue)); - } - else - { - G_MapIntoRoundQueue( - reqmapqueue.newmapnum, - reqmapqueue.newgametype, - ((reqmapqueue.flags & 1) != 0), - false - ); - } - - SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf); -} - -static char NodeToSplitPlayer(int node, int split) -{ - if (split == 0) - return nodetoplayer[node]; - else if (split == 1) - return nodetoplayer2[node]; - else if (split == 2) - return nodetoplayer3[node]; - else if (split == 3) - return nodetoplayer4[node]; - return -1; -} - -static void FuzzTiccmd(ticcmd_t* target) -{ - extern consvar_t cv_fuzz; - if (cv_fuzz.value) - { - target->forwardmove = P_RandomRange(PR_FUZZ, -MAXPLMOVE, MAXPLMOVE); - target->turning = P_RandomRange(PR_FUZZ, -KART_FULLTURN, KART_FULLTURN); - target->throwdir = P_RandomRange(PR_FUZZ, -KART_FULLTURN, KART_FULLTURN); - target->buttons = P_RandomRange(PR_FUZZ, 0, 255); - - // Make fuzzed players more likely to do impactful things - if (P_RandomRange(PR_FUZZ, 0, 500)) - { - target->buttons |= BT_ACCELERATE; - target->buttons &= ~BT_LOOKBACK; - target->buttons &= ~BT_RESPAWN; - target->buttons &= ~BT_BRAKE; - } - } -} - -/** Handles a packet received from a node that is in game - * - * \param node The packet sender - * \todo Choose a better name - * \sa HandlePacketFromAwayNode - * \sa GetPackets - * - */ -static void HandlePacketFromPlayer(SINT8 node) -{ - INT32 netconsole; - tic_t realend, realstart; - UINT8 *pak, *txtpak, numtxtpak; -#ifndef NOMD5 - UINT8 finalmd5[16];/* Well, it's the cool thing to do? */ -#endif - - txtpak = NULL; - - if (dedicated && node == 0) - netconsole = 0; - else - netconsole = nodetoplayer[node]; -#ifdef PARANOIA - if (netconsole >= MAXPLAYERS) - I_Error("bad table nodetoplayer: node %d player %d", doomcom->remotenode, netconsole); -#endif - - -#ifdef SIGNGAMETRAFFIC - if (server) - { - - int splitnodes; - if (IsPacketSigned(netbuffer->packettype)) - { - for (splitnodes = 0; splitnodes < MAXSPLITSCREENPLAYERS; splitnodes++) - { - int targetplayer = NodeToSplitPlayer(node, splitnodes); - if (targetplayer == -1) - continue; - - const void* message = &netbuffer->u; - if (IsSplitPlayerOnNodeGuest(node, splitnodes) || demo.playback) - { - //CONS_Printf("Throwing out a guest signature from node %d player %d\n", node, splitnodes); - } - else - { - if (crypto_eddsa_check(netbuffer->signature[splitnodes], players[targetplayer].public_key, message, doomcom->datalength - BASEPACKETSIZE)) - { - CONS_Alert(CONS_ERROR, "SIGFAIL! Packet type %d from node %d player %d\nkey %s size %d netconsole %d\n", - netbuffer->packettype, node, splitnodes, - GetPrettyRRID(players[targetplayer].public_key, true), doomcom->datalength - BASEPACKETSIZE, netconsole); - - // Something scary can happen when multiple kicks that resolve to the same node are processed in quick succession. - // Sometimes, a kick will still be left to process after the player's been disposed, and that causes the kick to resolve on the server instead! - // This sucks, so we check for a stale/misfiring kick beforehand. - if (netconsole != -1) - SendKick(netconsole, KICK_MSG_SIGFAIL); - // Net_CloseConnection(node); - // nodeingame[node] = false; - return; - } - } - - } - } - } -#endif - - switch (netbuffer->packettype) - { -// -------------------------------------------- SERVER RECEIVE ---------- - case PT_CLIENTCMD: - case PT_CLIENT2CMD: - case PT_CLIENT3CMD: - case PT_CLIENT4CMD: - case PT_CLIENTMIS: - case PT_CLIENT2MIS: - case PT_CLIENT3MIS: - case PT_CLIENT4MIS: - case PT_NODEKEEPALIVE: - case PT_NODEKEEPALIVEMIS: - if (client) - break; - - // To save bytes, only the low byte of tic numbers are sent - // Use ExpandTics to figure out what the rest of the bytes are - - realstart = ExpandTics(netbuffer->u.clientpak.client_tic, nettics[node]); - realend = ExpandTics(netbuffer->u.clientpak.resendfrom, nettics[node]); - - if (netbuffer->packettype == PT_CLIENTMIS || netbuffer->packettype == PT_CLIENT2MIS - || netbuffer->packettype == PT_CLIENT3MIS || netbuffer->packettype == PT_CLIENT4MIS - || netbuffer->packettype == PT_NODEKEEPALIVEMIS - || supposedtics[node] < realend) - { - supposedtics[node] = realend; - } - // Discard out of order packet - if (nettics[node] > realend) - { - DEBFILE(va("out of order ticcmd discarded nettics = %u\n", nettics[node])); - break; - } - - // Update the nettics - nettics[node] = realend; - - // This should probably still timeout though, as the node should always have a player 1 number - if (netconsole == -1) - break; - - // As long as clients send valid ticcmds, the server can keep running, so reset the timeout - /// \todo Use a separate cvar for that kind of timeout? - freezetimeout[node] = I_GetTime() + connectiontimeout; - - // Don't do anything for packets of type NODEKEEPALIVE? - // Sryder 2018/07/01: Update the freezetimeout still! - if (netbuffer->packettype == PT_NODEKEEPALIVE - || netbuffer->packettype == PT_NODEKEEPALIVEMIS) - break; - - // If we already received a ticcmd for this tic, just submit it for the next one. - tic_t faketic = maketic; - - if ((!!(netcmds[maketic % BACKUPTICS][netconsole].flags & TICCMD_RECEIVED)) - && (maketic - firstticstosend < BACKUPTICS)) - faketic++; - - FuzzTiccmd(&netbuffer->u.clientpak.cmd); - - // Copy ticcmd - G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][netconsole], &netbuffer->u.clientpak.cmd, 1); - - // Check ticcmd for "speed hacks" - if (CheckForSpeedHacks((UINT8)netconsole)) - break; - - // Splitscreen cmd - if (((netbuffer->packettype == PT_CLIENT2CMD || netbuffer->packettype == PT_CLIENT2MIS) - || (netbuffer->packettype == PT_CLIENT3CMD || netbuffer->packettype == PT_CLIENT3MIS) - || (netbuffer->packettype == PT_CLIENT4CMD || netbuffer->packettype == PT_CLIENT4MIS)) - && (nodetoplayer2[node] >= 0)) - { - FuzzTiccmd(&netbuffer->u.client2pak.cmd2); - G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)nodetoplayer2[node]], - &netbuffer->u.client2pak.cmd2, 1); - - if (CheckForSpeedHacks((UINT8)nodetoplayer2[node])) - break; - } - - if (((netbuffer->packettype == PT_CLIENT3CMD || netbuffer->packettype == PT_CLIENT3MIS) - || (netbuffer->packettype == PT_CLIENT4CMD || netbuffer->packettype == PT_CLIENT4MIS)) - && (nodetoplayer3[node] >= 0)) - { - FuzzTiccmd(&netbuffer->u.client3pak.cmd3); - G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)nodetoplayer3[node]], - &netbuffer->u.client3pak.cmd3, 1); - - if (CheckForSpeedHacks((UINT8)nodetoplayer3[node])) - break; - } - - if ((netbuffer->packettype == PT_CLIENT4CMD || netbuffer->packettype == PT_CLIENT4MIS) - && (nodetoplayer4[node] >= 0)) - { - FuzzTiccmd(&netbuffer->u.client4pak.cmd4); - G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)nodetoplayer4[node]], - &netbuffer->u.client4pak.cmd4, 1); - - if (CheckForSpeedHacks((UINT8)nodetoplayer4[node])) - break; - } - - // Check player consistancy during the level - if (realstart <= gametic && realstart + BACKUPTICS - 1 > gametic && gamestate == GS_LEVEL - && consistancy[realstart%BACKUPTICS] != SHORT(netbuffer->u.clientpak.consistancy) - && !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime() - && !SV_ResendingSavegameToAnyone()) - { - // Tell the client we are about to resend them the gamestate - netbuffer->packettype = PT_WILLRESENDGAMESTATE; - HSendPacket(node, true, 0, 0); - - resendingsavegame[node] = true; - - if (cv_blamecfail.value) - CONS_Printf(M_GetText("Synch failure for player %d (%s); expected %hd, got %hd\n"), - netconsole+1, player_names[netconsole], - consistancy[realstart%BACKUPTICS], - SHORT(netbuffer->u.clientpak.consistancy)); - DEBFILE(va("Restoring player %d (synch failure) [%update] %d!=%d\n", - netconsole, realstart, consistancy[realstart%BACKUPTICS], - SHORT(netbuffer->u.clientpak.consistancy))); - break; - } - break; - case PT_BASICKEEPALIVE: - if (client) - break; - - // This should probably still timeout though, as the node should always have a player 1 number - if (netconsole == -1) - break; - - // If a client sends this it should mean they are done receiving the savegame - sendingsavegame[node] = false; - - // As long as clients send keep alives, the server can keep running, so reset the timeout - /// \todo Use a separate cvar for that kind of timeout? - freezetimeout[node] = I_GetTime() + connectiontimeout; - break; - case PT_TEXTCMD: - case PT_TEXTCMD2: - case PT_TEXTCMD3: - case PT_TEXTCMD4: - if (netbuffer->packettype == PT_TEXTCMD2) // splitscreen special - netconsole = nodetoplayer2[node]; - else if (netbuffer->packettype == PT_TEXTCMD3) - netconsole = nodetoplayer3[node]; - else if (netbuffer->packettype == PT_TEXTCMD4) - netconsole = nodetoplayer4[node]; - - if (client) - break; - - if (netconsole < 0 || netconsole >= MAXPLAYERS) - Net_UnAcknowledgePacket(node); - else - { - size_t j; - tic_t tic = maketic; - UINT8 *textcmd; - - UINT16 incoming_size; - - { - UINT8 *incoming = netbuffer->u.textcmd; - - incoming_size = READUINT16(incoming); - } - - // ignore if the textcmd has a reported size of zero - // this shouldn't be sent at all - if (!incoming_size) - { - DEBFILE(va("GetPacket: Textcmd with size 0 detected! (node %u, player %d)\n", - node, netconsole)); - Net_UnAcknowledgePacket(node); - break; - } - - // ignore if the textcmd size var is actually larger than it should be - // BASEPACKETSIZE + 2 (for size) + textcmd[0] should == datalength - if (incoming_size > (size_t)doomcom->datalength-BASEPACKETSIZE-2) - { - DEBFILE(va("GetPacket: Bad Textcmd packet size! (expected %d, actual %s, node %u, player %d)\n", - incoming_size, sizeu1((size_t)doomcom->datalength-BASEPACKETSIZE-2), - node, netconsole)); - Net_UnAcknowledgePacket(node); - break; - } - - // check if tic that we are making isn't too large else we cannot send it :( - // doomcom->numslots+1 "+1" since doomcom->numslots can change within this time and sent time - j = software_MAXPACKETLENGTH - - (incoming_size + 3 + BASESERVERTICSSIZE - + (doomcom->numslots+1)*sizeof(ticcmd_t)); - - // search a tic that have enougth space in the ticcmd - while ((textcmd = D_GetExistingTextcmd(tic, netconsole)), - (TotalTextCmdPerTic(tic) > j || incoming_size + (textcmd ? ((UINT16*)textcmd)[0] : 0) > MAXTEXTCMD) - && tic < firstticstosend + BACKUPTICS) - tic++; - - if (tic >= firstticstosend + BACKUPTICS) - { - DEBFILE(va("GetPacket: Textcmd too long (max %s, used %s, mak %d, " - "tosend %u, node %u, player %d)\n", sizeu1(j), sizeu2(TotalTextCmdPerTic(maketic)), - maketic, firstticstosend, node, netconsole)); - Net_UnAcknowledgePacket(node); - break; - } - - // Make sure we have a buffer - if (!textcmd) textcmd = D_GetTextcmd(tic, netconsole); - - DEBFILE(va("textcmd put in tic %u at position %d (player %d) ftts %u mk %u\n", - tic, ((UINT16*)textcmd)[0]+2, netconsole, firstticstosend, maketic)); - - M_Memcpy(&textcmd[((UINT16*)textcmd)[0]+2], netbuffer->u.textcmd+2, incoming_size); - ((UINT16*)textcmd)[0] += incoming_size; - } - break; - case PT_SAY: - PT_Say(node); - break; - case PT_REQMAPQUEUE: - PT_ReqMapQueue(node); - break; - case PT_LOGIN: - if (client) - break; -#ifndef NOMD5 - if (doomcom->datalength < 16)/* ignore partial sends */ - break; - - if (!adminpasswordset) - { - CONS_Printf(M_GetText("Password from %s failed (no password set).\n"), player_names[netconsole]); - break; - } - - // Do the final pass to compare with the sent md5 - D_MD5PasswordPass(adminpassmd5, 16, va("PNUM%02d", netconsole), &finalmd5); - - if (!memcmp(netbuffer->u.md5sum, finalmd5, 16)) - { - CONS_Printf(M_GetText("%s passed authentication.\n"), player_names[netconsole]); - COM_BufInsertText(va("promote %d\n", netconsole)); // do this immediately - } - else - CONS_Printf(M_GetText("Password from %s failed.\n"), player_names[netconsole]); -#endif - break; - case PT_NODETIMEOUT: - case PT_CLIENTQUIT: - if (client) - break; - - // nodeingame will be put false in the execution of kick command - // this allow to send some packets to the quitting client to have their ack back - nodewaiting[node] = 0; - if (netconsole != -1 && playeringame[netconsole]) - { - UINT8 kickmsg; - - if (netbuffer->packettype == PT_NODETIMEOUT) - kickmsg = KICK_MSG_TIMEOUT; - else - kickmsg = KICK_MSG_PLAYER_QUIT; - - SendKick(netconsole, kickmsg); - - /* - nodetoplayer[node] = -1; - - if (nodetoplayer2[node] != -1 && nodetoplayer2[node] >= 0 - && playeringame[(UINT8)nodetoplayer2[node]]) - { - SendKick(nodetoplayer2[node], kickmsg); - nodetoplayer2[node] = -1; - } - - if (nodetoplayer3[node] != -1 && nodetoplayer3[node] >= 0 - && playeringame[(UINT8)nodetoplayer3[node]]) - { - SendKick(nodetoplayer3[node], kickmsg); - nodetoplayer3[node] = -1; - } - - if (nodetoplayer4[node] != -1 && nodetoplayer4[node] >= 0 - && playeringame[(UINT8)nodetoplayer4[node]]) - { - SendKick(nodetoplayer4[node], kickmsg); - nodetoplayer4[node] = -1; - } - */ - } - Net_CloseConnection(node); - nodeingame[node] = false; - nodeneedsauth[node] = false; - break; - case PT_CANRECEIVEGAMESTATE: - PT_CanReceiveGamestate(node); - break; - case PT_ASKLUAFILE: - if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_ASKED) - AddLuaFileToSendQueue(node, luafiletransfers->realfilename); - break; - case PT_HASLUAFILE: - if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_SENDING) - SV_HandleLuaFileSent(node); - break; - case PT_RECEIVEDGAMESTATE: - sendingsavegame[node] = false; - resendingsavegame[node] = false; - savegameresendcooldown[node] = I_GetTime() + 5 * TICRATE; - break; -// -------------------------------------------- CLIENT RECEIVE ---------- - case PT_SERVERTICS: - // Only accept PT_SERVERTICS from the server. - if (node != servernode) - { - CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_SERVERTICS", node); - if (server) - SendKick(netconsole, KICK_MSG_CON_FAIL); - break; - } - - realstart = ExpandTics(netbuffer->u.serverpak.starttic, maketic); - realend = realstart + netbuffer->u.serverpak.numtics; - - if (!txtpak) - txtpak = (UINT8 *)&netbuffer->u.serverpak.cmds[netbuffer->u.serverpak.numslots - * netbuffer->u.serverpak.numtics]; - - if (realend > gametic + CLIENTBACKUPTICS) - realend = gametic + CLIENTBACKUPTICS; - cl_packetmissed = realstart > neededtic; - - if (realstart <= neededtic && realend > neededtic) - { - tic_t i, j; - pak = (UINT8 *)&netbuffer->u.serverpak.cmds; - - for (i = realstart; i < realend; i++) - { - // clear first - D_Clearticcmd(i); - - // copy the tics - pak = G_ScpyTiccmd(netcmds[i%BACKUPTICS], pak, - netbuffer->u.serverpak.numslots*sizeof (ticcmd_t)); - - // copy the textcmds - numtxtpak = *txtpak++; - for (j = 0; j < numtxtpak; j++) - { - INT32 k = *txtpak++; // playernum - const size_t txtsize = ((UINT16*)txtpak)[0]+2; - - if (i >= gametic) // Don't copy old net commands - M_Memcpy(D_GetTextcmd(i, k), txtpak, txtsize); - txtpak += txtsize; - } - } - - neededtic = realend; - } - else - { - DEBFILE(va("frame not in bound: %u\n", neededtic)); - /*if (realend < neededtic - 2 * TICRATE || neededtic + 2 * TICRATE < realstart) - I_Error("Received an out of order PT_SERVERTICS packet!\n" - "Got tics %d-%d, needed tic %d\n\n" - "Please report this crash on the Master Board,\n" - "IRC or Discord so it can be fixed.\n", (INT32)realstart, (INT32)realend, (INT32)neededtic);*/ - } - break; - case PT_PING: - // Only accept PT_PING from the server. - if (node != servernode) - { - CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_PING", node); - if (server) - SendKick(netconsole, KICK_MSG_CON_FAIL); - break; - } - - //Update client ping table from the server. - if (client) - { - UINT8 i; - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i]) - { - playerpingtable[i] = (tic_t)netbuffer->u.netinfo.pingtable[i]; - playerpacketlosstable[i] = netbuffer->u.netinfo.packetloss[i]; - playerdelaytable[i] = netbuffer->u.netinfo.delay[i]; - } - } - - servermaxping = (tic_t)netbuffer->u.netinfo.pingtable[MAXPLAYERS]; - } - - break; - case PT_SERVERCFG: - break; - case PT_FILEFRAGMENT: - // Only accept PT_FILEFRAGMENT from the server. - if (node != servernode) - { - CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_FILEFRAGMENT", node); - if (server) - SendKick(netconsole, KICK_MSG_CON_FAIL); - break; - } - if (client) - PT_FileFragment(); - break; - case PT_FILEACK: - if (server) - PT_FileAck(); - break; - case PT_FILERECEIVED: - if (server) - PT_FileReceived(); - break; - case PT_WILLRESENDGAMESTATE: - PT_WillResendGamestate(); - break; - case PT_SENDINGLUAFILE: - if (client) - CL_PrepareDownloadLuaFile(); - break; - case PT_CHALLENGEALL: - if (demo.playback || node != servernode) // SERVER should still respond to this to prove its own identity, just not from clients. - break; - - int challengeplayers; - - memcpy(lastChallengeAll, netbuffer->u.challengeall.secret, sizeof(lastChallengeAll)); - - shouldsign_t safe = ShouldSignChallenge(lastChallengeAll); - if (safe != SIGN_OK) - { - if (safe == SIGN_BADIP) - HandleSigfail("External server sent the wrong IP"); - else if (safe == SIGN_BADTIME) - HandleSigfail("Bad timestamp - is your time set correctly?"); - else - HandleSigfail("Unknown auth error - contact a developer"); - break; - } - - netbuffer->packettype = PT_RESPONSEALL; - - #ifdef DEVELOP - if (cv_noresponse.value) - { - CV_AddValue(&cv_noresponse, -1); - CONS_Alert(CONS_WARNING, "cv_noresponse enabled, not sending PT_RESPONSEALL\n"); - break; - } - #endif - - // Don't leak uninitialized memory. - memset(&netbuffer->u.responseall, 0, sizeof(netbuffer->u.responseall)); - - for (challengeplayers = 0; challengeplayers <= splitscreen; challengeplayers++) - { - uint8_t signature[SIGNATURELENGTH]; - profile_t *localProfile = PR_GetLocalPlayerProfile(challengeplayers); - if (!PR_IsLocalPlayerGuest(challengeplayers)) // GUESTS don't have keys - { - crypto_eddsa_sign(signature, localProfile->secret_key, lastChallengeAll, sizeof(lastChallengeAll)); - - // If our keys are garbage (corrupted profile?), fail here instead of when the server boots us, so the player knows what's going on. - if (crypto_eddsa_check(signature, localProfile->public_key, lastChallengeAll, sizeof(lastChallengeAll)) != 0) - I_Error("Couldn't self-verify key associated with player %d, profile %d.\nProfile data may be corrupted.", challengeplayers, cv_lastprofile[challengeplayers].value); - } - - #ifdef DEVELOP - if (cv_badresponse.value) - { - CV_AddValue(&cv_badresponse, -1); - CONS_Alert(CONS_WARNING, "cv_badresponse enabled, scrubbing signature from PT_RESPONSEALL\n"); - memset(signature, 0, sizeof(signature)); - } - #endif - - memcpy(netbuffer->u.responseall.signature[challengeplayers], signature, sizeof(signature)); - } - - HSendPacket(servernode, true, 0, sizeof(netbuffer->u.responseall)); - break; - case PT_RESPONSEALL: - if (demo.playback || client) - break; - - int responseplayer; - for (responseplayer = 0; responseplayer < MAXSPLITSCREENPLAYERS; responseplayer++) - { - int targetplayer = NodeToSplitPlayer(node, responseplayer); - if (targetplayer == -1) - continue; - - if (!IsPlayerGuest(targetplayer)) - { - if (crypto_eddsa_check(netbuffer->u.responseall.signature[responseplayer], players[targetplayer].public_key, lastChallengeAll, sizeof(lastChallengeAll))) - { - // Something scary can happen when multiple kicks that resolve to the same node are processed in quick succession. - // Sometimes, a kick will still be left to process after the player's been disposed, and that causes the kick to resolve on the server instead! - // This sucks, so we check for a stale/misfiring kick beforehand. - if (playernode[targetplayer] != 0) - SendKick(targetplayer, KICK_MSG_SIGFAIL); - break; - } - else - { - memcpy(lastReceivedSignature[targetplayer], netbuffer->u.responseall.signature[responseplayer], sizeof(lastReceivedSignature[targetplayer])); - } - } - } - break; - case PT_RESULTSALL: - if (demo.playback || server || node != servernode || !expectChallenge) - break; - - int resultsplayer; - uint8_t allZero[PUBKEYLENGTH]; - memset(allZero, 0, sizeof(PUBKEYLENGTH)); - - for (resultsplayer = 0; resultsplayer < MAXPLAYERS; resultsplayer++) - { - if (!playeringame[resultsplayer]) - { - continue; - } - else if (IsPlayerGuest(resultsplayer)) - { - continue; - } - else if (memcmp(knownWhenChallenged[resultsplayer], allZero, sizeof(PUBKEYLENGTH)) == 0) - { - // Wasn't here for the challenge. - continue; - } - else if (memcmp(knownWhenChallenged[resultsplayer], players[resultsplayer].public_key, sizeof(knownWhenChallenged[resultsplayer])) != 0) - { - // A player left after the challenge process started, and someone else took their place. - // That means they haven't received a challenge either. - continue; - } - else - { - if (crypto_eddsa_check(netbuffer->u.resultsall.signature[resultsplayer], - knownWhenChallenged[resultsplayer], lastChallengeAll, sizeof(lastChallengeAll))) - { - CONS_Alert(CONS_WARNING, "PT_RESULTSALL had invalid signature %s for node %d player %d split %d, something doesn't add up!\n", - GetPrettyRRID(netbuffer->u.resultsall.signature[resultsplayer], true), playernode[resultsplayer], resultsplayer, players[resultsplayer].splitscreenindex); - HandleSigfail("Server sent invalid client signature."); - break; - } - } - } - csprng(lastChallengeAll, sizeof(lastChallengeAll)); - expectChallenge = false; - break; - default: - DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n", - netbuffer->packettype, node)); - } // end switch -} - -/** Handles all received packets, if any - * - * \todo Add details to this description (lol) - * - */ -static void GetPackets(void) -{ - SINT8 node; // The packet sender - - player_joining = false; - - while (HGetPacket()) - { - node = (SINT8)doomcom->remotenode; - - if (netbuffer->packettype == PT_CLIENTJOIN && server) - { - if (levelloading == false) // Otherwise just ignore - { - HandleConnect(node); - } - continue; - } - if (node == servernode && client && cl_mode != CL_SEARCHING) - { - if (netbuffer->packettype == PT_SERVERSHUTDOWN) - { - HandleShutdown(node); - continue; - } - if (netbuffer->packettype == PT_NODETIMEOUT) - { - HandleTimeout(node); - continue; - } - } - - if (netbuffer->packettype == PT_SERVERINFO) - { - HandleServerInfo(node); - continue; - } - - if (netbuffer->packettype == PT_PLAYERINFO) - continue; // We do nothing with PLAYERINFO, that's for the MS browser. - - // Packet received from someone already playing - if (nodeingame[node]) - HandlePacketFromPlayer(node); - // Packet received from someone not playing - else - HandlePacketFromAwayNode(node); - } -} - -// -// NetUpdate -// Builds ticcmds for console player, -// sends out a packet -// -// no more use random generator, because at very first tic isn't yet synchronized -// Note: It is called consistAncy on purpose. -// -static INT16 Consistancy(void) -{ - INT32 i; - UINT32 ret = 0; -#ifdef MOBJCONSISTANCY - thinker_t *th; - mobj_t *mo; -#endif - - DEBFILE(va("TIC %u ", gametic)); - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i]) - ret ^= 0xCCCC; - else if (!players[i].mo || gamestate != GS_LEVEL); - else - { - ret += players[i].mo->x; - ret -= players[i].mo->y; - ret += players[i].itemtype; - ret *= i+1; - } - } - // I give up - // Coop desynching enemies is painful - if (gamestate == GS_LEVEL) - { - for (i = 0; i < PRNUMSYNCED; i++) - { - if (i & 1) - { - ret -= P_GetRandSeed(i); - } - else - { - ret += P_GetRandSeed(i); - } - } - } - -#ifdef MOBJCONSISTANCY - if (gamestate == GS_LEVEL) - { - for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next) - { - if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed) - continue; - - mo = (mobj_t *)th; - - if (TypeIsNetSynced(mo->type) == false) - continue; - - if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_ELEMENTAL | MF_FIRE | MF_ENEMY | MF_PAIN | MF_DONTPUNT)) - { - ret -= mo->type; - ret += mo->x; - ret -= mo->y; - ret += mo->z; - ret -= mo->momx; - ret += mo->momy; - ret -= mo->momz; - ret += mo->angle; - ret -= mo->flags; - ret += mo->flags2; - ret -= mo->eflags; - if (mo->target && TypeIsNetSynced(mo->target->type)) - { - ret += mo->target->type; - ret -= mo->target->x; - ret += mo->target->y; - ret -= mo->target->z; - ret += mo->target->momx; - ret -= mo->target->momy; - ret += mo->target->momz; - ret -= mo->target->angle; - ret += mo->target->flags; - ret -= mo->target->flags2; - ret += mo->target->eflags; - ret -= mo->target->state - states; - ret += mo->target->tics; - ret -= mo->target->sprite; - //ret += mo->target->frame; - } - else - ret ^= 0x3333; - if (mo->tracer && TypeIsNetSynced(mo->tracer->type)) - { - ret += mo->tracer->type; - ret -= mo->tracer->x; - ret += mo->tracer->y; - ret -= mo->tracer->z; - ret += mo->tracer->momx; - ret -= mo->tracer->momy; - ret += mo->tracer->momz; - ret -= mo->tracer->angle; - ret += mo->tracer->flags; - ret -= mo->tracer->flags2; - ret += mo->tracer->eflags; - ret -= mo->tracer->state - states; - ret += mo->tracer->tics; - ret -= mo->tracer->sprite; - //ret += mo->tracer->frame; - } - else - ret ^= 0xAAAA; - // SRB2Kart: We use hnext & hprev very extensively - if (mo->hnext && TypeIsNetSynced(mo->hnext->type)) - { - ret += mo->hnext->type; - ret -= mo->hnext->x; - ret += mo->hnext->y; - ret -= mo->hnext->z; - ret += mo->hnext->momx; - ret -= mo->hnext->momy; - ret += mo->hnext->momz; - ret -= mo->hnext->angle; - ret += mo->hnext->flags; - ret -= mo->hnext->flags2; - ret += mo->hnext->eflags; - ret -= mo->hnext->state - states; - ret += mo->hnext->tics; - ret -= mo->hnext->sprite; - //ret += mo->hnext->frame; - } - else - ret ^= 0x5555; - if (mo->hprev && TypeIsNetSynced(mo->hprev->type)) - { - ret += mo->hprev->type; - ret -= mo->hprev->x; - ret += mo->hprev->y; - ret -= mo->hprev->z; - ret += mo->hprev->momx; - ret -= mo->hprev->momy; - ret += mo->hprev->momz; - ret -= mo->hprev->angle; - ret += mo->hprev->flags; - ret -= mo->hprev->flags2; - ret += mo->hprev->eflags; - ret -= mo->hprev->state - states; - ret += mo->hprev->tics; - ret -= mo->hprev->sprite; - //ret += mo->hprev->frame; - } - else - ret ^= 0xCCCC; - ret -= mo->state - states; - ret += mo->tics; - ret -= mo->sprite; - //ret += mo->frame; - } - } - } -#endif - - DEBFILE(va("Consistancy = %u\n", (ret & 0xFFFF))); - - return (INT16)(ret & 0xFFFF); -} - -// confusing, but this DOESN'T send PT_NODEKEEPALIVE, it sends PT_BASICKEEPALIVE -// used during wipes to tell the server that a node is still connected -static void CL_SendClientKeepAlive(void) -{ - netbuffer->packettype = PT_BASICKEEPALIVE; - - HSendPacket(servernode, false, 0, 0); -} - -static void SV_SendServerKeepAlive(void) -{ - INT32 n; - - for (n = 1; n < MAXNETNODES; n++) - { - if (nodeingame[n]) - { - netbuffer->packettype = PT_BASICKEEPALIVE; - HSendPacket(n, false, 0, 0); - } - } -} - -// send the client packet to the server -static void CL_SendClientCmd(void) -{ - size_t packetsize = 0; - boolean mis = false; - - netbuffer->packettype = PT_CLIENTCMD; - - if (cl_packetmissed) - { - netbuffer->packettype = PT_CLIENTMIS; - mis = true; - } - - netbuffer->u.clientpak.resendfrom = (UINT8)(neededtic & UINT8_MAX); - netbuffer->u.clientpak.client_tic = (UINT8)(gametic & UINT8_MAX); - - if (gamestate == GS_WAITINGPLAYERS) - { - // Send PT_NODEKEEPALIVE packet - netbuffer->packettype = (mis ? PT_NODEKEEPALIVEMIS : PT_NODEKEEPALIVE); - packetsize = sizeof (clientcmd_pak) - sizeof (ticcmd_t) - sizeof (INT16); - HSendPacket(servernode, false, 0, packetsize); - } - else if (gamestate != GS_NULL && (addedtogame || dedicated)) - { - UINT8 lagDelay = 0; - - if (lowest_lag > 0) - { - // Gentlemens' ping. - lagDelay = min(lowest_lag, MAXGENTLEMENDELAY); - - // Is our connection worse than our current gentleman point? - // Make sure it stays that way for a bit before increasing delay levels. - if (lagDelay > reference_lag) - { - spike_time++; - if (spike_time >= GENTLEMANSMOOTHING) - { - // Okay, this is genuinely the new baseline delay. - reference_lag = lagDelay; - spike_time = 0; - } - else - { - // Just a temporary fluctuation, ignore it. - lagDelay = reference_lag; - } - } - else - { - reference_lag = lagDelay; // Adjust quickly if the connection improves. - spike_time = 0; - } - - /* - if (server) // Clients have to wait for the gamestate to make it back. Servers don't! - lagDelay *= 2; // Simulate the HELLFUCK NIGHTMARE of a complete round trip. - */ - - // [deep breath in] - // Plausible, elegant explanation that is WRONG AND SUPER HARMFUL. - // Clients with stable connections were adding their mindelay to network delay, - // even when their mindelay was as high or higher than network delay—which made - // client delay APPEAR slower than host mindelay, by the exact value that made - // "lmao just double it" make sense at the time. - // - // While this fix made client connections match server mindelay in our most common - // test environment, it also masked an issue that seriously affected online handling - // responsiveness, completely ruining our opportunity to further investigate it! - // - // See UpdatePingTable. - // I am taking this shitty code to my grave as an example of "never trust your brain". - // -Tyron 2024-05-15 - - } - - packetsize = sizeof (clientcmd_pak); - G_MoveTiccmd(&netbuffer->u.clientpak.cmd, &localcmds[0][lagDelay], 1); - netbuffer->u.clientpak.consistancy = SHORT(consistancy[gametic % BACKUPTICS]); - - if (splitscreen) // Send a special packet with 2 cmd for splitscreen - { - netbuffer->packettype = (mis ? PT_CLIENT2MIS : PT_CLIENT2CMD); - packetsize = sizeof (client2cmd_pak); - G_MoveTiccmd(&netbuffer->u.client2pak.cmd2, &localcmds[1][lagDelay], 1); - - if (splitscreen > 1) - { - netbuffer->packettype = (mis ? PT_CLIENT3MIS : PT_CLIENT3CMD); - packetsize = sizeof (client3cmd_pak); - G_MoveTiccmd(&netbuffer->u.client3pak.cmd3, &localcmds[2][lagDelay], 1); - - if (splitscreen > 2) - { - netbuffer->packettype = (mis ? PT_CLIENT4MIS : PT_CLIENT4CMD); - packetsize = sizeof (client4cmd_pak); - G_MoveTiccmd(&netbuffer->u.client4pak.cmd4, &localcmds[3][lagDelay], 1); - } - } - } - - HSendPacket(servernode, false, 0, packetsize); - } - - if (cl_mode == CL_CONNECTED || dedicated) - { - UINT8 i; - // Send extra data if needed - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - { - if (((UINT16*)localtextcmd[i])[0]) - { - switch (i) - { - case 3: - netbuffer->packettype = PT_TEXTCMD4; - break; - case 2: - netbuffer->packettype = PT_TEXTCMD3; - break; - case 1: - netbuffer->packettype = PT_TEXTCMD2; - break; - default: - netbuffer->packettype = PT_TEXTCMD; - break; - } - - M_Memcpy(netbuffer->u.textcmd, localtextcmd[i], ((UINT16*)localtextcmd[i])[0]+2); - // All extra data have been sent - if (HSendPacket(servernode, true, 0, ((UINT16*)localtextcmd[i])[0]+2)) // Send can fail... - ((UINT16*)localtextcmd[i])[0] = 0; - } - } - } -} - -// send the server packet -// send tic from firstticstosend to maketic-1 -static void SV_SendTics(void) -{ - tic_t realfirsttic, lasttictosend, i; - UINT32 n; - INT32 j; - size_t packsize; - UINT8 *bufpos; - UINT8 *ntextcmd; - - // send to all client but not to me - // for each node create a packet with x tics and send it - // x is computed using supposedtics[n], max packet size and maketic - for (n = 1; n < MAXNETNODES; n++) - if (nodeingame[n]) - { - // assert supposedtics[n]>=nettics[n] - realfirsttic = supposedtics[n]; - - lasttictosend = nettics[n] + CLIENTBACKUPTICS; - if (lasttictosend > maketic) - lasttictosend = maketic; - - if (realfirsttic >= lasttictosend) - { - // well we have sent all tics we will so use extrabandwidth - // to resent packet that are supposed lost (this is necessary since lost - // packet detection work when we have received packet with firsttic > neededtic - // (getpacket servertics case) - DEBFILE(va("Nothing to send node %u mak=%u sup=%u net=%u \n", - n, lasttictosend, supposedtics[n], nettics[n])); - realfirsttic = nettics[n]; - if (realfirsttic >= lasttictosend || (I_GetTime() + n)&3) - // all tic are ok - continue; - DEBFILE(va("Sent %d anyway\n", realfirsttic)); - } - if (realfirsttic < firstticstosend) - realfirsttic = firstticstosend; - - // compute the length of the packet and cut it if too large - packsize = BASESERVERTICSSIZE; - for (i = realfirsttic; i < lasttictosend; i++) - { - packsize += sizeof (ticcmd_t) * doomcom->numslots; - packsize += TotalTextCmdPerTic(i); - - if (packsize > software_MAXPACKETLENGTH) - { - DEBFILE(va("packet too large (%s) at tic %d (should be from %d to %d)\n", - sizeu1(packsize), i, realfirsttic, lasttictosend)); - lasttictosend = i; - - // too bad: too much player have send extradata and there is too - // much data in one tic. - // To avoid it put the data on the next tic. (see getpacket - // textcmd case) but when numplayer changes the computation can be different - if (lasttictosend == realfirsttic) - { - if (packsize > MAXPACKETLENGTH) - I_Error("Too many players: can't send %s data for %d players to node %d\n" - "Well sorry nobody is perfect....\n", - sizeu1(packsize), doomcom->numslots, n); - else - { - lasttictosend++; // send it anyway! - DEBFILE("sending it anyway\n"); - } - } - break; - } - } - - // Send the tics - netbuffer->packettype = PT_SERVERTICS; - netbuffer->u.serverpak.starttic = (UINT8)realfirsttic; - netbuffer->u.serverpak.numtics = (UINT8)(lasttictosend - realfirsttic); - netbuffer->u.serverpak.numslots = (UINT8)SHORT(doomcom->numslots); - bufpos = (UINT8 *)&netbuffer->u.serverpak.cmds; - - for (i = realfirsttic; i < lasttictosend; i++) - { - bufpos = G_DcpyTiccmd(bufpos, netcmds[i%BACKUPTICS], doomcom->numslots * sizeof (ticcmd_t)); - } - - // add textcmds - for (i = realfirsttic; i < lasttictosend; i++) - { - ntextcmd = bufpos++; - *ntextcmd = 0; - for (j = 0; j < MAXPLAYERS; j++) - { - UINT8 *textcmd = D_GetExistingTextcmd(i, j); - INT32 size = textcmd ? ((UINT16*)textcmd)[0] : 0; - - if ((!j || playeringame[j]) && size) - { - (*ntextcmd)++; - WRITEUINT8(bufpos, j); - WRITEUINT16(bufpos, ((UINT16*)textcmd)[0]); - WRITEMEM(bufpos, &textcmd[2], size); - } - } - } - packsize = bufpos - (UINT8 *)&(netbuffer->u); - - HSendPacket(n, false, 0, packsize); - // when tic are too large, only one tic is sent so don't go backward! - if (lasttictosend-doomcom->extratics > realfirsttic) - supposedtics[n] = lasttictosend-doomcom->extratics; - else - supposedtics[n] = lasttictosend; - if (supposedtics[n] < nettics[n]) supposedtics[n] = nettics[n]; - } - // node 0 is me! - supposedtics[0] = maketic; -} - -// -// TryRunTics -// -static void CreateNewLocalCMD(UINT8 p, INT32 realtics) -{ - INT32 i; - - for (i = MAXGENTLEMENDELAY-1; i > 0; i--) - { - G_MoveTiccmd(&localcmds[p][i], &localcmds[p][i-1], 1); - } - - G_BuildTiccmd(&localcmds[p][0], realtics, p+1); - localcmds[p][0].flags |= TICCMD_RECEIVED; -} - -static void Local_Maketic(INT32 realtics) -{ - INT32 i; - - I_OsPolling(); // I_Getevent - D_ProcessEvents(true); // menu responder, cons responder, - // game responder calls HU_Responder, AM_Responder, - // and G_MapEventsToControls - - if (!dedicated) rendergametic = gametic; - - // translate inputs (keyboard/mouse/joystick) into game controls - for (i = 0; i <= splitscreen; i++) - { - CreateNewLocalCMD(i, realtics); - } -} - -// create missed tic -static void SV_Maketic(void) -{ - INT32 i; - - PS_ResetBotInfo(); - - for (i = 0; i < MAXPLAYERS; i++) - { - packetloss[i][maketic%PACKETMEASUREWINDOW] = false; - - if (!playeringame[i]) - continue; - - if (K_PlayerUsesBotMovement(&players[i])) - { - const precise_t t = I_GetPreciseTime(); - - K_BuildBotTiccmd(&players[i], &netcmds[maketic%BACKUPTICS][i]); - - ps_bots[i].isBot = true; - ps_bots[i].total = I_GetPreciseTime() - t; - ps_botticcmd_time += ps_bots[i].total; - continue; - } - - // We didn't receive this tic - if ((netcmds[maketic % BACKUPTICS][i].flags & TICCMD_RECEIVED) == 0) - { - ticcmd_t * ticcmd = &netcmds[(maketic ) % BACKUPTICS][i]; - ticcmd_t *prevticcmd = &netcmds[(maketic - 1) % BACKUPTICS][i]; - - { - DEBFILE(va("MISS tic%4d for player %d\n", maketic, i)); - // Copy the input from the previous tic - *ticcmd = *prevticcmd; - ticcmd->flags &= ~TICCMD_RECEIVED; - } - - // packetloss[i][leveltime%PACKETMEASUREWINDOW] = (cmd->flags & TICCMD_RECEIVED) ? false : true; - packetloss[i][maketic%PACKETMEASUREWINDOW] = true; - } - } - - // all tic are now proceed make the next - maketic++; -} - -boolean TryRunTics(tic_t realtics) -{ - boolean ticking; - - // the machine has lagged but it is not so bad - if (realtics > TICRATE/7) // FIXME: consistency failure!! - { - if (server) - realtics = 1; - else - realtics = TICRATE/7; - } - - if (singletics) - realtics = 1; - - if (realtics >= 1) - { - COM_BufTicker(); - if (mapchangepending) - D_MapChange(-1, 0, encoremode, false, 2, false, forcespecialstage); // finish the map change - } - - NetUpdate(); - - if (demo.playback) - { - neededtic = gametic + realtics; - // start a game after a demo - maketic += realtics; - firstticstosend = maketic; - tictoclear = firstticstosend; - } - - GetPackets(); - -#ifdef DEBUGFILE - if (debugfile && (realtics || neededtic > gametic)) - { - //SoM: 3/30/2000: Need long INT32 in the format string for args 4 & 5. - //Shut up stupid warning! - fprintf(debugfile, "------------ Tryruntic: REAL:%d NEED:%d GAME:%d LOAD: %d\n", - realtics, neededtic, gametic, debugload); - debugload = 100000; - } -#endif - - ticking = neededtic > gametic; - - if (ticking) - { - if (realtics) - hu_stopped = false; - } - - if (player_joining) - { - if (realtics) - hu_stopped = true; - return false; - } - - if (ticking) - { - boolean tickInterp = true; - - // run the count * tics - while (neededtic > gametic) - { - boolean dontRun = false; - - DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic)); - - ps_prevtictime = ps_tictime; - ps_tictime = I_GetPreciseTime(); - - dontRun = ExtraDataTicker(); - - if (levelloading == false - || gametic > levelstarttic + 5) // Don't lock-up if a malicious client is sending tons of netxcmds - { - // During level load, we want to pause - // execution until we've finished loading - // all of the netxcmds in our buffer. - dontRun = false; - } - - if (dontRun == false) - { - if (levelloading == true) - { - P_PostLoadLevel(); - } - - boolean run = (gametic % NEWTICRATERATIO) == 0; - - if (run && tickInterp) - { - // Update old view state BEFORE ticking so resetting - // the old interpolation state from game logic works. - R_UpdateViewInterpolation(); - tickInterp = false; // do not update again in sped-up tics - } - - G_Ticker(run); - } - - if (Playing() && netgame && (gametic % TICRATE == 0)) - { - Schedule_Run(); - - if (cv_livestudioaudience.value) - { - LiveStudioAudience(); - } - } - - gametic++; - consistancy[gametic % BACKUPTICS] = Consistancy(); - - ps_tictime = I_GetPreciseTime() - ps_tictime; - - // Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame. - if (client && gamestate == GS_LEVEL && leveltime > 1 && neededtic <= gametic + cv_netticbuffer.value) - { - break; - } - } - - if (F_IsDeferredContinueCredits()) - { - F_ContinueCredits(); - } - - if (D_IsDeferredStartTitle()) - { - D_StartTitle(); - } - } - else - { - if (realtics) - hu_stopped = true; - } - - return ticking; -} - - -/* Ping Update except better: - We call this once per second and check for people's pings. If their ping happens to be too high, we increment some timer and kick them out. - If they're not lagging, decrement the timer by 1. Of course, reset all of this if they leave. -*/ - -static INT32 pingtimeout[MAXPLAYERS]; - -static inline void PingUpdate(void) -{ - INT32 i, j; - boolean pingkick[MAXPLAYERS]; - UINT8 nonlaggers = 0; - memset(pingkick, 0, sizeof(pingkick)); - - netbuffer->packettype = PT_PING; - - //check for ping limit breakage. - if (cv_maxping.value) - { - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i] || P_IsMachineLocalPlayer(&players[i])) - { - pingtimeout[i] = 0; - continue; - } - - if ((cv_maxping.value) - && (realpingtable[i] / pingmeasurecount > (unsigned)cv_maxping.value)) - { - if (players[i].jointime > 10 * TICRATE) - { - pingkick[i] = true; - } - } - else - { - nonlaggers++; - - // you aren't lagging, but you aren't free yet. In case you'll keep spiking, we just make the timer go back down. (Very unstable net must still get kicked). - if (pingtimeout[i] > 0) - pingtimeout[i]--; - } - } - - //kick lagging players... unless everyone but the server's ping sucks. - //in that case, it is probably the server's fault. - if (nonlaggers > 0) - { - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i] || !pingkick[i]) - continue; - - // Don't kick on ping alone if we haven't reached our threshold yet. - if (++pingtimeout[i] < cv_pingtimeout.value) - continue; - - pingtimeout[i] = 0; - SendKick(i, KICK_MSG_PING_HIGH); - } - } - } - - //make the ping packet and clear server data for next one - for (i = 0; i < MAXPLAYERS; i++) - { - //CONS_Printf("player %d - total pings: %d\n", i, realpingtable[i]); - - netbuffer->u.netinfo.pingtable[i] = realpingtable[i] / pingmeasurecount; - //server takes a snapshot of the real ping for display. - //otherwise, pings fluctuate a lot and would be odd to look at. - playerpingtable[i] = realpingtable[i] / pingmeasurecount; - realpingtable[i] = 0; //Reset each as we go. - - UINT32 lost = 0; - for (j = 0; j < PACKETMEASUREWINDOW; j++) - { - if (packetloss[i][j]) - lost++; - } - - netbuffer->u.netinfo.packetloss[i] = lost; - netbuffer->u.netinfo.delay[i] = playerdelaytable[i]; - } - - // send the server's maxping as last element of our ping table. This is useful to let us know when we're about to get kicked. - netbuffer->u.netinfo.pingtable[MAXPLAYERS] = cv_maxping.value; - - //send out our ping packets - for (i = 0; i < MAXNETNODES; i++) - if (nodeingame[i]) - HSendPacket(i, true, 0, sizeof(netinfo_pak)); - - pingmeasurecount = 0; //Reset count -} - -static tic_t gametime = 0; - -static void UpdatePingTable(void) -{ - tic_t fastest; - tic_t lag; - - INT32 i; - - if (server) - { - if (Playing() && !(gametime % 8)) // Value chosen based on _my vibes man_ - PingUpdate(); - - fastest = 0; - - // update node latency values so we can take an average later. - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i] && playernode[i] > 0) - { - // TicsToMilliseconds can't handle pings over 1000ms lol - realpingtable[i] += GetLag(playernode[i]); - - if (!players[i].spectator) - { - lag = playerpingtable[i]; - if (! fastest || lag < fastest) - fastest = lag; - } - } - } - - if (server_lagless) - lowest_lag = 0; - else - lowest_lag = fastest; - - // Don't gentleman below your mindelay - if (lowest_lag < (tic_t)cv_mindelay.value) - lowest_lag = (tic_t)cv_mindelay.value; - - pingmeasurecount++; - - switch (playerpernode[0]) - { - case 4: - playerdelaytable[nodetoplayer4[0]] = lowest_lag; - /*FALLTHRU*/ - case 3: - playerdelaytable[nodetoplayer3[0]] = lowest_lag; - /*FALLTHRU*/ - case 2: - playerdelaytable[nodetoplayer2[0]] = lowest_lag; - /*FALLTHRU*/ - case 1: - playerdelaytable[nodetoplayer[0]] = lowest_lag; - } - } - else // We're a client, handle mindelay on the way out. - { - // Previously (neededtic - gametic) - WRONG VALUE! - // Pretty sure that's measuring jitter, not RTT. - // Stable connections would be punished by adding their mindelay to network delay! - tic_t mydelay = playerpingtable[consoleplayer]; - - if (mydelay < (tic_t)cv_mindelay.value) - lowest_lag = cv_mindelay.value - mydelay; - else - lowest_lag = 0; - } -} - -// It's that time again! Send everyone a safe message to sign, so we can show off their signature and prove we're playing fair. -static void SendChallenges(void) -{ - int i; - netbuffer->packettype = PT_CHALLENGEALL; - - #ifdef DEVELOP - if (cv_nochallenge.value) - { - CV_AddValue(&cv_nochallenge, -1); - CONS_Alert(CONS_WARNING, "cv_nochallenge enabled, not sending PT_CHALLENGEALL\n"); - return; - } - #endif - - memset(knownWhenChallenged, 0, sizeof(knownWhenChallenged)); - memset(lastReceivedSignature, 0, sizeof(lastReceivedSignature)); - - GenerateChallenge(netbuffer->u.challengeall.secret); - memcpy(lastChallengeAll, netbuffer->u.challengeall.secret, sizeof(lastChallengeAll)); - - // Take note of everyone's current key, so that players who disconnect and are replaced aren't held to the old player's challenge. - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i]) - memcpy(knownWhenChallenged[i], players[i].public_key, sizeof(knownWhenChallenged[i])); - } - - for (i = 0; i < MAXNETNODES; i++) - { - if (nodeingame[i]) - HSendPacket(i, true, 0, sizeof(challengeall_pak)); - } -} - -// Before we start sending out the results, we need to kick everyone who didn't respond. -// (If we try to do both at once, clients will still see players who failled in-game when the results arrive...) -static void KickUnverifiedPlayers(void) -{ - int i; - uint8_t allZero[SIGNATURELENGTH]; - memset(allZero, 0, SIGNATURELENGTH); - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i]) - continue; - if (memcmp(lastReceivedSignature[i], allZero, SIGNATURELENGTH) == 0) // We never got a response! - { - if (!IsPlayerGuest(i) && memcmp(&knownWhenChallenged[i], &players[i].public_key, sizeof(knownWhenChallenged[i])) == 0) - { - if (playernode[i] != servernode) - SendKick(i, KICK_MSG_SIGFAIL); - } - } - } -} - -// -static void SendChallengeResults(void) -{ - int i; - netbuffer->packettype = PT_RESULTSALL; - - #ifdef DEVELOP - if (cv_noresults.value) - { - CV_AddValue(&cv_noresults, -1); - CONS_Alert(CONS_WARNING, "cv_noresults enabled, not sending PT_RESULTSALL\n"); - return; - } - #endif - - uint8_t allZero[SIGNATURELENGTH]; - memset(allZero, 0, sizeof(allZero)); - - memset(&netbuffer->u.resultsall, 0, sizeof(netbuffer->u.resultsall)); - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i]) - continue; - - // Don't try to transmit signatures for players who didn't get here in time to send one. - // (Everyone who had their chance should have been kicked by KickUnverifiedPlayers by now.) - if (memcmp(lastReceivedSignature[i], allZero, SIGNATURELENGTH) == 0) - continue; - - memcpy(netbuffer->u.resultsall.signature[i], lastReceivedSignature[i], sizeof(netbuffer->u.resultsall.signature[i])); - #ifdef DEVELOP - if (cv_badresults.value) - { - CV_AddValue(&cv_badresults, -1); - CONS_Alert(CONS_WARNING, "cv_badresults enabled, scrubbing signature from PT_RESULTSALL\n"); - memset(netbuffer->u.resultsall.signature[i], 0, sizeof(netbuffer->u.resultsall.signature[i])); - } - #endif - } - - for (i = 0; i < MAXNETNODES; i++) - { - if (nodeingame[i]) - HSendPacket(i, true, 0, sizeof(resultsall_pak)); - } -} - -// Who should we try to verify when results come in? -// Store a public key for every active slot, so if players shuffle during challenge leniency, -// we don't incorrectly try to verify someone who didn't even get a challenge, throw a tantrum, and bail. -static void CheckPresentPlayers(void) -{ - int i; - memset(knownWhenChallenged, 0, sizeof(knownWhenChallenged)); - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i]) - { - continue; - } - else if (IsPlayerGuest(i)) - { - continue; - } - else - { - memcpy(knownWhenChallenged[i], players[i].public_key, sizeof(knownWhenChallenged[i])); - } - } -} - -// Handle "client-to-client" auth challenge flow. -void UpdateChallenges(void) -{ - if (!(Playing() && netgame)) - return; - - if (server) - { - if (leveltime == CHALLENGEALL_START) - SendChallenges(); - - if (leveltime == CHALLENGEALL_KICKUNRESPONSIVE) - KickUnverifiedPlayers(); - - if (leveltime == CHALLENGEALL_SENDRESULTS) - SendChallengeResults(); - - } - else // client - { - if (leveltime < CHALLENGEALL_START) - expectChallenge = true; - - if (leveltime == CHALLENGEALL_START) - CheckPresentPlayers(); - - if (leveltime > CHALLENGEALL_CLIENTCUTOFF && expectChallenge) - HandleSigfail("Didn't receive client signatures."); - } -} - -static void RenewHolePunch(void) -{ - static time_t past; - - const time_t now = time(NULL); - - if ((now - past) > 20) - { - I_NetRegisterHolePunch(); - past = now; - } -} - -// Handle timeouts to prevent definitive freezes from happenning -static void HandleNodeTimeouts(void) -{ - INT32 i; - - if (server) - { - for (i = 1; i < MAXNETNODES; i++) - if ((nodeingame[i] || nodeneedsauth[i]) && freezetimeout[i] < I_GetTime()) - Net_ConnectionTimeout(i); - - // In case the cvar value was lowered - if (joindelay) - joindelay = min(joindelay - 1, 3 * (tic_t)cv_joindelay.value * TICRATE); - } -} - -// Keep the network alive while not advancing tics! -void NetKeepAlive(void) -{ - tic_t nowtime; - INT32 realtics; - - nowtime = I_GetTime(); - realtics = nowtime - gametime; - - // return if there's no time passed since the last call - if (realtics <= 0) // nothing new to update - return; - - UpdatePingTable(); - - GetPackets(); - -#ifdef MASTERSERVER - MasterClient_Ticker(); -#endif - - if (netgame && serverrunning) - { - RenewHolePunch(); - } - - if (client) - { - // send keep alive - CL_SendClientKeepAlive(); - // No need to check for resynch because we aren't running any tics - } - else - { - SV_SendServerKeepAlive(); - } - - // No else because no tics are being run and we can't resynch during this - - Net_AckTicker(); - HandleNodeTimeouts(); - FileSendTicker(); -} - -// If a tree falls in the forest but nobody is around to hear it, does it make a tic? -#define DEDICATEDIDLETIME (10*TICRATE) - -void NetUpdate(void) -{ - static tic_t resptime = 0; - tic_t nowtime; - INT32 i; - INT32 realtics; - - nowtime = I_GetTime(); - realtics = nowtime - gametime; - - if (!demo.playback && g_fast_forward > 0) - { - realtics = 1; - } - else - { - if (realtics <= 0) // nothing new to update - return; - - if (realtics > 5) - { - if (server) - realtics = 1; - else - realtics = 5; - } - } - -#ifdef DEDICATEDIDLETIME - if (server && dedicated && gamestate == GS_LEVEL) - { - static tic_t dedicatedidle = 0; - - for (i = 1; i < MAXNETNODES; ++i) - if (nodeingame[i]) - { - if (dedicatedidle == DEDICATEDIDLETIME) - { - CONS_Printf("DEDICATED: Awakening from idle (Node %d detected...)\n", i); - dedicatedidle = 0; - } - break; - } - - if (i == MAXNETNODES) - { - if (leveltime == 2) - { - // On next tick... - dedicatedidle = DEDICATEDIDLETIME-1; - } - else if (dedicatedidle == DEDICATEDIDLETIME) - { - if (D_GetExistingTextcmd(gametic, 0) || D_GetExistingTextcmd(gametic+1, 0)) - { - CONS_Printf("DEDICATED: Awakening from idle (Netxcmd detected...)\n"); - dedicatedidle = 0; - } - else - { - realtics = 0; - } - } - else if ((dedicatedidle += realtics) >= DEDICATEDIDLETIME) - { - const char *idlereason = "at round start"; - if (leveltime > 3) - idlereason = va("for %d seconds", dedicatedidle/TICRATE); - - CONS_Printf("DEDICATED: No nodes %s, idling...\n", idlereason); - realtics = 0; - dedicatedidle = DEDICATEDIDLETIME; - } - } - } -#endif - - gametime = nowtime; - - UpdatePingTable(); - - if (client) - maketic = neededtic; - - Local_Maketic(realtics); // make local tic, and call menu? - - if (server) - CL_SendClientCmd(); // send it - - GetPackets(); // get packet from client or from server - - // client send the command after a receive of the server - // the server send before because in single player is beter - -#ifdef MASTERSERVER - MasterClient_Ticker(); // Acking the Master Server -#endif - - if (netgame && serverrunning) - { - RenewHolePunch(); - } - - if (client) - { - // If the client just finished redownloading the game state, load it - if (cl_redownloadinggamestate && fileneeded[0].status == FS_FOUND) - CL_ReloadReceivedSavegame(); - - CL_SendClientCmd(); // Send tic cmd - hu_redownloadinggamestate = cl_redownloadinggamestate; - } - else - { - if (!demo.playback && realtics > 0) - { - INT32 counts; - - hu_redownloadinggamestate = false; - - // Don't erase tics not acknowledged - counts = realtics; - - firstticstosend = gametic; - for (i = 0; i < MAXNETNODES; i++) - { - if (!nodeingame[i]) - continue; - if (nettics[i] < firstticstosend) - firstticstosend = nettics[i]; - if (maketic + counts >= nettics[i] + (BACKUPTICS - TICRATE)) - Net_ConnectionTimeout(i); - } - - if (maketic + counts >= firstticstosend + BACKUPTICS) - counts = firstticstosend+BACKUPTICS-maketic-1; - - for (i = 0; i < counts; i++) - SV_Maketic(); // Create missed tics and increment maketic - - for (; tictoclear < firstticstosend; tictoclear++) // Clear only when acknowledged - D_Clearticcmd(tictoclear); // Clear the maketic the new tic - - SV_SendTics(); - - neededtic = maketic; // The server is a client too - } - } - - if (server) - { - for(i = 0; i < MAXPLAYERS; i++) - { - if (stop_spamming[i] > 0) - stop_spamming[i]--; - } - } - - Net_AckTicker(); - HandleNodeTimeouts(); - - nowtime /= NEWTICRATERATIO; - - if (nowtime > resptime) - { - resptime = nowtime; -#ifdef HAVE_THREADS - I_lock_mutex(&k_menu_mutex); -#endif - M_Ticker(); - refreshdirmenu = 0; -#ifdef HAVE_THREADS - I_unlock_mutex(k_menu_mutex); -#endif - CON_Ticker(); - - M_ScreenshotTicker(); - } - - FileSendTicker(); -} - -/** Returns the number of players playing. - * \return Number of players. Can be zero if we're running a ::dedicated - * server. - * \author Graue - */ -INT32 D_NumPlayers(void) -{ - INT32 num = 0, ix; - - for (ix = 0; ix < MAXPLAYERS; ix++) - { - if (playeringame[ix] && !players[ix].bot) - { - num++; - } - } - - return num; -} - -/** Returns the number of players racing, not spectating and includes bots - * \return Number of players. Can be zero if we're running a ::dedicated - * server. - */ -INT32 D_NumPlayersInRace(void) -{ - INT32 numPlayers = 0; - INT32 i; - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i] && !players[i].spectator) - numPlayers++; - } - return numPlayers; -} - -/** Return whether a player is a real person (not a CPU) and not spectating. - */ -boolean D_IsPlayerHumanAndGaming (INT32 player_number) -{ - player_t * player = &players[player_number]; - return ( - playeringame[player_number] && - ! player->spectator && - ! player->bot - ); -} - -tic_t GetLag(INT32 node) -{ - // If the client has caught up to the server -- say, during a wipe -- lag is meaningless. - if (nettics[node] > gametic) - return 0; - return gametic - nettics[node]; -} - -#define REWIND_POINT_INTERVAL 4*TICRATE + 16 -rewind_t *rewindhead; - -void CL_ClearRewinds(void) -{ - rewind_t *head; - while ((head = rewindhead)) - { - rewindhead = rewindhead->next; - free(head); - } -} - -rewind_t *CL_SaveRewindPoint(size_t demopos) -{ - savebuffer_t save = {0}; - rewind_t *rewind; - - if (rewindhead && rewindhead->leveltime + REWIND_POINT_INTERVAL > leveltime) - return NULL; - - rewind = (rewind_t *)malloc(sizeof (rewind_t)); - if (!rewind) - return NULL; - - P_SaveBufferFromExisting(&save, rewind->savebuffer, NETSAVEGAMESIZE); - P_SaveNetGame(&save, false); - - rewind->leveltime = leveltime; - rewind->next = rewindhead; - rewind->demopos = demopos; - rewindhead = rewind; - - return rewind; -} - -rewind_t *CL_RewindToTime(tic_t time) -{ - savebuffer_t save = {0}; - rewind_t *rewind; - - while (rewindhead && rewindhead->leveltime > time) - { - rewind = rewindhead->next; - free(rewindhead); - rewindhead = rewind; - } - - if (!rewindhead) - return NULL; - - P_SaveBufferFromExisting(&save, rewindhead->savebuffer, NETSAVEGAMESIZE); - P_LoadNetGame(&save, false); - - wipegamestate = gamestate; // No fading back in! - timeinmap = leveltime; - - return rewindhead; -} - -void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest) -{ -#ifdef NOMD5 - (void)buffer; - (void)len; - (void)salt; - memset(dest, 0, 16); -#else - char tmpbuf[256]; - const size_t sl = strlen(salt); - - if (len > 256-sl) - len = 256-sl; - - memcpy(tmpbuf, buffer, len); - memmove(&tmpbuf[len], salt, sl); - //strcpy(&tmpbuf[len], salt); - len += strlen(salt); - if (len < 256) - memset(&tmpbuf[len],0,256-len); - - // Yes, we intentionally md5 the ENTIRE buffer regardless of size... - md5_buffer(tmpbuf, 256, dest); -#endif -} - -// Want to say something? XD_SAY is server only, gotta request that they send one on our behalf -void DoSayPacket(SINT8 target, UINT8 flags, UINT8 source, char *message) -{ - say_pak *packet = (void*)&netbuffer->u.say; - netbuffer->packettype = PT_SAY; - - memset(packet->message, 0, sizeof(packet->message)); - strcpy(packet->message, message); - - packet->source = source; - packet->flags = flags; - packet->target = target; - - HSendPacket(servernode, false, 0, sizeof(say_pak)); -} - -void DoSayPacketFromCommand(SINT8 target, size_t usedargs, UINT8 flags) -{ - char buf[2 + HU_MAXMSGLEN + 1]; - size_t numwords, ix; - char *msg = &buf[3]; - const size_t msgspace = sizeof buf - 2; - - numwords = COM_Argc() - usedargs; - I_Assert(numwords > 0); - - msg[0] = '\0'; - - for (ix = 0; ix < numwords; ix++) - { - if (ix > 0) - strlcat(msg, " ", msgspace); - strlcat(msg, COM_Argv(ix + usedargs), msgspace); - } - - DoSayPacket(target, flags, consoleplayer, msg); -} - -// This is meant to be targeted at player indices, not whatever the hell XD_SAY is doing with 1-indexed players. -void SendServerNotice(SINT8 target, char *message) -{ - if (client) - return; - DoSayCommand(message, target + 1, HU_PRIVNOTICE, servernode); -} +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by Kart Krew. +// Copyright (C) 2020 by Sonic Team Junior. +// Copyright (C) 2000 by DooM Legacy Team. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file d_clisrv.c +/// \brief SRB2 Network game communication and protocol, all OS independent parts. + +#include +#ifdef __GNUC__ +#include //for unlink +#endif + +#include + +#include "i_time.h" +#include "i_net.h" +#include "i_system.h" +#include "i_video.h" +#include "d_net.h" +#include "d_netfil.h" // fileneedednum +#include "d_main.h" +#include "g_game.h" +#include "st_stuff.h" +#include "hu_stuff.h" +#include "keys.h" +#include "g_input.h" // JOY1 +#include "k_menu.h" +#include "console.h" +#include "d_netfil.h" +#include "byteptr.h" +#include "p_saveg.h" +#include "z_zone.h" +#include "p_local.h" +#include "m_misc.h" +#include "am_map.h" +#include "m_random.h" +#include "mserv.h" +#include "y_inter.h" +#include "r_local.h" +#include "m_argv.h" +#include "p_setup.h" +#include "lzf.h" +#include "lua_script.h" +#include "lua_hook.h" +#include "md5.h" +#include "m_perfstats.h" +#include "monocypher/monocypher.h" +#include "stun.h" + +// SRB2Kart +#include "k_credits.h" +#include "k_kart.h" +#include "k_battle.h" +#include "k_pwrlv.h" +#include "k_bot.h" +#include "k_grandprix.h" +#include "doomstat.h" +#include "s_sound.h" // sfx_syfail +#include "m_cond.h" // netUnlocked +#include "g_party.h" +#include "k_vote.h" +#include "k_serverstats.h" +#include "k_zvote.h" +#include "music.h" +#include "k_bans.h" +#include "sanitize.h" +#include "r_fps.h" +#include "filesrch.h" // refreshdirmenu +#include "k_objects.h" + +// cl loading screen +#include "v_video.h" +#include "f_finale.h" +#include "k_hud.h" + +#ifdef HAVE_DISCORDRPC +#include "discord.h" +#endif + +// +// NETWORKING +// +// gametic is the tic about to (or currently being) run +// Server: +// maketic is the tic that hasn't had control made for it yet +// nettics is the tic for each node +// firstticstosend is the lowest value of nettics +// Client: +// neededtic is the tic needed by the client to run the game +// firstticstosend is used to optimize a condition +// Normally maketic >= gametic > 0 + +#define MAX_REASONLENGTH 30 +#define FORCECLOSE 0x8000 + +boolean server = true; // true or false but !server == client +#define client (!server) +boolean nodownload = false; +boolean serverrunning = false; +INT32 serverplayer = 0; +char motd[254], server_context[8]; // Message of the Day, Unique Context (even without Mumble support) + +UINT8 playerconsole[MAXPLAYERS]; + +// Server specific vars +UINT8 playernode[MAXPLAYERS]; + +// Minimum timeout for sending the savegame +// The actual timeout will be longer depending on the savegame length +tic_t jointimeout = (3*TICRATE); +static boolean sendingsavegame[MAXNETNODES]; // Are we sending the savegame? +static boolean resendingsavegame[MAXNETNODES]; // Are we resending the savegame? +static tic_t savegameresendcooldown[MAXNETNODES]; // How long before we can resend again? +static tic_t freezetimeout[MAXNETNODES]; // Until when can this node freeze the server before getting a timeout? + +// Incremented by cv_joindelay when a client joins, decremented each tic. +// If higher than cv_joindelay * 2 (3 joins in a short timespan), joins are temporarily disabled. +static tic_t joindelay = 0; + +UINT16 pingmeasurecount = 1; +UINT32 realpingtable[MAXPLAYERS]; //the base table of ping where an average will be sent to everyone. +UINT32 playerpingtable[MAXPLAYERS]; //table of player latency values. +UINT32 playerpacketlosstable[MAXPLAYERS]; +UINT32 playerdelaytable[MAXPLAYERS]; // mindelay values. + +#define GENTLEMANSMOOTHING (TICRATE) +static tic_t reference_lag; +static UINT8 spike_time; +static tic_t lowest_lag; +boolean server_lagless; + +SINT8 nodetoplayer[MAXNETNODES]; +SINT8 nodetoplayer2[MAXNETNODES]; // say the numplayer for this node if any (splitscreen) +SINT8 nodetoplayer3[MAXNETNODES]; // say the numplayer for this node if any (splitscreen == 2) +SINT8 nodetoplayer4[MAXNETNODES]; // say the numplayer for this node if any (splitscreen == 3) +UINT8 playerpernode[MAXNETNODES]; // used specialy for splitscreen +boolean nodeingame[MAXNETNODES]; // set false as nodes leave game +boolean nodeneedsauth[MAXNETNODES]; + +tic_t servermaxping = 20; // server's max delay, in frames. Defaults to 20 +static tic_t nettics[MAXNETNODES]; // what tic the client have received +static tic_t supposedtics[MAXNETNODES]; // nettics prevision for smaller packet +static UINT8 nodewaiting[MAXNETNODES]; +static tic_t firstticstosend; // min of the nettics +static tic_t tictoclear = 0; // optimize d_clearticcmd +static tic_t maketic; + +static INT16 consistancy[BACKUPTICS]; + +static UINT8 player_joining = false; +UINT8 hu_redownloadinggamestate = 0; + +// kart, true when a player is connecting or disconnecting so that the gameplay has stopped in its tracks +boolean hu_stopped = false; + +UINT8 adminpassmd5[16]; +boolean adminpasswordset = false; + +// Client specific +static ticcmd_t localcmds[MAXSPLITSCREENPLAYERS][MAXGENTLEMENDELAY]; +static boolean cl_packetmissed; +// here it is for the secondary local player (splitscreen) +static UINT8 mynode; // my address pointofview server +static boolean cl_redownloadinggamestate = false; + +static UINT8 localtextcmd[MAXSPLITSCREENPLAYERS][MAXTEXTCMD]; +static tic_t neededtic; +SINT8 servernode = 0; // the number of the server node +char connectedservername[MAXSERVERNAME]; +char connectedservercontact[MAXSERVERCONTACT]; +/// \brief do we accept new players? +/// \todo WORK! +boolean acceptnewnode = true; + +UINT32 ourIP; // Used when populating PT_SERVERCHALLENGE (guards against signature reuse) +uint8_t lastReceivedKey[MAXNETNODES][MAXSPLITSCREENPLAYERS][PUBKEYLENGTH]; // Player's public key (join process only! active players have it on player_t) +uint8_t lastSentChallenge[MAXNETNODES][CHALLENGELENGTH]; // The random message we asked them to sign in PT_SERVERCHALLENGE, check it in PT_CLIENTJOIN +uint8_t awaitingChallenge[CHALLENGELENGTH]; // The message the server asked our client to sign when joining +uint8_t lastChallengeAll[CHALLENGELENGTH]; // The message we asked EVERYONE to sign for client-to-client identity proofs +uint8_t lastReceivedSignature[MAXPLAYERS][SIGNATURELENGTH]; // Everyone's response to lastChallengeAll +uint8_t knownWhenChallenged[MAXPLAYERS][PUBKEYLENGTH]; // Everyone a client saw at the moment a challenge should be initiated +boolean expectChallenge = false; // Were we in-game before a client-to-client challenge should have been sent? + +uint8_t priorKeys[MAXPLAYERS][PUBKEYLENGTH]; // Make a note of keys before consuming a new gamestate, and if the server tries to send us a gamestate where keys differ, assume shenanigans + +boolean serverisfull = false; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not +tic_t firstconnectattempttime = 0; + +static OpusDecoder *g_player_opus_decoders[MAXPLAYERS]; +static UINT64 g_player_opus_lastframe[MAXPLAYERS]; +static OpusEncoder *g_local_opus_encoder; +static UINT64 g_local_opus_frame = 0; +#define SRB2_VOICE_OPUS_FRAME_SIZE 480 +static float g_local_voice_buffer[SRB2_VOICE_OPUS_FRAME_SIZE]; +static INT32 g_local_voice_buffer_len = 0; +static INT32 g_local_voice_threshold_time = 0; +float g_local_voice_last_peak = 0; +boolean g_local_voice_detected = false; + +// engine + +// Must be a power of two +#define TEXTCMD_HASH_SIZE 4 + +typedef struct textcmdplayer_s +{ + INT32 playernum; + UINT8 cmd[MAXTEXTCMD]; + struct textcmdplayer_s *next; +} textcmdplayer_t; + +typedef struct textcmdtic_s +{ + tic_t tic; + textcmdplayer_t *playercmds[TEXTCMD_HASH_SIZE]; + struct textcmdtic_s *next; +} textcmdtic_t; + +ticcmd_t netcmds[BACKUPTICS][MAXPLAYERS]; +static textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE] = {NULL}; + + +static tic_t stop_spamming[MAXPLAYERS]; + +// Generate a message for an authenticating client to sign, with some guarantees about who we are. +void GenerateChallenge(uint8_t *buf) +{ + #ifndef SRB2_LITTLE_ENDIAN + #error "FIXME: 64-bit timestamp field is not supported on Big Endian" + #endif + + UINT64 now = time(NULL); + csprng(buf, CHALLENGELENGTH); // Random noise as a baseline, but... + memcpy(buf, &now, sizeof(now)); // Timestamp limits the reuse window. + memcpy(buf + sizeof(now), &ourIP, sizeof(ourIP)); // IP prevents captured signatures from being used elsewhere. + + #ifdef DEVELOP + if (cv_badtime.value) + { + CV_AddValue(&cv_badtime, -1); + CONS_Alert(CONS_WARNING, "cv_badtime enabled, trashing time in auth message\n"); + memset(buf, 0, sizeof(now)); + } + + if (cv_badip.value) + { + CV_AddValue(&cv_badip, -1); + CONS_Alert(CONS_WARNING, "cv_badip enabled, trashing IP in auth message\n"); + memset(buf + sizeof(now), 0, sizeof(ourIP)); + } + #endif +} + +// Modified servers can throw softballs or reuse challenges. +// Don't sign anything that wasn't generated just for us! +shouldsign_t ShouldSignChallenge(uint8_t *message) +{ + #ifndef SRB2_LITTLE_ENDIAN + #error "FIXME: 64-bit timestamp field is not supported on Big Endian" + #endif + + UINT64 then, now; + UINT32 claimedIP, realIP; + + now = time(NULL); + memcpy(&then, message, sizeof(then)); + memcpy(&claimedIP, message + sizeof(then), sizeof(claimedIP)); + realIP = I_GetNodeAddressInt(servernode); + + if ((max(now, then) - min(now, then)) > 60*15) + return SIGN_BADTIME; + + if (realIP != claimedIP && I_IsExternalAddress(&realIP)) + return SIGN_BADIP; + + return SIGN_OK; +} + +static inline void *G_DcpyTiccmd(void* dest, const ticcmd_t* src, const size_t n) +{ + const size_t d = n / sizeof(ticcmd_t); + const size_t r = n % sizeof(ticcmd_t); + UINT8 *ret = dest; + + if (r) + M_Memcpy(dest, src, n); + else if (d) + G_MoveTiccmd(dest, src, d); + return ret+n; +} + +static inline void *G_ScpyTiccmd(ticcmd_t* dest, void* src, const size_t n) +{ + const size_t d = n / sizeof(ticcmd_t); + const size_t r = n % sizeof(ticcmd_t); + UINT8 *ret = src; + + if (r) + M_Memcpy(dest, src, n); + else if (d) + G_MoveTiccmd(dest, src, d); + return ret+n; +} + + + +// Some software don't support largest packet +// (original sersetup, not exactely, but the probability of sending a packet +// of 512 bytes is like 0.1) +UINT16 software_MAXPACKETLENGTH; + +/** Guesses the full value of a tic from its lowest byte, for a specific node + * + * \param low The lowest byte of the tic value + * \param basetic The last full tic value to compare against + * \return The full tic value + * + */ +tic_t ExpandTics(INT32 low, tic_t basetic) +{ + INT32 delta; + + delta = low - (basetic & UINT8_MAX); + + if (delta >= -64 && delta <= 64) + return (basetic & ~UINT8_MAX) + low; + else if (delta > 64) + return (basetic & ~UINT8_MAX) - 256 + low; + else //if (delta < -64) + return (basetic & ~UINT8_MAX) + 256 + low; +} + +// ----------------------------------------------------------------- +// Some extra data function for handle textcmd buffer +// ----------------------------------------------------------------- + +static void (*listnetxcmd[MAXNETXCMD])(const UINT8 **p, INT32 playernum); + +void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(const UINT8 **p, INT32 playernum)) +{ +#ifdef PARANOIA + if (id >= MAXNETXCMD) + I_Error("Command id %d too big", id); + if (listnetxcmd[id] != 0) + I_Error("Command id %d already used", id); +#endif + listnetxcmd[id] = cmd_f; +} + +void SendNetXCmdForPlayer(UINT8 playerid, netxcmd_t id, const void *param, size_t nparam) +{ + if (((UINT16*)localtextcmd[playerid])[0]+3+nparam > MAXTEXTCMD) + { + // for future reference: if (cht_debug) != debug disabled. + CONS_Alert(CONS_ERROR, M_GetText("NetXCmd buffer full, cannot add netcmd %d! (size: %d, needed: %s)\n"), id, ((UINT16*)localtextcmd[playerid])[0], sizeu1(nparam)); + return; + } + + ((UINT16*)localtextcmd[playerid])[0]++; + localtextcmd[playerid][((UINT16*)localtextcmd[playerid])[0] + 1] = (UINT8)id; + + if (param && nparam) + { + M_Memcpy(&localtextcmd[playerid][((UINT16*)localtextcmd[playerid])[0] + 2], param, nparam); + ((UINT16*)localtextcmd[playerid])[0] = ((UINT16*)localtextcmd[playerid])[0] + (UINT8)nparam; + } +} + +UINT8 GetFreeXCmdSize(UINT8 playerid) +{ + // -2 for the size and another -1 for the ID. + return (UINT8)(localtextcmd[playerid][0] - 3); +} + +// Frees all textcmd memory for the specified tic +static void D_FreeTextcmd(tic_t tic) +{ + textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)]; + textcmdtic_t *textcmdtic = *tctprev; + + while (textcmdtic && textcmdtic->tic != tic) + { + tctprev = &textcmdtic->next; + textcmdtic = textcmdtic->next; + } + + if (textcmdtic) + { + INT32 i; + + // Remove this tic from the list. + *tctprev = textcmdtic->next; + + // Free all players. + for (i = 0; i < TEXTCMD_HASH_SIZE; i++) + { + textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[i]; + + while (textcmdplayer) + { + textcmdplayer_t *tcpnext = textcmdplayer->next; + Z_Free(textcmdplayer); + textcmdplayer = tcpnext; + } + } + + // Free this tic's own memory. + Z_Free(textcmdtic); + } +} + +// Gets the buffer for the specified ticcmd, or NULL if there isn't one +static UINT8* D_GetExistingTextcmd(tic_t tic, INT32 playernum) +{ + textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)]; + while (textcmdtic && textcmdtic->tic != tic) textcmdtic = textcmdtic->next; + + // Do we have an entry for the tic? If so, look for player. + if (textcmdtic) + { + textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)]; + while (textcmdplayer && textcmdplayer->playernum != playernum) textcmdplayer = textcmdplayer->next; + + if (textcmdplayer) return textcmdplayer->cmd; + } + + return NULL; +} + +// Gets the buffer for the specified ticcmd, creating one if necessary +static UINT8* D_GetTextcmd(tic_t tic, INT32 playernum) +{ + textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)]; + textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)]; + textcmdplayer_t *textcmdplayer, **tcpprev; + + // Look for the tic. + while (textcmdtic && textcmdtic->tic != tic) + { + tctprev = &textcmdtic->next; + textcmdtic = textcmdtic->next; + } + + // If we don't have an entry for the tic, make it. + if (!textcmdtic) + { + textcmdtic = *tctprev = Z_Calloc(sizeof (textcmdtic_t), PU_STATIC, NULL); + textcmdtic->tic = tic; + } + + tcpprev = &textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)]; + textcmdplayer = *tcpprev; + + // Look for the player. + while (textcmdplayer && textcmdplayer->playernum != playernum) + { + tcpprev = &textcmdplayer->next; + textcmdplayer = textcmdplayer->next; + } + + // If we don't have an entry for the player, make it. + if (!textcmdplayer) + { + textcmdplayer = *tcpprev = Z_Calloc(sizeof (textcmdplayer_t), PU_STATIC, NULL); + textcmdplayer->playernum = playernum; + } + + return textcmdplayer->cmd; +} + +static boolean ExtraDataTicker(void) +{ + boolean anyNetCmd = false; + INT32 i; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] || i == 0) + { + const UINT8 *bufferstart = D_GetExistingTextcmd(gametic, i); + + if (bufferstart) + { + const UINT8 *curpos = bufferstart; + const UINT8 *bufferend = &curpos[((const UINT16*)curpos)[0]+2]; + + curpos += 2; + while (curpos < bufferend) + { + if (*curpos < MAXNETXCMD && listnetxcmd[*curpos]) + { + const UINT8 id = *curpos; + curpos++; + DEBFILE(va("executing x_cmd %s ply %u ", netxcmdnames[id - 1], i)); + (listnetxcmd[id])(&curpos, i); + DEBFILE("done\n"); + anyNetCmd = true; + } + else + { + if (server) + { + SendKick(i, KICK_MSG_CON_FAIL); + DEBFILE(va("player %d kicked [gametic=%u] reason as follows:\n", i, gametic)); + } + CONS_Alert(CONS_WARNING, M_GetText("Got unknown net command [%s]=%d (max %d)\n"), sizeu1(curpos - bufferstart), *curpos, bufferstart[0]); + break; + } + } + } + } + } + + // If you are a client, you can safely forget the net commands for this tic + // If you are the server, you need to remember them until every client has been acknowledged, + // because if you need to resend a PT_SERVERTICS packet, you will need to put the commands in it + if (client) + { + D_FreeTextcmd(gametic); + } + + return anyNetCmd; +} + +static void D_Clearticcmd(tic_t tic) +{ + INT32 i; + + D_FreeTextcmd(tic); + + for (i = 0; i < MAXPLAYERS; i++) + netcmds[tic%BACKUPTICS][i].flags = 0; + + DEBFILE(va("clear tic %5u (%2u)\n", tic, tic%BACKUPTICS)); +} + +void D_ResetTiccmds(void) +{ + INT32 i, j; + + for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) + { + for (j = 0; j < MAXGENTLEMENDELAY; j++) + { + memset(&localcmds[i][j], 0, sizeof(ticcmd_t)); + } + } + + // Reset the net command list + for (i = 0; i < TEXTCMD_HASH_SIZE; i++) + while (textcmds[i]) + D_Clearticcmd(textcmds[i]->tic); +} + +void D_ResetTiccmdAngle(UINT8 ss, angle_t angle) +{ + INT32 i; + + for (i = 0; i < MAXGENTLEMENDELAY; ++i) + { + localcmds[ss][i].angle = angle >> TICCMD_REDUCE; + } +} + +ticcmd_t *D_LocalTiccmd(UINT8 ss) +{ + return &localcmds[ss][0]; +} + +void SendKick(UINT8 playernum, UINT8 msg) +{ + UINT8 buf[2]; + + buf[0] = playernum; + buf[1] = msg; + SendNetXCmd(XD_KICK, &buf, 2); +} + +// ----------------------------------------------------------------- +// end of extra data function +// ----------------------------------------------------------------- + +// ----------------------------------------------------------------- +// extra data function for lmps +// ----------------------------------------------------------------- + +// if extradatabit is set, after the ziped tic you find this: +// +// type | description +// ---------+-------------- +// byte | size of the extradata +// byte | the extradata (xd) bits: see XD_... +// with this byte you know what parameter folow +// if (xd & XDNAMEANDCOLOR) +// byte | color +// char[MAXPLAYERNAME] | name of the player +// endif +// if (xd & XD_WEAPON_PREF) +// byte | original weapon switch: boolean, true if use the old +// | weapon switch methode +// char[NUMWEAPONS] | the weapon switch priority +// byte | autoaim: true if use the old autoaim system +// endif +/*boolean AddLmpExtradata(UINT8 **demo_point, INT32 playernum) +{ + UINT8 *textcmd = D_GetExistingTextcmd(gametic, playernum); + + if (!textcmd) + return false; + + M_Memcpy(*demo_point, textcmd, textcmd[0]+1); + *demo_point += textcmd[0]+1; + return true; +} + +void ReadLmpExtraData(UINT8 **demo_pointer, INT32 playernum) +{ + UINT8 nextra; + UINT8 *textcmd; + + if (!demo_pointer) + return; + + textcmd = D_GetTextcmd(gametic, playernum); + nextra = **demo_pointer; + M_Memcpy(textcmd, *demo_pointer, nextra + 1); + // increment demo pointer + *demo_pointer += nextra + 1; +}*/ + +// ----------------------------------------------------------------- +// end extra data function for lmps +// ----------------------------------------------------------------- + +static INT16 Consistancy(void); + +typedef enum +{ + CL_SEARCHING, + CL_CHECKFILES, + CL_DOWNLOADFILES, + CL_DOWNLOADFAILED, + CL_ASKJOIN, + CL_LOADFILES, + CL_SETUPFILES, + CL_WAITJOINRESPONSE, + CL_DOWNLOADSAVEGAME, + CL_CONNECTED, + CL_ABORTED, + CL_ASKFULLFILELIST, + CL_CONFIRMCONNECT, +#ifdef HAVE_CURL + CL_PREPAREHTTPFILES, + CL_DOWNLOADHTTPFILES, +#endif + CL_SENDKEY, + CL_WAITCHALLENGE, +} cl_mode_t; + +static void GetPackets(void); + +static cl_mode_t cl_mode = CL_SEARCHING; + +#ifdef HAVE_CURL +char http_source[MAX_MIRROR_LENGTH]; +#endif + +static UINT16 cl_lastcheckedfilecount = 0; // used for full file list + +// +// CL_DrawConnectionStatus +// +// Keep the local client informed of our status. +// +static inline void CL_DrawConnectionStatus(void) +{ + INT32 ccstime = I_GetTime(); + + // Draw background fade + if (!menuactive) // menu already draws its own fade + V_DrawFadeScreen(0xFF00, 16); // force default + + if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES && cl_mode != CL_CHECKFILES +#ifdef HAVE_CURL + && cl_mode != CL_DOWNLOADHTTPFILES +#endif + ) + { + INT32 i, animtime = ((ccstime / 4) & 15) + 16; + UINT8 palstart = (cl_mode == CL_SEARCHING) ? 32 : 96; + // 15 pal entries total. + const char *cltext; + + // Draw bottom box + M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); + K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, 0, "Press or to abort", 1, 2, V_YELLOWMAP); + + for (i = 0; i < 16; ++i) + V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-24, 16, 8, palstart + ((animtime - i) & 15)); + + switch (cl_mode) + { + case CL_DOWNLOADSAVEGAME: + if (lastfilenum != -1) + { + UINT32 currentsize = fileneeded[lastfilenum].currentsize; + UINT32 totalsize = fileneeded[lastfilenum].totalsize; + INT32 dldlength; + + cltext = M_GetText("Downloading game state..."); + Net_GetNetStat(); + + dldlength = (INT32)((currentsize/(double)totalsize) * 256); + if (dldlength > 256) + dldlength = 256; + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111); + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96); + + V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE, + va(" %4uK/%4uK",currentsize>>10,totalsize>>10)); + + V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE, + va("%3.1fK/s ", ((double)getbps)/1024)); + } + else + cltext = M_GetText("Waiting to download game state..."); + break; + case CL_ASKFULLFILELIST: + case CL_CONFIRMCONNECT: + case CL_DOWNLOADFAILED: + cltext = ""; + break; + case CL_SETUPFILES: + cltext = M_GetText("Configuring addons..."); + break; + case CL_ASKJOIN: + case CL_WAITJOINRESPONSE: + if (serverisfull) + cltext = M_GetText("Server full, waiting for a slot..."); + else + cltext = M_GetText("Requesting to join..."); + + break; +#ifdef HAVE_CURL + case CL_PREPAREHTTPFILES: + cltext = M_GetText("Waiting to download files..."); + break; +#endif + default: + cltext = M_GetText("Attempting to connect..."); + if (I_GetTime() - firstconnectattempttime > 15*TICRATE) + { + V_DrawCenteredString(BASEVIDWIDTH/2, 16, V_YELLOWMAP, "This is taking much longer than usual."); + V_DrawCenteredString(BASEVIDWIDTH/2, 16+8, V_YELLOWMAP, "Are you sure you've got the right IP?"); + V_DrawCenteredString(BASEVIDWIDTH/2, 16+16, V_YELLOWMAP, "The host may need to forward port 5029 UDP."); + } + break; + } + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, cltext); + } + else + { + if (cl_mode == CL_CHECKFILES) + { + INT32 totalfileslength; + INT32 checkednum = 0; + INT32 i; + + K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, 0, "Press or to abort", 1, 2, V_YELLOWMAP); + + //ima just count files here + for (i = 0; i < fileneedednum; i++) + if (fileneeded[i].status != FS_NOTCHECKED) + checkednum++; + + // Loading progress + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP, "Checking server addons..."); + totalfileslength = (INT32)((checkednum/(double)(fileneedednum)) * 256); + M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, 256, 8, 111); + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, totalfileslength, 8, 96); + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, + va(" %2u/%2u Files",checkednum,fileneedednum)); + } + else if (cl_mode == CL_LOADFILES) + { + INT32 totalfileslength; + INT32 loadcompletednum = 0; + INT32 i; + + K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, 0, "Press or to abort", 1, 2, V_YELLOWMAP); + + //ima just count files here + for (i = 0; i < fileneedednum; i++) + if (fileneeded[i].status == FS_OPEN) + loadcompletednum++; + + // Loading progress + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP, "Loading server addons..."); + totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256); + M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, 256, 8, 111); + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, totalfileslength, 8, 96); + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, + va(" %2u/%2u Files",loadcompletednum,fileneedednum)); + } + else if (lastfilenum != -1) + { + INT32 dldlength; + INT32 totalfileslength; + UINT32 totaldldsize; + static char tempname[28]; + fileneeded_t *file = &fileneeded[lastfilenum]; + char *filename = file->filename; + + // Draw the bottom box. + M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-58-8, 32, 1); + K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-58-14, 0, "Press or to abort", 1, 2, V_YELLOWMAP); + + Net_GetNetStat(); + dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256); + if (dldlength > 256) + dldlength = 256; + + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-58, 256, 8, 111); + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-58, dldlength, 8, 96); + + memset(tempname, 0, sizeof(tempname)); + // offset filename to just the name only part + filename += strlen(filename) - nameonlylength(filename); + + if (strlen(filename) > sizeof(tempname)-1) // too long to display fully + { + size_t endhalfpos = strlen(filename)-10; + // display as first 14 chars + ... + last 10 chars + // which should add up to 27 if our math(s) is correct + snprintf(tempname, sizeof(tempname), "%.14s...%.10s", filename, filename+endhalfpos); + } + else // we can copy the whole thing in safely + { + strncpy(tempname, filename, sizeof(tempname)-1); + } + + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-58-30, 0, + va(M_GetText("%s downloading"), ((cl_mode == CL_DOWNLOADHTTPFILES) ? "\x82""HTTP" : "\x85""Direct"))); + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-58-22, V_YELLOWMAP, + va(M_GetText("\"%s\""), tempname)); + V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-58, V_20TRANS|V_MONOSPACE, + va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10)); + V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-58, V_20TRANS|V_MONOSPACE, + va("%3.1fK/s ", ((double)getbps)/1024)); + + // Download progress + + if (fileneeded[lastfilenum].currentsize != fileneeded[lastfilenum].totalsize) + totaldldsize = downloadcompletedsize+fileneeded[lastfilenum].currentsize; //Add in single file progress download if applicable + else + totaldldsize = downloadcompletedsize; + + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-14, V_YELLOWMAP, "Overall Download Progress"); + totalfileslength = (INT32)((totaldldsize/(double)totalfilesrequestedsize) * 256); + M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, 256, 8, 111); + V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, totalfileslength, 8, 96); + + if (totalfilesrequestedsize>>20 >= 10) //display in MB if over 10MB + V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, + va(" %4uM/%4uM",totaldldsize>>20,totalfilesrequestedsize>>20)); + else + V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, + va(" %4uK/%4uK",totaldldsize>>10,totalfilesrequestedsize>>10)); + + V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE, + va("%2u/%2u Files ",downloadcompletednum,totalfilesrequestednum)); + } + else + { + INT32 i, animtime = ((ccstime / 4) & 15) + 16; + UINT8 palstart = (cl_mode == CL_SEARCHING) ? 128 : 160; + // 15 pal entries total. + + //Draw bottom box + M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1); + K_DrawGameControl(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, 0, "Press or to abort", 1, 2, V_YELLOWMAP); + + for (i = 0; i < 16; ++i) + V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-24, 16, 8, palstart + ((animtime - i) & 15)); + + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP, + M_GetText("Waiting to download files...")); + } + } +} + +static boolean CL_AskFileList(INT32 firstfile) +{ + netbuffer->packettype = PT_TELLFILESNEEDED; + netbuffer->u.filesneedednum = firstfile; + + return HSendPacket(servernode, false, 0, sizeof (INT32)); +} + +/** Sends a special packet to declare how many players in local + * Used only in arbitratrenetstart() + * Sends a PT_CLIENTJOIN packet to the server + * + * \return True if the packet was successfully sent + * \todo Improve the description... + * Because to be honest, I have no idea what arbitratrenetstart is... + * Is it even used...? + * + */ +static boolean CL_SendJoin(void) +{ + UINT8 localplayers = 1; + UINT8 i; + + if (netgame) + CONS_Printf(M_GetText("Sending join request...\n")); + netbuffer->packettype = PT_CLIENTJOIN; + + if (splitscreen) + localplayers += splitscreen; + + netbuffer->u.clientcfg.localplayers = localplayers; + netbuffer->u.clientcfg._255 = 255; + netbuffer->u.clientcfg.packetversion = PACKETVERSION; + netbuffer->u.clientcfg.version = VERSION; + netbuffer->u.clientcfg.subversion = SUBVERSION; + strncpy(netbuffer->u.clientcfg.application, SRB2APPLICATION, + sizeof netbuffer->u.clientcfg.application); + + for (i = 0; i <= splitscreen; i++) + { + // the MAXPLAYERS addition is necessary to communicate that g_localplayers is not yet safe to reference + CleanupPlayerName(MAXPLAYERS+i, cv_playername[i].zstring); + strncpy(netbuffer->u.clientcfg.names[i], cv_playername[i].zstring, MAXPLAYERNAME); + } + // privacy shield for the local players not joining this session + for (; i < MAXSPLITSCREENPLAYERS; i++) + strncpy(netbuffer->u.clientcfg.names[i], va("Player %c", 'A' + i), MAXPLAYERNAME); + + memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false, -1), MAXAVAILABILITY*sizeof(UINT8)); + + // Don't leak old signatures from prior sessions. + memset(&netbuffer->u.clientcfg.challengeResponse, 0, sizeof(((clientconfig_pak *)0)->challengeResponse)); + + if (client && netgame) + { + shouldsign_t safe = ShouldSignChallenge(awaitingChallenge); + + if (safe != SIGN_OK) + { + if (safe == SIGN_BADIP) + { + I_Error("External server IP didn't match the message it sent."); + } + else if (safe == SIGN_BADTIME) + { + I_Error("External server sent a message with an unusual timestamp.\nMake sure your system time is set correctly."); + } + else + { + I_Error("External server asked for a signature on something strange.\nPlease notify a developer if you've seen this more than once."); + } + return false; + } + } + + for (i = 0; i <= splitscreen; i++) + { + uint8_t signature[SIGNATURELENGTH]; + profile_t *localProfile = PR_GetLocalPlayerProfile(i); + + if (PR_IsLocalPlayerGuest(i)) // GUESTS don't have keys + { + memset(signature, 0, sizeof(signature)); + } + else + { + // If our keys are garbage (corrupted profile?), fail here instead of when the server boots us, so the player knows what's going on. + crypto_eddsa_sign(signature, localProfile->secret_key, awaitingChallenge, sizeof(awaitingChallenge)); + if (crypto_eddsa_check(signature, localProfile->public_key, awaitingChallenge, sizeof(awaitingChallenge)) != 0) + I_Error("Couldn't self-verify key associated with player %d, profile %d.\nProfile data may be corrupted.", i, cv_lastprofile[i].value); // I guess this is the most reasonable way to catch a malformed key. + } + + #ifdef DEVELOP + if (cv_badjoin.value) + { + CV_AddValue(&cv_badjoin, -1); + CONS_Alert(CONS_WARNING, "cv_badjoin enabled, scrubbing signature from CL_SendJoin\n"); + memset(signature, 0, sizeof(signature)); + } + #endif + + // Testing + // memset(signature, 0, sizeof(signature)); + + memcpy(&netbuffer->u.clientcfg.challengeResponse[i], signature, sizeof(signature)); + } + + return HSendPacket(servernode, false, 0, sizeof (clientconfig_pak)); +} + +static boolean CL_SendKey(void) +{ + int i; + netbuffer->packettype = PT_CLIENTKEY; + + memset(netbuffer->u.clientkey.key, 0, sizeof(((clientkey_pak *)0)->key)); + for (i = 0; i <= splitscreen; i++) + { + // GUEST profiles have all-zero keys. This will be handled at the end of the challenge process, don't worry about it. + memcpy(netbuffer->u.clientkey.key[i], PR_GetProfile(cv_lastprofile[i].value)->public_key, PUBKEYLENGTH); + } + return HSendPacket(servernode, false, 0, sizeof (clientkey_pak) ); +} + +static void SV_SendServerInfo(INT32 node, tic_t servertime) +{ + UINT8 *p; + size_t mirror_length; + const char *httpurl = cv_httpsource.string; + + netbuffer->packettype = PT_SERVERINFO; + netbuffer->u.serverinfo._255 = 255; + netbuffer->u.serverinfo.packetversion = PACKETVERSION; + + netbuffer->u.serverinfo.version = VERSION; + netbuffer->u.serverinfo.subversion = SUBVERSION; + + memcpy(netbuffer->u.serverinfo.commit, + comprevision_abbrev_bin, GIT_SHA_ABBREV); + + strncpy(netbuffer->u.serverinfo.application, SRB2APPLICATION, + sizeof netbuffer->u.serverinfo.application); + // return back the time value so client can compute their ping + netbuffer->u.serverinfo.time = (tic_t)LONG(servertime); + netbuffer->u.serverinfo.leveltime = (tic_t)LONG(leveltime); + + netbuffer->u.serverinfo.numberofplayer = (UINT8)D_NumPlayers(); + netbuffer->u.serverinfo.maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value)); + + if (!node) + netbuffer->u.serverinfo.refusereason = 0; + else if (!cv_allownewplayer.value) + netbuffer->u.serverinfo.refusereason = 1; + else if (D_NumPlayers() >= cv_maxconnections.value) + netbuffer->u.serverinfo.refusereason = 2; + else + netbuffer->u.serverinfo.refusereason = 0; + + strncpy(netbuffer->u.serverinfo.gametypename, gametypes[gametype]->name, + sizeof netbuffer->u.serverinfo.gametypename); + netbuffer->u.serverinfo.modifiedgame = (UINT8)modifiedgame; + netbuffer->u.serverinfo.cheatsenabled = CV_CheatsEnabled(); + + netbuffer->u.serverinfo.kartvars = (UINT8) ( + (gamespeed & SV_SPEEDMASK) | + (dedicated ? SV_DEDICATED : 0) | + (!cv_voice_servermute.value ? SV_VOICEENABLED : 0) + ); + + D_ParseCarets(netbuffer->u.serverinfo.servername, cv_servername.string, MAXSERVERNAME); + + M_Memcpy(netbuffer->u.serverinfo.mapmd5, mapmd5, 16); + + if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) && !(mapheaderinfo[gamemap-1]->zonttl[0])) + netbuffer->u.serverinfo.iszone = 1; + else + netbuffer->u.serverinfo.iszone = 0; + + memset(netbuffer->u.serverinfo.maptitle, 0, sizeof netbuffer->u.serverinfo.maptitle); + + if (!(mapheaderinfo[gamemap-1]->menuflags & LF2_HIDEINMENU) && mapheaderinfo[gamemap-1]->lvlttl[0]) + { + //strncpy(netbuffer->u.serverinfo.maptitle, (char *)mapheaderinfo[gamemap-1]->lvlttl, sizeof netbuffer->u.serverinfo.maptitle); + // set up the levelstring + if (netbuffer->u.serverinfo.iszone || (mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE)) + { + if (snprintf(netbuffer->u.serverinfo.maptitle, + sizeof netbuffer->u.serverinfo.maptitle, + "%s", + mapheaderinfo[gamemap-1]->lvlttl) < 0) + { + // If there's an encoding error, send "Unknown", we accept that the above may be truncated + strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", sizeof netbuffer->u.serverinfo.maptitle); + } + } + else + { + if (snprintf(netbuffer->u.serverinfo.maptitle, + sizeof netbuffer->u.serverinfo.maptitle, + "%s %s", + mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->zonttl) < 0) + { + // If there's an encoding error, send "Unknown", we accept that the above may be truncated + strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", sizeof netbuffer->u.serverinfo.maptitle); + } + } + } + else + strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", sizeof netbuffer->u.serverinfo.maptitle); + + netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum; + + memset(netbuffer->u.serverinfo.httpsource, 0, MAX_MIRROR_LENGTH); + + mirror_length = strlen(httpurl); + if (mirror_length > MAX_MIRROR_LENGTH) + mirror_length = MAX_MIRROR_LENGTH; + + if (snprintf(netbuffer->u.serverinfo.httpsource, mirror_length+1, "%s", httpurl) < 0) + // If there's an encoding error, send nothing, we accept that the above may be truncated + strncpy(netbuffer->u.serverinfo.httpsource, "", mirror_length); + + netbuffer->u.serverinfo.httpsource[MAX_MIRROR_LENGTH-1] = '\0'; + + if (K_UsingPowerLevels() != PWRLV_DISABLED) + netbuffer->u.serverinfo.avgpwrlv = K_CalculatePowerLevelAvg(); + else + netbuffer->u.serverinfo.avgpwrlv = -1; + + p = PutFileNeeded(0); + + HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u)); +} + +static void SV_SendPlayerInfo(INT32 node) +{ + UINT8 i; + netbuffer->packettype = PT_PLAYERINFO; + + for (i = 0; i < MSCOMPAT_MAXPLAYERS; i++) + { + if (i >= MAXPLAYERS) + { + netbuffer->u.playerinfo[i].num = 255; // Master Server compat + continue; + } + + if (!playeringame[i]) + { + netbuffer->u.playerinfo[i].num = 255; // This slot is empty. + continue; + } + + netbuffer->u.playerinfo[i].num = i; + strncpy(netbuffer->u.playerinfo[i].name, (const char *)&player_names[i], MAXPLAYERNAME+1); + netbuffer->u.playerinfo[i].name[MAXPLAYERNAME] = '\0'; + + //fetch IP address + //No, don't do that, you fuckface. + memset(netbuffer->u.playerinfo[i].address, 0, 4); + + if (players[i].spectator) + { + netbuffer->u.playerinfo[i].team = 255; + } + else + { + if (G_GametypeHasTeams()) + { + if (players[i].team == TEAM_UNASSIGNED) + { + netbuffer->u.playerinfo[i].team = 255; + } + else + { + netbuffer->u.playerinfo[i].team = players[i].team; + } + } + else + { + netbuffer->u.playerinfo[i].team = 0; + } + } + + netbuffer->u.playerinfo[i].score = LONG(players[i].score); + netbuffer->u.playerinfo[i].timeinserver = SHORT((UINT16)(players[i].jointime / TICRATE)); + netbuffer->u.playerinfo[i].skin = (UINT8)(players[i].skin); + + // Extra data + netbuffer->u.playerinfo[i].data = 0; //players[i].skincolor; + } + + HSendPacket(node, false, 0, sizeof(plrinfo) * MSCOMPAT_MAXPLAYERS); +} + +/** Sends a PT_SERVERCFG packet + * + * \param node The destination + * \return True if the packet was successfully sent + * + */ +static boolean SV_SendServerConfig(INT32 node) +{ + boolean waspacketsent; + + memset(&netbuffer->u.servercfg, 0, sizeof netbuffer->u.servercfg); + + netbuffer->packettype = PT_SERVERCFG; + + netbuffer->u.servercfg.version = VERSION; + netbuffer->u.servercfg.subversion = SUBVERSION; + + netbuffer->u.servercfg.serverplayer = (UINT8)serverplayer; + netbuffer->u.servercfg.totalslotnum = (UINT8)(doomcom->numslots); + netbuffer->u.servercfg.gametic = (tic_t)LONG(gametic); + netbuffer->u.servercfg.clientnode = (UINT8)node; + netbuffer->u.servercfg.gamestate = (UINT8)gamestate; + netbuffer->u.servercfg.gametype = (UINT8)gametype; + netbuffer->u.servercfg.modifiedgame = (UINT8)modifiedgame; + + netbuffer->u.servercfg.maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value)); + netbuffer->u.servercfg.allownewplayer = cv_allownewplayer.value; + netbuffer->u.servercfg.discordinvites = (boolean)cv_discordinvites.value; + + memcpy(netbuffer->u.servercfg.server_context, server_context, 8); + + D_ParseCarets(netbuffer->u.servercfg.server_name, cv_servername.string, MAXSERVERNAME); + D_ParseCarets(netbuffer->u.servercfg.server_contact, cv_server_contact.string, MAXSERVERCONTACT); + + { + const size_t len = sizeof (serverconfig_pak); + +#ifdef DEBUGFILE + if (debugfile) + { + fprintf(debugfile, "ServerConfig Packet about to be sent, size of packet:%s to node:%d\n", + sizeu1(len), node); + } +#endif + + waspacketsent = HSendPacket(node, true, 0, len); + } + +#ifdef DEBUGFILE + if (debugfile) + { + if (waspacketsent) + { + fprintf(debugfile, "ServerConfig Packet was sent\n"); + } + else + { + fprintf(debugfile, "ServerConfig Packet could not be sent right now\n"); + } + } +#endif + + return waspacketsent; +} + +static boolean SV_ResendingSavegameToAnyone(void) +{ + INT32 i; + + for (i = 0; i < MAXNETNODES; i++) + if (resendingsavegame[i]) + return true; + return false; +} + +static void SV_SendSaveGame(INT32 node, boolean resending) +{ + size_t length, compressedlen; + savebuffer_t save = {0}; + UINT8 *compressedsave; + UINT8 *buffertosend; + + // first save it in a malloced buffer + if (P_SaveBufferAlloc(&save, NETSAVEGAMESIZE) == false) + { + CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n")); + return; + } + + // Leave room for the uncompressed length. + save.p += sizeof(UINT32); + + P_SaveNetGame(&save, resending); + + length = save.p - save.buffer; + if (length > NETSAVEGAMESIZE) + { + P_SaveBufferFree(&save); + I_Error("Savegame buffer overrun"); + } + + // Allocate space for compressed save: one byte fewer than for the + // uncompressed data to ensure that the compression is worthwhile. + compressedsave = Z_Malloc(length - 1, PU_STATIC, NULL); + if (!compressedsave) + { + CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n")); + return; + } + + // Attempt to compress it. + if ((compressedlen = lzf_compress(save.buffer + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1))) + { + // Compressing succeeded; send compressed data + P_SaveBufferFree(&save); + + // State that we're compressed. + buffertosend = compressedsave; + WRITEUINT32(compressedsave, length - sizeof(UINT32)); + length = compressedlen + sizeof(UINT32); + } + else + { + // Compression failed to make it smaller; send original + Z_Free(compressedsave); + + // State that we're not compressed + buffertosend = save.buffer; + WRITEUINT32(save.buffer, 0); + } + + AddRamToSendQueue(node, buffertosend, length, SF_Z_RAM, 0); + + // Remember when we started sending the savegame so we can handle timeouts + sendingsavegame[node] = true; + freezetimeout[node] = I_GetTime() + jointimeout + length / 1024; // 1 extra tic for each kilobyte +} + +static void CL_DumpConsistency(const char *file_name) +{ + size_t length; + savebuffer_t save = {0}; + char tmpsave[1024]; + + snprintf(tmpsave, sizeof(tmpsave), "%s" PATHSEP "%s", srb2home, file_name); + + // first save it in a malloced buffer + if (P_SaveBufferAlloc(&save, NETSAVEGAMESIZE) == false) + { + CONS_Alert(CONS_ERROR, M_GetText("No more free memory for consistency dump\n")); + return; + } + + P_SaveNetGame(&save, false); + + length = save.p - save.buffer; + if (length > NETSAVEGAMESIZE) + { + P_SaveBufferFree(&save); + I_Error("Consistency dump buffer overrun"); + } + + // then save it! + if (!FIL_WriteFile(tmpsave, save.buffer, length)) + CONS_Printf(M_GetText("Didn't save %s for consistency dump"), tmpsave); + + P_SaveBufferFree(&save); +} + +#define TMPSAVENAME "$$$.sav" + +static void CL_LoadReceivedSavegame(boolean reloading) +{ + savebuffer_t save = {0}; + size_t length, decompressedlen; + char tmpsave[256]; + + sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home); + + if (P_SaveBufferFromFile(&save, tmpsave) == false) + { + I_Error("Can't read savegame sent"); + return; + } + + length = save.size; + CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length)); + + // Decompress saved game if necessary. + decompressedlen = READUINT32(save.p); + if (decompressedlen > 0) + { + UINT8 *decompressedbuffer = Z_Malloc(decompressedlen, PU_STATIC, NULL); + + lzf_decompress(save.p, length - sizeof(UINT32), decompressedbuffer, decompressedlen); + + P_SaveBufferFree(&save); + P_SaveBufferFromExisting(&save, decompressedbuffer, decompressedlen); + } + + paused = false; + demo.playback = false; + demo.attract = DEMO_ATTRACT_OFF; + titlemapinaction = false; + tutorialchallenge = TUTORIALSKIP_NONE; + automapactive = false; + + // load a base level + if (P_LoadNetGame(&save, reloading)) + { + if (!reloading) + { + CON_LogMessage(va(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap))); + + if (strlen(mapheaderinfo[gamemap-1]->lvlttl) > 0) + { + CON_LogMessage(va(": %s", mapheaderinfo[gamemap-1]->lvlttl)); + if (strlen(mapheaderinfo[gamemap-1]->zonttl) > 0) + CON_LogMessage(va(" %s", mapheaderinfo[gamemap-1]->zonttl)); + else if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE)) + CON_LogMessage(M_GetText(" Zone")); + if (mapheaderinfo[gamemap-1]->actnum > 0) + CON_LogMessage(va(" %d", mapheaderinfo[gamemap-1]->actnum)); + } + + CON_LogMessage("\"\n"); + } + } + + // done + P_SaveBufferFree(&save); + + if (unlink(tmpsave) == -1) + { + CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave); + } + + consistancy[gametic%BACKUPTICS] = Consistancy(); + CON_ToggleOff(); + + // Tell the server we have received and reloaded the gamestate + // so they know they can resume the game + netbuffer->packettype = PT_RECEIVEDGAMESTATE; + HSendPacket(servernode, true, 0, 0); + + if (reloading) + { + int i; + for (i = 0; i < MAXPLAYERS; i++) + { + if (memcmp(priorKeys[i], players[i].public_key, sizeof(priorKeys[i])) != 0) + { + HandleSigfail("Gamestate reload contained new keys"); + break; + } + } + } +} + +static void CL_ReloadReceivedSavegame(void) +{ + extern consvar_t cv_dumpconsistency; + if (cv_dumpconsistency.value) + { + CL_DumpConsistency("TEMP.consdump"); + } + + INT32 i; + for (i = 0; i < MAXPLAYERS; i++) + { + LUA_InvalidatePlayer(&players[i]); + sprintf(player_names[i], "Player %c", 'A' + i); + } + + CL_LoadReceivedSavegame(true); + + if (neededtic < gametic) + neededtic = gametic; + maketic = neededtic; + + for (i = 0; i <= r_splitscreen; i++) + { + P_ForceLocalAngle(&players[displayplayers[i]], players[displayplayers[i]].angleturn); + } + + for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) + { + camera[i].subsector = R_PointInSubsector(camera[i].x, camera[i].y); + } + + cl_redownloadinggamestate = false; + + CONS_Printf(M_GetText("Game state reloaded\n")); + + if (cv_dumpconsistency.value) + { + // This is dumb, but we want the file names + // to be pairable together with the server's + // version, and gametic being randomly off + // is a deal breaker. + char dump_name[1024]; + snprintf( + dump_name, sizeof(dump_name), + "%s_%u_%s-client.consdump", + server_context, + gametic, + player_names[consoleplayer] + ); + if (FIL_RenameFile("TEMP.consdump", dump_name) == false) + { + CONS_Alert(CONS_WARNING, "Failed to rename temporary consdump file.\n"); + } + } +} + +static void SendAskInfo(INT32 node) +{ + tic_t asktime; + + if (node != 0 && node != BROADCASTADDR && + cv_rendezvousserver.string[0]) + { + I_NetRequestHolePunch(node); + } + + asktime = I_GetTime(); + + netbuffer->packettype = PT_ASKINFO; + netbuffer->u.askinfo.version = VERSION; + netbuffer->u.askinfo.time = (tic_t)LONG(asktime); + + // Even if this never arrives due to the host being firewalled, we've + // now allowed traffic from the host to us in, so once the MS relays + // our address to the host, it'll be able to speak to us. + HSendPacket(node, false, 0, sizeof (askinfo_pak)); +} + +serverelem_t serverlist[MAXSERVERLIST]; +UINT32 serverlistcount = 0; +UINT32 serverlistultimatecount = 0; + +static boolean resendserverlistnode[MAXNETNODES]; +static tic_t serverlistepoch; + +static void SL_ClearServerList(INT32 connectedserver) +{ + UINT32 i; + + for (i = 0; i < serverlistcount; i++) + if (connectedserver != serverlist[i].node) + { + Net_CloseConnection(serverlist[i].node|FORCECLOSE); + serverlist[i].node = 0; + } + serverlistcount = 0; + + memset(resendserverlistnode, 0, sizeof resendserverlistnode); +} + +static UINT32 SL_SearchServer(INT32 node) +{ + UINT32 i; + for (i = 0; i < serverlistcount; i++) + if (serverlist[i].node == node) + return i; + + return UINT32_MAX; +} + +static boolean SL_InsertServer(serverinfo_pak* info, SINT8 node) +{ + UINT32 i; + + resendserverlistnode[node] = false; + + // search if not already on it + i = SL_SearchServer(node); + if (i == UINT32_MAX) + { + // not found, check for packet format rejections + + if (serverlistcount >= MAXSERVERLIST) + return false; // list full + + if (info->_255 != 255) + return false; // old packet format + + if (info->packetversion != PACKETVERSION) + return false; // old new packet format + + if (info->version != VERSION) + return false; // Not same version. + + if (info->subversion != SUBVERSION) + return false; // Close, but no cigar. + + if (strcmp(info->application, SRB2APPLICATION)) + return false; // that's a different mod + } + + const INT32 gtidentifier = G_GetGametypeByName(info->gametypename); + UINT8 gtcalc = GTCALC_RACE; + if (gtidentifier != GT_RACE) + { + gtcalc = (gtidentifier == GT_BATTLE) ? GTCALC_BATTLE : GTCALC_CUSTOM; + } + + if (i == UINT32_MAX) + { + // Still not added to list... check for modifiedgame rejections + if (serverlistultimatecount) + { + // We're on the server browser page. We can reject based on our room. + if ( + ( + info->modifiedgame != false // self-declared + || (gtcalc == GTCALC_CUSTOM) // not a main two gametype + ) != (mpmenu.room == 1) + ) + { + return false; // CORE vs MODDED! + } + } + + // Ok, FINALLY now we can confirm + i = serverlistcount++; + } + + serverlist[i].info = *info; + serverlist[i].node = node; + serverlist[i].cachedgtcalc = gtcalc; + + // resort server list + M_SortServerList(); + + return true; +} + +void CL_QueryServerList (msg_server_t *server_list) +{ + INT32 i; + + CL_UpdateServerList(); + + serverlistepoch = I_GetTime(); + + for (i = 0; server_list[i].header.buffer[0]; i++) + { + // Make sure MS version matches our own, to + // thwart nefarious servers who lie to the MS. + + /* lol bruh, that version COMES from the servers */ + //if (strcmp(version, server_list[i].version) == 0) + { + INT32 node = I_NetMakeNodewPort(server_list[i].ip, server_list[i].port); + if (node == -1) + break; // no more node free + SendAskInfo(node); + + resendserverlistnode[node] = true; + // Leave this node open. It'll be closed if the + // request times out (CL_TimeoutServerList). + } + } + + serverlistultimatecount = i; +} + +#define SERVERLISTRESENDRATE NEWTICRATE + +void CL_TimeoutServerList(void) +{ + if (netgame && serverlistultimatecount > serverlistcount) + { + const tic_t timediff = I_GetTime() - serverlistepoch; + const tic_t timetoresend = timediff % SERVERLISTRESENDRATE; + const boolean timedout = timediff > connectiontimeout; + + if (timedout || (timediff > 0 && timetoresend == 0)) + { + INT32 node; + + for (node = 1; node < MAXNETNODES; ++node) + { + if (resendserverlistnode[node]) + { + if (timedout) + Net_CloseConnection(node|FORCECLOSE); + else + SendAskInfo(node); + } + } + + if (timedout) + serverlistultimatecount = serverlistcount; + } + } +} + +void CL_UpdateServerList (void) +{ + SL_ClearServerList(0); + + if (!netgame && I_NetOpenSocket) + { + if (I_NetOpenSocket()) + { + netgame = true; + multiplayer = true; + } + } + + // search for local servers + if (netgame) + SendAskInfo(BROADCASTADDR); +} + +static void M_ConfirmConnect(INT32 choice) +{ + if (choice == MA_YES) + { + if (totalfilesrequestednum > 0) + { + #ifdef HAVE_CURL + if (http_source[0] == '\0' || curl_failedwebdownload) + #endif + { + if (CL_SendFileRequest()) + { + cl_mode = CL_DOWNLOADFILES; + } + else + { + cl_mode = CL_DOWNLOADFAILED; + } + } + #ifdef HAVE_CURL + else + cl_mode = CL_PREPAREHTTPFILES; + #endif + } + else + cl_mode = CL_LOADFILES; + + return; + } + + cl_mode = CL_ABORTED; +} + +static boolean CL_FinishedFileList(void) +{ + INT32 i; + char *downloadsize = NULL; + //CONS_Printf(M_GetText("Checking files...\n")); + i = CL_CheckFiles(); + if (i == 4) // still checking ... + { + return true; + } + else if (i == 3) // too many files + { + Command_ExitGame_f(); + M_StartMessage("Server Connection Failure", + M_GetText( + "You have too many WAD files loaded\n" + "to add ones the server is using.\n" + "Please restart Ring Racers before connecting.\n" + ), NULL, MM_NOTHING, NULL, "Back to Menu"); + return false; + } + else if (i == 2) // cannot join for some reason + { + Command_ExitGame_f(); + M_StartMessage("Server Connection Failure", + M_GetText( + "You have the wrong addons loaded.\n\n" + "To play on this server, restart\n" + "the game and don't load any addons.\n" + "Ring Racers will automatically add\n" + "everything you need when you join.\n" + ), NULL, MM_NOTHING, NULL, "Back to Menu"); + return false; + } + else if (i == 1) + { + if (serverisfull) + { + M_StartMessage("Server Connection Failure", + M_GetText( + "This server is full!\n" + "\n" + "You may load server addons (if any), and wait for a slot.\n" + ), &M_ConfirmConnect, MM_YESNO, "Continue", "Back to Menu"); + cl_mode = CL_CONFIRMCONNECT; + } + else + cl_mode = CL_LOADFILES; + } + else + { + // must download something + // can we, though? +#ifdef HAVE_CURL + if (http_source[0] == '\0' || curl_failedwebdownload) +#endif + { + if (!CL_CheckDownloadable()) // nope! + { + Command_ExitGame_f(); + M_StartMessage("Server Connection Failure", + M_GetText( + "An error occured when trying to\n" + "download missing addons.\n" + "(This is almost always a problem\n" + "with the server, not your game.)\n\n" + "See the console or log file\n" + "for additional details.\n" + ), NULL, MM_NOTHING, NULL, "Back to Menu"); + return false; + } + } + +#ifdef HAVE_CURL + if (!curl_failedwebdownload) +#endif + { + downloadcompletednum = 0; + downloadcompletedsize = 0; + totalfilesrequestednum = 0; + totalfilesrequestedsize = 0; + + for (i = 0; i < fileneedednum; i++) + if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD) + { + totalfilesrequestednum++; + totalfilesrequestedsize += fileneeded[i].totalsize; + } + + if (totalfilesrequestedsize>>20 >= 10) + downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20)); + else + downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10)); + + if (serverisfull) + M_StartMessage("Server Connection", + va(M_GetText( + "This server is full!\n" + "Download of %s additional content\n" + "is required to join.\n" + "\n" + "You may download, load server addons,\n" + "and wait for a slot.\n" + ), downloadsize), &M_ConfirmConnect, MM_YESNO, "Continue", "Back to Menu"); + else + M_StartMessage("Server Connection", + va(M_GetText( + "Download of %s additional content\n" + "is required to join.\n" + ), downloadsize), &M_ConfirmConnect, MM_YESNO, "Continue", "Back to Menu"); + + Z_Free(downloadsize); + cl_mode = CL_CONFIRMCONNECT; + } +#ifdef HAVE_CURL + else + { + if (CL_SendFileRequest()) + { + cl_mode = CL_DOWNLOADFILES; + } + else + { + cl_mode = CL_DOWNLOADFAILED; + } + } +#endif + } + return true; +} + +/** Called by CL_ServerConnectionTicker + * + * \param asksent The last time we asked the server to join. We re-ask every second in case our request got lost in transmit. + * \return False if the connection was aborted + * \sa CL_ServerConnectionTicker + * \sa CL_ConnectToServer + * + */ +static boolean CL_ServerConnectionSearchTicker(tic_t *asksent) +{ + INT32 i; + + // serverlist is updated by GetPacket function + if (serverlistcount > 0) + { + // this can be a responce to our broadcast request + if (servernode == -1 || servernode >= MAXNETNODES) + { + i = 0; + servernode = serverlist[i].node; + CONS_Printf(M_GetText("Found, ")); + } + else + { + i = SL_SearchServer(servernode); + if (i < 0) + return true; + } + + // Quit here rather than downloading files and being refused later. + if (serverlist[i].info.refusereason) + { + serverisfull = true; + } + + if (client) + { +#ifdef DEVELOP + // Commits do not match? Do not connect! + if (memcmp(serverlist[i].info.commit, + comprevision_abbrev_bin, + GIT_SHA_ABBREV)) + { + char theirs[GIT_SHA_ABBREV * 2 + 1]; + UINT8 n; + + for (n = 0; n < GIT_SHA_ABBREV; ++n) + { + sprintf(&theirs[n * 2], "%02hhx", + serverlist[i].info.commit[n]); + } + + Command_ExitGame_f(); + + M_StartMessage("Server Connection Failure", + va( + "Your EXE differs from the server.\n" + " Yours: %.*s\n" + "Theirs: %s\n\n", + GIT_SHA_ABBREV * 2, comprevision, theirs), NULL, MM_NOTHING, NULL, "Back to Menu"); + return false; + } +#endif + +#ifdef HAVE_CURL + if (serverlist[i].info.httpsource[0]) + strncpy(http_source, serverlist[i].info.httpsource, MAX_MIRROR_LENGTH); + else + http_source[0] = '\0'; +#else + if (serverlist[i].info.httpsource[0]) + CONS_Printf("We received a http url from the server, however it will not be used as this build lacks curl support (%s)\n", serverlist[i].info.httpsource); +#endif + D_ParseFileneeded(serverlist[i].info.fileneedednum, serverlist[i].info.fileneeded, 0); + if (serverlist[i].info.kartvars & SV_LOTSOFADDONS) + { + cl_mode = CL_ASKFULLFILELIST; + cl_lastcheckedfilecount = 0; + return true; + } + + cl_mode = CL_CHECKFILES; + } + else + { + cl_mode = CL_ASKJOIN; // files need not be checked for the server. + *asksent = 0; + } + + return true; + } + + // Ask the info to the server (askinfo packet) + if (I_GetTime() >= *asksent) + { + SendAskInfo(servernode); + *asksent = I_GetTime() + NEWTICRATE; + } + + return true; +} + +/** Called by CL_ConnectToServer + * + * \param tmpsave The name of the gamestate file??? + * \param oldtic Used for knowing when to poll events and redraw + * \param asksent The last time we asked the server to join. We re-ask every second in case our request got lost in transmit. + * \return False if the connection was aborted + * \sa CL_ServerConnectionSearchTicker + * \sa CL_ConnectToServer + * + */ +static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic_t *asksent) +{ + boolean waitmore; + INT32 i; + const UINT8 pid = 0; + + switch (cl_mode) + { + case CL_SEARCHING: + if (!CL_ServerConnectionSearchTicker(asksent)) + return false; + break; + + case CL_ASKFULLFILELIST: + if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved + cl_mode = CL_CHECKFILES; + else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent) + { + if (CL_AskFileList(fileneedednum)) + { + cl_lastcheckedfilecount = fileneedednum; + *asksent = I_GetTime() + NEWTICRATE; + } + } + break; + case CL_CHECKFILES: + if (!CL_FinishedFileList()) + return false; + break; +#ifdef HAVE_CURL + case CL_PREPAREHTTPFILES: + if (http_source[0]) + { + for (i = 0; i < fileneedednum; i++) + if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD) + { + curl_transfers++; + } + + cl_mode = CL_DOWNLOADHTTPFILES; + } + break; + + case CL_DOWNLOADHTTPFILES: + waitmore = false; + for (i = 0; i < fileneedednum; i++) + if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD) + { + if (!curl_running) + CURLPrepareFile(http_source, i); + waitmore = true; + break; + } + + if (curl_running) + CURLGetFile(); + + if (waitmore) + break; // exit the case + + if (curl_failedwebdownload && !curl_transfers) + { + CONS_Printf("One or more files failed to download, falling back to internal downloader\n"); + cl_mode = CL_CHECKFILES; + break; + } + + if (!curl_transfers) + cl_mode = CL_LOADFILES; + + break; +#endif + case CL_DOWNLOADFILES: + waitmore = false; + for (i = 0; i < fileneedednum; i++) + if (fileneeded[i].status == FS_DOWNLOADING + || fileneeded[i].status == FS_REQUESTED) + { + waitmore = true; + break; + } + if (waitmore) + break; // exit the case + + cl_mode = CL_LOADFILES; + break; + case CL_DOWNLOADFAILED: + { + CONS_Printf(M_GetText("Legacy downloader request packet failed.\n")); + CONS_Printf(M_GetText("Network game synchronization aborted.\n")); + Command_ExitGame_f(); + M_StartMessage("Server Connection Failure", + M_GetText( + "The direct download encountered an error.\n" + "See the logfile for more info.\n" + ), NULL, MM_NOTHING, NULL, "Back to Menu"); + return false; + } + case CL_LOADFILES: + if (CL_LoadServerFiles()) + cl_mode = CL_SETUPFILES; + + break; + case CL_SETUPFILES: + if (P_PartialAddGetStage() < 0 || P_MultiSetupWadFiles(false)) + { + *asksent = 0; //This ensure the first join ask is right away + firstconnectattempttime = I_GetTime(); + cl_mode = CL_SENDKEY; + } + break; + case CL_ASKJOIN: + if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server) + { + CONS_Printf(M_GetText("5 minute wait time exceeded.\n")); + CONS_Printf(M_GetText("Network game synchronization aborted.\n")); + Command_ExitGame_f(); + M_StartMessage("Server Connection Failure", + M_GetText( + "5 minute wait time exceeded.\n" + "You may retry connection.\n" + ), NULL, MM_NOTHING, NULL, "Return to Menu"); + return false; + } + // prepare structures to save the file + // WARNING: this can be useless in case of server not in GS_LEVEL + // but since the network layer doesn't provide ordered packets... + CL_PrepareDownloadSaveGame(tmpsave); + if (I_GetTime() >= *asksent && CL_SendJoin()) + { + *asksent = I_GetTime() + NEWTICRATE*3; + cl_mode = CL_WAITJOINRESPONSE; + } + break; + case CL_WAITJOINRESPONSE: + if (I_GetTime() >= *asksent) + { + cl_mode = CL_ASKJOIN; + } + break; + case CL_SENDKEY: + if (I_GetTime() >= *asksent && CL_SendKey()) + { + *asksent = I_GetTime() + NEWTICRATE*3; + cl_mode = CL_WAITCHALLENGE; + } + break; + case CL_WAITCHALLENGE: + if (I_GetTime() >= *asksent) + { + cl_mode = CL_SENDKEY; + } + break; + case CL_DOWNLOADSAVEGAME: + // At this state, the first (and only) needed file is the gamestate + if (fileneeded[0].status == FS_FOUND) + { + // Gamestate is now handled within CL_LoadReceivedSavegame() + CL_LoadReceivedSavegame(false); + cl_mode = CL_CONNECTED; + break; + } // don't break case continue to CL_CONNECTED + else + break; + case CL_CONNECTED: + case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect + default: + break; + + // Connection closed by cancel, timeout or refusal. + case CL_ABORTED: + cl_mode = CL_SEARCHING; + return false; + + } + + GetPackets(); + Net_AckTicker(); + + // Call it only once by tic + if (*oldtic != I_GetTime()) + { + I_OsPolling(); + + // Needs to be updated here for M_DrawEggaChannelAlignable + renderdeltatics = FRACUNIT; + rendertimefrac = FRACUNIT; + + G_ResetAllDeviceResponding(); + + if (netgame) + { + for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1)) + { + HandleGamepadDeviceEvents(&events[eventtail]); + G_MapEventsToControls(&events[eventtail]); + } + +#ifdef HAVE_THREADS + I_lock_mutex(&k_menu_mutex); +#endif + M_UpdateMenuCMD(0, true); + + if (cl_mode == CL_CONFIRMCONNECT) + { + if (menumessage.active) + M_HandleMenuMessage(); + } + else + { + if (M_MenuBackPressed(pid)) + cl_mode = CL_ABORTED; + } + + M_ScreenshotTicker(); + +#ifdef HAVE_THREADS + I_unlock_mutex(k_menu_mutex); +#endif + } + + if (cl_mode == CL_ABORTED) + { + CONS_Printf(M_GetText("Network game synchronization aborted.\n")); +// M_StartMessage("Server Connection", M_GetText("Network game synchronization aborted.\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + + Command_ExitGame_f(); + return false; + } + + if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME)) + FileReceiveTicker(); + + // why are these here? this is for servers, we're a client + //if (key == 's' && server) + // doomcom->numnodes = (INT16)pnumnodes; + //FileSendTicker(); + *oldtic = I_GetTime(); + + if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED) + { + if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADSAVEGAME) + { + M_DrawEggaChannelAlignable(true); + } + CL_DrawConnectionStatus(); + + if (cl_mode == CL_CONFIRMCONNECT) + { +#ifdef HAVE_THREADS + I_lock_mutex(&k_menu_mutex); +#endif + M_DrawMenuMessage(); +#ifdef HAVE_THREADS + I_unlock_mutex(k_menu_mutex); +#endif + } + I_UpdateNoVsync(); // page flip or blit buffer + +#ifdef HWRENDER + // Only take screenshots after drawing. + if (moviemode && rendermode == render_opengl) + M_LegacySaveFrame(); + if (rendermode == render_opengl && takescreenshot) + M_DoLegacyGLScreenShot(); +#endif + + if ((moviemode || takescreenshot) && rendermode == render_soft) + I_CaptureVideoFrame(); + S_UpdateSounds(); + S_UpdateClosedCaptions(); + } + } + else + { + I_Sleep(cv_sleep.value); + I_UpdateTime(); + } + + return true; +} + +/** Use adaptive send using net_bandwidth and stat.sendbytes + * + * \todo Better description... + * + */ +static void CL_ConnectToServer(void) +{ + INT32 pnumnodes, nodewaited = doomcom->numnodes, i; + tic_t oldtic; + tic_t asksent; + char tmpsave[256]; + + sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home); + + lastfilenum = -1; + + cl_mode = CL_SEARCHING; + + // Don't get a corrupt savegame error because tmpsave already exists + if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1) + I_Error("Can't delete %s\n", tmpsave); + + if (netgame) + { + if (servernode < 0 || servernode >= MAXNETNODES) + CONS_Printf(M_GetText("Searching for a server...\n")); + else + CONS_Printf(M_GetText("Contacting the server...\n")); + } + + if (cv_currprofile.value == -1 && !demo.playback) + { + PR_ApplyProfilePretend(cv_ttlprofilen.value, 0); + for (i = 1; i < cv_splitplayers.value; i++) + { + PR_ApplyProfile(cv_lastprofile[i].value, i); + } + + // Slightly sucks that we have to duplicate these from d_main.c, but + // the change to cv_lastprofile doesn't take in time for this codepath. + if (M_CheckParm("-profile")) + { + UINT8 num = atoi(M_GetNextParm()); + PR_ApplyProfile(num, 0); + } + if (M_CheckParm("-profile2")) + { + UINT8 num = atoi(M_GetNextParm()); + PR_ApplyProfile(num, 1); + } + if (M_CheckParm("-profile3")) + { + UINT8 num = atoi(M_GetNextParm()); + PR_ApplyProfile(num, 2); + } + if (M_CheckParm("-profile4")) + { + UINT8 num = atoi(M_GetNextParm()); + PR_ApplyProfile(num, 3); + } + } + if (gamestate == GS_INTERMISSION) + Y_EndIntermission(); // clean up intermission graphics etc + if (gamestate == GS_VOTING) + Y_EndVote(); + + DEBFILE(va("waiting %d nodes\n", doomcom->numnodes)); + G_SetGamestate(GS_WAITINGPLAYERS); + if (wipegamestate == GS_MENU) + M_ClearMenus(true); + wipegamestate = GS_WAITINGPLAYERS; + + ClearAdminPlayers(); + Schedule_Clear(); + Automate_Clear(); + K_ClearClientPowerLevels(); + K_ResetMidVote(); + + pnumnodes = 1; + oldtic = 0; + asksent = 0; + firstconnectattempttime = I_GetTime(); + + i = SL_SearchServer(servernode); + + if (i != -1) + { + char *gametypestr = serverlist[i].info.gametypename; + + CON_LogMessage(va(M_GetText("Connecting to: %s\n"), serverlist[i].info.servername)); + + gametypestr[sizeof serverlist[i].info.gametypename - 1] = '\0'; + CON_LogMessage(va(M_GetText("Gametype: %s\n"), gametypestr)); + + CON_LogMessage(va(M_GetText("Version: %d.%d\n"), + serverlist[i].info.version, serverlist[i].info.subversion)); + } + SL_ClearServerList(servernode); + + for (i = 0; i < MAXPLAYERS; i++) + { + CL_ClearPlayer(i); + } + + do + { + // If the connection was aborted for some reason, leave + if (!CL_ServerConnectionTicker(tmpsave, &oldtic, &asksent)) + { + if (P_PartialAddGetStage() >= 0) + P_MultiSetupWadFiles(true); // in case any partial adds were done + + return; + } + + if (server) + { + pnumnodes = 0; + for (i = 0; i < MAXNETNODES; i++) + if (nodeingame[i]) + pnumnodes++; + } + } + while (!(cl_mode == CL_CONNECTED && (client || (server && nodewaited <= pnumnodes)))); + + if (netgame) + F_StartWaitingPlayers(); + DEBFILE(va("Synchronisation Finished\n")); + + displayplayers[0] = consoleplayer; + + // At this point we've succesfully joined the server, if we joined by IP (ie: a valid joinedIP string), save it! + // @TODO: Save the proper server name, right now it doesn't seem like we can consistently retrieve it from the serverlist....? + // It works... sometimes but not always which is weird. + + tmpsave[0] = '\0'; // TEMPORARY -- connectedservername is currently only set for YOUR server + if (joinedIP[0]) // false if we have "" which is \0 + M_AddToJoinedIPs(joinedIP, tmpsave); //connectedservername); -- as above + + joinedIP[0] = '\0'; // And empty this for good measure regardless of whether or not we actually used it. + +} + +static void Command_connect(void) +{ + + // By default, clear the saved address that we'd save after succesfully joining just to be sure: + joinedIP[0] = '\0'; + + if (COM_Argc() < 2 || *COM_Argv(1) == 0) + { + CONS_Printf(M_GetText( + "Connect (port): connect to a server\n" + "Connect ANY: connect to the first lan server found\n" + //"Connect SELF: connect to your own server.\n" + )); + return; + } + + if (Playing() || demo.attract) + { + CONS_Printf(M_GetText("You cannot connect while in a game. End this game first.\n")); + return; + } + + // modified game check: no longer handled + // we don't request a restart unless the filelist differs + + server = false; + + // Get the server node. + if (netgame) + { + // used in menu to connect to a server in the list + if (stricmp(COM_Argv(1), "node") != 0) + { + CONS_Printf(M_GetText("You cannot connect via address while joining a server.\n")); + return; + } + servernode = (SINT8)atoi(COM_Argv(2)); + } + else + { + // Standard behaviour + if (I_NetOpenSocket) + { + I_NetOpenSocket(); + netgame = true; + multiplayer = true; + + if (!stricmp(COM_Argv(1), "any")) + servernode = BROADCASTADDR; + else if (I_NetMakeNodewPort) + { + if (COM_Argc() >= 3) // address AND port + servernode = I_NetMakeNodewPort(COM_Argv(1), COM_Argv(2)); + else // address only, or address:port + servernode = I_NetMakeNode(COM_Argv(1)); + + // Last IPs joined: + // Keep the address we typed in memory so that we can save it if we *succesfully* join the server + strlcpy(joinedIP, COM_Argv(1), MAX_LOGIP); + } + else + { + CONS_Alert(CONS_ERROR, M_GetText("There is no server identification with this network driver\n")); + D_CloseConnection(); + return; + } + } + else + { + CONS_Alert(CONS_ERROR, M_GetText("There is no network driver\n")); + return; + } + } + + if (splitscreen != cv_splitplayers.value-1) + { + splitscreen = cv_splitplayers.value-1; + SplitScreen_OnChange(); + } + + // Menu restore state. + restoreMenu = &PLAY_MP_OptSelectDef; + currentMenu->lastOn = itemOn; + + Music_Remap("menu", "NETMD2"); + + if (stricmp(Music_CurrentSong(), "NETMD2")) + { + Music_Play("menu"); + } + + if (setup_numplayers == 0) + { + setup_numplayers = 1; + } + + CL_ConnectToServer(); +} + +static void ResetNode(INT32 node); + +// +// CL_ClearPlayer +// +// Clears the player data so that a future client can use this slot +// +void CL_ClearPlayer(INT32 playernum) +{ + int i; + + // Handle mobj_t pointers. + if (G_GamestateUsesLevel() == true) + { + if (players[playernum].follower) + { + K_RemoveFollower(&players[playernum]); + } + +#define PlayerPointerRemove(field) \ + if (P_MobjWasRemoved(field) == false) \ + { \ + P_RemoveMobj(field); \ + P_SetTarget(&field, NULL); \ + } + + // These are mostly subservient to the player, and may not clean themselves up. + PlayerPointerRemove(players[playernum].mo); + PlayerPointerRemove(players[playernum].followmobj); + PlayerPointerRemove(players[playernum].stumbleIndicator); + PlayerPointerRemove(players[playernum].wavedashIndicator); + PlayerPointerRemove(players[playernum].trickIndicator); + +#undef PlayerPointerRemove + + // These have thinkers of their own. + P_SetTarget(&players[playernum].whip, NULL); + P_SetTarget(&players[playernum].hand, NULL); + P_SetTarget(&players[playernum].hoverhyudoro, NULL); + P_SetTarget(&players[playernum].ballhogreticule, NULL); + P_SetTarget(&players[playernum].ringShooter, NULL); + + // TODO: Any better handling in store? + P_SetTarget(&players[playernum].flickyAttacker, NULL); + P_SetTarget(&players[playernum].powerup.flickyController, NULL); + P_SetTarget(&players[playernum].powerup.barrier, NULL); + + // These are camera items and possibly belong to multiple players. + P_SetTarget(&players[playernum].skybox.viewpoint, NULL); + P_SetTarget(&players[playernum].skybox.centerpoint, NULL); + P_SetTarget(&players[playernum].awayview.mobj, NULL); + + } + + // Handle parties. + for (i = 0; i < MAXPLAYERS; ++i) + { + if (splitscreen_invitations[i] == playernum) + splitscreen_invitations[i] = -1; + } + splitscreen_invitations[playernum] = -1; + + playerconsole[playernum] = playernum; + + // Wipe the struct. + memset(&players[playernum], 0, sizeof (player_t)); + + // Handle values which should not be initialised to 0. + players[playernum].followerskin = -1; // don't have a ghost follower + players[playernum].fakeskin = players[playernum].lastfakeskin = MAXSKINS; // don't avoid eggman + + // Handle post-cleanup. + RemoveAdminPlayer(playernum); // don't stay admin after you're gone + + // Clear voice chat data + S_ResetVoiceQueue(playernum); + + { + // Destroy and recreate the opus decoder for this playernum + OpusDecoder *opusdecoder = g_player_opus_decoders[playernum]; + if (opusdecoder) + { + opus_decoder_destroy(opusdecoder); + opusdecoder = NULL; + } + int error; + opusdecoder = opus_decoder_create(48000, 1, &error); + if (error != OPUS_OK) + { + CONS_Alert(CONS_WARNING, "Failed to create Opus decoder for player %d: opus error %d\n", playernum, error); + opusdecoder = NULL; + } + g_player_opus_decoders[playernum] = opusdecoder; + } +} + +// +// CL_RemovePlayer +// +// Removes a player from the current game +// +void CL_RemovePlayer(INT32 playernum, kickreason_t reason) +{ + // Sanity check: exceptional cases (i.e. c-fails) can cause multiple + // kick commands to be issued for the same player. + if (!playeringame[playernum]) + return; + + if (server && !demo.playback && playernode[playernum] != UINT8_MAX && !players[playernum].bot) + { + INT32 node = playernode[playernum]; + //playerpernode[node] = 0; // It'd be better to remove them all at once, but ghosting happened, so continue to let CL_RemovePlayer do it one-by-one + playerpernode[node]--; + if (playerpernode[node] <= 0) + { + nodeingame[node] = false; + Net_CloseConnection(node); + ResetNode(node); + } + } + + K_CalculateBattleWanted(); + + LUA_HookPlayerQuit(&players[playernum], reason); // Lua hook for player quitting + + G_LeaveParty(playernum); + + // Reset player data + CL_ClearPlayer(playernum); + + // remove avatar of player + playeringame[playernum] = false; + demo_extradata[playernum] |= DXD_PLAYSTATE; + playernode[playernum] = UINT8_MAX; + while (!playeringame[doomcom->numslots-1] && doomcom->numslots > 1) + doomcom->numslots--; + + // Reset the name + sprintf(player_names[playernum], "Player %c", 'A' + playernum); + + player_name_changes[playernum] = 0; + + LUA_InvalidatePlayer(&players[playernum]); + + // don't look through someone's view who isn't there + G_ResetViews(); + + K_CheckBumpers(); + P_CheckRacers(); +} + +void CL_Reset(void) +{ + if (demo.recording) + G_CheckDemoStatus(); + + // reset client/server code + DEBFILE(va("\n-=-=-=-=-=-=-= Client reset =-=-=-=-=-=-=-\n\n")); + + if (servernode > 0 && servernode < MAXNETNODES) + { + nodeingame[(UINT8)servernode] = false; + Net_CloseConnection(servernode); + } + D_CloseConnection(); // netgame = false + multiplayer = false; + servernode = 0; + server = true; + doomcom->numnodes = 1; + doomcom->numslots = 1; + SV_StopServer(); + SV_ResetServer(); + + // make sure we don't leave any fileneeded gunk over from a failed join + fileneedednum = 0; + memset(fileneeded, 0, sizeof(fileneeded)); + + totalfilesrequestednum = 0; + totalfilesrequestedsize = 0; + firstconnectattempttime = 0; + serverisfull = false; + connectiontimeout = (tic_t)cv_nettimeout.value; //reset this temporary hack + + expectChallenge = false; + +#ifdef HAVE_CURL + curl_failedwebdownload = false; + curl_transfers = 0; + curl_running = false; + http_source[0] = '\0'; +#endif + + G_ResetAllDeviceRumbles(); + + // D_StartTitle should get done now, but the calling function will handle it +} + +static void Command_GetPlayerNum(void) +{ + INT32 i; + + for (i = 0; i < MAXPLAYERS; i++) + if (playeringame[i]) + { + if (serverplayer == i) + CONS_Printf(M_GetText("num:%2d node:%2d %s\n"), i, playernode[i], player_names[i]); + else + CONS_Printf(M_GetText("\x82num:%2d node:%2d %s\n"), i, playernode[i], player_names[i]); + } +} + +SINT8 nametonum(const char *name) +{ + INT32 playernum, i; + + if (!strcmp(name, "0")) + return 0; + + playernum = (SINT8)atoi(name); + + if (playernum < 0 || playernum >= MAXPLAYERS) + return -1; + + if (playernum) + { + if (playeringame[playernum]) + return (SINT8)playernum; + else + return -1; + } + + for (i = 0; i < MAXPLAYERS; i++) + if (playeringame[i] && !stricmp(player_names[i], name)) + return (SINT8)i; + + CONS_Printf(M_GetText("There is no player named \"%s\"\n"), name); + + return -1; +} + +/** Lists all players and their player numbers. + * + * \sa Command_GetPlayerNum + */ +static void Command_Nodes(void) +{ + INT32 i; + size_t maxlen = 0; + const char *address; + + for (i = 0; i < MAXPLAYERS; i++) + { + const size_t plen = strlen(player_names[i]); + if (playeringame[i] && plen > maxlen) + maxlen = plen; + } + + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i]) + { + CONS_Printf("%.2u: %*s", i, (int)maxlen, player_names[i]); + + if (playernode[i] != UINT8_MAX) + { + CONS_Printf(" [node %.2d]", playernode[i]); + if (I_GetNodeAddress && (address = I_GetNodeAddress(playernode[i])) != NULL) + CONS_Printf(" - %s", address); + } + + if (K_UsingPowerLevels() != PWRLV_DISABLED) // No power type?! + { + CONS_Printf(" [%.4d PWR]", clientpowerlevels[i][K_UsingPowerLevels()]); + } + + CONS_Printf(" [%d games]", SV_GetStatsByPlayerIndex(i)->finishedrounds); + + + CONS_Printf(" [RRID-%s]", GetPrettyRRID(players[i].public_key, true)); + + if (IsPlayerAdmin(i)) + CONS_Printf(M_GetText(" (verified admin)")); + + if (players[i].spectator) + CONS_Printf(M_GetText(" (spectator)")); + + CONS_Printf("\n"); + } + } +} + +static void Command_Ban(void) +{ + if (COM_Argc() < 2) + { + CONS_Printf(M_GetText("ban : ban and kick a player\n")); + return; + } + + if (!netgame) // Don't kick Tails in splitscreen! + { + CONS_Printf(M_GetText("This only works in a netgame.\n")); + return; + } + + if (server || IsPlayerAdmin(consoleplayer)) + { + UINT8 buf[3 + MAX_REASONLENGTH]; + UINT8 *p = buf; + const SINT8 pn = nametonum(COM_Argv(1)); + + if (pn == -1 || pn == 0) + return; + + WRITEUINT8(p, pn); + + if (COM_Argc() == 2) + { + WRITEUINT8(p, KICK_MSG_BANNED); + SendNetXCmd(XD_KICK, &buf, 2); + } + else + { + size_t i, j = COM_Argc(); + char message[MAX_REASONLENGTH]; + + //Steal from the motd code so you don't have to put the reason in quotes. + strlcpy(message, COM_Argv(2), sizeof message); + for (i = 3; i < j; i++) + { + strlcat(message, " ", sizeof message); + strlcat(message, COM_Argv(i), sizeof message); + } + + WRITEUINT8(p, KICK_MSG_CUSTOM_BAN); + WRITESTRINGN(p, message, MAX_REASONLENGTH); + SendNetXCmd(XD_KICK, &buf, p - buf); + } + } + else + CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n")); +} + +static void Command_Kick(void) +{ + if (COM_Argc() < 2) + { + CONS_Printf(M_GetText("kick : kick a player\n")); + return; + } + + if (!netgame) // Don't kick Tails in splitscreen! + { + CONS_Printf(M_GetText("This only works in a netgame.\n")); + return; + } + + if (server || IsPlayerAdmin(consoleplayer)) + { + UINT8 buf[3 + MAX_REASONLENGTH]; + UINT8 *p = buf; + const SINT8 pn = nametonum(COM_Argv(1)); + + if (pn == -1 || pn == 0) + return; + + // Special case if we are trying to kick a player who is downloading the game state: + // trigger a timeout instead of kicking them, because a kick would only + // take effect after they have finished downloading + if (server && playernode[pn] != UINT8_MAX && sendingsavegame[playernode[pn]]) + { + Net_ConnectionTimeout(playernode[pn]); + return; + } + + WRITESINT8(p, pn); + + if (COM_Argc() == 2) + { + WRITEUINT8(p, KICK_MSG_KICKED); + SendNetXCmd(XD_KICK, &buf, 2); + } + else + { + size_t i, j = COM_Argc(); + char message[MAX_REASONLENGTH]; + + //Steal from the motd code so you don't have to put the reason in quotes. + strlcpy(message, COM_Argv(2), sizeof message); + for (i = 3; i < j; i++) + { + strlcat(message, " ", sizeof message); + strlcat(message, COM_Argv(i), sizeof message); + } + + WRITEUINT8(p, KICK_MSG_CUSTOM_KICK); + WRITESTRINGN(p, message, MAX_REASONLENGTH); + SendNetXCmd(XD_KICK, &buf, p - buf); + } + } + else + CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n")); +} + +static void Got_KickCmd(const UINT8 **p, INT32 playernum) +{ + INT32 pnum, msg; + char buf[3 + MAX_REASONLENGTH]; + char *reason = buf; + kickreason_t kickreason = KR_KICK; + UINT32 banMinutes = 0; + + pnum = READUINT8(*p); + msg = READUINT8(*p); + + if (msg == KICK_MSG_CUSTOM_BAN || msg == KICK_MSG_CUSTOM_KICK) + { + READSTRINGN(*p, reason, MAX_REASONLENGTH+1); + } + else + { + memset(reason, 0, sizeof(buf)); + } + + // Is playernum authorized to make this kick? + if (playernum != serverplayer && !IsPlayerAdmin(playernum) + /*&& !(playernode[playernum] != UINT8_MAX && playerpernode[playernode[playernum]] == 2 + && nodetoplayer2[playernode[playernum]] == pnum)*/) + { + // We received a kick command from someone who isn't the + // server or admin, and who isn't in splitscreen removing + // player 2. Thus, it must be someone with a modified + // binary, trying to kick someone but without having + // authorization. + + // We deal with this by changing the kick reason to + // "consistency failure" and kicking the offending user + // instead. + + CONS_Alert(CONS_WARNING, M_GetText("Illegal kick command received from %s for player %d\n"), player_names[playernum], pnum); + + // In debug, print a longer message with more details. + // TODO Callum: Should we translate this? +/* + CONS_Debug(DBG_NETPLAY, + "So, you must be asking, why is this an illegal kick?\n" + "Well, let's take a look at the facts, shall we?\n" + "\n" + "playernum (this is the guy who did it), he's %d.\n" + "pnum (the guy he's trying to kick) is %d.\n" + "playernum's node is %d.\n" + "That node has %d players.\n" + "Player 2 on that node is %d.\n" + "pnum's node is %d.\n" + "That node has %d players.\n" + "Player 2 on that node is %d.\n" + "\n" + "If you think this is a bug, please report it, including all of the details above.\n", + playernum, pnum, + playernode[playernum], playerpernode[playernode[playernum]], + nodetoplayer2[playernode[playernum]], + playernode[pnum], playerpernode[playernode[pnum]], + nodetoplayer2[playernode[pnum]]); +*/ + pnum = playernum; + msg = KICK_MSG_CON_FAIL; + } + + if (g_midVote.active == true && g_midVote.victim == &players[pnum]) + { + if (g_midVote.type == MVT_KICK) + { + // Running the callback here would mean a very dumb infinite loop. + // We'll manually handle this here by changing the msg type. + if (msg != KICK_MSG_BANNED && msg != KICK_MSG_CUSTOM_BAN) + { + // of course, don't take the teeth out of a ban + msg = KICK_MSG_VOTE_KICK; + } + K_MidVoteFinalize(FRACUNIT); // Vote succeeded, so the delay is normal. + } + else + { + // It should be safe to run the vote callback directly. + K_MidVoteSuccess(); + } + } + + if (playernode[pnum] == servernode) + { + CONS_Printf(M_GetText("Ignoring kick attempt from %s on node %d (it's the server)\n"), player_names[playernum], servernode); + return; + } + + //CONS_Printf("\x82%s ", player_names[pnum]); + + // Save bans here. Used to be split between here and the actual command, depending on + // whenever the server did it or a remote admin did it, but it's simply more convenient + // to keep it all in one place. + if (server) + { + if (msg == KICK_MSG_KICKED || msg == KICK_MSG_VOTE_KICK || msg == KICK_MSG_CUSTOM_KICK) + { + // Kick as a temporary ban. + banMinutes = cv_kicktime.value; + } + + if (msg == KICK_MSG_BANNED || msg == KICK_MSG_CUSTOM_BAN || banMinutes) + { + SV_BanPlayer(pnum, banMinutes, reason); + } + } + + if (msg == KICK_MSG_PLAYER_QUIT) + S_StartSound(NULL, sfx_leave); // intended leave + else + S_StartSound(NULL, sfx_syfail); // he he he + + switch (msg) + { + case KICK_MSG_KICKED: + HU_AddChatText(va("\x82*%s has been kicked (No reason given)", player_names[pnum]), false); + kickreason = KR_KICK; + break; + case KICK_MSG_VOTE_KICK: + HU_AddChatText(va("\x82*%s has been kicked (Popular demand)", player_names[pnum]), false); + kickreason = KR_KICK; + break; + case KICK_MSG_PING_HIGH: + HU_AddChatText(va("\x82*%s left the game (Broke delay limit)", player_names[pnum]), false); + kickreason = KR_PINGLIMIT; + break; + case KICK_MSG_CON_FAIL: + HU_AddChatText(va("\x82*%s left the game (Synch failure)", player_names[pnum]), false); + kickreason = KR_SYNCH; + + if (M_CheckParm("-consisdump")) // Helps debugging some problems + { + INT32 i; + + CONS_Printf(M_GetText("Player kicked is #%d, dumping consistency...\n"), pnum); + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i]) + continue; + CONS_Printf("-------------------------------------\n"); + CONS_Printf("Player %d: %s\n", i, player_names[i]); + CONS_Printf("Skin: %d\n", players[i].skin); + CONS_Printf("Color: %d\n", players[i].skincolor); + CONS_Printf("Speed: %d\n",players[i].speed>>FRACBITS); + if (players[i].mo) + { + if (!players[i].mo->skin) + CONS_Printf("Mobj skin: NULL!\n"); + else + CONS_Printf("Mobj skin: %s\n", ((skin_t *)players[i].mo->skin)->name); + CONS_Printf("Position: %d, %d, %d\n", players[i].mo->x, players[i].mo->y, players[i].mo->z); + if (!players[i].mo->state) + CONS_Printf("State: S_NULL\n"); + else + CONS_Printf("State: %d\n", (statenum_t)(players[i].mo->state-states)); + } + else + CONS_Printf("Mobj: NULL\n"); + CONS_Printf("-------------------------------------\n"); + } + } + break; + case KICK_MSG_TIMEOUT: + HU_AddChatText(va("\x82*%s left the game (Connection timeout)", player_names[pnum]), false); + kickreason = KR_TIMEOUT; + break; + case KICK_MSG_SIGFAIL: + HU_AddChatText(va("\x82*%s left the game (Invalid signature)", player_names[pnum]), false); + kickreason = KR_TIMEOUT; + break; + case KICK_MSG_PLAYER_QUIT: + if (netgame) // not splitscreen/bots + HU_AddChatText(va("\x82*%s left the game", player_names[pnum]), false); + kickreason = KR_LEAVE; + break; + case KICK_MSG_GRIEF: + S_StartSound(NULL, sfx_cftbl1); + HU_AddChatText(va("\x82*%s has been kicked (Automatic grief detection)", player_names[pnum]), false); + kickreason = KR_KICK; + break; + case KICK_MSG_BANNED: + HU_AddChatText(va("\x82*%s has been banned (No reason given)", player_names[pnum]), false); + kickreason = KR_BAN; + break; + case KICK_MSG_CUSTOM_KICK: + HU_AddChatText(va("\x82*%s has been kicked (%s)", player_names[pnum], reason), false); + kickreason = KR_KICK; + break; + case KICK_MSG_CUSTOM_BAN: + HU_AddChatText(va("\x82*%s has been banned (%s)", player_names[pnum], reason), false); + kickreason = KR_BAN; + break; + } + + // SRB2Kart: kicks count as forfeit + switch (kickreason) + { + case KR_KICK: + case KR_BAN: + case KR_LEAVE: + // Intentional removals should be hit with a true forfeit. + K_PlayerForfeit(pnum, true); + break; + default: + // Otherwise, give remaining players the point compensation, but doesn't penalize who left. + K_PlayerForfeit(pnum, false); + break; + } + + if (playernode[pnum] == playernode[consoleplayer]) + { + LUA_HookBool(false, HOOK(GameQuit)); //Lua hooks handled differently now + + Command_ExitGame_f(); + + if (msg == KICK_MSG_CON_FAIL) + M_StartMessage("Server Disconnected", M_GetText("Server closed connection\n(Synch failure)\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + else if (msg == KICK_MSG_PING_HIGH) + M_StartMessage("Server Disconnected", M_GetText("Server closed connection\n(Broke delay limit)\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + else if (msg == KICK_MSG_TIMEOUT) // this one will probably never be seen? + M_StartMessage("Server Disconnected", M_GetText("Connection timed out\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + else if (msg == KICK_MSG_BANNED) + M_StartMessage("Server Disconnected", M_GetText("You have been banned by the server\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + else if (msg == KICK_MSG_CUSTOM_KICK) + M_StartMessage("Server Disconnected", M_GetText("You have been kicked\n(Automatic grief detection)\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + else if (msg == KICK_MSG_CUSTOM_KICK) + M_StartMessage("Server Disconnected", va(M_GetText("You have been kicked\n(%s)\n"), reason), NULL, MM_NOTHING, NULL, "Back to Menu"); + else if (msg == KICK_MSG_CUSTOM_BAN) + M_StartMessage("Server Disconnected", va(M_GetText("You have been banned\n(%s)\n"), reason), NULL, MM_NOTHING, NULL, "Back to Menu"); + else if (msg == KICK_MSG_SIGFAIL) + M_StartMessage("Server Disconnected", M_GetText("Server closed connection\n(Invalid signature)\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + else if (msg == KICK_MSG_VOTE_KICK) + M_StartMessage("Server Disconnected", M_GetText("You have been kicked by popular demand\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + else + M_StartMessage("Server Disconnected", M_GetText("You have been kicked by the server\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); + } + else if (server) + { + // Sal: Because kicks (and a lot of other commands) are player-based, we can't tell which player pnum is on the node from a glance. + // When we want to remove everyone from a node, we have to get the kicked player's node, then remove everyone on that node manually so we don't miss any. + // This avoids the bugs with older SRB2 version's online splitscreen kicks, specifically ghosting. + // On top of this, it can't just be a CL_RemovePlayer call; it has to be a server-sided. + // Clients don't bother setting any nodes for anything but THE server player (even ignoring the server's extra players!), so it'll often remove everyone because they all have node -1/255, insta-desync! + // And yes. This is a netxcmd wrap for just CL_RemovePlayer! :V + +#define removethisplayer(otherp) \ + if (otherp >= 0) \ + { \ + buf[0] = (UINT8)otherp; \ + if (otherp != pnum) \ + { \ + HU_AddChatText(va("\x82*%s left the game (Joined with %s)", player_names[otherp], player_names[pnum]), false); \ + buf[1] = KR_LEAVE; \ + } \ + else \ + buf[1] = (UINT8)kickreason; \ + SendNetXCmd(XD_REMOVEPLAYER, &buf, 2); \ + otherp = -1; \ + } + removethisplayer(nodetoplayer[playernode[pnum]]) + removethisplayer(nodetoplayer2[playernode[pnum]]) + removethisplayer(nodetoplayer3[playernode[pnum]]) + removethisplayer(nodetoplayer4[playernode[pnum]]) +#undef removethisplayer + } +} + +#ifdef HAVE_CURL +/** Add a login for HTTP downloads. If the + * user/password is missing, remove it. + * + * \sa Command_list_http_logins + */ +static void Command_set_http_login (void) +{ + HTTP_login *login; + HTTP_login **prev_next; + + if (COM_Argc() < 2) + { + CONS_Printf( + "set_http_login [user:password]: Set or remove a login to " + "authenticate HTTP downloads.\n" + ); + return; + } + + login = CURLGetLogin(COM_Argv(1), &prev_next); + + if (COM_Argc() == 2) + { + if (login) + { + (*prev_next) = login->next; + CONS_Printf("Login for '%s' removed.\n", login->url); + Z_Free(login); + } + } + else + { + if (login) + Z_Free(login->auth); + else + { + login = ZZ_Alloc(sizeof *login); + login->url = Z_StrDup(COM_Argv(1)); + } + + login->auth = Z_StrDup(COM_Argv(2)); + + login->next = curl_logins; + curl_logins = login; + } +} + +/** List logins for HTTP downloads. + * + * \sa Command_set_http_login + */ +static void Command_list_http_logins (void) +{ + HTTP_login *login; + + for ( + login = curl_logins; + login; + login = login->next + ){ + CONS_Printf( + "'%s' -> '%s'\n", + login->url, + login->auth + ); + } +} +#endif/*HAVE_CURL*/ + +static void Command_ResendGamestate(void) +{ + SINT8 playernum; + + if (COM_Argc() == 1) + { + CONS_Printf(M_GetText("resendgamestate : resend the game state to a player\n")); + return; + } + else if (client) + { + CONS_Printf(M_GetText("Only the server can use this.\n")); + return; + } + + playernum = nametonum(COM_Argv(1)); + if (playernum == -1 || playernum == 0) + return; + + // Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on + netbuffer->packettype = PT_WILLRESENDGAMESTATE; + if (!HSendPacket(playernode[playernum], true, 0, 0)) + { + CONS_Alert(CONS_ERROR, M_GetText("A problem occured, please try again.\n")); + return; + } +} + +static void Command_ServerMute(void) +{ + SINT8 playernum; + UINT8 buf[2]; + + if (!netgame) + { + CONS_Printf(M_GetText("This only works in a netgame.\n")); + return; + } + + if (COM_Argc() == 1) + { + CONS_Printf(M_GetText("servermute : server mute a player's voice\n")); + return; + } + else if (!(server || IsPlayerAdmin(consoleplayer))) + { + CONS_Printf(M_GetText("Only the server or an admin can use this.\n")); + return; + } + + playernum = nametonum(COM_Argv(1)); + if (playernum == -1) + return; + + buf[0] = playernum; + buf[1] = 1; + SendNetXCmd(XD_SERVERMUTEPLAYER, buf, 2); +} + +static void Command_ServerUnmute(void) +{ + SINT8 playernum; + UINT8 buf[2]; + + if (!netgame) + { + CONS_Printf(M_GetText("This only works in a netgame.\n")); + return; + } + + if (COM_Argc() == 1) + { + CONS_Printf(M_GetText("serverunmute : server unmute a player's voice\n")); + return; + } + else if (!(server || IsPlayerAdmin(consoleplayer))) + { + CONS_Printf(M_GetText("Only the server or an admin can use this.\n")); + return; + } + + playernum = nametonum(COM_Argv(1)); + if (playernum == -1) + return; + + buf[0] = playernum; + buf[1] = 0; + SendNetXCmd(XD_SERVERMUTEPLAYER, &buf, 2); +} + +static void Command_ServerDeafen(void) +{ + SINT8 playernum; + UINT8 buf[2]; + + if (COM_Argc() == 1) + { + CONS_Printf(M_GetText("serverdeafen : server deafen a player\n")); + return; + } + else if (client) + { + CONS_Printf(M_GetText("Only the server can use this.\n")); + return; + } + + playernum = nametonum(COM_Argv(1)); + if (playernum == -1) + return; + + buf[0] = playernum; + buf[1] = 1; + SendNetXCmd(XD_SERVERDEAFENPLAYER, &buf, 2); +} + +static void Command_ServerUndeafen(void) +{ + SINT8 playernum; + UINT8 buf[2]; + + if (COM_Argc() == 1) + { + CONS_Printf(M_GetText("serverundeafen : server undeafen a player\n")); + return; + } + else if (client) + { + CONS_Printf(M_GetText("Only the server can use this.\n")); + return; + } + + playernum = nametonum(COM_Argv(1)); + if (playernum == -1) + return; + + buf[0] = playernum; + buf[1] = 0; + SendNetXCmd(XD_SERVERDEAFENPLAYER, buf, 2); +} + +static void Got_AddPlayer(const UINT8 **p, INT32 playernum); +static void Got_RemovePlayer(const UINT8 **p, INT32 playernum); +static void Got_AddBot(const UINT8 **p, INT32 playernum); +static void Got_ServerMutePlayer(const UINT8 **p, INT32 playernum); +static void Got_ServerDeafenPlayer(const UINT8 **p, INT32 playernum); + +void Joinable_OnChange(void); +void Joinable_OnChange(void) +{ + UINT8 buf[3]; + UINT8 *p = buf; + UINT8 maxplayer; + + if (!server) + return; + + maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value)); + + WRITEUINT8(p, maxplayer); + WRITEUINT8(p, cv_allownewplayer.value); + WRITEUINT8(p, cv_discordinvites.value); + + SendNetXCmd(XD_DISCORD, &buf, 3); +} + +// called one time at init +void D_ClientServerInit(void) +{ + DEBFILE(va("- - -== Ring Racers v%d.%d "VERSIONSTRING" debugfile ==- - -\n", + VERSION, SUBVERSION)); + + COM_AddCommand("getplayernum", Command_GetPlayerNum); + COM_AddCommand("kick", Command_Kick); + COM_AddCommand("ban", Command_Ban); + COM_AddCommand("listbans", Command_Listbans); + COM_AddCommand("unban", Command_Unban); + COM_AddCommand("banip", Command_BanIP); + COM_AddCommand("connect", Command_connect); + COM_AddCommand("nodes", Command_Nodes); +#ifdef HAVE_CURL + COM_AddCommand("set_http_login", Command_set_http_login); + COM_AddCommand("list_http_logins", Command_list_http_logins); +#endif + COM_AddCommand("resendgamestate", Command_ResendGamestate); +#ifdef PACKETDROP + COM_AddCommand("drop", Command_Drop); + COM_AddCommand("droprate", Command_Droprate); +#endif + COM_AddCommand("numnodes", Command_Numnodes); + + RegisterNetXCmd(XD_KICK, Got_KickCmd); + RegisterNetXCmd(XD_ADDPLAYER, Got_AddPlayer); + RegisterNetXCmd(XD_REMOVEPLAYER, Got_RemovePlayer); + RegisterNetXCmd(XD_ADDBOT, Got_AddBot); + + COM_AddCommand("servermute", Command_ServerMute); + COM_AddCommand("serverunmute", Command_ServerUnmute); + COM_AddCommand("serverdeafen", Command_ServerDeafen); + COM_AddCommand("serverundeafen", Command_ServerUndeafen); + RegisterNetXCmd(XD_SERVERMUTEPLAYER, Got_ServerMutePlayer); + RegisterNetXCmd(XD_SERVERDEAFENPLAYER, Got_ServerDeafenPlayer); + + gametic = 0; + localgametic = 0; + + // do not send anything before the real begin + SV_StopServer(); + SV_ResetServer(); + if (dedicated) + SV_SpawnServer(); +} + +static void ResetNode(INT32 node) +{ + nodeingame[node] = false; + nodewaiting[node] = 0; + nodeneedsauth[node] = false; + + nettics[node] = gametic; + supposedtics[node] = gametic; + + nodetoplayer[node] = -1; + nodetoplayer2[node] = -1; + nodetoplayer3[node] = -1; + nodetoplayer4[node] = -1; + playerpernode[node] = 0; + + sendingsavegame[node] = false; + resendingsavegame[node] = false; + savegameresendcooldown[node] = 0; + + bannednode[node].banid = SIZE_MAX; + bannednode[node].timeleft = NO_BAN_TIME; +} + +void SV_ResetServer(void) +{ + INT32 i; + + // +1 because this command will be executed in com_executebuffer in + // tryruntic so gametic will be incremented, anyway maketic > gametic + // is not an issue + + maketic = gametic + 1; + neededtic = maketic; + tictoclear = maketic; + + joindelay = 0; + + for (i = 0; i < MAXNETNODES; i++) + ResetNode(i); + + for (i = 0; i < MAXPLAYERS; i++) + { + LUA_InvalidatePlayer(&players[i]); + sprintf(player_names[i], "Player %c", 'A' + i); + } + + memset(playeringame, false, sizeof playeringame); + memset(playernode, UINT8_MAX, sizeof playernode); + + pingmeasurecount = 1; + memset(realpingtable, 0, sizeof realpingtable); + memset(playerpingtable, 0, sizeof playerpingtable); + memset(playerpacketlosstable, 0, sizeof playerpacketlosstable); + memset(playerdelaytable, 0, sizeof playerdelaytable); + + ClearAdminPlayers(); + Schedule_Clear(); + Automate_Clear(); + K_ClearClientPowerLevels(); + G_ObliterateParties(); + K_ResetMidVote(); + + memset(splitscreen_invitations, -1, sizeof splitscreen_invitations); + memset(player_name_changes, 0, sizeof player_name_changes); + + mynode = 0; + cl_packetmissed = false; + cl_redownloadinggamestate = false; + + if (dedicated) + { + nodeingame[0] = true; + serverplayer = 0; + } + else + serverplayer = consoleplayer; + + if (server) + servernode = 0; + + doomcom->numslots = 0; + + // clear server_context + memset(server_context, '-', 8); + + strlcpy(connectedservername, "\0", MAXSERVERNAME); + strlcpy(connectedservercontact, "\0", MAXSERVERCONTACT); + + CV_RevertNetVars(); + + // Copy our unlocks to a place where net material can grab at/overwrite them safely. + // (permits all unlocks in dedicated) + M_SetNetUnlocked(); + + expectChallenge = false; + + DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n"); +} + +#ifndef TESTERS +static void SV_GenContext(void) +{ + UINT8 i; + + // generate server_context, as exactly 8 bytes of randomly mixed A-Z and a-z + // (hopefully M_Random is initialized!! if not this will be awfully silly!) + for (i = 0; i < 8; i++) + { + const char a = M_RandomKey(26*2); + if (a < 26) // uppercase + server_context[i] = 'A'+a; + else // lowercase + server_context[i] = 'a'+(a-26); + } + + D_ParseCarets(connectedservername, cv_servername.string, MAXSERVERNAME); + D_ParseCarets(connectedservercontact, cv_server_contact.string, MAXSERVERCONTACT); +} +#endif // TESTERS + +// +// D_QuitNetGame +// Called before quitting to leave a net game +// without hanging the other players +// +void D_QuitNetGame(void) +{ + if (!netgame || !netbuffer) + return; + + DEBFILE("===========================================================================\n" + " Quitting Game, closing connection\n" + "===========================================================================\n"); + + // abort send/receive of files + CloseNetFile(); + RemoveAllLuaFileTransfers(); + waitingforluafiletransfer = false; + waitingforluafilecommand = false; + + if (server) + { + INT32 i; + + netbuffer->packettype = PT_SERVERSHUTDOWN; + for (i = 0; i < MAXNETNODES; i++) + if (nodeingame[i]) + HSendPacket(i, true, 0, 0); +#ifdef MASTERSERVER + if (serverrunning && netgame && cv_advertise.value) // see mserv.c Online() + UnregisterServer(); +#endif + } + else if (servernode > 0 && servernode < MAXNETNODES && nodeingame[(UINT8)servernode]) + { + netbuffer->packettype = PT_CLIENTQUIT; + HSendPacket(servernode, true, 0, 0); + } + + D_CloseConnection(); + ClearAdminPlayers(); + Schedule_Clear(); + Automate_Clear(); + K_ClearClientPowerLevels(); + G_ObliterateParties(); + K_ResetMidVote(); + + DEBFILE("===========================================================================\n" + " Log finish\n" + "===========================================================================\n"); +#ifdef DEBUGFILE + if (debugfile) + { + fclose(debugfile); + debugfile = NULL; + } +#endif +} + +static void InitializeLocalVoiceEncoder(void) +{ + // Reset voice opus encoder for local "player 1" + OpusEncoder *encoder = g_local_opus_encoder; + if (encoder != NULL) + { + opus_encoder_destroy(encoder); + encoder = NULL; + } + int error; + encoder = opus_encoder_create(48000, 1, OPUS_APPLICATION_VOIP, &error); + opus_encoder_ctl(encoder, OPUS_SET_VBR(0)); + if (error != OPUS_OK) + { + CONS_Alert(CONS_WARNING, "Failed to create Opus voice encoder: opus error %d\n", error); + encoder = NULL; + } + g_local_opus_encoder = encoder; + g_local_opus_frame = 0; +} + +// Adds a node to the game (player will follow at map change or at savegame....) +static inline void SV_AddNode(INT32 node) +{ + nettics[node] = gametic; + supposedtics[node] = gametic; + // little hack because the server connects to itself and puts + // nodeingame when connected not here + if (node) + nodeingame[node] = true; + + nodeneedsauth[node] = false; +} + +// Xcmd XD_ADDPLAYER +static void Got_AddPlayer(const UINT8 **p, INT32 playernum) +{ + INT16 node, newplayernum; + UINT8 console; + UINT8 splitscreenplayer = 0; + UINT8 i; + player_t *newplayer; + uint8_t public_key[PUBKEYLENGTH]; + + if (playernum != serverplayer && !IsPlayerAdmin(playernum)) + { + // protect against hacked/buggy client + CONS_Alert(CONS_WARNING, M_GetText("Illegal add player command received from %s\n"), player_names[playernum]); + if (server) + SendKick(playernum, KICK_MSG_CON_FAIL); + return; + } + + node = READUINT8(*p); + newplayernum = READUINT8(*p); + + CONS_Debug(DBG_NETPLAY, "addplayer: %d %d\n", node, newplayernum); + + //G_SpectatePlayerOnJoin(newplayernum); -- caused desyncs in this spot :( + + if (newplayernum+1 > doomcom->numslots) + doomcom->numslots = (INT16)(newplayernum+1); + + newplayer = &players[newplayernum]; + + READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME); + READMEM(*p, public_key, PUBKEYLENGTH); + READMEM(*p, clientpowerlevels[newplayernum], sizeof(((serverplayer_t *)0)->powerlevels)); + + console = READUINT8(*p); + splitscreenplayer = READUINT8(*p); + + G_AddPlayer(newplayernum, console); + memcpy(players[newplayernum].public_key, public_key, PUBKEYLENGTH); + + for (i = 0; i < MAXAVAILABILITY; i++) + { + newplayer->availabilities[i] = READUINT8(*p); + } + + // the server is creating my player + if (node == mynode) + { + playernode[newplayernum] = 0; // for information only + + if (splitscreenplayer) + { + displayplayers[splitscreenplayer] = newplayernum; + g_localplayers[splitscreenplayer] = newplayernum; + DEBFILE(va("spawning sister # %d\n", splitscreenplayer)); + } + else + { + consoleplayer = newplayernum; + for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) + { + displayplayers[i] = newplayernum; + g_localplayers[i] = newplayernum; + } + DEBFILE("spawning me\n"); + + InitializeLocalVoiceEncoder(); + } + + P_ForceLocalAngle(newplayer, newplayer->angleturn); + + D_SendPlayerConfig(splitscreenplayer); + addedtogame = true; + } + + players[newplayernum].splitscreenindex = splitscreenplayer; + players[newplayernum].bot = false; + + if (node == mynode && splitscreenplayer == 0) + S_AttemptToRestoreMusic(); // Earliest viable point + + if (netgame) + { + char joinmsg[256]; + + strcpy(joinmsg, M_GetText("\x82*%s has joined the game (player %d)")); + strcpy(joinmsg, va(joinmsg, player_names[newplayernum], newplayernum)); + + if (node != mynode) + S_StartSound(NULL, sfx_join); + + // Merge join notification + IP to avoid clogging console/chat + if (server && cv_showjoinaddress.value && I_GetNodeAddress) + { + const char *address = I_GetNodeAddress(node); + if (address) + strcat(joinmsg, va(" (%s)", address)); + } + + HU_AddChatText(joinmsg, false); + } + + if (server && multiplayer && motd[0] != '\0') + COM_BufAddText(va("sayto %d %s\n", newplayernum, motd)); + + LUA_HookInt(newplayernum, HOOK(PlayerJoin)); + +#ifdef HAVE_DISCORDRPC + DRPC_UpdatePresence(); +#endif +} + +// Xcmd XD_REMOVEPLAYER +static void Got_RemovePlayer(const UINT8 **p, INT32 playernum) +{ + SINT8 pnum, reason; + + if (playernum != serverplayer && !IsPlayerAdmin(playernum)) + { + // protect against hacked/buggy client + CONS_Alert(CONS_WARNING, M_GetText("Illegal remove player command received from %s\n"), player_names[playernum]); + if (server) + { + SendKick(playernum, KICK_MSG_CON_FAIL); + } + return; + } + + pnum = READUINT8(*p); + reason = READUINT8(*p); + + CL_RemovePlayer(pnum, reason); + +#ifdef HAVE_DISCORDRPC + DRPC_UpdatePresence(); +#endif +} + +// Xcmd XD_ADDBOT +// Compacted version of XD_ADDPLAYER for simplicity +static void Got_AddBot(const UINT8 **p, INT32 playernum) +{ + INT16 newplayernum; + UINT8 skinnum = 0; + UINT8 difficulty = DIFFICULTBOT; + botStyle_e style = BOT_STYLE_NORMAL; + + if (playernum != serverplayer && !IsPlayerAdmin(playernum)) + { + // protect against hacked/buggy client + CONS_Alert(CONS_WARNING, M_GetText("Illegal add player command received from %s\n"), player_names[playernum]); + if (server) + { + SendKick(playernum, KICK_MSG_CON_FAIL); + } + return; + } + + newplayernum = READUINT8(*p); + skinnum = READUINT8(*p); + difficulty = READUINT8(*p); + style = READUINT8(*p); + + K_SetBot(newplayernum, skinnum, difficulty, style); +} + +// Xcmd XD_SERVERMUTEPLAYER +static void Got_ServerMutePlayer(const UINT8 **p, INT32 playernum) +{ + UINT8 forplayer = READUINT8(*p); + UINT8 muted = READUINT8(*p); + if (playernum != serverplayer) + { + CONS_Alert(CONS_WARNING, M_GetText("Illegal server mute player cmd from %s\n"), player_names[playernum]); + if (server) + { + SendKick(playernum, KICK_MSG_CON_FAIL); + } + } + if (muted && !(players[forplayer].pflags2 & PF2_SERVERMUTE)) + { + players[forplayer].pflags2 |= PF2_SERVERMUTE; + HU_AddChatText(va("\x82* %s was server muted.", player_names[forplayer]), false); + } + else if (!muted && players[forplayer].pflags2 & PF2_SERVERMUTE) + { + players[forplayer].pflags2 &= ~PF2_SERVERMUTE; + HU_AddChatText(va("\x82* %s was server unmuted.", player_names[forplayer]), false); + } +} + +// Xcmd XD_SERVERDEAFENPLAYER +static void Got_ServerDeafenPlayer(const UINT8 **p, INT32 playernum) +{ + UINT8 forplayer = READUINT8(*p); + UINT8 deafened = READUINT8(*p); + if (playernum != serverplayer) + { + CONS_Alert(CONS_WARNING, M_GetText("Illegal server deafen player cmd from %s\n"), player_names[playernum]); + if (server) + { + SendKick(playernum, KICK_MSG_CON_FAIL); + } + } + if (deafened && !(players[forplayer].pflags2 & PF2_SERVERDEAFEN)) + { + players[forplayer].pflags2 |= PF2_SERVERDEAFEN; + HU_AddChatText(va("\x82* %s was server deafened.", player_names[forplayer]), false); + } + else if (!deafened && players[forplayer].pflags2 & PF2_SERVERDEAFEN) + { + players[forplayer].pflags2 &= ~PF2_SERVERDEAFEN; + HU_AddChatText(va("\x82* %s was server undeafened.", player_names[forplayer]), false); + } +} + +static boolean SV_AddWaitingPlayers(SINT8 node, UINT8 *availabilities, + const char *name, uint8_t *key, UINT16 *pwr, + const char *name2, uint8_t *key2, UINT16 *pwr2, + const char *name3, uint8_t *key3, UINT16 *pwr3, + const char *name4, uint8_t *key4, UINT16 *pwr4) +{ + INT32 n, newplayernum, i; + UINT8 buf[4 + MAXPLAYERNAME + PUBKEYLENGTH + MAXAVAILABILITY + sizeof(((serverplayer_t *)0)->powerlevels)]; + UINT8 *buf_p = buf; + boolean newplayer = false; + + { + // splitscreen can allow 2+ players in one node + for (; nodewaiting[node] > 0; nodewaiting[node]--) + { + newplayer = true; + + { + UINT8 nobotoverwrite; + + // search for a free playernum + // we can't solely use playeringame since it is not updated here + for (newplayernum = dedicated ? 1 : 0; newplayernum < MAXPLAYERS; newplayernum++) + { + if (playeringame[newplayernum]) + continue; + + for (n = 0; n < MAXNETNODES; n++) + if (nodetoplayer[n] == newplayernum + || nodetoplayer2[n] == newplayernum + || nodetoplayer3[n] == newplayernum + || nodetoplayer4[n] == newplayernum) + break; + + if (n == MAXNETNODES) + break; + } + + nobotoverwrite = newplayernum; + + while (playeringame[nobotoverwrite] + && players[nobotoverwrite].bot + && nobotoverwrite < MAXPLAYERS) + { + // Overwrite bots if there are NO other slots available. + nobotoverwrite++; + } + + if (nobotoverwrite < MAXPLAYERS) + { + newplayernum = nobotoverwrite; + } + } + + // should never happen since we check the playernum + // before accepting the join + I_Assert(newplayernum < MAXPLAYERS); + + playernode[newplayernum] = (UINT8)node; + + // Reset the buffer to the start for multiple joiners + buf_p = buf; + + WRITEUINT8(buf_p, (UINT8)node); + WRITEUINT8(buf_p, newplayernum); + + if (playerpernode[node] < 1) + { + nodetoplayer[node] = newplayernum; + WRITESTRINGN(buf_p, name, MAXPLAYERNAME); + WRITEMEM(buf_p, key, PUBKEYLENGTH); + WRITEMEM(buf_p, pwr, sizeof(((serverplayer_t *)0)->powerlevels)); + } + else if (playerpernode[node] < 2) + { + nodetoplayer2[node] = newplayernum; + WRITESTRINGN(buf_p, name2, MAXPLAYERNAME); + WRITEMEM(buf_p, key2, PUBKEYLENGTH); + WRITEMEM(buf_p, pwr2, sizeof(((serverplayer_t *)0)->powerlevels)); + } + else if (playerpernode[node] < 3) + { + nodetoplayer3[node] = newplayernum; + WRITESTRINGN(buf_p, name3, MAXPLAYERNAME); + WRITEMEM(buf_p, key3, PUBKEYLENGTH); + WRITEMEM(buf_p, pwr3, sizeof(((serverplayer_t *)0)->powerlevels)); + } + else if (playerpernode[node] < 4) + { + nodetoplayer4[node] = newplayernum; + WRITESTRINGN(buf_p, name4, MAXPLAYERNAME); + WRITEMEM(buf_p, key4, PUBKEYLENGTH); + WRITEMEM(buf_p, pwr4, sizeof(((serverplayer_t *)0)->powerlevels)); + } + + WRITEUINT8(buf_p, nodetoplayer[node]); // consoleplayer + WRITEUINT8(buf_p, playerpernode[node]); // splitscreen num + + for (i = 0; i < MAXAVAILABILITY; i++) + { + WRITEUINT8(buf_p, availabilities[i]); + } + + playerpernode[node]++; + + SendNetXCmd(XD_ADDPLAYER, buf, buf_p - buf); + DEBFILE(va("Server added player %d node %d\n", newplayernum, node)); + } + } + + return newplayer; +} + +/*-------------------------------------------------- + boolean K_AddBotFromServer(UINT8 skin, UINT8 difficulty, botStyle_e style, UINT8 *p) + + See header file for description. +--------------------------------------------------*/ +boolean K_AddBotFromServer(UINT8 skin, UINT8 difficulty, botStyle_e style, UINT8 *p) +{ + UINT8 newplayernum = *p; + + // search for a free playernum + // we can't use playeringame since it is not updated here + for (; newplayernum < MAXPLAYERS; newplayernum++) + { + UINT8 n; + + for (n = 0; n < MAXNETNODES; n++) + { + if (nodetoplayer[n] == newplayernum + || nodetoplayer2[n] == newplayernum + || nodetoplayer3[n] == newplayernum + || nodetoplayer4[n] == newplayernum) + break; + } + + if (n == MAXNETNODES) + break; + } + + for (; newplayernum < MAXPLAYERS; newplayernum++) + { + if (playeringame[newplayernum] == false) + { + // free player slot + break; + } + } + + if (newplayernum >= MAXPLAYERS) + { + // nothing is free + *p = MAXPLAYERS; + return false; + } + + if (server) + { + UINT8 buf[4]; + UINT8 *buf_p = buf; + + WRITEUINT8(buf_p, newplayernum); + + if (skin > numskins) + { + skin = numskins; + } + + WRITEUINT8(buf_p, skin); + + if (difficulty < 1) + { + difficulty = 1; + } + else if (difficulty > MAXBOTDIFFICULTY) + { + difficulty = MAXBOTDIFFICULTY; + } + + WRITEUINT8(buf_p, difficulty); + WRITEUINT8(buf_p, style); + + SendNetXCmd(XD_ADDBOT, buf, buf_p - buf); + DEBFILE(va("Server added bot %d\n", newplayernum)); + } + + // use the next free slot (we can't put playeringame[newplayernum] = true here) + *p = newplayernum+1; + return true; +} + +void CL_AddSplitscreenPlayer(void) +{ + if (cl_mode == CL_CONNECTED) + CL_SendJoin(); +} + +void CL_RemoveSplitscreenPlayer(UINT8 p) +{ + if (cl_mode != CL_CONNECTED) + return; + + SendKick(p, KICK_MSG_PLAYER_QUIT); +} + +#ifndef TESTERS +static void GotOurIP(UINT32 address) +{ + #ifdef DEVELOP + { + const unsigned char * p = (const unsigned char *)&address; + CONS_Printf("Got IP of %u.%u.%u.%u\n", p[0], p[1], p[2], p[3]); + } + #endif + ourIP = address; +} +#endif + +// is there a game running +boolean Playing(void) +{ + return (server && serverrunning) || (client && cl_mode == CL_CONNECTED); +} + +boolean SV_SpawnServer(void) +{ +#ifdef TESTERS + /* Just don't let the testers play. Easy. */ + I_Error("What do you think you're doing?"); + return false; +#else + boolean result = false; + if (demo.playback) + G_StopDemo(); // reset engine parameter + + if (!serverrunning) + { + CON_LogMessage(M_GetText("Starting Server....\n")); + serverrunning = true; + SV_ResetServer(); + SV_GenContext(); + if (netgame) + { + if (I_NetOpenSocket) + { + I_NetOpenSocket(); + } + + if (cv_advertise.value) + { + RegisterServer(); + } + + ourIP = 0; + STUN_bind(GotOurIP); + } + + // non dedicated server just connect to itself + if (!dedicated) + CL_ConnectToServer(); + else doomcom->numslots = 1; + } + + + + // strictly speaking, i'm not convinced the following is necessary + // but I'm not confident enough to remove it entirely in case it breaks something + { + UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false, -1); + SINT8 node = 0; + for (; node < MAXNETNODES; node++) + result |= SV_AddWaitingPlayers(node, availabilitiesbuffer, + cv_playername[0].zstring, PR_GetLocalPlayerProfile(0)->public_key, SV_GetStatsByKey(PR_GetLocalPlayerProfile(0)->public_key)->powerlevels, + cv_playername[1].zstring, PR_GetLocalPlayerProfile(1)->public_key, SV_GetStatsByKey(PR_GetLocalPlayerProfile(1)->public_key)->powerlevels, + cv_playername[2].zstring, PR_GetLocalPlayerProfile(2)->public_key, SV_GetStatsByKey(PR_GetLocalPlayerProfile(2)->public_key)->powerlevels, + cv_playername[3].zstring, PR_GetLocalPlayerProfile(3)->public_key, SV_GetStatsByKey(PR_GetLocalPlayerProfile(3)->public_key)->powerlevels); + } + return result; +#endif +} + +void SV_StopServer(void) +{ + tic_t i; + + if (gamestate == GS_INTERMISSION) + Y_EndIntermission(); + if (gamestate == GS_VOTING) + Y_EndVote(); + + G_SetGamestate(GS_NULL); + wipegamestate = GS_NULL; + + for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) + ((UINT16*)localtextcmd[i])[0] = 0; + + for (i = firstticstosend; i < firstticstosend + BACKUPTICS; i++) + D_Clearticcmd(i); + + consoleplayer = 0; + cl_mode = CL_ABORTED; + maketic = gametic+1; + neededtic = maketic; + serverrunning = false; + titlemapinaction = false; +} + +// called at singleplayer start and stopdemo +void SV_StartSinglePlayerServer(INT32 dogametype, boolean donetgame) +{ + INT32 lastgametype = gametype; + server = true; + multiplayer = (modeattacking == ATTACKING_NONE); + joinedIP[0] = '\0'; // Make sure to empty this so that we don't save garbage when we start our own game. (because yes we use this for netgames too....) + + netgame = false; // so setting timelimit works... (XD_NETVAR doesn't play nice with SV_StopServer) + + G_SetGametype(dogametype); + if (gametype != lastgametype) + D_GameTypeChanged(lastgametype); + + netgame = donetgame; + + // no more tic the game with this settings! + SV_StopServer(); +} + +static void SV_SendRefuse(INT32 node, const char *reason) +{ + strcpy(netbuffer->u.serverrefuse.reason, reason); + + netbuffer->packettype = PT_SERVERREFUSE; + HSendPacket(node, false, 0, strlen(netbuffer->u.serverrefuse.reason) + 1); + Net_CloseConnection(node); +} + +// used at txtcmds received to check packetsize bound +static size_t TotalTextCmdPerTic(tic_t tic) +{ + INT32 i; + size_t total = 1; // num of textcmds in the tic (ntextcmd byte) + + for (i = 0; i < MAXPLAYERS; i++) + { + UINT8 *textcmd = D_GetExistingTextcmd(tic, i); + if ((!i || playeringame[i]) && textcmd) + total += 3 + ((UINT16*)textcmd)[0]; // "+2" for size and playernum + } + + return total; +} + +#ifdef SIGNGAMETRAFFIC + static boolean IsSplitPlayerOnNodeGuest(int node, int split) + { + char allZero[PUBKEYLENGTH]; + memset(allZero, 0, PUBKEYLENGTH); + + if (split == 0) + return PR_IsKeyGuest(players[nodetoplayer[node]].public_key); + else if (split == 1) + return PR_IsKeyGuest(players[nodetoplayer2[node]].public_key); + else if (split == 2) + return PR_IsKeyGuest(players[nodetoplayer3[node]].public_key); + else if (split == 3) + return PR_IsKeyGuest(players[nodetoplayer4[node]].public_key); + else + I_Error("IsSplitPlayerOnNodeGuest: Out of bounds"); + return false; // unreachable + } +#endif + +static boolean IsPlayerGuest(int player) +{ + return PR_IsKeyGuest(players[player].public_key); +} + +/** Called when a PT_CLIENTJOIN packet is received + * + * \param node The packet sender + * + */ +static void HandleConnect(SINT8 node) +{ + char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1]; + INT32 i, j; + UINT8 availabilitiesbuffer[MAXAVAILABILITY]; + + // Sal: Dedicated mode is INCREDIBLY hacked together. + // If a server filled out, then it'd overwrite the host and turn everyone into weird husks..... + // It's too much effort to legimately fix right now. Just prevent it from reaching that state. + UINT8 maxplayers = min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value); + UINT8 connectedplayers = 0; + + for (i = dedicated ? 1 : 0; i < MAXPLAYERS; i++) + { + // We use this to count players because it is affected by SV_AddWaitingPlayers when + // more than one client joins on the same tic, unlike playeringame and D_NumPlayers. + // UINT8_MAX denotes no node for that player. + + if (playernode[i] != UINT8_MAX) + { + // Sal: This hack sucks, but it should be safe. + // playeringame is set for bots immediately; they are deterministic instead of a netxcmd. + // If a bot is added with netxcmd in the future, then the node check is still here too. + // So at worst, a theoretical netxcmd added bot will block real joiners for the time + // it takes for the command to process, but not cause any horrifying player overwriting. + if (playeringame[i] && players[i].bot) + { + continue; + } + + connectedplayers++; + } + } + + banrecord_t *ban = SV_GetBanByAddress(node); + if (ban == NULL) + { + for (i = 0; i < netbuffer->u.clientcfg.localplayers - playerpernode[node]; i++) + { + if (ban == NULL) + ban = SV_GetBanByKey(lastReceivedKey[node][i]); + } + } + + if (ban != NULL && node != 0) + { + UINT32 timeremaining = 0; + if (ban->expires > time(NULL)) + { + timeremaining = ban->expires - time(NULL); + int minutes = (timeremaining + 30) / 60; + int hours = (minutes + 1) / 60; + int days = (hours + 1) / 24; + + if (days) + { + SV_SendRefuse(node, va("K|%s\n(Time remaining: %d day%s)", ban->reason, days, days > 1 ? "s" : "")); + } + else if (hours) + { + SV_SendRefuse(node, va("K|%s\n(Time remaining: %d hour%s)", ban->reason, hours, hours > 1 ? "s" : "")); + } + else if (minutes) + { + SV_SendRefuse(node, va("K|%s\n(Time remaining: %d minute%s)", ban->reason, minutes, minutes > 1 ? "s" : "")); + } + else + { + SV_SendRefuse(node, va("K|%s\n(Time remaining: <1 minute)", ban->reason)); + } + } + else + { + SV_SendRefuse(node, va("B|%s", ban->reason)); + } + } + else if (netbuffer->u.clientcfg._255 != 255 || + netbuffer->u.clientcfg.packetversion != PACKETVERSION) + { + SV_SendRefuse(node, "Incompatible packet formats."); + } + else if (strncmp(netbuffer->u.clientcfg.application, SRB2APPLICATION, + sizeof netbuffer->u.clientcfg.application)) + { + SV_SendRefuse(node, "Different Ring Racers modifications\nare not compatible."); + } + else if (netbuffer->u.clientcfg.version != VERSION + || netbuffer->u.clientcfg.subversion != SUBVERSION) + { + SV_SendRefuse(node, va(M_GetText("Different Ring Racers versions cannot\nplay a netgame!\n(server version %d.%d)"), VERSION, SUBVERSION)); + } + else if (!cv_allownewplayer.value && node) + { + SV_SendRefuse(node, M_GetText("The server is not accepting\njoins for the moment.")); + } + else if (connectedplayers >= maxplayers) + { + SV_SendRefuse(node, va(M_GetText("Maximum players reached: %d"), maxplayers)); + } + else if (netgame && netbuffer->u.clientcfg.localplayers > MAXSPLITSCREENPLAYERS) // Hacked client? + { + SV_SendRefuse(node, M_GetText("Too many players from\nthis node.")); + } + else if (netgame && connectedplayers + netbuffer->u.clientcfg.localplayers > maxplayers) + { + SV_SendRefuse(node, va(M_GetText("Number of local players\nwould exceed maximum: %d"), maxplayers)); + } + else if (netgame && !netbuffer->u.clientcfg.localplayers) // Stealth join? + { + SV_SendRefuse(node, M_GetText("No players from\nthis node.")); + } + else if (luafiletransfers) + { + SV_SendRefuse(node, M_GetText("The server is broadcasting a file\nrequested by a Lua script.\nPlease wait a bit and then\ntry rejoining.")); + } + else if (netgame && joindelay > 2 * (tic_t)cv_joindelay.value * TICRATE) + { + SV_SendRefuse(node, va(M_GetText("Too many people are connecting.\nPlease wait %d seconds and then\ntry rejoining."), + (joindelay - 2 * cv_joindelay.value * TICRATE) / TICRATE)); + } + else + { + int sigcheck; + boolean newnode = false; + + for (i = 0; i < netbuffer->u.clientcfg.localplayers - playerpernode[node]; i++) + { + strlcpy(names[i], netbuffer->u.clientcfg.names[i], MAXPLAYERNAME + 1); + if (!EnsurePlayerNameIsGood(names[i], -1)) + { + SV_SendRefuse(node, "Bad player name"); + return; + } + + if (node == 0) // Hey, that's us. We're always allowed to do what we want. + { + memcpy(lastReceivedKey[node][i], PR_GetLocalPlayerProfile(i)->public_key, sizeof(lastReceivedKey[node][i])); + } + else // Remote player, gotta check their signature. + { + if (PR_IsKeyGuest(lastReceivedKey[node][i])) // IsSplitPlayerOnNodeGuest isn't appropriate here, they're not in-game yet! + { + if (!cv_allowguests.value) + { + SV_SendRefuse(node, M_GetText("The server doesn't allow GUESTs.\nCreate a profile to join!")); + return; + } + + sigcheck = 0; // Always succeeds. Yes, this is a success response. C R Y P T O + } + else + { + sigcheck = crypto_eddsa_check(netbuffer->u.clientcfg.challengeResponse[i], lastReceivedKey[node][i], lastSentChallenge[node], CHALLENGELENGTH); + } + + if (netgame && sigcheck != 0) + { + SV_SendRefuse(node, M_GetText("Signature verification failed.")); + return; + } + } + + // Check non-GUESTS for duplicate pubkeys, they'll create nonsense stats + if (!PR_IsKeyGuest(lastReceivedKey[node][i])) + { + // Players already here + for (j = 0; j < MAXPLAYERS; j++) + { + if (!playeringame[j]) + continue; + if (memcmp(lastReceivedKey[node][i], players[j].public_key, PUBKEYLENGTH) == 0) + { + #ifdef DEVELOP + CONS_Alert(CONS_WARNING, "Joining player's pubkey matches existing player, stat updates will be nonsense!\n"); + #else + SV_SendRefuse(node, M_GetText("Duplicate pubkey already on server.\n(Did you share your profile?)")); + return; + #endif + } + } + + // Players we're trying to add + for (j = 0; j < netbuffer->u.clientcfg.localplayers - playerpernode[node]; j++) + { + if (PR_IsKeyGuest(lastReceivedKey[node][j])) + continue; + if (i == j) + continue; + if (memcmp(lastReceivedKey[node][i], lastReceivedKey[node][j], PUBKEYLENGTH) == 0) + { + #ifdef DEVELOP + CONS_Alert(CONS_WARNING, "Players with same pubkey in the joning party, stat updates will be nonsense!\n"); + #else + SV_SendRefuse(node, M_GetText("Duplicate pubkey in local party.\n(How did you even do this?)")); + return; + #endif + } + } + } + } + + memcpy(availabilitiesbuffer, netbuffer->u.clientcfg.availabilities, sizeof(availabilitiesbuffer)); + + // client authorised to join + nodewaiting[node] = (UINT8)(netbuffer->u.clientcfg.localplayers - playerpernode[node]); + if (!nodeingame[node]) + { + gamestate_t backupstate = gamestate; + newnode = true; + + SV_AddNode(node); + +#ifdef VANILLAJOINNEXTROUND + if (cv_joinnextround.value && gameaction == ga_nothing) + G_SetGamestate(GS_WAITINGPLAYERS); +#endif + if (!SV_SendServerConfig(node)) + { + G_SetGamestate(backupstate); + /// \note Shouldn't SV_SendRefuse be called before ResetNode? + SV_SendRefuse(node, M_GetText("Server couldn't send info, please try again")); + ResetNode(node); // Yeah, lets try it! + /// \todo fix this !!! + return; // restart the while + } + //if (gamestate != GS_LEVEL) // GS_INTERMISSION, etc? + // SV_SendPlayerConfigs(node); // send bare minimum player info + G_SetGamestate(backupstate); + DEBFILE("new node joined\n"); + } + if (nodewaiting[node]) + { + if (node && newnode) + { + SV_SendSaveGame(node, false); // send a complete game state + DEBFILE("send savegame\n"); + } + + SV_AddWaitingPlayers(node, availabilitiesbuffer, + names[0], lastReceivedKey[node][0], SV_GetStatsByKey(lastReceivedKey[node][0])->powerlevels, + names[1], lastReceivedKey[node][1], SV_GetStatsByKey(lastReceivedKey[node][1])->powerlevels, + names[2], lastReceivedKey[node][2], SV_GetStatsByKey(lastReceivedKey[node][2])->powerlevels, + names[3], lastReceivedKey[node][3], SV_GetStatsByKey(lastReceivedKey[node][3])->powerlevels); + joindelay += cv_joindelay.value * TICRATE; + player_joining = true; + } + } +} + +/** Called when a PT_SERVERSHUTDOWN packet is received + * + * \param node The packet sender (should be the server) + * + */ +static void HandleShutdown(SINT8 node) +{ + (void)node; + LUA_HookBool(false, HOOK(GameQuit)); + Command_ExitGame_f(); + M_StartMessage("Server Disconnected", M_GetText("Server has shutdown\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); +} + +/** Called when a PT_NODETIMEOUT packet is received + * + * \param node The packet sender (should be the server) + * + */ +static void HandleTimeout(SINT8 node) +{ + (void)node; + LUA_HookBool(false, HOOK(GameQuit)); + Command_ExitGame_f(); + M_StartMessage("Server Disconnected", M_GetText("Server Timeout\n"), NULL, MM_NOTHING, NULL, "Back to Menu"); +} + +// Called when a signature check fails and we suspect the server is playing games. +void HandleSigfail(const char *string) +{ + if (server) // This situation is basically guaranteed to be nonsense. + { + CONS_Alert(CONS_ERROR, "Auth error! %s\n", string); + return; // Keep the game running, you're probably testing. + } + + LUA_HookBool(false, HOOK(GameQuit)); + Command_ExitGame_f(); + M_StartMessage("Server Disconnected", va(M_GetText("Signature check failed.\n(%s)\n"), string), NULL, MM_NOTHING, NULL, "Back to Menu"); +} + +/** Called when a PT_SERVERINFO packet is received + * + * \param node The packet sender + * \note What happens if the packet comes from a client or something like that? + * + */ +static void HandleServerInfo(SINT8 node) +{ + // compute ping in ms + const tic_t ticnow = I_GetTime(); + const tic_t ticthen = (tic_t)LONG(netbuffer->u.serverinfo.time); + const tic_t ticdiff = (ticnow - ticthen)*1000/NEWTICRATE; + netbuffer->u.serverinfo.time = (tic_t)LONG(ticdiff); + netbuffer->u.serverinfo.servername[MAXSERVERNAME-1] = 0; + netbuffer->u.serverinfo.application + [sizeof netbuffer->u.serverinfo.application - 1] = '\0'; + netbuffer->u.serverinfo.gametypename + [sizeof netbuffer->u.serverinfo.gametypename - 1] = '\0'; + D_SanitizeKeepColors(netbuffer->u.serverinfo.servername, netbuffer->u.serverinfo.servername, MAXSERVERNAME); + + // If we have cause to reject it, it's not worth observing. + if ( + SL_InsertServer(&netbuffer->u.serverinfo, node) == false + && serverlistultimatecount + ) + { + serverlistultimatecount--; + } +} + +static void PT_WillResendGamestate(void) +{ + char tmpsave[1024]; + + if (server || cl_redownloadinggamestate) + return; + + // Don't let the server pull a fast one with everyone's identity! + // Save the public keys we see, so if the server tries to swap one, we'll know. + int i; + for (i = 0; i < MAXPLAYERS; i++) + { + memcpy(priorKeys[i], players[i].public_key, sizeof(priorKeys[i])); + } + + // Send back a PT_CANRECEIVEGAMESTATE packet to the server + // so they know they can start sending the game state + netbuffer->packettype = PT_CANRECEIVEGAMESTATE; + if (!HSendPacket(servernode, true, 0, 0)) + return; + + CONS_Printf(M_GetText("Reloading game state...\n")); + + sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home); + + // Don't get a corrupt savegame error because tmpsave already exists + if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1) + I_Error("Can't delete %s\n", tmpsave); + + CL_PrepareDownloadSaveGame(tmpsave); + + cl_redownloadinggamestate = true; +} + +static void PT_CanReceiveGamestate(SINT8 node) +{ + if (client || sendingsavegame[node]) + return; + + CONS_Printf(M_GetText("Resending game state to %s...\n"), player_names[nodetoplayer[node]]); + + extern consvar_t cv_dumpconsistency; + if (cv_dumpconsistency.value) + { + char dump_name[1024]; + snprintf( + dump_name, sizeof(dump_name), + "%s_%u_%s-server.consdump", + server_context, + gametic, + player_names[nodetoplayer[node]] + ); + CL_DumpConsistency(dump_name); + } + + SV_SendSaveGame(node, true); // Resend a complete game state + resendingsavegame[node] = true; +} + +/** Handles a packet received from a node that isn't in game + * + * \param node The packet sender + * \todo Choose a better name, as the packet can also come from the server apparently? + * \sa HandlePacketFromPlayer + * \sa GetPackets + * + */ +static void HandlePacketFromAwayNode(SINT8 node) +{ + if (node != servernode) + DEBFILE(va("Received packet from unknown host %d\n", node)); + +// macro for packets that should only be sent by the server +// if it is NOT from the server, bail out and close the connection! +#define SERVERONLY \ + if (node != servernode) \ + { \ + Net_CloseConnection(node); \ + break; \ + } + switch (netbuffer->packettype) + { + case PT_ASKINFOVIAMS: +#if 0 + if (server && serverrunning) + { + INT32 clientnode; + if (ms_RoomId < 0) // ignore if we're not actually on the MS right now + { + Net_CloseConnection(node); // and yes, close connection + return; + } + clientnode = I_NetMakeNode(netbuffer->u.msaskinfo.clientaddr); + if (clientnode != -1) + { + SV_SendServerInfo(clientnode, (tic_t)LONG(netbuffer->u.msaskinfo.time)); + SV_SendPlayerInfo(clientnode); // Send extra info + Net_CloseConnection(clientnode); + // Don't close connection to MS... + } + else + Net_CloseConnection(node); // ...unless the IP address is not valid + } + else + Net_CloseConnection(node); // you're not supposed to get it, so ignore it +#else + Net_CloseConnection(node); +#endif + break; + + case PT_TELLFILESNEEDED: + if (server && serverrunning) + { + UINT8 *p; + INT32 firstfile = netbuffer->u.filesneedednum; + + netbuffer->packettype = PT_MOREFILESNEEDED; + netbuffer->u.filesneededcfg.first = firstfile; + netbuffer->u.filesneededcfg.more = 0; + + p = PutFileNeeded(firstfile); + + HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u)); + } + else // Shouldn't get this if you aren't the server...? + Net_CloseConnection(node); + break; + + case PT_MOREFILESNEEDED: + if (server && serverrunning) + { // But wait I thought I'm the server? + Net_CloseConnection(node); + break; + } + SERVERONLY + if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum) + { + D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first); + if (!netbuffer->u.filesneededcfg.more) + cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list + } + break; + + case PT_ASKINFO: + if (server && serverrunning) + { + SV_SendServerInfo(node, (tic_t)LONG(netbuffer->u.askinfo.time)); + SV_SendPlayerInfo(node); // Send extra info + } + Net_CloseConnection(node); + break; + + case PT_SERVERREFUSE: // Negative response of client join request + if (server && serverrunning) + { // But wait I thought I'm the server? + Net_CloseConnection(node); + break; + } + SERVERONLY + if (cl_mode == CL_WAITJOINRESPONSE) + { + // Save the reason so it can be displayed after quitting the netgame + char *reason = strdup(netbuffer->u.serverrefuse.reason); + if (!reason) + I_Error("Out of memory!\n"); + + if (strstr(reason, "Maximum players reached")) + { + serverisfull = true; + //Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer + //We set it back to the value of cv_nettimeout.value in CL_Reset + connectiontimeout = NEWTICRATE*7; + cl_mode = CL_ASKJOIN; + free(reason); + break; + } + + Command_ExitGame_f(); + + if (reason[1] == '|') + { + M_StartMessage("Server Connection Failure", + va("You have been %sfrom the server\n\nReason:\n%s", + (reason[0] == 'B') ? "banned\n" : "temporarily\nkicked ", + reason+2), NULL, MM_NOTHING, NULL, "Back to Menu"); + } + else + { + M_StartMessage("Server Connection Failure", + va(M_GetText("Server refuses connection\n\nReason:\n%s"), + reason), NULL, MM_NOTHING, NULL, "Back to Menu"); + } + + free(reason); + + // Will be reset by caller. Signals refusal. + cl_mode = CL_ABORTED; + } + break; + + case PT_SERVERCFG: // Positive response of client join request + { + if (server && serverrunning && node != servernode) + { // but wait I thought I'm the server? + Net_CloseConnection(node); + break; + } + SERVERONLY + /// \note how would this happen? and is it doing the right thing if it does? + if (!(cl_mode == CL_WAITJOINRESPONSE || cl_mode == CL_ASKJOIN)) + break; + + if (client) + { + maketic = gametic = neededtic = (tic_t)LONG(netbuffer->u.servercfg.gametic); + + G_SetGametype(netbuffer->u.servercfg.gametype); + + modifiedgame = netbuffer->u.servercfg.modifiedgame; + + memcpy(server_context, netbuffer->u.servercfg.server_context, 8); + + D_SanitizeKeepColors(connectedservername, netbuffer->u.servercfg.server_name, MAXSERVERNAME); + D_SanitizeKeepColors(connectedservercontact, netbuffer->u.servercfg.server_contact, MAXSERVERCONTACT); + } + +#ifdef HAVE_DISCORDRPC + discordInfo.maxPlayers = netbuffer->u.servercfg.maxplayer; + discordInfo.joinsAllowed = netbuffer->u.servercfg.allownewplayer; + discordInfo.everyoneCanInvite = netbuffer->u.servercfg.discordinvites; +#endif + + nodeingame[(UINT8)servernode] = true; + serverplayer = netbuffer->u.servercfg.serverplayer; + doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum); + mynode = netbuffer->u.servercfg.clientnode; + if (serverplayer >= 0) + playernode[(UINT8)serverplayer] = servernode; + + if (netgame) + CONS_Printf(M_GetText("Join accepted, waiting for complete game state...\n")); + DEBFILE(va("Server accept join gametic=%u mynode=%d\n", gametic, mynode)); + + /// \note Wait. What if a Lua script uses some global custom variables synched with the NetVars hook? + /// Shouldn't them be downloaded even at intermission time? + /// Also, according to HandleConnect, the server will send the savegame even during intermission... + /// Sryder 2018-07-05: If we don't want to send the player config another way we need to send the gamestate + /// At almost any gamestate there could be joiners... So just always send gamestate? + cl_mode = ((server) ? CL_CONNECTED : CL_DOWNLOADSAVEGAME); + break; + } + + // Handled in d_netfil.c + case PT_FILEFRAGMENT: + if (server) + { // But wait I thought I'm the server? + Net_CloseConnection(node); + break; + } + SERVERONLY + PT_FileFragment(); + break; + + case PT_FILEACK: + if (server) + PT_FileAck(); + break; + + case PT_FILERECEIVED: + if (server) + PT_FileReceived(); + break; + + case PT_REQUESTFILE: + if (server) + { + if (!cv_downloading.value || !PT_RequestFile(node)) + Net_CloseConnection(node); // close connection if one of the requested files could not be sent, or you disabled downloading anyway + } + else + Net_CloseConnection(node); // nope + break; + + case PT_NODETIMEOUT: + case PT_CLIENTQUIT: + if (server) + { + Net_CloseConnection(node); + nodeneedsauth[node] = false; + } + break; + + case PT_CLIENTCMD: + break; // This is not an "unknown packet" + + case PT_SERVERTICS: + // Do not remove my own server (we have just get a out of order packet) + if (node == servernode) + break; + /* FALLTHRU */ + case PT_CLIENTKEY: + if (server) + { + PT_ClientKey(node); + + // Client's not in the server yet, but we still need to lock up the node. + // Otherwise, someone else could request a challenge on the same node and trash it. + nodeneedsauth[node] = true; + freezetimeout[node] = I_GetTime() + jointimeout; + } + break; + case PT_SERVERCHALLENGE: + if (server && serverrunning && node != servernode) + { + Net_CloseConnection(node); + break; + } + if (cl_mode != CL_WAITCHALLENGE) + break; + memcpy(awaitingChallenge, netbuffer->u.serverchallenge.secret, sizeof(awaitingChallenge)); + cl_mode = CL_ASKJOIN; + break; + default: + DEBFILE(va("unknown packet received (%d) from unknown host\n",netbuffer->packettype)); + Net_CloseConnection(node); + break; // Ignore it + + } +#undef SERVERONLY +} + +/** Checks ticcmd for "speed hacks" + * + * \param p Which player + * \return True if player is hacking + * \sa HandlePacketFromPlayer + * + */ +static boolean CheckForSpeedHacks(UINT8 p) +{ + if (netcmds[maketic%BACKUPTICS][p].forwardmove > MAXPLMOVE || netcmds[maketic%BACKUPTICS][p].forwardmove < -MAXPLMOVE + || netcmds[maketic%BACKUPTICS][p].turning > KART_FULLTURN || netcmds[maketic%BACKUPTICS][p].turning < -KART_FULLTURN + || netcmds[maketic%BACKUPTICS][p].throwdir > KART_FULLTURN || netcmds[maketic%BACKUPTICS][p].throwdir < -KART_FULLTURN) + { + CONS_Alert(CONS_WARNING, M_GetText("Illegal movement value received from node %d\n"), playernode[p]); + //D_Clearticcmd(k); + + SendKick(p, KICK_MSG_CON_FAIL); + return true; + } + + return false; +} + +static void PT_Say(int node) +{ + if (client) + return; // Only sent to servers, why are we receiving this? + + say_pak say = netbuffer->u.say; + + // Check for a spoofed source. + if (say.source == serverplayer) + { + // Servers aren't guaranteed to have a playernode, dedis exist. + if (node != servernode) + return; + } + else + { + if (playernode[say.source] != node) + return; + } + + if ((cv_mute.value || say.flags & (HU_CSAY|HU_SHOUT)) && say.source != serverplayer && !(IsPlayerAdmin(say.source))) + { + CONS_Debug(DBG_NETPLAY,"Received SAY cmd from Player %d (%s), but cv_mute is on.\n", say.source+1, player_names[say.source]); + return; + } + + if ((say.flags & HU_PRIVNOTICE) && !(IsPlayerAdmin(say.source))) + { + CONS_Debug(DBG_NETPLAY,"Received SAY cmd from Player %d (%s) with an illegal HU_PRIVNOTICE flag.\n", say.source+1, player_names[say.source]); + SendKick(say.source, KICK_MSG_CON_FAIL); + return; + } + + { + size_t i; + for (i = 0; i < sizeof say.message && say.message[i]; i++) + { + if (say.message[i] & 0x80) + { + CONS_Alert(CONS_WARNING, M_GetText("Illegal say command received from %s containing invalid characters\n"), player_names[say.source]); + SendKick(say.source, KICK_MSG_CON_FAIL); + return; + } + } + } + + if (stop_spamming[say.source] != 0 && consoleplayer != say.source && cv_chatspamprotection.value && !(say.flags & (HU_CSAY|HU_SHOUT))) + { + CONS_Debug(DBG_NETPLAY,"Received SAY cmd too quickly from Player %d (%s), assuming as spam and blocking message.\n", say.source+1, player_names[say.source]); + stop_spamming[say.source] = 4; + return; + } + + stop_spamming[say.source] = 4; + + serverplayer_t *stats = SV_GetStatsByPlayerIndex(say.source); + + if (stats->finishedrounds < (uint32_t)cv_gamestochat.value && !(consoleplayer == say.source || IsPlayerAdmin(say.source))) + { + CONS_Debug(DBG_NETPLAY,"Received SAY cmd from Player %d (%s), but they aren't permitted to chat yet.\n", say.source+1, player_names[say.source]); + + char rejectmsg[256]; + strlcpy(rejectmsg, va("Please finish in %d more games to use chat.", cv_gamestochat.value - stats->finishedrounds), 256); + if (IsPlayerGuest(say.source)) + strlcpy(rejectmsg, va("GUESTs can't chat on this server. Rejoin with a profile to track your playtime."), 256); + SendServerNotice(say.source, rejectmsg); + + return; + } + + DoSayCommand(say.message, say.target, say.flags, say.source); +} + +static void PT_ReqMapQueue(int node) +{ + if (client) + return; // Only sent to servers, why are we receiving this? + + reqmapqueue_pak reqmapqueue = netbuffer->u.reqmapqueue; + + // Check for a spoofed source. + if (reqmapqueue.source == serverplayer) + { + // Servers aren't guaranteed to have a playernode, dedis exist. + if (node != servernode) + return; + } + else + { + if (playernode[reqmapqueue.source] != node) + return; + + if (!IsPlayerAdmin(reqmapqueue.source)) + { + CONS_Debug(DBG_NETPLAY,"Received illegal request map queue cmd from Player %d (%s).\n", reqmapqueue.source+1, player_names[reqmapqueue.source]); + SendKick(reqmapqueue.source, KICK_MSG_CON_FAIL); + return; + } + } + + const boolean doclear = (reqmapqueue.newgametype == ROUNDQUEUE_CMD_CLEAR); + + // The following prints will only appear when multiple clients + // attempt to affect the round queue at similar time increments + if (doclear == true) + { + if (roundqueue.size == 0) + { + // therefore this one doesn't really need a error print + // because what both players wanted was done anyways + //CONS_Alert(CONS_ERROR, "queuemap: Queue is already empty!\n"); + return; + } + } + else if (reqmapqueue.newgametype == ROUNDQUEUE_CMD_SHOW) + { + char maprevealmsg[256]; + if (roundqueue.size == 0) + { + strlcpy(maprevealmsg, "There are no Rounds queued.", 256); + } + else if (roundqueue.position >= roundqueue.size) + { + strlcpy(maprevealmsg, "There are no more Rounds queued!", 256); + } + else + { + char *title = G_BuildMapTitle(roundqueue.entries[roundqueue.position].mapnum + 1); + + strlcpy( + maprevealmsg, + va("The next Round will be on \"%s\".", title), + 256 + ); + + Z_Free(title); + } + DoSayCommand(maprevealmsg, 0, HU_SHOUT, servernode); + + return; + } + else if (roundqueue.size >= ROUNDQUEUE_MAX) + { + CONS_Alert(CONS_ERROR, "Recieved REQMAPQUEUE, but unable to add map beyond %u\n", roundqueue.size); + + // But this one does, because otherwise it's silent failure! + char rejectmsg[256]; + strlcpy(rejectmsg, "The server couldn't queue your chosen map.", 256); + SendServerNotice(reqmapqueue.source, rejectmsg); + + return; + } + + if (reqmapqueue.newmapnum == NEXTMAP_VOTING) + { + UINT8 numPlayers = 0, i; + for (i = 0; i < MAXPLAYERS; ++i) + { + if (!playeringame[i] || players[i].spectator) + { + continue; + } + + extern consvar_t cv_forcebots; // debug + + if (!(gametypes[reqmapqueue.newgametype]->rules & GTR_BOTS) && players[i].bot && !cv_forcebots.value) + { + // Gametype doesn't support bots + continue; + } + + numPlayers++; + } + + reqmapqueue.newmapnum = G_RandMapPerPlayerCount(G_TOLFlag(reqmapqueue.newgametype), UINT16_MAX, false, false, NULL, numPlayers); + } + + if (reqmapqueue.newmapnum >= nummapheaders) + { + CONS_Alert(CONS_ERROR, "Recieved REQMAPQUEUE, but unable to add map of invalid ID (%u)\n", reqmapqueue.newmapnum); + + char rejectmsg[256]; + strlcpy(rejectmsg, "The server couldn't queue your chosen map.", 256); + SendServerNotice(reqmapqueue.source, rejectmsg); + + return; + } + + G_AddMapToBuffer(reqmapqueue.newmapnum); + + UINT8 buf[1+2+1]; + UINT8 *buf_p = buf; + + WRITEUINT8(buf_p, reqmapqueue.flags); + WRITEUINT16(buf_p, reqmapqueue.newgametype); + + WRITEUINT8(buf_p, roundqueue.size); + + // Match Got_MapQueuecmd, but with the addition of reqmapqueue.newmapnum available to us + if (doclear == true) + { + memset(&roundqueue, 0, sizeof(struct roundqueue)); + } + else + { + G_MapIntoRoundQueue( + reqmapqueue.newmapnum, + reqmapqueue.newgametype, + ((reqmapqueue.flags & 1) != 0), + false + ); + } + + SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf); +} + +static void PT_HandleVoiceClient(SINT8 node, boolean isserver) +{ + if (!isserver && node != servernode) + { + // We should never receive voice packets from anything other than the server + return; + } + + if (dedicated) + { + // don't bother decoding on dedicated + return; + } + + doomdata_t *pak = (doomdata_t*)(doomcom->data); + voice_pak *pl = &pak->u.voice; + + UINT64 framenum = (UINT64)LONGLONG(pl->frame); + INT32 playernum = pl->flags & VOICE_PAK_FLAGS_PLAYERNUM_BITS; + if (playernum >= MAXPLAYERS || playernum < 0) + { + // ignore + return; + } + + boolean terminal = (pl->flags & VOICE_PAK_FLAGS_TERMINAL_BIT) > 0; + UINT32 framesize = doomcom->datalength - BASEPACKETSIZE - sizeof(voice_pak); + UINT8 *frame = (UINT8*)(pl) + sizeof(voice_pak); + + OpusDecoder *decoder = g_player_opus_decoders[playernum]; + if (decoder == NULL) + { + return; + } + float *decoded_out = Z_Malloc(sizeof(float) * SRB2_VOICE_OPUS_FRAME_SIZE, PU_STATIC, NULL); + + INT32 decoded_samples = 0; + UINT64 missedframes = 0; + if (framenum > g_player_opus_lastframe[playernum]) + { + missedframes = min((framenum - g_player_opus_lastframe[playernum]) - 1, 16); + } + + for (UINT64 i = 0; i < missedframes; i++) + { + decoded_samples = opus_decode_float(decoder, NULL, 0, decoded_out, SRB2_VOICE_OPUS_FRAME_SIZE, 0); + if (decoded_samples < 0) + { + continue; + } + if (cv_voice_chat.value != 0 && playernum != g_localplayers[0]) + { + S_QueueVoiceFrameFromPlayer(playernum, (void*)decoded_out, decoded_samples * sizeof(float), false); + } + } + g_player_opus_lastframe[playernum] = framenum; + + decoded_samples = opus_decode_float(decoder, frame, framesize, decoded_out, SRB2_VOICE_OPUS_FRAME_SIZE, 0); + if (decoded_samples < 0) + { + Z_Free(decoded_out); + return; + } + + if (cv_voice_chat.value != 0 && playernum != g_localplayers[0]) + { + S_QueueVoiceFrameFromPlayer(playernum, (void*)decoded_out, decoded_samples * sizeof(float), terminal); + } + S_SetPlayerVoiceActive(playernum); + + Z_Free(decoded_out); +} + +static void PT_HandleVoiceServer(SINT8 node) +{ + // Relay to client nodes except the sender + doomdata_t *pak = (doomdata_t*)(doomcom->data); + voice_pak *pl = &pak->u.voice; + int playernum = -1; + player_t *player; + + if (cv_voice_servermute.value != 0) + { + // Don't even relay voice packets if voice_servermute is on + return; + } + + if ((pl->flags & VOICE_PAK_FLAGS_PLAYERNUM_BITS) > 0 || (pl->flags & VOICE_PAK_FLAGS_RESERVED_BITS) > 0) + { + // All bits except the terminal bit must be unset when sending to client + // Anything else is an illegal message + return; + } + + playernum = nodetoplayer[node]; + if (!(playernum >= 0 && playernum < MAXPLAYERS)) + { + return; + } + player = &players[playernum]; + + if (player->pflags2 & (PF2_SELFMUTE | PF2_SELFDEAFEN | PF2_SERVERMUTE | PF2_SERVERDEAFEN)) + { + // ignore, they should not be able to broadcast voice + return; + } + + // Preserve terminal bit, blank all other bits + pl->flags &= VOICE_PAK_FLAGS_TERMINAL_BIT; + // Add playernum to lower bits + pl->flags |= (playernum & VOICE_PAK_FLAGS_PLAYERNUM_BITS); + + for (int i = 0; i < MAXPLAYERS; i++) + { + UINT8 pnode = playernode[i]; + if (pnode == UINT8_MAX) + { + continue; + } + + // Is this node P1 on that node? + boolean isp1onnode = nodetoplayer[pnode] >= 0 && nodetoplayer[pnode] < MAXPLAYERS; + + if (pnode != node && pnode != servernode && isp1onnode && !(players[i].pflags2 & (PF2_SELFDEAFEN | PF2_SERVERDEAFEN))) + { + HSendPacket(pnode, false, 0, doomcom->datalength - BASEPACKETSIZE); + } + } + PT_HandleVoiceClient(node, true); +} + +static void PT_HandleVoice(SINT8 node) +{ + if (server) + { + PT_HandleVoiceServer(node); + } + else + { + PT_HandleVoiceClient(node, false); + } +} + +static char NodeToSplitPlayer(int node, int split) +{ + if (split == 0) + return nodetoplayer[node]; + else if (split == 1) + return nodetoplayer2[node]; + else if (split == 2) + return nodetoplayer3[node]; + else if (split == 3) + return nodetoplayer4[node]; + return -1; +} + +static void FuzzTiccmd(ticcmd_t* target) +{ + extern consvar_t cv_fuzz; + if (cv_fuzz.value) + { + target->forwardmove = P_RandomRange(PR_FUZZ, -MAXPLMOVE, MAXPLMOVE); + target->turning = P_RandomRange(PR_FUZZ, -KART_FULLTURN, KART_FULLTURN); + target->throwdir = P_RandomRange(PR_FUZZ, -KART_FULLTURN, KART_FULLTURN); + target->buttons = P_RandomRange(PR_FUZZ, 0, 255); + + // Make fuzzed players more likely to do impactful things + if (P_RandomRange(PR_FUZZ, 0, 500)) + { + target->buttons |= BT_ACCELERATE; + target->buttons &= ~BT_LOOKBACK; + target->buttons &= ~BT_RESPAWN; + target->buttons &= ~BT_BRAKE; + } + } +} + +/** Handles a packet received from a node that is in game + * + * \param node The packet sender + * \todo Choose a better name + * \sa HandlePacketFromAwayNode + * \sa GetPackets + * + */ +static void HandlePacketFromPlayer(SINT8 node) +{ + INT32 netconsole; + tic_t realend, realstart; + UINT8 *pak, *txtpak, numtxtpak; +#ifndef NOMD5 + UINT8 finalmd5[16];/* Well, it's the cool thing to do? */ +#endif + + txtpak = NULL; + + if (dedicated && node == 0) + netconsole = 0; + else + netconsole = nodetoplayer[node]; +#ifdef PARANOIA + if (netconsole >= MAXPLAYERS) + I_Error("bad table nodetoplayer: node %d player %d", doomcom->remotenode, netconsole); +#endif + + +#ifdef SIGNGAMETRAFFIC + if (server) + { + + int splitnodes; + if (IsPacketSigned(netbuffer->packettype)) + { + for (splitnodes = 0; splitnodes < MAXSPLITSCREENPLAYERS; splitnodes++) + { + int targetplayer = NodeToSplitPlayer(node, splitnodes); + if (targetplayer == -1) + continue; + + const void* message = &netbuffer->u; + if (IsSplitPlayerOnNodeGuest(node, splitnodes) || demo.playback) + { + //CONS_Printf("Throwing out a guest signature from node %d player %d\n", node, splitnodes); + } + else + { + if (crypto_eddsa_check(netbuffer->signature[splitnodes], players[targetplayer].public_key, message, doomcom->datalength - BASEPACKETSIZE)) + { + CONS_Alert(CONS_ERROR, "SIGFAIL! Packet type %d from node %d player %d\nkey %s size %d netconsole %d\n", + netbuffer->packettype, node, splitnodes, + GetPrettyRRID(players[targetplayer].public_key, true), doomcom->datalength - BASEPACKETSIZE, netconsole); + + // Something scary can happen when multiple kicks that resolve to the same node are processed in quick succession. + // Sometimes, a kick will still be left to process after the player's been disposed, and that causes the kick to resolve on the server instead! + // This sucks, so we check for a stale/misfiring kick beforehand. + if (netconsole != -1) + SendKick(netconsole, KICK_MSG_SIGFAIL); + // Net_CloseConnection(node); + // nodeingame[node] = false; + return; + } + } + + } + } + } +#endif + + switch (netbuffer->packettype) + { +// -------------------------------------------- SERVER RECEIVE ---------- + case PT_CLIENTCMD: + case PT_CLIENT2CMD: + case PT_CLIENT3CMD: + case PT_CLIENT4CMD: + case PT_CLIENTMIS: + case PT_CLIENT2MIS: + case PT_CLIENT3MIS: + case PT_CLIENT4MIS: + case PT_NODEKEEPALIVE: + case PT_NODEKEEPALIVEMIS: + if (client) + break; + + // To save bytes, only the low byte of tic numbers are sent + // Use ExpandTics to figure out what the rest of the bytes are + + realstart = ExpandTics(netbuffer->u.clientpak.client_tic, nettics[node]); + realend = ExpandTics(netbuffer->u.clientpak.resendfrom, nettics[node]); + + if (netbuffer->packettype == PT_CLIENTMIS || netbuffer->packettype == PT_CLIENT2MIS + || netbuffer->packettype == PT_CLIENT3MIS || netbuffer->packettype == PT_CLIENT4MIS + || netbuffer->packettype == PT_NODEKEEPALIVEMIS + || supposedtics[node] < realend) + { + supposedtics[node] = realend; + } + // Discard out of order packet + if (nettics[node] > realend) + { + DEBFILE(va("out of order ticcmd discarded nettics = %u\n", nettics[node])); + break; + } + + // Update the nettics + nettics[node] = realend; + + // This should probably still timeout though, as the node should always have a player 1 number + if (netconsole == -1) + break; + + // As long as clients send valid ticcmds, the server can keep running, so reset the timeout + /// \todo Use a separate cvar for that kind of timeout? + freezetimeout[node] = I_GetTime() + connectiontimeout; + + // Don't do anything for packets of type NODEKEEPALIVE? + // Sryder 2018/07/01: Update the freezetimeout still! + if (netbuffer->packettype == PT_NODEKEEPALIVE + || netbuffer->packettype == PT_NODEKEEPALIVEMIS) + break; + + // If we already received a ticcmd for this tic, just submit it for the next one. + tic_t faketic = maketic; + + if ((!!(netcmds[maketic % BACKUPTICS][netconsole].flags & TICCMD_RECEIVED)) + && (maketic - firstticstosend < BACKUPTICS)) + faketic++; + + FuzzTiccmd(&netbuffer->u.clientpak.cmd); + + // Copy ticcmd + G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][netconsole], &netbuffer->u.clientpak.cmd, 1); + + // Check ticcmd for "speed hacks" + if (CheckForSpeedHacks((UINT8)netconsole)) + break; + + // Splitscreen cmd + if (((netbuffer->packettype == PT_CLIENT2CMD || netbuffer->packettype == PT_CLIENT2MIS) + || (netbuffer->packettype == PT_CLIENT3CMD || netbuffer->packettype == PT_CLIENT3MIS) + || (netbuffer->packettype == PT_CLIENT4CMD || netbuffer->packettype == PT_CLIENT4MIS)) + && (nodetoplayer2[node] >= 0)) + { + FuzzTiccmd(&netbuffer->u.client2pak.cmd2); + G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)nodetoplayer2[node]], + &netbuffer->u.client2pak.cmd2, 1); + + if (CheckForSpeedHacks((UINT8)nodetoplayer2[node])) + break; + } + + if (((netbuffer->packettype == PT_CLIENT3CMD || netbuffer->packettype == PT_CLIENT3MIS) + || (netbuffer->packettype == PT_CLIENT4CMD || netbuffer->packettype == PT_CLIENT4MIS)) + && (nodetoplayer3[node] >= 0)) + { + FuzzTiccmd(&netbuffer->u.client3pak.cmd3); + G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)nodetoplayer3[node]], + &netbuffer->u.client3pak.cmd3, 1); + + if (CheckForSpeedHacks((UINT8)nodetoplayer3[node])) + break; + } + + if ((netbuffer->packettype == PT_CLIENT4CMD || netbuffer->packettype == PT_CLIENT4MIS) + && (nodetoplayer4[node] >= 0)) + { + FuzzTiccmd(&netbuffer->u.client4pak.cmd4); + G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)nodetoplayer4[node]], + &netbuffer->u.client4pak.cmd4, 1); + + if (CheckForSpeedHacks((UINT8)nodetoplayer4[node])) + break; + } + + // Check player consistancy during the level + if (realstart <= gametic && realstart + BACKUPTICS - 1 > gametic && gamestate == GS_LEVEL + && consistancy[realstart%BACKUPTICS] != SHORT(netbuffer->u.clientpak.consistancy) + && !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime() + && !SV_ResendingSavegameToAnyone()) + { + // Tell the client we are about to resend them the gamestate + netbuffer->packettype = PT_WILLRESENDGAMESTATE; + HSendPacket(node, true, 0, 0); + + resendingsavegame[node] = true; + + if (cv_blamecfail.value) + CONS_Printf(M_GetText("Synch failure for player %d (%s); expected %hd, got %hd\n"), + netconsole+1, player_names[netconsole], + consistancy[realstart%BACKUPTICS], + SHORT(netbuffer->u.clientpak.consistancy)); + DEBFILE(va("Restoring player %d (synch failure) [%update] %d!=%d\n", + netconsole, realstart, consistancy[realstart%BACKUPTICS], + SHORT(netbuffer->u.clientpak.consistancy))); + break; + } + break; + case PT_BASICKEEPALIVE: + if (client) + break; + + // This should probably still timeout though, as the node should always have a player 1 number + if (netconsole == -1) + break; + + // If a client sends this it should mean they are done receiving the savegame + sendingsavegame[node] = false; + + // As long as clients send keep alives, the server can keep running, so reset the timeout + /// \todo Use a separate cvar for that kind of timeout? + freezetimeout[node] = I_GetTime() + connectiontimeout; + break; + case PT_TEXTCMD: + case PT_TEXTCMD2: + case PT_TEXTCMD3: + case PT_TEXTCMD4: + if (netbuffer->packettype == PT_TEXTCMD2) // splitscreen special + netconsole = nodetoplayer2[node]; + else if (netbuffer->packettype == PT_TEXTCMD3) + netconsole = nodetoplayer3[node]; + else if (netbuffer->packettype == PT_TEXTCMD4) + netconsole = nodetoplayer4[node]; + + if (client) + break; + + if (netconsole < 0 || netconsole >= MAXPLAYERS) + Net_UnAcknowledgePacket(node); + else + { + size_t j; + tic_t tic = maketic; + UINT8 *textcmd; + + UINT16 incoming_size; + + { + UINT8 *incoming = netbuffer->u.textcmd; + + incoming_size = READUINT16(incoming); + } + + // ignore if the textcmd has a reported size of zero + // this shouldn't be sent at all + if (!incoming_size) + { + DEBFILE(va("GetPacket: Textcmd with size 0 detected! (node %u, player %d)\n", + node, netconsole)); + Net_UnAcknowledgePacket(node); + break; + } + + // ignore if the textcmd size var is actually larger than it should be + // BASEPACKETSIZE + 2 (for size) + textcmd[0] should == datalength + if (incoming_size > (size_t)doomcom->datalength-BASEPACKETSIZE-2) + { + DEBFILE(va("GetPacket: Bad Textcmd packet size! (expected %d, actual %s, node %u, player %d)\n", + incoming_size, sizeu1((size_t)doomcom->datalength-BASEPACKETSIZE-2), + node, netconsole)); + Net_UnAcknowledgePacket(node); + break; + } + + // check if tic that we are making isn't too large else we cannot send it :( + // doomcom->numslots+1 "+1" since doomcom->numslots can change within this time and sent time + j = software_MAXPACKETLENGTH + - (incoming_size + 3 + BASESERVERTICSSIZE + + (doomcom->numslots+1)*sizeof(ticcmd_t)); + + // search a tic that have enougth space in the ticcmd + while ((textcmd = D_GetExistingTextcmd(tic, netconsole)), + (TotalTextCmdPerTic(tic) > j || incoming_size + (textcmd ? ((UINT16*)textcmd)[0] : 0) > MAXTEXTCMD) + && tic < firstticstosend + BACKUPTICS) + tic++; + + if (tic >= firstticstosend + BACKUPTICS) + { + DEBFILE(va("GetPacket: Textcmd too long (max %s, used %s, mak %d, " + "tosend %u, node %u, player %d)\n", sizeu1(j), sizeu2(TotalTextCmdPerTic(maketic)), + maketic, firstticstosend, node, netconsole)); + Net_UnAcknowledgePacket(node); + break; + } + + // Make sure we have a buffer + if (!textcmd) textcmd = D_GetTextcmd(tic, netconsole); + + DEBFILE(va("textcmd put in tic %u at position %d (player %d) ftts %u mk %u\n", + tic, ((UINT16*)textcmd)[0]+2, netconsole, firstticstosend, maketic)); + + M_Memcpy(&textcmd[((UINT16*)textcmd)[0]+2], netbuffer->u.textcmd+2, incoming_size); + ((UINT16*)textcmd)[0] += incoming_size; + } + break; + case PT_SAY: + PT_Say(node); + break; + case PT_REQMAPQUEUE: + PT_ReqMapQueue(node); + break; + case PT_LOGIN: + if (client) + break; +#ifndef NOMD5 + if (doomcom->datalength < 16)/* ignore partial sends */ + break; + + if (!adminpasswordset) + { + CONS_Printf(M_GetText("Password from %s failed (no password set).\n"), player_names[netconsole]); + break; + } + + // Do the final pass to compare with the sent md5 + D_MD5PasswordPass(adminpassmd5, 16, va("PNUM%02d", netconsole), &finalmd5); + + if (!memcmp(netbuffer->u.md5sum, finalmd5, 16)) + { + CONS_Printf(M_GetText("%s passed authentication.\n"), player_names[netconsole]); + COM_BufInsertText(va("promote %d\n", netconsole)); // do this immediately + } + else + CONS_Printf(M_GetText("Password from %s failed.\n"), player_names[netconsole]); +#endif + break; + case PT_NODETIMEOUT: + case PT_CLIENTQUIT: + if (client) + break; + + // nodeingame will be put false in the execution of kick command + // this allow to send some packets to the quitting client to have their ack back + nodewaiting[node] = 0; + if (netconsole != -1 && playeringame[netconsole]) + { + UINT8 kickmsg; + + if (netbuffer->packettype == PT_NODETIMEOUT) + kickmsg = KICK_MSG_TIMEOUT; + else + kickmsg = KICK_MSG_PLAYER_QUIT; + + SendKick(netconsole, kickmsg); + + /* + nodetoplayer[node] = -1; + + if (nodetoplayer2[node] != -1 && nodetoplayer2[node] >= 0 + && playeringame[(UINT8)nodetoplayer2[node]]) + { + SendKick(nodetoplayer2[node], kickmsg); + nodetoplayer2[node] = -1; + } + + if (nodetoplayer3[node] != -1 && nodetoplayer3[node] >= 0 + && playeringame[(UINT8)nodetoplayer3[node]]) + { + SendKick(nodetoplayer3[node], kickmsg); + nodetoplayer3[node] = -1; + } + + if (nodetoplayer4[node] != -1 && nodetoplayer4[node] >= 0 + && playeringame[(UINT8)nodetoplayer4[node]]) + { + SendKick(nodetoplayer4[node], kickmsg); + nodetoplayer4[node] = -1; + } + */ + } + Net_CloseConnection(node); + nodeingame[node] = false; + nodeneedsauth[node] = false; + break; + case PT_CANRECEIVEGAMESTATE: + PT_CanReceiveGamestate(node); + break; + case PT_ASKLUAFILE: + if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_ASKED) + AddLuaFileToSendQueue(node, luafiletransfers->realfilename); + break; + case PT_HASLUAFILE: + if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_SENDING) + SV_HandleLuaFileSent(node); + break; + case PT_RECEIVEDGAMESTATE: + sendingsavegame[node] = false; + resendingsavegame[node] = false; + savegameresendcooldown[node] = I_GetTime() + 5 * TICRATE; + break; +// -------------------------------------------- CLIENT RECEIVE ---------- + case PT_SERVERTICS: + // Only accept PT_SERVERTICS from the server. + if (node != servernode) + { + CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_SERVERTICS", node); + if (server) + SendKick(netconsole, KICK_MSG_CON_FAIL); + break; + } + + realstart = ExpandTics(netbuffer->u.serverpak.starttic, maketic); + realend = realstart + netbuffer->u.serverpak.numtics; + + if (!txtpak) + txtpak = (UINT8 *)&netbuffer->u.serverpak.cmds[netbuffer->u.serverpak.numslots + * netbuffer->u.serverpak.numtics]; + + if (realend > gametic + CLIENTBACKUPTICS) + realend = gametic + CLIENTBACKUPTICS; + cl_packetmissed = realstart > neededtic; + + if (realstart <= neededtic && realend > neededtic) + { + tic_t i, j; + pak = (UINT8 *)&netbuffer->u.serverpak.cmds; + + for (i = realstart; i < realend; i++) + { + // clear first + D_Clearticcmd(i); + + // copy the tics + pak = G_ScpyTiccmd(netcmds[i%BACKUPTICS], pak, + netbuffer->u.serverpak.numslots*sizeof (ticcmd_t)); + + // copy the textcmds + numtxtpak = *txtpak++; + for (j = 0; j < numtxtpak; j++) + { + INT32 k = *txtpak++; // playernum + const size_t txtsize = ((UINT16*)txtpak)[0]+2; + + if (i >= gametic) // Don't copy old net commands + M_Memcpy(D_GetTextcmd(i, k), txtpak, txtsize); + txtpak += txtsize; + } + } + + neededtic = realend; + } + else + { + DEBFILE(va("frame not in bound: %u\n", neededtic)); + /*if (realend < neededtic - 2 * TICRATE || neededtic + 2 * TICRATE < realstart) + I_Error("Received an out of order PT_SERVERTICS packet!\n" + "Got tics %d-%d, needed tic %d\n\n" + "Please report this crash on the Master Board,\n" + "IRC or Discord so it can be fixed.\n", (INT32)realstart, (INT32)realend, (INT32)neededtic);*/ + } + break; + case PT_PING: + // Only accept PT_PING from the server. + if (node != servernode) + { + CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_PING", node); + if (server) + SendKick(netconsole, KICK_MSG_CON_FAIL); + break; + } + + //Update client ping table from the server. + if (client) + { + UINT8 i; + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i]) + { + playerpingtable[i] = (tic_t)netbuffer->u.netinfo.pingtable[i]; + playerpacketlosstable[i] = netbuffer->u.netinfo.packetloss[i]; + playerdelaytable[i] = netbuffer->u.netinfo.delay[i]; + } + } + + servermaxping = (tic_t)netbuffer->u.netinfo.pingtable[MAXPLAYERS]; + } + + break; + case PT_SERVERCFG: + break; + case PT_FILEFRAGMENT: + // Only accept PT_FILEFRAGMENT from the server. + if (node != servernode) + { + CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_FILEFRAGMENT", node); + if (server) + SendKick(netconsole, KICK_MSG_CON_FAIL); + break; + } + if (client) + PT_FileFragment(); + break; + case PT_FILEACK: + if (server) + PT_FileAck(); + break; + case PT_FILERECEIVED: + if (server) + PT_FileReceived(); + break; + case PT_WILLRESENDGAMESTATE: + PT_WillResendGamestate(); + break; + case PT_SENDINGLUAFILE: + if (client) + CL_PrepareDownloadLuaFile(); + break; + case PT_CHALLENGEALL: + if (demo.playback || node != servernode) // SERVER should still respond to this to prove its own identity, just not from clients. + break; + + int challengeplayers; + + memcpy(lastChallengeAll, netbuffer->u.challengeall.secret, sizeof(lastChallengeAll)); + + shouldsign_t safe = ShouldSignChallenge(lastChallengeAll); + if (safe != SIGN_OK) + { + if (safe == SIGN_BADIP) + HandleSigfail("External server sent the wrong IP"); + else if (safe == SIGN_BADTIME) + HandleSigfail("Bad timestamp - is your time set correctly?"); + else + HandleSigfail("Unknown auth error - contact a developer"); + break; + } + + netbuffer->packettype = PT_RESPONSEALL; + + #ifdef DEVELOP + if (cv_noresponse.value) + { + CV_AddValue(&cv_noresponse, -1); + CONS_Alert(CONS_WARNING, "cv_noresponse enabled, not sending PT_RESPONSEALL\n"); + break; + } + #endif + + // Don't leak uninitialized memory. + memset(&netbuffer->u.responseall, 0, sizeof(netbuffer->u.responseall)); + + for (challengeplayers = 0; challengeplayers <= splitscreen; challengeplayers++) + { + uint8_t signature[SIGNATURELENGTH]; + profile_t *localProfile = PR_GetLocalPlayerProfile(challengeplayers); + if (!PR_IsLocalPlayerGuest(challengeplayers)) // GUESTS don't have keys + { + crypto_eddsa_sign(signature, localProfile->secret_key, lastChallengeAll, sizeof(lastChallengeAll)); + + // If our keys are garbage (corrupted profile?), fail here instead of when the server boots us, so the player knows what's going on. + if (crypto_eddsa_check(signature, localProfile->public_key, lastChallengeAll, sizeof(lastChallengeAll)) != 0) + I_Error("Couldn't self-verify key associated with player %d, profile %d.\nProfile data may be corrupted.", challengeplayers, cv_lastprofile[challengeplayers].value); + } + + #ifdef DEVELOP + if (cv_badresponse.value) + { + CV_AddValue(&cv_badresponse, -1); + CONS_Alert(CONS_WARNING, "cv_badresponse enabled, scrubbing signature from PT_RESPONSEALL\n"); + memset(signature, 0, sizeof(signature)); + } + #endif + + memcpy(netbuffer->u.responseall.signature[challengeplayers], signature, sizeof(signature)); + } + + HSendPacket(servernode, true, 0, sizeof(netbuffer->u.responseall)); + break; + case PT_RESPONSEALL: + if (demo.playback || client) + break; + + int responseplayer; + for (responseplayer = 0; responseplayer < MAXSPLITSCREENPLAYERS; responseplayer++) + { + int targetplayer = NodeToSplitPlayer(node, responseplayer); + if (targetplayer == -1) + continue; + + if (!IsPlayerGuest(targetplayer)) + { + if (crypto_eddsa_check(netbuffer->u.responseall.signature[responseplayer], players[targetplayer].public_key, lastChallengeAll, sizeof(lastChallengeAll))) + { + // Something scary can happen when multiple kicks that resolve to the same node are processed in quick succession. + // Sometimes, a kick will still be left to process after the player's been disposed, and that causes the kick to resolve on the server instead! + // This sucks, so we check for a stale/misfiring kick beforehand. + if (playernode[targetplayer] != 0) + SendKick(targetplayer, KICK_MSG_SIGFAIL); + break; + } + else + { + memcpy(lastReceivedSignature[targetplayer], netbuffer->u.responseall.signature[responseplayer], sizeof(lastReceivedSignature[targetplayer])); + } + } + } + break; + case PT_RESULTSALL: + if (demo.playback || server || node != servernode || !expectChallenge) + break; + + int resultsplayer; + uint8_t allZero[PUBKEYLENGTH]; + memset(allZero, 0, sizeof(PUBKEYLENGTH)); + + for (resultsplayer = 0; resultsplayer < MAXPLAYERS; resultsplayer++) + { + if (!playeringame[resultsplayer]) + { + continue; + } + else if (IsPlayerGuest(resultsplayer)) + { + continue; + } + else if (memcmp(knownWhenChallenged[resultsplayer], allZero, sizeof(PUBKEYLENGTH)) == 0) + { + // Wasn't here for the challenge. + continue; + } + else if (memcmp(knownWhenChallenged[resultsplayer], players[resultsplayer].public_key, sizeof(knownWhenChallenged[resultsplayer])) != 0) + { + // A player left after the challenge process started, and someone else took their place. + // That means they haven't received a challenge either. + continue; + } + else + { + if (crypto_eddsa_check(netbuffer->u.resultsall.signature[resultsplayer], + knownWhenChallenged[resultsplayer], lastChallengeAll, sizeof(lastChallengeAll))) + { + CONS_Alert(CONS_WARNING, "PT_RESULTSALL had invalid signature %s for node %d player %d split %d, something doesn't add up!\n", + GetPrettyRRID(netbuffer->u.resultsall.signature[resultsplayer], true), playernode[resultsplayer], resultsplayer, players[resultsplayer].splitscreenindex); + HandleSigfail("Server sent invalid client signature."); + break; + } + } + } + csprng(lastChallengeAll, sizeof(lastChallengeAll)); + expectChallenge = false; + break; + case PT_VOICE: + PT_HandleVoice(node); + break; + default: + DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n", + netbuffer->packettype, node)); + } // end switch +} + +/** Handles all received packets, if any + * + * \todo Add details to this description (lol) + * + */ +static void GetPackets(void) +{ + SINT8 node; // The packet sender + + player_joining = false; + + while (HGetPacket()) + { + node = (SINT8)doomcom->remotenode; + + if (netbuffer->packettype == PT_CLIENTJOIN && server) + { + if (levelloading == false) // Otherwise just ignore + { + HandleConnect(node); + } + continue; + } + if (node == servernode && client && cl_mode != CL_SEARCHING) + { + if (netbuffer->packettype == PT_SERVERSHUTDOWN) + { + HandleShutdown(node); + continue; + } + if (netbuffer->packettype == PT_NODETIMEOUT) + { + HandleTimeout(node); + continue; + } + } + + if (netbuffer->packettype == PT_SERVERINFO) + { + HandleServerInfo(node); + continue; + } + + if (netbuffer->packettype == PT_PLAYERINFO) + continue; // We do nothing with PLAYERINFO, that's for the MS browser. + + // Packet received from someone already playing + if (nodeingame[node]) + HandlePacketFromPlayer(node); + // Packet received from someone not playing + else + HandlePacketFromAwayNode(node); + } +} + +// +// NetUpdate +// Builds ticcmds for console player, +// sends out a packet +// +// no more use random generator, because at very first tic isn't yet synchronized +// Note: It is called consistAncy on purpose. +// +static INT16 Consistancy(void) +{ + INT32 i; + UINT32 ret = 0; +#ifdef MOBJCONSISTANCY + thinker_t *th; + mobj_t *mo; +#endif + + DEBFILE(va("TIC %u ", gametic)); + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i]) + ret ^= 0xCCCC; + else if (!players[i].mo || gamestate != GS_LEVEL); + else + { + ret += players[i].mo->x; + ret -= players[i].mo->y; + ret += players[i].itemtype; + ret *= i+1; + } + } + // I give up + // Coop desynching enemies is painful + if (gamestate == GS_LEVEL) + { + for (i = 0; i < PRNUMSYNCED; i++) + { + if (i & 1) + { + ret -= P_GetRandSeed(i); + } + else + { + ret += P_GetRandSeed(i); + } + } + } + +#ifdef MOBJCONSISTANCY + if (gamestate == GS_LEVEL) + { + for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next) + { + if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed) + continue; + + mo = (mobj_t *)th; + + if (TypeIsNetSynced(mo->type) == false) + continue; + + if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_ELEMENTAL | MF_FIRE | MF_ENEMY | MF_PAIN | MF_DONTPUNT)) + { + ret -= mo->type; + ret += mo->x; + ret -= mo->y; + ret += mo->z; + ret -= mo->momx; + ret += mo->momy; + ret -= mo->momz; + ret += mo->angle; + ret -= mo->flags; + ret += mo->flags2; + ret -= mo->eflags; + if (mo->target && TypeIsNetSynced(mo->target->type)) + { + ret += mo->target->type; + ret -= mo->target->x; + ret += mo->target->y; + ret -= mo->target->z; + ret += mo->target->momx; + ret -= mo->target->momy; + ret += mo->target->momz; + ret -= mo->target->angle; + ret += mo->target->flags; + ret -= mo->target->flags2; + ret += mo->target->eflags; + ret -= mo->target->state - states; + ret += mo->target->tics; + ret -= mo->target->sprite; + //ret += mo->target->frame; + } + else + ret ^= 0x3333; + if (mo->tracer && TypeIsNetSynced(mo->tracer->type)) + { + ret += mo->tracer->type; + ret -= mo->tracer->x; + ret += mo->tracer->y; + ret -= mo->tracer->z; + ret += mo->tracer->momx; + ret -= mo->tracer->momy; + ret += mo->tracer->momz; + ret -= mo->tracer->angle; + ret += mo->tracer->flags; + ret -= mo->tracer->flags2; + ret += mo->tracer->eflags; + ret -= mo->tracer->state - states; + ret += mo->tracer->tics; + ret -= mo->tracer->sprite; + //ret += mo->tracer->frame; + } + else + ret ^= 0xAAAA; + // SRB2Kart: We use hnext & hprev very extensively + if (mo->hnext && TypeIsNetSynced(mo->hnext->type)) + { + ret += mo->hnext->type; + ret -= mo->hnext->x; + ret += mo->hnext->y; + ret -= mo->hnext->z; + ret += mo->hnext->momx; + ret -= mo->hnext->momy; + ret += mo->hnext->momz; + ret -= mo->hnext->angle; + ret += mo->hnext->flags; + ret -= mo->hnext->flags2; + ret += mo->hnext->eflags; + ret -= mo->hnext->state - states; + ret += mo->hnext->tics; + ret -= mo->hnext->sprite; + //ret += mo->hnext->frame; + } + else + ret ^= 0x5555; + if (mo->hprev && TypeIsNetSynced(mo->hprev->type)) + { + ret += mo->hprev->type; + ret -= mo->hprev->x; + ret += mo->hprev->y; + ret -= mo->hprev->z; + ret += mo->hprev->momx; + ret -= mo->hprev->momy; + ret += mo->hprev->momz; + ret -= mo->hprev->angle; + ret += mo->hprev->flags; + ret -= mo->hprev->flags2; + ret += mo->hprev->eflags; + ret -= mo->hprev->state - states; + ret += mo->hprev->tics; + ret -= mo->hprev->sprite; + //ret += mo->hprev->frame; + } + else + ret ^= 0xCCCC; + ret -= mo->state - states; + ret += mo->tics; + ret -= mo->sprite; + //ret += mo->frame; + } + } + } +#endif + + DEBFILE(va("Consistancy = %u\n", (ret & 0xFFFF))); + + return (INT16)(ret & 0xFFFF); +} + +// confusing, but this DOESN'T send PT_NODEKEEPALIVE, it sends PT_BASICKEEPALIVE +// used during wipes to tell the server that a node is still connected +static void CL_SendClientKeepAlive(void) +{ + netbuffer->packettype = PT_BASICKEEPALIVE; + + HSendPacket(servernode, false, 0, 0); +} + +static void SV_SendServerKeepAlive(void) +{ + INT32 n; + + for (n = 1; n < MAXNETNODES; n++) + { + if (nodeingame[n]) + { + netbuffer->packettype = PT_BASICKEEPALIVE; + HSendPacket(n, false, 0, 0); + } + } +} + +// send the client packet to the server +static void CL_SendClientCmd(void) +{ + size_t packetsize = 0; + boolean mis = false; + + netbuffer->packettype = PT_CLIENTCMD; + + if (cl_packetmissed) + { + netbuffer->packettype = PT_CLIENTMIS; + mis = true; + } + + netbuffer->u.clientpak.resendfrom = (UINT8)(neededtic & UINT8_MAX); + netbuffer->u.clientpak.client_tic = (UINT8)(gametic & UINT8_MAX); + + if (gamestate == GS_WAITINGPLAYERS) + { + // Send PT_NODEKEEPALIVE packet + netbuffer->packettype = (mis ? PT_NODEKEEPALIVEMIS : PT_NODEKEEPALIVE); + packetsize = sizeof (clientcmd_pak) - sizeof (ticcmd_t) - sizeof (INT16); + HSendPacket(servernode, false, 0, packetsize); + } + else if (gamestate != GS_NULL && (addedtogame || dedicated)) + { + UINT8 lagDelay = 0; + + if (lowest_lag > 0) + { + // Gentlemens' ping. + lagDelay = min(lowest_lag, MAXGENTLEMENDELAY); + + // Is our connection worse than our current gentleman point? + // Make sure it stays that way for a bit before increasing delay levels. + if (lagDelay > reference_lag) + { + spike_time++; + if (spike_time >= GENTLEMANSMOOTHING) + { + // Okay, this is genuinely the new baseline delay. + reference_lag = lagDelay; + spike_time = 0; + } + else + { + // Just a temporary fluctuation, ignore it. + lagDelay = reference_lag; + } + } + else + { + reference_lag = lagDelay; // Adjust quickly if the connection improves. + spike_time = 0; + } + + /* + if (server) // Clients have to wait for the gamestate to make it back. Servers don't! + lagDelay *= 2; // Simulate the HELLFUCK NIGHTMARE of a complete round trip. + */ + + // [deep breath in] + // Plausible, elegant explanation that is WRONG AND SUPER HARMFUL. + // Clients with stable connections were adding their mindelay to network delay, + // even when their mindelay was as high or higher than network delay—which made + // client delay APPEAR slower than host mindelay, by the exact value that made + // "lmao just double it" make sense at the time. + // + // While this fix made client connections match server mindelay in our most common + // test environment, it also masked an issue that seriously affected online handling + // responsiveness, completely ruining our opportunity to further investigate it! + // + // See UpdatePingTable. + // I am taking this shitty code to my grave as an example of "never trust your brain". + // -Tyron 2024-05-15 + + } + + packetsize = sizeof (clientcmd_pak); + G_MoveTiccmd(&netbuffer->u.clientpak.cmd, &localcmds[0][lagDelay], 1); + netbuffer->u.clientpak.consistancy = SHORT(consistancy[gametic % BACKUPTICS]); + + if (splitscreen) // Send a special packet with 2 cmd for splitscreen + { + netbuffer->packettype = (mis ? PT_CLIENT2MIS : PT_CLIENT2CMD); + packetsize = sizeof (client2cmd_pak); + G_MoveTiccmd(&netbuffer->u.client2pak.cmd2, &localcmds[1][lagDelay], 1); + + if (splitscreen > 1) + { + netbuffer->packettype = (mis ? PT_CLIENT3MIS : PT_CLIENT3CMD); + packetsize = sizeof (client3cmd_pak); + G_MoveTiccmd(&netbuffer->u.client3pak.cmd3, &localcmds[2][lagDelay], 1); + + if (splitscreen > 2) + { + netbuffer->packettype = (mis ? PT_CLIENT4MIS : PT_CLIENT4CMD); + packetsize = sizeof (client4cmd_pak); + G_MoveTiccmd(&netbuffer->u.client4pak.cmd4, &localcmds[3][lagDelay], 1); + } + } + } + + HSendPacket(servernode, false, 0, packetsize); + } + + if (cl_mode == CL_CONNECTED || dedicated) + { + UINT8 i; + // Send extra data if needed + for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) + { + if (((UINT16*)localtextcmd[i])[0]) + { + switch (i) + { + case 3: + netbuffer->packettype = PT_TEXTCMD4; + break; + case 2: + netbuffer->packettype = PT_TEXTCMD3; + break; + case 1: + netbuffer->packettype = PT_TEXTCMD2; + break; + default: + netbuffer->packettype = PT_TEXTCMD; + break; + } + + M_Memcpy(netbuffer->u.textcmd, localtextcmd[i], ((UINT16*)localtextcmd[i])[0]+2); + // All extra data have been sent + if (HSendPacket(servernode, true, 0, ((UINT16*)localtextcmd[i])[0]+2)) // Send can fail... + ((UINT16*)localtextcmd[i])[0] = 0; + } + } + } +} + +// send the server packet +// send tic from firstticstosend to maketic-1 +static void SV_SendTics(void) +{ + tic_t realfirsttic, lasttictosend, i; + UINT32 n; + INT32 j; + size_t packsize; + UINT8 *bufpos; + UINT8 *ntextcmd; + + // send to all client but not to me + // for each node create a packet with x tics and send it + // x is computed using supposedtics[n], max packet size and maketic + for (n = 1; n < MAXNETNODES; n++) + if (nodeingame[n]) + { + // assert supposedtics[n]>=nettics[n] + realfirsttic = supposedtics[n]; + + lasttictosend = nettics[n] + CLIENTBACKUPTICS; + if (lasttictosend > maketic) + lasttictosend = maketic; + + if (realfirsttic >= lasttictosend) + { + // well we have sent all tics we will so use extrabandwidth + // to resent packet that are supposed lost (this is necessary since lost + // packet detection work when we have received packet with firsttic > neededtic + // (getpacket servertics case) + DEBFILE(va("Nothing to send node %u mak=%u sup=%u net=%u \n", + n, lasttictosend, supposedtics[n], nettics[n])); + realfirsttic = nettics[n]; + if (realfirsttic >= lasttictosend || (I_GetTime() + n)&3) + // all tic are ok + continue; + DEBFILE(va("Sent %d anyway\n", realfirsttic)); + } + if (realfirsttic < firstticstosend) + realfirsttic = firstticstosend; + + // compute the length of the packet and cut it if too large + packsize = BASESERVERTICSSIZE; + for (i = realfirsttic; i < lasttictosend; i++) + { + packsize += sizeof (ticcmd_t) * doomcom->numslots; + packsize += TotalTextCmdPerTic(i); + + if (packsize > software_MAXPACKETLENGTH) + { + DEBFILE(va("packet too large (%s) at tic %d (should be from %d to %d)\n", + sizeu1(packsize), i, realfirsttic, lasttictosend)); + lasttictosend = i; + + // too bad: too much player have send extradata and there is too + // much data in one tic. + // To avoid it put the data on the next tic. (see getpacket + // textcmd case) but when numplayer changes the computation can be different + if (lasttictosend == realfirsttic) + { + if (packsize > MAXPACKETLENGTH) + I_Error("Too many players: can't send %s data for %d players to node %d\n" + "Well sorry nobody is perfect....\n", + sizeu1(packsize), doomcom->numslots, n); + else + { + lasttictosend++; // send it anyway! + DEBFILE("sending it anyway\n"); + } + } + break; + } + } + + // Send the tics + netbuffer->packettype = PT_SERVERTICS; + netbuffer->u.serverpak.starttic = (UINT8)realfirsttic; + netbuffer->u.serverpak.numtics = (UINT8)(lasttictosend - realfirsttic); + netbuffer->u.serverpak.numslots = (UINT8)SHORT(doomcom->numslots); + bufpos = (UINT8 *)&netbuffer->u.serverpak.cmds; + + for (i = realfirsttic; i < lasttictosend; i++) + { + bufpos = G_DcpyTiccmd(bufpos, netcmds[i%BACKUPTICS], doomcom->numslots * sizeof (ticcmd_t)); + } + + // add textcmds + for (i = realfirsttic; i < lasttictosend; i++) + { + ntextcmd = bufpos++; + *ntextcmd = 0; + for (j = 0; j < MAXPLAYERS; j++) + { + UINT8 *textcmd = D_GetExistingTextcmd(i, j); + INT32 size = textcmd ? ((UINT16*)textcmd)[0] : 0; + + if ((!j || playeringame[j]) && size) + { + (*ntextcmd)++; + WRITEUINT8(bufpos, j); + WRITEUINT16(bufpos, ((UINT16*)textcmd)[0]); + WRITEMEM(bufpos, &textcmd[2], size); + } + } + } + packsize = bufpos - (UINT8 *)&(netbuffer->u); + + HSendPacket(n, false, 0, packsize); + // when tic are too large, only one tic is sent so don't go backward! + if (lasttictosend-doomcom->extratics > realfirsttic) + supposedtics[n] = lasttictosend-doomcom->extratics; + else + supposedtics[n] = lasttictosend; + if (supposedtics[n] < nettics[n]) supposedtics[n] = nettics[n]; + } + // node 0 is me! + supposedtics[0] = maketic; +} + +// +// TryRunTics +// +static void CreateNewLocalCMD(UINT8 p, INT32 realtics) +{ + INT32 i; + + for (i = MAXGENTLEMENDELAY-1; i > 0; i--) + { + G_MoveTiccmd(&localcmds[p][i], &localcmds[p][i-1], 1); + } + + G_BuildTiccmd(&localcmds[p][0], realtics, p+1); + localcmds[p][0].flags |= TICCMD_RECEIVED; +} + +static void Local_Maketic(INT32 realtics) +{ + INT32 i; + + I_OsPolling(); // I_Getevent + D_ProcessEvents(true); // menu responder, cons responder, + // game responder calls HU_Responder, AM_Responder, + // and G_MapEventsToControls + + if (!dedicated) rendergametic = gametic; + + // translate inputs (keyboard/mouse/joystick) into game controls + for (i = 0; i <= splitscreen; i++) + { + CreateNewLocalCMD(i, realtics); + } +} + +// create missed tic +static void SV_Maketic(void) +{ + INT32 i; + + PS_ResetBotInfo(); + + for (i = 0; i < MAXPLAYERS; i++) + { + packetloss[i][maketic%PACKETMEASUREWINDOW] = false; + + if (!playeringame[i]) + continue; + + if (K_PlayerUsesBotMovement(&players[i])) + { + const precise_t t = I_GetPreciseTime(); + + K_BuildBotTiccmd(&players[i], &netcmds[maketic%BACKUPTICS][i]); + + ps_bots[i].isBot = true; + ps_bots[i].total = I_GetPreciseTime() - t; + ps_botticcmd_time += ps_bots[i].total; + continue; + } + + // We didn't receive this tic + if ((netcmds[maketic % BACKUPTICS][i].flags & TICCMD_RECEIVED) == 0) + { + ticcmd_t * ticcmd = &netcmds[(maketic ) % BACKUPTICS][i]; + ticcmd_t *prevticcmd = &netcmds[(maketic - 1) % BACKUPTICS][i]; + + { + DEBFILE(va("MISS tic%4d for player %d\n", maketic, i)); + // Copy the input from the previous tic + *ticcmd = *prevticcmd; + ticcmd->flags &= ~TICCMD_RECEIVED; + } + + // packetloss[i][leveltime%PACKETMEASUREWINDOW] = (cmd->flags & TICCMD_RECEIVED) ? false : true; + packetloss[i][maketic%PACKETMEASUREWINDOW] = true; + } + } + + // all tic are now proceed make the next + maketic++; +} + +boolean TryRunTics(tic_t realtics) +{ + boolean ticking; + + // the machine has lagged but it is not so bad + if (realtics > TICRATE/7) // FIXME: consistency failure!! + { + if (server) + realtics = 1; + else + realtics = TICRATE/7; + } + + if (singletics) + realtics = 1; + + if (realtics >= 1) + { + COM_BufTicker(); + if (mapchangepending) + D_MapChange(-1, 0, encoremode, false, 2, false, forcespecialstage); // finish the map change + } + + NetUpdate(); + + if (demo.playback) + { + neededtic = gametic + realtics; + // start a game after a demo + maketic += realtics; + firstticstosend = maketic; + tictoclear = firstticstosend; + } + + GetPackets(); + +#ifdef DEBUGFILE + if (debugfile && (realtics || neededtic > gametic)) + { + //SoM: 3/30/2000: Need long INT32 in the format string for args 4 & 5. + //Shut up stupid warning! + fprintf(debugfile, "------------ Tryruntic: REAL:%d NEED:%d GAME:%d LOAD: %d\n", + realtics, neededtic, gametic, debugload); + debugload = 100000; + } +#endif + + ticking = neededtic > gametic; + + if (ticking) + { + if (realtics) + hu_stopped = false; + } + + if (player_joining) + { + if (realtics) + hu_stopped = true; + return false; + } + + if (ticking) + { + boolean tickInterp = true; + + // run the count * tics + while (neededtic > gametic) + { + boolean dontRun = false; + + DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic)); + + ps_prevtictime = ps_tictime; + ps_tictime = I_GetPreciseTime(); + + dontRun = ExtraDataTicker(); + + if (levelloading == false + || gametic > levelstarttic + 5) // Don't lock-up if a malicious client is sending tons of netxcmds + { + // During level load, we want to pause + // execution until we've finished loading + // all of the netxcmds in our buffer. + dontRun = false; + } + + if (dontRun == false) + { + if (levelloading == true) + { + P_PostLoadLevel(); + } + + boolean run = (gametic % NEWTICRATERATIO) == 0; + + if (run && tickInterp) + { + // Update old view state BEFORE ticking so resetting + // the old interpolation state from game logic works. + R_UpdateViewInterpolation(); + tickInterp = false; // do not update again in sped-up tics + } + + G_Ticker(run); + } + + if (Playing() && netgame && (gametic % TICRATE == 0)) + { + Schedule_Run(); + + if (cv_livestudioaudience.value) + { + LiveStudioAudience(); + } + } + + gametic++; + consistancy[gametic % BACKUPTICS] = Consistancy(); + + ps_tictime = I_GetPreciseTime() - ps_tictime; + + // Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame. + if (client && gamestate == GS_LEVEL && leveltime > 1 && neededtic <= gametic + cv_netticbuffer.value) + { + break; + } + } + + if (F_IsDeferredContinueCredits()) + { + F_ContinueCredits(); + } + + if (D_IsDeferredStartTitle()) + { + D_StartTitle(); + } + } + else + { + if (realtics) + hu_stopped = true; + } + + return ticking; +} + + +/* Ping Update except better: + We call this once per second and check for people's pings. If their ping happens to be too high, we increment some timer and kick them out. + If they're not lagging, decrement the timer by 1. Of course, reset all of this if they leave. +*/ + +static INT32 pingtimeout[MAXPLAYERS]; + +static inline void PingUpdate(void) +{ + INT32 i, j; + boolean pingkick[MAXPLAYERS]; + UINT8 nonlaggers = 0; + memset(pingkick, 0, sizeof(pingkick)); + + netbuffer->packettype = PT_PING; + + //check for ping limit breakage. + if (cv_maxping.value) + { + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || P_IsMachineLocalPlayer(&players[i])) + { + pingtimeout[i] = 0; + continue; + } + + if ((cv_maxping.value) + && (realpingtable[i] / pingmeasurecount > (unsigned)cv_maxping.value)) + { + if (players[i].jointime > 10 * TICRATE) + { + pingkick[i] = true; + } + } + else + { + nonlaggers++; + + // you aren't lagging, but you aren't free yet. In case you'll keep spiking, we just make the timer go back down. (Very unstable net must still get kicked). + if (pingtimeout[i] > 0) + pingtimeout[i]--; + } + } + + //kick lagging players... unless everyone but the server's ping sucks. + //in that case, it is probably the server's fault. + if (nonlaggers > 0) + { + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || !pingkick[i]) + continue; + + // Don't kick on ping alone if we haven't reached our threshold yet. + if (++pingtimeout[i] < cv_pingtimeout.value) + continue; + + pingtimeout[i] = 0; + SendKick(i, KICK_MSG_PING_HIGH); + } + } + } + + //make the ping packet and clear server data for next one + for (i = 0; i < MAXPLAYERS; i++) + { + //CONS_Printf("player %d - total pings: %d\n", i, realpingtable[i]); + + netbuffer->u.netinfo.pingtable[i] = realpingtable[i] / pingmeasurecount; + //server takes a snapshot of the real ping for display. + //otherwise, pings fluctuate a lot and would be odd to look at. + playerpingtable[i] = realpingtable[i] / pingmeasurecount; + realpingtable[i] = 0; //Reset each as we go. + + UINT32 lost = 0; + for (j = 0; j < PACKETMEASUREWINDOW; j++) + { + if (packetloss[i][j]) + lost++; + } + + netbuffer->u.netinfo.packetloss[i] = lost; + netbuffer->u.netinfo.delay[i] = playerdelaytable[i]; + } + + // send the server's maxping as last element of our ping table. This is useful to let us know when we're about to get kicked. + netbuffer->u.netinfo.pingtable[MAXPLAYERS] = cv_maxping.value; + + //send out our ping packets + for (i = 0; i < MAXNETNODES; i++) + if (nodeingame[i]) + HSendPacket(i, true, 0, sizeof(netinfo_pak)); + + pingmeasurecount = 0; //Reset count +} + +static tic_t gametime = 0; + +static void UpdatePingTable(void) +{ + tic_t fastest; + tic_t lag; + + INT32 i; + + if (server) + { + if (Playing() && !(gametime % 8)) // Value chosen based on _my vibes man_ + PingUpdate(); + + fastest = 0; + + // update node latency values so we can take an average later. + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] && playernode[i] > 0) + { + // TicsToMilliseconds can't handle pings over 1000ms lol + realpingtable[i] += GetLag(playernode[i]); + + if (!players[i].spectator) + { + lag = playerpingtable[i]; + if (! fastest || lag < fastest) + fastest = lag; + } + } + } + + if (server_lagless) + lowest_lag = 0; + else + lowest_lag = fastest; + + // Don't gentleman below your mindelay + if (lowest_lag < (tic_t)cv_mindelay.value) + lowest_lag = (tic_t)cv_mindelay.value; + + pingmeasurecount++; + + switch (playerpernode[0]) + { + case 4: + playerdelaytable[nodetoplayer4[0]] = lowest_lag; + /*FALLTHRU*/ + case 3: + playerdelaytable[nodetoplayer3[0]] = lowest_lag; + /*FALLTHRU*/ + case 2: + playerdelaytable[nodetoplayer2[0]] = lowest_lag; + /*FALLTHRU*/ + case 1: + playerdelaytable[nodetoplayer[0]] = lowest_lag; + } + } + else // We're a client, handle mindelay on the way out. + { + // Previously (neededtic - gametic) - WRONG VALUE! + // Pretty sure that's measuring jitter, not RTT. + // Stable connections would be punished by adding their mindelay to network delay! + tic_t mydelay = playerpingtable[consoleplayer]; + + if (mydelay < (tic_t)cv_mindelay.value) + lowest_lag = cv_mindelay.value - mydelay; + else + lowest_lag = 0; + } +} + +// It's that time again! Send everyone a safe message to sign, so we can show off their signature and prove we're playing fair. +static void SendChallenges(void) +{ + int i; + netbuffer->packettype = PT_CHALLENGEALL; + + #ifdef DEVELOP + if (cv_nochallenge.value) + { + CV_AddValue(&cv_nochallenge, -1); + CONS_Alert(CONS_WARNING, "cv_nochallenge enabled, not sending PT_CHALLENGEALL\n"); + return; + } + #endif + + memset(knownWhenChallenged, 0, sizeof(knownWhenChallenged)); + memset(lastReceivedSignature, 0, sizeof(lastReceivedSignature)); + + GenerateChallenge(netbuffer->u.challengeall.secret); + memcpy(lastChallengeAll, netbuffer->u.challengeall.secret, sizeof(lastChallengeAll)); + + // Take note of everyone's current key, so that players who disconnect and are replaced aren't held to the old player's challenge. + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i]) + memcpy(knownWhenChallenged[i], players[i].public_key, sizeof(knownWhenChallenged[i])); + } + + for (i = 0; i < MAXNETNODES; i++) + { + if (nodeingame[i]) + HSendPacket(i, true, 0, sizeof(challengeall_pak)); + } +} + +// Before we start sending out the results, we need to kick everyone who didn't respond. +// (If we try to do both at once, clients will still see players who failled in-game when the results arrive...) +static void KickUnverifiedPlayers(void) +{ + int i; + uint8_t allZero[SIGNATURELENGTH]; + memset(allZero, 0, SIGNATURELENGTH); + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i]) + continue; + if (memcmp(lastReceivedSignature[i], allZero, SIGNATURELENGTH) == 0) // We never got a response! + { + if (!IsPlayerGuest(i) && memcmp(&knownWhenChallenged[i], &players[i].public_key, sizeof(knownWhenChallenged[i])) == 0) + { + if (playernode[i] != servernode) + SendKick(i, KICK_MSG_SIGFAIL); + } + } + } +} + +// +static void SendChallengeResults(void) +{ + int i; + netbuffer->packettype = PT_RESULTSALL; + + #ifdef DEVELOP + if (cv_noresults.value) + { + CV_AddValue(&cv_noresults, -1); + CONS_Alert(CONS_WARNING, "cv_noresults enabled, not sending PT_RESULTSALL\n"); + return; + } + #endif + + uint8_t allZero[SIGNATURELENGTH]; + memset(allZero, 0, sizeof(allZero)); + + memset(&netbuffer->u.resultsall, 0, sizeof(netbuffer->u.resultsall)); + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i]) + continue; + + // Don't try to transmit signatures for players who didn't get here in time to send one. + // (Everyone who had their chance should have been kicked by KickUnverifiedPlayers by now.) + if (memcmp(lastReceivedSignature[i], allZero, SIGNATURELENGTH) == 0) + continue; + + memcpy(netbuffer->u.resultsall.signature[i], lastReceivedSignature[i], sizeof(netbuffer->u.resultsall.signature[i])); + #ifdef DEVELOP + if (cv_badresults.value) + { + CV_AddValue(&cv_badresults, -1); + CONS_Alert(CONS_WARNING, "cv_badresults enabled, scrubbing signature from PT_RESULTSALL\n"); + memset(netbuffer->u.resultsall.signature[i], 0, sizeof(netbuffer->u.resultsall.signature[i])); + } + #endif + } + + for (i = 0; i < MAXNETNODES; i++) + { + if (nodeingame[i]) + HSendPacket(i, true, 0, sizeof(resultsall_pak)); + } +} + +// Who should we try to verify when results come in? +// Store a public key for every active slot, so if players shuffle during challenge leniency, +// we don't incorrectly try to verify someone who didn't even get a challenge, throw a tantrum, and bail. +static void CheckPresentPlayers(void) +{ + int i; + memset(knownWhenChallenged, 0, sizeof(knownWhenChallenged)); + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i]) + { + continue; + } + else if (IsPlayerGuest(i)) + { + continue; + } + else + { + memcpy(knownWhenChallenged[i], players[i].public_key, sizeof(knownWhenChallenged[i])); + } + } +} + +// Handle "client-to-client" auth challenge flow. +void UpdateChallenges(void) +{ + if (!(Playing() && netgame)) + return; + + if (server) + { + if (leveltime == CHALLENGEALL_START) + SendChallenges(); + + if (leveltime == CHALLENGEALL_KICKUNRESPONSIVE) + KickUnverifiedPlayers(); + + if (leveltime == CHALLENGEALL_SENDRESULTS) + SendChallengeResults(); + + } + else // client + { + if (leveltime < CHALLENGEALL_START) + expectChallenge = true; + + if (leveltime == CHALLENGEALL_START) + CheckPresentPlayers(); + + if (leveltime > CHALLENGEALL_CLIENTCUTOFF && expectChallenge) + HandleSigfail("Didn't receive client signatures."); + } +} + +static void RenewHolePunch(void) +{ + static time_t past; + + const time_t now = time(NULL); + + if ((now - past) > 20) + { + I_NetRegisterHolePunch(); + past = now; + } +} + +// Handle timeouts to prevent definitive freezes from happenning +static void HandleNodeTimeouts(void) +{ + INT32 i; + + if (server) + { + for (i = 1; i < MAXNETNODES; i++) + if ((nodeingame[i] || nodeneedsauth[i]) && freezetimeout[i] < I_GetTime()) + Net_ConnectionTimeout(i); + + // In case the cvar value was lowered + if (joindelay) + joindelay = min(joindelay - 1, 3 * (tic_t)cv_joindelay.value * TICRATE); + } +} + +// Keep the network alive while not advancing tics! +void NetKeepAlive(void) +{ + tic_t nowtime; + INT32 realtics; + + nowtime = I_GetTime(); + realtics = nowtime - gametime; + + // return if there's no time passed since the last call + if (realtics <= 0) // nothing new to update + return; + + UpdatePingTable(); + + GetPackets(); + +#ifdef MASTERSERVER + MasterClient_Ticker(); +#endif + + if (netgame && serverrunning) + { + RenewHolePunch(); + } + + if (client) + { + // send keep alive + CL_SendClientKeepAlive(); + // No need to check for resynch because we aren't running any tics + } + else + { + SV_SendServerKeepAlive(); + } + + // No else because no tics are being run and we can't resynch during this + + Net_AckTicker(); + HandleNodeTimeouts(); + FileSendTicker(); + + // Update voice whenever possible. + NetVoiceUpdate(); +} + +// If a tree falls in the forest but nobody is around to hear it, does it make a tic? +#define DEDICATEDIDLETIME (10*TICRATE) + +void NetUpdate(void) +{ + static tic_t resptime = 0; + tic_t nowtime; + INT32 i; + INT32 realtics; + + nowtime = I_GetTime(); + realtics = nowtime - gametime; + + if (!demo.playback && g_fast_forward > 0) + { + realtics = 1; + } + else + { + if (realtics <= 0) // nothing new to update + return; + + if (realtics > 5) + { + if (server) + realtics = 1; + else + realtics = 5; + } + } + +#ifdef DEDICATEDIDLETIME + if (server && dedicated && gamestate == GS_LEVEL) + { + static tic_t dedicatedidle = 0; + + for (i = 1; i < MAXNETNODES; ++i) + if (nodeingame[i]) + { + if (dedicatedidle == DEDICATEDIDLETIME) + { + CONS_Printf("DEDICATED: Awakening from idle (Node %d detected...)\n", i); + dedicatedidle = 0; + } + break; + } + + if (i == MAXNETNODES) + { + if (leveltime == 2) + { + // On next tick... + dedicatedidle = DEDICATEDIDLETIME-1; + } + else if (dedicatedidle == DEDICATEDIDLETIME) + { + if (D_GetExistingTextcmd(gametic, 0) || D_GetExistingTextcmd(gametic+1, 0)) + { + CONS_Printf("DEDICATED: Awakening from idle (Netxcmd detected...)\n"); + dedicatedidle = 0; + } + else + { + realtics = 0; + } + } + else if ((dedicatedidle += realtics) >= DEDICATEDIDLETIME) + { + const char *idlereason = "at round start"; + if (leveltime > 3) + idlereason = va("for %d seconds", dedicatedidle/TICRATE); + + CONS_Printf("DEDICATED: No nodes %s, idling...\n", idlereason); + realtics = 0; + dedicatedidle = DEDICATEDIDLETIME; + } + } + } +#endif + + gametime = nowtime; + + UpdatePingTable(); + + if (client) + maketic = neededtic; + + Local_Maketic(realtics); // make local tic, and call menu? + + if (server) + CL_SendClientCmd(); // send it + + GetPackets(); // get packet from client or from server + + // client send the command after a receive of the server + // the server send before because in single player is beter + +#ifdef MASTERSERVER + MasterClient_Ticker(); // Acking the Master Server +#endif + + if (netgame && serverrunning) + { + RenewHolePunch(); + } + + if (client) + { + // If the client just finished redownloading the game state, load it + if (cl_redownloadinggamestate && fileneeded[0].status == FS_FOUND) + CL_ReloadReceivedSavegame(); + + CL_SendClientCmd(); // Send tic cmd + hu_redownloadinggamestate = cl_redownloadinggamestate; + } + else + { + if (!demo.playback && realtics > 0) + { + INT32 counts; + + hu_redownloadinggamestate = false; + + // Don't erase tics not acknowledged + counts = realtics; + + firstticstosend = gametic; + for (i = 0; i < MAXNETNODES; i++) + { + if (!nodeingame[i]) + continue; + if (nettics[i] < firstticstosend) + firstticstosend = nettics[i]; + if (maketic + counts >= nettics[i] + (BACKUPTICS - TICRATE)) + Net_ConnectionTimeout(i); + } + + if (maketic + counts >= firstticstosend + BACKUPTICS) + counts = firstticstosend+BACKUPTICS-maketic-1; + + for (i = 0; i < counts; i++) + SV_Maketic(); // Create missed tics and increment maketic + + for (; tictoclear < firstticstosend; tictoclear++) // Clear only when acknowledged + D_Clearticcmd(tictoclear); // Clear the maketic the new tic + + SV_SendTics(); + + neededtic = maketic; // The server is a client too + } + } + + if (server) + { + for(i = 0; i < MAXPLAYERS; i++) + { + if (stop_spamming[i] > 0) + stop_spamming[i]--; + } + } + + Net_AckTicker(); + HandleNodeTimeouts(); + + nowtime /= NEWTICRATERATIO; + + if (nowtime > resptime) + { + resptime = nowtime; +#ifdef HAVE_THREADS + I_lock_mutex(&k_menu_mutex); +#endif + M_Ticker(); + refreshdirmenu = 0; +#ifdef HAVE_THREADS + I_unlock_mutex(k_menu_mutex); +#endif + CON_Ticker(); + + M_ScreenshotTicker(); + } + + FileSendTicker(); +} + +void NetVoiceUpdate(void) +{ + UINT8 *encoded = NULL; + + if (dedicated) + { + return; + } + + // This necessarily runs every frame, not every tic + S_SoundInputSetEnabled(true); + + UINT32 bytes_dequed = 0; + do + { + // We need to drain the input queue completely, so do this in a full loop + + INT32 to_read = (SRB2_VOICE_OPUS_FRAME_SIZE - g_local_voice_buffer_len) * sizeof(float); + if (to_read > 0) + { + // Attempt to fill the voice frame buffer + + bytes_dequed = S_SoundInputDequeueSamples((void*)(g_local_voice_buffer + g_local_voice_buffer_len), to_read); + g_local_voice_buffer_len += bytes_dequed / 4; + } + else + { + bytes_dequed = 0; + } + + if (g_local_voice_buffer_len < SRB2_VOICE_OPUS_FRAME_SIZE) + { + continue; + } + + // Amp of +10 dB is appromiately "twice as loud" + float ampfactor = powf(10, (float) cv_voice_inputamp.value / 20.f); + for (int i = 0; i < g_local_voice_buffer_len; i++) + { + g_local_voice_buffer[i] *= ampfactor; + } + + float softmem = 0.f; + opus_pcm_soft_clip(g_local_voice_buffer, SRB2_VOICE_OPUS_FRAME_SIZE, 1, &softmem); + + // Voice detection gate open/close + float maxamplitude = 0.f; + for (int i = 0; i < g_local_voice_buffer_len; i++) + { + maxamplitude = max(fabsf(g_local_voice_buffer[i]), maxamplitude); + } + // 20. * log_10(amplitude) -> decibels (up to 0) + // lower than -30 dB is usually inaudible + g_local_voice_last_peak = maxamplitude; + maxamplitude = 20.f * logf(maxamplitude); + if (maxamplitude > (float) cv_voice_activationthreshold.value) + { + g_local_voice_threshold_time = I_GetTime(); + g_local_voice_detected = true; + } + + switch (cv_voice_mode.value) + { + case 0: + if (I_GetTime() - g_local_voice_threshold_time > 15) + { + g_local_voice_buffer_len = 0; + g_local_voice_detected = false; + continue; + } + break; + case 1: + if (!g_voicepushtotalk_on) + { + g_local_voice_buffer_len = 0; + g_local_voice_detected = false; + continue; + } + g_local_voice_detected = true; + break; + default: + g_local_voice_buffer_len = 0; + continue; + } + + if (cv_voice_chat.value == 0) + { + g_local_voice_buffer_len = 0; + continue; + } + + if (!encoded) + { + encoded = Z_Malloc(sizeof(UINT8) * 1400, PU_STATIC, NULL); + } + + if (g_local_opus_encoder == NULL) + { + InitializeLocalVoiceEncoder(); + } + OpusEncoder *encoder = g_local_opus_encoder; + + INT32 result = opus_encode_float(encoder, g_local_voice_buffer, SRB2_VOICE_OPUS_FRAME_SIZE, encoded, 1400); + if (result < 0) + { + continue; + } + + // Only send a voice packet and set local player voice active if: + // 1. In a netgame, + // 2. Not self-muted by cvar + // 3. The consoleplayer is not server or self muted or deafened + if (netgame && !cv_voice_selfmute.value && !(players[consoleplayer].pflags2 & (PF2_SERVERMUTE | PF2_SELFMUTE | PF2_SELFDEAFEN | PF2_SERVERDEAFEN))) + { + DoVoicePacket(servernode, g_local_opus_frame, encoded, result); + S_SetPlayerVoiceActive(consoleplayer); + } + + if (cv_voice_loopback.value) + { + result = opus_decode_float(g_player_opus_decoders[consoleplayer], encoded, result, g_local_voice_buffer, SRB2_VOICE_OPUS_FRAME_SIZE, 0); + S_QueueVoiceFrameFromPlayer(consoleplayer, g_local_voice_buffer, result * sizeof(float), false); + } + + g_local_voice_buffer_len = 0; + g_local_opus_frame += 1; + } while (bytes_dequed > 0); + + if (encoded) Z_Free(encoded); + return; +} + +/** Returns the number of players playing. + * \return Number of players. Can be zero if we're running a ::dedicated + * server. + * \author Graue + */ +INT32 D_NumPlayers(void) +{ + INT32 num = 0, ix; + + for (ix = 0; ix < MAXPLAYERS; ix++) + { + if (playeringame[ix] && !players[ix].bot) + { + num++; + } + } + + return num; +} + +/** Returns the number of players racing, not spectating and includes bots + * \return Number of players. Can be zero if we're running a ::dedicated + * server. + */ +INT32 D_NumPlayersInRace(void) +{ + INT32 numPlayers = 0; + INT32 i; + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] && !players[i].spectator) + numPlayers++; + } + return numPlayers; +} + +/** Return whether a player is a real person (not a CPU) and not spectating. + */ +boolean D_IsPlayerHumanAndGaming (INT32 player_number) +{ + player_t * player = &players[player_number]; + return ( + playeringame[player_number] && + ! player->spectator && + ! player->bot + ); +} + +tic_t GetLag(INT32 node) +{ + // If the client has caught up to the server -- say, during a wipe -- lag is meaningless. + if (nettics[node] > gametic) + return 0; + return gametic - nettics[node]; +} + +#define REWIND_POINT_INTERVAL 4*TICRATE + 16 +rewind_t *rewindhead; + +void CL_ClearRewinds(void) +{ + rewind_t *head; + while ((head = rewindhead)) + { + rewindhead = rewindhead->next; + free(head); + } +} + +rewind_t *CL_SaveRewindPoint(size_t demopos) +{ + savebuffer_t save = {0}; + rewind_t *rewind; + + if (rewindhead && rewindhead->leveltime + REWIND_POINT_INTERVAL > leveltime) + return NULL; + + rewind = (rewind_t *)malloc(sizeof (rewind_t)); + if (!rewind) + return NULL; + + P_SaveBufferFromExisting(&save, rewind->savebuffer, NETSAVEGAMESIZE); + P_SaveNetGame(&save, false); + + rewind->leveltime = leveltime; + rewind->next = rewindhead; + rewind->demopos = demopos; + rewindhead = rewind; + + return rewind; +} + +rewind_t *CL_RewindToTime(tic_t time) +{ + savebuffer_t save = {0}; + rewind_t *rewind; + + while (rewindhead && rewindhead->leveltime > time) + { + rewind = rewindhead->next; + free(rewindhead); + rewindhead = rewind; + } + + if (!rewindhead) + return NULL; + + P_SaveBufferFromExisting(&save, rewindhead->savebuffer, NETSAVEGAMESIZE); + P_LoadNetGame(&save, false); + + wipegamestate = gamestate; // No fading back in! + timeinmap = leveltime; + + return rewindhead; +} + +void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest) +{ +#ifdef NOMD5 + (void)buffer; + (void)len; + (void)salt; + memset(dest, 0, 16); +#else + char tmpbuf[256]; + const size_t sl = strlen(salt); + + if (len > 256-sl) + len = 256-sl; + + memcpy(tmpbuf, buffer, len); + memmove(&tmpbuf[len], salt, sl); + //strcpy(&tmpbuf[len], salt); + len += strlen(salt); + if (len < 256) + memset(&tmpbuf[len],0,256-len); + + // Yes, we intentionally md5 the ENTIRE buffer regardless of size... + md5_buffer(tmpbuf, 256, dest); +#endif +} + +// Want to say something? XD_SAY is server only, gotta request that they send one on our behalf +void DoSayPacket(SINT8 target, UINT8 flags, UINT8 source, char *message) +{ + say_pak *packet = (void*)&netbuffer->u.say; + netbuffer->packettype = PT_SAY; + + memset(packet->message, 0, sizeof(packet->message)); + strcpy(packet->message, message); + + packet->source = source; + packet->flags = flags; + packet->target = target; + + HSendPacket(servernode, false, 0, sizeof(say_pak)); +} + +void DoSayPacketFromCommand(SINT8 target, size_t usedargs, UINT8 flags) +{ + char buf[2 + HU_MAXMSGLEN + 1]; + size_t numwords, ix; + char *msg = &buf[3]; + const size_t msgspace = sizeof buf - 2; + + numwords = COM_Argc() - usedargs; + I_Assert(numwords > 0); + + msg[0] = '\0'; + + for (ix = 0; ix < numwords; ix++) + { + if (ix > 0) + strlcat(msg, " ", msgspace); + strlcat(msg, COM_Argv(ix + usedargs), msgspace); + } + + DoSayPacket(target, flags, consoleplayer, msg); +} + +void DoVoicePacket(SINT8 target, UINT64 frame, const UINT8* opusdata, size_t len) +{ + voice_pak *pl = &netbuffer->u.voice; + netbuffer->packettype = PT_VOICE; + pl->frame = (UINT64)LONGLONG(frame); + pl->flags = 0; + I_Assert(MAXPACKETLENGTH - sizeof(voice_pak) - BASEPACKETSIZE >= len); + memcpy((UINT8*)netbuffer + BASEPACKETSIZE + sizeof(voice_pak), opusdata, len); + HSendPacket(target, false, 0, sizeof(voice_pak) + len); +} + +// This is meant to be targeted at player indices, not whatever the hell XD_SAY is doing with 1-indexed players. +void SendServerNotice(SINT8 target, char *message) +{ + if (client) + return; + DoSayCommand(message, target + 1, HU_PRIVNOTICE, servernode); +} diff --git a/src/d_clisrv.h b/src/d_clisrv.h index f9577ae3d..2f6fef581 100644 --- a/src/d_clisrv.h +++ b/src/d_clisrv.h @@ -137,6 +137,8 @@ typedef enum PT_REQMAPQUEUE, // Client requesting a roundqueue operation + PT_VOICE, // Voice packet for either side + NUMPACKETTYPE } packettype_t; @@ -283,6 +285,7 @@ struct clientconfig_pak #define SV_SPEEDMASK 0x03 // used to send kartspeed #define SV_DEDICATED 0x40 // server is dedicated +#define SV_VOICEENABLED 0x80 // voice_mute is off/voice chat is enabled #define SV_LOTSOFADDONS 0x20 // flag used to ask for full file list in d_netfil #define MAXFILENEEDED 915 @@ -418,6 +421,22 @@ struct netinfo_pak UINT32 delay[MAXPLAYERS+1]; } ATTRPACK; +// Sent by both sides. Contains Opus-encoded voice packet +// flags bitset map (left to right, low to high) +// | PPPPPTRR | -- P = Player num, T = Terminal, R = Reserved (0) +// Data following voice header is a single Opus frame +struct voice_pak +{ + UINT64 frame; + UINT8 flags; +} ATTRPACK; + +#define VOICE_PAK_FLAGS_PLAYERNUM_BITS 0x1F +#define VOICE_PAK_FLAGS_TERMINAL_BIT 0x20 +#define VOICE_PAK_FLAGS_RESERVED0_BIT 0x40 +#define VOICE_PAK_FLAGS_RESERVED1_BIT 0x80 +#define VOICE_PAK_FLAGS_RESERVED_BITS (VOICE_PAK_FLAGS_RESERVED0_BIT | VOICE_PAK_FLAGS_RESERVED1_BIT) + // // Network packet data // @@ -462,6 +481,7 @@ struct doomdata_t resultsall_pak resultsall; // 1024 bytes. Also, you really shouldn't trust anything here. say_pak say; // I don't care anymore. reqmapqueue_pak reqmapqueue; // Formerly XD_REQMAPQUEUE + voice_pak voice; // Unreliable voice data, variable length } u; // This is needed to pack diff packet types data together } ATTRPACK; @@ -606,6 +626,7 @@ void SendKick(UINT8 playernum, UINT8 msg); // Create any new ticcmds and broadcast to other players. void NetKeepAlive(void); void NetUpdate(void); +void NetVoiceUpdate(void); void SV_StartSinglePlayerServer(INT32 dogametype, boolean donetgame); boolean SV_SpawnServer(void); @@ -710,6 +731,7 @@ void HandleSigfail(const char *string); void DoSayPacket(SINT8 target, UINT8 flags, UINT8 source, char *message); void DoSayPacketFromCommand(SINT8 target, size_t usedargs, UINT8 flags); +void DoVoicePacket(SINT8 target, UINT64 frame, const UINT8* opusdata, size_t len); void SendServerNotice(SINT8 target, char *message); #ifdef __cplusplus diff --git a/src/d_main.cpp b/src/d_main.cpp index 4add5edd6..78af3a439 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -161,6 +161,7 @@ INT32 postimgparam[MAXSPLITSCREENPLAYERS]; boolean sound_disabled = false; boolean digital_disabled = false; +boolean g_voice_disabled = false; #ifdef DEBUGFILE INT32 debugload = 0; @@ -1079,6 +1080,7 @@ void D_SRB2Loop(void) // consoleplayer -> displayplayers (hear sounds from viewpoint) S_UpdateSounds(); // move positional sounds + NetVoiceUpdate(); // update voice recording whenever possible if (realtics > 0 || singletics) { S_UpdateClosedCaptions(); @@ -1095,6 +1097,7 @@ void D_SRB2Loop(void) #endif Music_Tick(); + S_UpdateVoicePositionalProperties(); // Fully completed frame made. finishprecise = I_GetPreciseTime(); @@ -1885,12 +1888,14 @@ void D_SRB2Main(void) { sound_disabled = true; digital_disabled = true; + g_voice_disabled = true; } if (M_CheckParm("-noaudio")) // combines -nosound and -nomusic { sound_disabled = true; digital_disabled = true; + g_voice_disabled = true; } else { @@ -1905,9 +1910,13 @@ void D_SRB2Main(void) if (M_CheckParm("-nodigmusic")) digital_disabled = true; // WARNING: DOS version initmusic in I_StartupSound } + if (M_CheckParm("-novoice")) + { + g_voice_disabled = true; + } } - if (!( sound_disabled && digital_disabled )) + if (!( sound_disabled && digital_disabled && g_voice_disabled )) { CONS_Printf("S_InitSfxChannels(): Setting up sound channels.\n"); I_StartupSound(); diff --git a/src/d_netcmd.c b/src/d_netcmd.c index fb5a10471..e0f00e472 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -1175,6 +1175,8 @@ enum { WP_AUTOROULETTE = 1<<2, WP_ANALOGSTICK = 1<<3, WP_AUTORING = 1<<4, + WP_SELFMUTE = 1<<5, + WP_SELFDEAFEN = 1<<6 }; void WeaponPref_Send(UINT8 ssplayer) @@ -1196,6 +1198,15 @@ void WeaponPref_Send(UINT8 ssplayer) if (cv_autoring[ssplayer].value) prefs |= WP_AUTORING; + if (ssplayer == 0) + { + if (cv_voice_selfmute.value) + prefs |= WP_SELFMUTE; + + if (!cv_voice_chat.value) + prefs |= WP_SELFDEAFEN; + } + UINT8 buf[2]; buf[0] = prefs; buf[1] = cv_mindelay.value; @@ -1235,6 +1246,7 @@ size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum) UINT8 prefs = READUINT8(p); player->pflags &= ~(PF_KICKSTARTACCEL|PF_SHRINKME|PF_AUTOROULETTE|PF_AUTORING); + player->pflags2 &= ~(PF2_SELFMUTE | PF2_SELFDEAFEN); if (prefs & WP_KICKSTARTACCEL) player->pflags |= PF_KICKSTARTACCEL; @@ -1253,6 +1265,12 @@ size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum) if (prefs & WP_AUTORING) player->pflags |= PF_AUTORING; + if (prefs & WP_SELFMUTE) + player->pflags2 |= PF2_SELFMUTE; + + if (prefs & WP_SELFDEAFEN) + player->pflags2 |= PF2_SELFDEAFEN; + if (leveltime < 2) { // BAD HACK: No other place I tried to slot this in @@ -7032,6 +7050,18 @@ void Mute_OnChange(void) HU_AddChatText(M_GetText("\x82*Chat is no longer muted."), false); } +void VoiceMute_OnChange(void); +void VoiceMute_OnChange(void) +{ + if (leveltime <= 1) + return; // avoid having this notification put in our console / log when we boot the server. + + if (cv_voice_servermute.value) + HU_AddChatText(M_GetText("\x82*Voice chat has been muted."), false); + else + HU_AddChatText(M_GetText("\x82*Voice chat is no longer muted."), false); +} + /** Hack to clear all changed flags after game start. * A lot of code (written by dummies, obviously) uses COM_BufAddText() to run * commands and change consvars, especially on game start. This is problematic diff --git a/src/d_netcmd.h b/src/d_netcmd.h index c395d82a9..70c653254 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -61,6 +61,7 @@ extern consvar_t cv_netstat; extern consvar_t cv_countdowntime; extern consvar_t cv_mute; +extern consvar_t cv_voice_servermute; extern consvar_t cv_pause; extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_maxplayers; @@ -185,6 +186,8 @@ typedef enum XD_CALLZVOTE, // 39 XD_SETZVOTE, // 40 XD_TEAMCHANGE, // 41 + XD_SERVERMUTEPLAYER, // 42 + XD_SERVERDEAFENPLAYER, // 43 MAXNETXCMD } netxcmd_t; diff --git a/src/d_player.h b/src/d_player.h index f6397d682..594423652 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -132,6 +132,14 @@ typedef enum PF_NOFASTFALL = (INT32)(1U<<31), // Has already done ebrake/fastfall behavior for this input. Fastfalling needs a new input to prevent unwanted bounces on unexpected airtime. } pflags_t; +typedef enum +{ + PF2_SELFMUTE = 1<<1, + PF2_SELFDEAFEN = 1<<2, + PF2_SERVERMUTE = 1<<3, + PF2_SERVERDEAFEN = 1<<4, +} pflags2_t; + typedef enum { // Are animation frames playing? @@ -634,6 +642,7 @@ struct player_t // Bit flags. // See pflags_t, above. UINT32 pflags; + UINT32 pflags2; // playing animation. panim_t panim; @@ -1054,7 +1063,7 @@ struct player_t UINT8 amps; UINT8 amppickup; UINT8 ampspending; - + UINT16 overdrive; UINT16 overshield; fixed_t overdrivepower; diff --git a/src/doomstat.h b/src/doomstat.h index 476c4b2a8..1495951e0 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -239,6 +239,7 @@ extern boolean forceresetplayers, deferencoremode, forcespecialstage; extern boolean sound_disabled; extern boolean digital_disabled; +extern boolean g_voice_disabled; // ========================= // Status flags for refresh. diff --git a/src/dummy/i_sound.c b/src/dummy/i_sound.c index 308d78f56..06dd06c0a 100644 --- a/src/dummy/i_sound.c +++ b/src/dummy/i_sound.c @@ -224,3 +224,30 @@ boolean I_FadeInPlaySong(UINT32 ms, boolean looping) (void)looping; return false; } + +boolean I_SoundInputIsEnabled(void) +{ + return false; +} + +boolean I_SoundInputSetEnabled(boolean enabled) +{ + return false; +} + +UINT32 I_SoundInputDequeueSamples(void *data, UINT32 len) +{ + return 0; +} + +void I_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal) +{ +} + +void I_SetPlayerVoiceProperties(INT32 playernum, float volume, float sep) +{ +} + +void I_ResetVoiceQueue(INT32 playernum) +{ +} diff --git a/src/g_game.c b/src/g_game.c index 23b4a363b..1876bf501 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -2201,6 +2201,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) UINT32 followitem; INT32 pflags; + INT32 pflags2; UINT8 team; @@ -2348,6 +2349,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) xtralife = players[player].xtralife; pflags = (players[player].pflags & (PF_WANTSTOJOIN|PF_KICKSTARTACCEL|PF_SHRINKME|PF_SHRINKACTIVE|PF_AUTOROULETTE|PF_ANALOGSTICK|PF_AUTORING)); + pflags2 = (players[player].pflags2 & (PF2_SELFMUTE | PF2_SELFDEAFEN | PF2_SERVERMUTE | PF2_SERVERDEAFEN)); // SRB2kart memcpy(&itemRoulette, &players[player].itemRoulette, sizeof (itemRoulette)); @@ -2539,6 +2541,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) p->roundscore = roundscore; p->lives = lives; p->pflags = pflags; + p->pflags2 = pflags2; p->team = team; p->jointime = jointime; p->splitscreenindex = splitscreenindex; @@ -5916,7 +5919,7 @@ boolean G_SameTeam(const player_t *a, const player_t *b) } // Free for all. - return false; + return false; } UINT8 G_CountTeam(UINT8 team) diff --git a/src/g_input.c b/src/g_input.c index 313c81722..f91a48d6e 100644 --- a/src/g_input.c +++ b/src/g_input.c @@ -910,6 +910,7 @@ static const char *gamecontrolname[num_gamecontrols] = "screenshot", "startmovie", "startlossless", + "voicepushtotalk" }; #define NUMKEYNAMES (sizeof (keynames)/sizeof (keyname_t)) @@ -1332,7 +1333,7 @@ INT32 G_FindPlayerBindForGameControl(INT32 player, gamecontrols_e control) } } - return (bestbind != -1) ? bestbind : anybind; // If we couldn't find a device-appropriate bind, try to at least use something + return (bestbind != -1) ? bestbind : anybind; // If we couldn't find a device-appropriate bind, try to at least use something } static void setcontrol(UINT8 player) diff --git a/src/g_input.h b/src/g_input.h index ccca4cbe9..7c236cce8 100644 --- a/src/g_input.h +++ b/src/g_input.h @@ -114,6 +114,7 @@ typedef enum gc_screenshot, gc_startmovie, gc_startlossless, + gc_voicepushtotalk, num_gamecontrols, diff --git a/src/hu_stuff.c b/src/hu_stuff.c index 2c50bb99f..c39b8009d 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -86,6 +86,7 @@ patch_t *frameslash; // framerate stuff. Used in screen.c static player_t *plr; boolean hu_keystrokes; // :) boolean chat_on; // entering a chat message? +boolean g_voicepushtotalk_on; // holding PTT? static char w_chat[HU_MAXMSGLEN + 1]; static size_t c_input = 0; // let's try to make the chat input less shitty. static boolean headsupactive = false; @@ -1102,6 +1103,24 @@ void HU_clearChatChars(void) // boolean HU_Responder(event_t *ev) { + // Handle Push-to-Talk + if (ev->data1 == gamecontrol[0][gc_voicepushtotalk][0] + || ev->data1 == gamecontrol[0][gc_voicepushtotalk][1] + || ev->data1 == gamecontrol[0][gc_voicepushtotalk][2] + || ev->data1 == gamecontrol[0][gc_voicepushtotalk][3]) + { + if (ev->type == ev_keydown) + { + g_voicepushtotalk_on = true; + return true; + } + else if (ev->type == ev_keyup) + { + g_voicepushtotalk_on = false; + return true; + } + } + if (ev->type != ev_keydown) return false; @@ -1912,25 +1931,25 @@ static void HU_DrawTitlecardCEcho(size_t num) { INT32 ofs; INT32 timer = (INT32)(elapsed - timeroffset); - + if (timer <= 0) return; // we don't care. - + line = strchr(echoptr, '\\'); - + if (line == NULL) break; *line = '\0'; - + ofs = V_CenteredTitleCardStringOffset(echoptr, p4); V_DrawTitleCardString(x - ofs, y, echoptr, 0, false, timer, fadeout, p4); y += p4 ? 18 : 32; - + // offset the timer for the next line. timeroffset += strlen(echoptr); - + // set the ptr to the \0 we made and advance it because we don't want an empty string. echoptr = line; echoptr++; diff --git a/src/hu_stuff.h b/src/hu_stuff.h index 5747cd85e..12230ba56 100644 --- a/src/hu_stuff.h +++ b/src/hu_stuff.h @@ -125,6 +125,9 @@ void HU_AddChatText(const char *text, boolean playsound); // set true when entering a chat message extern boolean chat_on; +// set true when push-to-talk is held +extern boolean g_voicepushtotalk_on; + // keystrokes in the console or chat window extern boolean hu_keystrokes; diff --git a/src/hud/spectator.cpp b/src/hud/spectator.cpp index 1f7568a5c..44e152d3b 100644 --- a/src/hud/spectator.cpp +++ b/src/hud/spectator.cpp @@ -27,6 +27,7 @@ #include "../k_hud.h" #include "../p_local.h" #include "../r_fps.h" +#include "../s_sound.h" extern "C" consvar_t cv_maxplayers; diff --git a/src/i_sound.h b/src/i_sound.h index ea346b859..4a5cd79ad 100644 --- a/src/i_sound.h +++ b/src/i_sound.h @@ -114,6 +114,8 @@ void I_UpdateSoundParams(INT32 handle, UINT8 vol, UINT8 sep, UINT8 pitch); */ void I_SetSfxVolume(int volume); +void I_SetVoiceVolume(int volume); + /// ------------------------ // MUSIC SYSTEM /// ------------------------ @@ -246,6 +248,22 @@ boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void)); boolean I_FadeOutStopSong(UINT32 ms); boolean I_FadeInPlaySong(UINT32 ms, boolean looping); +// AUDIO INPUT (Microphones) +boolean I_SoundInputIsEnabled(void); +boolean I_SoundInputSetEnabled(boolean enabled); +UINT32 I_SoundInputDequeueSamples(void *data, UINT32 len); + +// VOICE CHAT + +/// Queue a frame of samples of voice data from a player. Voice format is MONO F32 SYSTEM ENDIANNESS. +/// If there is too much data being queued, old samples will be truncated +void I_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal); + +void I_SetPlayerVoiceProperties(INT32 playernum, float volume, float sep); + +/// Reset the voice queue for the given player. Use when server connection ends +void I_ResetVoiceQueue(INT32 playernum); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/k_hud.cpp b/src/k_hud.cpp index 2b89011b3..11ac3423f 100644 --- a/src/k_hud.cpp +++ b/src/k_hud.cpp @@ -1,6984 +1,7104 @@ -// DR. ROBOTNIK'S RING RACERS -//----------------------------------------------------------------------------- -// Copyright (C) 2024 by Kart Krew. -// -// This program is free software distributed under the -// terms of the GNU General Public License, version 2. -// See the 'LICENSE' file for more details. -//----------------------------------------------------------------------------- -/// \file k_hud.c -/// \brief HUD drawing functions exclusive to Kart - -#include -#include -#include -#include - -#include "v_draw.hpp" - -#include "k_hud.h" -#include "k_kart.h" -#include "k_battle.h" -#include "k_grandprix.h" -#include "k_specialstage.h" -#include "k_objects.h" -#include "k_boss.h" -#include "k_color.h" -#include "k_director.h" -#include "screen.h" -#include "doomtype.h" -#include "doomdef.h" -#include "hu_stuff.h" -#include "d_netcmd.h" -#include "v_video.h" -#include "r_draw.h" -#include "st_stuff.h" -#include "lua_hud.h" -#include "doomstat.h" -#include "d_clisrv.h" -#include "g_game.h" -#include "p_local.h" -#include "z_zone.h" -#include "m_cond.h" -#include "r_main.h" -#include "s_sound.h" -#include "r_things.h" -#include "r_fps.h" -#include "m_random.h" -#include "k_roulette.h" -#include "k_bot.h" -#include "k_rank.h" -#include "g_party.h" -#include "k_hitlag.h" -#include "g_input.h" -#include "k_dialogue.h" -#include "f_finale.h" -#include "m_easing.h" - -//{ Patch Definitions -static patch_t *kp_nodraw; - -static patch_t *kp_timesticker; -static patch_t *kp_timestickerwide; -static patch_t *kp_lapsticker; -static patch_t *kp_lapstickerwide; -static patch_t *kp_lapstickernarrow; -static patch_t *kp_exp[2]; -static patch_t *kp_splitlapflag; -static patch_t *kp_bumpersticker; -static patch_t *kp_bumperstickerwide; -static patch_t *kp_capsulesticker; -static patch_t *kp_capsulestickerwide; -static patch_t *kp_karmasticker; -static patch_t *kp_spheresticker; -static patch_t *kp_splitspheresticker; -static patch_t *kp_splitkarmabomb; -static patch_t *kp_timeoutsticker; - -static patch_t *kp_prestartbulb[15]; -static patch_t *kp_prestartletters[7]; - -static patch_t *kp_prestartbulb_split[15]; -static patch_t *kp_prestartletters_split[7]; - -static patch_t *kp_startcountdown[20]; -static patch_t *kp_racefault[6]; -static patch_t *kp_racefinish[6]; - -static patch_t *kp_positionnum[10][2][2]; // number, overlay or underlay, splitscreen - -patch_t *kp_facenum[MAXPLAYERS+1]; -static patch_t *kp_facehighlight[8]; - -static patch_t *kp_nocontestminimap; -patch_t *kp_unknownminimap; -static patch_t *kp_spbminimap; -static patch_t *kp_wouldyoustillcatchmeifiwereaworm; -static patch_t *kp_catcherminimap; -static patch_t *kp_emeraldminimap[2]; -static patch_t *kp_capsuleminimap[3]; -static patch_t *kp_battleufominimap; -static patch_t *kp_superflickyminimap; - -static patch_t *kp_ringsticker[2]; -static patch_t *kp_ringstickersplit[4]; -static patch_t *kp_ring[6]; -static patch_t *kp_smallring[6]; -static patch_t *kp_ringdebtminus; -static patch_t *kp_ringdebtminussmall; -static patch_t *kp_ringspblock[16]; -static patch_t *kp_ringspblocksmall[16]; -static patch_t *kp_amps[7][12]; -static patch_t *kp_amps_underlay[12]; -static patch_t *kp_overdrive[32]; - -static patch_t *kp_speedometersticker; -static patch_t *kp_speedometerlabel[4]; - -static patch_t *kp_rankbumper; -static patch_t *kp_bigbumper; -static patch_t *kp_tinybumper[2]; -static patch_t *kp_ranknobumpers; -static patch_t *kp_rankcapsule; -static patch_t *kp_rankemerald; -static patch_t *kp_rankemeraldflash; -static patch_t *kp_rankemeraldback; -static patch_t *kp_pts[2]; - -static patch_t *kp_goal[2][2]; // [skull][4p] -static patch_t *kp_goalrod[2]; // [4p] -static patch_t *kp_goaltext1p; - -static patch_t *kp_battlewin; -static patch_t *kp_battlecool; -static patch_t *kp_battlelose; -static patch_t *kp_battlewait; -static patch_t *kp_battleinfo; -static patch_t *kp_wanted; -static patch_t *kp_wantedsplit; -static patch_t *kp_wantedreticle; -static patch_t *kp_minimapdot; - -static patch_t *kp_itembg[6]; -static patch_t *kp_ringbg[4]; -static patch_t *kp_itemtimer[2]; -static patch_t *kp_itemmulsticker[2]; -static patch_t *kp_itemx; - -static patch_t *kp_sadface[3]; -static patch_t *kp_sneaker[3]; -static patch_t *kp_rocketsneaker[3]; -static patch_t *kp_invincibility[19]; -static patch_t *kp_banana[3]; -static patch_t *kp_eggman[3]; -static patch_t *kp_orbinaut[6]; -static patch_t *kp_jawz[3]; -static patch_t *kp_mine[3]; -static patch_t *kp_landmine[3]; -static patch_t *kp_ballhog[3]; -static patch_t *kp_selfpropelledbomb[3]; -static patch_t *kp_grow[3]; -static patch_t *kp_shrink[3]; -static patch_t *kp_lightningshield[3]; -static patch_t *kp_bubbleshield[3]; -static patch_t *kp_flameshield[3]; -static patch_t *kp_hyudoro[3]; -static patch_t *kp_pogospring[3]; -static patch_t *kp_superring[3]; -static patch_t *kp_kitchensink[3]; -static patch_t *kp_droptarget[3]; -static patch_t *kp_gardentop[3]; -static patch_t *kp_gachabom[3]; -static patch_t *kp_bar[2]; -static patch_t *kp_doublebar[2]; -static patch_t *kp_triplebar[2]; -static patch_t *kp_slotring[2]; -static patch_t *kp_seven[2]; -static patch_t *kp_jackpot[2]; - -static patch_t *kp_check[6]; - -static patch_t *kp_rival[2]; -static patch_t *kp_localtag[4][2]; - -static patch_t *kp_talk; -static patch_t *kp_typdot; - -patch_t *kp_eggnum[6]; - -static patch_t *kp_flameshieldmeter[FLAMESHIELD_MAX][2]; -static patch_t *kp_flameshieldmeter_bg[FLAMESHIELD_MAX][2]; - -static patch_t *kp_fpview[3]; -static patch_t *kp_inputwheel[5]; - -static patch_t *kp_challenger[25]; - -static patch_t *kp_lapanim_lap[7]; -static patch_t *kp_lapanim_final[11]; -static patch_t *kp_lapanim_number[10][3]; -static patch_t *kp_lapanim_emblem[2]; -static patch_t *kp_lapanim_hand[3]; - -static patch_t *kp_yougotem; -static patch_t *kp_itemminimap; - -static patch_t *kp_alagles[10]; -static patch_t *kp_blagles[6]; - -static patch_t *kp_cpu[2]; - -static patch_t *kp_nametagstem; - -static patch_t *kp_bossbar[8]; -static patch_t *kp_bossret[4]; - -static patch_t *kp_trickcool[2]; - -patch_t *kp_autoroulette; -patch_t *kp_autoring; - -patch_t *kp_capsuletarget_arrow[2][2]; -patch_t *kp_capsuletarget_icon[2]; -patch_t *kp_capsuletarget_far[2][2]; -patch_t *kp_capsuletarget_far_text[2]; -patch_t *kp_capsuletarget_near[2][8]; - -patch_t *kp_superflickytarget[2][4]; - -patch_t *kp_spraycantarget_far[2][6]; -patch_t *kp_spraycantarget_near[2][6]; - -patch_t *kp_button_a[2][2]; -patch_t *kp_button_b[2][2]; -patch_t *kp_button_c[2][2]; -patch_t *kp_button_x[2][2]; -patch_t *kp_button_y[2][2]; -patch_t *kp_button_z[2][2]; -patch_t *kp_button_start[2][2]; -patch_t *kp_button_l[2][2]; -patch_t *kp_button_r[2][2]; -patch_t *kp_button_up[2][2]; -patch_t *kp_button_down[2][2]; -patch_t *kp_button_right[2][2]; -patch_t *kp_button_left[2][2]; -patch_t *kp_button_lua1[2][2]; -patch_t *kp_button_lua2[2][2]; -patch_t *kp_button_lua3[2][2]; - -patch_t *gen_button_a[2][2]; -patch_t *gen_button_b[2][2]; -patch_t *gen_button_x[2][2]; -patch_t *gen_button_y[2][2]; -patch_t *gen_button_lb[2][2]; -patch_t *gen_button_rb[2][2]; -patch_t *gen_button_lt[2][2]; -patch_t *gen_button_rt[2][2]; -patch_t *gen_button_start[2][2]; -patch_t *gen_button_back[2][2]; -patch_t *gen_button_ls[2][2]; -patch_t *gen_button_rs[2][2]; -patch_t *gen_button_dpad[2][2]; - -patch_t *gen_button_keyleft[2]; -patch_t *gen_button_keyright[2]; -patch_t *gen_button_keycenter[2]; - -static void K_LoadButtonGraphics(patch_t *kp[2][2], const char* code) -{ - HU_UpdatePatch(&kp[0][0], "TLB_%s", code); - HU_UpdatePatch(&kp[0][1], "TLB_%sB", code); - HU_UpdatePatch(&kp[1][0], "TLBS%s", code); - HU_UpdatePatch(&kp[1][1], "TLBS%sB", code); -} - -static void K_LoadGenericButtonGraphics(patch_t *kp[2][2], const char* code) -{ - HU_UpdatePatch(&kp[0][0], "TLG_%s", code); - HU_UpdatePatch(&kp[0][1], "TLG_%sB", code); - HU_UpdatePatch(&kp[1][0], "TLGS%s", code); - HU_UpdatePatch(&kp[1][1], "TLGS%sB", code); -} - -void K_LoadKartHUDGraphics(void) -{ - INT32 i, j, k; - char buffer[9]; - - // Null Stuff - HU_UpdatePatch(&kp_nodraw, "K_TRNULL"); - - // Stickers - HU_UpdatePatch(&kp_timesticker, "K_STTIME"); - HU_UpdatePatch(&kp_timestickerwide, "K_STTIMW"); - HU_UpdatePatch(&kp_lapsticker, "K_STLAPS"); - HU_UpdatePatch(&kp_lapstickerwide, "K_STLAPW"); - HU_UpdatePatch(&kp_lapstickernarrow, "K_STLAPN"); - HU_UpdatePatch(&kp_exp[0], "K_STEXP"); - HU_UpdatePatch(&kp_exp[1], "K_SPTEXP"); - HU_UpdatePatch(&kp_splitlapflag, "K_SPTLAP"); - HU_UpdatePatch(&kp_bumpersticker, "K_STBALN"); - HU_UpdatePatch(&kp_bumperstickerwide, "K_STBALW"); - HU_UpdatePatch(&kp_capsulesticker, "K_STCAPN"); - HU_UpdatePatch(&kp_capsulestickerwide, "K_STCAPW"); - HU_UpdatePatch(&kp_karmasticker, "K_STKARM"); - HU_UpdatePatch(&kp_spheresticker, "K_STBSMT"); - HU_UpdatePatch(&kp_splitspheresticker, "K_SPBSMT"); - HU_UpdatePatch(&kp_splitkarmabomb, "K_SPTKRM"); - HU_UpdatePatch(&kp_timeoutsticker, "K_STTOUT"); - - // Pre-start countdown bulbs - sprintf(buffer, "K_BULBxx"); - for (i = 0; i < 15; i++) - { - buffer[6] = '0'+((i+1)/10); - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_prestartbulb[i], "%s", buffer); - } - - sprintf(buffer, "K_SBLBxx"); - for (i = 0; i < 15; i++) - { - buffer[6] = '0'+((i+1)/10); - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_prestartbulb_split[i], "%s", buffer); - } - - // Pre-start position letters - HU_UpdatePatch(&kp_prestartletters[0], "K_PL_P"); - HU_UpdatePatch(&kp_prestartletters[1], "K_PL_O"); - HU_UpdatePatch(&kp_prestartletters[2], "K_PL_S"); - HU_UpdatePatch(&kp_prestartletters[3], "K_PL_I"); - HU_UpdatePatch(&kp_prestartletters[4], "K_PL_T"); - HU_UpdatePatch(&kp_prestartletters[5], "K_PL_N"); - HU_UpdatePatch(&kp_prestartletters[6], "K_PL_EX"); - - HU_UpdatePatch(&kp_prestartletters_split[0], "K_SPL_P"); - HU_UpdatePatch(&kp_prestartletters_split[1], "K_SPL_O"); - HU_UpdatePatch(&kp_prestartletters_split[2], "K_SPL_S"); - HU_UpdatePatch(&kp_prestartletters_split[3], "K_SPL_I"); - HU_UpdatePatch(&kp_prestartletters_split[4], "K_SPL_T"); - HU_UpdatePatch(&kp_prestartletters_split[5], "K_SPL_N"); - HU_UpdatePatch(&kp_prestartletters_split[6], "K_SPL_EX"); - - // Starting countdown - HU_UpdatePatch(&kp_startcountdown[0], "K_CNT3A"); - HU_UpdatePatch(&kp_startcountdown[1], "K_CNT2A"); - HU_UpdatePatch(&kp_startcountdown[2], "K_CNT1A"); - HU_UpdatePatch(&kp_startcountdown[3], "K_CNTGOA"); - HU_UpdatePatch(&kp_startcountdown[4], "K_DUEL1"); - HU_UpdatePatch(&kp_startcountdown[5], "K_CNT3B"); - HU_UpdatePatch(&kp_startcountdown[6], "K_CNT2B"); - HU_UpdatePatch(&kp_startcountdown[7], "K_CNT1B"); - HU_UpdatePatch(&kp_startcountdown[8], "K_CNTGOB"); - HU_UpdatePatch(&kp_startcountdown[9], "K_DUEL2"); - // Splitscreen - HU_UpdatePatch(&kp_startcountdown[10], "K_SMC3A"); - HU_UpdatePatch(&kp_startcountdown[11], "K_SMC2A"); - HU_UpdatePatch(&kp_startcountdown[12], "K_SMC1A"); - HU_UpdatePatch(&kp_startcountdown[13], "K_SMCGOA"); - HU_UpdatePatch(&kp_startcountdown[14], "K_SDUEL1"); - HU_UpdatePatch(&kp_startcountdown[15], "K_SMC3B"); - HU_UpdatePatch(&kp_startcountdown[16], "K_SMC2B"); - HU_UpdatePatch(&kp_startcountdown[17], "K_SMC1B"); - HU_UpdatePatch(&kp_startcountdown[18], "K_SMCGOB"); - HU_UpdatePatch(&kp_startcountdown[19], "K_SDUEL2"); - - // Fault - HU_UpdatePatch(&kp_racefault[0], "K_FAULTA"); - HU_UpdatePatch(&kp_racefault[1], "K_FAULTB"); - // Splitscreen - HU_UpdatePatch(&kp_racefault[2], "K_SMFLTA"); - HU_UpdatePatch(&kp_racefault[3], "K_SMFLTB"); - // 2P splitscreen - HU_UpdatePatch(&kp_racefault[4], "K_2PFLTA"); - HU_UpdatePatch(&kp_racefault[5], "K_2PFLTB"); - - // Finish - HU_UpdatePatch(&kp_racefinish[0], "K_FINA"); - HU_UpdatePatch(&kp_racefinish[1], "K_FINB"); - // Splitscreen - HU_UpdatePatch(&kp_racefinish[2], "K_SMFINA"); - HU_UpdatePatch(&kp_racefinish[3], "K_SMFINB"); - // 2P splitscreen - HU_UpdatePatch(&kp_racefinish[4], "K_2PFINA"); - HU_UpdatePatch(&kp_racefinish[5], "K_2PFINB"); - - // Position numbers - sprintf(buffer, "KRNKxyz"); - for (i = 0; i < 10; i++) - { - buffer[6] = '0'+i; - - for (j = 0; j < 2; j++) - { - buffer[5] = 'A'+j; - - for (k = 0; k < 2; k++) - { - if (k > 0) - { - buffer[4] = 'S'; - } - else - { - buffer[4] = 'B'; - } - - HU_UpdatePatch(&kp_positionnum[i][j][k], "%s", buffer); - } - } - } - - sprintf(buffer, "OPPRNKxx"); - for (i = 0; i <= MAXPLAYERS; i++) - { - buffer[6] = '0'+(i/10); - buffer[7] = '0'+(i%10); - HU_UpdatePatch(&kp_facenum[i], "%s", buffer); - } - - sprintf(buffer, "K_CHILIx"); - for (i = 0; i < 8; i++) - { - buffer[7] = '0'+(i+1); - HU_UpdatePatch(&kp_facehighlight[i], "%s", buffer); - } - - // Special minimap icons - HU_UpdatePatch(&kp_nocontestminimap, "MINIDEAD"); - HU_UpdatePatch(&kp_unknownminimap, "HUHMAP"); - HU_UpdatePatch(&kp_spbminimap, "SPBMMAP"); - - HU_UpdatePatch(&kp_wouldyoustillcatchmeifiwereaworm, "MINIPROG"); - HU_UpdatePatch(&kp_catcherminimap, "UFOMAP"); - HU_UpdatePatch(&kp_emeraldminimap[0], "EMEMAP"); - HU_UpdatePatch(&kp_emeraldminimap[1], "SUPMAP"); - - HU_UpdatePatch(&kp_capsuleminimap[0], "MINICAP1"); - HU_UpdatePatch(&kp_capsuleminimap[1], "MINICAP2"); - HU_UpdatePatch(&kp_capsuleminimap[2], "MINICAP3"); - - HU_UpdatePatch(&kp_battleufominimap, "MINIBUFO"); - HU_UpdatePatch(&kp_superflickyminimap, "FLKMAPA"); - - // Rings & Lives - HU_UpdatePatch(&kp_ringsticker[0], "RNGBACKA"); - HU_UpdatePatch(&kp_ringsticker[1], "RNGBACKB"); - - sprintf(buffer, "K_RINGx"); - for (i = 0; i < 6; i++) - { - buffer[6] = '0'+(i+1); - HU_UpdatePatch(&kp_ring[i], "%s", buffer); - } - - // Amps - { - // Levels 1-6 - sprintf(buffer, "b_xAMPxx"); - for (i = 0; i < 6; i++) - { - buffer[2] = '0'+i+1; - for (j = 0; j < 12; j++) - { - buffer[6] = '0'+((j) / 10); - buffer[7] = '0'+((j) % 10); - HU_UpdatePatch(&kp_amps[i][j], "%s", buffer); - } - } - - // Level 7 - buffer[2] = '7'; - buffer[1] = 'A'; - for (j = 0; j < 12; j++) - { - buffer[6] = '0'+((j) / 10); - buffer[7] = '0'+((j) % 10); - HU_UpdatePatch(&kp_amps[i][j], "%s", buffer); - } - buffer[1] = 'B'; - for (j = 0; j < 12; j++) - { - buffer[6] = '0'+((j) / 10); - buffer[7] = '0'+((j) % 10); - HU_UpdatePatch(&kp_amps_underlay[j], "%s", buffer); - } - } - - sprintf(buffer, "b_OVRDxx"); - for (i = 0; i < 32; i++) - { - buffer[6] = '0'+((i) / 10); - buffer[7] = '0'+((i) % 10); - HU_UpdatePatch(&kp_overdrive[i], "%s", buffer); - } - - HU_UpdatePatch(&kp_ringdebtminus, "RDEBTMIN"); - - sprintf(buffer, "SPBRNGxx"); - for (i = 0; i < 16; i++) - { - buffer[6] = '0'+((i+1) / 10); - buffer[7] = '0'+((i+1) % 10); - HU_UpdatePatch(&kp_ringspblock[i], "%s", buffer); - } - - HU_UpdatePatch(&kp_ringstickersplit[0], "SMRNGBGA"); - HU_UpdatePatch(&kp_ringstickersplit[1], "SMRNGBGB"); - - sprintf(buffer, "K_SRINGx"); - for (i = 0; i < 6; i++) - { - buffer[7] = '0'+(i+1); - HU_UpdatePatch(&kp_smallring[i], "%s", buffer); - } - - HU_UpdatePatch(&kp_ringdebtminussmall, "SRDEBTMN"); - - sprintf(buffer, "SPBRGSxx"); - for (i = 0; i < 16; i++) - { - buffer[6] = '0'+((i+1) / 10); - buffer[7] = '0'+((i+1) % 10); - HU_UpdatePatch(&kp_ringspblocksmall[i], "%s", buffer); - } - - // Speedometer - HU_UpdatePatch(&kp_speedometersticker, "K_SPDMBG"); - - sprintf(buffer, "K_SPDMLx"); - for (i = 0; i < 4; i++) - { - buffer[7] = '0'+(i+1); - HU_UpdatePatch(&kp_speedometerlabel[i], "%s", buffer); - } - - // Extra ranking icons - HU_UpdatePatch(&kp_rankbumper, "K_BLNICO"); - HU_UpdatePatch(&kp_bigbumper, "K_BLNREG"); - HU_UpdatePatch(&kp_tinybumper[0], "K_BLNA"); - HU_UpdatePatch(&kp_tinybumper[1], "K_BLNB"); - HU_UpdatePatch(&kp_ranknobumpers, "K_NOBLNS"); - HU_UpdatePatch(&kp_rankcapsule, "K_CAPICO"); - HU_UpdatePatch(&kp_rankemerald, "K_EMERC"); - HU_UpdatePatch(&kp_rankemeraldflash, "K_EMERW"); - HU_UpdatePatch(&kp_rankemeraldback, "K_EMERBK"); - HU_UpdatePatch(&kp_pts[0], "K_POINTS"); - HU_UpdatePatch(&kp_pts[1], "K_POINT4"); - - // Battle goal - HU_UpdatePatch(&kp_goal[0][0], "K_ST1GLA"); - HU_UpdatePatch(&kp_goal[1][0], "K_ST1GLB"); - HU_UpdatePatch(&kp_goal[0][1], "K_ST4GLA"); - HU_UpdatePatch(&kp_goal[1][1], "K_ST4GLB"); - HU_UpdatePatch(&kp_goalrod[0], "K_ST1GLD"); - HU_UpdatePatch(&kp_goalrod[1], "K_ST4GLD"); - HU_UpdatePatch(&kp_goaltext1p, "K_ST1GLC"); - - // Battle graphics - HU_UpdatePatch(&kp_battlewin, "K_BWIN"); - HU_UpdatePatch(&kp_battlecool, "K_BCOOL"); - HU_UpdatePatch(&kp_battlelose, "K_BLOSE"); - HU_UpdatePatch(&kp_battlewait, "K_BWAIT"); - HU_UpdatePatch(&kp_battleinfo, "K_BINFO"); - HU_UpdatePatch(&kp_wanted, "K_WANTED"); - HU_UpdatePatch(&kp_wantedsplit, "4PWANTED"); - HU_UpdatePatch(&kp_wantedreticle, "MMAPWANT"); - HU_UpdatePatch(&kp_minimapdot, "MMAPDOT"); - - // Kart Item Windows - HU_UpdatePatch(&kp_itembg[0], "K_ITBG"); - HU_UpdatePatch(&kp_itembg[1], "K_ITBGD"); - HU_UpdatePatch(&kp_itemtimer[0], "K_ITIMER"); - HU_UpdatePatch(&kp_itemmulsticker[0], "K_ITMUL"); - HU_UpdatePatch(&kp_itemx, "K_ITX"); - - HU_UpdatePatch(&kp_ringbg[0], "K_RBBG"); - HU_UpdatePatch(&kp_ringbg[1], "K_SBBG"); - - HU_UpdatePatch(&kp_sadface[0], "K_ITSAD"); - HU_UpdatePatch(&kp_sneaker[0], "K_ITSHOE"); - HU_UpdatePatch(&kp_rocketsneaker[0], "K_ITRSHE"); - - sprintf(buffer, "K_ITINVx"); - for (i = 0; i < 7; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_invincibility[i], "%s", buffer); - } - HU_UpdatePatch(&kp_banana[0], "K_ITBANA"); - HU_UpdatePatch(&kp_eggman[0], "K_ITEGGM"); - sprintf(buffer, "K_ITORBx"); - for (i = 0; i < 4; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_orbinaut[i], "%s", buffer); - } - HU_UpdatePatch(&kp_jawz[0], "K_ITJAWZ"); - HU_UpdatePatch(&kp_mine[0], "K_ITMINE"); - HU_UpdatePatch(&kp_landmine[0], "K_ITLNDM"); - HU_UpdatePatch(&kp_ballhog[0], "K_ITBHOG"); - HU_UpdatePatch(&kp_selfpropelledbomb[0], "K_ITSPB"); - HU_UpdatePatch(&kp_grow[0], "K_ITGROW"); - HU_UpdatePatch(&kp_shrink[0], "K_ITSHRK"); - HU_UpdatePatch(&kp_lightningshield[0], "K_ITTHNS"); - HU_UpdatePatch(&kp_bubbleshield[0], "K_ITBUBS"); - HU_UpdatePatch(&kp_flameshield[0], "K_ITFLMS"); - HU_UpdatePatch(&kp_hyudoro[0], "K_ITHYUD"); - HU_UpdatePatch(&kp_pogospring[0], "K_ITPOGO"); - HU_UpdatePatch(&kp_superring[0], "K_ITRING"); - HU_UpdatePatch(&kp_kitchensink[0], "K_ITSINK"); - HU_UpdatePatch(&kp_droptarget[0], "K_ITDTRG"); - HU_UpdatePatch(&kp_gardentop[0], "K_ITGTOP"); - HU_UpdatePatch(&kp_gachabom[0], "K_ITGBOM"); - HU_UpdatePatch(&kp_bar[0], "K_RBBAR"); - HU_UpdatePatch(&kp_doublebar[0], "K_RBBAR2"); - HU_UpdatePatch(&kp_triplebar[0], "K_RBBAR3"); - HU_UpdatePatch(&kp_slotring[0], "K_RBRING"); - HU_UpdatePatch(&kp_seven[0], "K_RBSEV"); - HU_UpdatePatch(&kp_jackpot[0], "K_RBJACK"); - - sprintf(buffer, "FSMFGxxx"); - for (i = 0; i < FLAMESHIELD_MAX; i++) - { - buffer[5] = '0'+((i+1)/100); - buffer[6] = '0'+(((i+1)/10)%10); - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_flameshieldmeter[i][0], "%s", buffer); - } - - sprintf(buffer, "FSMBGxxx"); - for (i = 0; i < FLAMESHIELD_MAX; i++) - { - buffer[5] = '0'+((i+1)/100); - buffer[6] = '0'+(((i+1)/10)%10); - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_flameshieldmeter_bg[i][0], "%s", buffer); - } - - // Splitscreen - HU_UpdatePatch(&kp_itembg[2], "K_ISBG"); - HU_UpdatePatch(&kp_itembg[3], "K_ISBGD"); - HU_UpdatePatch(&kp_itemtimer[1], "K_ISIMER"); - HU_UpdatePatch(&kp_itemmulsticker[1], "K_ISMUL"); - - HU_UpdatePatch(&kp_sadface[1], "K_ISSAD"); - HU_UpdatePatch(&kp_sneaker[1], "K_ISSHOE"); - HU_UpdatePatch(&kp_rocketsneaker[1], "K_ISRSHE"); - sprintf(buffer, "K_ISINVx"); - for (i = 0; i < 6; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_invincibility[i+7], "%s", buffer); - } - HU_UpdatePatch(&kp_banana[1], "K_ISBANA"); - HU_UpdatePatch(&kp_eggman[1], "K_ISEGGM"); - HU_UpdatePatch(&kp_orbinaut[4], "K_ISORBN"); - HU_UpdatePatch(&kp_jawz[1], "K_ISJAWZ"); - HU_UpdatePatch(&kp_mine[1], "K_ISMINE"); - HU_UpdatePatch(&kp_landmine[1], "K_ISLNDM"); - HU_UpdatePatch(&kp_ballhog[1], "K_ISBHOG"); - HU_UpdatePatch(&kp_selfpropelledbomb[1], "K_ISSPB"); - HU_UpdatePatch(&kp_grow[1], "K_ISGROW"); - HU_UpdatePatch(&kp_shrink[1], "K_ISSHRK"); - HU_UpdatePatch(&kp_lightningshield[1], "K_ISTHNS"); - HU_UpdatePatch(&kp_bubbleshield[1], "K_ISBUBS"); - HU_UpdatePatch(&kp_flameshield[1], "K_ISFLMS"); - HU_UpdatePatch(&kp_hyudoro[1], "K_ISHYUD"); - HU_UpdatePatch(&kp_pogospring[1], "K_ISPOGO"); - HU_UpdatePatch(&kp_superring[1], "K_ISRING"); - HU_UpdatePatch(&kp_kitchensink[1], "K_ISSINK"); - HU_UpdatePatch(&kp_droptarget[1], "K_ISDTRG"); - HU_UpdatePatch(&kp_gardentop[1], "K_ISGTOP"); - HU_UpdatePatch(&kp_gachabom[1], "K_ISGBOM"); - HU_UpdatePatch(&kp_bar[1], "K_SBBAR"); - HU_UpdatePatch(&kp_doublebar[1], "K_SBBAR2"); - HU_UpdatePatch(&kp_triplebar[1], "K_SBBAR3"); - HU_UpdatePatch(&kp_slotring[1], "K_SBRING"); - HU_UpdatePatch(&kp_seven[1], "K_SBSEV"); - HU_UpdatePatch(&kp_jackpot[1], "K_SBJACK"); - - sprintf(buffer, "FSMFSxxx"); - for (i = 0; i < 120; i++) - { - buffer[5] = '0'+((i+1)/100); - buffer[6] = '0'+(((i+1)/10)%10); - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_flameshieldmeter[i][1], "%s", buffer); - } - - sprintf(buffer, "FSMBS0xx"); - for (i = 0; i < 120; i++) - { - buffer[5] = '0'+((i+1)/100); - buffer[6] = '0'+(((i+1)/10)%10); - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_flameshieldmeter_bg[i][1], "%s", buffer); - } - - // 4P item spy - HU_UpdatePatch(&kp_itembg[4], "ISPYBG"); - HU_UpdatePatch(&kp_itembg[5], "ISPYBGD"); - - //HU_UpdatePatch(&kp_sadface[2], "ISPYSAD"); - HU_UpdatePatch(&kp_sneaker[2], "ISPYSHOE"); - HU_UpdatePatch(&kp_rocketsneaker[2], "ISPYRSHE"); - sprintf(buffer, "ISPYINVx"); - for (i = 0; i < 6; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_invincibility[i+13], "%s", buffer); - } - HU_UpdatePatch(&kp_banana[2], "ISPYBANA"); - HU_UpdatePatch(&kp_eggman[2], "ISPYEGGM"); - HU_UpdatePatch(&kp_orbinaut[5], "ISPYORBN"); - HU_UpdatePatch(&kp_jawz[2], "ISPYJAWZ"); - HU_UpdatePatch(&kp_mine[2], "ISPYMINE"); - HU_UpdatePatch(&kp_landmine[2], "ISPYLNDM"); - HU_UpdatePatch(&kp_ballhog[2], "ISPYBHOG"); - HU_UpdatePatch(&kp_selfpropelledbomb[2], "ISPYSPB"); - HU_UpdatePatch(&kp_grow[2], "ISPYGROW"); - HU_UpdatePatch(&kp_shrink[2], "ISPYSHRK"); - HU_UpdatePatch(&kp_lightningshield[2], "ISPYTHNS"); - HU_UpdatePatch(&kp_bubbleshield[2], "ISPYBUBS"); - HU_UpdatePatch(&kp_flameshield[2], "ISPYFLMS"); - HU_UpdatePatch(&kp_hyudoro[2], "ISPYHYUD"); - HU_UpdatePatch(&kp_pogospring[2], "ISPYPOGO"); - HU_UpdatePatch(&kp_superring[2], "ISPYRING"); - HU_UpdatePatch(&kp_kitchensink[2], "ISPYSINK"); - HU_UpdatePatch(&kp_droptarget[2], "ISPYDTRG"); - HU_UpdatePatch(&kp_gardentop[2], "ISPYGTOP"); - HU_UpdatePatch(&kp_gachabom[2], "ISPYGBOM"); - - // CHECK indicators - sprintf(buffer, "K_CHECKx"); - for (i = 0; i < 6; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_check[i], "%s", buffer); - } - - // Rival indicators - sprintf(buffer, "K_RIVALx"); - for (i = 0; i < 2; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_rival[i], "%s", buffer); - } - - // Rival indicators - sprintf(buffer, "K_SSPLxx"); - for (i = 0; i < 4; i++) - { - buffer[6] = 'A'+i; - for (j = 0; j < 2; j++) - { - buffer[7] = '1'+j; - HU_UpdatePatch(&kp_localtag[i][j], "%s", buffer); - } - } - - // Typing indicator - HU_UpdatePatch(&kp_talk, "K_TALK"); - HU_UpdatePatch(&kp_typdot, "K_TYPDOT"); - - // Eggman warning numbers - sprintf(buffer, "K_EGGNx"); - for (i = 0; i < 6; i++) - { - buffer[6] = '0'+i; - HU_UpdatePatch(&kp_eggnum[i], "%s", buffer); - } - - // First person mode - HU_UpdatePatch(&kp_fpview[0], "VIEWA0"); - HU_UpdatePatch(&kp_fpview[1], "VIEWB0D0"); - HU_UpdatePatch(&kp_fpview[2], "VIEWC0E0"); - - // Input UI Wheel - sprintf(buffer, "K_WHEELx"); - for (i = 0; i < 5; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_inputwheel[i], "%s", buffer); - } - - // HERE COMES A NEW CHALLENGER - sprintf(buffer, "K_CHALxx"); - for (i = 0; i < 25; i++) - { - buffer[6] = '0'+((i+1)/10); - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_challenger[i], "%s", buffer); - } - - // Lap start animation - sprintf(buffer, "K_LAP0x"); - for (i = 0; i < 7; i++) - { - buffer[6] = '0'+(i+1); - HU_UpdatePatch(&kp_lapanim_lap[i], "%s", buffer); - } - - sprintf(buffer, "K_LAPFxx"); - for (i = 0; i < 11; i++) - { - buffer[6] = '0'+((i+1)/10); - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_lapanim_final[i], "%s", buffer); - } - - sprintf(buffer, "K_LAPNxx"); - for (i = 0; i < 10; i++) - { - buffer[6] = '0'+i; - for (j = 0; j < 3; j++) - { - buffer[7] = '0'+(j+1); - HU_UpdatePatch(&kp_lapanim_number[i][j], "%s", buffer); - } - } - - sprintf(buffer, "K_LAPE0x"); - for (i = 0; i < 2; i++) - { - buffer[7] = '0'+(i+1); - HU_UpdatePatch(&kp_lapanim_emblem[i], "%s", buffer); - } - - sprintf(buffer, "K_LAPH0x"); - for (i = 0; i < 3; i++) - { - buffer[7] = '0'+(i+1); - HU_UpdatePatch(&kp_lapanim_hand[i], "%s", buffer); - } - - HU_UpdatePatch(&kp_yougotem, "YOUGOTEM"); - HU_UpdatePatch(&kp_itemminimap, "MMAPITEM"); - - sprintf(buffer, "ALAGLESx"); - for (i = 0; i < 10; ++i) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_alagles[i], "%s", buffer); - } - - sprintf(buffer, "BLAGLESx"); - for (i = 0; i < 6; ++i) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_blagles[i], "%s", buffer); - } - - HU_UpdatePatch(&kp_cpu[0], "K_CPU1"); - HU_UpdatePatch(&kp_cpu[1], "K_CPU2"); - - HU_UpdatePatch(&kp_nametagstem, "K_NAMEST"); - - HU_UpdatePatch(&kp_trickcool[0], "K_COOL1"); - HU_UpdatePatch(&kp_trickcool[1], "K_COOL2"); - - HU_UpdatePatch(&kp_autoroulette, "A11YITEM"); - HU_UpdatePatch(&kp_autoring, "A11YRING"); - - sprintf(buffer, "K_BOSB0x"); - for (i = 0; i < 8; i++) - { - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_bossbar[i], "%s", buffer); - } - - sprintf(buffer, "K_BOSR0x"); - for (i = 0; i < 4; i++) - { - buffer[7] = '0'+((i+1)%10); - HU_UpdatePatch(&kp_bossret[i], "%s", buffer); - } - - sprintf(buffer, "HCAPARxx"); - for (i = 0; i < 2; i++) - { - buffer[6] = 'A'+i; - - for (j = 0; j < 2; j++) - { - buffer[7] = '0'+j; - HU_UpdatePatch(&kp_capsuletarget_arrow[i][j], "%s", buffer); - } - } - - sprintf(buffer, "HUDCAPDx"); - for (i = 0; i < 2; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_capsuletarget_far_text[i], "%s", buffer); - } - - sprintf(buffer, "HUDCAPCx"); - for (i = 0; i < 2; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_capsuletarget_icon[i], "%s", buffer); - } - - sprintf(buffer, "HUDCAPBx"); - for (i = 0; i < 2; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_capsuletarget_far[0][i], "%s", buffer); - } - - sprintf(buffer, "HUDC4PBx"); - for (i = 0; i < 2; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_capsuletarget_far[1][i], "%s", buffer); - } - - sprintf(buffer, "HUDCAPAx"); - for (i = 0; i < 8; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_capsuletarget_near[0][i], "%s", buffer); - } - - sprintf(buffer, "HUDC4PAx"); - for (i = 0; i < 8; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_capsuletarget_near[1][i], "%s", buffer); - } - - sprintf(buffer, "HUDFLKAx"); - for (i = 0; i < 4; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_superflickytarget[0][i], "%s", buffer); - } - - sprintf(buffer, "H4PFLKAx"); - for (i = 0; i < 4; i++) - { - buffer[7] = '0'+i; - HU_UpdatePatch(&kp_superflickytarget[1][i], "%s", buffer); - } - - sprintf(buffer, "SPCNBFAx"); - for (i = 0; i < 6; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_spraycantarget_far[0][i], "%s", buffer); - } - - sprintf(buffer, "SPCNSFAx"); - for (i = 0; i < 6; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_spraycantarget_far[1][i], "%s", buffer); - } - - sprintf(buffer, "SPCNBCLx"); - for (i = 0; i < 6; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_spraycantarget_near[0][i], "%s", buffer); - } - - sprintf(buffer, "SPCNSCLx"); - for (i = 0; i < 6; i++) - { - buffer[7] = '1'+i; - HU_UpdatePatch(&kp_spraycantarget_near[1][i], "%s", buffer); - } - - K_LoadButtonGraphics(kp_button_a, "A"); - K_LoadButtonGraphics(kp_button_b, "B"); - K_LoadButtonGraphics(kp_button_c, "C"); - K_LoadButtonGraphics(kp_button_x, "X"); - K_LoadButtonGraphics(kp_button_y, "Y"); - K_LoadButtonGraphics(kp_button_z, "Z"); - K_LoadButtonGraphics(kp_button_l, "L1"); - K_LoadButtonGraphics(kp_button_r, "R1"); - K_LoadButtonGraphics(kp_button_up, "ARU"); - K_LoadButtonGraphics(kp_button_down, "ARD"); - K_LoadButtonGraphics(kp_button_right, "ARR"); - K_LoadButtonGraphics(kp_button_left, "ARL"); - K_LoadButtonGraphics(kp_button_start, "S"); - - K_LoadGenericButtonGraphics(kp_button_lua1, "LU1"); - K_LoadGenericButtonGraphics(kp_button_lua2, "LU2"); - K_LoadGenericButtonGraphics(kp_button_lua3, "LU3"); - - HU_UpdatePatch(&gen_button_keyleft[0], "TLK_L"); - HU_UpdatePatch(&gen_button_keyleft[1], "TLK_LB"); - HU_UpdatePatch(&gen_button_keyright[0], "TLK_R"); - HU_UpdatePatch(&gen_button_keyright[1], "TLK_RB"); - HU_UpdatePatch(&gen_button_keycenter[0], "TLK_M"); - HU_UpdatePatch(&gen_button_keycenter[1], "TLK_MB"); - - K_LoadGenericButtonGraphics(gen_button_dpad, "DP"); - K_LoadGenericButtonGraphics(gen_button_a, "A"); - K_LoadGenericButtonGraphics(gen_button_b, "B"); - K_LoadGenericButtonGraphics(gen_button_x, "X"); - K_LoadGenericButtonGraphics(gen_button_y, "Y"); - K_LoadGenericButtonGraphics(gen_button_lb, "L1"); - K_LoadGenericButtonGraphics(gen_button_rb, "R1"); - K_LoadGenericButtonGraphics(gen_button_lt, "L2"); - K_LoadGenericButtonGraphics(gen_button_rt, "R2"); - K_LoadGenericButtonGraphics(gen_button_ls, "L3"); - K_LoadGenericButtonGraphics(gen_button_rs, "R3"); - K_LoadGenericButtonGraphics(gen_button_start, "S"); - K_LoadGenericButtonGraphics(gen_button_back, "I"); -} - -// For the item toggle menu -const char *K_GetItemPatch(UINT8 item, boolean tiny) -{ - switch (item) - { - case KITEM_SNEAKER: - case KRITEM_DUALSNEAKER: - case KRITEM_TRIPLESNEAKER: - return (tiny ? "K_ISSHOE" : "K_ITSHOE"); - case KITEM_ROCKETSNEAKER: - return (tiny ? "K_ISRSHE" : "K_ITRSHE"); - case KITEM_INVINCIBILITY: - return (tiny ? "K_ISINV1" : "K_ITINV1"); - case KITEM_BANANA: - case KRITEM_TRIPLEBANANA: - return (tiny ? "K_ISBANA" : "K_ITBANA"); - case KITEM_EGGMAN: - return (tiny ? "K_ISEGGM" : "K_ITEGGM"); - case KITEM_ORBINAUT: - return (tiny ? "K_ISORBN" : "K_ITORB1"); - case KITEM_JAWZ: - case KRITEM_DUALJAWZ: - return (tiny ? "K_ISJAWZ" : "K_ITJAWZ"); - case KITEM_MINE: - return (tiny ? "K_ISMINE" : "K_ITMINE"); - case KITEM_LANDMINE: - return (tiny ? "K_ISLNDM" : "K_ITLNDM"); - case KITEM_BALLHOG: - return (tiny ? "K_ISBHOG" : "K_ITBHOG"); - case KITEM_SPB: - return (tiny ? "K_ISSPB" : "K_ITSPB"); - case KITEM_GROW: - return (tiny ? "K_ISGROW" : "K_ITGROW"); - case KITEM_SHRINK: - return (tiny ? "K_ISSHRK" : "K_ITSHRK"); - case KITEM_LIGHTNINGSHIELD: - return (tiny ? "K_ISTHNS" : "K_ITTHNS"); - case KITEM_BUBBLESHIELD: - return (tiny ? "K_ISBUBS" : "K_ITBUBS"); - case KITEM_FLAMESHIELD: - return (tiny ? "K_ISFLMS" : "K_ITFLMS"); - case KITEM_HYUDORO: - return (tiny ? "K_ISHYUD" : "K_ITHYUD"); - case KITEM_POGOSPRING: - return (tiny ? "K_ISPOGO" : "K_ITPOGO"); - case KITEM_SUPERRING: - return (tiny ? "K_ISRING" : "K_ITRING"); - case KITEM_KITCHENSINK: - return (tiny ? "K_ISSINK" : "K_ITSINK"); - case KITEM_DROPTARGET: - return (tiny ? "K_ISDTRG" : "K_ITDTRG"); - case KITEM_GARDENTOP: - return (tiny ? "K_ISGTOP" : "K_ITGTOP"); - case KITEM_GACHABOM: - case KRITEM_TRIPLEGACHABOM: - return (tiny ? "K_ISGBOM" : "K_ITGBOM"); - case KRITEM_TRIPLEORBINAUT: - return (tiny ? "K_ISORBN" : "K_ITORB3"); - case KRITEM_QUADORBINAUT: - return (tiny ? "K_ISORBN" : "K_ITORB4"); - default: - return (tiny ? "K_ISSAD" : "K_ITSAD"); - } -} - -static patch_t *K_GetCachedItemPatch(INT32 item, UINT8 offset) -{ - patch_t **kp[1 + NUMKARTITEMS] = { - kp_sadface, - NULL, - kp_sneaker, - kp_rocketsneaker, - kp_invincibility, - kp_banana, - kp_eggman, - kp_orbinaut, - kp_jawz, - kp_mine, - kp_landmine, - kp_ballhog, - kp_selfpropelledbomb, - kp_grow, - kp_shrink, - kp_lightningshield, - kp_bubbleshield, - kp_flameshield, - kp_hyudoro, - kp_pogospring, - kp_superring, - kp_kitchensink, - kp_droptarget, - kp_gardentop, - kp_gachabom, - }; - - if (item == KITEM_SAD || (item > KITEM_NONE && item < NUMKARTITEMS)) - return kp[item - KITEM_SAD][offset]; - else - return NULL; -} - -patch_t *K_GetSmallStaticCachedItemPatch(kartitems_t item) -{ - UINT8 offset; - - item = static_cast(K_ItemResultToType(item)); - - switch (item) - { - case KITEM_INVINCIBILITY: - offset = 7; - break; - - case KITEM_ORBINAUT: - offset = 4; - break; - - default: - offset = 1; - } - - return K_GetCachedItemPatch(item, offset); -} - -static patch_t *K_GetCachedSlotMachinePatch(INT32 item, UINT8 offset) -{ - patch_t **kp[KSM__MAX] = { - kp_bar, - kp_doublebar, - kp_triplebar, - kp_slotring, - kp_seven, - kp_jackpot, - }; - - if (item >= 0 && item < KSM__MAX) - return kp[item][offset]; - else - return NULL; -} - -//} - -INT32 ITEM_X, ITEM_Y; // Item Window -INT32 TIME_X, TIME_Y; // Time Sticker -INT32 LAPS_X, LAPS_Y; // Lap Sticker -INT32 POSI_X, POSI_Y; // Position Number -INT32 FACE_X, FACE_Y; // Top-four Faces -INT32 STCD_X, STCD_Y; // Starting countdown -INT32 CHEK_Y; // CHECK graphic -INT32 MINI_X, MINI_Y; // Minimap -INT32 WANT_X, WANT_Y; // Battle WANTED poster - -// This is for the P2 and P4 side of splitscreen. Then we'll flip P1's and P2's to the bottom with V_SPLITSCREEN. -INT32 ITEM2_X, ITEM2_Y; -INT32 LAPS2_X, LAPS2_Y; -INT32 POSI2_X, POSI2_Y; - -// trick "cool" -INT32 TCOOL_X, TCOOL_Y; - -// This version of the function was prototyped in Lua by Nev3r ... a HUGE thank you goes out to them! -void K_ObjectTracking(trackingResult_t *result, const vector3_t *point, boolean reverse) -{ -#define NEWTAN(x) FINETANGENT(((x + ANGLE_90) >> ANGLETOFINESHIFT) & 4095) // tan function used by Lua -#define NEWCOS(x) FINECOSINE((x >> ANGLETOFINESHIFT) & FINEMASK) - - angle_t viewpointAngle, viewpointAiming, viewpointRoll; - - INT32 screenWidth, screenHeight; - fixed_t screenHalfW, screenHalfH; - - const fixed_t baseFov = 90*FRACUNIT; - fixed_t fovDiff, fov, fovTangent, fg; - - fixed_t h; - INT32 da, dp; - - UINT8 cameraNum = R_GetViewNumber(); - - I_Assert(result != NULL); - I_Assert(point != NULL); - - // Initialize defaults - result->x = result->y = 0; - result->scale = FRACUNIT; - result->onScreen = false; - - // Take the view's properties as necessary. - if (reverse) - { - viewpointAngle = (INT32)(viewangle + ANGLE_180); - viewpointAiming = (INT32)InvAngle(aimingangle); - viewpointRoll = (INT32)viewroll; - } - else - { - viewpointAngle = (INT32)viewangle; - viewpointAiming = (INT32)aimingangle; - viewpointRoll = (INT32)InvAngle(viewroll); - } - - // Calculate screen size adjustments. - screenWidth = vid.width/vid.dupx; - screenHeight = vid.height/vid.dupy; - - if (r_splitscreen >= 2) - { - // Half-wide screens - screenWidth >>= 1; - } - - if (r_splitscreen >= 1) - { - // Half-tall screens - screenHeight >>= 1; - } - - screenHalfW = (screenWidth >> 1) << FRACBITS; - screenHalfH = (screenHeight >> 1) << FRACBITS; - - // Calculate FOV adjustments. - fovDiff = R_FOV(cameraNum) - baseFov; - fov = ((baseFov - fovDiff) / 2) - (stplyr->fovadd / 2); - fovTangent = NEWTAN(FixedAngle(fov)); - - if (r_splitscreen == 1) - { - // Splitscreen FOV is adjusted to maintain expected vertical view - fovTangent = 10*fovTangent/17; - } - - fg = (screenWidth >> 1) * fovTangent; - - // Determine viewpoint factors. - h = R_PointToDist2(point->x, point->y, viewx, viewy); - da = AngleDeltaSigned(viewpointAngle, R_PointToAngle2(viewx, viewy, point->x, point->y)); - dp = AngleDeltaSigned(viewpointAiming, R_PointToAngle2(0, 0, h, viewz)); - - if (reverse) - { - da = -(da); - } - - // Set results relative to top left! - result->x = FixedMul(NEWTAN(da), fg); - result->y = FixedMul((NEWTAN(viewpointAiming) - FixedDiv((point->z - viewz), 1 + FixedMul(NEWCOS(da), h))), fg); - - result->angle = da; - result->pitch = dp; - result->fov = fg; - - // Rotate for screen roll... - if (viewpointRoll) - { - fixed_t tempx = result->x; - viewpointRoll >>= ANGLETOFINESHIFT; - result->x = FixedMul(FINECOSINE(viewpointRoll), tempx) - FixedMul(FINESINE(viewpointRoll), result->y); - result->y = FixedMul(FINESINE(viewpointRoll), tempx) + FixedMul(FINECOSINE(viewpointRoll), result->y); - } - - // Flipped screen? - if (encoremode) - { - result->x = -result->x; - } - - // Center results. - result->x += screenHalfW; - result->y += screenHalfH; - - result->scale = FixedDiv(screenHalfW, h+1); - - result->onScreen = !((abs(da) > ANG60) || (abs(AngleDeltaSigned(viewpointAiming, R_PointToAngle2(0, 0, h, (viewz - point->z)))) > ANGLE_45)); - - // Cheap dirty hacks for some split-screen related cases - if (result->x < 0 || result->x > (screenWidth << FRACBITS)) - { - result->onScreen = false; - } - - if (result->y < 0 || result->y > (screenHeight << FRACBITS)) - { - result->onScreen = false; - } - - // adjust to non-green-resolution screen coordinates - result->x -= ((vid.width/vid.dupx) - BASEVIDWIDTH)<<(FRACBITS-((r_splitscreen >= 2) ? 2 : 1)); - result->y -= ((vid.height/vid.dupy) - BASEVIDHEIGHT)<<(FRACBITS-((r_splitscreen >= 1) ? 2 : 1)); - - return; - -#undef NEWTAN -#undef NEWCOS -} - -static void K_initKartHUD(void) -{ - /* - BASEVIDWIDTH = 320 - BASEVIDHEIGHT = 200 - - Item window graphic is 41 x 33 - - Time Sticker graphic is 116 x 11 - Time Font is a solid block of (8 x [12) x 14], equal to 96 x 14 - Therefore, timestamp is 116 x 14 altogether - - Lap Sticker is 80 x 11 - Lap flag is 22 x 20 - Lap Font is a solid block of (3 x [12) x 14], equal to 36 x 14 - Therefore, lapstamp is 80 x 20 altogether - - Position numbers are 43 x 53 - - Faces are 32 x 32 - Faces draw downscaled at 16 x 16 - Therefore, the allocated space for them is 16 x 67 altogether - - ---- - - ORIGINAL CZ64 SPLITSCREEN: - - Item window: - if (!splitscreen) { ICONX = 139; ICONY = 20; } - else { ICONX = BASEVIDWIDTH-315; ICONY = 60; } - - Time: 236, STRINGY( 12) - Lap: BASEVIDWIDTH-304, STRINGY(BASEVIDHEIGHT-189) - - */ - - // Single Screen (defaults) - // Item Window - ITEM_X = 5; // 5 - ITEM_Y = 5; // 5 - // Level Timer - TIME_X = BASEVIDWIDTH - 148; // 172 - TIME_Y = 9; // 9 - // Level Laps - LAPS_X = 9; // 9 - LAPS_Y = BASEVIDHEIGHT - 29; // 171 - // Position Number - POSI_X = BASEVIDWIDTH - 9; // 268 - POSI_Y = BASEVIDHEIGHT - 9; // 138 - // Top-Four Faces - FACE_X = 9; // 9 - FACE_Y = 92; // 92 - // Starting countdown - STCD_X = BASEVIDWIDTH/2; // 9 - STCD_Y = BASEVIDHEIGHT/2; // 92 - // CHECK graphic - CHEK_Y = BASEVIDHEIGHT; // 200 - // Minimap - MINI_X = BASEVIDWIDTH - 50; // 270 - MINI_Y = (BASEVIDHEIGHT/2)-16; // 84 - // Battle WANTED poster - WANT_X = BASEVIDWIDTH - 55; // 270 - WANT_Y = BASEVIDHEIGHT- 71; // 176 - - // trick COOL - TCOOL_X = (BASEVIDWIDTH)/2; - TCOOL_Y = (BASEVIDHEIGHT)/2 -10; - - if (r_splitscreen) // Splitscreen - { - ITEM_X = 5; - ITEM_Y = 3; - - LAPS_Y = (BASEVIDHEIGHT/2)-24; - - POSI_Y = (BASEVIDHEIGHT/2)- 2; - - STCD_Y = BASEVIDHEIGHT/4; - - MINI_X -= 16; - MINI_Y = (BASEVIDHEIGHT/2); - - if (r_splitscreen > 1) // 3P/4P Small Splitscreen - { - // 1P (top left) - ITEM_X = -9; - ITEM_Y = -8; - - LAPS_X = 3; - LAPS_Y = (BASEVIDHEIGHT/2)-12; - - POSI_X = 24; - POSI_Y = (BASEVIDHEIGHT/2)-26; - - // 2P (top right) - ITEM2_X = (BASEVIDWIDTH/2)-39; - ITEM2_Y = -8; - - LAPS2_X = (BASEVIDWIDTH/2)-43; - LAPS2_Y = (BASEVIDHEIGHT/2)-12; - - POSI2_X = (BASEVIDWIDTH/2)-4; - POSI2_Y = (BASEVIDHEIGHT/2)-26; - - // Reminder that 3P and 4P are just 1P and 2P splitscreen'd to the bottom. - - STCD_X = BASEVIDWIDTH/4; - - MINI_X = (3*BASEVIDWIDTH/4); - MINI_Y = (3*BASEVIDHEIGHT/4); - - TCOOL_X = (BASEVIDWIDTH)/4; - - if (r_splitscreen > 2) // 4P-only - { - MINI_X = (BASEVIDWIDTH/2); - MINI_Y = (BASEVIDHEIGHT/2); - } - } - } -} - -void K_DrawMapThumbnail(fixed_t x, fixed_t y, fixed_t width, UINT32 flags, UINT16 map, const UINT8 *colormap) -{ - patch_t *PictureOfLevel = NULL; - - if (map >= nummapheaders || !mapheaderinfo[map]) - { - PictureOfLevel = nolvl; - } - else if (!mapheaderinfo[map]->thumbnailPic) - { - PictureOfLevel = blanklvl; - } - else - { - PictureOfLevel = static_cast(mapheaderinfo[map]->thumbnailPic); - } - - K_DrawLikeMapThumbnail(x, y, width, flags, PictureOfLevel, colormap); -} - -void K_DrawLikeMapThumbnail(fixed_t x, fixed_t y, fixed_t width, UINT32 flags, patch_t *patch, const UINT8 *colormap) -{ - if (flags & V_FLIP) - x += width; - - V_DrawFixedPatch( - x, y, - FixedDiv(width, (320 << FRACBITS)), - flags, - patch, - colormap - ); -} - -// see also K_DrawNameTagItemSpy -static void K_drawKartItem(void) -{ - // ITEM_X = BASEVIDWIDTH-50; // 270 - // ITEM_Y = 24; // 24 - - // Why write V_DrawScaledPatch calls over and over when they're all the same? - // Set to 'no item' just in case. - const UINT8 offset = ((r_splitscreen > 1) ? 1 : 0); - patch_t *localpatch[3] = { kp_nodraw, kp_nodraw, kp_nodraw }; - UINT8 localamt[3] = {0, 0, 0}; - patch_t *localbg = ((offset) ? kp_itembg[2] : kp_itembg[0]); - patch_t *localinv = ((offset) ? kp_invincibility[((leveltime % (6*3)) / 3) + 7] : kp_invincibility[(leveltime % (7*3)) / 3]); - INT32 fx = 0, fy = 0, fflags = 0; // final coords for hud and flags... - const INT32 numberdisplaymin = ((!offset && stplyr->itemtype == KITEM_ORBINAUT) ? 5 : 2); - INT32 itembar = 0; - INT32 maxl = 0; // itembar's normal highest value - const INT32 barlength = (offset ? 12 : 26); - skincolornum_t localcolor[3] = { static_cast(stplyr->skincolor) }; - SINT8 colormode[3] = { TC_RAINBOW }; - boolean flipamount = false; // Used for 3P/4P splitscreen to flip item amount stuff - - fixed_t rouletteOffset = 0; - fixed_t rouletteSpace = ROULETTE_SPACING; - vector2_t rouletteCrop = {7, 7}; - INT32 i; - - boolean flashOnOne = false; - boolean flashOnTwo = false; - - if (stplyr->itemRoulette.itemListLen > 0) - { - // Init with item roulette stuff. - for (i = 0; i < 3; i++) - { - const SINT8 indexOfs = i-1; - const size_t index = (stplyr->itemRoulette.itemListLen + (stplyr->itemRoulette.index + indexOfs)) % stplyr->itemRoulette.itemListLen; - - const SINT8 result = stplyr->itemRoulette.itemList[index]; - const SINT8 item = K_ItemResultToType(result); - const boolean usingDebugItemAmount = cv_kartdebugitem.value != KITEM_NONE && cv_kartdebugitem.value == item && cv_kartdebugamount.value > 1; - const UINT8 amt = usingDebugItemAmount ? cv_kartdebugamount.value : K_ItemResultToAmount(result, &stplyr->itemRoulette); - - switch (item) - { - case KITEM_INVINCIBILITY: - localpatch[i] = localinv; - localamt[i] = amt; - break; - - case KITEM_ORBINAUT: - localpatch[i] = kp_orbinaut[(offset ? 4 : std::min(amt-1, 3))]; - if (amt > 4) - localamt[i] = amt; - break; - - default: - localpatch[i] = K_GetCachedItemPatch(item, offset); - if (item != KITEM_BALLHOG || amt != 5) - localamt[i] = amt; - break; - } - } - } - - if (stplyr->itemRoulette.active == true) - { - rouletteOffset = K_GetRouletteOffset(&stplyr->itemRoulette, rendertimefrac, 0); - } - else - { - // I'm doing this a little weird and drawing mostly in reverse order - // The only actual reason is to make sneakers line up this way in the code below - // This shouldn't have any actual baring over how it functions - // Hyudoro is first, because we're drawing it on top of the player's current item - - localcolor[1] = SKINCOLOR_NONE; - rouletteOffset = stplyr->karthud[khud_rouletteoffset]; - - if (stplyr->stealingtimer < 0) - { - localpatch[1] = kp_hyudoro[offset]; - flashOnTwo = true; - } - else if ((stplyr->stealingtimer > 0) && (leveltime & 2)) - { - localpatch[1] = kp_hyudoro[offset]; - } - else if (stplyr->eggmanexplode > 1) - { - localpatch[1] = kp_eggman[offset]; - flashOnOne = true; - } - else if (stplyr->ballhogcharge > 0) - { - // itembar = stplyr->ballhogcharge; - // maxl = (((stplyr->itemamount-1) * BALLHOGINCREMENT) + 1); - - itembar = stplyr->ballhogcharge % BALLHOGINCREMENT; - maxl = BALLHOGINCREMENT; - - localpatch[1] = kp_ballhog[offset]; - flashOnOne = true; - } - else if (stplyr->rocketsneakertimer > 1) - { - itembar = stplyr->rocketsneakertimer; - maxl = (itemtime*3) - barlength; - - localpatch[1] = kp_rocketsneaker[offset]; - flashOnOne = true; - } - else if (stplyr->sadtimer > 0) - { - localpatch[1] = kp_sadface[offset]; - flashOnTwo = true; - } - else if (stplyr->itemRoulette.reserved > 0) - { - localpatch[1] = kp_nodraw; - } - else - { - if (stplyr->itemamount <= 0) - return; - - switch (stplyr->itemtype) - { - case KITEM_INVINCIBILITY: - localpatch[1] = localinv; - localbg = kp_itembg[offset+1]; - break; - - case KITEM_ORBINAUT: - localpatch[1] = kp_orbinaut[(offset ? 4 : std::min(stplyr->itemamount-1, 3))]; - break; - - case KITEM_SPB: - case KITEM_LIGHTNINGSHIELD: - case KITEM_BUBBLESHIELD: - case KITEM_FLAMESHIELD: - localbg = kp_itembg[offset+1]; - /*FALLTHRU*/ - - default: - localpatch[1] = K_GetCachedItemPatch(stplyr->itemtype, offset); - - if (localpatch[1] == NULL) - localpatch[1] = kp_nodraw; // diagnose underflows - break; - } - - if ((stplyr->itemflags & IF_ITEMOUT)) - flashOnOne = true; - } - - if (!cv_reducevfx.value) - { - if (flashOnOne && !(leveltime & 1)) - localpatch[1] = kp_nodraw; - else if (flashOnTwo && !(leveltime & 2)) - localpatch[1] = kp_nodraw; - } - - if (stplyr->karthud[khud_itemblink] && (leveltime & 1) && !(cv_reducevfx.value)) - { - colormode[1] = TC_BLINK; - - switch (stplyr->karthud[khud_itemblinkmode]) - { - case 2: - localcolor[1] = static_cast(K_RainbowColor(leveltime)); - break; - case 1: - localcolor[1] = SKINCOLOR_RED; - break; - default: - localcolor[1] = SKINCOLOR_WHITE; - break; - } - } - else - { - // Hide the other items. - // Effectively lets the other roulette items - // show flicker away after you select. - localpatch[0] = localpatch[2] = kp_nodraw; - } - } - - // pain and suffering defined below - if (offset) - { - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - fx = ITEM_X; - fy = ITEM_Y; - fflags = V_SNAPTOLEFT|V_SNAPTOTOP|V_SPLITSCREEN; - } - else // else, that means we're P2 or P4. - { - fx = ITEM2_X; - fy = ITEM2_Y; - fflags = V_SNAPTORIGHT|V_SNAPTOTOP|V_SPLITSCREEN; - flipamount = true; - } - - rouletteSpace = ROULETTE_SPACING_SPLITSCREEN; - rouletteOffset = FixedMul(rouletteOffset, FixedDiv(ROULETTE_SPACING_SPLITSCREEN, ROULETTE_SPACING)); - rouletteCrop.x = 16; - rouletteCrop.y = 15; - } - else - { - fx = ITEM_X; - fy = ITEM_Y; - fflags = V_SNAPTOTOP|V_SNAPTOLEFT|V_SPLITSCREEN; - } - - if (r_splitscreen == 1) - { - fy -= 5; - } - - UINT8 *boxmap = NULL; - if (stplyr->itemRoulette.active && (stplyr->itemRoulette.speed - stplyr->itemRoulette.tics < 3) && stplyr->itemRoulette.index == 0) - { - boxmap = R_GetTranslationColormap(TC_ALLWHITE, SKINCOLOR_WHITE, GTC_CACHE); - } - V_DrawMappedPatch(fx, fy, V_HUDTRANS|V_SLIDEIN|fflags, localbg, boxmap); - - // Need to draw these in a particular order, for sorting. - V_SetClipRect( - (fx + rouletteCrop.x) << FRACBITS, (fy + rouletteCrop.y) << FRACBITS, - rouletteSpace, rouletteSpace, - V_SLIDEIN|fflags - ); - - auto draw_item = [&](fixed_t y, int i) - { - const UINT8 *colormap = (localcolor[i] ? R_GetTranslationColormap(colormode[i], localcolor[i], GTC_CACHE) : NULL); - V_DrawFixedPatch( - fx< 1) - { - using srb2::Draw; - Draw( - fx + rouletteCrop.x + FixedToFloat(rouletteSpace/2), - fy + rouletteCrop.y + FixedToFloat(rouletteOffset + y + rouletteSpace) - (r_splitscreen > 1 ? 15 : 33)) - .font(r_splitscreen > 1 ? Draw::Font::kRollingNum4P : Draw::Font::kRollingNum) - .align(Draw::Align::kCenter) - .flags(V_HUDTRANS|V_SLIDEIN|fflags) - .colormap(colormap) - .text("{}", localamt[i]); - } - }; - - draw_item(rouletteSpace, 0); - draw_item(-rouletteSpace, 2); - - if (stplyr->itemRoulette.active == true) - { - // Draw the item underneath the box. - draw_item(0, 1); - V_ClearClipRect(); - } - else - { - // Draw the item above the box. - V_ClearClipRect(); - - // A little goofy, but helps with ballhog charge conveyance—you're "loading" them. - UINT8 fakeitemamount = stplyr->itemamount - (stplyr->ballhogcharge / BALLHOGINCREMENT); - - boolean transflag = V_HUDTRANS; - - if (cv_reducevfx.value && (flashOnOne || flashOnTwo)) - { - transflag = V_HUDTRANSHALF; - } - - if (fakeitemamount >= numberdisplaymin && stplyr->itemRoulette.active == false) - { - // Then, the numbers: - V_DrawScaledPatch( - fx + (flipamount ? 48 : 0), fy, - V_HUDTRANS|V_SLIDEIN|fflags|(flipamount ? V_FLIP : 0), - kp_itemmulsticker[offset] - ); // flip this graphic for p2 and p4 in split and shift it. - - V_DrawFixedPatch( - fx< 2) - { - V_DrawFill(fx+x+length, fy+y+1, 1, height, 12|fflags); // the right one - if (height == 2) - V_DrawFill(fx+x+2, fy+y+2, length-2, 1, 8|fflags); // the dulled underside - V_DrawFill(fx+x+2, fy+y+1, length-2, 1, 0|fflags); // the shine - } - } - - // Quick Eggman numbers - if (stplyr->eggmanexplode > 1) - V_DrawScaledPatch(fx+17, fy+13-offset, V_HUDTRANS|V_SLIDEIN|fflags, kp_eggnum[std::min(5, G_TicsToSeconds(stplyr->eggmanexplode))]); - - if (stplyr->itemtype == KITEM_FLAMESHIELD && stplyr->flamelength > 0) - { - INT32 numframes = FLAMESHIELD_MAX; - INT32 absolutemax = numframes; - INT32 flamemax = stplyr->flamelength; - INT32 flamemeter = std::min(static_cast(stplyr->flamemeter), flamemax); - - INT32 bf = numframes - stplyr->flamelength; - INT32 ff = numframes - ((flamemeter * numframes) / absolutemax); - - INT32 xo = 6, yo = 4; - INT32 flip = 0; - - if (offset) - { - xo++; - - if (!(R_GetViewNumber() & 1)) // Flip for P1 and P3 (yes, that's correct) - { - xo -= 62; - flip = V_FLIP; - } - } - - /* - INT32 fmin = (8 * (bf-1)); - if (ff < fmin) - ff = fmin; - */ - - if (bf >= 0 && bf < numframes) - V_DrawScaledPatch(fx-xo, fy-yo, V_HUDTRANS|V_SLIDEIN|fflags|flip, kp_flameshieldmeter_bg[bf][offset]); - - if (ff >= 0 && ff < numframes && stplyr->flamemeter > 0) - { - if ((stplyr->flamemeter > flamemax) && (leveltime & 1)) - { - UINT8 *fsflash = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_WHITE, GTC_CACHE); - V_DrawMappedPatch(fx-xo, fy-yo, V_HUDTRANS|V_SLIDEIN|fflags|flip, kp_flameshieldmeter[ff][offset], fsflash); - } - else - { - V_DrawScaledPatch(fx-xo, fy-yo, V_HUDTRANS|V_SLIDEIN|fflags|flip, kp_flameshieldmeter[ff][offset]); - } - } - } -} - -static void K_drawKartSlotMachine(void) -{ - // ITEM_X = BASEVIDWIDTH-50; // 270 - // ITEM_Y = 24; // 24 - - // Why write V_DrawScaledPatch calls over and over when they're all the same? - // Set to 'no item' just in case. - const UINT8 offset = ((r_splitscreen > 1) ? 1 : 0); - - patch_t *localpatch[3] = { kp_nodraw, kp_nodraw, kp_nodraw }; - patch_t *localbg = offset ? kp_ringbg[1] : kp_ringbg[0]; - - // == SHITGARBAGE UNLIMITED 2: RISE OF MY ASS == - // FIVE LAYERS OF BULLSHIT PER-PIXEL SHOVING BECAUSE THE PATCHES HAVE DIFFERENT OFFSETS - // IF YOU ARE HERE TO ADJUST THE RINGBOX HUD TURN OFF YOUR COMPUTER AND GO TO YOUR LOCAL PARK - - INT32 fx = 0, fy = 0, fflags = 0; // final coords for hud and flags... - INT32 boxoffx = 0; - INT32 boxoffy = -6; - INT32 vstretch = 0; - INT32 hstretch = 3; - INT32 splitbsx = 0, splitbsy = 0; - skincolornum_t localcolor[3] = { static_cast(stplyr->skincolor) }; - SINT8 colormode[3] = { TC_RAINBOW }; - - fixed_t rouletteOffset = 0; - fixed_t rouletteSpace = SLOT_SPACING; - vector2_t rouletteCrop = {10, 10}; - INT32 i; - - if (stplyr->itemRoulette.itemListLen > 0) - { - // Init with item roulette stuff. - for (i = 0; i < 3; i++) - { - const SINT8 indexOfs = i-1; - const size_t index = (stplyr->itemRoulette.itemListLen + (stplyr->itemRoulette.index + indexOfs)) % stplyr->itemRoulette.itemListLen; - - const SINT8 result = stplyr->itemRoulette.itemList[index]; - - localpatch[i] = K_GetCachedSlotMachinePatch(result, offset); - } - } - - if (stplyr->itemRoulette.active == true) - { - rouletteOffset = K_GetSlotOffset(&stplyr->itemRoulette, rendertimefrac, 0); - } - else - { - rouletteOffset = stplyr->karthud[khud_rouletteoffset]; - - if (!stplyr->ringboxdelay) - { - return; - } - } - - if (stplyr->karthud[khud_itemblink] && (leveltime & 1) && !cv_reducevfx.value) - { - colormode[1] = TC_BLINK; - localcolor[1] = SKINCOLOR_WHITE; - - // This looks kinda wild with the white-background patch. - /* - switch (stplyr->ringboxaward) - { - case 5: // JACKPOT! - localcolor[1] = K_RainbowColor(leveltime); - break; - default: - localcolor[1] = SKINCOLOR_WHITE; - break; - } - */ - } - - // pain and suffering defined below - if (offset) - { - boxoffx -= 4; - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - fx = ITEM_X + 10; - fy = ITEM_Y + 10; - fflags = V_SNAPTOLEFT|V_SNAPTOTOP|V_SPLITSCREEN; - } - else // else, that means we're P2 or P4. - { - fx = ITEM2_X + 7; - fy = ITEM2_Y + 10; - fflags = V_SNAPTORIGHT|V_SNAPTOTOP|V_SPLITSCREEN; - } - - rouletteSpace = SLOT_SPACING_SPLITSCREEN; - rouletteOffset = FixedMul(rouletteOffset, FixedDiv(SLOT_SPACING_SPLITSCREEN, SLOT_SPACING)); - rouletteCrop.x = 16; - rouletteCrop.y = 13; - splitbsx = -6; - splitbsy = -6; - boxoffy += 2; - hstretch = 0; - } - else - { - fx = ITEM_X; - fy = ITEM_Y; - fflags = V_SNAPTOTOP|V_SNAPTOLEFT|V_SPLITSCREEN; - } - - if (r_splitscreen == 1) - { - fy -= 5; - } - - V_DrawScaledPatch(fx, fy, V_HUDTRANS|V_SLIDEIN|fflags, localbg); - - V_SetClipRect( - ((fx + rouletteCrop.x + boxoffx + splitbsx) << FRACBITS), ((fy + rouletteCrop.y + boxoffy - vstretch + splitbsy) << FRACBITS), - rouletteSpace + (hstretch< 0) - { - if (drawtime >= timelimitintics) - { - jitter = 2; - if (drawtime & 2) - jitter = -jitter; - drawtime = 0; - } - else - { - drawtime = timelimitintics - drawtime; - if (secretextratime) - ; - else if (extratimeintics) - { - jitter = 2; - if (leveltime & 1) - jitter = -jitter; - } - else if (drawtime <= 5*TICRATE) - { - jitter = ((drawtime <= 3*TICRATE) && (((drawtime-1) % TICRATE) >= TICRATE-2)) - ? 3 : 1; - if (drawtime & 2) - jitter = -jitter; - } - } - } - } - - if (return_jitter) - { - *return_jitter = jitter; - } - - return drawtime; -} - -INT32 K_drawKartMicroTime(const char *todrawtext, INT32 workx, INT32 worky, INT32 splitflags) -{ - using srb2::Draw; - Draw::TextElement text(todrawtext); - text.flags(splitflags); - text.font(Draw::Font::kZVote); - - INT32 result = text.width(); - Draw(workx - result, worky).text(text); - - return result; -} - -void K_drawKartTimestamp(tic_t drawtime, INT32 TX, INT32 TY, INT32 splitflags, UINT8 mode) -{ - // TIME_X = BASEVIDWIDTH-124; // 196 - // TIME_Y = 6; // 6 - - INT32 jitter = 0; - - drawtime = K_TranslateTimer(drawtime, mode, &jitter); - - V_DrawScaledPatch(TX, TY, splitflags, ((mode == 2) ? kp_lapstickerwide : kp_timestickerwide)); - - TX += 33; - - if (drawtime == UINT32_MAX) - ; - else if (mode && !drawtime) - { - // apostrophe location _'__ __ - V_DrawTimerString(TX+24, TY+3, splitflags, va("'")); - - // quotation mark location _ __"__ - V_DrawTimerString(TX+60, TY+3, splitflags, va("\"")); - } - else - { - tic_t worktime = drawtime/(60*TICRATE); - - if (worktime >= 100) - { - jitter = (drawtime & 1 ? 1 : -1); - worktime = 99; - drawtime = (100*(60*TICRATE))-1; - } - - // minutes time 00 __ __ - V_DrawTimerString(TX, TY+3+jitter, splitflags, va("%d", worktime/10)); - V_DrawTimerString(TX+12, TY+3-jitter, splitflags, va("%d", worktime%10)); - - // apostrophe location _'__ __ - V_DrawTimerString(TX+24, TY+3, splitflags, va("'")); - - worktime = (drawtime/TICRATE % 60); - - // seconds time _ 00 __ - V_DrawTimerString(TX+36, TY+3+jitter, splitflags, va("%d", worktime/10)); - V_DrawTimerString(TX+48, TY+3-jitter, splitflags, va("%d", worktime%10)); - - // quotation mark location _ __"__ - V_DrawTimerString(TX+60, TY+3, splitflags, va("\"")); - - worktime = G_TicsToCentiseconds(drawtime); - - // tics _ __ 00 - V_DrawTimerString(TX+72, TY+3+jitter, splitflags, va("%d", worktime/10)); - V_DrawTimerString(TX+84, TY+3-jitter, splitflags, va("%d", worktime%10)); - } - - // Medal data! - if ((modeattacking || (mode == 1)) - && !demo.playback) - { - INT32 workx = TX + 96, worky = TY+18; - UINT8 i = stickermedalinfo.visiblecount; - - if (stickermedalinfo.targettext[0] != '\0') - { - if (!mode) - { - if (stickermedalinfo.jitter) - { - jitter = stickermedalinfo.jitter+3; - if (jitter & 2) - workx += jitter/4; - else - workx -= jitter/4; - } - - if (stickermedalinfo.norecord == true) - { - splitflags = (splitflags &~ V_HUDTRANS)|V_HUDTRANSHALF; - } - } - - workx -= K_drawKartMicroTime(stickermedalinfo.targettext, workx, worky, splitflags); - } - - workx -= (((1 + i - stickermedalinfo.platinumcount)*6) - 1); - - if (!mode) - splitflags = (splitflags &~ V_HUDTRANSHALF)|V_HUDTRANS; - while (i > 0) - { - i--; - - if (gamedata->collected[(stickermedalinfo.emblems[i]-emblemlocations)]) - { - V_DrawMappedPatch(workx, worky, splitflags, - static_cast(W_CachePatchName(M_GetEmblemPatch(stickermedalinfo.emblems[i], false), PU_CACHE)), - R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(stickermedalinfo.emblems[i]), GTC_CACHE) - ); - - workx += 6; - } - else if ( - stickermedalinfo.emblems[i]->type != ET_TIME - || stickermedalinfo.emblems[i]->tag != AUTOMEDAL_PLATINUM - ) - { - V_DrawMappedPatch(workx, worky, splitflags, - static_cast(W_CachePatchName("NEEDIT", PU_CACHE)), - NULL - ); - - workx += 6; - } - } - } - - if (modeattacking & ATTACKING_SPB && stplyr->SPBdistance > 0) - { - UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, static_cast(stplyr->skincolor), GTC_CACHE); - INT32 ybar = 180; - INT32 widthbar = 120, xbar = 160 - widthbar/2, currentx; - INT32 barflags = V_SNAPTOBOTTOM|V_SLIDEIN; - INT32 transflags = ((6)<skin][FACE_MINIMAP], colormap); - - // vibes-based math - currentx = (stplyr->SPBdistance/mapobjectscale - mobjinfo[MT_SPB].radius/FRACUNIT - mobjinfo[MT_PLAYER].radius/FRACUNIT) * 6; - if (currentx > 0) - { - currentx = sqrt(currentx); - if (currentx > widthbar) - currentx = widthbar; - } - else - { - currentx = 0; - } - V_DrawScaledPatch(160 + widthbar/2 - currentx - 5, ybar - 7, barflags, kp_spbminimap); - } -} - -static fixed_t K_DrawKartPositionNumPatch(UINT8 num, UINT8 splitIndex, UINT8 *color, fixed_t x, fixed_t y, fixed_t scale, INT32 flags) -{ - fixed_t w = FRACUNIT; - fixed_t h = FRACUNIT; - INT32 overlayFlags[2]; - INT32 i; - - if (num > 9) - { - return x; // invalid input - } - - if ((mapheaderinfo[gamemap - 1]->levelflags & LF_SUBTRACTNUM) == LF_SUBTRACTNUM) - { - overlayFlags[0] = V_SUBTRACT; - overlayFlags[1] = V_ADD; - } - else - { - overlayFlags[0] = V_ADD; - overlayFlags[1] = V_SUBTRACT; - } - - w = SHORT(kp_positionnum[num][0][splitIndex]->width) * scale; - h = SHORT(kp_positionnum[num][0][splitIndex]->height) * scale; - - x -= w; - - if (flags & V_SNAPTOBOTTOM) - { - y -= h; - } - - for (i = 1; i >= 0; i--) - { - V_DrawFixedPatch( - x, y, scale, - flags | overlayFlags[i], - kp_positionnum[num][i][splitIndex], - color - ); - } - - return x; -} - -void K_DrawKartPositionNumXY( - UINT8 num, - UINT8 splitIndex, - fixed_t fx, fixed_t fy, fixed_t scale, INT32 fflags, - tic_t counter, boolean subtract, - boolean exit, boolean lastLap, boolean losing - ) -{ - if (cv_reducevfx.value != 0) - { - // Reduce the flashing rate - counter /= 4; - } - - counter /= 3; // Alternate colors every three frames - - UINT8 *color = NULL; - if (exit && num == 1) - { - // 1st place winner? You get rainbows!! - color = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_POSNUM_BEST1 + (counter % 6)), GTC_CACHE); - } - else if (exit || lastLap) - { - // On the final lap, or already won. - boolean useRedNums = losing; - - if (subtract) - { - // Subtracting RED will look BLUE, and vice versa. - useRedNums = !useRedNums; - } - - if (useRedNums == true) - { - color = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_POSNUM_LOSE1 + (counter % 3)), GTC_CACHE); - } - else - { - color = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_POSNUM_WIN1 + (counter % 3)), GTC_CACHE); - } - } - else - { - color = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_POSNUM, GTC_CACHE); - } - - if ((fflags & V_SNAPTORIGHT) == 0) - { - UINT8 adjustNum = num; - do - { - fixed_t w = SHORT(kp_positionnum[adjustNum % 10][0][splitIndex]->width) * scale; - fx += w; - adjustNum /= 10; - } while (adjustNum); - } - - // Draw the number - do - { - fx = K_DrawKartPositionNumPatch( - (num % 10), splitIndex, color, - fx, fy, scale, V_SPLITSCREEN|fflags - ); - num /= 10; - } while (num); -} - -static void K_DrawKartPositionNum(UINT8 num) -{ - UINT8 splitIndex = (r_splitscreen > 0) ? 1 : 0; - fixed_t scale = FRACUNIT; - fixed_t fx = 0, fy = 0; - transnum_t trans = static_cast(0); - INT32 fflags = 0; - - if (stplyr->lives <= 0 && stplyr->playerstate == PST_DEAD) - { - return; - } - - if (leveltime < (starttime + NUMTRANSMAPS)) - { - trans = static_cast((starttime + NUMTRANSMAPS) - leveltime); - } - - if (trans >= NUMTRANSMAPS) - { - return; - } - - if (stplyr->positiondelay > 0 || K_PlayerTallyActive(stplyr) == true) - { - const UINT8 delay = (stplyr->exiting) ? POS_DELAY_TIME : stplyr->positiondelay; - const fixed_t add = (scale * 3) >> ((r_splitscreen == 1) ? 1 : 2); - scale += std::min((add * (delay * delay)) / (POS_DELAY_TIME * POS_DELAY_TIME), add); - } - - // pain and suffering defined below - if (!r_splitscreen) - { - fx = BASEVIDWIDTH << FRACBITS; - fy = BASEVIDHEIGHT << FRACBITS; - fflags = V_SNAPTOBOTTOM|V_SNAPTORIGHT; - } - else if (r_splitscreen == 1) // for this splitscreen, we'll use case by case because it's a bit different. - { - fx = BASEVIDWIDTH << FRACBITS; - - if (R_GetViewNumber() == 0) - { - // for player 1: display this at the top right, above the minimap. - fy = 0; - fflags = V_SNAPTOTOP|V_SNAPTORIGHT; - } - else - { - // if we're not p1, that means we're p2. display this at the bottom right, below the minimap. - fy = BASEVIDHEIGHT << FRACBITS; - fflags = V_SNAPTOBOTTOM|V_SNAPTORIGHT; - } - - fy >>= 1; - } - else - { - fy = BASEVIDHEIGHT << FRACBITS; - - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - // If we are P1 or P3... - fx = 0; - fflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM; - } - else - { - // else, that means we're P2 or P4. - fx = BASEVIDWIDTH << FRACBITS; - fflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM; - } - - fx >>= 1; - fy >>= 1; - - // We're putting it in the same corner as - // the rest of our HUD, so it needs raised. - fy -= (21 << FRACBITS); - } - - if (trans > 0) - { - fflags |= (trans << V_ALPHASHIFT); - } - - K_DrawKartPositionNumXY( - num, - splitIndex, - fx, fy, scale, fflags, - leveltime, - ((mapheaderinfo[gamemap - 1]->levelflags & LF_SUBTRACTNUM) == LF_SUBTRACTNUM), - stplyr->exiting, - (stplyr->laps >= numlaps), - K_IsPlayerLosing(stplyr) - ); -} - -struct PositionFacesInfo -{ - INT32 ranklines = 0; - INT32 strank = -1; - INT32 numplayersingame = 0; - INT32 rankplayer[MAXPLAYERS] = {}; - - PositionFacesInfo(); - void draw_1p(); - void draw_4p_battle(int x, int y, INT32 flags); - - player_t* top() const { return &players[rankplayer[0]]; } - UINT32 top_score() const { return G_TeamOrIndividualScore( top() ); } - - bool near_goal() const - { - if (g_pointlimit == 0) - return false; - constexpr tic_t kThreshold = 5; - return std::max(kThreshold, g_pointlimit) - kThreshold <= top_score(); - } - - skincolornum_t vomit_color() const - { - if (!near_goal()) - { - return static_cast(top()->skincolor); - } - - constexpr int kCycleSpeed = 4; - constexpr std::array kColors = { - SKINCOLOR_RED, - SKINCOLOR_VOMIT, - SKINCOLOR_YELLOW, - SKINCOLOR_GREEN, - SKINCOLOR_JET, - SKINCOLOR_MOONSET, - }; - return kColors[leveltime / kCycleSpeed % kColors.size()]; - } -}; - -PositionFacesInfo::PositionFacesInfo() -{ - INT32 i, j; - - for (i = 0; i < MAXPLAYERS; i++) - { - rankplayer[i] = -1; - - if (!playeringame[i] || players[i].spectator || !players[i].mo) - continue; - - numplayersingame++; - } - - if (numplayersingame <= 1) - return; - - boolean completed[MAXPLAYERS] = {}; - - for (j = 0; j < numplayersingame; j++) - { - UINT8 lowestposition = MAXPLAYERS+1; - for (i = 0; i < MAXPLAYERS; i++) - { - if (completed[i] || !playeringame[i] || players[i].spectator || !players[i].mo) - continue; - - if (players[i].position >= lowestposition) - continue; - - rankplayer[ranklines] = i; - lowestposition = players[i].position; - } - - i = rankplayer[ranklines]; - - completed[i] = true; - - if (players+i == stplyr) - strank = ranklines; - - //if (ranklines == 5) - //break; // Only draw the top 5 players -- we do this a different way now... - - ranklines++; - } -} - -void PositionFacesInfo::draw_1p() -{ - // FACE_X = 15; // 15 - // FACE_Y = 72; // 72 - - INT32 Y = FACE_Y-9; // -9 to offset where it's being drawn if there are more than one - INT32 i, j; - INT32 bumperx, emeraldx; - INT32 xoff, yoff, flipflag = 0; - UINT8 workingskin; - UINT8 *colormap; - UINT32 skinflags; - - if (gametyperules & GTR_POINTLIMIT) // playing battle - { - Y += 40; - if (ranklines < 3) - Y -= 18; - } - else if (ranklines < 5) - Y += (9*ranklines); - else - Y += (9*5); - - ranklines--; - i = ranklines; - - if (gametyperules & GTR_POINTLIMIT) // playing battle - { - // 3 lines max in Battle - if (i > 2) - i = 2; - ranklines = 0; - - // You must appear on the leaderboard, even if you don't rank top 3 - if (strank > i) - { - strank = i; - rankplayer[strank] = stplyr - players; - } - - // Draw GOAL - bool skull = g_pointlimit && (g_pointlimit <= G_TeamOrIndividualScore(stplyr)); - INT32 height = i*18; - INT32 GOAL_Y = Y-height; - - colormap = nullptr; - - if (skincolornum_t vomit = vomit_color()) - { - colormap = R_GetTranslationColormap(TC_DEFAULT, vomit, GTC_CACHE); - } - - V_DrawMappedPatch(FACE_X-5, GOAL_Y-32, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_goal[skull][0], colormap); - - // Flashing KO - if (skull) - { - if (leveltime % 16 < 8) - V_DrawScaledPatch(FACE_X-5, GOAL_Y-32, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_goaltext1p); - } - else if (g_pointlimit) - { - using srb2::Draw; - Draw(FACE_X+8.5, GOAL_Y-15) - .font(Draw::Font::kZVote) - .align(Draw::Align::kCenter) - .flags(V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT) - .text("{:02}", g_pointlimit); - } - - // Line cutting behind rank faces - V_DrawScaledPatch(FACE_X+6, GOAL_Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_goalrod[0]); - } - else if (strank <= 2) // too close to the top, or a spectator? would have had (strank == -1) called out, but already caught by (strank <= 2) - { - if (i > 4) // could be both... - i = 4; - ranklines = 0; - } - else if (strank+2 >= ranklines) // too close to the bottom? - { - ranklines -= 4; - if (ranklines < 0) - ranklines = 0; - } - else - { - i = strank+2; - ranklines = strank-2; - } - - for (; i >= ranklines; i--) - { - if (!playeringame[rankplayer[i]]) continue; - if (players[rankplayer[i]].spectator) continue; - if (!players[rankplayer[i]].mo) continue; - - bumperx = FACE_X+19; - emeraldx = FACE_X+16; - - skinflags = (demo.playback) - ? demo.skinlist[demo.currentskinid[rankplayer[i]]].flags - : skins[players[rankplayer[i]].skin].flags; - - // Flip SF_IRONMAN portraits, but only if they're transformed - if (skinflags & SF_IRONMAN - && !(players[rankplayer[i]].charflags & SF_IRONMAN) ) - { - flipflag = V_FLIP|V_VFLIP; // blonic flip - xoff = yoff = 16; - } else - { - flipflag = 0; - xoff = yoff = 0; - } - - if (players[rankplayer[i]].mo->color) - { - if ((skin_t*)players[rankplayer[i]].mo->skin) - workingskin = (skin_t*)players[rankplayer[i]].mo->skin - skins; - else - workingskin = players[rankplayer[i]].skin; - - colormap = R_GetTranslationColormap(workingskin, static_cast(players[rankplayer[i]].mo->color), GTC_CACHE); - if (players[rankplayer[i]].mo->colorized) - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(players[rankplayer[i]].mo->color), GTC_CACHE); - else - colormap = R_GetTranslationColormap(workingskin, static_cast(players[rankplayer[i]].mo->color), GTC_CACHE); - - V_DrawMappedPatch(FACE_X + xoff, Y + yoff, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT|flipflag, faceprefix[workingskin][FACE_RANK], colormap); - - if (LUA_HudEnabled(hud_battlebumpers)) - { - const UINT8 bumpers = K_Bumpers(&players[rankplayer[i]]); - - if (bumpers > 0) - { - V_DrawMappedPatch(bumperx-2, Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_tinybumper[0], colormap); - for (j = 1; j < bumpers; j++) - { - bumperx += 5; - V_DrawMappedPatch(bumperx, Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_tinybumper[1], colormap); - } - } - } - } - - for (j = 0; j < 7; j++) - { - UINT32 emeraldFlag = (1 << j); - skincolornum_t emeraldColor = static_cast(SKINCOLOR_CHAOSEMERALD1 + j); - - if (players[rankplayer[i]].emeralds & emeraldFlag) - { - colormap = R_GetTranslationColormap(TC_DEFAULT, emeraldColor, GTC_CACHE); - V_DrawMappedPatch(emeraldx, Y+7, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_rankemerald, colormap); - emeraldx += 7; - } - } - - if (i == strank) - V_DrawScaledPatch(FACE_X, Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_facehighlight[(leveltime / 4) % 8]); - - if ((gametyperules & GTR_BUMPERS) && (players[rankplayer[i]].pflags & PF_ELIMINATED)) - V_DrawScaledPatch(FACE_X-4, Y-3, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_ranknobumpers); - else if (K_Cooperative()) - ; - else if (gametyperules & GTR_CIRCUIT) - { - INT32 pos = players[rankplayer[i]].position; - if (pos < 0 || pos > MAXPLAYERS) - pos = 0; - // Draws the little number over the face - V_DrawScaledPatch(FACE_X-5, Y+10, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_facenum[pos]); - } - else if (gametyperules & GTR_POINTLIMIT) - { - INT32 flags = V_HUDTRANS | V_SLIDEIN | V_SNAPTOLEFT; - - colormap = NULL; - - if (g_pointlimit && g_pointlimit <= players[rankplayer[i]].roundscore) - { - if (leveltime % 8 < 4) - { - colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_TANGERINE, GTC_CACHE); - } - - flags |= V_STRINGDANCE; - } - - V_DrawStringScaled( - (FACE_X - 5) * FRACUNIT, - (Y + 10) * FRACUNIT, - FRACUNIT, - FRACUNIT, - FRACUNIT, - flags, - colormap, - PINGF_FONT, - va("%d", players[rankplayer[i]].roundscore) - ); - } - - Y -= 18; - } -} - -void PositionFacesInfo::draw_4p_battle(int x, int y, INT32 flags) -{ - using srb2::Draw; - Draw row = Draw(x, y).flags(V_HUDTRANS | V_SLIDEIN | flags).font(Draw::Font::kPing); - - UINT8 skull = [] - { - if (g_pointlimit == 0) - return 0; - - int party = G_PartySize(consoleplayer); - for (int i = 0; i < party; ++i) - { - // Is any party member about to win? - if (g_pointlimit <= players[G_PartyMember(consoleplayer, i)].roundscore) - { - return 1; - } - } - return 0; - }(); - - skincolornum_t vomit = vomit_color(); - (vomit ? row.colormap(vomit) : row).patch(kp_goal[skull][1]); - - if (!skull && g_pointlimit) - { - row.xy(8.5, 5).align(Draw::Align::kCenter).text("{:02}", g_pointlimit); - } - - row.xy(7, 18).patch(kp_goalrod[1]); - - auto head = [&](Draw col, int i) - { - const player_t& p = players[rankplayer[i]]; - col.colormap(p.skin, static_cast(p.skincolor)).patch(faceprefix[p.skin][FACE_MINIMAP]); - - bool dance = g_pointlimit && (g_pointlimit <= p.roundscore); - bool flash = dance && leveltime % 8 < 4; - ( - flash ? - col.xy(8, 6).colorize(SKINCOLOR_TANGERINE).flags(V_STRINGDANCE) : - col.xy(8, 6).flags(dance ? V_STRINGDANCE : 0) - ).text("{:02}", p.roundscore); - }; - - // Draw top 2 players - head(row.xy(2, 31), 1); - head(row.xy(2, 18), 0); -} - -static boolean K_drawKartPositionFaces(void) -{ - PositionFacesInfo state{}; - - if (state.numplayersingame <= 1) - return true; - - if (!LUA_HudEnabled(hud_minirankings)) - return false; // Don't proceed but still return true for free play above if HUD is disabled. - - switch (r_splitscreen) - { - case 0: - state.draw_1p(); - break; - - case 1: - state.draw_4p_battle(292, 78, V_SNAPTORIGHT); - break; - - case 2: - case 3: - state.draw_4p_battle(152, 9, V_SNAPTOTOP); - state.draw_4p_battle(152, 147, V_SNAPTOBOTTOM); - break; - } - - return false; -} - -static void K_drawBossHealthBar(void) -{ - UINT8 i = 0, barstatus = 1, randlen = 0, darken = 0; - const INT32 startx = BASEVIDWIDTH - 23; - INT32 starty = BASEVIDHEIGHT - 25; - INT32 rolrand = 0, randtemp = 0; - boolean randsign = false; - - if (bossinfo.barlen <= 1) - return; - - // Entire bar juddering! - if (lt_exitticker < (TICRATE/2)) - ; - else if (bossinfo.visualbarimpact) - { - INT32 mag = std::min((bossinfo.visualbarimpact/4) + 1, 8u); - if (bossinfo.visualbarimpact & 1) - starty -= mag; - else - starty += mag; - } - - if ((lt_ticker >= lt_endtime) && bossinfo.enemyname) - { - if (lt_exitticker == 0) - { - rolrand = 5; - } - else if (lt_exitticker == 1) - { - rolrand = 7; - } - else - { - rolrand = 10; - } - V_DrawRightAlignedThinString(startx, starty-rolrand, V_FORCEUPPERCASE|V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT, bossinfo.enemyname); - rolrand = 0; - } - - // Used for colour and randomisation. - if (bossinfo.healthbar <= (bossinfo.visualdiv/FRACUNIT)) - { - barstatus = 3; - } - else if (bossinfo.healthbar <= bossinfo.healthbarpinch) - { - barstatus = 2; - } - - randtemp = bossinfo.visualbar-(bossinfo.visualdiv/(2*FRACUNIT)); - if (randtemp > 0) - randlen = P_RandomKey(PR_INTERPHUDRANDOM, randtemp)+1; - randsign = P_RandomChance(PR_INTERPHUDRANDOM, FRACUNIT/2); - - // Right wing. - V_DrawScaledPatch(startx-1, starty, V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_FLIP, kp_bossbar[0]); - - // Draw the bar itself... - while (i < bossinfo.barlen) - { - V_DrawScaledPatch(startx-i, starty, V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT, kp_bossbar[1]); - if (i < bossinfo.visualbar) - { - randlen--; - if (!randlen) - { - randtemp = bossinfo.visualbar-(bossinfo.visualdiv/(2*FRACUNIT)); - if (randtemp > 0) - randlen = P_RandomKey(PR_INTERPHUDRANDOM, randtemp)+1; - if (barstatus > 1) - { - rolrand = P_RandomKey(PR_INTERPHUDRANDOM, barstatus)+1; - } - else - { - rolrand = 1; - } - if (randsign) - { - rolrand = -rolrand; - } - randsign = !randsign; - } - else - { - rolrand = 0; - } - if (lt_exitticker < (TICRATE/2)) - ; - else if ((bossinfo.visualbar - i) < (INT32)(bossinfo.visualbarimpact/8)) - { - if (bossinfo.visualbarimpact & 1) - rolrand += (bossinfo.visualbar - i); - else - rolrand -= (bossinfo.visualbar - i); - } - if (bossinfo.visualdiv) - { - fixed_t work = 0; - if ((i+1) == bossinfo.visualbar) - darken = 1; - else - { - darken = 0; - // a hybrid fixed-int modulo... - while ((work/FRACUNIT) < bossinfo.visualbar) - { - if (work/FRACUNIT != i) - { - work += bossinfo.visualdiv; - continue; - } - darken = 1; - break; - } - } - } - V_DrawScaledPatch(startx-i, starty+rolrand, V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT, kp_bossbar[(2*barstatus)+darken]); - } - i++; - } - - // Left wing. - V_DrawScaledPatch(startx-i, starty, V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT, kp_bossbar[0]); -} - -static void K_drawKartEmeralds(void) -{ - static const INT32 emeraldOffsets[7][3] = { - {34, 0, 15}, - {25, 8, 11}, - {43, 8, 19}, - {16, 0, 7}, - {52, 0, 23}, - { 7, 8, 3}, - {61, 8, 27} - }; - - INT32 splitflags = V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_SPLITSCREEN; - INT32 startx = BASEVIDWIDTH - 77; - INT32 starty = BASEVIDHEIGHT - 29; - INT32 i = 0, xindex = 0; - - { - if (r_splitscreen) - { - starty = (starty/2) - 8; - } - starty -= 8; - - if (r_splitscreen < 2) - { - startx -= 8; - if (r_splitscreen == 1 && R_GetViewNumber() == 0) - { - starty = 1; - } - V_DrawScaledPatch(startx, starty, V_HUDTRANS|splitflags, kp_rankemeraldback); - } - else - { - xindex = 2; - starty -= 15; - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - startx = LAPS_X; - splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - } - else // else, that means we're P2 or P4. - { - startx = LAPS2_X + 1; - splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - } - } - } - - for (i = 0; i < 7; i++) - { - UINT32 emeraldFlag = (1 << i); - skincolornum_t emeraldColor = static_cast(SKINCOLOR_CHAOSEMERALD1 + i); - - if (stplyr->emeralds & emeraldFlag) - { - boolean whiteFlash = (leveltime & 1); - UINT8 *colormap; - - if (i & 1) - { - whiteFlash = !whiteFlash; - } - - colormap = R_GetTranslationColormap(TC_DEFAULT, emeraldColor, GTC_CACHE); - V_DrawMappedPatch( - startx + emeraldOffsets[i][xindex], starty + emeraldOffsets[i][1], - V_HUDTRANS|splitflags, - kp_rankemerald, colormap - ); - - if (whiteFlash == true) - { - V_DrawScaledPatch( - startx + emeraldOffsets[i][xindex], starty + emeraldOffsets[i][1], - V_HUDTRANSHALF|splitflags, - kp_rankemeraldflash - ); - } - } - } -} - -INT32 K_GetTransFlagFromFixed(fixed_t value) -{ - value = std::clamp(value, FRACUNIT/2, FRACUNIT*3/2); - - // Calculate distance from 1.0 - fixed_t distance = abs(FRACUNIT - value); - - // Map the distance to 0-10 range (10 = closest to 1.0, 0 = farthest from 1.0) - INT32 transLevel = 10 - ((distance * 10) / (FRACUNIT/2)); - - // Map 0-10 to V_TRANS flags - switch (transLevel) { - case 10: return V_70TRANS; // Most transparent (closest to 1.0) - case 9: return V_60TRANS; - case 8: return V_TRANSLUCENT; - case 7: return V_40TRANS; - case 6: return V_30TRANS; - case 5: return V_20TRANS; - case 4: return V_20TRANS; - case 3: return V_10TRANS; - case 2: return V_10TRANS; - case 1: - case 0: return 0; // Fully opaque (farthest from 1.0) - default: return V_90TRANS; // Shouldn't happen, but default to most transparent - } -} - -static void K_drawKartTeamScores(void) -{ - if (G_GametypeHasTeams() == false) - { - return; - } - - for (INT32 i = TEAM_UNASSIGNED+1; i < TEAM__MAX; i++) - { - INT32 x = BASEVIDWIDTH/2; - - x += -12 + (24 * (i - 1)); - - V_DrawCenteredString(x, 5, g_teaminfo[i].chat_color, va("%d", g_teamscores[i])); - - if (stplyr->team == i) - { - UINT32 individual_score = stplyr->teamimportance; - if (gametyperules & GTR_POINTLIMIT) - { - individual_score = stplyr->roundscore; - } - - V_DrawCenteredString(x, 15, g_teaminfo[i].chat_color, va("+%d", individual_score)); - } - } -} - -static void K_drawKartLaps(void) -{ - INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; - INT32 bump = 0; - boolean drewsticker = false; - - // Jesus Christ. - // I do not understand the way this system of offsets is laid out at all, - // so it's probably going to be pretty bad to maintain. Sorry. - - if (numlaps != 1) - { - if (r_splitscreen > 1) - bump = 27; - else - bump = 40; - } - - if (numlaps != 1) - { - if (r_splitscreen > 1) - { - - INT32 fx = 0, fy = 0, fr = 0; - INT32 flipflag = 0; - - // pain and suffering defined below - if (r_splitscreen < 2) // don't change shit for THIS splitscreen. - { - fx = LAPS_X; - fy = LAPS_Y; - } - else - { - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - fx = LAPS_X; - fy = LAPS_Y; - splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - } - else // else, that means we're P2 or P4. - { - fx = LAPS2_X; - fy = LAPS2_Y; - splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - flipflag = V_FLIP; // make the string right aligned and other shit - } - } - - fr = fx; - - if (flipflag) - fr += 15; - - drewsticker = true; - K_DrawMarginSticker(fx-1-(flipflag ? 10 : 0), fy+1, 25+bump, V_HUDTRANS|V_SLIDEIN|splitflags, true, flipflag); - - V_DrawScaledPatch(fr, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_splitlapflag); - //V_DrawScaledPatch(fx+22, fy, V_HUDTRANS|V_SLIDEIN|splitflags, frameslash); - - V_DrawScaledPatch(fr+12, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[(stplyr->laps) % 10]); - V_DrawScaledPatch(fr+16, fy, V_HUDTRANS|V_SLIDEIN|splitflags, frameslash); - V_DrawScaledPatch(fr+20, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[(numlaps) % 10]); - } - else - { - K_DrawSticker(LAPS_X+13, LAPS_Y+5, 25+bump, V_HUDTRANS|V_SLIDEIN|splitflags, false); - drewsticker = true; - - // Laps - V_DrawScaledPatch(LAPS_X, LAPS_Y, V_HUDTRANS|V_SLIDEIN|splitflags, kp_lapsticker); - - using srb2::Draw; - Draw row = Draw(LAPS_X+25, LAPS_Y+3).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer); - row.text("{:01}/{:01}", std::min(stplyr->laps, numlaps), numlaps); - - // V_DrawTimerString(LAPS_X+33, LAPS_Y+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d", std::min(stplyr->laps, numlaps), numlaps)); - } - } - - UINT16 displayEXP = std::clamp(FixedMul(std::max(stplyr->exp, FRACUNIT/2), (500/K_GetNumGradingPoints())*stplyr->gradingpointnum), 0, 999); - - // EXP - if (r_splitscreen > 1) - { - INT32 fx = 0, fy = 0, fr = 0; - INT32 flipflag = 0; - - // pain and suffering defined below - if (r_splitscreen < 2) // don't change shit for THIS splitscreen. - { - fx = LAPS_X; - fy = LAPS_Y; - } - else - { - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - fx = LAPS_X+bump; - fy = LAPS_Y; - splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - } - else // else, that means we're P2 or P4. - { - fx = LAPS2_X-bump; - fy = LAPS2_Y; - splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - flipflag = V_FLIP; // make the string right aligned and other shit - } - } - - fr = fx; - - if (flipflag) - fr += 15; - - if (!drewsticker) - K_DrawMarginSticker(fr-1+(flipflag ? 2 : 0), fy+1, 25+bump, V_HUDTRANS|V_SLIDEIN|splitflags, true, flipflag); - // WHAT IS THIS? - // WHAT ARE YOU FUCKING TALKING ABOUT? - V_DrawMappedPatch(fr, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_exp[1], R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_MUSTARD, GTC_CACHE)); - auto transflag = K_GetTransFlagFromFixed(stplyr->exp); - skincolornum_t overlaycolor = stplyr->exp < FRACUNIT ? SKINCOLOR_RUBY : SKINCOLOR_ULTRAMARINE ; - auto colormap = R_GetTranslationColormap(TC_RAINBOW, overlaycolor, GTC_CACHE); - V_DrawMappedPatch(fr, fy, transflag|V_SLIDEIN|splitflags, kp_exp[1], colormap); - - // EXP - V_DrawScaledPatch(fr+11, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP/100]); - V_DrawScaledPatch(fr+15, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP/10%10]); - V_DrawScaledPatch(fr+19, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP%10]); - } - else - { - if (!drewsticker) - K_DrawSticker(LAPS_X+13, LAPS_Y+5, 25+bump, V_HUDTRANS|V_SLIDEIN|splitflags, false); - - V_DrawMappedPatch(LAPS_X+bump, LAPS_Y, V_HUDTRANS|V_SLIDEIN|splitflags, kp_exp[0], R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_MUSTARD, GTC_CACHE)); - - auto transflag = K_GetTransFlagFromFixed(stplyr->exp); - skincolornum_t overlaycolor = stplyr->exp < FRACUNIT ? SKINCOLOR_RUBY : SKINCOLOR_ULTRAMARINE ; - auto colormap = R_GetTranslationColormap(TC_RAINBOW, overlaycolor, GTC_CACHE); - V_DrawMappedPatch(LAPS_X+bump, LAPS_Y, transflag|V_SLIDEIN|splitflags, kp_exp[0], colormap); - - using srb2::Draw; - Draw row = Draw(LAPS_X+23+bump, LAPS_Y+3).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer); - row.text("{:03}", displayEXP); - } -} - -#define RINGANIM_FLIPFRAME (RINGANIM_NUMFRAMES/2) - -static void K_DrawLivesDigits(INT32 x, INT32 y, INT32 width, INT32 flags, patch_t *font[10]) -{ - const SINT8 tens = stplyr->lives / 10; - - if (tens) - { - V_DrawScaledPatch(x, y, flags, font[tens % 10]); - x += width; - } - - V_DrawScaledPatch(x, y, flags, font[stplyr->lives % 10]); -} - -static void K_drawRingCounter(boolean gametypeinfoshown) -{ - const boolean uselives = G_GametypeUsesLives(); - SINT8 ringanim_realframe = stplyr->karthud[khud_ringframe]; - INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; - UINT8 rn[2]; - INT32 ringflip = 0; - UINT8 *ringmap = NULL; - boolean colorring = false; - INT32 ringx = 0, fy = 0; - - rn[0] = ((abs(stplyr->hudrings) / 10) % 10); - rn[1] = (abs(stplyr->hudrings) % 10); - - if (stplyr->hudrings <= 0 && stplyr->ringvisualwarning > 1) - { - colorring = true; - if ((leveltime/2 & 1)) - { - ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_CRIMSON, GTC_CACHE); - } - else - { - ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_WHITE, GTC_CACHE); - } - } - else if (stplyr->hudrings <= 0 && (leveltime/5 & 1)) // In debt - { - ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_CRIMSON, GTC_CACHE); - colorring = true; - } - else if (stplyr->hudrings >= 20) // Maxed out - ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_YELLOW, GTC_CACHE); - - if (stplyr->karthud[khud_ringframe] > RINGANIM_FLIPFRAME) - { - ringflip = V_FLIP; - ringanim_realframe = RINGANIM_NUMFRAMES-stplyr->karthud[khud_ringframe]; - ringx += SHORT((r_splitscreen > 1) ? kp_smallring[ringanim_realframe]->width : kp_ring[ringanim_realframe]->width); - } - - if (r_splitscreen > 1) - { - INT32 fx = 0, fr = 0; - INT32 flipflag = 0; - - // pain and suffering defined below - if (r_splitscreen < 2) // don't change shit for THIS splitscreen. - { - fx = LAPS_X; - fy = LAPS_Y; - } - else - { - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - fx = LAPS_X; - fy = LAPS_Y; - splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - } - else // else, that means we're P2 or P4. - { - fx = LAPS2_X; - fy = LAPS2_Y; - splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - flipflag = V_FLIP; // make the string right aligned and other shit - } - } - - fr = fx; - - if (gametypeinfoshown) - { - fy -= 10; - } - - // Rings - if (!uselives) - { - V_DrawScaledPatch(fx-2 + (flipflag ? (SHORT(kp_ringstickersplit[1]->width) - 3) : 0), fy, V_HUDTRANS|V_SLIDEIN|splitflags|flipflag, kp_ringstickersplit[1]); - if (flipflag) - fr += 15; - } - else - V_DrawScaledPatch(fx-2 + (flipflag ? (SHORT(kp_ringstickersplit[0]->width) - 3) : 0), fy, V_HUDTRANS|V_SLIDEIN|splitflags|flipflag, kp_ringstickersplit[0]); - - V_DrawMappedPatch(fr+ringx, fy-3, V_HUDTRANS|V_SLIDEIN|splitflags|ringflip, kp_smallring[ringanim_realframe], (colorring ? ringmap : NULL)); - - if (stplyr->hudrings < 0) // Draw the minus for ring debt - { - V_DrawMappedPatch(fr+11, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringdebtminussmall, ringmap); - V_DrawMappedPatch(fr+15, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[rn[0]], ringmap); - V_DrawMappedPatch(fr+19, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[rn[1]], ringmap); - } - else - { - V_DrawMappedPatch(fr+11, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[rn[0]], ringmap); - V_DrawMappedPatch(fr+15, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[rn[1]], ringmap); - } - - // SPB ring lock - if (stplyr->pflags & PF_RINGLOCK) - V_DrawScaledPatch(fr-12, fy-13, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringspblocksmall[stplyr->karthud[khud_ringspblock]]); - - UINT32 greyout = V_HUDTRANS; - SINT8 superoffset = 5; - if (stplyr->superringdisplay) - { - greyout = V_HUDTRANSHALF; - if (flipflag && !uselives) - superoffset = -25 - (stplyr->superringdisplay >= 10 ? 3 : 0) - (stplyr->superringdisplay >= 100 ? 3 : 0); - } - - // Lives - if (uselives) - { - UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, static_cast(stplyr->skincolor), GTC_CACHE); - V_DrawMappedPatch(fr+21, fy-3, V_SLIDEIN|splitflags|greyout, faceprefix[stplyr->skin][FACE_MINIMAP], colormap); - if (stplyr->lives >= 0) - K_DrawLivesDigits(fr+34, fy, 4, V_SLIDEIN|splitflags|greyout, fontv[PINGNUM_FONT].font); - } - - if (stplyr->superringdisplay && !(stplyr->superringalert % 2)) - { - using srb2::Draw; - Draw row = Draw(fr+19+superoffset, fy).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kPing).colorize(SKINCOLOR_SAPPHIRE); - row.text("+{:01}", abs(stplyr->superringdisplay)); - } - } - else - { - fy = LAPS_Y; - - if (gametypeinfoshown) - { - fy -= 11; - - if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS) - fy -= 4; - } - else - { - fy += 9; - } - - // Rings - using srb2::Draw; - Draw(LAPS_X+7, fy+1) - .flags(V_HUDTRANS|V_SLIDEIN|splitflags) - .align(Draw::Align::kCenter) - .width(uselives ? (stplyr->lives >= 10 ? 70 : 64) : 33) - .small_sticker(); - - if (stplyr->overdrive) - { - V_DrawMappedPatch(LAPS_X+7-8, fy-5-8, V_HUDTRANS|V_SLIDEIN|splitflags, kp_overdrive[leveltime%32], R_GetTranslationColormap(TC_RAINBOW, static_cast(stplyr->skincolor), GTC_CACHE)); - } - else - { - V_DrawMappedPatch(LAPS_X+ringx+7, fy-5, V_HUDTRANS|V_SLIDEIN|splitflags|ringflip, kp_ring[ringanim_realframe], (colorring ? ringmap : NULL)); - - if (stplyr->amps) - { - UINT8 amplevel = std::min(stplyr->amps / AMPLEVEL, 6); - - V_DrawMappedPatch(LAPS_X+7-7, fy-5-8, V_HUDTRANS|V_SLIDEIN|splitflags, kp_amps[amplevel][leveltime%12], R_GetTranslationColormap(TC_RAINBOW, static_cast(stplyr->skincolor), GTC_CACHE)); - if (amplevel == 6) - { - V_DrawMappedPatch(LAPS_X+7-7, fy-5-8, V_ADD|V_HUDTRANS|V_SLIDEIN|splitflags, kp_amps_underlay[leveltime%12], R_GetTranslationColormap(TC_RAINBOW, static_cast(stplyr->skincolor), GTC_CACHE)); - } - } - } - - // "Why fy-4? Why LAPS_X+29+1?" - // "use magic numbers" - jartha 2024-03-05 - if (stplyr->hudrings < 0) // Draw the minus for ring debt - { - V_DrawMappedPatch(LAPS_X+23-1, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringdebtminus, ringmap); - using srb2::Draw; - Draw row = Draw(LAPS_X+29+0, fy-4).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer).colormap(ringmap); - row.text("{:02}", abs(stplyr->hudrings)); - // V_DrawMappedPatch(LAPS_X+29, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[TALLNUM_FONT].font[rn[0]], ringmap); - // V_DrawMappedPatch(LAPS_X+35, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[TALLNUM_FONT].font[rn[1]], ringmap); - } - else - { - using srb2::Draw; - Draw row = Draw(LAPS_X+23+3, fy-4).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer).colormap(ringmap); - row.text("{:02}", abs(stplyr->hudrings)); - // V_DrawMappedPatch(LAPS_X+23, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[TALLNUM_FONT].font[rn[0]], ringmap); - // V_DrawMappedPatch(LAPS_X+29, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[TALLNUM_FONT].font[rn[1]], ringmap); - } - - // SPB ring lock - if (stplyr->pflags & PF_RINGLOCK) - V_DrawScaledPatch(LAPS_X-5, fy-17, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringspblock[stplyr->karthud[khud_ringspblock]]); - - UINT32 greyout = V_HUDTRANS; - - if (stplyr->superringdisplay) - { - greyout = V_HUDTRANSHALF; - } - - // Lives - if (uselives) - { - UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, static_cast(stplyr->skincolor), GTC_CACHE); - V_DrawMappedPatch(LAPS_X+46, fy-5, V_SLIDEIN|splitflags|greyout, faceprefix[stplyr->skin][FACE_RANK], colormap); - SINT8 livescount = 0; - if (stplyr->lives > 0) - { - livescount = stplyr->lives; - if (livescount > 10) - livescount = 10; - } - using srb2::Draw; - Draw row = Draw(LAPS_X+65, fy-4).flags(V_SLIDEIN|splitflags|greyout).font(Draw::Font::kThinTimer); - row.text("{}", livescount); - } - - if (stplyr->superringdisplay && !(stplyr->superringalert % 2)) - { - using srb2::Draw; - Draw row = Draw(LAPS_X+23+3+15, fy-4).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer).colorize(SKINCOLOR_SAPPHIRE); - row.text("+{:01}", abs(stplyr->superringdisplay)); - } - } -} - -#undef RINGANIM_FLIPFRAME - -static void K_drawKartAccessibilityIcons(boolean gametypeinfoshown, INT32 fx) -{ - INT32 fy = LAPS_Y-14; - INT32 splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - - boolean mirror = false; - - fx += LAPS_X; - - if (r_splitscreen < 2) // adjust to speedometer height - { - if (battleprisons) - { - fy -= 2; - } - - if (gametypeinfoshown) - { - fy -= 11; - - if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS) - fy -= 4; - } - else - { - fy += 9; - } - } - else - { - fx = LAPS_X+44; - fy = LAPS_Y; - if (R_GetViewNumber() & 1) // If we are not P1 or P3... - { - splitflags ^= (V_SNAPTOLEFT|V_SNAPTORIGHT); - fx = (BASEVIDWIDTH/2) - fx; - mirror = true; - } - } - - // Kickstart Accel - if (stplyr->pflags & PF_KICKSTARTACCEL) - { - if (mirror) - fx -= 10; - - SINT8 col = 0, wid, fil, ofs; - UINT8 i = 7; - ofs = (stplyr->kickstartaccel == ACCEL_KICKSTART) ? 1 : 0; - fil = i-(stplyr->kickstartaccel*i)/ACCEL_KICKSTART; - - V_DrawFill(fx+4, fy+ofs-1, 2, 1, 31|V_SLIDEIN|splitflags); - V_DrawFill(fx, (fy+ofs-1)+8, 10, 1, 31|V_SLIDEIN|splitflags); - - while (i--) - { - wid = (i/2)+1; - V_DrawFill(fx+4-wid, fy+ofs+i, 2+(wid*2), 1, 31|V_SLIDEIN|splitflags); - if (fil > 0) - { - if (i < fil) - col = 23; - else if (i == fil) - col = 3; - else - col = 5 + (i-fil)*2; - } - else if ((leveltime % 7) == i) - col = 0; - else - col = 3; - V_DrawFill(fx+5-wid, fy+ofs+i, (wid*2), 1, col|V_SLIDEIN|splitflags); - } - - if (mirror) - fx--; - else - fx += 10 + 1; - } - - // Auto Roulette - if (stplyr->pflags & PF_AUTOROULETTE) - { - if (mirror) - fx -= 12; - - V_DrawScaledPatch(fx, fy-1, V_SLIDEIN|splitflags, kp_autoroulette); - - if (mirror) - fx--; - else - fx += 12 + 1; - } - - if (stplyr->pflags & PF_AUTORING) - { - if (mirror) - fx -= 14; - - V_DrawScaledPatch(fx, fy-1, V_SLIDEIN|splitflags, kp_autoring); - - if (mirror) - fx--; - else - fx += 14 + 1; - } -} - -static void K_drawKartSpeedometer(boolean gametypeinfoshown) -{ - static fixed_t convSpeed; - UINT8 labeln = 0; - UINT8 numbers[3]; - INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; - INT32 fy = LAPS_Y-14; - - if (battleprisons) - { - fy -= 2; - } - - if (!stplyr->exiting) // Keep the same speed value as when you crossed the finish line! - { - switch (cv_kartspeedometer.value) - { - case 1: // Sonic Drift 2 style percentage - default: - convSpeed = (stplyr->speed * 100) / K_GetKartSpeed(stplyr, false, true); // Based on top speed! - labeln = 0; - break; - case 2: // Kilometers - convSpeed = FixedDiv(FixedMul(stplyr->speed, 142371), mapobjectscale) / FRACUNIT; // 2.172409058 - labeln = 1; - break; - case 3: // Miles - convSpeed = FixedDiv(FixedMul(stplyr->speed, 88465), mapobjectscale) / FRACUNIT; // 1.349868774 - labeln = 2; - break; - case 4: // Fracunits - convSpeed = FixedDiv(stplyr->speed, mapobjectscale) / FRACUNIT; // 1.0. duh. - labeln = 3; - break; - } - } - - // Don't overflow - // (negative speed IS really high speed :V) - if (convSpeed > 999 || convSpeed < 0) - convSpeed = 999; - - numbers[0] = ((convSpeed / 100) % 10); - numbers[1] = ((convSpeed / 10) % 10); - numbers[2] = (convSpeed % 10); - - if (gametypeinfoshown) - { - fy -= 11; - - if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS) - fy -= 4; - } - else - { - fy += 9; - } - - using srb2::Draw; - Draw(LAPS_X+7, fy+1).flags(V_HUDTRANS|V_SLIDEIN|splitflags).align(Draw::Align::kCenter).width(42).small_sticker(); - V_DrawScaledPatch(LAPS_X+7, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[numbers[0]]); - V_DrawScaledPatch(LAPS_X+13, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[numbers[1]]); - V_DrawScaledPatch(LAPS_X+19, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[numbers[2]]); - V_DrawScaledPatch(LAPS_X+29, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_speedometerlabel[labeln]); - - K_drawKartAccessibilityIcons(gametypeinfoshown, 56); -} - -static void K_drawBlueSphereMeter(boolean gametypeinfoshown) -{ - const UINT8 maxBars = 4; - // see also K_DrawNameTagSphereMeter - const UINT8 segColors[] = {73, 64, 52, 54, 55, 35, 34, 33, 202, 180, 181, 182, 164, 165, 166, 153, 152}; - const UINT8 sphere = std::clamp(static_cast(stplyr->spheres), 0, 40); - - UINT8 numBars = std::min((sphere / 10), +maxBars); - UINT8 colorIndex = (sphere * sizeof(segColors)) / (40 + 1); - INT32 fx, fy; - UINT8 i; - INT32 splitflags = V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; - INT32 flipflag = 0; - INT32 xstep = 15; - - // pain and suffering defined below - if (r_splitscreen < 2) // don't change shit for THIS splitscreen. - { - fx = LAPS_X; - fy = LAPS_Y-4; - - if (battleprisons) - { - if (r_splitscreen == 1) - { - fy -= 8; - } - else - { - fy -= 5; - } - } - else if (r_splitscreen == 1) - { - fy -= 5; - } - - if (gametypeinfoshown) - { - fy -= 11 + 4; - } - else - { - fy += 9; - } - - V_DrawScaledPatch(fx, fy, splitflags|flipflag, kp_spheresticker); - } - else - { - xstep = 8; - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - fx = LAPS_X-2; - fy = LAPS_Y; - } - else // else, that means we're P2 or P4. - { - fx = LAPS2_X+(SHORT(kp_splitspheresticker->width) - 10); - fy = LAPS2_Y; - splitflags ^= V_SNAPTOLEFT|V_SNAPTORIGHT; - flipflag = V_FLIP; // make the string right aligned and other shit - xstep = -xstep; - } - - if (battleprisons) - { - fy -= 5; - } - - if (gametypeinfoshown) - { - fy -= 16; - } - - V_DrawScaledPatch(fx, fy, splitflags|flipflag, kp_splitspheresticker); - } - - if (r_splitscreen < 2) - { - fx += 25; - } - else - { - fx += (flipflag) ? -18 : 13; - } - - for (i = 0; i <= numBars; i++) - { - UINT8 segLen = (r_splitscreen < 2) ? 10 : 5; - - if (i == numBars) - { - segLen = (sphere % 10); - if (r_splitscreen < 2) - ; - else - { - segLen = (segLen+1)/2; // offset so nonzero spheres shows up IMMEDIATELY - if (!segLen) - break; - if (flipflag) - fx += (5-segLen); - } - } - - if (r_splitscreen < 2) - { - V_DrawFill(fx, fy + 6, segLen, 3, segColors[std::max(colorIndex-1, 0)] | splitflags); - V_DrawFill(fx, fy + 7, segLen, 1, segColors[std::max(colorIndex-2, 0)] | splitflags); - V_DrawFill(fx, fy + 9, segLen, 3, segColors[colorIndex] | splitflags); - } - else - { - V_DrawFill(fx, fy + 5, segLen, 1, segColors[std::max(colorIndex-1, 0)] | splitflags); - V_DrawFill(fx, fy + 6, segLen, 1, segColors[std::max(colorIndex-2, 0)] | splitflags); - V_DrawFill(fx, fy + 7, segLen, 2, segColors[colorIndex] | splitflags); - } - - fx += xstep; - } -} - -static void K_drawKartBumpersOrKarma(void) -{ - UINT8 *colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(stplyr->skincolor), GTC_CACHE); - INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; - - if (r_splitscreen > 1) - { - INT32 fx = 0, fy = 0; - INT32 flipflag = 0; - - // pain and suffering defined below - if (r_splitscreen < 2) // don't change shit for THIS splitscreen. - { - fx = LAPS_X; - fy = LAPS_Y; - } - else - { - if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... - { - fx = LAPS_X; - fy = LAPS_Y; - splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - } - else // else, that means we're P2 or P4. - { - fx = LAPS2_X; - fy = LAPS2_Y; - splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; - flipflag = V_FLIP; // make the string right aligned and other shit - } - } - - { - using srb2::Draw; - int width = 39; - if (!battleprisons) - { - constexpr int kPad = 16; - if (flipflag) - fx -= kPad; - width += kPad; - } - Draw(fx-1 + (flipflag ? width + 3 : 0), fy+1) - .flags(V_HUDTRANS|V_SLIDEIN|splitflags) - .align(flipflag ? Draw::Align::kRight : Draw::Align::kLeft) - .width(width) - .small_sticker(); - } - - fx += 2; - - if (battleprisons) - { - V_DrawScaledPatch(fx+22, fy, V_HUDTRANS|V_SLIDEIN|splitflags, frameslash); - V_DrawMappedPatch(fx-1, fy-2, V_HUDTRANS|V_SLIDEIN|splitflags, kp_rankcapsule, NULL); - - if (numtargets > 9 || maptargets > 9) - { - UINT8 ln[2]; - ln[0] = ((numtargets / 10) % 10); - ln[1] = (numtargets % 10); - - V_DrawScaledPatch(fx+13, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[ln[0]]); - V_DrawScaledPatch(fx+17, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[ln[1]]); - - ln[0] = ((maptargets / 10) % 10); - ln[1] = (maptargets % 10); - - V_DrawScaledPatch(fx+27, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[ln[0]]); - V_DrawScaledPatch(fx+31, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[ln[1]]); - } - else - { - V_DrawScaledPatch(fx+13, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[numtargets % 10]); - V_DrawScaledPatch(fx+27, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[maptargets % 10]); - } - } - else - { - const UINT8 bumpers = K_Bumpers(stplyr); - const bool dance = g_pointlimit && (g_pointlimit <= stplyr->roundscore); - - V_DrawMappedPatch(fx-1, fy-2, V_HUDTRANS|V_SLIDEIN|splitflags, kp_rankbumper, colormap); - - using srb2::Draw; - Draw row = Draw(fx+12, fy).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kPing); - row.text("{:02}", bumpers); - if (dance && leveltime % 8 < 4) - { - row = row.colorize(SKINCOLOR_TANGERINE); - } - row.xy(10, -2).patch(kp_pts[1]); - row - .x(31) - .flags(dance ? V_STRINGDANCE : 0) - .text("{:02}", stplyr->roundscore); - } - } - else - { - INT32 fy = r_splitscreen == 1 ? LAPS_Y-3 : LAPS_Y; - - if (battleprisons) - { - if (numtargets > 9 && maptargets > 9) - V_DrawMappedPatch(LAPS_X, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_capsulestickerwide, NULL); - else - V_DrawMappedPatch(LAPS_X, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_capsulesticker, NULL); - V_DrawTimerString(LAPS_X+47, fy+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d", numtargets, maptargets)); - } - else - { - const UINT8 bumpers = K_Bumpers(stplyr); - const bool dance = g_pointlimit && (g_pointlimit <= stplyr->roundscore); - - if (r_splitscreen == 0) - { - fy += 2; - } - - K_DrawSticker(LAPS_X+12, fy+5, 75, V_HUDTRANS|V_SLIDEIN|splitflags, false); - V_DrawMappedPatch(LAPS_X+12, fy-2, V_HUDTRANS|V_SLIDEIN|splitflags, kp_bigbumper, colormap); - - using srb2::Draw; - Draw row = Draw(LAPS_X+12+23+1, fy+3).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer); - row.text("{:02}", bumpers); - if (dance && leveltime % 8 < 4) - { - row = row.colorize(SKINCOLOR_TANGERINE); - } - row.xy(12, -2).patch(kp_pts[0]); - row - .x(12+27) - .flags(dance ? V_STRINGDANCE : 0) - .text("{:02}", stplyr->roundscore); - } - } -} - -#if 0 -static void K_drawKartWanted(void) -{ - UINT8 i, numwanted = 0; - UINT8 *colormap = NULL; - INT32 basex = 0, basey = 0; - - if (!splitscreen) - return; - - if (stplyr != &players[displayplayers[0]]) - return; - - for (i = 0; i < 4; i++) - { - if (battlewanted[i] == -1) - break; - numwanted++; - } - - if (numwanted <= 0) - return; - - // set X/Y coords depending on splitscreen. - if (r_splitscreen < 3) // 1P and 2P use the same code. - { - basex = WANT_X; - basey = WANT_Y; - if (r_splitscreen == 2) - { - basey += 16; // slight adjust for 3P - basex -= 6; - } - } - else if (r_splitscreen == 3) // 4P splitscreen... - { - basex = BASEVIDWIDTH/2 - (SHORT(kp_wantedsplit->width)/2); // center on screen - basey = BASEVIDHEIGHT - 55; - //basey2 = 4; - } - - if (battlewanted[0] != -1) - colormap = R_GetTranslationColormap(TC_DEFAULT, players[battlewanted[0]].skincolor, GTC_CACHE); - V_DrawFixedPatch(basex< 1 ? kp_wantedsplit : kp_wanted), colormap); - /*if (basey2) - V_DrawFixedPatch(basex< 1 ? 13 : 8), y = basey+(r_splitscreen > 1 ? 16 : 21); - fixed_t scale = FRACUNIT/2; - player_t *p = &players[battlewanted[i]]; - - if (battlewanted[i] == -1) - break; - - if (numwanted == 1) - scale = FRACUNIT; - else - { - if (i & 1) - x += 16; - if (i > 1) - y += 16; - } - - if (players[battlewanted[i]].skincolor) - { - colormap = R_GetTranslationColormap(TC_RAINBOW, p->skincolor, GTC_CACHE); - V_DrawFixedPatch(x<skin][FACE_WANTED] : faceprefix[p->skin][FACE_RANK]), colormap); - /*if (basey2) // again with 4p stuff - V_DrawFixedPatch(x<skin][FACE_WANTED] : faceprefix[p->skin][FACE_RANK]), colormap);*/ - } - } -} -#endif //if 0 - -static void K_drawKartPlayerCheck(void) -{ - const fixed_t maxdistance = FixedMul(1280 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)); - UINT8 i; - INT32 splitflags = V_SNAPTOBOTTOM|V_SPLITSCREEN; - fixed_t y = CHEK_Y * FRACUNIT; - - if (stplyr == NULL || stplyr->mo == NULL || P_MobjWasRemoved(stplyr->mo)) - { - return; - } - - if (stplyr->spectator || stplyr->awayview.tics) - { - return; - } - - if (stplyr->cmd.buttons & BT_LOOKBACK) - { - return; - } - - for (i = 0; i < MAXPLAYERS; i++) - { - player_t *checkplayer = &players[i]; - fixed_t distance = maxdistance+1; - UINT8 *colormap = NULL; - UINT8 pnum = 0; - vector3_t v; - vector3_t pPos; - trackingResult_t result; - - if (!playeringame[i] || checkplayer->spectator) - { - // Not in-game - continue; - } - - if (checkplayer->mo == NULL || P_MobjWasRemoved(checkplayer->mo)) - { - // No object - continue; - } - - if (checkplayer == stplyr) - { - // This is you! - continue; - } - - v.x = R_InterpolateFixed(checkplayer->mo->old_x, checkplayer->mo->x); - v.y = R_InterpolateFixed(checkplayer->mo->old_y, checkplayer->mo->y); - v.z = R_InterpolateFixed(checkplayer->mo->old_z, checkplayer->mo->z); - - pPos.x = R_InterpolateFixed(stplyr->mo->old_x, stplyr->mo->x); - pPos.y = R_InterpolateFixed(stplyr->mo->old_y, stplyr->mo->y); - pPos.z = R_InterpolateFixed(stplyr->mo->old_z, stplyr->mo->z); - - distance = R_PointToDist2(pPos.x, pPos.y, v.x, v.y); - - if (distance > maxdistance) - { - // Too far away - continue; - } - - if ((checkplayer->invincibilitytimer <= 0) && (leveltime & 2)) - { - pnum++; // white frames - } - - if (checkplayer->itemtype == KITEM_GROW || checkplayer->growshrinktimer > 0) - { - pnum += 4; - } - else if (checkplayer->itemtype == KITEM_INVINCIBILITY || checkplayer->invincibilitytimer) - { - pnum += 2; - } - - K_ObjectTracking(&result, &v, true); - - if (result.onScreen == true) - { - colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(checkplayer->mo->color), GTC_CACHE); - V_DrawFixedPatch(result.x, y, FRACUNIT, V_HUDTRANS|V_SPLITSCREEN|splitflags, kp_check[pnum], colormap); - } - } -} - -static boolean K_ShowPlayerNametag(player_t *p) -{ - if (cv_seenames.value == 0) - { - return false; - } - - if (demo.playback == true && camera[R_GetViewNumber()].freecam == true) - { - return true; - } - - if (stplyr == p) - { - return false; - } - - if (gametyperules & GTR_CIRCUIT) - { - if ((p->position == 0) - || (stplyr->position == 0) - || (p->position < stplyr->position-2) - || (p->position > stplyr->position+2)) - { - return false; - } - } - - return true; -} - -static void K_DrawTypingDot(fixed_t x, fixed_t y, UINT8 duration, player_t *p, INT32 flags) -{ - if (p->typing_duration > duration) - { - V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_typdot, NULL); - } -} - -static void K_DrawTypingNotifier(fixed_t x, fixed_t y, player_t *p, INT32 flags) -{ - if (p->cmd.flags & TICCMD_TYPING) - { - V_DrawFixedPatch(x, y, FRACUNIT, V_SPLITSCREEN|flags, kp_talk, NULL); - - /* spacing closer with the last two looks a better most of the time */ - K_DrawTypingDot(x + 3*FRACUNIT, y, 15, p, flags); - K_DrawTypingDot(x + 6*FRACUNIT - FRACUNIT/3, y, 31, p, flags); - K_DrawTypingDot(x + 9*FRACUNIT - FRACUNIT/3, y, 47, p, flags); - } -} - -// see also K_drawKartItem -static void K_DrawNameTagItemSpy(INT32 x, INT32 y, player_t *p, INT32 flags) -{ - using srb2::Draw; - bool tiny = r_splitscreen > 1; - SINT8 flip = 1, flipboxoffset = 0; - if ((flags & V_VFLIP) == V_VFLIP) - { - // Remove the v_vflip flag - it makes things messy, but we also understand - // that we want to make this look okay for flipped players, so simply use this - // opportunity to flip vertical offsets accordingly instead. - flags &= ~V_VFLIP; - flip = P_MobjFlip(p->mo); - flipboxoffset = 8; - } - - Draw bar = Draw(x, y).flags(V_NOSCALESTART|flags); - Draw box = tiny ? bar.xy(-22 * vid.dupx, (-17+flipboxoffset) * vid.dupy) : bar.xy(-40 * vid.dupx, (-26+flipboxoffset) * vid.dupy); - - box.colorize(p->skincolor).patch(kp_itembg[tiny ? 4 : 2]); - - INT32 item_type = KITEM_NONE; - - if (p->itemRoulette.active == true && p->itemRoulette.ringbox == false) - { - item_type = 1 + (leveltime % (NUMKARTITEMS - 1)); - } - else if (!(p->itemflags & IF_ITEMOUT) || (leveltime & 1)) - { - item_type = p->itemtype; - } - - switch (item_type) - { - case KITEM_NONE: - { - break; - } - - case KITEM_INVINCIBILITY: - { - box.patch(kp_invincibility[((leveltime % (6*3)) / 3) + (tiny ? 13 : 7)]); - break; - } - - case KITEM_ORBINAUT: - { - box.patch(kp_orbinaut[4 + tiny]); - break; - } - - default: - { - if (patch_t *ico = K_GetCachedItemPatch(item_type, 1 + tiny)) - { - box.patch(ico); - } - break; - } - } - - if (p->itemamount > 1) - { - (tiny ? - bar.xy(-3 * vid.dupx, (-4*flip) * vid.dupy).font(Draw::Font::kPing) : - bar.xy(-4 * vid.dupx, (-2*flip) * vid.dupy).font(Draw::Font::kThinTimer) - ) - .align(Draw::Align::kRight) - .text("{}", p->itemamount); - } -} - -static void K_DrawNameTagSphereMeter(INT32 x, INT32 y, INT32 width, INT32 spheres, INT32 flags) -{ - using srb2::Draw; - Draw bar = Draw(x + vid.dupx, y).flags(V_NOSCALESTART).height(vid.dupy); - - // see also K_drawBlueSphereMeter - const UINT8 segColors[] = {73, 64, 52, 54, 55, 35, 34, 33, 202, 180, 181, 182, 164, 165, 166, 153, 152}; - - spheres = std::clamp(spheres, 0, 40); - int colorIndex = (spheres * sizeof segColors) / (40 + 1); - - int px = r_splitscreen > 1 ? 1 : 2; - int b = 10 * px; - int m = spheres * px; - - while (m > 0) - { - if (b > m) - b = m; - - Draw seg = bar.width(b); - - seg.fill(segColors[std::max(colorIndex - 1, 0)]); - seg.y(vid.dupy).fill(segColors[std::max(colorIndex - 2, 0)]); - seg.y(2 * vid.dupy).height(2 * vid.dupy).fill(segColors[colorIndex]); - seg.y(4 * vid.dupy).fill(31); - - bar = bar.x(b + vid.dupx); - m -= b; - } -} - -static void K_DrawLocalTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT8 id, UINT32 flags) -{ - UINT8 blink = ((leveltime / 7) & 1); - UINT8 *colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(p->skincolor), GTC_CACHE); - - V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_localtag[id][blink], colormap); -} - -static void K_DrawRivalTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT32 flags) -{ - if ((p->itemtype != KITEM_NONE && p->itemamount != 0) - || (p->itemRoulette.active == true && p->itemRoulette.ringbox == false)) - { - INT32 barx = 0, bary = 0; - - barx = (x * vid.dupx) / FRACUNIT; - bary = (y * vid.dupy) / FRACUNIT; - - barx += (16 * vid.dupx); - bary -= (25 * vid.dupx); - - // Center it if necessary - if (vid.width != BASEVIDWIDTH * vid.dupx) - { - barx += (vid.width - (BASEVIDWIDTH * vid.dupx)) / 2; - } - - if (vid.height != BASEVIDHEIGHT * vid.dupy) - { - bary += (vid.height - (BASEVIDHEIGHT * vid.dupy)) / 2; - } - - K_DrawNameTagItemSpy(barx, bary, p, flags); - } - - UINT8 blink = ((leveltime / 7) & 1); - V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_rival[blink], NULL); -} - -static void K_DrawCPUTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT32 flags) -{ - if ((p->itemtype != KITEM_NONE && p->itemamount != 0) - || (p->itemRoulette.active == true && p->itemRoulette.ringbox == false)) - { - INT32 barx = 0, bary = 0; - - barx = (x * vid.dupx) / FRACUNIT; - bary = (y * vid.dupy) / FRACUNIT; - - barx += (16 * vid.dupx); - bary -= (25 * vid.dupx); - - // Center it if necessary - if (vid.width != BASEVIDWIDTH * vid.dupx) - { - barx += (vid.width - (BASEVIDWIDTH * vid.dupx)) / 2; - } - - if (vid.height != BASEVIDHEIGHT * vid.dupy) - { - bary += (vid.height - (BASEVIDHEIGHT * vid.dupy)) / 2; - } - - K_DrawNameTagItemSpy(barx, bary, p, flags); - } - - UINT8 blink = ((leveltime / 7) & 1); - V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_cpu[blink], NULL); -} - -static void K_DrawNameTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT32 flags) -{ - const INT32 clr = skincolors[p->skincolor].chatcolor; - const INT32 namelen = V_ThinStringWidth(player_names[p - players], 0); - - UINT8 *colormap = V_GetStringColormap(clr); - INT32 barx = 0, bary = 0, barw = 0; - INT32 flipped = P_MobjFlip(p->mo), flipfilloffset = 0, flipfontoffset = 0, flipspheresoffset = 0; - if (flipped == -1) - { - flipfilloffset = -3; // You cannot really flip drawfill. - flipfontoffset = -9; // Accounts for font height. - flipspheresoffset = 2; - } - - UINT8 cnum = R_GetViewNumber(); - - // Since there's no "V_DrawFixedFill", and I don't feel like making it, - // fuck it, we're gonna just V_NOSCALESTART hack it - if (r_splitscreen > 1 && cnum & 1) - { - x += (BASEVIDWIDTH/2) * FRACUNIT; - } - - if ((r_splitscreen == 1 && cnum == 1) - || (r_splitscreen > 1 && cnum > 1)) - { - y += (BASEVIDHEIGHT/2) * FRACUNIT; - } - - barw = (namelen * vid.dupx); - - barx = (x * vid.dupx) / FRACUNIT; - bary = (y * vid.dupy) / FRACUNIT; - - barx += (6 * vid.dupx); - bary -= ((16 + flipfilloffset) * vid.dupx) * flipped; - - // Center it if necessary - if (vid.width != BASEVIDWIDTH * vid.dupx) - { - barx += (vid.width - (BASEVIDWIDTH * vid.dupx)) / 2; - } - - if (vid.height != BASEVIDHEIGHT * vid.dupy) - { - bary += (vid.height - (BASEVIDHEIGHT * vid.dupy)) / 2; - } - - // see also K_CullTargetList - if ((p->itemtype != KITEM_NONE && p->itemamount != 0) - || (p->itemRoulette.active == true && p->itemRoulette.ringbox == false)) - { - K_DrawNameTagItemSpy(barx, bary, p, flags); - } - - if (gametyperules & GTR_SPHERES) - { - K_DrawNameTagSphereMeter(barx, bary + (((4 + flipspheresoffset) * vid.dupy) * P_MobjFlip(p->mo)), barw, p->spheres, flags); - } - - // Lat: 10/06/2020: colormap can be NULL on the frame you join a game, just arbitrarily use palette indexes 31 and 0 instead of whatever the colormap would give us instead to avoid crashes. - V_DrawFill(barx, bary, barw, (3 * vid.dupy), (colormap ? colormap[31] : 31)|V_NOSCALESTART|flags); - V_DrawFill(barx, bary + vid.dupy, barw, vid.dupy, (colormap ? colormap[0] : 0)|V_NOSCALESTART|flags); - // END DRAWFILL DUMBNESS - - // Draw the stem - V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_nametagstem, colormap); - - // Draw the name itself - V_DrawThinStringAtFixed(x + (5*FRACUNIT), y - (((26 + flipfontoffset) * FRACUNIT) * P_MobjFlip(p->mo)), clr|flags, player_names[p - players]); -} - -playertagtype_t K_WhichPlayerTag(player_t *p) -{ - UINT8 cnum = R_GetViewNumber(); - - if (!(demo.playback == true && camera[cnum].freecam == true) && P_IsDisplayPlayer(p) && - p != &players[displayplayers[cnum]]) - { - return PLAYERTAG_LOCAL; - } - else if (p->bot) - { - if (p->botvars.rival == true || cv_levelskull.value) - { - return PLAYERTAG_RIVAL; - } - else if (K_ShowPlayerNametag(p) == true) - { - return PLAYERTAG_CPU; - } - } - else if (netgame || demo.playback) - { - if (K_ShowPlayerNametag(p) == true) - { - return PLAYERTAG_NAME; - } - } - - return PLAYERTAG_NONE; -} - -void K_DrawPlayerTag(fixed_t x, fixed_t y, player_t *p, playertagtype_t type, boolean foreground) -{ - INT32 flags = P_IsObjectFlipped(p->mo) ? V_VFLIP : 0; - - switch (type) - { - case PLAYERTAG_LOCAL: - flags |= V_SPLITSCREEN; - K_DrawLocalTagForPlayer(x, y, p, G_PartyPosition(p - players), flags); - break; - - case PLAYERTAG_RIVAL: - flags |= V_SPLITSCREEN; - K_DrawRivalTagForPlayer(x, y, p, flags); - break; - - case PLAYERTAG_CPU: - flags |= V_SPLITSCREEN; - flags |= foreground ? 0 : V_60TRANS; - K_DrawCPUTagForPlayer(x, y, p, flags); - break; - - case PLAYERTAG_NAME: - flags |= foreground ? 0 : V_60TRANS; - K_DrawNameTagForPlayer(x, y, p, flags); - K_DrawTypingNotifier(x, y, p, flags); - break; - - default: - break; - } -} - -typedef struct weakspotdraw_t -{ - UINT8 i; - INT32 x; - INT32 y; - boolean candrawtag; -} weakspotdraw_t; - -static void K_DrawWeakSpot(weakspotdraw_t *ws) -{ - UINT8 *colormap; - UINT8 j = (bossinfo.weakspots[ws->i].type == SPOT_BUMP) ? 1 : 0; - tic_t flashtime = ~1; // arbitrary high even number - - if (bossinfo.weakspots[ws->i].time < TICRATE) - { - if (bossinfo.weakspots[ws->i].time & 1) - return; - - flashtime = bossinfo.weakspots[ws->i].time; - } - else if (bossinfo.weakspots[ws->i].time > (WEAKSPOTANIMTIME - TICRATE)) - flashtime = WEAKSPOTANIMTIME - bossinfo.weakspots[ws->i].time; - - if (flashtime & 1) - colormap = R_GetTranslationColormap(TC_ALLWHITE, SKINCOLOR_NONE, GTC_CACHE); - else - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(bossinfo.weakspots[ws->i].color), GTC_CACHE); - - V_DrawFixedPatch(ws->x, ws->y, FRACUNIT, 0, kp_bossret[j], colormap); - - if (!ws->candrawtag || flashtime & 1 || flashtime < TICRATE/2) - return; - - V_DrawFixedPatch(ws->x, ws->y, FRACUNIT, 0, kp_bossret[j+1], colormap); -} - -static void K_drawKartNameTags(void) -{ - vector3_t c; - UINT8 cnum = R_GetViewNumber(); - size_t i, j; - - if (stplyr == NULL || stplyr->mo == NULL || P_MobjWasRemoved(stplyr->mo)) - { - return; - } - - if (stplyr->awayview.tics) - { - return; - } - - // Crop within splitscreen bounds - switch (r_splitscreen) - { - case 1: - V_SetClipRect( - 0, - cnum == 1 ? (BASEVIDHEIGHT / 2) * FRACUNIT : 0, - BASEVIDWIDTH * FRACUNIT, - (BASEVIDHEIGHT / 2) * FRACUNIT, - 0 - ); - break; - - case 2: - case 3: - V_SetClipRect( - cnum & 1 ? (BASEVIDWIDTH / 2) * FRACUNIT : 0, - cnum > 1 ? (BASEVIDHEIGHT / 2) * FRACUNIT : 0, - (BASEVIDWIDTH / 2) * FRACUNIT, - (BASEVIDHEIGHT / 2) * FRACUNIT, - 0 - ); - break; - } - - c.x = viewx; - c.y = viewy; - c.z = viewz; - - // Maybe shouldn't be handling this here... but the camera info is too good. - if (bossinfo.valid == true) - { - weakspotdraw_t weakspotdraw[NUMWEAKSPOTS]; - UINT8 numdraw = 0; - boolean onleft = false; - - for (i = 0; i < NUMWEAKSPOTS; i++) - { - trackingResult_t result; - vector3_t v; - - if (bossinfo.weakspots[i].spot == NULL || P_MobjWasRemoved(bossinfo.weakspots[i].spot)) - { - // No object - continue; - } - - if (bossinfo.weakspots[i].time == 0 || bossinfo.weakspots[i].type == SPOT_NONE) - { - // not visible - continue; - } - - v.x = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_x, bossinfo.weakspots[i].spot->x); - v.y = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_y, bossinfo.weakspots[i].spot->y); - v.z = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_z, bossinfo.weakspots[i].spot->z); - - v.z += (bossinfo.weakspots[i].spot->height / 2); - - K_ObjectTracking(&result, &v, false); - if (result.onScreen == false) - { - continue; - } - - weakspotdraw[numdraw].i = i; - weakspotdraw[numdraw].x = result.x; - weakspotdraw[numdraw].y = result.y; - weakspotdraw[numdraw].candrawtag = true; - - for (j = 0; j < numdraw; j++) - { - if (abs(weakspotdraw[j].x - weakspotdraw[numdraw].x) > 50*FRACUNIT) - { - continue; - } - - onleft = (weakspotdraw[j].x < weakspotdraw[numdraw].x); - - if (abs((onleft ? -5 : 5) - + weakspotdraw[j].y - weakspotdraw[numdraw].y) > 18*FRACUNIT) - { - continue; - } - - if (weakspotdraw[j].x < weakspotdraw[numdraw].x) - { - weakspotdraw[j].candrawtag = false; - break; - } - - weakspotdraw[numdraw].candrawtag = false; - break; - } - - numdraw++; - } - - for (i = 0; i < numdraw; i++) - { - K_DrawWeakSpot(&weakspotdraw[i]); - } - } - - K_drawTargetHUD(&c, stplyr); - - V_ClearClipRect(); -} - -#define PROGRESSION_BAR_WIDTH 120 - -static INT32 K_getKartProgressionMinimapDistance(UINT32 distancetofinish) -{ - INT32 dist; - - if (specialstageinfo.maxDist == 0U) - { - return 0; - } - - dist = specialstageinfo.maxDist/PROGRESSION_BAR_WIDTH; - - dist = (specialstageinfo.maxDist-distancetofinish)/dist; - - if (dist > PROGRESSION_BAR_WIDTH) - { - return PROGRESSION_BAR_WIDTH; - } - - if (dist < 0) - { - return 0; - } - - return dist; -} - -static void K_drawKartProgressionMinimapIcon(UINT32 distancetofinish, INT32 hudx, INT32 hudy, INT32 flags, patch_t *icon, UINT8 *colormap) -{ - if (distancetofinish == UINT32_MAX) - return; - - hudx += K_getKartProgressionMinimapDistance(distancetofinish); - - hudx = ((hudx - (SHORT(icon->width)/2))<height)/2))<width))/2)<height))/2)<nextwaypoint) - { - return 4; - } - else if (wp->numnextwaypoints == 0 || wp->numprevwaypoints == 0) - { - return 3; - } - else if (!K_GetWaypointIsEnabled(wp)) // disabled - { - return 2; - } - else if (K_GetWaypointIsShortcut(wp)) // shortcut - { - return 1; - } - else - { - return 0; - } -} - -static void K_drawKartMinimapWaypoint(waypoint_t *wp, UINT8 rank, INT32 hudx, INT32 hudy, INT32 flags) -{ - static UINT8 colors[] = - { - 0x95, // blue (0 - default) - 0x20, // pink (1 - shortcut) - 0x10, // gray (2 - disabled) - 0x40, // yellow (3 - error) - 0x70, // green (4 - player) - }; - - UINT8 pal = colors[rank]; // blue - UINT8 size = 3; - - if (rank == 4) - { - size = 6; - } - - if (!(flags & V_NOSCALESTART)) - { - hudx *= vid.dupx; - hudy *= vid.dupy; - } - - K_drawKartMinimapDot(wp->mobj->x, wp->mobj->y, hudx, hudy, flags | V_NOSCALESTART, pal, size); -} - -INT32 K_GetMinimapTransFlags(const boolean usingProgressBar) -{ - INT32 minimaptrans = 4; - boolean dofade = (usingProgressBar && r_splitscreen > 0) || (!usingProgressBar && r_splitscreen >= 1); - - if (dofade) - { - minimaptrans = FixedMul(minimaptrans, (st_translucency * FRACUNIT) / 10); - - // If the minimap is fully transparent, just get your 0 back. Bail out with this. - if (!minimaptrans) - return minimaptrans; - } - - minimaptrans = ((10-minimaptrans)< 0) - { - y = BASEVIDHEIGHT/2; - } - else - { - y = 180; - } - - workingPic = kp_wouldyoustillcatchmeifiwereaworm; - } - - // Really looking forward to never writing this loop again - UINT8 bestplayer = MAXPLAYERS; - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i]) - continue; - if (players[i].spectator) - continue; - if (players[i].position == 1) - bestplayer = i; - } - - if (bestplayer == MAXPLAYERS || leveltime < starttime) // POSITION / no players - minipal = ((leveltime/10)%2) ? SKINCOLOR_WHITE : SKINCOLOR_BLACK; - else if (players[bestplayer].laps >= numlaps) // Final lap - minipal = K_RainbowColor(leveltime); - else // Standard: color to leader - minipal = players[bestplayer].skincolor; - - if (doencore) - { - V_DrawFixedPatch( - (x + (SHORT(workingPic->width)/2))*FRACUNIT, - (y - (SHORT(workingPic->height)/2))*FRACUNIT, - FRACUNIT, - splitflags|minimaptrans|V_FLIP, - workingPic, - R_GetTranslationColormap(TC_DEFAULT, static_cast(minipal), GTC_CACHE) - ); - } - else - { - V_DrawFixedPatch( - (x - (SHORT(workingPic->width)/2))*FRACUNIT, - (y - (SHORT(workingPic->height)/2))*FRACUNIT, - FRACUNIT, - splitflags|minimaptrans, - workingPic, - R_GetTranslationColormap(TC_DEFAULT, static_cast(minipal), GTC_CACHE) - ); - } - - // most icons will be rendered semi-ghostly. - splitflags |= V_HUDTRANSHALF; - - // let offsets transfer to the heads, too! - if (doencore) - x += SHORT(workingPic->leftoffset); - else - x -= SHORT(workingPic->leftoffset); - y -= SHORT(workingPic->topoffset); - - if (doprogressionbar == true) - { - x -= PROGRESSION_BAR_WIDTH/2; - } - - // Draw the super item in Battle - if (doprogressionbar == false && (gametyperules & GTR_OVERTIME) && battleovertime.enabled) - { - if (battleovertime.enabled >= 10*TICRATE || (battleovertime.enabled & 1)) - { - const INT32 prevsplitflags = splitflags; - splitflags &= ~V_HUDTRANSHALF; - splitflags |= V_HUDTRANS; - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(K_RainbowColor(leveltime)), GTC_CACHE); - K_drawKartMinimapIcon(battleovertime.x, battleovertime.y, x, y, splitflags, kp_itemminimap, colormap); - splitflags = prevsplitflags; - } - } - - // initialize - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - localplayers[i] = -1; - - // Player's tiny icons on the Automap. (drawn opposite direction so player 1 is drawn last in splitscreen) - if (ghosts && doprogressionbar == false) // future work: show ghosts on progression bar - { - demoghost *g = ghosts; - while (g) - { - if (g->mo && !P_MobjWasRemoved(g->mo) && g->mo->skin) - { - skin = ((skin_t*)g->mo->skin)-skins; - - workingPic = R_CanShowSkinInDemo(skin) ? faceprefix[skin][FACE_MINIMAP] : kp_unknownminimap; - - if (g->mo->color) - { - if (g->mo->colorized) - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(g->mo->color), GTC_CACHE); - else - colormap = R_GetTranslationColormap(skin, static_cast(g->mo->color), GTC_CACHE); - } - else - colormap = NULL; - - interpx = R_InterpolateFixed(g->mo->old_x, g->mo->x); - interpy = R_InterpolateFixed(g->mo->old_y, g->mo->y); - - K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap); - } - - g = g->next; - } - } - - { - for (i = MAXPLAYERS-1; i >= 0; i--) - { - if (!playeringame[i]) - continue; - if (!players[i].mo || players[i].spectator || !players[i].mo->skin - || (doprogressionbar == false && players[i].exiting)) - continue; - - // This player is out of the game! - if ((gametyperules & GTR_BUMPERS) && (players[i].pflags & PF_ELIMINATED)) - continue; - - // This gets set for a player who has GAME OVER'd - if (P_MobjIsReappearing(players[i].mo)) - continue; - - if (i == displayplayers[0] || i == displayplayers[1] || i == displayplayers[2] || i == displayplayers[3]) - { - // Draw display players on top of everything else - localplayers[numlocalplayers++] = i; - continue; - } - - if (players[i].hyudorotimer > 0) - { - if (!((players[i].hyudorotimer < TICRATE/2 - || players[i].hyudorotimer > hyudorotime-(TICRATE/2)) - && !(leveltime & 1))) - continue; - } - - mobj = players[i].mo; - - if (mobj->health <= 0 && (players[i].pflags & PF_NOCONTEST)) - { - if (P_MobjWasRemoved(mobj->tracer)) - { - continue; - } - - if (mobj->tracer->renderflags & RF_DONTDRAW) - { - continue; - } - - workingPic = kp_nocontestminimap; - colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(mobj->color), GTC_CACHE); - - mobj = mobj->tracer; - } - else - { - skin = ((skin_t*)mobj->skin)-skins; - - workingPic = R_CanShowSkinInDemo(skin) ? faceprefix[skin][FACE_MINIMAP] : kp_unknownminimap; - - if (mobj->color) - { - if (mobj->colorized) - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(mobj->color), GTC_CACHE); - else - colormap = R_GetTranslationColormap(skin, static_cast(mobj->color), GTC_CACHE); - } - else - colormap = NULL; - } - - if (doprogressionbar == false) - { - interpx = R_InterpolateFixed(mobj->old_x, mobj->x); - interpy = R_InterpolateFixed(mobj->old_y, mobj->y); - - K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap); - - // Target reticule - if (((gametyperules & GTR_CIRCUIT) && players[i].position == spbplace) - || ((gametyperules & (GTR_BOSS|GTR_POINTLIMIT)) == GTR_POINTLIMIT && K_IsPlayerWanted(&players[i]))) - { - K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, kp_wantedreticle, NULL); - } - } - else - { - K_drawKartProgressionMinimapIcon(players[i].distancetofinish, x, y, splitflags, workingPic, colormap); - } - } - } - - // draw minimap-pertinent objects - if (doprogressionbar == true) - { - // future work: support these specific objects on this - } - else for (mobj = trackercap; mobj; mobj = next) - { - next = mobj->itnext; - - workingPic = NULL; - colormap = NULL; - - if (mobj->health <= 0) - continue; - - switch (mobj->type) - { - case MT_SPB: - workingPic = kp_spbminimap; -#if 0 - if (mobj->target && !P_MobjWasRemoved(mobj->target) && mobj->target->player && mobj->target->player->skincolor) - { - colormap = R_GetTranslationColormap(TC_RAINBOW, mobj->target->player->skincolor, GTC_CACHE); - } - else -#endif - if (mobj->color) - { - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(mobj->color), GTC_CACHE); - } - - break; - case MT_BATTLECAPSULE: - workingPic = kp_capsuleminimap[(mobj->extravalue1 != 0 ? 1 : 0)]; - break; - case MT_CDUFO: - if (battleprisons) - workingPic = kp_capsuleminimap[2]; - break; - case MT_BATTLEUFO: - workingPic = kp_battleufominimap; - break; - case MT_SUPER_FLICKY: - workingPic = kp_superflickyminimap; - if (mobj_t* owner = Obj_SuperFlickyOwner(mobj); owner && owner->color) - { - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(owner->color), GTC_CACHE); - } - break; - default: - break; - } - - if (!workingPic) - continue; - - interpx = R_InterpolateFixed(mobj->old_x, mobj->x); - interpy = R_InterpolateFixed(mobj->old_y, mobj->y); - - K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap); - } - - // draw our local players here, opaque. - { - splitflags &= ~V_HUDTRANSHALF; - splitflags |= V_HUDTRANS; - } - - // ...but first, any boss targets. - if (doprogressionbar == true) - { - if (specialstageinfo.valid == true) - { - UINT32 distancetofinish = K_GetSpecialUFODistance(); - if (distancetofinish > 0 && specialstageinfo.ufo != NULL && P_MobjWasRemoved(specialstageinfo.ufo) == false) - { - colormap = NULL; - if (specialstageinfo.ufo->health > 1) - { - workingPic = kp_catcherminimap; - } - else - { - UINT8 emid = 0; - if (specialstageinfo.ufo->cvmem > 7) - emid = 1; - workingPic = kp_emeraldminimap[emid]; - - if (specialstageinfo.ufo->color) - { - colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(specialstageinfo.ufo->color), GTC_CACHE); - } - } - - K_drawKartProgressionMinimapIcon(distancetofinish, x, y, splitflags, workingPic, colormap); - } - } - - // future work: support boss minimap icons on the progression bar - } - else if (bossinfo.valid == true) - { - for (i = 0; i < NUMWEAKSPOTS; i++) - { - // exists at all? - if (bossinfo.weakspots[i].spot == NULL || P_MobjWasRemoved(bossinfo.weakspots[i].spot)) - continue; - // shows on the minimap? - if (bossinfo.weakspots[i].minimap == false) - continue; - // in the flashing period? - if ((bossinfo.weakspots[i].time > (WEAKSPOTANIMTIME-(TICRATE/2))) && (bossinfo.weakspots[i].time & 1)) - continue; - - colormap = NULL; - - if (bossinfo.weakspots[i].color) - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(bossinfo.weakspots[i].color), GTC_CACHE); - - interpx = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_x, bossinfo.weakspots[i].spot->x); - interpy = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_y, bossinfo.weakspots[i].spot->y); - - // temporary graphic? - K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, kp_wantedreticle, colormap); - } - } - - for (i = 0; i < numlocalplayers; i++) - { - boolean nocontest = false; - - if (localplayers[i] == -1) - continue; // this doesn't interest us - - if ((players[localplayers[i]].hyudorotimer > 0) && (leveltime & 1)) - continue; - - mobj = players[localplayers[i]].mo; - - // This gets set for a player who has GAME OVER'd - if (P_MobjIsReappearing(mobj)) - continue; - - if (mobj->health <= 0 && (players[localplayers[i]].pflags & PF_NOCONTEST)) - { - if (P_MobjWasRemoved(mobj->tracer)) - { - continue; - } - - if (mobj->tracer->renderflags & RF_DONTDRAW) - { - continue; - } - - workingPic = kp_nocontestminimap; - colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(mobj->color), GTC_CACHE); - - mobj = mobj->tracer; - - nocontest = true; - } - else - { - skin = ((skin_t*)mobj->skin)-skins; - - workingPic = R_CanShowSkinInDemo(skin) ? faceprefix[skin][FACE_MINIMAP] : kp_unknownminimap; - - if (mobj->color) - { - if (mobj->colorized) - colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(mobj->color), GTC_CACHE); - else - colormap = R_GetTranslationColormap(skin, static_cast(mobj->color), GTC_CACHE); - } - else - colormap = NULL; - } - - if (doprogressionbar == false) - { - interpx = R_InterpolateFixed(mobj->old_x, mobj->x); - interpy = R_InterpolateFixed(mobj->old_y, mobj->y); - - K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap); - - // Target reticule - if (((gametyperules & GTR_CIRCUIT) && players[localplayers[i]].position == spbplace) - || ((gametyperules & (GTR_BOSS|GTR_POINTLIMIT)) == GTR_POINTLIMIT && K_IsPlayerWanted(&players[localplayers[i]]))) - { - K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, kp_wantedreticle, NULL); - } - - if (!nocontest) - { - angle_t ang = R_InterpolateAngle(mobj->old_angle, mobj->angle); - if (encoremode) - ang = ANGLE_180 - ang; - - if (skin && mobj->color && !mobj->colorized // relevant to redo - && skins[skin].starttranscolor != skins[0].starttranscolor) // redoing would have an affect - { - colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(mobj->color), GTC_CACHE); - } - - K_drawKartMinimapIcon( - interpx, - interpy, - x + FixedMul(FCOS(ang), ICON_DOT_RADIUS), - y - FixedMul(FSIN(ang), ICON_DOT_RADIUS), - splitflags, - kp_minimapdot, - colormap - ); - } - } - else - { - K_drawKartProgressionMinimapIcon(players[localplayers[i]].distancetofinish, x, y, splitflags, workingPic, colormap); - } - } - - if (doprogressionbar == false && cv_kartdebugwaypoints.value != 0) - { - struct MiniWaypoint - { - waypoint_t* waypoint; - UINT8 rank; - - MiniWaypoint(waypoint_t* wp) : waypoint(wp), rank(K_RankMinimapWaypoint(wp)) {} - - bool operator<(const MiniWaypoint& b) const noexcept { return rank < b.rank; } - }; - - std::vector waypoints; - size_t idx; - - waypoints.reserve(K_GetNumWaypoints()); - - for (idx = 0; idx < K_GetNumWaypoints(); ++idx) - { - waypoint_t *wp = K_GetWaypointFromIndex(idx); - - I_Assert(wp != NULL); - - waypoints.push_back(wp); - } - - std::sort(waypoints.begin(), waypoints.end()); - - for (MiniWaypoint& wp : waypoints) - { - K_drawKartMinimapWaypoint(wp.waypoint, wp.rank, x, y, splitflags); - } - } -} - -#undef PROGRESSION_BAR_WIDTH - -static void K_drawKartFinish(boolean finish) -{ - INT32 timer, minsplitstationary, pnum = 0, splitflags = V_SPLITSCREEN; - patch_t **kptodraw; - - if (finish) - { - if (gametyperules & GTR_SPECIALSTART) - return; - - timer = stplyr->karthud[khud_finish]; - kptodraw = kp_racefinish; - minsplitstationary = 2; - } - else - { - timer = stplyr->karthud[khud_fault]; - kptodraw = kp_racefault; - minsplitstationary = 1; - } - - if (!timer || timer > 2*TICRATE) - return; - - if ((timer % (2*5)) / 5) // blink - pnum = 1; - - if (r_splitscreen > 0) - pnum += (r_splitscreen > 1) ? 2 : 4; - - if (r_splitscreen >= minsplitstationary) // 3/4p, stationary FIN - { - V_DrawScaledPatch(STCD_X - (SHORT(kptodraw[pnum]->width)/2), STCD_Y - (SHORT(kptodraw[pnum]->height)/2), splitflags, kptodraw[pnum]); - return; - } - - //else -- 1/2p, scrolling FINISH - { - INT32 x, xval, ox, interpx, pwidth; - - x = ((vid.width<width)<height)<<(FRACBITS-1)), - FRACUNIT, - splitflags, kptodraw[pnum], NULL); - } -} - -static void K_drawKartStartBulbs(void) -{ - const UINT8 start_animation[14] = { - 1, 2, 3, 4, 5, 6, 7, 8, - 7, 6, - 9, 10, 11, 12 - }; - - const UINT8 loop_animation[4] = { - 12, 13, 12, 14 - }; - - const UINT8 chillloop_animation[2] = { - 11, 12 - }; - - const UINT8 letters_order[10] = { - 0, 1, 2, 3, 4, 3, 1, 5, 6, 6 - }; - - const UINT8 letters_transparency[40] = { - 0, 2, 4, 6, 8, - 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, - 10, 8, 6, 4, 2 - }; - - fixed_t spacing = 24*FRACUNIT; - - fixed_t startx = (BASEVIDWIDTH/2)*FRACUNIT; - fixed_t starty = 48*FRACUNIT; - fixed_t x, y; - - UINT8 numperrow = numbulbs/2; - UINT8 i; - - if (r_splitscreen >= 1) - { - spacing /= 2; - starty /= 3; - - if (r_splitscreen > 1) - { - startx /= 2; - } - } - - startx += (spacing/2); - - if (numbulbs <= 10) - { - // No second row - numperrow = numbulbs; - } - else - { - if (numbulbs & 1) - { - numperrow++; - } - - starty -= (spacing/2); - } - - startx -= (spacing/2) * numperrow; - - x = startx; - y = starty; - - for (i = 0; i < numbulbs; i++) - { - UINT8 patchnum = 0; - INT32 bulbtic = (leveltime - introtime - TICRATE) - (bulbtime * i); - - if (i == numperrow) - { - y += spacing; - x = startx + (spacing/2); - } - - if (bulbtic > 0) - { - if (bulbtic < 14) - { - patchnum = start_animation[bulbtic]; - } - else - { - const INT32 length = (bulbtime * 3); - - bulbtic -= 14; - - // Reduce VFX disables the bulb animation while still presenting this indicator - - if (bulbtic > length) - { - bulbtic -= length; - - if (cv_reducevfx.value != 0) - { - patchnum = chillloop_animation[0]; - } - else - { - patchnum = chillloop_animation[bulbtic % 2]; - } - } - else - { - if (cv_reducevfx.value != 0) - { - patchnum = loop_animation[0]; - } - else - { - patchnum = loop_animation[bulbtic % 4]; - } - } - } - } - - V_DrawFixedPatch(x, y, FRACUNIT, V_SNAPTOTOP|V_SPLITSCREEN, - (r_splitscreen ? kp_prestartbulb_split[patchnum] : kp_prestartbulb[patchnum]), NULL); - x += spacing; - } - - x = 70*FRACUNIT; - y = starty; - - if (r_splitscreen == 1) - { - x = 106*FRACUNIT; - } - else if (r_splitscreen > 1) - { - x = 28*FRACUNIT; - } - - if (timeinmap < 16) - return; // temporary for current map start behaviour - - for (i = 0; i < 10; i++) - { - UINT8 patchnum = letters_order[i]; - INT32 transflag = letters_transparency[(leveltime - i) % 40]; - patch_t *patch = (r_splitscreen ? kp_prestartletters_split[patchnum] : kp_prestartletters[patchnum]); - - if (transflag >= 10) - ; - else - { - if (transflag != 0) - transflag = transflag << FF_TRANSSHIFT; - - V_DrawFixedPatch(x, y, FRACUNIT, V_SNAPTOTOP|V_SPLITSCREEN|transflag, patch, NULL); - } - - if (i < 9) - { - x += (SHORT(patch->width)) * FRACUNIT/2; - - patchnum = letters_order[i+1]; - patch = (r_splitscreen ? kp_prestartletters_split[patchnum] : kp_prestartletters[patchnum]); - x += (SHORT(patch->width)) * FRACUNIT/2; - - if (r_splitscreen) - x -= FRACUNIT; - } - } -} - -static void K_drawKartStartCountdown(void) -{ - INT32 pnum = 0; - - if (leveltime >= introtime && leveltime < starttime-(3*TICRATE)) - { - if (numbulbs > 1) - K_drawKartStartBulbs(); - } - else - { - - if (leveltime >= starttime-(2*TICRATE)) // 2 - pnum++; - if (leveltime >= starttime-TICRATE) // 1 - pnum++; - - if (leveltime >= starttime) // GO! - { - UINT8 i; - UINT8 numplayers = 0; - - pnum++; - - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i] && !players[i].spectator) - numplayers++; - - if (numplayers > 2) - break; - } - - if (inDuel == true) - { - pnum++; // DUEL - } - } - - if ((leveltime % (2*5)) / 5) // blink - pnum += 5; - if (r_splitscreen) // splitscreen - pnum += 10; - - V_DrawScaledPatch(STCD_X - (SHORT(kp_startcountdown[pnum]->width)/2), STCD_Y - (SHORT(kp_startcountdown[pnum]->height)/2), V_SPLITSCREEN, kp_startcountdown[pnum]); - } -} - -static void K_drawKartFirstPerson(void) -{ - static INT32 pnum[4], turn[4], drift[4]; - const INT16 steerThreshold = KART_FULLTURN / 2; - INT32 pn = 0, tn = 0, dr = 0; - INT32 target = 0, splitflags = V_SNAPTOBOTTOM|V_SPLITSCREEN; - INT32 x = BASEVIDWIDTH/2, y = BASEVIDHEIGHT; - fixed_t scale; - UINT8 *colmap = NULL; - - if (stplyr->spectator || !stplyr->mo || (stplyr->mo->renderflags & RF_DONTDRAW)) - return; - - { - UINT8 view = R_GetViewNumber(); - pn = pnum[view]; - tn = turn[view]; - dr = drift[view]; - } - - if (r_splitscreen) - { - y >>= 1; - if (r_splitscreen > 1) - x >>= 1; - } - - { - if (stplyr->speed < (20*stplyr->mo->scale) && (leveltime & 1) && !r_splitscreen) - y++; - - if (stplyr->mo->renderflags & RF_TRANSMASK) - splitflags |= ((stplyr->mo->renderflags & RF_TRANSMASK) >> RF_TRANSSHIFT) << FF_TRANSSHIFT; - else if (stplyr->mo->frame & FF_TRANSMASK) - splitflags |= (stplyr->mo->frame & FF_TRANSMASK); - } - - if (stplyr->steering > steerThreshold) // strong left turn - target = 2; - else if (stplyr->steering < -steerThreshold) // strong right turn - target = -2; - else if (stplyr->steering > 0) // weak left turn - target = 1; - else if (stplyr->steering < 0) // weak right turn - target = -1; - else // forward - target = 0; - - if (encoremode) - target = -target; - - if (pn < target) - pn++; - else if (pn > target) - pn--; - - if (pn < 0) - splitflags |= V_FLIP; // right turn - - target = abs(pn); - if (target > 2) - target = 2; - - x <<= FRACBITS; - y <<= FRACBITS; - - if (tn != stplyr->steering/50) - tn -= (tn - (stplyr->steering/50))/8; - - if (dr != stplyr->drift*16) - dr -= (dr - (stplyr->drift*16))/8; - - if (r_splitscreen == 1) - { - scale = (2*FRACUNIT)/3; - y += FRACUNIT/(vid.dupx < vid.dupy ? vid.dupx : vid.dupy); // correct a one-pixel gap on the screen view (not the basevid view) - } - else if (r_splitscreen) - scale = FRACUNIT/2; - else - scale = FRACUNIT; - - if (stplyr->mo) - { - UINT8 driftcolor = K_DriftSparkColor(stplyr, stplyr->driftcharge); - const angle_t ang = R_PointToAngle2(0, 0, stplyr->rmomx, stplyr->rmomy) - stplyr->drawangle; - // yes, the following is correct. no, you do not need to swap the x and y. - fixed_t xoffs = -P_ReturnThrustY(stplyr->mo, ang, (BASEVIDWIDTH<<(FRACBITS-2))/2); - fixed_t yoffs = -P_ReturnThrustX(stplyr->mo, ang, 4*FRACUNIT); - - // hitlag vibrating - if (stplyr->mo->hitlag > 0 && (stplyr->mo->eflags & MFE_DAMAGEHITLAG)) - { - fixed_t mul = stplyr->mo->hitlag * HITLAGJITTERS; - if (r_splitscreen && mul > FRACUNIT) - mul = FRACUNIT; - - if (leveltime & 1) - { - mul = -mul; - } - - xoffs = FixedMul(xoffs, mul); - yoffs = FixedMul(yoffs, mul); - - } - - if ((yoffs += 4*FRACUNIT) < 0) - yoffs = 0; - - if (r_splitscreen) - xoffs = FixedMul(xoffs, scale); - - xoffs -= (tn)*scale; - xoffs -= (dr)*scale; - - if (stplyr->drawangle == stplyr->mo->angle) - { - const fixed_t mag = FixedDiv(stplyr->speed, 10*stplyr->mo->scale); - - if (mag < FRACUNIT) - { - xoffs = FixedMul(xoffs, mag); - if (!r_splitscreen) - yoffs = FixedMul(yoffs, mag); - } - } - - if (stplyr->mo->momz > 0) // TO-DO: Draw more of the kart so we can remove this if! - yoffs += stplyr->mo->momz/3; - - if (encoremode) - x -= xoffs; - else - x += xoffs; - if (!r_splitscreen) - y += yoffs; - - - if ((leveltime & 1) && (driftcolor != SKINCOLOR_NONE)) // drift sparks! - colmap = R_GetTranslationColormap(TC_RAINBOW, static_cast(driftcolor), GTC_CACHE); - else if (stplyr->mo->colorized && stplyr->mo->color) // invincibility/grow/shrink! - colmap = R_GetTranslationColormap(TC_RAINBOW, static_cast(stplyr->mo->color), GTC_CACHE); - } - - V_DrawFixedPatch(x, y, scale, splitflags, kp_fpview[target], colmap); - - { - UINT8 view = R_GetViewNumber(); - pnum[view] = pn; - turn[view] = tn; - drift[view] = dr; - } -} - -static void K_drawInput(void) -{ - UINT8 viewnum = R_GetViewNumber(); - boolean freecam = camera[viewnum].freecam; //disable some hud elements w/ freecam - - if (!cv_drawinput.value && !modeattacking && gametype != GT_TUTORIAL) - return; - - if (stplyr->spectator || freecam || demo.attract) - return; - - INT32 def[4][3] = { - {247, 156, V_SNAPTOBOTTOM | V_SNAPTORIGHT}, // 1p - {247, 56, V_SNAPTOBOTTOM | V_SNAPTORIGHT}, // 2p - {6, 52, V_SNAPTOBOTTOM | V_SNAPTOLEFT}, // 4p left - {282 - BASEVIDWIDTH/2, 52, V_SNAPTOBOTTOM | V_SNAPTORIGHT}, // 4p right - }; - INT32 k = r_splitscreen <= 1 ? r_splitscreen : 2 + (viewnum & 1); - INT32 flags = def[k][2] | V_SPLITSCREEN; - char mode = ((stplyr->pflags & PF_ANALOGSTICK) ? '4' : '2') + (r_splitscreen > 1); - bool local = !demo.playback && P_IsMachineLocalPlayer(stplyr); - fixed_t slide = K_GetDialogueSlide(FRACUNIT); - INT32 tallySlide = []() -> INT32 - { - if (r_splitscreen <= 1) - { - return 0; - } - if (!stplyr->tally.active) - { - return 0; - } - constexpr INT32 kSlideDown = 22; - if (stplyr->tally.state == TALLY_ST_GOTTHRU_SLIDEIN || - stplyr->tally.state == TALLY_ST_GAMEOVER_SLIDEIN) - { - return static_cast(Easing_OutQuad(std::min(stplyr->tally.transition * 2, FRACUNIT), 0, kSlideDown)); - } - return kSlideDown; - }(); - if (slide) - flags &= ~(V_SNAPTORIGHT); // don't draw underneath the dialogue box in non-green resolutions - - // Move above the boss health bar. - // TODO: boss HUD only works in 1P, so this only works in 1P too. - if (LUA_HudEnabled(hud_position) && bossinfo.valid) - { - constexpr tic_t kDelay = 2u; - // See K_drawBossHealthBar - tic_t start = lt_endtime - 1u; - tic_t t = std::clamp(lt_ticker, start, start + kDelay) - start; - def[0][1] -= 24 + Easing_Linear(t * FRACUNIT / kDelay, 0, 7); - } - - K_DrawInputDisplay( - def[k][0] - FixedToFloat(34 * slide), - def[k][1] - FixedToFloat(51 * slide) + tallySlide, - flags, - mode, - (local ? G_LocalSplitscreenPartyPosition : G_PartyPosition)(stplyr - players), - local, - stplyr->speed > 0 - ); -} - -static void K_drawChallengerScreen(void) -{ - // This is an insanely complicated animation. - static UINT8 anim[52] = { - 0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13, // frame 1-14, 2 tics: HERE COMES A NEW slides in - 14,14,14,14,14,14, // frame 15, 6 tics: pause on the W - 15,16,17,18, // frame 16-19, 1 tic: CHALLENGER approaches screen - 19,20,19,20,19,20,19,20,19,20, // frame 20-21, 1 tic, 5 alternating: all text vibrates from impact - 21,22,23,24 // frame 22-25, 1 tic: CHALLENGER turns gold - }; - const UINT8 offset = std::min(52-1u, (3*TICRATE)-mapreset); - - V_DrawFadeScreen(0xFF00, 16); // Fade out - V_DrawScaledPatch(0, 0, 0, kp_challenger[anim[offset]]); -} - -static void K_drawLapStartAnim(void) -{ - if (demo.attract == DEMO_ATTRACT_CREDITS) - { - return; - } - - // This is an EVEN MORE insanely complicated animation. - const UINT8 t = stplyr->karthud[khud_lapanimation]; - const UINT8 progress = 80 - t; - - const UINT8 tOld = t + 1; - const UINT8 progressOld = 80 - tOld; - - const tic_t leveltimeOld = leveltime - 1; - - UINT8 *colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(stplyr->skincolor), GTC_CACHE); - - fixed_t interpx, interpy, newval, oldval; - - newval = (BASEVIDWIDTH/2 + (32 * std::max(0, t - 76))) * FRACUNIT; - oldval = (BASEVIDWIDTH/2 + (32 * std::max(0, tOld - 76))) * FRACUNIT; - interpx = R_InterpolateFixed(oldval, newval); - - newval = (48 - (32 * std::max(0, progress - 76))) * FRACUNIT; - oldval = (48 - (32 * std::max(0, progressOld - 76))) * FRACUNIT; - interpy = R_InterpolateFixed(oldval, newval); - - V_DrawFixedPatch( - interpx, interpy, - FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, - (modeattacking ? kp_lapanim_emblem[1] : kp_lapanim_emblem[0]), colormap); - - if (stplyr->karthud[khud_laphand] >= 1 && stplyr->karthud[khud_laphand] <= 3) - { - newval = (4 - abs((signed)((leveltime % 8) - 4))) * FRACUNIT; - oldval = (4 - abs((signed)((leveltimeOld % 8) - 4))) * FRACUNIT; - interpy += R_InterpolateFixed(oldval, newval); - - V_DrawFixedPatch( - interpx, interpy, - FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, - kp_lapanim_hand[stplyr->karthud[khud_laphand]-1], NULL); - } - - if (stplyr->latestlap == (UINT8)(numlaps)) - { - newval = (62 - (32 * std::max(0, progress - 76))) * FRACUNIT; - oldval = (62 - (32 * std::max(0, progressOld - 76))) * FRACUNIT; - interpx = R_InterpolateFixed(oldval, newval); - - V_DrawFixedPatch( - interpx, // 27 - 30*FRACUNIT, // 24 - FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, - kp_lapanim_final[std::min(progress/2, 10)], NULL); - - if (progress/2-12 >= 0) - { - newval = (188 + (32 * std::max(0, progress - 76))) * FRACUNIT; - oldval = (188 + (32 * std::max(0, progressOld - 76))) * FRACUNIT; - interpx = R_InterpolateFixed(oldval, newval); - - V_DrawFixedPatch( - interpx, // 194 - 30*FRACUNIT, // 24 - FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, - kp_lapanim_lap[std::min(progress/2-12, 6)], NULL); - } - } - else - { - newval = (82 - (32 * std::max(0, progress - 76))) * FRACUNIT; - oldval = (82 - (32 * std::max(0, progressOld - 76))) * FRACUNIT; - interpx = R_InterpolateFixed(oldval, newval); - - V_DrawFixedPatch( - interpx, // 61 - 30*FRACUNIT, // 24 - FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, - kp_lapanim_lap[std::min(progress/2, 6)], NULL); - - if (progress/2-8 >= 0) - { - newval = (188 + (32 * std::max(0, progress - 76))) * FRACUNIT; - oldval = (188 + (32 * std::max(0, progressOld - 76))) * FRACUNIT; - interpx = R_InterpolateFixed(oldval, newval); - - V_DrawFixedPatch( - interpx, // 194 - 30*FRACUNIT, // 24 - FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, - kp_lapanim_number[(((UINT32)stplyr->latestlap) / 10)][std::min(progress/2-8, 2)], NULL); - - if (progress/2-10 >= 0) - { - newval = (208 + (32 * std::max(0, progress - 76))) * FRACUNIT; - oldval = (208 + (32 * std::max(0, progressOld - 76))) * FRACUNIT; - interpx = R_InterpolateFixed(oldval, newval); - - V_DrawFixedPatch( - interpx, // 221 - 30*FRACUNIT, // 24 - FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, - kp_lapanim_number[(((UINT32)stplyr->latestlap) % 10)][std::min(progress/2-10, 2)], NULL); - } - } - } -} - -// stretch for "COOOOOL" popup. -// I can't be fucked to find out any math behind this so have a table lmao -static fixed_t stretch[6][2] = { - {FRACUNIT/4, FRACUNIT*4}, - {FRACUNIT/2, FRACUNIT*2}, - {FRACUNIT, FRACUNIT}, - {FRACUNIT*4, FRACUNIT/2}, - {FRACUNIT*8, FRACUNIT/4}, - {FRACUNIT*4, FRACUNIT/2}, -}; - -static void K_drawTrickCool(void) -{ - - tic_t timer = TICRATE - stplyr->karthud[khud_trickcool]; - - if (timer <= 6) - { - V_DrawStretchyFixedPatch(TCOOL_X<spectator == true) - return; - - if (M_NotFreePlay() == true) - return; - - if (lt_exitticker < TICRATE/2) - return; - - if (((leveltime-lt_endtime) % TICRATE) < TICRATE/2) - return; - - INT32 h_snap = r_splitscreen < 2 ? V_SNAPTORIGHT | V_SLIDEIN : V_HUDTRANS; - fixed_t x = ((r_splitscreen > 1 ? BASEVIDWIDTH/4 : BASEVIDWIDTH - (LAPS_X+6)) * FRACUNIT); - fixed_t y = ((r_splitscreen ? BASEVIDHEIGHT/2 : BASEVIDHEIGHT) - 20) * FRACUNIT; - - x -= V_StringScaledWidth( - FRACUNIT, - FRACUNIT, - FRACUNIT, - V_SNAPTOBOTTOM|h_snap|V_SPLITSCREEN, - KART_FONT, - "FREE PLAY" - ) / (r_splitscreen > 1 ? 2 : 1); - - V_DrawStringScaled( - x, - y, - FRACUNIT, - FRACUNIT, - FRACUNIT, - V_SNAPTOBOTTOM|h_snap|V_SPLITSCREEN, - NULL, - KART_FONT, - "FREE PLAY" - ); -} - -static void -Draw_party_ping (int ss, INT32 snap) -{ - UINT32 ping = playerpingtable[displayplayers[ss]]; - UINT32 mindelay = playerdelaytable[displayplayers[ss]]; - HU_drawMiniPing(0, 0, ping, mindelay, V_SPLITSCREEN|V_SNAPTOTOP|snap); -} - -static void -K_drawMiniPing (void) -{ - UINT32 f = V_SNAPTORIGHT; - UINT8 i = R_GetViewNumber(); - - if (r_splitscreen > 1 && !(i & 1)) - { - f = V_SNAPTOLEFT; - } - - Draw_party_ping(i, f); -} - -void K_drawButton(fixed_t x, fixed_t y, INT32 flags, patch_t *button[2], boolean pressed) -{ - V_DrawFixedPatch(x, y, FRACUNIT, flags, button[(pressed == true) ? 1 : 0], NULL); -} - -void K_drawButtonAnim(INT32 x, INT32 y, INT32 flags, patch_t *button[2], tic_t animtic) -{ - const UINT8 anim_duration = 16; - const boolean anim = ((animtic % (anim_duration * 2)) < anim_duration); - K_drawButton(x << FRACBITS, y << FRACBITS, flags, button, anim); -} - -static void K_drawDistributionDebugger(void) -{ - itemroulette_t rouletteData = {0}; - - const fixed_t scale = (FRACUNIT >> 1); - const fixed_t pad = 9 * scale; - - fixed_t x = -pad; - - if (R_GetViewNumber() != 0) // only for p1 - { - return; - } - - K_FillItemRouletteData(stplyr, &rouletteData, false, true); - - if (cv_kartdebugdistribution.value <= 1) - return; - - V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+10, V_SNAPTOTOP|V_SNAPTORIGHT, va("speed = %u", rouletteData.speed)); - - V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+22, V_SNAPTOTOP|V_SNAPTORIGHT, va("baseDist = %u", rouletteData.baseDist)); - V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+30, V_SNAPTOTOP|V_SNAPTORIGHT, va("dist = %u", rouletteData.dist)); - - V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+42, V_SNAPTOTOP|V_SNAPTORIGHT, va("firstDist = %u", rouletteData.firstDist)); - V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+50, V_SNAPTOTOP|V_SNAPTORIGHT, va("secondDist = %u", rouletteData.secondDist)); - V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+58, V_SNAPTOTOP|V_SNAPTORIGHT, va("secondToFirst = %u", rouletteData.secondToFirst)); - -#ifndef ITEM_LIST_SIZE - Z_Free(rouletteData.itemList); -#endif -} - -static void K_DrawWaypointDebugger(void) -{ - if (cv_kartdebugwaypoints.value == 0) - return; - - if (R_GetViewNumber() != 0) // only for p1 - return; - - constexpr int kH = 8; - using srb2::Draw; - Draw::TextElement label; - label.font(Draw::Font::kThin); - label.flags(V_AQUAMAP); - Draw line = Draw(8, 110).font(Draw::Font::kMenu); - auto put = [&](const char* label_str, auto&&... args) - { - constexpr int kTabWidth = 48; - label.string(label_str); - int x = label.width() + kTabWidth; - x -= x % kTabWidth; - line.size(x + 4, 2).y(7).fill(31); - line.text(label); - line.x(x).text(args...); - line = line.y(kH); - }; - - if (netgame) - { - line = line.y(-kH); - put("Online griefing:", "[{}, {}]", stplyr->griefValue/TICRATE, stplyr->griefStrikes); - } - - put("Current Waypoint ID:", "{}", K_GetWaypointID(stplyr->currentwaypoint)); - put("Next Waypoint ID:", "{}{}", K_GetWaypointID(stplyr->nextwaypoint), ((stplyr->pflags & PF_WRONGWAY) ? " (WRONG WAY)" : "")); - put("Respawn Waypoint ID:", "{}", K_GetWaypointID(stplyr->respawn.wp)); - put("Finishline Distance:", "{}", stplyr->distancetofinish); - put("Last Safe Lap:", "{}", stplyr->lastsafelap); - - if (numcheatchecks > 0) - { - if (stplyr->cheatchecknum == numcheatchecks) - put("Cheat Check:", "{} / {} (Can finish)", stplyr->cheatchecknum, numcheatchecks); - else - put("Cheat Check:", "{} / {}", stplyr->cheatchecknum, numcheatchecks); - put("Last Safe Cheat Check:", "{}", stplyr->lastsafecheatcheck); - } - - if (stplyr->bigwaypointgap) - { - put("Auto Respawn Timer:", "{}", stplyr->bigwaypointgap); - } -} - -static void K_DrawBotDebugger(void) -{ - player_t *bot = NULL; - - if (cv_kartdebugbots.value == 0) - { - return; - } - - if (R_GetViewNumber() != 0) // only for p1 - { - return; - } - - if (stplyr->bot == true) - { - // we ARE the bot - bot = stplyr; - } - else - { - // get winning bot - size_t i; - for (i = 0; i < MAXPLAYERS; i++) - { - player_t *p = NULL; - - if (playeringame[i] == false) - { - continue; - } - - p = &players[i]; - if (p->spectator == true || p->bot == false) - { - continue; - } - - if (bot == NULL || p->distancetofinish < bot->distancetofinish) - { - bot = p; - } - } - } - - if (bot == NULL) - { - // no bot exists? - return; - } - - V_DrawSmallString(16, 8, V_YELLOWMAP, va("Bot: %s", player_names[bot - players])); - - V_DrawSmallString(8, 14, 0, va("Difficulty: %d / %d", bot->botvars.difficulty, MAXBOTDIFFICULTY)); - V_DrawSmallString(8, 18, 0, va("Difficulty increase: %d", bot->botvars.diffincrease)); - V_DrawSmallString(8, 22, 0, va("Rival: %d", (UINT8)(bot->botvars.rival == true))); - V_DrawSmallString(8, 26, 0, va("Rubberbanding: %.02f", FIXED_TO_FLOAT(bot->botvars.rubberband) * 100.0f)); - - V_DrawSmallString(8, 32, 0, va("Item delay: %d", bot->botvars.itemdelay)); - V_DrawSmallString(8, 36, 0, va("Item confirm: %d", bot->botvars.itemconfirm)); - - V_DrawSmallString(8, 42, 0, va("Turn: %d / %d / %d", -BOTTURNCONFIRM, bot->botvars.turnconfirm, BOTTURNCONFIRM)); - V_DrawSmallString(8, 46, 0, va("Spindash: %d / %d", bot->botvars.spindashconfirm, BOTSPINDASHCONFIRM)); - V_DrawSmallString(8, 50, 0, va("Respawn: %d / %d", bot->botvars.respawnconfirm, BOTRESPAWNCONFIRM)); - - V_DrawSmallString(8, 56, 0, va("Item priority: %d", bot->botvars.roulettePriority)); - V_DrawSmallString(8, 60, 0, va("Item timeout: %d", bot->botvars.rouletteTimeout)); - - V_DrawSmallString(8, 66, 0, va("Complexity: %d", K_GetTrackComplexity())); - V_DrawSmallString(8, 70, 0, va("Bot modifier: %.2f", FixedToFloat(K_BotMapModifier()))); -} - -static void K_DrawGPRankDebugger(void) -{ - gp_rank_e grade = GRADE_E; - char gradeChar = '?'; - - if (cv_debugrank.value == 0) - { - return; - } - - if (R_GetViewNumber() != 0) // only for p1 - { - return; - } - - if (grandprixinfo.gp == false) - { - return; - } - - grade = K_CalculateGPGrade(&grandprixinfo.rank); - - V_DrawThinString(0, 0, V_SNAPTOTOP|V_SNAPTOLEFT, - va("POS: %d / %d", grandprixinfo.rank.position, RANK_NEUTRAL_POSITION)); - V_DrawThinString(0, 10, V_SNAPTOTOP|V_SNAPTOLEFT, - va("PTS: %d / %d", grandprixinfo.rank.winPoints, grandprixinfo.rank.totalPoints)); - V_DrawThinString(0, 20, V_SNAPTOTOP|V_SNAPTOLEFT, - va("LAPS: %d / %d", grandprixinfo.rank.laps, grandprixinfo.rank.totalLaps)); - V_DrawThinString(0, 30, V_SNAPTOTOP|V_SNAPTOLEFT, - va("CONTINUES: %d", grandprixinfo.rank.continuesUsed)); - V_DrawThinString(0, 40, V_SNAPTOTOP|V_SNAPTOLEFT, - va("PRISONS: %d / %d", grandprixinfo.rank.prisons, grandprixinfo.rank.totalPrisons)); - V_DrawThinString(0, 50, V_SNAPTOTOP|V_SNAPTOLEFT, - va("RINGS: %d / %d", grandprixinfo.rank.rings, grandprixinfo.rank.totalRings)); - V_DrawThinString(0, 60, V_SNAPTOTOP|V_SNAPTOLEFT, - va("EMERALD: %s", (grandprixinfo.rank.specialWon == true) ? "YES" : "NO")); - - switch (grade) - { - case GRADE_E: { gradeChar = 'E'; break; } - case GRADE_D: { gradeChar = 'D'; break; } - case GRADE_C: { gradeChar = 'C'; break; } - case GRADE_B: { gradeChar = 'B'; break; } - case GRADE_A: { gradeChar = 'A'; break; } - case GRADE_S: { gradeChar = 'S'; break; } - default: { break; } - } - - V_DrawThinString(0, 90, V_SNAPTOTOP|V_SNAPTOLEFT|V_YELLOWMAP, - va(" ** FINAL GRADE: %c", gradeChar)); -} - -typedef enum -{ - MM_IN, - MM_HOLD, - MM_OUT, -} messagemode_t; - -typedef struct -{ - std::string text; - sfxenum_t sound; -} message_t; - -struct messagestate_t -{ - std::deque messages; - std::string objective = ""; - tic_t timer = 0; - boolean persist = false; - messagemode_t mode = MM_IN; - const tic_t speedyswitch = 2*TICRATE; - const tic_t lazyswitch = 4*TICRATE; - - void add(std::string msg) - { - messages.push_back(msg); - } - - void clear() - { - messages.clear(); - switch_mode(MM_IN); - } - - void switch_mode(messagemode_t nextmode) - { - mode = nextmode; - timer = 0; - } - - void tick() - { - if (messages.size() == 0) - { - if (!objective.empty()) - restore(); - else - return; - } - - if (exitcountdown) - return; - - if (timer == 0 && mode == MM_IN) - S_StartSound(NULL, sfx_s3k47); - - timer++; - - switch (mode) - { - case MM_IN: - if (timer > messages[0].length()) - switch_mode(MM_HOLD); - break; - case MM_HOLD: - if (messages.size() > 1 && timer > speedyswitch) // Waiting message, switch to it right away! - next(); - else if (timer > lazyswitch && !persist) // If there's no pending message, we can chill for a bit. - switch_mode(MM_OUT); - break; - case MM_OUT: - if (timer > messages[0].length()) - next(); - break; - } - } - - void restore() - { - switch_mode(MM_IN); - persist = true; - messages.clear(); - messages.push_front(objective); - } - - void next() - { - switch_mode(MM_IN); - persist = false; - if (messages.size() > 0) - messages.pop_front(); - } - -}; - -static std::vector messagestates{MAXSPLITSCREENPLAYERS}; - -void K_AddMessage(const char *msg, boolean interrupt, boolean persist) -{ - for (auto &state : messagestates) - { - if (interrupt) - state.clear(); - - std::string parsedmsg = srb2::Draw::TextElement().parse(msg).string(); - - if (persist) - state.objective = parsedmsg; - else - state.add(parsedmsg); - } -} - -void K_ClearPersistentMessages() -{ - for (auto &state : messagestates) - { - state.objective = ""; - state.clear(); - } -} - -// Return value can be used for "paired" splitscreen messages, true = was displayed -void K_AddMessageForPlayer(player_t *player, const char *msg, boolean interrupt, boolean persist) -{ - if (!player) - return; - - if (player && !P_IsDisplayPlayer(player)) - return; - - if (player && K_PlayerUsesBotMovement(player)) - return; - - messagestate_t *state = &messagestates[G_PartyPosition(player - players)]; - - if (interrupt) - state->clear(); - - std::string parsedmsg = srb2::Draw::TextElement().as(player - players).parse(msg).string(); - - if (persist) - state->objective = parsedmsg; - else - state->add(parsedmsg); -} - -void K_ClearPersistentMessageForPlayer(player_t *player) -{ - if (!player) - return; - - if (player && !P_IsDisplayPlayer(player)) - return; - - messagestate_t *state = &messagestates[G_PartyPosition(player - players)]; - state->objective = ""; -} - -void K_TickMessages() -{ - for (auto &state : messagestates) - { - state.tick(); - } -} - -static void K_DrawMessageFeed(void) -{ - int i; - - if (exitcountdown) - return; - - for (i = 0; i <= r_splitscreen; i++) - { - messagestate_t state = messagestates[i]; - - if (state.messages.size() == 0) - continue; - - std::string msg = state.messages[0]; - - UINT8 sublen = state.timer; - if (state.mode == MM_IN) - sublen = state.timer; - else if (state.mode == MM_HOLD) - sublen = msg.length(); - else if (state.mode == MM_OUT) - sublen = msg.length() - state.timer; - - std::string submsg = msg.substr(0, sublen); - - using srb2::Draw; - - Draw::TextElement text(submsg); - - text.font(Draw::Font::kMenu); - - UINT8 x = BASEVIDWIDTH/2; - UINT8 y = 10; - SINT8 shift = 0; - if (r_splitscreen >= 2) - { - text.font(Draw::Font::kThin); - shift = -2; - - x = BASEVIDWIDTH/4; - y = 5; - - if (i % 2) - x += BASEVIDWIDTH/2; - - if (i >= 2) - y += BASEVIDHEIGHT / 2; - } - else if (r_splitscreen >= 1) - { - y = 5; - - if (i >= 1) - y += BASEVIDHEIGHT / 2; - } - - UINT16 sw = text.width(); - - K_DrawSticker(x - sw/2, y, sw, 0, true); - Draw(x, y+shift).align(Draw::Align::kCenter).text(text); - } -} - -void K_drawKartHUD(void) -{ - boolean islonesome = false; - UINT8 viewnum = R_GetViewNumber(); - boolean freecam = camera[viewnum].freecam; //disable some hud elements w/ freecam - - // Define the X and Y for each drawn object - // This is handled by console/menu values - K_initKartHUD(); - - // Draw that fun first person HUD! Drawn ASAP so it looks more "real". - if (!camera[viewnum].chase && !freecam) - K_drawKartFirstPerson(); - - if (mapreset) - { - // HERE COMES A NEW CHALLENGER - if (R_GetViewNumber() == 0) - K_drawChallengerScreen(); - return; - } - - // Draw full screen stuff that turns off the rest of the HUD - if (R_GetViewNumber() == 0) - { - if (g_emeraldWin) - K_drawEmeraldWin(false); - } - - // In case of font debugging break glass -#if 0 - using srb2::Draw; - - if (1) - { - CV_StealthSetValue(cv_descriptiveinput, 0); - Draw::TextElement text = Draw::TextElement().parse("Hamburger Hamburger\n\nHamburger Hamburger\n\nHamburger \xEB\xEF\xA0\xEB\xEF\xA1\xEB\xEF\xA2\xEB\xEF\xA3\xEB\xEF\xA4\xEB\xEF\xA5\xEB\xEF\xA6\xEB\xEF\xA7\xEB\xEF\xA8\xEB\xEF\xA9\xEB\xEF\xAA\xEB\xEF\xAB\xEB\xEF\xAC Hamburger"); - - UINT8 fakeoff = (stplyr - players)*40; - Draw(5, 5+fakeoff).align((srb2::Draw::Align)0).font(Draw::Font::kMenu).text(text); - Draw(40, 80+fakeoff).align((srb2::Draw::Align)0).font(Draw::Font::kThin).text(text); - } - - if (0) - { - Draw::TextElement text = Draw::TextElement().parse("\xEELEFTSPACE\xEE\n\xEESPC\xEE \xEETAB\xEE\nA \xEF\xA0 A\nB \xEF\xA1 B\nX \xEF\xA2 X\nY \xEF\xA3 Y\nLB \xEF\xA4 LB\nRB \xEF\xA5 RB\nLT \xEF\xA6 LT\nRT \xEF\xA7 RT\nST \xEF\xA8 ST\nBK \xEF\xA9 BK\nLS \xEF\xAA LS\nRS \xEF\xAB RS\n"); - - UINT8 offset = 0; - Draw(160+offset, 5).align((srb2::Draw::Align)1).font(Draw::Font::kThin).text(text); - Draw(55+offset, 5).align((srb2::Draw::Align)1).font(Draw::Font::kMenu).text(text); - } - - if (0) - { - Draw::TextElement text = Draw::TextElement().parse("\xEELEFTSPACE\xEE\n\xEESPC\xEE \xEETAB\xEE\nA \xEB\xEF\xA0 A\nB \xEB\xEF\xA1 B\nX \xEB\xEF\xA2 X\nY \xEB\xEF\xA3 Y\nLB \xEB\xEF\xA4 LB\nRB \xEB\xEF\xA5 RB\nLT \xEB\xEF\xA6 LT\nRT \xEB\xEF\xA7 RT\nST \xEB\xEF\xA8 ST\nBK \xEB\xEF\xA9 BK\nLS \xEB\xEF\xAA LS\nRS \xEB\xEF\xAB RS\n"); - - UINT8 offset = 0; - Draw(160+offset, 5).align((srb2::Draw::Align)1).font(Draw::Font::kThin).text(text); - Draw(55+offset, 5).align((srb2::Draw::Align)1).font(Draw::Font::kMenu).text(text); - } -#endif - - - if (!demo.attract) - { - // Draw the CHECK indicator before the other items, so it's overlapped by everything else - if (LUA_HudEnabled(hud_check)) // delete lua when? - if (!splitscreen && !players[displayplayers[0]].exiting && !freecam) - K_drawKartPlayerCheck(); - - // nametags - if (LUA_HudEnabled(hud_names) && R_DrawPickups()) - K_drawKartNameTags(); - - // Draw WANTED status -#if 0 - if (gametype == GT_BATTLE) - { - if (LUA_HudEnabled(hud_wanted)) - K_drawKartWanted(); - } -#endif - - if (LUA_HudEnabled(hud_minimap)) - K_drawKartMinimap(); - } - - if (demo.attract) - ; - else if (gametype == GT_TUTORIAL) - { - islonesome = true; - } - else if (!r_splitscreen) - { - // Draw the timestamp - if (LUA_HudEnabled(hud_time)) - { - bool ta = modeattacking && !demo.playback; - INT32 flags = V_HUDTRANS|V_SLIDEIN|V_SNAPTOTOP|V_SNAPTORIGHT; - - tic_t realtime = stplyr->realtime; - - if (stplyr->karthud[khud_lapanimation] - && !stplyr->exiting - && stplyr->laptime[LAP_LAST] != 0 - && stplyr->laptime[LAP_LAST] != UINT32_MAX) - { - if ((stplyr->karthud[khud_lapanimation] / 5) & 1) - { - realtime = stplyr->laptime[LAP_LAST]; - } - else - { - realtime = UINT32_MAX; - } - } - - if (modeattacking || (gametyperules & GTR_TIMELIMIT) || cv_drawtimer.value) - K_drawKartTimestamp(realtime, TIME_X, TIME_Y + (ta ? 2 : 0), flags, 0); - - if (modeattacking) - { - if (ta) - { - using srb2::Draw; - Draw::TextElement text = Draw::TextElement().parse(" Restart"); - Draw(BASEVIDWIDTH - 19, 2) - .flags(flags | V_YELLOWMAP) - .align(Draw::Align::kRight) - .text(text.string()); - } - else - { - using srb2::Draw; - Draw row = Draw(BASEVIDWIDTH - 20, TIME_Y + 18).flags(flags).align(Draw::Align::kRight); - auto insert = [&](const char *label, UINT32 tics) - { - Draw::TextElement text = - tics != UINT32_MAX ? - Draw::TextElement( - "{:02}'{:02}\"{:02}", - G_TicsToMinutes(tics, true), - G_TicsToSeconds(tics), - G_TicsToCentiseconds(tics) - ) : - Draw::TextElement("--'--\"--"); - text.font(Draw::Font::kZVote); - row.x(-text.width()).flags(V_ORANGEMAP).text(label); - row.y(1).text(text); - row = row.y(10); - }; - if (modeattacking & ATTACKING_TIME) - insert("Finish: ", hu_demotime); - if (modeattacking & ATTACKING_LAP) - insert("Best Lap: ", hu_demolap); - } - } - } - - islonesome = K_drawKartPositionFaces(); - } - else - { - islonesome = M_NotFreePlay() == false; - - if (r_splitscreen == 1) - { - if (LUA_HudEnabled(hud_time)) - { - K_drawKart2PTimestamp(); - } - - if (viewnum == r_splitscreen && gametyperules & GTR_POINTLIMIT) - { - K_drawKartPositionFaces(); - } - } - else if (viewnum == r_splitscreen) - { - if (LUA_HudEnabled(hud_time)) - { - K_drawKart4PTimestamp(); - } - - if (gametyperules & GTR_POINTLIMIT) - { - K_drawKartPositionFaces(); - } - } - } - - if (!stplyr->spectator && !freecam) // Bottom of the screen elements, don't need in spectate mode - { - if (demo.attract) - { - if (demo.attract == DEMO_ATTRACT_TITLE) // Draw logo on title screen demos - { - INT32 x = BASEVIDWIDTH - 8, y = BASEVIDHEIGHT-8, snapflags = V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_SLIDEIN; - patch_t *pat = static_cast(W_CachePatchName((M_UseAlternateTitleScreen() ? "MTSJUMPR1" : "MTSBUMPR1"), PU_CACHE)); - const UINT8 *colormap = nullptr; - - if (INT32 fade = F_AttractDemoExitFade()) - { - // TODO: Twodee cannot handle - // V_DrawCustomFadeScreen. - // However, since the screen fade just - // uses a colormap, the same colormap can - // be applied on a per-patch basis. - // I'm only bothering to apply this - // colormap to the attract mode sticker, - // since it's the lone HUD element. - if (lighttable_t *clm = V_LoadCustomFadeMap("FADEMAP0")) - { - // This must be statically allocated for Twodee - static UINT8 *colormap_storage; - const UINT8 *fadetable = V_OffsetIntoFadeMap(clm, fade); - - if (!colormap_storage) - Z_MallocAlign(256, PU_STATIC, &colormap_storage, 8); - - memcpy(colormap_storage, fadetable, 256); - colormap = colormap_storage; - - Z_Free(clm); - } - } - - if (r_splitscreen == 3) - { - x = BASEVIDWIDTH/2; - y = BASEVIDHEIGHT/2; - snapflags = 0; - } - - V_DrawMappedPatch(x-(SHORT(pat->width)), y-(SHORT(pat->height)), snapflags, pat, colormap); - } - } - else - { - boolean gametypeinfoshown = false; - - if (K_PlayerTallyActive(stplyr) == true) - { - K_DrawPlayerTally(); - } - - if (LUA_HudEnabled(hud_position)) - { - if (bossinfo.valid) - { - K_drawBossHealthBar(); - } - else if (freecam) - ; - else if ((gametyperules & GTR_POWERSTONES) && !K_PlayerTallyActive(stplyr)) - { - if (!battleprisons) - K_drawKartEmeralds(); - } - else if (!islonesome && !K_Cooperative()) - { - K_DrawKartPositionNum(stplyr->position); - } - } - - if (G_GametypeHasTeams() == true) - { - K_drawKartTeamScores(); - } - - if (LUA_HudEnabled(hud_gametypeinfo)) - { - if (gametyperules & GTR_CIRCUIT) - { - K_drawKartLaps(); - gametypeinfoshown = true; - } - else if (gametyperules & GTR_BUMPERS) - { - K_drawKartBumpersOrKarma(); - gametypeinfoshown = true; - } - } - - // Draw the speedometer and/or accessibility icons - if (cv_kartspeedometer.value && !r_splitscreen && (LUA_HudEnabled(hud_speedometer))) - { - K_drawKartSpeedometer(gametypeinfoshown); - } - else - { - K_drawKartAccessibilityIcons(gametypeinfoshown, 0); - } - - if (gametyperules & GTR_SPHERES) - { - K_drawBlueSphereMeter(gametypeinfoshown); - } - else - { - K_drawRingCounter(gametypeinfoshown); - } - - // Draw the item window - if (LUA_HudEnabled(hud_item) && !freecam) - { - if (stplyr->itemRoulette.ringbox && stplyr->itemamount == 0 && stplyr->itemtype == 0) - { - K_drawKartSlotMachine(); - } - else - { - K_drawKartItem(); - } - } - } - } - - // Draw the countdowns after everything else. - if (stplyr->lives <= 0 && stplyr->playerstate == PST_DEAD) - { - ; - } - else if (stplyr->karthud[khud_fault] != 0 && stplyr->karthud[khud_finish] == 0) - { - K_drawKartFinish(false); - } - else if (starttime != introtime - && leveltime >= introtime - && leveltime < starttime+TICRATE) - { - K_drawKartStartCountdown(); - } - else if (racecountdown && (!r_splitscreen || !stplyr->exiting)) - { - char *countstr = va("%d", racecountdown/TICRATE); - - if (r_splitscreen > 1) - V_DrawCenteredString(BASEVIDWIDTH/4, LAPS_Y+1, V_SPLITSCREEN, countstr); - else - { - INT32 karlen = strlen(countstr)*6; // half of 12 - V_DrawTimerString((BASEVIDWIDTH/2)-karlen, LAPS_Y+3, V_SPLITSCREEN, countstr); - } - } - - // Race overlays - if (!freecam) - { - if (stplyr->exiting) - K_drawKartFinish(true); - else if (!(gametyperules & GTR_CIRCUIT)) - ; - else if (stplyr->karthud[khud_lapanimation] && !r_splitscreen) - K_drawLapStartAnim(); - } - - // trick panel cool trick - if (stplyr->karthud[khud_trickcool]) - K_drawTrickCool(); - - if ((freecam || stplyr->spectator) && LUA_HudEnabled(hud_textspectator)) - { - K_drawSpectatorHUD(false); - } - - if (R_GetViewNumber() == 0 && g_emeraldWin) - K_drawEmeraldWin(true); - - if (modeattacking || freecam) // everything after here is MP and debug only - { - K_drawInput(); - goto debug; - } - - if ((gametyperules & GTR_KARMA) && !r_splitscreen && (stplyr->karthud[khud_yougotem] % 2)) // * YOU GOT EM * - V_DrawScaledPatch(BASEVIDWIDTH/2 - (SHORT(kp_yougotem->width)/2), 32, V_HUDTRANS, kp_yougotem); - - // Draw FREE PLAY. - K_drawKartFreePlay(); - - if ((netgame || cv_mindelay.value) && r_splitscreen && Playing()) - { - K_drawMiniPing(); - } - - K_drawKartPowerUps(); - - if (K_DirectorIsAvailable(viewnum) == true && LUA_HudEnabled(hud_textspectator)) - { - K_drawSpectatorHUD(true); - } - else - { - K_drawInput(); - } - - if (cv_kartdebugdistribution.value) - K_drawDistributionDebugger(); - - if (cv_kartdebugnodes.value) - { - UINT8 p; - for (p = 0; p < MAXPLAYERS; p++) - V_DrawString(8, 64+(8*p), V_YELLOWMAP, va("%d - %d (%dl)", p, playernode[p], players[p].cmd.latency)); - } - - if (cv_kartdebugcolorize.value && stplyr->mo && stplyr->mo->skin) - { - INT32 x = 0, y = 0; - UINT16 c; - - for (c = 0; c < numskincolors; c++) - { - if (skincolors[c].accessible) - { - UINT8 *cm = R_GetTranslationColormap(TC_RAINBOW, static_cast(c), GTC_CACHE); - V_DrawFixedPatch(x<>1, 0, faceprefix[stplyr->skin][FACE_WANTED], cm); - - x += 16; - if (x > BASEVIDWIDTH-16) - { - x = 0; - y += 16; - } - } - } - } - -debug: - K_DrawWaypointDebugger(); - K_DrawBotDebugger(); - K_DrawDirectorDebugger(); - K_DrawGPRankDebugger(); - K_DrawMessageFeed(); -} - -void K_DrawSticker(INT32 x, INT32 y, INT32 width, INT32 flags, boolean isSmall) -{ - patch_t *stickerEnd; - INT32 height; - - if (isSmall == true) - { - stickerEnd = static_cast(W_CachePatchName("K_STIKE2", PU_CACHE)); - height = 6; - } - else - { - stickerEnd = static_cast(W_CachePatchName("K_STIKEN", PU_CACHE)); - height = 11; - } - - V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, flags, stickerEnd, NULL); - V_DrawFill(x, y, width, height, 24|flags); - V_DrawFixedPatch((x + width)*FRACUNIT, y*FRACUNIT, FRACUNIT, flags|V_FLIP, stickerEnd, NULL); -} - -void K_DrawMarginSticker(INT32 x, INT32 y, INT32 width, INT32 flags, boolean isSmall, boolean leftedge) -{ - patch_t *stickerEnd; - INT32 height; - - if (isSmall == true) - { - stickerEnd = static_cast(W_CachePatchName("K_STIKE2", PU_CACHE)); - height = 6; - } - else - { - stickerEnd = static_cast(W_CachePatchName("K_STIKEN", PU_CACHE)); - height = 11; - } - - if (leftedge) - V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, flags, stickerEnd, NULL); - V_DrawFill(x, y, width, height, 24|flags); - if (!leftedge) - V_DrawFixedPatch((x + width)*FRACUNIT, y*FRACUNIT, FRACUNIT, flags|V_FLIP, stickerEnd, NULL); -} - -// common fonts: 0 = thin, 8 = menu. sorry we have to launder a C++ enum in here -INT32 K_DrawGameControl(UINT16 x, UINT16 y, UINT8 player, const char *str, UINT8 alignment, UINT8 font, UINT32 flags) -{ - using srb2::Draw; - - Draw::TextElement text = Draw::TextElement().as(player).parse(str).font((Draw::Font)font); - - INT32 width = text.width(); - - Draw(x, y).align((srb2::Draw::Align)alignment).flags(flags).text(text); - - return width; -} \ No newline at end of file +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by Kart Krew. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file k_hud.c +/// \brief HUD drawing functions exclusive to Kart + +#include +#include +#include +#include + +#include + +#include "k_hud.h" +#include "k_kart.h" +#include "k_battle.h" +#include "k_grandprix.h" +#include "k_specialstage.h" +#include "k_objects.h" +#include "k_boss.h" +#include "k_color.h" +#include "k_director.h" +#include "screen.h" +#include "doomtype.h" +#include "doomdef.h" +#include "hu_stuff.h" +#include "d_netcmd.h" +#include "v_video.h" +#include "r_draw.h" +#include "st_stuff.h" +#include "lua_hud.h" +#include "doomstat.h" +#include "d_clisrv.h" +#include "g_game.h" +#include "p_local.h" +#include "z_zone.h" +#include "m_cond.h" +#include "r_main.h" +#include "s_sound.h" +#include "r_things.h" +#include "r_fps.h" +#include "m_random.h" +#include "k_roulette.h" +#include "k_bot.h" +#include "k_rank.h" +#include "g_party.h" +#include "k_hitlag.h" +#include "g_input.h" +#include "k_dialogue.h" +#include "f_finale.h" +#include "m_easing.h" +#include "v_draw.hpp" + +//{ Patch Definitions +static patch_t *kp_nodraw; + +static patch_t *kp_timesticker; +static patch_t *kp_timestickerwide; +static patch_t *kp_lapsticker; +static patch_t *kp_lapstickerwide; +static patch_t *kp_lapstickernarrow; +static patch_t *kp_exp[2]; +static patch_t *kp_splitlapflag; +static patch_t *kp_bumpersticker; +static patch_t *kp_bumperstickerwide; +static patch_t *kp_capsulesticker; +static patch_t *kp_capsulestickerwide; +static patch_t *kp_karmasticker; +static patch_t *kp_spheresticker; +static patch_t *kp_splitspheresticker; +static patch_t *kp_splitkarmabomb; +static patch_t *kp_timeoutsticker; + +static patch_t *kp_prestartbulb[15]; +static patch_t *kp_prestartletters[7]; + +static patch_t *kp_prestartbulb_split[15]; +static patch_t *kp_prestartletters_split[7]; + +static patch_t *kp_startcountdown[20]; +static patch_t *kp_racefault[6]; +static patch_t *kp_racefinish[6]; + +static patch_t *kp_positionnum[10][2][2]; // number, overlay or underlay, splitscreen + +patch_t *kp_facenum[MAXPLAYERS+1]; +static patch_t *kp_facehighlight[8]; + +static patch_t *kp_nocontestminimap; +patch_t *kp_unknownminimap; +static patch_t *kp_spbminimap; +static patch_t *kp_wouldyoustillcatchmeifiwereaworm; +static patch_t *kp_catcherminimap; +static patch_t *kp_emeraldminimap[2]; +static patch_t *kp_capsuleminimap[3]; +static patch_t *kp_battleufominimap; +static patch_t *kp_superflickyminimap; + +static patch_t *kp_ringsticker[2]; +static patch_t *kp_ringstickersplit[4]; +static patch_t *kp_ring[6]; +static patch_t *kp_smallring[6]; +static patch_t *kp_ringdebtminus; +static patch_t *kp_ringdebtminussmall; +static patch_t *kp_ringspblock[16]; +static patch_t *kp_ringspblocksmall[16]; +static patch_t *kp_amps[7][12]; +static patch_t *kp_amps_underlay[12]; +static patch_t *kp_overdrive[32]; + +static patch_t *kp_speedometersticker; +static patch_t *kp_speedometerlabel[4]; + +static patch_t *kp_rankbumper; +static patch_t *kp_bigbumper; +static patch_t *kp_tinybumper[2]; +static patch_t *kp_ranknobumpers; +static patch_t *kp_rankcapsule; +static patch_t *kp_rankemerald; +static patch_t *kp_rankemeraldflash; +static patch_t *kp_rankemeraldback; +static patch_t *kp_pts[2]; + +static patch_t *kp_goal[2][2]; // [skull][4p] +static patch_t *kp_goalrod[2]; // [4p] +static patch_t *kp_goaltext1p; + +static patch_t *kp_battlewin; +static patch_t *kp_battlecool; +static patch_t *kp_battlelose; +static patch_t *kp_battlewait; +static patch_t *kp_battleinfo; +static patch_t *kp_wanted; +static patch_t *kp_wantedsplit; +static patch_t *kp_wantedreticle; +static patch_t *kp_minimapdot; + +static patch_t *kp_itembg[6]; +static patch_t *kp_ringbg[4]; +static patch_t *kp_itemtimer[2]; +static patch_t *kp_itemmulsticker[2]; +static patch_t *kp_itemx; + +static patch_t *kp_sadface[3]; +static patch_t *kp_sneaker[3]; +static patch_t *kp_rocketsneaker[3]; +static patch_t *kp_invincibility[19]; +static patch_t *kp_banana[3]; +static patch_t *kp_eggman[3]; +static patch_t *kp_orbinaut[6]; +static patch_t *kp_jawz[3]; +static patch_t *kp_mine[3]; +static patch_t *kp_landmine[3]; +static patch_t *kp_ballhog[3]; +static patch_t *kp_selfpropelledbomb[3]; +static patch_t *kp_grow[3]; +static patch_t *kp_shrink[3]; +static patch_t *kp_lightningshield[3]; +static patch_t *kp_bubbleshield[3]; +static patch_t *kp_flameshield[3]; +static patch_t *kp_hyudoro[3]; +static patch_t *kp_pogospring[3]; +static patch_t *kp_superring[3]; +static patch_t *kp_kitchensink[3]; +static patch_t *kp_droptarget[3]; +static patch_t *kp_gardentop[3]; +static patch_t *kp_gachabom[3]; +static patch_t *kp_bar[2]; +static patch_t *kp_doublebar[2]; +static patch_t *kp_triplebar[2]; +static patch_t *kp_slotring[2]; +static patch_t *kp_seven[2]; +static patch_t *kp_jackpot[2]; + +static patch_t *kp_check[6]; + +static patch_t *kp_rival[2]; +static patch_t *kp_localtag[4][2]; + +static patch_t *kp_talk; +static patch_t *kp_typdot; + +patch_t *kp_eggnum[6]; + +static patch_t *kp_flameshieldmeter[FLAMESHIELD_MAX][2]; +static patch_t *kp_flameshieldmeter_bg[FLAMESHIELD_MAX][2]; + +static patch_t *kp_fpview[3]; +static patch_t *kp_inputwheel[5]; + +static patch_t *kp_challenger[25]; + +static patch_t *kp_lapanim_lap[7]; +static patch_t *kp_lapanim_final[11]; +static patch_t *kp_lapanim_number[10][3]; +static patch_t *kp_lapanim_emblem[2]; +static patch_t *kp_lapanim_hand[3]; + +static patch_t *kp_yougotem; +static patch_t *kp_itemminimap; + +static patch_t *kp_alagles[10]; +static patch_t *kp_blagles[6]; + +static patch_t *kp_cpu[2]; + +static patch_t *kp_nametagstem; + +static patch_t *kp_bossbar[8]; +static patch_t *kp_bossret[4]; + +static patch_t *kp_trickcool[2]; + +static patch_t *kp_voice_localactive[16]; +static patch_t *kp_voice_localactiveoverlay[16]; +static patch_t *kp_voice_localopen; +static patch_t *kp_voice_localmuted; +static patch_t *kp_voice_localdeafened; +static patch_t *kp_voice_remoteactive; +static patch_t *kp_voice_remoteopen; +static patch_t *kp_voice_remotemuted; +static patch_t *kp_voice_remotedeafened; +static patch_t *kp_voice_tagactive[3]; + +patch_t *kp_autoroulette; +patch_t *kp_autoring; + +patch_t *kp_capsuletarget_arrow[2][2]; +patch_t *kp_capsuletarget_icon[2]; +patch_t *kp_capsuletarget_far[2][2]; +patch_t *kp_capsuletarget_far_text[2]; +patch_t *kp_capsuletarget_near[2][8]; + +patch_t *kp_superflickytarget[2][4]; + +patch_t *kp_spraycantarget_far[2][6]; +patch_t *kp_spraycantarget_near[2][6]; + +patch_t *kp_button_a[2][2]; +patch_t *kp_button_b[2][2]; +patch_t *kp_button_c[2][2]; +patch_t *kp_button_x[2][2]; +patch_t *kp_button_y[2][2]; +patch_t *kp_button_z[2][2]; +patch_t *kp_button_start[2][2]; +patch_t *kp_button_l[2][2]; +patch_t *kp_button_r[2][2]; +patch_t *kp_button_up[2][2]; +patch_t *kp_button_down[2][2]; +patch_t *kp_button_right[2][2]; +patch_t *kp_button_left[2][2]; +patch_t *kp_button_lua1[2][2]; +patch_t *kp_button_lua2[2][2]; +patch_t *kp_button_lua3[2][2]; + +patch_t *gen_button_a[2][2]; +patch_t *gen_button_b[2][2]; +patch_t *gen_button_x[2][2]; +patch_t *gen_button_y[2][2]; +patch_t *gen_button_lb[2][2]; +patch_t *gen_button_rb[2][2]; +patch_t *gen_button_lt[2][2]; +patch_t *gen_button_rt[2][2]; +patch_t *gen_button_start[2][2]; +patch_t *gen_button_back[2][2]; +patch_t *gen_button_ls[2][2]; +patch_t *gen_button_rs[2][2]; +patch_t *gen_button_dpad[2][2]; + +patch_t *gen_button_keyleft[2]; +patch_t *gen_button_keyright[2]; +patch_t *gen_button_keycenter[2]; + +static void K_LoadButtonGraphics(patch_t *kp[2][2], const char* code) +{ + HU_UpdatePatch(&kp[0][0], "TLB_%s", code); + HU_UpdatePatch(&kp[0][1], "TLB_%sB", code); + HU_UpdatePatch(&kp[1][0], "TLBS%s", code); + HU_UpdatePatch(&kp[1][1], "TLBS%sB", code); +} + +static void K_LoadGenericButtonGraphics(patch_t *kp[2][2], const char* code) +{ + HU_UpdatePatch(&kp[0][0], "TLG_%s", code); + HU_UpdatePatch(&kp[0][1], "TLG_%sB", code); + HU_UpdatePatch(&kp[1][0], "TLGS%s", code); + HU_UpdatePatch(&kp[1][1], "TLGS%sB", code); +} + +void K_LoadKartHUDGraphics(void) +{ + INT32 i, j, k; + char buffer[9]; + + // Null Stuff + HU_UpdatePatch(&kp_nodraw, "K_TRNULL"); + + // Stickers + HU_UpdatePatch(&kp_timesticker, "K_STTIME"); + HU_UpdatePatch(&kp_timestickerwide, "K_STTIMW"); + HU_UpdatePatch(&kp_lapsticker, "K_STLAPS"); + HU_UpdatePatch(&kp_lapstickerwide, "K_STLAPW"); + HU_UpdatePatch(&kp_lapstickernarrow, "K_STLAPN"); + HU_UpdatePatch(&kp_exp[0], "K_STEXP"); + HU_UpdatePatch(&kp_exp[1], "K_SPTEXP"); + HU_UpdatePatch(&kp_splitlapflag, "K_SPTLAP"); + HU_UpdatePatch(&kp_bumpersticker, "K_STBALN"); + HU_UpdatePatch(&kp_bumperstickerwide, "K_STBALW"); + HU_UpdatePatch(&kp_capsulesticker, "K_STCAPN"); + HU_UpdatePatch(&kp_capsulestickerwide, "K_STCAPW"); + HU_UpdatePatch(&kp_karmasticker, "K_STKARM"); + HU_UpdatePatch(&kp_spheresticker, "K_STBSMT"); + HU_UpdatePatch(&kp_splitspheresticker, "K_SPBSMT"); + HU_UpdatePatch(&kp_splitkarmabomb, "K_SPTKRM"); + HU_UpdatePatch(&kp_timeoutsticker, "K_STTOUT"); + + // Pre-start countdown bulbs + sprintf(buffer, "K_BULBxx"); + for (i = 0; i < 15; i++) + { + buffer[6] = '0'+((i+1)/10); + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_prestartbulb[i], "%s", buffer); + } + + sprintf(buffer, "K_SBLBxx"); + for (i = 0; i < 15; i++) + { + buffer[6] = '0'+((i+1)/10); + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_prestartbulb_split[i], "%s", buffer); + } + + // Pre-start position letters + HU_UpdatePatch(&kp_prestartletters[0], "K_PL_P"); + HU_UpdatePatch(&kp_prestartletters[1], "K_PL_O"); + HU_UpdatePatch(&kp_prestartletters[2], "K_PL_S"); + HU_UpdatePatch(&kp_prestartletters[3], "K_PL_I"); + HU_UpdatePatch(&kp_prestartletters[4], "K_PL_T"); + HU_UpdatePatch(&kp_prestartletters[5], "K_PL_N"); + HU_UpdatePatch(&kp_prestartletters[6], "K_PL_EX"); + + HU_UpdatePatch(&kp_prestartletters_split[0], "K_SPL_P"); + HU_UpdatePatch(&kp_prestartletters_split[1], "K_SPL_O"); + HU_UpdatePatch(&kp_prestartletters_split[2], "K_SPL_S"); + HU_UpdatePatch(&kp_prestartletters_split[3], "K_SPL_I"); + HU_UpdatePatch(&kp_prestartletters_split[4], "K_SPL_T"); + HU_UpdatePatch(&kp_prestartletters_split[5], "K_SPL_N"); + HU_UpdatePatch(&kp_prestartletters_split[6], "K_SPL_EX"); + + // Starting countdown + HU_UpdatePatch(&kp_startcountdown[0], "K_CNT3A"); + HU_UpdatePatch(&kp_startcountdown[1], "K_CNT2A"); + HU_UpdatePatch(&kp_startcountdown[2], "K_CNT1A"); + HU_UpdatePatch(&kp_startcountdown[3], "K_CNTGOA"); + HU_UpdatePatch(&kp_startcountdown[4], "K_DUEL1"); + HU_UpdatePatch(&kp_startcountdown[5], "K_CNT3B"); + HU_UpdatePatch(&kp_startcountdown[6], "K_CNT2B"); + HU_UpdatePatch(&kp_startcountdown[7], "K_CNT1B"); + HU_UpdatePatch(&kp_startcountdown[8], "K_CNTGOB"); + HU_UpdatePatch(&kp_startcountdown[9], "K_DUEL2"); + // Splitscreen + HU_UpdatePatch(&kp_startcountdown[10], "K_SMC3A"); + HU_UpdatePatch(&kp_startcountdown[11], "K_SMC2A"); + HU_UpdatePatch(&kp_startcountdown[12], "K_SMC1A"); + HU_UpdatePatch(&kp_startcountdown[13], "K_SMCGOA"); + HU_UpdatePatch(&kp_startcountdown[14], "K_SDUEL1"); + HU_UpdatePatch(&kp_startcountdown[15], "K_SMC3B"); + HU_UpdatePatch(&kp_startcountdown[16], "K_SMC2B"); + HU_UpdatePatch(&kp_startcountdown[17], "K_SMC1B"); + HU_UpdatePatch(&kp_startcountdown[18], "K_SMCGOB"); + HU_UpdatePatch(&kp_startcountdown[19], "K_SDUEL2"); + + // Fault + HU_UpdatePatch(&kp_racefault[0], "K_FAULTA"); + HU_UpdatePatch(&kp_racefault[1], "K_FAULTB"); + // Splitscreen + HU_UpdatePatch(&kp_racefault[2], "K_SMFLTA"); + HU_UpdatePatch(&kp_racefault[3], "K_SMFLTB"); + // 2P splitscreen + HU_UpdatePatch(&kp_racefault[4], "K_2PFLTA"); + HU_UpdatePatch(&kp_racefault[5], "K_2PFLTB"); + + // Finish + HU_UpdatePatch(&kp_racefinish[0], "K_FINA"); + HU_UpdatePatch(&kp_racefinish[1], "K_FINB"); + // Splitscreen + HU_UpdatePatch(&kp_racefinish[2], "K_SMFINA"); + HU_UpdatePatch(&kp_racefinish[3], "K_SMFINB"); + // 2P splitscreen + HU_UpdatePatch(&kp_racefinish[4], "K_2PFINA"); + HU_UpdatePatch(&kp_racefinish[5], "K_2PFINB"); + + // Position numbers + sprintf(buffer, "KRNKxyz"); + for (i = 0; i < 10; i++) + { + buffer[6] = '0'+i; + + for (j = 0; j < 2; j++) + { + buffer[5] = 'A'+j; + + for (k = 0; k < 2; k++) + { + if (k > 0) + { + buffer[4] = 'S'; + } + else + { + buffer[4] = 'B'; + } + + HU_UpdatePatch(&kp_positionnum[i][j][k], "%s", buffer); + } + } + } + + sprintf(buffer, "OPPRNKxx"); + for (i = 0; i <= MAXPLAYERS; i++) + { + buffer[6] = '0'+(i/10); + buffer[7] = '0'+(i%10); + HU_UpdatePatch(&kp_facenum[i], "%s", buffer); + } + + sprintf(buffer, "K_CHILIx"); + for (i = 0; i < 8; i++) + { + buffer[7] = '0'+(i+1); + HU_UpdatePatch(&kp_facehighlight[i], "%s", buffer); + } + + // Special minimap icons + HU_UpdatePatch(&kp_nocontestminimap, "MINIDEAD"); + HU_UpdatePatch(&kp_unknownminimap, "HUHMAP"); + HU_UpdatePatch(&kp_spbminimap, "SPBMMAP"); + + HU_UpdatePatch(&kp_wouldyoustillcatchmeifiwereaworm, "MINIPROG"); + HU_UpdatePatch(&kp_catcherminimap, "UFOMAP"); + HU_UpdatePatch(&kp_emeraldminimap[0], "EMEMAP"); + HU_UpdatePatch(&kp_emeraldminimap[1], "SUPMAP"); + + HU_UpdatePatch(&kp_capsuleminimap[0], "MINICAP1"); + HU_UpdatePatch(&kp_capsuleminimap[1], "MINICAP2"); + HU_UpdatePatch(&kp_capsuleminimap[2], "MINICAP3"); + + HU_UpdatePatch(&kp_battleufominimap, "MINIBUFO"); + HU_UpdatePatch(&kp_superflickyminimap, "FLKMAPA"); + + // Rings & Lives + HU_UpdatePatch(&kp_ringsticker[0], "RNGBACKA"); + HU_UpdatePatch(&kp_ringsticker[1], "RNGBACKB"); + + sprintf(buffer, "K_RINGx"); + for (i = 0; i < 6; i++) + { + buffer[6] = '0'+(i+1); + HU_UpdatePatch(&kp_ring[i], "%s", buffer); + } + + // Amps + { + // Levels 1-6 + sprintf(buffer, "b_xAMPxx"); + for (i = 0; i < 6; i++) + { + buffer[2] = '0'+i+1; + for (j = 0; j < 12; j++) + { + buffer[6] = '0'+((j) / 10); + buffer[7] = '0'+((j) % 10); + HU_UpdatePatch(&kp_amps[i][j], "%s", buffer); + } + } + + // Level 7 + buffer[2] = '7'; + buffer[1] = 'A'; + for (j = 0; j < 12; j++) + { + buffer[6] = '0'+((j) / 10); + buffer[7] = '0'+((j) % 10); + HU_UpdatePatch(&kp_amps[i][j], "%s", buffer); + } + buffer[1] = 'B'; + for (j = 0; j < 12; j++) + { + buffer[6] = '0'+((j) / 10); + buffer[7] = '0'+((j) % 10); + HU_UpdatePatch(&kp_amps_underlay[j], "%s", buffer); + } + } + + sprintf(buffer, "b_OVRDxx"); + for (i = 0; i < 32; i++) + { + buffer[6] = '0'+((i) / 10); + buffer[7] = '0'+((i) % 10); + HU_UpdatePatch(&kp_overdrive[i], "%s", buffer); + } + + HU_UpdatePatch(&kp_ringdebtminus, "RDEBTMIN"); + + sprintf(buffer, "SPBRNGxx"); + for (i = 0; i < 16; i++) + { + buffer[6] = '0'+((i+1) / 10); + buffer[7] = '0'+((i+1) % 10); + HU_UpdatePatch(&kp_ringspblock[i], "%s", buffer); + } + + HU_UpdatePatch(&kp_ringstickersplit[0], "SMRNGBGA"); + HU_UpdatePatch(&kp_ringstickersplit[1], "SMRNGBGB"); + + sprintf(buffer, "K_SRINGx"); + for (i = 0; i < 6; i++) + { + buffer[7] = '0'+(i+1); + HU_UpdatePatch(&kp_smallring[i], "%s", buffer); + } + + HU_UpdatePatch(&kp_ringdebtminussmall, "SRDEBTMN"); + + sprintf(buffer, "SPBRGSxx"); + for (i = 0; i < 16; i++) + { + buffer[6] = '0'+((i+1) / 10); + buffer[7] = '0'+((i+1) % 10); + HU_UpdatePatch(&kp_ringspblocksmall[i], "%s", buffer); + } + + // Speedometer + HU_UpdatePatch(&kp_speedometersticker, "K_SPDMBG"); + + sprintf(buffer, "K_SPDMLx"); + for (i = 0; i < 4; i++) + { + buffer[7] = '0'+(i+1); + HU_UpdatePatch(&kp_speedometerlabel[i], "%s", buffer); + } + + // Extra ranking icons + HU_UpdatePatch(&kp_rankbumper, "K_BLNICO"); + HU_UpdatePatch(&kp_bigbumper, "K_BLNREG"); + HU_UpdatePatch(&kp_tinybumper[0], "K_BLNA"); + HU_UpdatePatch(&kp_tinybumper[1], "K_BLNB"); + HU_UpdatePatch(&kp_ranknobumpers, "K_NOBLNS"); + HU_UpdatePatch(&kp_rankcapsule, "K_CAPICO"); + HU_UpdatePatch(&kp_rankemerald, "K_EMERC"); + HU_UpdatePatch(&kp_rankemeraldflash, "K_EMERW"); + HU_UpdatePatch(&kp_rankemeraldback, "K_EMERBK"); + HU_UpdatePatch(&kp_pts[0], "K_POINTS"); + HU_UpdatePatch(&kp_pts[1], "K_POINT4"); + + // Battle goal + HU_UpdatePatch(&kp_goal[0][0], "K_ST1GLA"); + HU_UpdatePatch(&kp_goal[1][0], "K_ST1GLB"); + HU_UpdatePatch(&kp_goal[0][1], "K_ST4GLA"); + HU_UpdatePatch(&kp_goal[1][1], "K_ST4GLB"); + HU_UpdatePatch(&kp_goalrod[0], "K_ST1GLD"); + HU_UpdatePatch(&kp_goalrod[1], "K_ST4GLD"); + HU_UpdatePatch(&kp_goaltext1p, "K_ST1GLC"); + + // Battle graphics + HU_UpdatePatch(&kp_battlewin, "K_BWIN"); + HU_UpdatePatch(&kp_battlecool, "K_BCOOL"); + HU_UpdatePatch(&kp_battlelose, "K_BLOSE"); + HU_UpdatePatch(&kp_battlewait, "K_BWAIT"); + HU_UpdatePatch(&kp_battleinfo, "K_BINFO"); + HU_UpdatePatch(&kp_wanted, "K_WANTED"); + HU_UpdatePatch(&kp_wantedsplit, "4PWANTED"); + HU_UpdatePatch(&kp_wantedreticle, "MMAPWANT"); + HU_UpdatePatch(&kp_minimapdot, "MMAPDOT"); + + // Kart Item Windows + HU_UpdatePatch(&kp_itembg[0], "K_ITBG"); + HU_UpdatePatch(&kp_itembg[1], "K_ITBGD"); + HU_UpdatePatch(&kp_itemtimer[0], "K_ITIMER"); + HU_UpdatePatch(&kp_itemmulsticker[0], "K_ITMUL"); + HU_UpdatePatch(&kp_itemx, "K_ITX"); + + HU_UpdatePatch(&kp_ringbg[0], "K_RBBG"); + HU_UpdatePatch(&kp_ringbg[1], "K_SBBG"); + + HU_UpdatePatch(&kp_sadface[0], "K_ITSAD"); + HU_UpdatePatch(&kp_sneaker[0], "K_ITSHOE"); + HU_UpdatePatch(&kp_rocketsneaker[0], "K_ITRSHE"); + + sprintf(buffer, "K_ITINVx"); + for (i = 0; i < 7; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_invincibility[i], "%s", buffer); + } + HU_UpdatePatch(&kp_banana[0], "K_ITBANA"); + HU_UpdatePatch(&kp_eggman[0], "K_ITEGGM"); + sprintf(buffer, "K_ITORBx"); + for (i = 0; i < 4; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_orbinaut[i], "%s", buffer); + } + HU_UpdatePatch(&kp_jawz[0], "K_ITJAWZ"); + HU_UpdatePatch(&kp_mine[0], "K_ITMINE"); + HU_UpdatePatch(&kp_landmine[0], "K_ITLNDM"); + HU_UpdatePatch(&kp_ballhog[0], "K_ITBHOG"); + HU_UpdatePatch(&kp_selfpropelledbomb[0], "K_ITSPB"); + HU_UpdatePatch(&kp_grow[0], "K_ITGROW"); + HU_UpdatePatch(&kp_shrink[0], "K_ITSHRK"); + HU_UpdatePatch(&kp_lightningshield[0], "K_ITTHNS"); + HU_UpdatePatch(&kp_bubbleshield[0], "K_ITBUBS"); + HU_UpdatePatch(&kp_flameshield[0], "K_ITFLMS"); + HU_UpdatePatch(&kp_hyudoro[0], "K_ITHYUD"); + HU_UpdatePatch(&kp_pogospring[0], "K_ITPOGO"); + HU_UpdatePatch(&kp_superring[0], "K_ITRING"); + HU_UpdatePatch(&kp_kitchensink[0], "K_ITSINK"); + HU_UpdatePatch(&kp_droptarget[0], "K_ITDTRG"); + HU_UpdatePatch(&kp_gardentop[0], "K_ITGTOP"); + HU_UpdatePatch(&kp_gachabom[0], "K_ITGBOM"); + HU_UpdatePatch(&kp_bar[0], "K_RBBAR"); + HU_UpdatePatch(&kp_doublebar[0], "K_RBBAR2"); + HU_UpdatePatch(&kp_triplebar[0], "K_RBBAR3"); + HU_UpdatePatch(&kp_slotring[0], "K_RBRING"); + HU_UpdatePatch(&kp_seven[0], "K_RBSEV"); + HU_UpdatePatch(&kp_jackpot[0], "K_RBJACK"); + + sprintf(buffer, "FSMFGxxx"); + for (i = 0; i < FLAMESHIELD_MAX; i++) + { + buffer[5] = '0'+((i+1)/100); + buffer[6] = '0'+(((i+1)/10)%10); + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_flameshieldmeter[i][0], "%s", buffer); + } + + sprintf(buffer, "FSMBGxxx"); + for (i = 0; i < FLAMESHIELD_MAX; i++) + { + buffer[5] = '0'+((i+1)/100); + buffer[6] = '0'+(((i+1)/10)%10); + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_flameshieldmeter_bg[i][0], "%s", buffer); + } + + // Splitscreen + HU_UpdatePatch(&kp_itembg[2], "K_ISBG"); + HU_UpdatePatch(&kp_itembg[3], "K_ISBGD"); + HU_UpdatePatch(&kp_itemtimer[1], "K_ISIMER"); + HU_UpdatePatch(&kp_itemmulsticker[1], "K_ISMUL"); + + HU_UpdatePatch(&kp_sadface[1], "K_ISSAD"); + HU_UpdatePatch(&kp_sneaker[1], "K_ISSHOE"); + HU_UpdatePatch(&kp_rocketsneaker[1], "K_ISRSHE"); + sprintf(buffer, "K_ISINVx"); + for (i = 0; i < 6; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_invincibility[i+7], "%s", buffer); + } + HU_UpdatePatch(&kp_banana[1], "K_ISBANA"); + HU_UpdatePatch(&kp_eggman[1], "K_ISEGGM"); + HU_UpdatePatch(&kp_orbinaut[4], "K_ISORBN"); + HU_UpdatePatch(&kp_jawz[1], "K_ISJAWZ"); + HU_UpdatePatch(&kp_mine[1], "K_ISMINE"); + HU_UpdatePatch(&kp_landmine[1], "K_ISLNDM"); + HU_UpdatePatch(&kp_ballhog[1], "K_ISBHOG"); + HU_UpdatePatch(&kp_selfpropelledbomb[1], "K_ISSPB"); + HU_UpdatePatch(&kp_grow[1], "K_ISGROW"); + HU_UpdatePatch(&kp_shrink[1], "K_ISSHRK"); + HU_UpdatePatch(&kp_lightningshield[1], "K_ISTHNS"); + HU_UpdatePatch(&kp_bubbleshield[1], "K_ISBUBS"); + HU_UpdatePatch(&kp_flameshield[1], "K_ISFLMS"); + HU_UpdatePatch(&kp_hyudoro[1], "K_ISHYUD"); + HU_UpdatePatch(&kp_pogospring[1], "K_ISPOGO"); + HU_UpdatePatch(&kp_superring[1], "K_ISRING"); + HU_UpdatePatch(&kp_kitchensink[1], "K_ISSINK"); + HU_UpdatePatch(&kp_droptarget[1], "K_ISDTRG"); + HU_UpdatePatch(&kp_gardentop[1], "K_ISGTOP"); + HU_UpdatePatch(&kp_gachabom[1], "K_ISGBOM"); + HU_UpdatePatch(&kp_bar[1], "K_SBBAR"); + HU_UpdatePatch(&kp_doublebar[1], "K_SBBAR2"); + HU_UpdatePatch(&kp_triplebar[1], "K_SBBAR3"); + HU_UpdatePatch(&kp_slotring[1], "K_SBRING"); + HU_UpdatePatch(&kp_seven[1], "K_SBSEV"); + HU_UpdatePatch(&kp_jackpot[1], "K_SBJACK"); + + sprintf(buffer, "FSMFSxxx"); + for (i = 0; i < 120; i++) + { + buffer[5] = '0'+((i+1)/100); + buffer[6] = '0'+(((i+1)/10)%10); + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_flameshieldmeter[i][1], "%s", buffer); + } + + sprintf(buffer, "FSMBS0xx"); + for (i = 0; i < 120; i++) + { + buffer[5] = '0'+((i+1)/100); + buffer[6] = '0'+(((i+1)/10)%10); + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_flameshieldmeter_bg[i][1], "%s", buffer); + } + + // 4P item spy + HU_UpdatePatch(&kp_itembg[4], "ISPYBG"); + HU_UpdatePatch(&kp_itembg[5], "ISPYBGD"); + + //HU_UpdatePatch(&kp_sadface[2], "ISPYSAD"); + HU_UpdatePatch(&kp_sneaker[2], "ISPYSHOE"); + HU_UpdatePatch(&kp_rocketsneaker[2], "ISPYRSHE"); + sprintf(buffer, "ISPYINVx"); + for (i = 0; i < 6; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_invincibility[i+13], "%s", buffer); + } + HU_UpdatePatch(&kp_banana[2], "ISPYBANA"); + HU_UpdatePatch(&kp_eggman[2], "ISPYEGGM"); + HU_UpdatePatch(&kp_orbinaut[5], "ISPYORBN"); + HU_UpdatePatch(&kp_jawz[2], "ISPYJAWZ"); + HU_UpdatePatch(&kp_mine[2], "ISPYMINE"); + HU_UpdatePatch(&kp_landmine[2], "ISPYLNDM"); + HU_UpdatePatch(&kp_ballhog[2], "ISPYBHOG"); + HU_UpdatePatch(&kp_selfpropelledbomb[2], "ISPYSPB"); + HU_UpdatePatch(&kp_grow[2], "ISPYGROW"); + HU_UpdatePatch(&kp_shrink[2], "ISPYSHRK"); + HU_UpdatePatch(&kp_lightningshield[2], "ISPYTHNS"); + HU_UpdatePatch(&kp_bubbleshield[2], "ISPYBUBS"); + HU_UpdatePatch(&kp_flameshield[2], "ISPYFLMS"); + HU_UpdatePatch(&kp_hyudoro[2], "ISPYHYUD"); + HU_UpdatePatch(&kp_pogospring[2], "ISPYPOGO"); + HU_UpdatePatch(&kp_superring[2], "ISPYRING"); + HU_UpdatePatch(&kp_kitchensink[2], "ISPYSINK"); + HU_UpdatePatch(&kp_droptarget[2], "ISPYDTRG"); + HU_UpdatePatch(&kp_gardentop[2], "ISPYGTOP"); + HU_UpdatePatch(&kp_gachabom[2], "ISPYGBOM"); + + // CHECK indicators + sprintf(buffer, "K_CHECKx"); + for (i = 0; i < 6; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_check[i], "%s", buffer); + } + + // Rival indicators + sprintf(buffer, "K_RIVALx"); + for (i = 0; i < 2; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_rival[i], "%s", buffer); + } + + // Rival indicators + sprintf(buffer, "K_SSPLxx"); + for (i = 0; i < 4; i++) + { + buffer[6] = 'A'+i; + for (j = 0; j < 2; j++) + { + buffer[7] = '1'+j; + HU_UpdatePatch(&kp_localtag[i][j], "%s", buffer); + } + } + + // Typing indicator + HU_UpdatePatch(&kp_talk, "K_TALK"); + HU_UpdatePatch(&kp_typdot, "K_TYPDOT"); + + // Eggman warning numbers + sprintf(buffer, "K_EGGNx"); + for (i = 0; i < 6; i++) + { + buffer[6] = '0'+i; + HU_UpdatePatch(&kp_eggnum[i], "%s", buffer); + } + + // First person mode + HU_UpdatePatch(&kp_fpview[0], "VIEWA0"); + HU_UpdatePatch(&kp_fpview[1], "VIEWB0D0"); + HU_UpdatePatch(&kp_fpview[2], "VIEWC0E0"); + + // Input UI Wheel + sprintf(buffer, "K_WHEELx"); + for (i = 0; i < 5; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_inputwheel[i], "%s", buffer); + } + + // HERE COMES A NEW CHALLENGER + sprintf(buffer, "K_CHALxx"); + for (i = 0; i < 25; i++) + { + buffer[6] = '0'+((i+1)/10); + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_challenger[i], "%s", buffer); + } + + // Lap start animation + sprintf(buffer, "K_LAP0x"); + for (i = 0; i < 7; i++) + { + buffer[6] = '0'+(i+1); + HU_UpdatePatch(&kp_lapanim_lap[i], "%s", buffer); + } + + sprintf(buffer, "K_LAPFxx"); + for (i = 0; i < 11; i++) + { + buffer[6] = '0'+((i+1)/10); + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_lapanim_final[i], "%s", buffer); + } + + sprintf(buffer, "K_LAPNxx"); + for (i = 0; i < 10; i++) + { + buffer[6] = '0'+i; + for (j = 0; j < 3; j++) + { + buffer[7] = '0'+(j+1); + HU_UpdatePatch(&kp_lapanim_number[i][j], "%s", buffer); + } + } + + sprintf(buffer, "K_LAPE0x"); + for (i = 0; i < 2; i++) + { + buffer[7] = '0'+(i+1); + HU_UpdatePatch(&kp_lapanim_emblem[i], "%s", buffer); + } + + sprintf(buffer, "K_LAPH0x"); + for (i = 0; i < 3; i++) + { + buffer[7] = '0'+(i+1); + HU_UpdatePatch(&kp_lapanim_hand[i], "%s", buffer); + } + + HU_UpdatePatch(&kp_yougotem, "YOUGOTEM"); + HU_UpdatePatch(&kp_itemminimap, "MMAPITEM"); + + sprintf(buffer, "ALAGLESx"); + for (i = 0; i < 10; ++i) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_alagles[i], "%s", buffer); + } + + sprintf(buffer, "BLAGLESx"); + for (i = 0; i < 6; ++i) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_blagles[i], "%s", buffer); + } + + HU_UpdatePatch(&kp_cpu[0], "K_CPU1"); + HU_UpdatePatch(&kp_cpu[1], "K_CPU2"); + + HU_UpdatePatch(&kp_nametagstem, "K_NAMEST"); + + HU_UpdatePatch(&kp_trickcool[0], "K_COOL1"); + HU_UpdatePatch(&kp_trickcool[1], "K_COOL2"); + + HU_UpdatePatch(&kp_autoroulette, "A11YITEM"); + HU_UpdatePatch(&kp_autoring, "A11YRING"); + + sprintf(buffer, "K_BOSB0x"); + for (i = 0; i < 8; i++) + { + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_bossbar[i], "%s", buffer); + } + + sprintf(buffer, "K_BOSR0x"); + for (i = 0; i < 4; i++) + { + buffer[7] = '0'+((i+1)%10); + HU_UpdatePatch(&kp_bossret[i], "%s", buffer); + } + + sprintf(buffer, "HCAPARxx"); + for (i = 0; i < 2; i++) + { + buffer[6] = 'A'+i; + + for (j = 0; j < 2; j++) + { + buffer[7] = '0'+j; + HU_UpdatePatch(&kp_capsuletarget_arrow[i][j], "%s", buffer); + } + } + + sprintf(buffer, "HUDCAPDx"); + for (i = 0; i < 2; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_far_text[i], "%s", buffer); + } + + sprintf(buffer, "HUDCAPCx"); + for (i = 0; i < 2; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_icon[i], "%s", buffer); + } + + sprintf(buffer, "HUDCAPBx"); + for (i = 0; i < 2; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_far[0][i], "%s", buffer); + } + + sprintf(buffer, "HUDC4PBx"); + for (i = 0; i < 2; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_far[1][i], "%s", buffer); + } + + sprintf(buffer, "HUDCAPAx"); + for (i = 0; i < 8; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_near[0][i], "%s", buffer); + } + + sprintf(buffer, "HUDC4PAx"); + for (i = 0; i < 8; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_near[1][i], "%s", buffer); + } + + sprintf(buffer, "HUDFLKAx"); + for (i = 0; i < 4; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_superflickytarget[0][i], "%s", buffer); + } + + sprintf(buffer, "H4PFLKAx"); + for (i = 0; i < 4; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_superflickytarget[1][i], "%s", buffer); + } + + sprintf(buffer, "SPCNBFAx"); + for (i = 0; i < 6; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_spraycantarget_far[0][i], "%s", buffer); + } + + sprintf(buffer, "SPCNSFAx"); + for (i = 0; i < 6; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_spraycantarget_far[1][i], "%s", buffer); + } + + sprintf(buffer, "SPCNBCLx"); + for (i = 0; i < 6; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_spraycantarget_near[0][i], "%s", buffer); + } + + sprintf(buffer, "SPCNSCLx"); + for (i = 0; i < 6; i++) + { + buffer[7] = '1'+i; + HU_UpdatePatch(&kp_spraycantarget_near[1][i], "%s", buffer); + } + + K_LoadButtonGraphics(kp_button_a, "A"); + K_LoadButtonGraphics(kp_button_b, "B"); + K_LoadButtonGraphics(kp_button_c, "C"); + K_LoadButtonGraphics(kp_button_x, "X"); + K_LoadButtonGraphics(kp_button_y, "Y"); + K_LoadButtonGraphics(kp_button_z, "Z"); + K_LoadButtonGraphics(kp_button_l, "L1"); + K_LoadButtonGraphics(kp_button_r, "R1"); + K_LoadButtonGraphics(kp_button_up, "ARU"); + K_LoadButtonGraphics(kp_button_down, "ARD"); + K_LoadButtonGraphics(kp_button_right, "ARR"); + K_LoadButtonGraphics(kp_button_left, "ARL"); + K_LoadButtonGraphics(kp_button_start, "S"); + + K_LoadGenericButtonGraphics(kp_button_lua1, "LU1"); + K_LoadGenericButtonGraphics(kp_button_lua2, "LU2"); + K_LoadGenericButtonGraphics(kp_button_lua3, "LU3"); + + HU_UpdatePatch(&gen_button_keyleft[0], "TLK_L"); + HU_UpdatePatch(&gen_button_keyleft[1], "TLK_LB"); + HU_UpdatePatch(&gen_button_keyright[0], "TLK_R"); + HU_UpdatePatch(&gen_button_keyright[1], "TLK_RB"); + HU_UpdatePatch(&gen_button_keycenter[0], "TLK_M"); + HU_UpdatePatch(&gen_button_keycenter[1], "TLK_MB"); + + K_LoadGenericButtonGraphics(gen_button_dpad, "DP"); + K_LoadGenericButtonGraphics(gen_button_a, "A"); + K_LoadGenericButtonGraphics(gen_button_b, "B"); + K_LoadGenericButtonGraphics(gen_button_x, "X"); + K_LoadGenericButtonGraphics(gen_button_y, "Y"); + K_LoadGenericButtonGraphics(gen_button_lb, "L1"); + K_LoadGenericButtonGraphics(gen_button_rb, "R1"); + K_LoadGenericButtonGraphics(gen_button_lt, "L2"); + K_LoadGenericButtonGraphics(gen_button_rt, "R2"); + K_LoadGenericButtonGraphics(gen_button_ls, "L3"); + K_LoadGenericButtonGraphics(gen_button_rs, "R3"); + K_LoadGenericButtonGraphics(gen_button_start, "S"); + K_LoadGenericButtonGraphics(gen_button_back, "I"); + + HU_UpdatePatch(&kp_voice_localopen, "VOXCLO"); + for (i = 0; i < 16; i++) + { + HU_UpdatePatch(&kp_voice_localactive[i], "VOXCLA%d", i); + HU_UpdatePatch(&kp_voice_localactiveoverlay[i], "VOXCLB%d", i); + } + HU_UpdatePatch(&kp_voice_localmuted, "VOXCLM"); + HU_UpdatePatch(&kp_voice_localdeafened, "VOXCLD"); + HU_UpdatePatch(&kp_voice_remoteopen, "VOXCRO"); + HU_UpdatePatch(&kp_voice_remoteactive, "VOXCRA"); + HU_UpdatePatch(&kp_voice_remotemuted, "VOXCRM"); + HU_UpdatePatch(&kp_voice_remotedeafened, "VOXCRD"); + for (i = 0; i < 3; i++) + { + HU_UpdatePatch(&kp_voice_tagactive[i], "VOXCTA%d", i); + } +} + +// For the item toggle menu +const char *K_GetItemPatch(UINT8 item, boolean tiny) +{ + switch (item) + { + case KITEM_SNEAKER: + case KRITEM_DUALSNEAKER: + case KRITEM_TRIPLESNEAKER: + return (tiny ? "K_ISSHOE" : "K_ITSHOE"); + case KITEM_ROCKETSNEAKER: + return (tiny ? "K_ISRSHE" : "K_ITRSHE"); + case KITEM_INVINCIBILITY: + return (tiny ? "K_ISINV1" : "K_ITINV1"); + case KITEM_BANANA: + case KRITEM_TRIPLEBANANA: + return (tiny ? "K_ISBANA" : "K_ITBANA"); + case KITEM_EGGMAN: + return (tiny ? "K_ISEGGM" : "K_ITEGGM"); + case KITEM_ORBINAUT: + return (tiny ? "K_ISORBN" : "K_ITORB1"); + case KITEM_JAWZ: + case KRITEM_DUALJAWZ: + return (tiny ? "K_ISJAWZ" : "K_ITJAWZ"); + case KITEM_MINE: + return (tiny ? "K_ISMINE" : "K_ITMINE"); + case KITEM_LANDMINE: + return (tiny ? "K_ISLNDM" : "K_ITLNDM"); + case KITEM_BALLHOG: + return (tiny ? "K_ISBHOG" : "K_ITBHOG"); + case KITEM_SPB: + return (tiny ? "K_ISSPB" : "K_ITSPB"); + case KITEM_GROW: + return (tiny ? "K_ISGROW" : "K_ITGROW"); + case KITEM_SHRINK: + return (tiny ? "K_ISSHRK" : "K_ITSHRK"); + case KITEM_LIGHTNINGSHIELD: + return (tiny ? "K_ISTHNS" : "K_ITTHNS"); + case KITEM_BUBBLESHIELD: + return (tiny ? "K_ISBUBS" : "K_ITBUBS"); + case KITEM_FLAMESHIELD: + return (tiny ? "K_ISFLMS" : "K_ITFLMS"); + case KITEM_HYUDORO: + return (tiny ? "K_ISHYUD" : "K_ITHYUD"); + case KITEM_POGOSPRING: + return (tiny ? "K_ISPOGO" : "K_ITPOGO"); + case KITEM_SUPERRING: + return (tiny ? "K_ISRING" : "K_ITRING"); + case KITEM_KITCHENSINK: + return (tiny ? "K_ISSINK" : "K_ITSINK"); + case KITEM_DROPTARGET: + return (tiny ? "K_ISDTRG" : "K_ITDTRG"); + case KITEM_GARDENTOP: + return (tiny ? "K_ISGTOP" : "K_ITGTOP"); + case KITEM_GACHABOM: + case KRITEM_TRIPLEGACHABOM: + return (tiny ? "K_ISGBOM" : "K_ITGBOM"); + case KRITEM_TRIPLEORBINAUT: + return (tiny ? "K_ISORBN" : "K_ITORB3"); + case KRITEM_QUADORBINAUT: + return (tiny ? "K_ISORBN" : "K_ITORB4"); + default: + return (tiny ? "K_ISSAD" : "K_ITSAD"); + } +} + +static patch_t *K_GetCachedItemPatch(INT32 item, UINT8 offset) +{ + patch_t **kp[1 + NUMKARTITEMS] = { + kp_sadface, + NULL, + kp_sneaker, + kp_rocketsneaker, + kp_invincibility, + kp_banana, + kp_eggman, + kp_orbinaut, + kp_jawz, + kp_mine, + kp_landmine, + kp_ballhog, + kp_selfpropelledbomb, + kp_grow, + kp_shrink, + kp_lightningshield, + kp_bubbleshield, + kp_flameshield, + kp_hyudoro, + kp_pogospring, + kp_superring, + kp_kitchensink, + kp_droptarget, + kp_gardentop, + kp_gachabom, + }; + + if (item == KITEM_SAD || (item > KITEM_NONE && item < NUMKARTITEMS)) + return kp[item - KITEM_SAD][offset]; + else + return NULL; +} + +patch_t *K_GetSmallStaticCachedItemPatch(kartitems_t item) +{ + UINT8 offset; + + item = static_cast(K_ItemResultToType(item)); + + switch (item) + { + case KITEM_INVINCIBILITY: + offset = 7; + break; + + case KITEM_ORBINAUT: + offset = 4; + break; + + default: + offset = 1; + } + + return K_GetCachedItemPatch(item, offset); +} + +static patch_t *K_GetCachedSlotMachinePatch(INT32 item, UINT8 offset) +{ + patch_t **kp[KSM__MAX] = { + kp_bar, + kp_doublebar, + kp_triplebar, + kp_slotring, + kp_seven, + kp_jackpot, + }; + + if (item >= 0 && item < KSM__MAX) + return kp[item][offset]; + else + return NULL; +} + +//} + +INT32 ITEM_X, ITEM_Y; // Item Window +INT32 TIME_X, TIME_Y; // Time Sticker +INT32 LAPS_X, LAPS_Y; // Lap Sticker +INT32 POSI_X, POSI_Y; // Position Number +INT32 FACE_X, FACE_Y; // Top-four Faces +INT32 STCD_X, STCD_Y; // Starting countdown +INT32 CHEK_Y; // CHECK graphic +INT32 MINI_X, MINI_Y; // Minimap +INT32 WANT_X, WANT_Y; // Battle WANTED poster + +// This is for the P2 and P4 side of splitscreen. Then we'll flip P1's and P2's to the bottom with V_SPLITSCREEN. +INT32 ITEM2_X, ITEM2_Y; +INT32 LAPS2_X, LAPS2_Y; +INT32 POSI2_X, POSI2_Y; + +// trick "cool" +INT32 TCOOL_X, TCOOL_Y; + +// This version of the function was prototyped in Lua by Nev3r ... a HUGE thank you goes out to them! +void K_ObjectTracking(trackingResult_t *result, const vector3_t *point, boolean reverse) +{ +#define NEWTAN(x) FINETANGENT(((x + ANGLE_90) >> ANGLETOFINESHIFT) & 4095) // tan function used by Lua +#define NEWCOS(x) FINECOSINE((x >> ANGLETOFINESHIFT) & FINEMASK) + + angle_t viewpointAngle, viewpointAiming, viewpointRoll; + + INT32 screenWidth, screenHeight; + fixed_t screenHalfW, screenHalfH; + + const fixed_t baseFov = 90*FRACUNIT; + fixed_t fovDiff, fov, fovTangent, fg; + + fixed_t h; + INT32 da, dp; + + UINT8 cameraNum = R_GetViewNumber(); + + I_Assert(result != NULL); + I_Assert(point != NULL); + + // Initialize defaults + result->x = result->y = 0; + result->scale = FRACUNIT; + result->onScreen = false; + + // Take the view's properties as necessary. + if (reverse) + { + viewpointAngle = (INT32)(viewangle + ANGLE_180); + viewpointAiming = (INT32)InvAngle(aimingangle); + viewpointRoll = (INT32)viewroll; + } + else + { + viewpointAngle = (INT32)viewangle; + viewpointAiming = (INT32)aimingangle; + viewpointRoll = (INT32)InvAngle(viewroll); + } + + // Calculate screen size adjustments. + screenWidth = vid.width/vid.dupx; + screenHeight = vid.height/vid.dupy; + + if (r_splitscreen >= 2) + { + // Half-wide screens + screenWidth >>= 1; + } + + if (r_splitscreen >= 1) + { + // Half-tall screens + screenHeight >>= 1; + } + + screenHalfW = (screenWidth >> 1) << FRACBITS; + screenHalfH = (screenHeight >> 1) << FRACBITS; + + // Calculate FOV adjustments. + fovDiff = R_FOV(cameraNum) - baseFov; + fov = ((baseFov - fovDiff) / 2) - (stplyr->fovadd / 2); + fovTangent = NEWTAN(FixedAngle(fov)); + + if (r_splitscreen == 1) + { + // Splitscreen FOV is adjusted to maintain expected vertical view + fovTangent = 10*fovTangent/17; + } + + fg = (screenWidth >> 1) * fovTangent; + + // Determine viewpoint factors. + h = R_PointToDist2(point->x, point->y, viewx, viewy); + da = AngleDeltaSigned(viewpointAngle, R_PointToAngle2(viewx, viewy, point->x, point->y)); + dp = AngleDeltaSigned(viewpointAiming, R_PointToAngle2(0, 0, h, viewz)); + + if (reverse) + { + da = -(da); + } + + // Set results relative to top left! + result->x = FixedMul(NEWTAN(da), fg); + result->y = FixedMul((NEWTAN(viewpointAiming) - FixedDiv((point->z - viewz), 1 + FixedMul(NEWCOS(da), h))), fg); + + result->angle = da; + result->pitch = dp; + result->fov = fg; + + // Rotate for screen roll... + if (viewpointRoll) + { + fixed_t tempx = result->x; + viewpointRoll >>= ANGLETOFINESHIFT; + result->x = FixedMul(FINECOSINE(viewpointRoll), tempx) - FixedMul(FINESINE(viewpointRoll), result->y); + result->y = FixedMul(FINESINE(viewpointRoll), tempx) + FixedMul(FINECOSINE(viewpointRoll), result->y); + } + + // Flipped screen? + if (encoremode) + { + result->x = -result->x; + } + + // Center results. + result->x += screenHalfW; + result->y += screenHalfH; + + result->scale = FixedDiv(screenHalfW, h+1); + + result->onScreen = !((abs(da) > ANG60) || (abs(AngleDeltaSigned(viewpointAiming, R_PointToAngle2(0, 0, h, (viewz - point->z)))) > ANGLE_45)); + + // Cheap dirty hacks for some split-screen related cases + if (result->x < 0 || result->x > (screenWidth << FRACBITS)) + { + result->onScreen = false; + } + + if (result->y < 0 || result->y > (screenHeight << FRACBITS)) + { + result->onScreen = false; + } + + // adjust to non-green-resolution screen coordinates + result->x -= ((vid.width/vid.dupx) - BASEVIDWIDTH)<<(FRACBITS-((r_splitscreen >= 2) ? 2 : 1)); + result->y -= ((vid.height/vid.dupy) - BASEVIDHEIGHT)<<(FRACBITS-((r_splitscreen >= 1) ? 2 : 1)); + + return; + +#undef NEWTAN +#undef NEWCOS +} + +static void K_initKartHUD(void) +{ + /* + BASEVIDWIDTH = 320 + BASEVIDHEIGHT = 200 + + Item window graphic is 41 x 33 + + Time Sticker graphic is 116 x 11 + Time Font is a solid block of (8 x [12) x 14], equal to 96 x 14 + Therefore, timestamp is 116 x 14 altogether + + Lap Sticker is 80 x 11 + Lap flag is 22 x 20 + Lap Font is a solid block of (3 x [12) x 14], equal to 36 x 14 + Therefore, lapstamp is 80 x 20 altogether + + Position numbers are 43 x 53 + + Faces are 32 x 32 + Faces draw downscaled at 16 x 16 + Therefore, the allocated space for them is 16 x 67 altogether + + ---- + + ORIGINAL CZ64 SPLITSCREEN: + + Item window: + if (!splitscreen) { ICONX = 139; ICONY = 20; } + else { ICONX = BASEVIDWIDTH-315; ICONY = 60; } + + Time: 236, STRINGY( 12) + Lap: BASEVIDWIDTH-304, STRINGY(BASEVIDHEIGHT-189) + + */ + + // Single Screen (defaults) + // Item Window + ITEM_X = 5; // 5 + ITEM_Y = 5; // 5 + // Level Timer + TIME_X = BASEVIDWIDTH - 148; // 172 + TIME_Y = 9; // 9 + // Level Laps + LAPS_X = 9; // 9 + LAPS_Y = BASEVIDHEIGHT - 29; // 171 + // Position Number + POSI_X = BASEVIDWIDTH - 9; // 268 + POSI_Y = BASEVIDHEIGHT - 9; // 138 + // Top-Four Faces + FACE_X = 9; // 9 + FACE_Y = 92; // 92 + // Starting countdown + STCD_X = BASEVIDWIDTH/2; // 9 + STCD_Y = BASEVIDHEIGHT/2; // 92 + // CHECK graphic + CHEK_Y = BASEVIDHEIGHT; // 200 + // Minimap + MINI_X = BASEVIDWIDTH - 50; // 270 + MINI_Y = (BASEVIDHEIGHT/2)-16; // 84 + // Battle WANTED poster + WANT_X = BASEVIDWIDTH - 55; // 270 + WANT_Y = BASEVIDHEIGHT- 71; // 176 + + // trick COOL + TCOOL_X = (BASEVIDWIDTH)/2; + TCOOL_Y = (BASEVIDHEIGHT)/2 -10; + + if (r_splitscreen) // Splitscreen + { + ITEM_X = 5; + ITEM_Y = 3; + + LAPS_Y = (BASEVIDHEIGHT/2)-24; + + POSI_Y = (BASEVIDHEIGHT/2)- 2; + + STCD_Y = BASEVIDHEIGHT/4; + + MINI_X -= 16; + MINI_Y = (BASEVIDHEIGHT/2); + + if (r_splitscreen > 1) // 3P/4P Small Splitscreen + { + // 1P (top left) + ITEM_X = -9; + ITEM_Y = -8; + + LAPS_X = 3; + LAPS_Y = (BASEVIDHEIGHT/2)-12; + + POSI_X = 24; + POSI_Y = (BASEVIDHEIGHT/2)-26; + + // 2P (top right) + ITEM2_X = (BASEVIDWIDTH/2)-39; + ITEM2_Y = -8; + + LAPS2_X = (BASEVIDWIDTH/2)-43; + LAPS2_Y = (BASEVIDHEIGHT/2)-12; + + POSI2_X = (BASEVIDWIDTH/2)-4; + POSI2_Y = (BASEVIDHEIGHT/2)-26; + + // Reminder that 3P and 4P are just 1P and 2P splitscreen'd to the bottom. + + STCD_X = BASEVIDWIDTH/4; + + MINI_X = (3*BASEVIDWIDTH/4); + MINI_Y = (3*BASEVIDHEIGHT/4); + + TCOOL_X = (BASEVIDWIDTH)/4; + + if (r_splitscreen > 2) // 4P-only + { + MINI_X = (BASEVIDWIDTH/2); + MINI_Y = (BASEVIDHEIGHT/2); + } + } + } +} + +void K_DrawMapThumbnail(fixed_t x, fixed_t y, fixed_t width, UINT32 flags, UINT16 map, const UINT8 *colormap) +{ + patch_t *PictureOfLevel = NULL; + + if (map >= nummapheaders || !mapheaderinfo[map]) + { + PictureOfLevel = nolvl; + } + else if (!mapheaderinfo[map]->thumbnailPic) + { + PictureOfLevel = blanklvl; + } + else + { + PictureOfLevel = static_cast(mapheaderinfo[map]->thumbnailPic); + } + + K_DrawLikeMapThumbnail(x, y, width, flags, PictureOfLevel, colormap); +} + +void K_DrawLikeMapThumbnail(fixed_t x, fixed_t y, fixed_t width, UINT32 flags, patch_t *patch, const UINT8 *colormap) +{ + if (flags & V_FLIP) + x += width; + + V_DrawFixedPatch( + x, y, + FixedDiv(width, (320 << FRACBITS)), + flags, + patch, + colormap + ); +} + +// see also K_DrawNameTagItemSpy +static void K_drawKartItem(void) +{ + // ITEM_X = BASEVIDWIDTH-50; // 270 + // ITEM_Y = 24; // 24 + + // Why write V_DrawScaledPatch calls over and over when they're all the same? + // Set to 'no item' just in case. + const UINT8 offset = ((r_splitscreen > 1) ? 1 : 0); + patch_t *localpatch[3] = { kp_nodraw, kp_nodraw, kp_nodraw }; + UINT8 localamt[3] = {0, 0, 0}; + patch_t *localbg = ((offset) ? kp_itembg[2] : kp_itembg[0]); + patch_t *localinv = ((offset) ? kp_invincibility[((leveltime % (6*3)) / 3) + 7] : kp_invincibility[(leveltime % (7*3)) / 3]); + INT32 fx = 0, fy = 0, fflags = 0; // final coords for hud and flags... + const INT32 numberdisplaymin = ((!offset && stplyr->itemtype == KITEM_ORBINAUT) ? 5 : 2); + INT32 itembar = 0; + INT32 maxl = 0; // itembar's normal highest value + const INT32 barlength = (offset ? 12 : 26); + skincolornum_t localcolor[3] = { static_cast(stplyr->skincolor) }; + SINT8 colormode[3] = { TC_RAINBOW }; + boolean flipamount = false; // Used for 3P/4P splitscreen to flip item amount stuff + + fixed_t rouletteOffset = 0; + fixed_t rouletteSpace = ROULETTE_SPACING; + vector2_t rouletteCrop = {7, 7}; + INT32 i; + + boolean flashOnOne = false; + boolean flashOnTwo = false; + + if (stplyr->itemRoulette.itemListLen > 0) + { + // Init with item roulette stuff. + for (i = 0; i < 3; i++) + { + const SINT8 indexOfs = i-1; + const size_t index = (stplyr->itemRoulette.itemListLen + (stplyr->itemRoulette.index + indexOfs)) % stplyr->itemRoulette.itemListLen; + + const SINT8 result = stplyr->itemRoulette.itemList[index]; + const SINT8 item = K_ItemResultToType(result); + const boolean usingDebugItemAmount = cv_kartdebugitem.value != KITEM_NONE && cv_kartdebugitem.value == item && cv_kartdebugamount.value > 1; + const UINT8 amt = usingDebugItemAmount ? cv_kartdebugamount.value : K_ItemResultToAmount(result, &stplyr->itemRoulette); + + switch (item) + { + case KITEM_INVINCIBILITY: + localpatch[i] = localinv; + localamt[i] = amt; + break; + + case KITEM_ORBINAUT: + localpatch[i] = kp_orbinaut[(offset ? 4 : std::min(amt-1, 3))]; + if (amt > 4) + localamt[i] = amt; + break; + + default: + localpatch[i] = K_GetCachedItemPatch(item, offset); + if (item != KITEM_BALLHOG || amt != 5) + localamt[i] = amt; + break; + } + } + } + + if (stplyr->itemRoulette.active == true) + { + rouletteOffset = K_GetRouletteOffset(&stplyr->itemRoulette, rendertimefrac, 0); + } + else + { + // I'm doing this a little weird and drawing mostly in reverse order + // The only actual reason is to make sneakers line up this way in the code below + // This shouldn't have any actual baring over how it functions + // Hyudoro is first, because we're drawing it on top of the player's current item + + localcolor[1] = SKINCOLOR_NONE; + rouletteOffset = stplyr->karthud[khud_rouletteoffset]; + + if (stplyr->stealingtimer < 0) + { + localpatch[1] = kp_hyudoro[offset]; + flashOnTwo = true; + } + else if ((stplyr->stealingtimer > 0) && (leveltime & 2)) + { + localpatch[1] = kp_hyudoro[offset]; + } + else if (stplyr->eggmanexplode > 1) + { + localpatch[1] = kp_eggman[offset]; + flashOnOne = true; + } + else if (stplyr->ballhogcharge > 0) + { + // itembar = stplyr->ballhogcharge; + // maxl = (((stplyr->itemamount-1) * BALLHOGINCREMENT) + 1); + + itembar = stplyr->ballhogcharge % BALLHOGINCREMENT; + maxl = BALLHOGINCREMENT; + + localpatch[1] = kp_ballhog[offset]; + flashOnOne = true; + } + else if (stplyr->rocketsneakertimer > 1) + { + itembar = stplyr->rocketsneakertimer; + maxl = (itemtime*3) - barlength; + + localpatch[1] = kp_rocketsneaker[offset]; + flashOnOne = true; + } + else if (stplyr->sadtimer > 0) + { + localpatch[1] = kp_sadface[offset]; + flashOnTwo = true; + } + else if (stplyr->itemRoulette.reserved > 0) + { + localpatch[1] = kp_nodraw; + } + else + { + if (stplyr->itemamount <= 0) + return; + + switch (stplyr->itemtype) + { + case KITEM_INVINCIBILITY: + localpatch[1] = localinv; + localbg = kp_itembg[offset+1]; + break; + + case KITEM_ORBINAUT: + localpatch[1] = kp_orbinaut[(offset ? 4 : std::min(stplyr->itemamount-1, 3))]; + break; + + case KITEM_SPB: + case KITEM_LIGHTNINGSHIELD: + case KITEM_BUBBLESHIELD: + case KITEM_FLAMESHIELD: + localbg = kp_itembg[offset+1]; + /*FALLTHRU*/ + + default: + localpatch[1] = K_GetCachedItemPatch(stplyr->itemtype, offset); + + if (localpatch[1] == NULL) + localpatch[1] = kp_nodraw; // diagnose underflows + break; + } + + if ((stplyr->itemflags & IF_ITEMOUT)) + flashOnOne = true; + } + + if (!cv_reducevfx.value) + { + if (flashOnOne && !(leveltime & 1)) + localpatch[1] = kp_nodraw; + else if (flashOnTwo && !(leveltime & 2)) + localpatch[1] = kp_nodraw; + } + + if (stplyr->karthud[khud_itemblink] && (leveltime & 1) && !(cv_reducevfx.value)) + { + colormode[1] = TC_BLINK; + + switch (stplyr->karthud[khud_itemblinkmode]) + { + case 2: + localcolor[1] = static_cast(K_RainbowColor(leveltime)); + break; + case 1: + localcolor[1] = SKINCOLOR_RED; + break; + default: + localcolor[1] = SKINCOLOR_WHITE; + break; + } + } + else + { + // Hide the other items. + // Effectively lets the other roulette items + // show flicker away after you select. + localpatch[0] = localpatch[2] = kp_nodraw; + } + } + + // pain and suffering defined below + if (offset) + { + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + fx = ITEM_X; + fy = ITEM_Y; + fflags = V_SNAPTOLEFT|V_SNAPTOTOP|V_SPLITSCREEN; + } + else // else, that means we're P2 or P4. + { + fx = ITEM2_X; + fy = ITEM2_Y; + fflags = V_SNAPTORIGHT|V_SNAPTOTOP|V_SPLITSCREEN; + flipamount = true; + } + + rouletteSpace = ROULETTE_SPACING_SPLITSCREEN; + rouletteOffset = FixedMul(rouletteOffset, FixedDiv(ROULETTE_SPACING_SPLITSCREEN, ROULETTE_SPACING)); + rouletteCrop.x = 16; + rouletteCrop.y = 15; + } + else + { + fx = ITEM_X; + fy = ITEM_Y; + fflags = V_SNAPTOTOP|V_SNAPTOLEFT|V_SPLITSCREEN; + } + + if (r_splitscreen == 1) + { + fy -= 5; + } + + UINT8 *boxmap = NULL; + if (stplyr->itemRoulette.active && (stplyr->itemRoulette.speed - stplyr->itemRoulette.tics < 3) && stplyr->itemRoulette.index == 0) + { + boxmap = R_GetTranslationColormap(TC_ALLWHITE, SKINCOLOR_WHITE, GTC_CACHE); + } + V_DrawMappedPatch(fx, fy, V_HUDTRANS|V_SLIDEIN|fflags, localbg, boxmap); + + // Need to draw these in a particular order, for sorting. + V_SetClipRect( + (fx + rouletteCrop.x) << FRACBITS, (fy + rouletteCrop.y) << FRACBITS, + rouletteSpace, rouletteSpace, + V_SLIDEIN|fflags + ); + + auto draw_item = [&](fixed_t y, int i) + { + const UINT8 *colormap = (localcolor[i] ? R_GetTranslationColormap(colormode[i], localcolor[i], GTC_CACHE) : NULL); + V_DrawFixedPatch( + fx< 1) + { + using srb2::Draw; + Draw( + fx + rouletteCrop.x + FixedToFloat(rouletteSpace/2), + fy + rouletteCrop.y + FixedToFloat(rouletteOffset + y + rouletteSpace) - (r_splitscreen > 1 ? 15 : 33)) + .font(r_splitscreen > 1 ? Draw::Font::kRollingNum4P : Draw::Font::kRollingNum) + .align(Draw::Align::kCenter) + .flags(V_HUDTRANS|V_SLIDEIN|fflags) + .colormap(colormap) + .text("{}", localamt[i]); + } + }; + + draw_item(rouletteSpace, 0); + draw_item(-rouletteSpace, 2); + + if (stplyr->itemRoulette.active == true) + { + // Draw the item underneath the box. + draw_item(0, 1); + V_ClearClipRect(); + } + else + { + // Draw the item above the box. + V_ClearClipRect(); + + // A little goofy, but helps with ballhog charge conveyance—you're "loading" them. + UINT8 fakeitemamount = stplyr->itemamount - (stplyr->ballhogcharge / BALLHOGINCREMENT); + + boolean transflag = V_HUDTRANS; + + if (cv_reducevfx.value && (flashOnOne || flashOnTwo)) + { + transflag = V_HUDTRANSHALF; + } + + if (fakeitemamount >= numberdisplaymin && stplyr->itemRoulette.active == false) + { + // Then, the numbers: + V_DrawScaledPatch( + fx + (flipamount ? 48 : 0), fy, + V_HUDTRANS|V_SLIDEIN|fflags|(flipamount ? V_FLIP : 0), + kp_itemmulsticker[offset] + ); // flip this graphic for p2 and p4 in split and shift it. + + V_DrawFixedPatch( + fx< 2) + { + V_DrawFill(fx+x+length, fy+y+1, 1, height, 12|fflags); // the right one + if (height == 2) + V_DrawFill(fx+x+2, fy+y+2, length-2, 1, 8|fflags); // the dulled underside + V_DrawFill(fx+x+2, fy+y+1, length-2, 1, 0|fflags); // the shine + } + } + + // Quick Eggman numbers + if (stplyr->eggmanexplode > 1) + V_DrawScaledPatch(fx+17, fy+13-offset, V_HUDTRANS|V_SLIDEIN|fflags, kp_eggnum[std::min(5, G_TicsToSeconds(stplyr->eggmanexplode))]); + + if (stplyr->itemtype == KITEM_FLAMESHIELD && stplyr->flamelength > 0) + { + INT32 numframes = FLAMESHIELD_MAX; + INT32 absolutemax = numframes; + INT32 flamemax = stplyr->flamelength; + INT32 flamemeter = std::min(static_cast(stplyr->flamemeter), flamemax); + + INT32 bf = numframes - stplyr->flamelength; + INT32 ff = numframes - ((flamemeter * numframes) / absolutemax); + + INT32 xo = 6, yo = 4; + INT32 flip = 0; + + if (offset) + { + xo++; + + if (!(R_GetViewNumber() & 1)) // Flip for P1 and P3 (yes, that's correct) + { + xo -= 62; + flip = V_FLIP; + } + } + + /* + INT32 fmin = (8 * (bf-1)); + if (ff < fmin) + ff = fmin; + */ + + if (bf >= 0 && bf < numframes) + V_DrawScaledPatch(fx-xo, fy-yo, V_HUDTRANS|V_SLIDEIN|fflags|flip, kp_flameshieldmeter_bg[bf][offset]); + + if (ff >= 0 && ff < numframes && stplyr->flamemeter > 0) + { + if ((stplyr->flamemeter > flamemax) && (leveltime & 1)) + { + UINT8 *fsflash = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_WHITE, GTC_CACHE); + V_DrawMappedPatch(fx-xo, fy-yo, V_HUDTRANS|V_SLIDEIN|fflags|flip, kp_flameshieldmeter[ff][offset], fsflash); + } + else + { + V_DrawScaledPatch(fx-xo, fy-yo, V_HUDTRANS|V_SLIDEIN|fflags|flip, kp_flameshieldmeter[ff][offset]); + } + } + } +} + +static void K_drawKartSlotMachine(void) +{ + // ITEM_X = BASEVIDWIDTH-50; // 270 + // ITEM_Y = 24; // 24 + + // Why write V_DrawScaledPatch calls over and over when they're all the same? + // Set to 'no item' just in case. + const UINT8 offset = ((r_splitscreen > 1) ? 1 : 0); + + patch_t *localpatch[3] = { kp_nodraw, kp_nodraw, kp_nodraw }; + patch_t *localbg = offset ? kp_ringbg[1] : kp_ringbg[0]; + + // == SHITGARBAGE UNLIMITED 2: RISE OF MY ASS == + // FIVE LAYERS OF BULLSHIT PER-PIXEL SHOVING BECAUSE THE PATCHES HAVE DIFFERENT OFFSETS + // IF YOU ARE HERE TO ADJUST THE RINGBOX HUD TURN OFF YOUR COMPUTER AND GO TO YOUR LOCAL PARK + + INT32 fx = 0, fy = 0, fflags = 0; // final coords for hud and flags... + INT32 boxoffx = 0; + INT32 boxoffy = -6; + INT32 vstretch = 0; + INT32 hstretch = 3; + INT32 splitbsx = 0, splitbsy = 0; + skincolornum_t localcolor[3] = { static_cast(stplyr->skincolor) }; + SINT8 colormode[3] = { TC_RAINBOW }; + + fixed_t rouletteOffset = 0; + fixed_t rouletteSpace = SLOT_SPACING; + vector2_t rouletteCrop = {10, 10}; + INT32 i; + + if (stplyr->itemRoulette.itemListLen > 0) + { + // Init with item roulette stuff. + for (i = 0; i < 3; i++) + { + const SINT8 indexOfs = i-1; + const size_t index = (stplyr->itemRoulette.itemListLen + (stplyr->itemRoulette.index + indexOfs)) % stplyr->itemRoulette.itemListLen; + + const SINT8 result = stplyr->itemRoulette.itemList[index]; + + localpatch[i] = K_GetCachedSlotMachinePatch(result, offset); + } + } + + if (stplyr->itemRoulette.active == true) + { + rouletteOffset = K_GetSlotOffset(&stplyr->itemRoulette, rendertimefrac, 0); + } + else + { + rouletteOffset = stplyr->karthud[khud_rouletteoffset]; + + if (!stplyr->ringboxdelay) + { + return; + } + } + + if (stplyr->karthud[khud_itemblink] && (leveltime & 1) && !cv_reducevfx.value) + { + colormode[1] = TC_BLINK; + localcolor[1] = SKINCOLOR_WHITE; + + // This looks kinda wild with the white-background patch. + /* + switch (stplyr->ringboxaward) + { + case 5: // JACKPOT! + localcolor[1] = K_RainbowColor(leveltime); + break; + default: + localcolor[1] = SKINCOLOR_WHITE; + break; + } + */ + } + + // pain and suffering defined below + if (offset) + { + boxoffx -= 4; + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + fx = ITEM_X + 10; + fy = ITEM_Y + 10; + fflags = V_SNAPTOLEFT|V_SNAPTOTOP|V_SPLITSCREEN; + } + else // else, that means we're P2 or P4. + { + fx = ITEM2_X + 7; + fy = ITEM2_Y + 10; + fflags = V_SNAPTORIGHT|V_SNAPTOTOP|V_SPLITSCREEN; + } + + rouletteSpace = SLOT_SPACING_SPLITSCREEN; + rouletteOffset = FixedMul(rouletteOffset, FixedDiv(SLOT_SPACING_SPLITSCREEN, SLOT_SPACING)); + rouletteCrop.x = 16; + rouletteCrop.y = 13; + splitbsx = -6; + splitbsy = -6; + boxoffy += 2; + hstretch = 0; + } + else + { + fx = ITEM_X; + fy = ITEM_Y; + fflags = V_SNAPTOTOP|V_SNAPTOLEFT|V_SPLITSCREEN; + } + + if (r_splitscreen == 1) + { + fy -= 5; + } + + V_DrawScaledPatch(fx, fy, V_HUDTRANS|V_SLIDEIN|fflags, localbg); + + V_SetClipRect( + ((fx + rouletteCrop.x + boxoffx + splitbsx) << FRACBITS), ((fy + rouletteCrop.y + boxoffy - vstretch + splitbsy) << FRACBITS), + rouletteSpace + (hstretch< 0) + { + if (drawtime >= timelimitintics) + { + jitter = 2; + if (drawtime & 2) + jitter = -jitter; + drawtime = 0; + } + else + { + drawtime = timelimitintics - drawtime; + if (secretextratime) + ; + else if (extratimeintics) + { + jitter = 2; + if (leveltime & 1) + jitter = -jitter; + } + else if (drawtime <= 5*TICRATE) + { + jitter = ((drawtime <= 3*TICRATE) && (((drawtime-1) % TICRATE) >= TICRATE-2)) + ? 3 : 1; + if (drawtime & 2) + jitter = -jitter; + } + } + } + } + + if (return_jitter) + { + *return_jitter = jitter; + } + + return drawtime; +} + +INT32 K_drawKartMicroTime(const char *todrawtext, INT32 workx, INT32 worky, INT32 splitflags) +{ + using srb2::Draw; + Draw::TextElement text(todrawtext); + text.flags(splitflags); + text.font(Draw::Font::kZVote); + + INT32 result = text.width(); + Draw(workx - result, worky).text(text); + + return result; +} + +void K_drawKartTimestamp(tic_t drawtime, INT32 TX, INT32 TY, INT32 splitflags, UINT8 mode) +{ + // TIME_X = BASEVIDWIDTH-124; // 196 + // TIME_Y = 6; // 6 + + INT32 jitter = 0; + + drawtime = K_TranslateTimer(drawtime, mode, &jitter); + + V_DrawScaledPatch(TX, TY, splitflags, ((mode == 2) ? kp_lapstickerwide : kp_timestickerwide)); + + TX += 33; + + if (drawtime == UINT32_MAX) + ; + else if (mode && !drawtime) + { + // apostrophe location _'__ __ + V_DrawTimerString(TX+24, TY+3, splitflags, va("'")); + + // quotation mark location _ __"__ + V_DrawTimerString(TX+60, TY+3, splitflags, va("\"")); + } + else + { + tic_t worktime = drawtime/(60*TICRATE); + + if (worktime >= 100) + { + jitter = (drawtime & 1 ? 1 : -1); + worktime = 99; + drawtime = (100*(60*TICRATE))-1; + } + + // minutes time 00 __ __ + V_DrawTimerString(TX, TY+3+jitter, splitflags, va("%d", worktime/10)); + V_DrawTimerString(TX+12, TY+3-jitter, splitflags, va("%d", worktime%10)); + + // apostrophe location _'__ __ + V_DrawTimerString(TX+24, TY+3, splitflags, va("'")); + + worktime = (drawtime/TICRATE % 60); + + // seconds time _ 00 __ + V_DrawTimerString(TX+36, TY+3+jitter, splitflags, va("%d", worktime/10)); + V_DrawTimerString(TX+48, TY+3-jitter, splitflags, va("%d", worktime%10)); + + // quotation mark location _ __"__ + V_DrawTimerString(TX+60, TY+3, splitflags, va("\"")); + + worktime = G_TicsToCentiseconds(drawtime); + + // tics _ __ 00 + V_DrawTimerString(TX+72, TY+3+jitter, splitflags, va("%d", worktime/10)); + V_DrawTimerString(TX+84, TY+3-jitter, splitflags, va("%d", worktime%10)); + } + + // Medal data! + if ((modeattacking || (mode == 1)) + && !demo.playback) + { + INT32 workx = TX + 96, worky = TY+18; + UINT8 i = stickermedalinfo.visiblecount; + + if (stickermedalinfo.targettext[0] != '\0') + { + if (!mode) + { + if (stickermedalinfo.jitter) + { + jitter = stickermedalinfo.jitter+3; + if (jitter & 2) + workx += jitter/4; + else + workx -= jitter/4; + } + + if (stickermedalinfo.norecord == true) + { + splitflags = (splitflags &~ V_HUDTRANS)|V_HUDTRANSHALF; + } + } + + workx -= K_drawKartMicroTime(stickermedalinfo.targettext, workx, worky, splitflags); + } + + workx -= (((1 + i - stickermedalinfo.platinumcount)*6) - 1); + + if (!mode) + splitflags = (splitflags &~ V_HUDTRANSHALF)|V_HUDTRANS; + while (i > 0) + { + i--; + + if (gamedata->collected[(stickermedalinfo.emblems[i]-emblemlocations)]) + { + V_DrawMappedPatch(workx, worky, splitflags, + static_cast(W_CachePatchName(M_GetEmblemPatch(stickermedalinfo.emblems[i], false), PU_CACHE)), + R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(stickermedalinfo.emblems[i]), GTC_CACHE) + ); + + workx += 6; + } + else if ( + stickermedalinfo.emblems[i]->type != ET_TIME + || stickermedalinfo.emblems[i]->tag != AUTOMEDAL_PLATINUM + ) + { + V_DrawMappedPatch(workx, worky, splitflags, + static_cast(W_CachePatchName("NEEDIT", PU_CACHE)), + NULL + ); + + workx += 6; + } + } + } + + if (modeattacking & ATTACKING_SPB && stplyr->SPBdistance > 0) + { + UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, static_cast(stplyr->skincolor), GTC_CACHE); + INT32 ybar = 180; + INT32 widthbar = 120, xbar = 160 - widthbar/2, currentx; + INT32 barflags = V_SNAPTOBOTTOM|V_SLIDEIN; + INT32 transflags = ((6)<skin][FACE_MINIMAP], colormap); + + // vibes-based math + currentx = (stplyr->SPBdistance/mapobjectscale - mobjinfo[MT_SPB].radius/FRACUNIT - mobjinfo[MT_PLAYER].radius/FRACUNIT) * 6; + if (currentx > 0) + { + currentx = sqrt(currentx); + if (currentx > widthbar) + currentx = widthbar; + } + else + { + currentx = 0; + } + V_DrawScaledPatch(160 + widthbar/2 - currentx - 5, ybar - 7, barflags, kp_spbminimap); + } +} + +static fixed_t K_DrawKartPositionNumPatch(UINT8 num, UINT8 splitIndex, UINT8 *color, fixed_t x, fixed_t y, fixed_t scale, INT32 flags) +{ + fixed_t w = FRACUNIT; + fixed_t h = FRACUNIT; + INT32 overlayFlags[2]; + INT32 i; + + if (num > 9) + { + return x; // invalid input + } + + if ((mapheaderinfo[gamemap - 1]->levelflags & LF_SUBTRACTNUM) == LF_SUBTRACTNUM) + { + overlayFlags[0] = V_SUBTRACT; + overlayFlags[1] = V_ADD; + } + else + { + overlayFlags[0] = V_ADD; + overlayFlags[1] = V_SUBTRACT; + } + + w = SHORT(kp_positionnum[num][0][splitIndex]->width) * scale; + h = SHORT(kp_positionnum[num][0][splitIndex]->height) * scale; + + x -= w; + + if (flags & V_SNAPTOBOTTOM) + { + y -= h; + } + + for (i = 1; i >= 0; i--) + { + V_DrawFixedPatch( + x, y, scale, + flags | overlayFlags[i], + kp_positionnum[num][i][splitIndex], + color + ); + } + + return x; +} + +void K_DrawKartPositionNumXY( + UINT8 num, + UINT8 splitIndex, + fixed_t fx, fixed_t fy, fixed_t scale, INT32 fflags, + tic_t counter, boolean subtract, + boolean exit, boolean lastLap, boolean losing + ) +{ + if (cv_reducevfx.value != 0) + { + // Reduce the flashing rate + counter /= 4; + } + + counter /= 3; // Alternate colors every three frames + + UINT8 *color = NULL; + if (exit && num == 1) + { + // 1st place winner? You get rainbows!! + color = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_POSNUM_BEST1 + (counter % 6)), GTC_CACHE); + } + else if (exit || lastLap) + { + // On the final lap, or already won. + boolean useRedNums = losing; + + if (subtract) + { + // Subtracting RED will look BLUE, and vice versa. + useRedNums = !useRedNums; + } + + if (useRedNums == true) + { + color = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_POSNUM_LOSE1 + (counter % 3)), GTC_CACHE); + } + else + { + color = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_POSNUM_WIN1 + (counter % 3)), GTC_CACHE); + } + } + else + { + color = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_POSNUM, GTC_CACHE); + } + + if ((fflags & V_SNAPTORIGHT) == 0) + { + UINT8 adjustNum = num; + do + { + fixed_t w = SHORT(kp_positionnum[adjustNum % 10][0][splitIndex]->width) * scale; + fx += w; + adjustNum /= 10; + } while (adjustNum); + } + + // Draw the number + do + { + fx = K_DrawKartPositionNumPatch( + (num % 10), splitIndex, color, + fx, fy, scale, V_SPLITSCREEN|fflags + ); + num /= 10; + } while (num); +} + +static void K_DrawKartPositionNum(UINT8 num) +{ + UINT8 splitIndex = (r_splitscreen > 0) ? 1 : 0; + fixed_t scale = FRACUNIT; + fixed_t fx = 0, fy = 0; + transnum_t trans = static_cast(0); + INT32 fflags = 0; + + if (stplyr->lives <= 0 && stplyr->playerstate == PST_DEAD) + { + return; + } + + if (leveltime < (starttime + NUMTRANSMAPS)) + { + trans = static_cast((starttime + NUMTRANSMAPS) - leveltime); + } + + if (trans >= NUMTRANSMAPS) + { + return; + } + + if (stplyr->positiondelay > 0 || K_PlayerTallyActive(stplyr) == true) + { + const UINT8 delay = (stplyr->exiting) ? POS_DELAY_TIME : stplyr->positiondelay; + const fixed_t add = (scale * 3) >> ((r_splitscreen == 1) ? 1 : 2); + scale += std::min((add * (delay * delay)) / (POS_DELAY_TIME * POS_DELAY_TIME), add); + } + + // pain and suffering defined below + if (!r_splitscreen) + { + fx = BASEVIDWIDTH << FRACBITS; + fy = BASEVIDHEIGHT << FRACBITS; + fflags = V_SNAPTOBOTTOM|V_SNAPTORIGHT; + } + else if (r_splitscreen == 1) // for this splitscreen, we'll use case by case because it's a bit different. + { + fx = BASEVIDWIDTH << FRACBITS; + + if (R_GetViewNumber() == 0) + { + // for player 1: display this at the top right, above the minimap. + fy = 0; + fflags = V_SNAPTOTOP|V_SNAPTORIGHT; + } + else + { + // if we're not p1, that means we're p2. display this at the bottom right, below the minimap. + fy = BASEVIDHEIGHT << FRACBITS; + fflags = V_SNAPTOBOTTOM|V_SNAPTORIGHT; + } + + fy >>= 1; + } + else + { + fy = BASEVIDHEIGHT << FRACBITS; + + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + // If we are P1 or P3... + fx = 0; + fflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM; + } + else + { + // else, that means we're P2 or P4. + fx = BASEVIDWIDTH << FRACBITS; + fflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM; + } + + fx >>= 1; + fy >>= 1; + + // We're putting it in the same corner as + // the rest of our HUD, so it needs raised. + fy -= (21 << FRACBITS); + } + + if (trans > 0) + { + fflags |= (trans << V_ALPHASHIFT); + } + + K_DrawKartPositionNumXY( + num, + splitIndex, + fx, fy, scale, fflags, + leveltime, + ((mapheaderinfo[gamemap - 1]->levelflags & LF_SUBTRACTNUM) == LF_SUBTRACTNUM), + stplyr->exiting, + (stplyr->laps >= numlaps), + K_IsPlayerLosing(stplyr) + ); +} + +struct PositionFacesInfo +{ + INT32 ranklines = 0; + INT32 strank = -1; + INT32 numplayersingame = 0; + INT32 rankplayer[MAXPLAYERS] = {}; + + PositionFacesInfo(); + void draw_1p(); + void draw_4p_battle(int x, int y, INT32 flags); + + player_t* top() const { return &players[rankplayer[0]]; } + UINT32 top_score() const { return G_TeamOrIndividualScore( top() ); } + + bool near_goal() const + { + if (g_pointlimit == 0) + return false; + constexpr tic_t kThreshold = 5; + return std::max(kThreshold, g_pointlimit) - kThreshold <= top_score(); + } + + skincolornum_t vomit_color() const + { + if (!near_goal()) + { + return static_cast(top()->skincolor); + } + + constexpr int kCycleSpeed = 4; + constexpr std::array kColors = { + SKINCOLOR_RED, + SKINCOLOR_VOMIT, + SKINCOLOR_YELLOW, + SKINCOLOR_GREEN, + SKINCOLOR_JET, + SKINCOLOR_MOONSET, + }; + return kColors[leveltime / kCycleSpeed % kColors.size()]; + } +}; + +PositionFacesInfo::PositionFacesInfo() +{ + INT32 i, j; + + for (i = 0; i < MAXPLAYERS; i++) + { + rankplayer[i] = -1; + + if (!playeringame[i] || players[i].spectator || !players[i].mo) + continue; + + numplayersingame++; + } + + if (numplayersingame <= 1) + return; + + boolean completed[MAXPLAYERS] = {}; + + for (j = 0; j < numplayersingame; j++) + { + UINT8 lowestposition = MAXPLAYERS+1; + for (i = 0; i < MAXPLAYERS; i++) + { + if (completed[i] || !playeringame[i] || players[i].spectator || !players[i].mo) + continue; + + if (players[i].position >= lowestposition) + continue; + + rankplayer[ranklines] = i; + lowestposition = players[i].position; + } + + i = rankplayer[ranklines]; + + completed[i] = true; + + if (players+i == stplyr) + strank = ranklines; + + //if (ranklines == 5) + //break; // Only draw the top 5 players -- we do this a different way now... + + ranklines++; + } +} + +void PositionFacesInfo::draw_1p() +{ + // FACE_X = 15; // 15 + // FACE_Y = 72; // 72 + + INT32 Y = FACE_Y-9; // -9 to offset where it's being drawn if there are more than one + INT32 i, j; + INT32 bumperx, emeraldx; + INT32 xoff, yoff, flipflag = 0; + UINT8 workingskin; + UINT8 *colormap; + UINT32 skinflags; + + if (gametyperules & GTR_POINTLIMIT) // playing battle + { + Y += 40; + if (ranklines < 3) + Y -= 18; + } + else if (ranklines < 5) + Y += (9*ranklines); + else + Y += (9*5); + + ranklines--; + i = ranklines; + + if (gametyperules & GTR_POINTLIMIT) // playing battle + { + // 3 lines max in Battle + if (i > 2) + i = 2; + ranklines = 0; + + // You must appear on the leaderboard, even if you don't rank top 3 + if (strank > i) + { + strank = i; + rankplayer[strank] = stplyr - players; + } + + // Draw GOAL + bool skull = g_pointlimit && (g_pointlimit <= G_TeamOrIndividualScore(stplyr)); + INT32 height = i*18; + INT32 GOAL_Y = Y-height; + + colormap = nullptr; + + if (skincolornum_t vomit = vomit_color()) + { + colormap = R_GetTranslationColormap(TC_DEFAULT, vomit, GTC_CACHE); + } + + V_DrawMappedPatch(FACE_X-5, GOAL_Y-32, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_goal[skull][0], colormap); + + // Flashing KO + if (skull) + { + if (leveltime % 16 < 8) + V_DrawScaledPatch(FACE_X-5, GOAL_Y-32, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_goaltext1p); + } + else if (g_pointlimit) + { + using srb2::Draw; + Draw(FACE_X+8.5, GOAL_Y-15) + .font(Draw::Font::kZVote) + .align(Draw::Align::kCenter) + .flags(V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT) + .text("{:02}", g_pointlimit); + } + + // Line cutting behind rank faces + V_DrawScaledPatch(FACE_X+6, GOAL_Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_goalrod[0]); + } + else if (strank <= 2) // too close to the top, or a spectator? would have had (strank == -1) called out, but already caught by (strank <= 2) + { + if (i > 4) // could be both... + i = 4; + ranklines = 0; + } + else if (strank+2 >= ranklines) // too close to the bottom? + { + ranklines -= 4; + if (ranklines < 0) + ranklines = 0; + } + else + { + i = strank+2; + ranklines = strank-2; + } + + for (; i >= ranklines; i--) + { + if (!playeringame[rankplayer[i]]) continue; + if (players[rankplayer[i]].spectator) continue; + if (!players[rankplayer[i]].mo) continue; + + bumperx = FACE_X+19; + emeraldx = FACE_X+16; + + skinflags = (demo.playback) + ? demo.skinlist[demo.currentskinid[rankplayer[i]]].flags + : skins[players[rankplayer[i]].skin].flags; + + // Flip SF_IRONMAN portraits, but only if they're transformed + if (skinflags & SF_IRONMAN + && !(players[rankplayer[i]].charflags & SF_IRONMAN) ) + { + flipflag = V_FLIP|V_VFLIP; // blonic flip + xoff = yoff = 16; + } else + { + flipflag = 0; + xoff = yoff = 0; + } + + if (players[rankplayer[i]].mo->color) + { + if ((skin_t*)players[rankplayer[i]].mo->skin) + workingskin = (skin_t*)players[rankplayer[i]].mo->skin - skins; + else + workingskin = players[rankplayer[i]].skin; + + colormap = R_GetTranslationColormap(workingskin, static_cast(players[rankplayer[i]].mo->color), GTC_CACHE); + if (players[rankplayer[i]].mo->colorized) + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(players[rankplayer[i]].mo->color), GTC_CACHE); + else + colormap = R_GetTranslationColormap(workingskin, static_cast(players[rankplayer[i]].mo->color), GTC_CACHE); + + V_DrawMappedPatch(FACE_X + xoff, Y + yoff, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT|flipflag, faceprefix[workingskin][FACE_RANK], colormap); + + if (LUA_HudEnabled(hud_battlebumpers)) + { + const UINT8 bumpers = K_Bumpers(&players[rankplayer[i]]); + + if (bumpers > 0) + { + V_DrawMappedPatch(bumperx-2, Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_tinybumper[0], colormap); + for (j = 1; j < bumpers; j++) + { + bumperx += 5; + V_DrawMappedPatch(bumperx, Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_tinybumper[1], colormap); + } + } + } + } + + for (j = 0; j < 7; j++) + { + UINT32 emeraldFlag = (1 << j); + skincolornum_t emeraldColor = static_cast(SKINCOLOR_CHAOSEMERALD1 + j); + + if (players[rankplayer[i]].emeralds & emeraldFlag) + { + colormap = R_GetTranslationColormap(TC_DEFAULT, emeraldColor, GTC_CACHE); + V_DrawMappedPatch(emeraldx, Y+7, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_rankemerald, colormap); + emeraldx += 7; + } + } + + if (i == strank) + V_DrawScaledPatch(FACE_X, Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_facehighlight[(leveltime / 4) % 8]); + + if ((gametyperules & GTR_BUMPERS) && (players[rankplayer[i]].pflags & PF_ELIMINATED)) + V_DrawScaledPatch(FACE_X-4, Y-3, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_ranknobumpers); + else if (K_Cooperative()) + ; + else if (gametyperules & GTR_CIRCUIT) + { + INT32 pos = players[rankplayer[i]].position; + if (pos < 0 || pos > MAXPLAYERS) + pos = 0; + // Draws the little number over the face + V_DrawScaledPatch(FACE_X-5, Y+10, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_facenum[pos]); + } + else if (gametyperules & GTR_POINTLIMIT) + { + INT32 flags = V_HUDTRANS | V_SLIDEIN | V_SNAPTOLEFT; + + colormap = NULL; + + if (g_pointlimit && g_pointlimit <= players[rankplayer[i]].roundscore) + { + if (leveltime % 8 < 4) + { + colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_TANGERINE, GTC_CACHE); + } + + flags |= V_STRINGDANCE; + } + + V_DrawStringScaled( + (FACE_X - 5) * FRACUNIT, + (Y + 10) * FRACUNIT, + FRACUNIT, + FRACUNIT, + FRACUNIT, + flags, + colormap, + PINGF_FONT, + va("%d", players[rankplayer[i]].roundscore) + ); + } + + // Voice speaking indicator + if (netgame && !players[rankplayer[i]].bot && cv_voice_servermute.value == 0) + { + patch_t *voxmic; + if (S_IsPlayerVoiceActive(rankplayer[i])) + { + voxmic = kp_voice_remoteactive; + } + else if (players[rankplayer[i]].pflags2 & (PF2_SELFDEAFEN | PF2_SERVERDEAFEN)) + { + voxmic = kp_voice_remotedeafened; + } + else if (players[rankplayer[i]].pflags2 & (PF2_SELFMUTE | PF2_SERVERMUTE)) + { + voxmic = kp_voice_remotemuted; + } + else + { + voxmic = kp_voice_remoteopen; + } + + V_DrawScaledPatch(FACE_X + 10, Y - 4, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, voxmic); + } + + Y -= 18; + } +} + +void PositionFacesInfo::draw_4p_battle(int x, int y, INT32 flags) +{ + using srb2::Draw; + Draw row = Draw(x, y).flags(V_HUDTRANS | V_SLIDEIN | flags).font(Draw::Font::kPing); + + UINT8 skull = [] + { + if (g_pointlimit == 0) + return 0; + + int party = G_PartySize(consoleplayer); + for (int i = 0; i < party; ++i) + { + // Is any party member about to win? + if (g_pointlimit <= players[G_PartyMember(consoleplayer, i)].roundscore) + { + return 1; + } + } + return 0; + }(); + + skincolornum_t vomit = vomit_color(); + (vomit ? row.colormap(vomit) : row).patch(kp_goal[skull][1]); + + if (!skull && g_pointlimit) + { + row.xy(8.5, 5).align(Draw::Align::kCenter).text("{:02}", g_pointlimit); + } + + row.xy(7, 18).patch(kp_goalrod[1]); + + auto head = [&](Draw col, int i) + { + const player_t& p = players[rankplayer[i]]; + col.colormap(p.skin, static_cast(p.skincolor)).patch(faceprefix[p.skin][FACE_MINIMAP]); + + bool dance = g_pointlimit && (g_pointlimit <= p.roundscore); + bool flash = dance && leveltime % 8 < 4; + ( + flash ? + col.xy(8, 6).colorize(SKINCOLOR_TANGERINE).flags(V_STRINGDANCE) : + col.xy(8, 6).flags(dance ? V_STRINGDANCE : 0) + ).text("{:02}", p.roundscore); + }; + + // Draw top 2 players + head(row.xy(2, 31), 1); + head(row.xy(2, 18), 0); +} + +static boolean K_drawKartPositionFaces(void) +{ + PositionFacesInfo state{}; + + if (state.numplayersingame <= 1) + return true; + + if (!LUA_HudEnabled(hud_minirankings)) + return false; // Don't proceed but still return true for free play above if HUD is disabled. + + switch (r_splitscreen) + { + case 0: + state.draw_1p(); + break; + + case 1: + state.draw_4p_battle(292, 78, V_SNAPTORIGHT); + break; + + case 2: + case 3: + state.draw_4p_battle(152, 9, V_SNAPTOTOP); + state.draw_4p_battle(152, 147, V_SNAPTOBOTTOM); + break; + } + + return false; +} + +static void K_drawBossHealthBar(void) +{ + UINT8 i = 0, barstatus = 1, randlen = 0, darken = 0; + const INT32 startx = BASEVIDWIDTH - 23; + INT32 starty = BASEVIDHEIGHT - 25; + INT32 rolrand = 0, randtemp = 0; + boolean randsign = false; + + if (bossinfo.barlen <= 1) + return; + + // Entire bar juddering! + if (lt_exitticker < (TICRATE/2)) + ; + else if (bossinfo.visualbarimpact) + { + INT32 mag = std::min((bossinfo.visualbarimpact/4) + 1, 8u); + if (bossinfo.visualbarimpact & 1) + starty -= mag; + else + starty += mag; + } + + if ((lt_ticker >= lt_endtime) && bossinfo.enemyname) + { + if (lt_exitticker == 0) + { + rolrand = 5; + } + else if (lt_exitticker == 1) + { + rolrand = 7; + } + else + { + rolrand = 10; + } + V_DrawRightAlignedThinString(startx, starty-rolrand, V_FORCEUPPERCASE|V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT, bossinfo.enemyname); + rolrand = 0; + } + + // Used for colour and randomisation. + if (bossinfo.healthbar <= (bossinfo.visualdiv/FRACUNIT)) + { + barstatus = 3; + } + else if (bossinfo.healthbar <= bossinfo.healthbarpinch) + { + barstatus = 2; + } + + randtemp = bossinfo.visualbar-(bossinfo.visualdiv/(2*FRACUNIT)); + if (randtemp > 0) + randlen = P_RandomKey(PR_INTERPHUDRANDOM, randtemp)+1; + randsign = P_RandomChance(PR_INTERPHUDRANDOM, FRACUNIT/2); + + // Right wing. + V_DrawScaledPatch(startx-1, starty, V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_FLIP, kp_bossbar[0]); + + // Draw the bar itself... + while (i < bossinfo.barlen) + { + V_DrawScaledPatch(startx-i, starty, V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT, kp_bossbar[1]); + if (i < bossinfo.visualbar) + { + randlen--; + if (!randlen) + { + randtemp = bossinfo.visualbar-(bossinfo.visualdiv/(2*FRACUNIT)); + if (randtemp > 0) + randlen = P_RandomKey(PR_INTERPHUDRANDOM, randtemp)+1; + if (barstatus > 1) + { + rolrand = P_RandomKey(PR_INTERPHUDRANDOM, barstatus)+1; + } + else + { + rolrand = 1; + } + if (randsign) + { + rolrand = -rolrand; + } + randsign = !randsign; + } + else + { + rolrand = 0; + } + if (lt_exitticker < (TICRATE/2)) + ; + else if ((bossinfo.visualbar - i) < (INT32)(bossinfo.visualbarimpact/8)) + { + if (bossinfo.visualbarimpact & 1) + rolrand += (bossinfo.visualbar - i); + else + rolrand -= (bossinfo.visualbar - i); + } + if (bossinfo.visualdiv) + { + fixed_t work = 0; + if ((i+1) == bossinfo.visualbar) + darken = 1; + else + { + darken = 0; + // a hybrid fixed-int modulo... + while ((work/FRACUNIT) < bossinfo.visualbar) + { + if (work/FRACUNIT != i) + { + work += bossinfo.visualdiv; + continue; + } + darken = 1; + break; + } + } + } + V_DrawScaledPatch(startx-i, starty+rolrand, V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT, kp_bossbar[(2*barstatus)+darken]); + } + i++; + } + + // Left wing. + V_DrawScaledPatch(startx-i, starty, V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT, kp_bossbar[0]); +} + +static void K_drawKartEmeralds(void) +{ + static const INT32 emeraldOffsets[7][3] = { + {34, 0, 15}, + {25, 8, 11}, + {43, 8, 19}, + {16, 0, 7}, + {52, 0, 23}, + { 7, 8, 3}, + {61, 8, 27} + }; + + INT32 splitflags = V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_SPLITSCREEN; + INT32 startx = BASEVIDWIDTH - 77; + INT32 starty = BASEVIDHEIGHT - 29; + INT32 i = 0, xindex = 0; + + { + if (r_splitscreen) + { + starty = (starty/2) - 8; + } + starty -= 8; + + if (r_splitscreen < 2) + { + startx -= 8; + if (r_splitscreen == 1 && R_GetViewNumber() == 0) + { + starty = 1; + } + V_DrawScaledPatch(startx, starty, V_HUDTRANS|splitflags, kp_rankemeraldback); + } + else + { + xindex = 2; + starty -= 15; + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + startx = LAPS_X; + splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + } + else // else, that means we're P2 or P4. + { + startx = LAPS2_X + 1; + splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + } + } + } + + for (i = 0; i < 7; i++) + { + UINT32 emeraldFlag = (1 << i); + skincolornum_t emeraldColor = static_cast(SKINCOLOR_CHAOSEMERALD1 + i); + + if (stplyr->emeralds & emeraldFlag) + { + boolean whiteFlash = (leveltime & 1); + UINT8 *colormap; + + if (i & 1) + { + whiteFlash = !whiteFlash; + } + + colormap = R_GetTranslationColormap(TC_DEFAULT, emeraldColor, GTC_CACHE); + V_DrawMappedPatch( + startx + emeraldOffsets[i][xindex], starty + emeraldOffsets[i][1], + V_HUDTRANS|splitflags, + kp_rankemerald, colormap + ); + + if (whiteFlash == true) + { + V_DrawScaledPatch( + startx + emeraldOffsets[i][xindex], starty + emeraldOffsets[i][1], + V_HUDTRANSHALF|splitflags, + kp_rankemeraldflash + ); + } + } + } +} + +INT32 K_GetTransFlagFromFixed(fixed_t value) +{ + value = std::clamp(value, FRACUNIT/2, FRACUNIT*3/2); + + // Calculate distance from 1.0 + fixed_t distance = abs(FRACUNIT - value); + + // Map the distance to 0-10 range (10 = closest to 1.0, 0 = farthest from 1.0) + INT32 transLevel = 10 - ((distance * 10) / (FRACUNIT/2)); + + // Map 0-10 to V_TRANS flags + switch (transLevel) { + case 10: return V_70TRANS; // Most transparent (closest to 1.0) + case 9: return V_60TRANS; + case 8: return V_TRANSLUCENT; + case 7: return V_40TRANS; + case 6: return V_30TRANS; + case 5: return V_20TRANS; + case 4: return V_20TRANS; + case 3: return V_10TRANS; + case 2: return V_10TRANS; + case 1: + case 0: return 0; // Fully opaque (farthest from 1.0) + default: return V_90TRANS; // Shouldn't happen, but default to most transparent + } +} + +static void K_drawKartTeamScores(void) +{ + if (G_GametypeHasTeams() == false) + { + return; + } + + for (INT32 i = TEAM_UNASSIGNED+1; i < TEAM__MAX; i++) + { + INT32 x = BASEVIDWIDTH/2; + + x += -12 + (24 * (i - 1)); + + V_DrawCenteredString(x, 5, g_teaminfo[i].chat_color, va("%d", g_teamscores[i])); + + if (stplyr->team == i) + { + UINT32 individual_score = stplyr->teamimportance; + if (gametyperules & GTR_POINTLIMIT) + { + individual_score = stplyr->roundscore; + } + + V_DrawCenteredString(x, 15, g_teaminfo[i].chat_color, va("+%d", individual_score)); + } + } +} + +static void K_drawKartLaps(void) +{ + INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; + INT32 bump = 0; + boolean drewsticker = false; + + // Jesus Christ. + // I do not understand the way this system of offsets is laid out at all, + // so it's probably going to be pretty bad to maintain. Sorry. + + if (numlaps != 1) + { + if (r_splitscreen > 1) + bump = 27; + else + bump = 40; + } + + if (numlaps != 1) + { + if (r_splitscreen > 1) + { + + INT32 fx = 0, fy = 0, fr = 0; + INT32 flipflag = 0; + + // pain and suffering defined below + if (r_splitscreen < 2) // don't change shit for THIS splitscreen. + { + fx = LAPS_X; + fy = LAPS_Y; + } + else + { + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + fx = LAPS_X; + fy = LAPS_Y; + splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + } + else // else, that means we're P2 or P4. + { + fx = LAPS2_X; + fy = LAPS2_Y; + splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + flipflag = V_FLIP; // make the string right aligned and other shit + } + } + + fr = fx; + + if (flipflag) + fr += 15; + + drewsticker = true; + K_DrawMarginSticker(fx-1-(flipflag ? 10 : 0), fy+1, 25+bump, V_HUDTRANS|V_SLIDEIN|splitflags, true, flipflag); + + V_DrawScaledPatch(fr, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_splitlapflag); + //V_DrawScaledPatch(fx+22, fy, V_HUDTRANS|V_SLIDEIN|splitflags, frameslash); + + V_DrawScaledPatch(fr+12, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[(stplyr->laps) % 10]); + V_DrawScaledPatch(fr+16, fy, V_HUDTRANS|V_SLIDEIN|splitflags, frameslash); + V_DrawScaledPatch(fr+20, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[(numlaps) % 10]); + } + else + { + K_DrawSticker(LAPS_X+13, LAPS_Y+5, 25+bump, V_HUDTRANS|V_SLIDEIN|splitflags, false); + drewsticker = true; + + // Laps + V_DrawScaledPatch(LAPS_X, LAPS_Y, V_HUDTRANS|V_SLIDEIN|splitflags, kp_lapsticker); + + using srb2::Draw; + Draw row = Draw(LAPS_X+25, LAPS_Y+3).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer); + row.text("{:01}/{:01}", std::min(stplyr->laps, numlaps), numlaps); + + // V_DrawTimerString(LAPS_X+33, LAPS_Y+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d", std::min(stplyr->laps, numlaps), numlaps)); + } + } + + UINT16 displayEXP = std::clamp(FixedMul(std::max(stplyr->exp, FRACUNIT/2), (500/K_GetNumGradingPoints())*stplyr->gradingpointnum), 0, 999); + + // EXP + if (r_splitscreen > 1) + { + INT32 fx = 0, fy = 0, fr = 0; + INT32 flipflag = 0; + + // pain and suffering defined below + if (r_splitscreen < 2) // don't change shit for THIS splitscreen. + { + fx = LAPS_X; + fy = LAPS_Y; + } + else + { + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + fx = LAPS_X+bump; + fy = LAPS_Y; + splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + } + else // else, that means we're P2 or P4. + { + fx = LAPS2_X-bump; + fy = LAPS2_Y; + splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + flipflag = V_FLIP; // make the string right aligned and other shit + } + } + + fr = fx; + + if (flipflag) + fr += 15; + + if (!drewsticker) + K_DrawMarginSticker(fr-1+(flipflag ? 2 : 0), fy+1, 25+bump, V_HUDTRANS|V_SLIDEIN|splitflags, true, flipflag); + // WHAT IS THIS? + // WHAT ARE YOU FUCKING TALKING ABOUT? + V_DrawMappedPatch(fr, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_exp[1], R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_MUSTARD, GTC_CACHE)); + auto transflag = K_GetTransFlagFromFixed(stplyr->exp); + skincolornum_t overlaycolor = stplyr->exp < FRACUNIT ? SKINCOLOR_RUBY : SKINCOLOR_ULTRAMARINE ; + auto colormap = R_GetTranslationColormap(TC_RAINBOW, overlaycolor, GTC_CACHE); + V_DrawMappedPatch(fr, fy, transflag|V_SLIDEIN|splitflags, kp_exp[1], colormap); + + // EXP + V_DrawScaledPatch(fr+11, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP/100]); + V_DrawScaledPatch(fr+15, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP/10%10]); + V_DrawScaledPatch(fr+19, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP%10]); + } + else + { + if (!drewsticker) + K_DrawSticker(LAPS_X+13, LAPS_Y+5, 25+bump, V_HUDTRANS|V_SLIDEIN|splitflags, false); + + V_DrawMappedPatch(LAPS_X+bump, LAPS_Y, V_HUDTRANS|V_SLIDEIN|splitflags, kp_exp[0], R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_MUSTARD, GTC_CACHE)); + + auto transflag = K_GetTransFlagFromFixed(stplyr->exp); + skincolornum_t overlaycolor = stplyr->exp < FRACUNIT ? SKINCOLOR_RUBY : SKINCOLOR_ULTRAMARINE ; + auto colormap = R_GetTranslationColormap(TC_RAINBOW, overlaycolor, GTC_CACHE); + V_DrawMappedPatch(LAPS_X+bump, LAPS_Y, transflag|V_SLIDEIN|splitflags, kp_exp[0], colormap); + + using srb2::Draw; + Draw row = Draw(LAPS_X+23+bump, LAPS_Y+3).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer); + row.text("{:03}", displayEXP); + } +} + +#define RINGANIM_FLIPFRAME (RINGANIM_NUMFRAMES/2) + +static void K_DrawLivesDigits(INT32 x, INT32 y, INT32 width, INT32 flags, patch_t *font[10]) +{ + const SINT8 tens = stplyr->lives / 10; + + if (tens) + { + V_DrawScaledPatch(x, y, flags, font[tens % 10]); + x += width; + } + + V_DrawScaledPatch(x, y, flags, font[stplyr->lives % 10]); +} + +static void K_drawRingCounter(boolean gametypeinfoshown) +{ + const boolean uselives = G_GametypeUsesLives(); + SINT8 ringanim_realframe = stplyr->karthud[khud_ringframe]; + INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; + UINT8 rn[2]; + INT32 ringflip = 0; + UINT8 *ringmap = NULL; + boolean colorring = false; + INT32 ringx = 0, fy = 0; + + rn[0] = ((abs(stplyr->hudrings) / 10) % 10); + rn[1] = (abs(stplyr->hudrings) % 10); + + if (stplyr->hudrings <= 0 && stplyr->ringvisualwarning > 1) + { + colorring = true; + if ((leveltime/2 & 1)) + { + ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_CRIMSON, GTC_CACHE); + } + else + { + ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_WHITE, GTC_CACHE); + } + } + else if (stplyr->hudrings <= 0 && (leveltime/5 & 1)) // In debt + { + ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_CRIMSON, GTC_CACHE); + colorring = true; + } + else if (stplyr->hudrings >= 20) // Maxed out + ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_YELLOW, GTC_CACHE); + + if (stplyr->karthud[khud_ringframe] > RINGANIM_FLIPFRAME) + { + ringflip = V_FLIP; + ringanim_realframe = RINGANIM_NUMFRAMES-stplyr->karthud[khud_ringframe]; + ringx += SHORT((r_splitscreen > 1) ? kp_smallring[ringanim_realframe]->width : kp_ring[ringanim_realframe]->width); + } + + if (r_splitscreen > 1) + { + INT32 fx = 0, fr = 0; + INT32 flipflag = 0; + + // pain and suffering defined below + if (r_splitscreen < 2) // don't change shit for THIS splitscreen. + { + fx = LAPS_X; + fy = LAPS_Y; + } + else + { + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + fx = LAPS_X; + fy = LAPS_Y; + splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + } + else // else, that means we're P2 or P4. + { + fx = LAPS2_X; + fy = LAPS2_Y; + splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + flipflag = V_FLIP; // make the string right aligned and other shit + } + } + + fr = fx; + + if (gametypeinfoshown) + { + fy -= 10; + } + + // Rings + if (!uselives) + { + V_DrawScaledPatch(fx-2 + (flipflag ? (SHORT(kp_ringstickersplit[1]->width) - 3) : 0), fy, V_HUDTRANS|V_SLIDEIN|splitflags|flipflag, kp_ringstickersplit[1]); + if (flipflag) + fr += 15; + } + else + V_DrawScaledPatch(fx-2 + (flipflag ? (SHORT(kp_ringstickersplit[0]->width) - 3) : 0), fy, V_HUDTRANS|V_SLIDEIN|splitflags|flipflag, kp_ringstickersplit[0]); + + V_DrawMappedPatch(fr+ringx, fy-3, V_HUDTRANS|V_SLIDEIN|splitflags|ringflip, kp_smallring[ringanim_realframe], (colorring ? ringmap : NULL)); + + if (stplyr->hudrings < 0) // Draw the minus for ring debt + { + V_DrawMappedPatch(fr+11, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringdebtminussmall, ringmap); + V_DrawMappedPatch(fr+15, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[rn[0]], ringmap); + V_DrawMappedPatch(fr+19, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[rn[1]], ringmap); + } + else + { + V_DrawMappedPatch(fr+11, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[rn[0]], ringmap); + V_DrawMappedPatch(fr+15, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[rn[1]], ringmap); + } + + // SPB ring lock + if (stplyr->pflags & PF_RINGLOCK) + V_DrawScaledPatch(fr-12, fy-13, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringspblocksmall[stplyr->karthud[khud_ringspblock]]); + + UINT32 greyout = V_HUDTRANS; + SINT8 superoffset = 5; + if (stplyr->superringdisplay) + { + greyout = V_HUDTRANSHALF; + if (flipflag && !uselives) + superoffset = -25 - (stplyr->superringdisplay >= 10 ? 3 : 0) - (stplyr->superringdisplay >= 100 ? 3 : 0); + } + + // Lives + if (uselives) + { + UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, static_cast(stplyr->skincolor), GTC_CACHE); + V_DrawMappedPatch(fr+21, fy-3, V_SLIDEIN|splitflags|greyout, faceprefix[stplyr->skin][FACE_MINIMAP], colormap); + if (stplyr->lives >= 0) + K_DrawLivesDigits(fr+34, fy, 4, V_SLIDEIN|splitflags|greyout, fontv[PINGNUM_FONT].font); + } + + if (stplyr->superringdisplay && !(stplyr->superringalert % 2)) + { + using srb2::Draw; + Draw row = Draw(fr+19+superoffset, fy).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kPing).colorize(SKINCOLOR_SAPPHIRE); + row.text("+{:01}", abs(stplyr->superringdisplay)); + } + } + else + { + fy = LAPS_Y; + + if (gametypeinfoshown) + { + fy -= 11; + + if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS) + fy -= 4; + } + else + { + fy += 9; + } + + // Rings + using srb2::Draw; + Draw(LAPS_X+7, fy+1) + .flags(V_HUDTRANS|V_SLIDEIN|splitflags) + .align(Draw::Align::kCenter) + .width(uselives ? (stplyr->lives >= 10 ? 70 : 64) : 33) + .small_sticker(); + + if (stplyr->overdrive) + { + V_DrawMappedPatch(LAPS_X+7-8, fy-5-8, V_HUDTRANS|V_SLIDEIN|splitflags, kp_overdrive[leveltime%32], R_GetTranslationColormap(TC_RAINBOW, static_cast(stplyr->skincolor), GTC_CACHE)); + } + else + { + V_DrawMappedPatch(LAPS_X+ringx+7, fy-5, V_HUDTRANS|V_SLIDEIN|splitflags|ringflip, kp_ring[ringanim_realframe], (colorring ? ringmap : NULL)); + + if (stplyr->amps) + { + UINT8 amplevel = std::min(stplyr->amps / AMPLEVEL, 6); + + V_DrawMappedPatch(LAPS_X+7-7, fy-5-8, V_HUDTRANS|V_SLIDEIN|splitflags, kp_amps[amplevel][leveltime%12], R_GetTranslationColormap(TC_RAINBOW, static_cast(stplyr->skincolor), GTC_CACHE)); + if (amplevel == 6) + { + V_DrawMappedPatch(LAPS_X+7-7, fy-5-8, V_ADD|V_HUDTRANS|V_SLIDEIN|splitflags, kp_amps_underlay[leveltime%12], R_GetTranslationColormap(TC_RAINBOW, static_cast(stplyr->skincolor), GTC_CACHE)); + } + } + } + + // "Why fy-4? Why LAPS_X+29+1?" + // "use magic numbers" - jartha 2024-03-05 + if (stplyr->hudrings < 0) // Draw the minus for ring debt + { + V_DrawMappedPatch(LAPS_X+23-1, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringdebtminus, ringmap); + using srb2::Draw; + Draw row = Draw(LAPS_X+29+0, fy-4).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer).colormap(ringmap); + row.text("{:02}", abs(stplyr->hudrings)); + // V_DrawMappedPatch(LAPS_X+29, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[TALLNUM_FONT].font[rn[0]], ringmap); + // V_DrawMappedPatch(LAPS_X+35, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[TALLNUM_FONT].font[rn[1]], ringmap); + } + else + { + using srb2::Draw; + Draw row = Draw(LAPS_X+23+3, fy-4).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer).colormap(ringmap); + row.text("{:02}", abs(stplyr->hudrings)); + // V_DrawMappedPatch(LAPS_X+23, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[TALLNUM_FONT].font[rn[0]], ringmap); + // V_DrawMappedPatch(LAPS_X+29, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[TALLNUM_FONT].font[rn[1]], ringmap); + } + + // SPB ring lock + if (stplyr->pflags & PF_RINGLOCK) + V_DrawScaledPatch(LAPS_X-5, fy-17, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringspblock[stplyr->karthud[khud_ringspblock]]); + + UINT32 greyout = V_HUDTRANS; + + if (stplyr->superringdisplay) + { + greyout = V_HUDTRANSHALF; + } + + // Lives + if (uselives) + { + UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, static_cast(stplyr->skincolor), GTC_CACHE); + V_DrawMappedPatch(LAPS_X+46, fy-5, V_SLIDEIN|splitflags|greyout, faceprefix[stplyr->skin][FACE_RANK], colormap); + SINT8 livescount = 0; + if (stplyr->lives > 0) + { + livescount = stplyr->lives; + if (livescount > 10) + livescount = 10; + } + using srb2::Draw; + Draw row = Draw(LAPS_X+65, fy-4).flags(V_SLIDEIN|splitflags|greyout).font(Draw::Font::kThinTimer); + row.text("{}", livescount); + } + + if (stplyr->superringdisplay && !(stplyr->superringalert % 2)) + { + using srb2::Draw; + Draw row = Draw(LAPS_X+23+3+15, fy-4).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer).colorize(SKINCOLOR_SAPPHIRE); + row.text("+{:01}", abs(stplyr->superringdisplay)); + } + } +} + +#undef RINGANIM_FLIPFRAME + +static void K_drawKartAccessibilityIcons(boolean gametypeinfoshown, INT32 fx) +{ + INT32 fy = LAPS_Y-14; + INT32 splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + + boolean mirror = false; + + fx += LAPS_X; + + if (r_splitscreen < 2) // adjust to speedometer height + { + if (battleprisons) + { + fy -= 2; + } + + if (gametypeinfoshown) + { + fy -= 11; + + if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS) + fy -= 4; + } + else + { + fy += 9; + } + } + else + { + fx = LAPS_X+44; + fy = LAPS_Y; + if (R_GetViewNumber() & 1) // If we are not P1 or P3... + { + splitflags ^= (V_SNAPTOLEFT|V_SNAPTORIGHT); + fx = (BASEVIDWIDTH/2) - fx; + mirror = true; + } + } + + // Kickstart Accel + if (stplyr->pflags & PF_KICKSTARTACCEL) + { + if (mirror) + fx -= 10; + + SINT8 col = 0, wid, fil, ofs; + UINT8 i = 7; + ofs = (stplyr->kickstartaccel == ACCEL_KICKSTART) ? 1 : 0; + fil = i-(stplyr->kickstartaccel*i)/ACCEL_KICKSTART; + + V_DrawFill(fx+4, fy+ofs-1, 2, 1, 31|V_SLIDEIN|splitflags); + V_DrawFill(fx, (fy+ofs-1)+8, 10, 1, 31|V_SLIDEIN|splitflags); + + while (i--) + { + wid = (i/2)+1; + V_DrawFill(fx+4-wid, fy+ofs+i, 2+(wid*2), 1, 31|V_SLIDEIN|splitflags); + if (fil > 0) + { + if (i < fil) + col = 23; + else if (i == fil) + col = 3; + else + col = 5 + (i-fil)*2; + } + else if ((leveltime % 7) == i) + col = 0; + else + col = 3; + V_DrawFill(fx+5-wid, fy+ofs+i, (wid*2), 1, col|V_SLIDEIN|splitflags); + } + + if (mirror) + fx--; + else + fx += 10 + 1; + } + + // Auto Roulette + if (stplyr->pflags & PF_AUTOROULETTE) + { + if (mirror) + fx -= 12; + + V_DrawScaledPatch(fx, fy-1, V_SLIDEIN|splitflags, kp_autoroulette); + + if (mirror) + fx--; + else + fx += 12 + 1; + } + + if (stplyr->pflags & PF_AUTORING) + { + if (mirror) + fx -= 14; + + V_DrawScaledPatch(fx, fy-1, V_SLIDEIN|splitflags, kp_autoring); + + if (mirror) + fx--; + else + fx += 14 + 1; + } +} + +static void K_drawKartSpeedometer(boolean gametypeinfoshown) +{ + static fixed_t convSpeed; + UINT8 labeln = 0; + UINT8 numbers[3]; + INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; + INT32 fy = LAPS_Y-14; + + if (battleprisons) + { + fy -= 2; + } + + if (!stplyr->exiting) // Keep the same speed value as when you crossed the finish line! + { + switch (cv_kartspeedometer.value) + { + case 1: // Sonic Drift 2 style percentage + default: + convSpeed = (stplyr->speed * 100) / K_GetKartSpeed(stplyr, false, true); // Based on top speed! + labeln = 0; + break; + case 2: // Kilometers + convSpeed = FixedDiv(FixedMul(stplyr->speed, 142371), mapobjectscale) / FRACUNIT; // 2.172409058 + labeln = 1; + break; + case 3: // Miles + convSpeed = FixedDiv(FixedMul(stplyr->speed, 88465), mapobjectscale) / FRACUNIT; // 1.349868774 + labeln = 2; + break; + case 4: // Fracunits + convSpeed = FixedDiv(stplyr->speed, mapobjectscale) / FRACUNIT; // 1.0. duh. + labeln = 3; + break; + } + } + + // Don't overflow + // (negative speed IS really high speed :V) + if (convSpeed > 999 || convSpeed < 0) + convSpeed = 999; + + numbers[0] = ((convSpeed / 100) % 10); + numbers[1] = ((convSpeed / 10) % 10); + numbers[2] = (convSpeed % 10); + + if (gametypeinfoshown) + { + fy -= 11; + + if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS) + fy -= 4; + } + else + { + fy += 9; + } + + using srb2::Draw; + Draw(LAPS_X+7, fy+1).flags(V_HUDTRANS|V_SLIDEIN|splitflags).align(Draw::Align::kCenter).width(42).small_sticker(); + V_DrawScaledPatch(LAPS_X+7, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[numbers[0]]); + V_DrawScaledPatch(LAPS_X+13, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[numbers[1]]); + V_DrawScaledPatch(LAPS_X+19, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[numbers[2]]); + V_DrawScaledPatch(LAPS_X+29, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_speedometerlabel[labeln]); + + K_drawKartAccessibilityIcons(gametypeinfoshown, 56); +} + +static void K_drawBlueSphereMeter(boolean gametypeinfoshown) +{ + const UINT8 maxBars = 4; + // see also K_DrawNameTagSphereMeter + const UINT8 segColors[] = {73, 64, 52, 54, 55, 35, 34, 33, 202, 180, 181, 182, 164, 165, 166, 153, 152}; + const UINT8 sphere = std::clamp(static_cast(stplyr->spheres), 0, 40); + + UINT8 numBars = std::min((sphere / 10), +maxBars); + UINT8 colorIndex = (sphere * sizeof(segColors)) / (40 + 1); + INT32 fx, fy; + UINT8 i; + INT32 splitflags = V_HUDTRANS|V_SLIDEIN|V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; + INT32 flipflag = 0; + INT32 xstep = 15; + + // pain and suffering defined below + if (r_splitscreen < 2) // don't change shit for THIS splitscreen. + { + fx = LAPS_X; + fy = LAPS_Y-4; + + if (battleprisons) + { + if (r_splitscreen == 1) + { + fy -= 8; + } + else + { + fy -= 5; + } + } + else if (r_splitscreen == 1) + { + fy -= 5; + } + + if (gametypeinfoshown) + { + fy -= 11 + 4; + } + else + { + fy += 9; + } + + V_DrawScaledPatch(fx, fy, splitflags|flipflag, kp_spheresticker); + } + else + { + xstep = 8; + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + fx = LAPS_X-2; + fy = LAPS_Y; + } + else // else, that means we're P2 or P4. + { + fx = LAPS2_X+(SHORT(kp_splitspheresticker->width) - 10); + fy = LAPS2_Y; + splitflags ^= V_SNAPTOLEFT|V_SNAPTORIGHT; + flipflag = V_FLIP; // make the string right aligned and other shit + xstep = -xstep; + } + + if (battleprisons) + { + fy -= 5; + } + + if (gametypeinfoshown) + { + fy -= 16; + } + + V_DrawScaledPatch(fx, fy, splitflags|flipflag, kp_splitspheresticker); + } + + if (r_splitscreen < 2) + { + fx += 25; + } + else + { + fx += (flipflag) ? -18 : 13; + } + + for (i = 0; i <= numBars; i++) + { + UINT8 segLen = (r_splitscreen < 2) ? 10 : 5; + + if (i == numBars) + { + segLen = (sphere % 10); + if (r_splitscreen < 2) + ; + else + { + segLen = (segLen+1)/2; // offset so nonzero spheres shows up IMMEDIATELY + if (!segLen) + break; + if (flipflag) + fx += (5-segLen); + } + } + + if (r_splitscreen < 2) + { + V_DrawFill(fx, fy + 6, segLen, 3, segColors[std::max(colorIndex-1, 0)] | splitflags); + V_DrawFill(fx, fy + 7, segLen, 1, segColors[std::max(colorIndex-2, 0)] | splitflags); + V_DrawFill(fx, fy + 9, segLen, 3, segColors[colorIndex] | splitflags); + } + else + { + V_DrawFill(fx, fy + 5, segLen, 1, segColors[std::max(colorIndex-1, 0)] | splitflags); + V_DrawFill(fx, fy + 6, segLen, 1, segColors[std::max(colorIndex-2, 0)] | splitflags); + V_DrawFill(fx, fy + 7, segLen, 2, segColors[colorIndex] | splitflags); + } + + fx += xstep; + } +} + +static void K_drawKartBumpersOrKarma(void) +{ + UINT8 *colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(stplyr->skincolor), GTC_CACHE); + INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; + + if (r_splitscreen > 1) + { + INT32 fx = 0, fy = 0; + INT32 flipflag = 0; + + // pain and suffering defined below + if (r_splitscreen < 2) // don't change shit for THIS splitscreen. + { + fx = LAPS_X; + fy = LAPS_Y; + } + else + { + if (!(R_GetViewNumber() & 1)) // If we are P1 or P3... + { + fx = LAPS_X; + fy = LAPS_Y; + splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + } + else // else, that means we're P2 or P4. + { + fx = LAPS2_X; + fy = LAPS2_Y; + splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN; + flipflag = V_FLIP; // make the string right aligned and other shit + } + } + + { + using srb2::Draw; + int width = 39; + if (!battleprisons) + { + constexpr int kPad = 16; + if (flipflag) + fx -= kPad; + width += kPad; + } + Draw(fx-1 + (flipflag ? width + 3 : 0), fy+1) + .flags(V_HUDTRANS|V_SLIDEIN|splitflags) + .align(flipflag ? Draw::Align::kRight : Draw::Align::kLeft) + .width(width) + .small_sticker(); + } + + fx += 2; + + if (battleprisons) + { + V_DrawScaledPatch(fx+22, fy, V_HUDTRANS|V_SLIDEIN|splitflags, frameslash); + V_DrawMappedPatch(fx-1, fy-2, V_HUDTRANS|V_SLIDEIN|splitflags, kp_rankcapsule, NULL); + + if (numtargets > 9 || maptargets > 9) + { + UINT8 ln[2]; + ln[0] = ((numtargets / 10) % 10); + ln[1] = (numtargets % 10); + + V_DrawScaledPatch(fx+13, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[ln[0]]); + V_DrawScaledPatch(fx+17, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[ln[1]]); + + ln[0] = ((maptargets / 10) % 10); + ln[1] = (maptargets % 10); + + V_DrawScaledPatch(fx+27, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[ln[0]]); + V_DrawScaledPatch(fx+31, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[ln[1]]); + } + else + { + V_DrawScaledPatch(fx+13, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[numtargets % 10]); + V_DrawScaledPatch(fx+27, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[maptargets % 10]); + } + } + else + { + const UINT8 bumpers = K_Bumpers(stplyr); + const bool dance = g_pointlimit && (g_pointlimit <= stplyr->roundscore); + + V_DrawMappedPatch(fx-1, fy-2, V_HUDTRANS|V_SLIDEIN|splitflags, kp_rankbumper, colormap); + + using srb2::Draw; + Draw row = Draw(fx+12, fy).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kPing); + row.text("{:02}", bumpers); + if (dance && leveltime % 8 < 4) + { + row = row.colorize(SKINCOLOR_TANGERINE); + } + row.xy(10, -2).patch(kp_pts[1]); + row + .x(31) + .flags(dance ? V_STRINGDANCE : 0) + .text("{:02}", stplyr->roundscore); + } + } + else + { + INT32 fy = r_splitscreen == 1 ? LAPS_Y-3 : LAPS_Y; + + if (battleprisons) + { + if (numtargets > 9 && maptargets > 9) + V_DrawMappedPatch(LAPS_X, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_capsulestickerwide, NULL); + else + V_DrawMappedPatch(LAPS_X, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_capsulesticker, NULL); + V_DrawTimerString(LAPS_X+47, fy+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d", numtargets, maptargets)); + } + else + { + const UINT8 bumpers = K_Bumpers(stplyr); + const bool dance = g_pointlimit && (g_pointlimit <= stplyr->roundscore); + + if (r_splitscreen == 0) + { + fy += 2; + } + + K_DrawSticker(LAPS_X+12, fy+5, 75, V_HUDTRANS|V_SLIDEIN|splitflags, false); + V_DrawMappedPatch(LAPS_X+12, fy-2, V_HUDTRANS|V_SLIDEIN|splitflags, kp_bigbumper, colormap); + + using srb2::Draw; + Draw row = Draw(LAPS_X+12+23+1, fy+3).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer); + row.text("{:02}", bumpers); + if (dance && leveltime % 8 < 4) + { + row = row.colorize(SKINCOLOR_TANGERINE); + } + row.xy(12, -2).patch(kp_pts[0]); + row + .x(12+27) + .flags(dance ? V_STRINGDANCE : 0) + .text("{:02}", stplyr->roundscore); + } + } +} + +#if 0 +static void K_drawKartWanted(void) +{ + UINT8 i, numwanted = 0; + UINT8 *colormap = NULL; + INT32 basex = 0, basey = 0; + + if (!splitscreen) + return; + + if (stplyr != &players[displayplayers[0]]) + return; + + for (i = 0; i < 4; i++) + { + if (battlewanted[i] == -1) + break; + numwanted++; + } + + if (numwanted <= 0) + return; + + // set X/Y coords depending on splitscreen. + if (r_splitscreen < 3) // 1P and 2P use the same code. + { + basex = WANT_X; + basey = WANT_Y; + if (r_splitscreen == 2) + { + basey += 16; // slight adjust for 3P + basex -= 6; + } + } + else if (r_splitscreen == 3) // 4P splitscreen... + { + basex = BASEVIDWIDTH/2 - (SHORT(kp_wantedsplit->width)/2); // center on screen + basey = BASEVIDHEIGHT - 55; + //basey2 = 4; + } + + if (battlewanted[0] != -1) + colormap = R_GetTranslationColormap(TC_DEFAULT, players[battlewanted[0]].skincolor, GTC_CACHE); + V_DrawFixedPatch(basex< 1 ? kp_wantedsplit : kp_wanted), colormap); + /*if (basey2) + V_DrawFixedPatch(basex< 1 ? 13 : 8), y = basey+(r_splitscreen > 1 ? 16 : 21); + fixed_t scale = FRACUNIT/2; + player_t *p = &players[battlewanted[i]]; + + if (battlewanted[i] == -1) + break; + + if (numwanted == 1) + scale = FRACUNIT; + else + { + if (i & 1) + x += 16; + if (i > 1) + y += 16; + } + + if (players[battlewanted[i]].skincolor) + { + colormap = R_GetTranslationColormap(TC_RAINBOW, p->skincolor, GTC_CACHE); + V_DrawFixedPatch(x<skin][FACE_WANTED] : faceprefix[p->skin][FACE_RANK]), colormap); + /*if (basey2) // again with 4p stuff + V_DrawFixedPatch(x<skin][FACE_WANTED] : faceprefix[p->skin][FACE_RANK]), colormap);*/ + } + } +} +#endif //if 0 + +static void K_drawKartPlayerCheck(void) +{ + const fixed_t maxdistance = FixedMul(1280 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)); + UINT8 i; + INT32 splitflags = V_SNAPTOBOTTOM|V_SPLITSCREEN; + fixed_t y = CHEK_Y * FRACUNIT; + + if (stplyr == NULL || stplyr->mo == NULL || P_MobjWasRemoved(stplyr->mo)) + { + return; + } + + if (stplyr->spectator || stplyr->awayview.tics) + { + return; + } + + if (stplyr->cmd.buttons & BT_LOOKBACK) + { + return; + } + + for (i = 0; i < MAXPLAYERS; i++) + { + player_t *checkplayer = &players[i]; + fixed_t distance = maxdistance+1; + UINT8 *colormap = NULL; + UINT8 pnum = 0; + vector3_t v; + vector3_t pPos; + trackingResult_t result; + + if (!playeringame[i] || checkplayer->spectator) + { + // Not in-game + continue; + } + + if (checkplayer->mo == NULL || P_MobjWasRemoved(checkplayer->mo)) + { + // No object + continue; + } + + if (checkplayer == stplyr) + { + // This is you! + continue; + } + + v.x = R_InterpolateFixed(checkplayer->mo->old_x, checkplayer->mo->x); + v.y = R_InterpolateFixed(checkplayer->mo->old_y, checkplayer->mo->y); + v.z = R_InterpolateFixed(checkplayer->mo->old_z, checkplayer->mo->z); + + pPos.x = R_InterpolateFixed(stplyr->mo->old_x, stplyr->mo->x); + pPos.y = R_InterpolateFixed(stplyr->mo->old_y, stplyr->mo->y); + pPos.z = R_InterpolateFixed(stplyr->mo->old_z, stplyr->mo->z); + + distance = R_PointToDist2(pPos.x, pPos.y, v.x, v.y); + + if (distance > maxdistance) + { + // Too far away + continue; + } + + if ((checkplayer->invincibilitytimer <= 0) && (leveltime & 2)) + { + pnum++; // white frames + } + + if (checkplayer->itemtype == KITEM_GROW || checkplayer->growshrinktimer > 0) + { + pnum += 4; + } + else if (checkplayer->itemtype == KITEM_INVINCIBILITY || checkplayer->invincibilitytimer) + { + pnum += 2; + } + + K_ObjectTracking(&result, &v, true); + + if (result.onScreen == true) + { + colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(checkplayer->mo->color), GTC_CACHE); + V_DrawFixedPatch(result.x, y, FRACUNIT, V_HUDTRANS|V_SPLITSCREEN|splitflags, kp_check[pnum], colormap); + } + } +} + +static boolean K_ShowPlayerNametag(player_t *p) +{ + if (cv_seenames.value == 0) + { + return false; + } + + if (demo.playback == true && camera[R_GetViewNumber()].freecam == true) + { + return true; + } + + if (stplyr == p) + { + return false; + } + + if (gametyperules & GTR_CIRCUIT) + { + if ((p->position == 0) + || (stplyr->position == 0) + || (p->position < stplyr->position-2) + || (p->position > stplyr->position+2)) + { + return false; + } + } + + return true; +} + +static void K_DrawTypingDot(fixed_t x, fixed_t y, UINT8 duration, player_t *p, INT32 flags) +{ + if (p->typing_duration > duration) + { + V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_typdot, NULL); + } +} + +static void K_DrawTypingNotifier(fixed_t x, fixed_t y, player_t *p, INT32 flags) +{ + int playernum = p - players; + if (p->cmd.flags & TICCMD_TYPING || S_IsPlayerVoiceActive(playernum)) + { + V_DrawFixedPatch(x, y, FRACUNIT, V_SPLITSCREEN|flags, kp_talk, NULL); + } + if (p->cmd.flags & TICCMD_TYPING) + { + /* spacing closer with the last two looks a better most of the time */ + K_DrawTypingDot(x + 3*FRACUNIT, y, 15, p, flags); + K_DrawTypingDot(x + 6*FRACUNIT - FRACUNIT/3, y, 31, p, flags); + K_DrawTypingDot(x + 9*FRACUNIT - FRACUNIT/3, y, 47, p, flags); + } + else if (S_IsPlayerVoiceActive(playernum)) + { + patch_t* voxmic = kp_voice_tagactive[(leveltime / 3) % 3]; + V_DrawFixedPatch(x + 6*FRACUNIT, y - 12*FRACUNIT, FRACUNIT, V_SPLITSCREEN|flags, voxmic, NULL); + } +} + +// see also K_drawKartItem +static void K_DrawNameTagItemSpy(INT32 x, INT32 y, player_t *p, INT32 flags) +{ + using srb2::Draw; + bool tiny = r_splitscreen > 1; + SINT8 flip = 1, flipboxoffset = 0; + if ((flags & V_VFLIP) == V_VFLIP) + { + // Remove the v_vflip flag - it makes things messy, but we also understand + // that we want to make this look okay for flipped players, so simply use this + // opportunity to flip vertical offsets accordingly instead. + flags &= ~V_VFLIP; + flip = P_MobjFlip(p->mo); + flipboxoffset = 8; + } + + Draw bar = Draw(x, y).flags(V_NOSCALESTART|flags); + Draw box = tiny ? bar.xy(-22 * vid.dupx, (-17+flipboxoffset) * vid.dupy) : bar.xy(-40 * vid.dupx, (-26+flipboxoffset) * vid.dupy); + + box.colorize(p->skincolor).patch(kp_itembg[tiny ? 4 : 2]); + + INT32 item_type = KITEM_NONE; + + if (p->itemRoulette.active == true && p->itemRoulette.ringbox == false) + { + item_type = 1 + (leveltime % (NUMKARTITEMS - 1)); + } + else if (!(p->itemflags & IF_ITEMOUT) || (leveltime & 1)) + { + item_type = p->itemtype; + } + + switch (item_type) + { + case KITEM_NONE: + { + break; + } + + case KITEM_INVINCIBILITY: + { + box.patch(kp_invincibility[((leveltime % (6*3)) / 3) + (tiny ? 13 : 7)]); + break; + } + + case KITEM_ORBINAUT: + { + box.patch(kp_orbinaut[4 + tiny]); + break; + } + + default: + { + if (patch_t *ico = K_GetCachedItemPatch(item_type, 1 + tiny)) + { + box.patch(ico); + } + break; + } + } + + if (p->itemamount > 1) + { + (tiny ? + bar.xy(-3 * vid.dupx, (-4*flip) * vid.dupy).font(Draw::Font::kPing) : + bar.xy(-4 * vid.dupx, (-2*flip) * vid.dupy).font(Draw::Font::kThinTimer) + ) + .align(Draw::Align::kRight) + .text("{}", p->itemamount); + } +} + +static void K_DrawNameTagSphereMeter(INT32 x, INT32 y, INT32 width, INT32 spheres, INT32 flags) +{ + using srb2::Draw; + Draw bar = Draw(x + vid.dupx, y).flags(V_NOSCALESTART).height(vid.dupy); + + // see also K_drawBlueSphereMeter + const UINT8 segColors[] = {73, 64, 52, 54, 55, 35, 34, 33, 202, 180, 181, 182, 164, 165, 166, 153, 152}; + + spheres = std::clamp(spheres, 0, 40); + int colorIndex = (spheres * sizeof segColors) / (40 + 1); + + int px = r_splitscreen > 1 ? 1 : 2; + int b = 10 * px; + int m = spheres * px; + + while (m > 0) + { + if (b > m) + b = m; + + Draw seg = bar.width(b); + + seg.fill(segColors[std::max(colorIndex - 1, 0)]); + seg.y(vid.dupy).fill(segColors[std::max(colorIndex - 2, 0)]); + seg.y(2 * vid.dupy).height(2 * vid.dupy).fill(segColors[colorIndex]); + seg.y(4 * vid.dupy).fill(31); + + bar = bar.x(b + vid.dupx); + m -= b; + } +} + +static void K_DrawLocalTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT8 id, UINT32 flags) +{ + UINT8 blink = ((leveltime / 7) & 1); + UINT8 *colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(p->skincolor), GTC_CACHE); + + V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_localtag[id][blink], colormap); +} + +static void K_DrawRivalTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT32 flags) +{ + if ((p->itemtype != KITEM_NONE && p->itemamount != 0) + || (p->itemRoulette.active == true && p->itemRoulette.ringbox == false)) + { + INT32 barx = 0, bary = 0; + + barx = (x * vid.dupx) / FRACUNIT; + bary = (y * vid.dupy) / FRACUNIT; + + barx += (16 * vid.dupx); + bary -= (25 * vid.dupx); + + // Center it if necessary + if (vid.width != BASEVIDWIDTH * vid.dupx) + { + barx += (vid.width - (BASEVIDWIDTH * vid.dupx)) / 2; + } + + if (vid.height != BASEVIDHEIGHT * vid.dupy) + { + bary += (vid.height - (BASEVIDHEIGHT * vid.dupy)) / 2; + } + + K_DrawNameTagItemSpy(barx, bary, p, flags); + } + + UINT8 blink = ((leveltime / 7) & 1); + V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_rival[blink], NULL); +} + +static void K_DrawCPUTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT32 flags) +{ + if ((p->itemtype != KITEM_NONE && p->itemamount != 0) + || (p->itemRoulette.active == true && p->itemRoulette.ringbox == false)) + { + INT32 barx = 0, bary = 0; + + barx = (x * vid.dupx) / FRACUNIT; + bary = (y * vid.dupy) / FRACUNIT; + + barx += (16 * vid.dupx); + bary -= (25 * vid.dupx); + + // Center it if necessary + if (vid.width != BASEVIDWIDTH * vid.dupx) + { + barx += (vid.width - (BASEVIDWIDTH * vid.dupx)) / 2; + } + + if (vid.height != BASEVIDHEIGHT * vid.dupy) + { + bary += (vid.height - (BASEVIDHEIGHT * vid.dupy)) / 2; + } + + K_DrawNameTagItemSpy(barx, bary, p, flags); + } + + UINT8 blink = ((leveltime / 7) & 1); + V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_cpu[blink], NULL); +} + +static void K_DrawNameTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT32 flags) +{ + const INT32 clr = skincolors[p->skincolor].chatcolor; + const INT32 namelen = V_ThinStringWidth(player_names[p - players], 0); + + UINT8 *colormap = V_GetStringColormap(clr); + INT32 barx = 0, bary = 0, barw = 0; + INT32 flipped = P_MobjFlip(p->mo), flipfilloffset = 0, flipfontoffset = 0, flipspheresoffset = 0; + if (flipped == -1) + { + flipfilloffset = -3; // You cannot really flip drawfill. + flipfontoffset = -9; // Accounts for font height. + flipspheresoffset = 2; + } + + UINT8 cnum = R_GetViewNumber(); + + // Since there's no "V_DrawFixedFill", and I don't feel like making it, + // fuck it, we're gonna just V_NOSCALESTART hack it + if (r_splitscreen > 1 && cnum & 1) + { + x += (BASEVIDWIDTH/2) * FRACUNIT; + } + + if ((r_splitscreen == 1 && cnum == 1) + || (r_splitscreen > 1 && cnum > 1)) + { + y += (BASEVIDHEIGHT/2) * FRACUNIT; + } + + barw = (namelen * vid.dupx); + + barx = (x * vid.dupx) / FRACUNIT; + bary = (y * vid.dupy) / FRACUNIT; + + barx += (6 * vid.dupx); + bary -= ((16 + flipfilloffset) * vid.dupx) * flipped; + + // Center it if necessary + if (vid.width != BASEVIDWIDTH * vid.dupx) + { + barx += (vid.width - (BASEVIDWIDTH * vid.dupx)) / 2; + } + + if (vid.height != BASEVIDHEIGHT * vid.dupy) + { + bary += (vid.height - (BASEVIDHEIGHT * vid.dupy)) / 2; + } + + // see also K_CullTargetList + if ((p->itemtype != KITEM_NONE && p->itemamount != 0) + || (p->itemRoulette.active == true && p->itemRoulette.ringbox == false)) + { + K_DrawNameTagItemSpy(barx, bary, p, flags); + } + + if (gametyperules & GTR_SPHERES) + { + K_DrawNameTagSphereMeter(barx, bary + (((4 + flipspheresoffset) * vid.dupy) * P_MobjFlip(p->mo)), barw, p->spheres, flags); + } + + // Lat: 10/06/2020: colormap can be NULL on the frame you join a game, just arbitrarily use palette indexes 31 and 0 instead of whatever the colormap would give us instead to avoid crashes. + V_DrawFill(barx, bary, barw, (3 * vid.dupy), (colormap ? colormap[31] : 31)|V_NOSCALESTART|flags); + V_DrawFill(barx, bary + vid.dupy, barw, vid.dupy, (colormap ? colormap[0] : 0)|V_NOSCALESTART|flags); + // END DRAWFILL DUMBNESS + + // Draw the stem + V_DrawFixedPatch(x, y, FRACUNIT, flags, kp_nametagstem, colormap); + + // Draw the name itself + V_DrawThinStringAtFixed(x + (5*FRACUNIT), y - (((26 + flipfontoffset) * FRACUNIT) * P_MobjFlip(p->mo)), clr|flags, player_names[p - players]); +} + +playertagtype_t K_WhichPlayerTag(player_t *p) +{ + UINT8 cnum = R_GetViewNumber(); + + if (!(demo.playback == true && camera[cnum].freecam == true) && P_IsDisplayPlayer(p) && + p != &players[displayplayers[cnum]]) + { + return PLAYERTAG_LOCAL; + } + else if (p->bot) + { + if (p->botvars.rival == true || cv_levelskull.value) + { + return PLAYERTAG_RIVAL; + } + else if (K_ShowPlayerNametag(p) == true) + { + return PLAYERTAG_CPU; + } + } + else if (netgame || demo.playback) + { + if (K_ShowPlayerNametag(p) == true) + { + return PLAYERTAG_NAME; + } + } + + return PLAYERTAG_NONE; +} + +void K_DrawPlayerTag(fixed_t x, fixed_t y, player_t *p, playertagtype_t type, boolean foreground) +{ + INT32 flags = P_IsObjectFlipped(p->mo) ? V_VFLIP : 0; + + switch (type) + { + case PLAYERTAG_LOCAL: + flags |= V_SPLITSCREEN; + K_DrawLocalTagForPlayer(x, y, p, G_PartyPosition(p - players), flags); + break; + + case PLAYERTAG_RIVAL: + flags |= V_SPLITSCREEN; + K_DrawRivalTagForPlayer(x, y, p, flags); + break; + + case PLAYERTAG_CPU: + flags |= V_SPLITSCREEN; + flags |= foreground ? 0 : V_60TRANS; + K_DrawCPUTagForPlayer(x, y, p, flags); + break; + + case PLAYERTAG_NAME: + flags |= foreground ? 0 : V_60TRANS; + K_DrawNameTagForPlayer(x, y, p, flags); + K_DrawTypingNotifier(x, y, p, flags); + break; + + default: + break; + } +} + +typedef struct weakspotdraw_t +{ + UINT8 i; + INT32 x; + INT32 y; + boolean candrawtag; +} weakspotdraw_t; + +static void K_DrawWeakSpot(weakspotdraw_t *ws) +{ + UINT8 *colormap; + UINT8 j = (bossinfo.weakspots[ws->i].type == SPOT_BUMP) ? 1 : 0; + tic_t flashtime = ~1; // arbitrary high even number + + if (bossinfo.weakspots[ws->i].time < TICRATE) + { + if (bossinfo.weakspots[ws->i].time & 1) + return; + + flashtime = bossinfo.weakspots[ws->i].time; + } + else if (bossinfo.weakspots[ws->i].time > (WEAKSPOTANIMTIME - TICRATE)) + flashtime = WEAKSPOTANIMTIME - bossinfo.weakspots[ws->i].time; + + if (flashtime & 1) + colormap = R_GetTranslationColormap(TC_ALLWHITE, SKINCOLOR_NONE, GTC_CACHE); + else + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(bossinfo.weakspots[ws->i].color), GTC_CACHE); + + V_DrawFixedPatch(ws->x, ws->y, FRACUNIT, 0, kp_bossret[j], colormap); + + if (!ws->candrawtag || flashtime & 1 || flashtime < TICRATE/2) + return; + + V_DrawFixedPatch(ws->x, ws->y, FRACUNIT, 0, kp_bossret[j+1], colormap); +} + +static void K_drawKartNameTags(void) +{ + vector3_t c; + UINT8 cnum = R_GetViewNumber(); + size_t i, j; + + if (stplyr == NULL || stplyr->mo == NULL || P_MobjWasRemoved(stplyr->mo)) + { + return; + } + + if (stplyr->awayview.tics) + { + return; + } + + // Crop within splitscreen bounds + switch (r_splitscreen) + { + case 1: + V_SetClipRect( + 0, + cnum == 1 ? (BASEVIDHEIGHT / 2) * FRACUNIT : 0, + BASEVIDWIDTH * FRACUNIT, + (BASEVIDHEIGHT / 2) * FRACUNIT, + 0 + ); + break; + + case 2: + case 3: + V_SetClipRect( + cnum & 1 ? (BASEVIDWIDTH / 2) * FRACUNIT : 0, + cnum > 1 ? (BASEVIDHEIGHT / 2) * FRACUNIT : 0, + (BASEVIDWIDTH / 2) * FRACUNIT, + (BASEVIDHEIGHT / 2) * FRACUNIT, + 0 + ); + break; + } + + c.x = viewx; + c.y = viewy; + c.z = viewz; + + // Maybe shouldn't be handling this here... but the camera info is too good. + if (bossinfo.valid == true) + { + weakspotdraw_t weakspotdraw[NUMWEAKSPOTS]; + UINT8 numdraw = 0; + boolean onleft = false; + + for (i = 0; i < NUMWEAKSPOTS; i++) + { + trackingResult_t result; + vector3_t v; + + if (bossinfo.weakspots[i].spot == NULL || P_MobjWasRemoved(bossinfo.weakspots[i].spot)) + { + // No object + continue; + } + + if (bossinfo.weakspots[i].time == 0 || bossinfo.weakspots[i].type == SPOT_NONE) + { + // not visible + continue; + } + + v.x = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_x, bossinfo.weakspots[i].spot->x); + v.y = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_y, bossinfo.weakspots[i].spot->y); + v.z = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_z, bossinfo.weakspots[i].spot->z); + + v.z += (bossinfo.weakspots[i].spot->height / 2); + + K_ObjectTracking(&result, &v, false); + if (result.onScreen == false) + { + continue; + } + + weakspotdraw[numdraw].i = i; + weakspotdraw[numdraw].x = result.x; + weakspotdraw[numdraw].y = result.y; + weakspotdraw[numdraw].candrawtag = true; + + for (j = 0; j < numdraw; j++) + { + if (abs(weakspotdraw[j].x - weakspotdraw[numdraw].x) > 50*FRACUNIT) + { + continue; + } + + onleft = (weakspotdraw[j].x < weakspotdraw[numdraw].x); + + if (abs((onleft ? -5 : 5) + + weakspotdraw[j].y - weakspotdraw[numdraw].y) > 18*FRACUNIT) + { + continue; + } + + if (weakspotdraw[j].x < weakspotdraw[numdraw].x) + { + weakspotdraw[j].candrawtag = false; + break; + } + + weakspotdraw[numdraw].candrawtag = false; + break; + } + + numdraw++; + } + + for (i = 0; i < numdraw; i++) + { + K_DrawWeakSpot(&weakspotdraw[i]); + } + } + + K_drawTargetHUD(&c, stplyr); + + V_ClearClipRect(); +} + +#define PROGRESSION_BAR_WIDTH 120 + +static INT32 K_getKartProgressionMinimapDistance(UINT32 distancetofinish) +{ + INT32 dist; + + if (specialstageinfo.maxDist == 0U) + { + return 0; + } + + dist = specialstageinfo.maxDist/PROGRESSION_BAR_WIDTH; + + dist = (specialstageinfo.maxDist-distancetofinish)/dist; + + if (dist > PROGRESSION_BAR_WIDTH) + { + return PROGRESSION_BAR_WIDTH; + } + + if (dist < 0) + { + return 0; + } + + return dist; +} + +static void K_drawKartProgressionMinimapIcon(UINT32 distancetofinish, INT32 hudx, INT32 hudy, INT32 flags, patch_t *icon, UINT8 *colormap) +{ + if (distancetofinish == UINT32_MAX) + return; + + hudx += K_getKartProgressionMinimapDistance(distancetofinish); + + hudx = ((hudx - (SHORT(icon->width)/2))<height)/2))<width))/2)<height))/2)<nextwaypoint) + { + return 4; + } + else if (wp->numnextwaypoints == 0 || wp->numprevwaypoints == 0) + { + return 3; + } + else if (!K_GetWaypointIsEnabled(wp)) // disabled + { + return 2; + } + else if (K_GetWaypointIsShortcut(wp)) // shortcut + { + return 1; + } + else + { + return 0; + } +} + +static void K_drawKartMinimapWaypoint(waypoint_t *wp, UINT8 rank, INT32 hudx, INT32 hudy, INT32 flags) +{ + static UINT8 colors[] = + { + 0x95, // blue (0 - default) + 0x20, // pink (1 - shortcut) + 0x10, // gray (2 - disabled) + 0x40, // yellow (3 - error) + 0x70, // green (4 - player) + }; + + UINT8 pal = colors[rank]; // blue + UINT8 size = 3; + + if (rank == 4) + { + size = 6; + } + + if (!(flags & V_NOSCALESTART)) + { + hudx *= vid.dupx; + hudy *= vid.dupy; + } + + K_drawKartMinimapDot(wp->mobj->x, wp->mobj->y, hudx, hudy, flags | V_NOSCALESTART, pal, size); +} + +INT32 K_GetMinimapTransFlags(const boolean usingProgressBar) +{ + INT32 minimaptrans = 4; + boolean dofade = (usingProgressBar && r_splitscreen > 0) || (!usingProgressBar && r_splitscreen >= 1); + + if (dofade) + { + minimaptrans = FixedMul(minimaptrans, (st_translucency * FRACUNIT) / 10); + + // If the minimap is fully transparent, just get your 0 back. Bail out with this. + if (!minimaptrans) + return minimaptrans; + } + + minimaptrans = ((10-minimaptrans)< 0) + { + y = BASEVIDHEIGHT/2; + } + else + { + y = 180; + } + + workingPic = kp_wouldyoustillcatchmeifiwereaworm; + } + + // Really looking forward to never writing this loop again + UINT8 bestplayer = MAXPLAYERS; + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i]) + continue; + if (players[i].spectator) + continue; + if (players[i].position == 1) + bestplayer = i; + } + + if (bestplayer == MAXPLAYERS || leveltime < starttime) // POSITION / no players + minipal = ((leveltime/10)%2) ? SKINCOLOR_WHITE : SKINCOLOR_BLACK; + else if (players[bestplayer].laps >= numlaps) // Final lap + minipal = K_RainbowColor(leveltime); + else // Standard: color to leader + minipal = players[bestplayer].skincolor; + + if (doencore) + { + V_DrawFixedPatch( + (x + (SHORT(workingPic->width)/2))*FRACUNIT, + (y - (SHORT(workingPic->height)/2))*FRACUNIT, + FRACUNIT, + splitflags|minimaptrans|V_FLIP, + workingPic, + R_GetTranslationColormap(TC_DEFAULT, static_cast(minipal), GTC_CACHE) + ); + } + else + { + V_DrawFixedPatch( + (x - (SHORT(workingPic->width)/2))*FRACUNIT, + (y - (SHORT(workingPic->height)/2))*FRACUNIT, + FRACUNIT, + splitflags|minimaptrans, + workingPic, + R_GetTranslationColormap(TC_DEFAULT, static_cast(minipal), GTC_CACHE) + ); + } + + // most icons will be rendered semi-ghostly. + splitflags |= V_HUDTRANSHALF; + + // let offsets transfer to the heads, too! + if (doencore) + x += SHORT(workingPic->leftoffset); + else + x -= SHORT(workingPic->leftoffset); + y -= SHORT(workingPic->topoffset); + + if (doprogressionbar == true) + { + x -= PROGRESSION_BAR_WIDTH/2; + } + + // Draw the super item in Battle + if (doprogressionbar == false && (gametyperules & GTR_OVERTIME) && battleovertime.enabled) + { + if (battleovertime.enabled >= 10*TICRATE || (battleovertime.enabled & 1)) + { + const INT32 prevsplitflags = splitflags; + splitflags &= ~V_HUDTRANSHALF; + splitflags |= V_HUDTRANS; + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(K_RainbowColor(leveltime)), GTC_CACHE); + K_drawKartMinimapIcon(battleovertime.x, battleovertime.y, x, y, splitflags, kp_itemminimap, colormap); + splitflags = prevsplitflags; + } + } + + // initialize + for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) + localplayers[i] = -1; + + // Player's tiny icons on the Automap. (drawn opposite direction so player 1 is drawn last in splitscreen) + if (ghosts && doprogressionbar == false) // future work: show ghosts on progression bar + { + demoghost *g = ghosts; + while (g) + { + if (g->mo && !P_MobjWasRemoved(g->mo) && g->mo->skin) + { + skin = ((skin_t*)g->mo->skin)-skins; + + workingPic = R_CanShowSkinInDemo(skin) ? faceprefix[skin][FACE_MINIMAP] : kp_unknownminimap; + + if (g->mo->color) + { + if (g->mo->colorized) + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(g->mo->color), GTC_CACHE); + else + colormap = R_GetTranslationColormap(skin, static_cast(g->mo->color), GTC_CACHE); + } + else + colormap = NULL; + + interpx = R_InterpolateFixed(g->mo->old_x, g->mo->x); + interpy = R_InterpolateFixed(g->mo->old_y, g->mo->y); + + K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap); + } + + g = g->next; + } + } + + { + for (i = MAXPLAYERS-1; i >= 0; i--) + { + if (!playeringame[i]) + continue; + if (!players[i].mo || players[i].spectator || !players[i].mo->skin + || (doprogressionbar == false && players[i].exiting)) + continue; + + // This player is out of the game! + if ((gametyperules & GTR_BUMPERS) && (players[i].pflags & PF_ELIMINATED)) + continue; + + // This gets set for a player who has GAME OVER'd + if (P_MobjIsReappearing(players[i].mo)) + continue; + + if (i == displayplayers[0] || i == displayplayers[1] || i == displayplayers[2] || i == displayplayers[3]) + { + // Draw display players on top of everything else + localplayers[numlocalplayers++] = i; + continue; + } + + if (players[i].hyudorotimer > 0) + { + if (!((players[i].hyudorotimer < TICRATE/2 + || players[i].hyudorotimer > hyudorotime-(TICRATE/2)) + && !(leveltime & 1))) + continue; + } + + mobj = players[i].mo; + + if (mobj->health <= 0 && (players[i].pflags & PF_NOCONTEST)) + { + if (P_MobjWasRemoved(mobj->tracer)) + { + continue; + } + + if (mobj->tracer->renderflags & RF_DONTDRAW) + { + continue; + } + + workingPic = kp_nocontestminimap; + colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(mobj->color), GTC_CACHE); + + mobj = mobj->tracer; + } + else + { + skin = ((skin_t*)mobj->skin)-skins; + + workingPic = R_CanShowSkinInDemo(skin) ? faceprefix[skin][FACE_MINIMAP] : kp_unknownminimap; + + if (mobj->color) + { + if (mobj->colorized) + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(mobj->color), GTC_CACHE); + else + colormap = R_GetTranslationColormap(skin, static_cast(mobj->color), GTC_CACHE); + } + else + colormap = NULL; + } + + if (doprogressionbar == false) + { + interpx = R_InterpolateFixed(mobj->old_x, mobj->x); + interpy = R_InterpolateFixed(mobj->old_y, mobj->y); + + K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap); + + // Target reticule + if (((gametyperules & GTR_CIRCUIT) && players[i].position == spbplace) + || ((gametyperules & (GTR_BOSS|GTR_POINTLIMIT)) == GTR_POINTLIMIT && K_IsPlayerWanted(&players[i]))) + { + K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, kp_wantedreticle, NULL); + } + } + else + { + K_drawKartProgressionMinimapIcon(players[i].distancetofinish, x, y, splitflags, workingPic, colormap); + } + } + } + + // draw minimap-pertinent objects + if (doprogressionbar == true) + { + // future work: support these specific objects on this + } + else for (mobj = trackercap; mobj; mobj = next) + { + next = mobj->itnext; + + workingPic = NULL; + colormap = NULL; + + if (mobj->health <= 0) + continue; + + switch (mobj->type) + { + case MT_SPB: + workingPic = kp_spbminimap; +#if 0 + if (mobj->target && !P_MobjWasRemoved(mobj->target) && mobj->target->player && mobj->target->player->skincolor) + { + colormap = R_GetTranslationColormap(TC_RAINBOW, mobj->target->player->skincolor, GTC_CACHE); + } + else +#endif + if (mobj->color) + { + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(mobj->color), GTC_CACHE); + } + + break; + case MT_BATTLECAPSULE: + workingPic = kp_capsuleminimap[(mobj->extravalue1 != 0 ? 1 : 0)]; + break; + case MT_CDUFO: + if (battleprisons) + workingPic = kp_capsuleminimap[2]; + break; + case MT_BATTLEUFO: + workingPic = kp_battleufominimap; + break; + case MT_SUPER_FLICKY: + workingPic = kp_superflickyminimap; + if (mobj_t* owner = Obj_SuperFlickyOwner(mobj); owner && owner->color) + { + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(owner->color), GTC_CACHE); + } + break; + default: + break; + } + + if (!workingPic) + continue; + + interpx = R_InterpolateFixed(mobj->old_x, mobj->x); + interpy = R_InterpolateFixed(mobj->old_y, mobj->y); + + K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap); + } + + // draw our local players here, opaque. + { + splitflags &= ~V_HUDTRANSHALF; + splitflags |= V_HUDTRANS; + } + + // ...but first, any boss targets. + if (doprogressionbar == true) + { + if (specialstageinfo.valid == true) + { + UINT32 distancetofinish = K_GetSpecialUFODistance(); + if (distancetofinish > 0 && specialstageinfo.ufo != NULL && P_MobjWasRemoved(specialstageinfo.ufo) == false) + { + colormap = NULL; + if (specialstageinfo.ufo->health > 1) + { + workingPic = kp_catcherminimap; + } + else + { + UINT8 emid = 0; + if (specialstageinfo.ufo->cvmem > 7) + emid = 1; + workingPic = kp_emeraldminimap[emid]; + + if (specialstageinfo.ufo->color) + { + colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(specialstageinfo.ufo->color), GTC_CACHE); + } + } + + K_drawKartProgressionMinimapIcon(distancetofinish, x, y, splitflags, workingPic, colormap); + } + } + + // future work: support boss minimap icons on the progression bar + } + else if (bossinfo.valid == true) + { + for (i = 0; i < NUMWEAKSPOTS; i++) + { + // exists at all? + if (bossinfo.weakspots[i].spot == NULL || P_MobjWasRemoved(bossinfo.weakspots[i].spot)) + continue; + // shows on the minimap? + if (bossinfo.weakspots[i].minimap == false) + continue; + // in the flashing period? + if ((bossinfo.weakspots[i].time > (WEAKSPOTANIMTIME-(TICRATE/2))) && (bossinfo.weakspots[i].time & 1)) + continue; + + colormap = NULL; + + if (bossinfo.weakspots[i].color) + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(bossinfo.weakspots[i].color), GTC_CACHE); + + interpx = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_x, bossinfo.weakspots[i].spot->x); + interpy = R_InterpolateFixed(bossinfo.weakspots[i].spot->old_y, bossinfo.weakspots[i].spot->y); + + // temporary graphic? + K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, kp_wantedreticle, colormap); + } + } + + for (i = 0; i < numlocalplayers; i++) + { + boolean nocontest = false; + + if (localplayers[i] == -1) + continue; // this doesn't interest us + + if ((players[localplayers[i]].hyudorotimer > 0) && (leveltime & 1)) + continue; + + mobj = players[localplayers[i]].mo; + + // This gets set for a player who has GAME OVER'd + if (P_MobjIsReappearing(mobj)) + continue; + + if (mobj->health <= 0 && (players[localplayers[i]].pflags & PF_NOCONTEST)) + { + if (P_MobjWasRemoved(mobj->tracer)) + { + continue; + } + + if (mobj->tracer->renderflags & RF_DONTDRAW) + { + continue; + } + + workingPic = kp_nocontestminimap; + colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(mobj->color), GTC_CACHE); + + mobj = mobj->tracer; + + nocontest = true; + } + else + { + skin = ((skin_t*)mobj->skin)-skins; + + workingPic = R_CanShowSkinInDemo(skin) ? faceprefix[skin][FACE_MINIMAP] : kp_unknownminimap; + + if (mobj->color) + { + if (mobj->colorized) + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(mobj->color), GTC_CACHE); + else + colormap = R_GetTranslationColormap(skin, static_cast(mobj->color), GTC_CACHE); + } + else + colormap = NULL; + } + + if (doprogressionbar == false) + { + interpx = R_InterpolateFixed(mobj->old_x, mobj->x); + interpy = R_InterpolateFixed(mobj->old_y, mobj->y); + + K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap); + + // Target reticule + if (((gametyperules & GTR_CIRCUIT) && players[localplayers[i]].position == spbplace) + || ((gametyperules & (GTR_BOSS|GTR_POINTLIMIT)) == GTR_POINTLIMIT && K_IsPlayerWanted(&players[localplayers[i]]))) + { + K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, kp_wantedreticle, NULL); + } + + if (!nocontest) + { + angle_t ang = R_InterpolateAngle(mobj->old_angle, mobj->angle); + if (encoremode) + ang = ANGLE_180 - ang; + + if (skin && mobj->color && !mobj->colorized // relevant to redo + && skins[skin].starttranscolor != skins[0].starttranscolor) // redoing would have an affect + { + colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(mobj->color), GTC_CACHE); + } + + K_drawKartMinimapIcon( + interpx, + interpy, + x + FixedMul(FCOS(ang), ICON_DOT_RADIUS), + y - FixedMul(FSIN(ang), ICON_DOT_RADIUS), + splitflags, + kp_minimapdot, + colormap + ); + } + } + else + { + K_drawKartProgressionMinimapIcon(players[localplayers[i]].distancetofinish, x, y, splitflags, workingPic, colormap); + } + } + + if (doprogressionbar == false && cv_kartdebugwaypoints.value != 0) + { + struct MiniWaypoint + { + waypoint_t* waypoint; + UINT8 rank; + + MiniWaypoint(waypoint_t* wp) : waypoint(wp), rank(K_RankMinimapWaypoint(wp)) {} + + bool operator<(const MiniWaypoint& b) const noexcept { return rank < b.rank; } + }; + + std::vector waypoints; + size_t idx; + + waypoints.reserve(K_GetNumWaypoints()); + + for (idx = 0; idx < K_GetNumWaypoints(); ++idx) + { + waypoint_t *wp = K_GetWaypointFromIndex(idx); + + I_Assert(wp != NULL); + + waypoints.push_back(wp); + } + + std::sort(waypoints.begin(), waypoints.end()); + + for (MiniWaypoint& wp : waypoints) + { + K_drawKartMinimapWaypoint(wp.waypoint, wp.rank, x, y, splitflags); + } + } +} + +#undef PROGRESSION_BAR_WIDTH + +static void K_drawKartFinish(boolean finish) +{ + INT32 timer, minsplitstationary, pnum = 0, splitflags = V_SPLITSCREEN; + patch_t **kptodraw; + + if (finish) + { + if (gametyperules & GTR_SPECIALSTART) + return; + + timer = stplyr->karthud[khud_finish]; + kptodraw = kp_racefinish; + minsplitstationary = 2; + } + else + { + timer = stplyr->karthud[khud_fault]; + kptodraw = kp_racefault; + minsplitstationary = 1; + } + + if (!timer || timer > 2*TICRATE) + return; + + if ((timer % (2*5)) / 5) // blink + pnum = 1; + + if (r_splitscreen > 0) + pnum += (r_splitscreen > 1) ? 2 : 4; + + if (r_splitscreen >= minsplitstationary) // 3/4p, stationary FIN + { + V_DrawScaledPatch(STCD_X - (SHORT(kptodraw[pnum]->width)/2), STCD_Y - (SHORT(kptodraw[pnum]->height)/2), splitflags, kptodraw[pnum]); + return; + } + + //else -- 1/2p, scrolling FINISH + { + INT32 x, xval, ox, interpx, pwidth; + + x = ((vid.width<width)<height)<<(FRACBITS-1)), + FRACUNIT, + splitflags, kptodraw[pnum], NULL); + } +} + +static void K_drawKartStartBulbs(void) +{ + const UINT8 start_animation[14] = { + 1, 2, 3, 4, 5, 6, 7, 8, + 7, 6, + 9, 10, 11, 12 + }; + + const UINT8 loop_animation[4] = { + 12, 13, 12, 14 + }; + + const UINT8 chillloop_animation[2] = { + 11, 12 + }; + + const UINT8 letters_order[10] = { + 0, 1, 2, 3, 4, 3, 1, 5, 6, 6 + }; + + const UINT8 letters_transparency[40] = { + 0, 2, 4, 6, 8, + 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, + 10, 8, 6, 4, 2 + }; + + fixed_t spacing = 24*FRACUNIT; + + fixed_t startx = (BASEVIDWIDTH/2)*FRACUNIT; + fixed_t starty = 48*FRACUNIT; + fixed_t x, y; + + UINT8 numperrow = numbulbs/2; + UINT8 i; + + if (r_splitscreen >= 1) + { + spacing /= 2; + starty /= 3; + + if (r_splitscreen > 1) + { + startx /= 2; + } + } + + startx += (spacing/2); + + if (numbulbs <= 10) + { + // No second row + numperrow = numbulbs; + } + else + { + if (numbulbs & 1) + { + numperrow++; + } + + starty -= (spacing/2); + } + + startx -= (spacing/2) * numperrow; + + x = startx; + y = starty; + + for (i = 0; i < numbulbs; i++) + { + UINT8 patchnum = 0; + INT32 bulbtic = (leveltime - introtime - TICRATE) - (bulbtime * i); + + if (i == numperrow) + { + y += spacing; + x = startx + (spacing/2); + } + + if (bulbtic > 0) + { + if (bulbtic < 14) + { + patchnum = start_animation[bulbtic]; + } + else + { + const INT32 length = (bulbtime * 3); + + bulbtic -= 14; + + // Reduce VFX disables the bulb animation while still presenting this indicator + + if (bulbtic > length) + { + bulbtic -= length; + + if (cv_reducevfx.value != 0) + { + patchnum = chillloop_animation[0]; + } + else + { + patchnum = chillloop_animation[bulbtic % 2]; + } + } + else + { + if (cv_reducevfx.value != 0) + { + patchnum = loop_animation[0]; + } + else + { + patchnum = loop_animation[bulbtic % 4]; + } + } + } + } + + V_DrawFixedPatch(x, y, FRACUNIT, V_SNAPTOTOP|V_SPLITSCREEN, + (r_splitscreen ? kp_prestartbulb_split[patchnum] : kp_prestartbulb[patchnum]), NULL); + x += spacing; + } + + x = 70*FRACUNIT; + y = starty; + + if (r_splitscreen == 1) + { + x = 106*FRACUNIT; + } + else if (r_splitscreen > 1) + { + x = 28*FRACUNIT; + } + + if (timeinmap < 16) + return; // temporary for current map start behaviour + + for (i = 0; i < 10; i++) + { + UINT8 patchnum = letters_order[i]; + INT32 transflag = letters_transparency[(leveltime - i) % 40]; + patch_t *patch = (r_splitscreen ? kp_prestartletters_split[patchnum] : kp_prestartletters[patchnum]); + + if (transflag >= 10) + ; + else + { + if (transflag != 0) + transflag = transflag << FF_TRANSSHIFT; + + V_DrawFixedPatch(x, y, FRACUNIT, V_SNAPTOTOP|V_SPLITSCREEN|transflag, patch, NULL); + } + + if (i < 9) + { + x += (SHORT(patch->width)) * FRACUNIT/2; + + patchnum = letters_order[i+1]; + patch = (r_splitscreen ? kp_prestartletters_split[patchnum] : kp_prestartletters[patchnum]); + x += (SHORT(patch->width)) * FRACUNIT/2; + + if (r_splitscreen) + x -= FRACUNIT; + } + } +} + +static void K_drawKartStartCountdown(void) +{ + INT32 pnum = 0; + + if (leveltime >= introtime && leveltime < starttime-(3*TICRATE)) + { + if (numbulbs > 1) + K_drawKartStartBulbs(); + } + else + { + + if (leveltime >= starttime-(2*TICRATE)) // 2 + pnum++; + if (leveltime >= starttime-TICRATE) // 1 + pnum++; + + if (leveltime >= starttime) // GO! + { + UINT8 i; + UINT8 numplayers = 0; + + pnum++; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] && !players[i].spectator) + numplayers++; + + if (numplayers > 2) + break; + } + + if (inDuel == true) + { + pnum++; // DUEL + } + } + + if ((leveltime % (2*5)) / 5) // blink + pnum += 5; + if (r_splitscreen) // splitscreen + pnum += 10; + + V_DrawScaledPatch(STCD_X - (SHORT(kp_startcountdown[pnum]->width)/2), STCD_Y - (SHORT(kp_startcountdown[pnum]->height)/2), V_SPLITSCREEN, kp_startcountdown[pnum]); + } +} + +static void K_drawKartFirstPerson(void) +{ + static INT32 pnum[4], turn[4], drift[4]; + const INT16 steerThreshold = KART_FULLTURN / 2; + INT32 pn = 0, tn = 0, dr = 0; + INT32 target = 0, splitflags = V_SNAPTOBOTTOM|V_SPLITSCREEN; + INT32 x = BASEVIDWIDTH/2, y = BASEVIDHEIGHT; + fixed_t scale; + UINT8 *colmap = NULL; + + if (stplyr->spectator || !stplyr->mo || (stplyr->mo->renderflags & RF_DONTDRAW)) + return; + + { + UINT8 view = R_GetViewNumber(); + pn = pnum[view]; + tn = turn[view]; + dr = drift[view]; + } + + if (r_splitscreen) + { + y >>= 1; + if (r_splitscreen > 1) + x >>= 1; + } + + { + if (stplyr->speed < (20*stplyr->mo->scale) && (leveltime & 1) && !r_splitscreen) + y++; + + if (stplyr->mo->renderflags & RF_TRANSMASK) + splitflags |= ((stplyr->mo->renderflags & RF_TRANSMASK) >> RF_TRANSSHIFT) << FF_TRANSSHIFT; + else if (stplyr->mo->frame & FF_TRANSMASK) + splitflags |= (stplyr->mo->frame & FF_TRANSMASK); + } + + if (stplyr->steering > steerThreshold) // strong left turn + target = 2; + else if (stplyr->steering < -steerThreshold) // strong right turn + target = -2; + else if (stplyr->steering > 0) // weak left turn + target = 1; + else if (stplyr->steering < 0) // weak right turn + target = -1; + else // forward + target = 0; + + if (encoremode) + target = -target; + + if (pn < target) + pn++; + else if (pn > target) + pn--; + + if (pn < 0) + splitflags |= V_FLIP; // right turn + + target = abs(pn); + if (target > 2) + target = 2; + + x <<= FRACBITS; + y <<= FRACBITS; + + if (tn != stplyr->steering/50) + tn -= (tn - (stplyr->steering/50))/8; + + if (dr != stplyr->drift*16) + dr -= (dr - (stplyr->drift*16))/8; + + if (r_splitscreen == 1) + { + scale = (2*FRACUNIT)/3; + y += FRACUNIT/(vid.dupx < vid.dupy ? vid.dupx : vid.dupy); // correct a one-pixel gap on the screen view (not the basevid view) + } + else if (r_splitscreen) + scale = FRACUNIT/2; + else + scale = FRACUNIT; + + if (stplyr->mo) + { + UINT8 driftcolor = K_DriftSparkColor(stplyr, stplyr->driftcharge); + const angle_t ang = R_PointToAngle2(0, 0, stplyr->rmomx, stplyr->rmomy) - stplyr->drawangle; + // yes, the following is correct. no, you do not need to swap the x and y. + fixed_t xoffs = -P_ReturnThrustY(stplyr->mo, ang, (BASEVIDWIDTH<<(FRACBITS-2))/2); + fixed_t yoffs = -P_ReturnThrustX(stplyr->mo, ang, 4*FRACUNIT); + + // hitlag vibrating + if (stplyr->mo->hitlag > 0 && (stplyr->mo->eflags & MFE_DAMAGEHITLAG)) + { + fixed_t mul = stplyr->mo->hitlag * HITLAGJITTERS; + if (r_splitscreen && mul > FRACUNIT) + mul = FRACUNIT; + + if (leveltime & 1) + { + mul = -mul; + } + + xoffs = FixedMul(xoffs, mul); + yoffs = FixedMul(yoffs, mul); + + } + + if ((yoffs += 4*FRACUNIT) < 0) + yoffs = 0; + + if (r_splitscreen) + xoffs = FixedMul(xoffs, scale); + + xoffs -= (tn)*scale; + xoffs -= (dr)*scale; + + if (stplyr->drawangle == stplyr->mo->angle) + { + const fixed_t mag = FixedDiv(stplyr->speed, 10*stplyr->mo->scale); + + if (mag < FRACUNIT) + { + xoffs = FixedMul(xoffs, mag); + if (!r_splitscreen) + yoffs = FixedMul(yoffs, mag); + } + } + + if (stplyr->mo->momz > 0) // TO-DO: Draw more of the kart so we can remove this if! + yoffs += stplyr->mo->momz/3; + + if (encoremode) + x -= xoffs; + else + x += xoffs; + if (!r_splitscreen) + y += yoffs; + + + if ((leveltime & 1) && (driftcolor != SKINCOLOR_NONE)) // drift sparks! + colmap = R_GetTranslationColormap(TC_RAINBOW, static_cast(driftcolor), GTC_CACHE); + else if (stplyr->mo->colorized && stplyr->mo->color) // invincibility/grow/shrink! + colmap = R_GetTranslationColormap(TC_RAINBOW, static_cast(stplyr->mo->color), GTC_CACHE); + } + + V_DrawFixedPatch(x, y, scale, splitflags, kp_fpview[target], colmap); + + { + UINT8 view = R_GetViewNumber(); + pnum[view] = pn; + turn[view] = tn; + drift[view] = dr; + } +} + +static void K_drawInput(void) +{ + UINT8 viewnum = R_GetViewNumber(); + boolean freecam = camera[viewnum].freecam; //disable some hud elements w/ freecam + + if (!cv_drawinput.value && !modeattacking && gametype != GT_TUTORIAL) + return; + + if (stplyr->spectator || freecam || demo.attract) + return; + + INT32 def[4][3] = { + {247, 156, V_SNAPTOBOTTOM | V_SNAPTORIGHT}, // 1p + {247, 56, V_SNAPTOBOTTOM | V_SNAPTORIGHT}, // 2p + {6, 52, V_SNAPTOBOTTOM | V_SNAPTOLEFT}, // 4p left + {282 - BASEVIDWIDTH/2, 52, V_SNAPTOBOTTOM | V_SNAPTORIGHT}, // 4p right + }; + INT32 k = r_splitscreen <= 1 ? r_splitscreen : 2 + (viewnum & 1); + INT32 flags = def[k][2] | V_SPLITSCREEN; + char mode = ((stplyr->pflags & PF_ANALOGSTICK) ? '4' : '2') + (r_splitscreen > 1); + bool local = !demo.playback && P_IsMachineLocalPlayer(stplyr); + fixed_t slide = K_GetDialogueSlide(FRACUNIT); + INT32 tallySlide = []() -> INT32 + { + if (r_splitscreen <= 1) + { + return 0; + } + if (!stplyr->tally.active) + { + return 0; + } + constexpr INT32 kSlideDown = 22; + if (stplyr->tally.state == TALLY_ST_GOTTHRU_SLIDEIN || + stplyr->tally.state == TALLY_ST_GAMEOVER_SLIDEIN) + { + return static_cast(Easing_OutQuad(std::min(stplyr->tally.transition * 2, FRACUNIT), 0, kSlideDown)); + } + return kSlideDown; + }(); + if (slide) + flags &= ~(V_SNAPTORIGHT); // don't draw underneath the dialogue box in non-green resolutions + + // Move above the boss health bar. + // TODO: boss HUD only works in 1P, so this only works in 1P too. + if (LUA_HudEnabled(hud_position) && bossinfo.valid) + { + constexpr tic_t kDelay = 2u; + // See K_drawBossHealthBar + tic_t start = lt_endtime - 1u; + tic_t t = std::clamp(lt_ticker, start, start + kDelay) - start; + def[0][1] -= 24 + Easing_Linear(t * FRACUNIT / kDelay, 0, 7); + } + + K_DrawInputDisplay( + def[k][0] - FixedToFloat(34 * slide), + def[k][1] - FixedToFloat(51 * slide) + tallySlide, + flags, + mode, + (local ? G_LocalSplitscreenPartyPosition : G_PartyPosition)(stplyr - players), + local, + stplyr->speed > 0 + ); +} + +static void K_drawChallengerScreen(void) +{ + // This is an insanely complicated animation. + static UINT8 anim[52] = { + 0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13, // frame 1-14, 2 tics: HERE COMES A NEW slides in + 14,14,14,14,14,14, // frame 15, 6 tics: pause on the W + 15,16,17,18, // frame 16-19, 1 tic: CHALLENGER approaches screen + 19,20,19,20,19,20,19,20,19,20, // frame 20-21, 1 tic, 5 alternating: all text vibrates from impact + 21,22,23,24 // frame 22-25, 1 tic: CHALLENGER turns gold + }; + const UINT8 offset = std::min(52-1u, (3*TICRATE)-mapreset); + + V_DrawFadeScreen(0xFF00, 16); // Fade out + V_DrawScaledPatch(0, 0, 0, kp_challenger[anim[offset]]); +} + +static void K_drawLapStartAnim(void) +{ + if (demo.attract == DEMO_ATTRACT_CREDITS) + { + return; + } + + // This is an EVEN MORE insanely complicated animation. + const UINT8 t = stplyr->karthud[khud_lapanimation]; + const UINT8 progress = 80 - t; + + const UINT8 tOld = t + 1; + const UINT8 progressOld = 80 - tOld; + + const tic_t leveltimeOld = leveltime - 1; + + UINT8 *colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(stplyr->skincolor), GTC_CACHE); + + fixed_t interpx, interpy, newval, oldval; + + newval = (BASEVIDWIDTH/2 + (32 * std::max(0, t - 76))) * FRACUNIT; + oldval = (BASEVIDWIDTH/2 + (32 * std::max(0, tOld - 76))) * FRACUNIT; + interpx = R_InterpolateFixed(oldval, newval); + + newval = (48 - (32 * std::max(0, progress - 76))) * FRACUNIT; + oldval = (48 - (32 * std::max(0, progressOld - 76))) * FRACUNIT; + interpy = R_InterpolateFixed(oldval, newval); + + V_DrawFixedPatch( + interpx, interpy, + FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, + (modeattacking ? kp_lapanim_emblem[1] : kp_lapanim_emblem[0]), colormap); + + if (stplyr->karthud[khud_laphand] >= 1 && stplyr->karthud[khud_laphand] <= 3) + { + newval = (4 - abs((signed)((leveltime % 8) - 4))) * FRACUNIT; + oldval = (4 - abs((signed)((leveltimeOld % 8) - 4))) * FRACUNIT; + interpy += R_InterpolateFixed(oldval, newval); + + V_DrawFixedPatch( + interpx, interpy, + FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, + kp_lapanim_hand[stplyr->karthud[khud_laphand]-1], NULL); + } + + if (stplyr->latestlap == (UINT8)(numlaps)) + { + newval = (62 - (32 * std::max(0, progress - 76))) * FRACUNIT; + oldval = (62 - (32 * std::max(0, progressOld - 76))) * FRACUNIT; + interpx = R_InterpolateFixed(oldval, newval); + + V_DrawFixedPatch( + interpx, // 27 + 30*FRACUNIT, // 24 + FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, + kp_lapanim_final[std::min(progress/2, 10)], NULL); + + if (progress/2-12 >= 0) + { + newval = (188 + (32 * std::max(0, progress - 76))) * FRACUNIT; + oldval = (188 + (32 * std::max(0, progressOld - 76))) * FRACUNIT; + interpx = R_InterpolateFixed(oldval, newval); + + V_DrawFixedPatch( + interpx, // 194 + 30*FRACUNIT, // 24 + FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, + kp_lapanim_lap[std::min(progress/2-12, 6)], NULL); + } + } + else + { + newval = (82 - (32 * std::max(0, progress - 76))) * FRACUNIT; + oldval = (82 - (32 * std::max(0, progressOld - 76))) * FRACUNIT; + interpx = R_InterpolateFixed(oldval, newval); + + V_DrawFixedPatch( + interpx, // 61 + 30*FRACUNIT, // 24 + FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, + kp_lapanim_lap[std::min(progress/2, 6)], NULL); + + if (progress/2-8 >= 0) + { + newval = (188 + (32 * std::max(0, progress - 76))) * FRACUNIT; + oldval = (188 + (32 * std::max(0, progressOld - 76))) * FRACUNIT; + interpx = R_InterpolateFixed(oldval, newval); + + V_DrawFixedPatch( + interpx, // 194 + 30*FRACUNIT, // 24 + FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, + kp_lapanim_number[(((UINT32)stplyr->latestlap) / 10)][std::min(progress/2-8, 2)], NULL); + + if (progress/2-10 >= 0) + { + newval = (208 + (32 * std::max(0, progress - 76))) * FRACUNIT; + oldval = (208 + (32 * std::max(0, progressOld - 76))) * FRACUNIT; + interpx = R_InterpolateFixed(oldval, newval); + + V_DrawFixedPatch( + interpx, // 221 + 30*FRACUNIT, // 24 + FRACUNIT, V_SNAPTOTOP|V_HUDTRANS, + kp_lapanim_number[(((UINT32)stplyr->latestlap) % 10)][std::min(progress/2-10, 2)], NULL); + } + } + } +} + +// stretch for "COOOOOL" popup. +// I can't be fucked to find out any math behind this so have a table lmao +static fixed_t stretch[6][2] = { + {FRACUNIT/4, FRACUNIT*4}, + {FRACUNIT/2, FRACUNIT*2}, + {FRACUNIT, FRACUNIT}, + {FRACUNIT*4, FRACUNIT/2}, + {FRACUNIT*8, FRACUNIT/4}, + {FRACUNIT*4, FRACUNIT/2}, +}; + +static void K_drawTrickCool(void) +{ + + tic_t timer = TICRATE - stplyr->karthud[khud_trickcool]; + + if (timer <= 6) + { + V_DrawStretchyFixedPatch(TCOOL_X<spectator == true) + return; + + if (M_NotFreePlay() == true) + return; + + if (lt_exitticker < TICRATE/2) + return; + + if (((leveltime-lt_endtime) % TICRATE) < TICRATE/2) + return; + + INT32 h_snap = r_splitscreen < 2 ? V_SNAPTORIGHT | V_SLIDEIN : V_HUDTRANS; + fixed_t x = ((r_splitscreen > 1 ? BASEVIDWIDTH/4 : BASEVIDWIDTH - (LAPS_X+6)) * FRACUNIT); + fixed_t y = ((r_splitscreen ? BASEVIDHEIGHT/2 : BASEVIDHEIGHT) - 20) * FRACUNIT; + + x -= V_StringScaledWidth( + FRACUNIT, + FRACUNIT, + FRACUNIT, + V_SNAPTOBOTTOM|h_snap|V_SPLITSCREEN, + KART_FONT, + "FREE PLAY" + ) / (r_splitscreen > 1 ? 2 : 1); + + V_DrawStringScaled( + x, + y, + FRACUNIT, + FRACUNIT, + FRACUNIT, + V_SNAPTOBOTTOM|h_snap|V_SPLITSCREEN, + NULL, + KART_FONT, + "FREE PLAY" + ); +} + +static void +Draw_party_ping (int ss, INT32 snap) +{ + UINT32 ping = playerpingtable[displayplayers[ss]]; + UINT32 mindelay = playerdelaytable[displayplayers[ss]]; + HU_drawMiniPing(0, 0, ping, mindelay, V_SPLITSCREEN|V_SNAPTOTOP|snap); +} + +static void +K_drawMiniPing (void) +{ + UINT32 f = V_SNAPTORIGHT; + UINT8 i = R_GetViewNumber(); + + if (r_splitscreen > 1 && !(i & 1)) + { + f = V_SNAPTOLEFT; + } + + Draw_party_ping(i, f); +} + +void K_drawButton(fixed_t x, fixed_t y, INT32 flags, patch_t *button[2], boolean pressed) +{ + V_DrawFixedPatch(x, y, FRACUNIT, flags, button[(pressed == true) ? 1 : 0], NULL); +} + +void K_drawButtonAnim(INT32 x, INT32 y, INT32 flags, patch_t *button[2], tic_t animtic) +{ + const UINT8 anim_duration = 16; + const boolean anim = ((animtic % (anim_duration * 2)) < anim_duration); + K_drawButton(x << FRACBITS, y << FRACBITS, flags, button, anim); +} + +static void K_drawDistributionDebugger(void) +{ + itemroulette_t rouletteData = {0}; + + const fixed_t scale = (FRACUNIT >> 1); + const fixed_t pad = 9 * scale; + + fixed_t x = -pad; + + if (R_GetViewNumber() != 0) // only for p1 + { + return; + } + + K_FillItemRouletteData(stplyr, &rouletteData, false, true); + + if (cv_kartdebugdistribution.value <= 1) + return; + + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+10, V_SNAPTOTOP|V_SNAPTORIGHT, va("speed = %u", rouletteData.speed)); + + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+22, V_SNAPTOTOP|V_SNAPTORIGHT, va("baseDist = %u", rouletteData.baseDist)); + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+30, V_SNAPTOTOP|V_SNAPTORIGHT, va("dist = %u", rouletteData.dist)); + + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+42, V_SNAPTOTOP|V_SNAPTORIGHT, va("firstDist = %u", rouletteData.firstDist)); + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+50, V_SNAPTOTOP|V_SNAPTORIGHT, va("secondDist = %u", rouletteData.secondDist)); + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+58, V_SNAPTOTOP|V_SNAPTORIGHT, va("secondToFirst = %u", rouletteData.secondToFirst)); + +#ifndef ITEM_LIST_SIZE + Z_Free(rouletteData.itemList); +#endif +} + +static void K_DrawWaypointDebugger(void) +{ + if (cv_kartdebugwaypoints.value == 0) + return; + + if (R_GetViewNumber() != 0) // only for p1 + return; + + constexpr int kH = 8; + using srb2::Draw; + Draw::TextElement label; + label.font(Draw::Font::kThin); + label.flags(V_AQUAMAP); + Draw line = Draw(8, 110).font(Draw::Font::kMenu); + auto put = [&](const char* label_str, auto&&... args) + { + constexpr int kTabWidth = 48; + label.string(label_str); + int x = label.width() + kTabWidth; + x -= x % kTabWidth; + line.size(x + 4, 2).y(7).fill(31); + line.text(label); + line.x(x).text(args...); + line = line.y(kH); + }; + + if (netgame) + { + line = line.y(-kH); + put("Online griefing:", "[{}, {}]", stplyr->griefValue/TICRATE, stplyr->griefStrikes); + } + + put("Current Waypoint ID:", "{}", K_GetWaypointID(stplyr->currentwaypoint)); + put("Next Waypoint ID:", "{}{}", K_GetWaypointID(stplyr->nextwaypoint), ((stplyr->pflags & PF_WRONGWAY) ? " (WRONG WAY)" : "")); + put("Respawn Waypoint ID:", "{}", K_GetWaypointID(stplyr->respawn.wp)); + put("Finishline Distance:", "{}", stplyr->distancetofinish); + put("Last Safe Lap:", "{}", stplyr->lastsafelap); + + if (numcheatchecks > 0) + { + if (stplyr->cheatchecknum == numcheatchecks) + put("Cheat Check:", "{} / {} (Can finish)", stplyr->cheatchecknum, numcheatchecks); + else + put("Cheat Check:", "{} / {}", stplyr->cheatchecknum, numcheatchecks); + put("Last Safe Cheat Check:", "{}", stplyr->lastsafecheatcheck); + } + + if (stplyr->bigwaypointgap) + { + put("Auto Respawn Timer:", "{}", stplyr->bigwaypointgap); + } +} + +static void K_DrawBotDebugger(void) +{ + player_t *bot = NULL; + + if (cv_kartdebugbots.value == 0) + { + return; + } + + if (R_GetViewNumber() != 0) // only for p1 + { + return; + } + + if (stplyr->bot == true) + { + // we ARE the bot + bot = stplyr; + } + else + { + // get winning bot + size_t i; + for (i = 0; i < MAXPLAYERS; i++) + { + player_t *p = NULL; + + if (playeringame[i] == false) + { + continue; + } + + p = &players[i]; + if (p->spectator == true || p->bot == false) + { + continue; + } + + if (bot == NULL || p->distancetofinish < bot->distancetofinish) + { + bot = p; + } + } + } + + if (bot == NULL) + { + // no bot exists? + return; + } + + V_DrawSmallString(16, 8, V_YELLOWMAP, va("Bot: %s", player_names[bot - players])); + + V_DrawSmallString(8, 14, 0, va("Difficulty: %d / %d", bot->botvars.difficulty, MAXBOTDIFFICULTY)); + V_DrawSmallString(8, 18, 0, va("Difficulty increase: %d", bot->botvars.diffincrease)); + V_DrawSmallString(8, 22, 0, va("Rival: %d", (UINT8)(bot->botvars.rival == true))); + V_DrawSmallString(8, 26, 0, va("Rubberbanding: %.02f", FIXED_TO_FLOAT(bot->botvars.rubberband) * 100.0f)); + + V_DrawSmallString(8, 32, 0, va("Item delay: %d", bot->botvars.itemdelay)); + V_DrawSmallString(8, 36, 0, va("Item confirm: %d", bot->botvars.itemconfirm)); + + V_DrawSmallString(8, 42, 0, va("Turn: %d / %d / %d", -BOTTURNCONFIRM, bot->botvars.turnconfirm, BOTTURNCONFIRM)); + V_DrawSmallString(8, 46, 0, va("Spindash: %d / %d", bot->botvars.spindashconfirm, BOTSPINDASHCONFIRM)); + V_DrawSmallString(8, 50, 0, va("Respawn: %d / %d", bot->botvars.respawnconfirm, BOTRESPAWNCONFIRM)); + + V_DrawSmallString(8, 56, 0, va("Item priority: %d", bot->botvars.roulettePriority)); + V_DrawSmallString(8, 60, 0, va("Item timeout: %d", bot->botvars.rouletteTimeout)); + + V_DrawSmallString(8, 66, 0, va("Complexity: %d", K_GetTrackComplexity())); + V_DrawSmallString(8, 70, 0, va("Bot modifier: %.2f", FixedToFloat(K_BotMapModifier()))); +} + +static void K_DrawGPRankDebugger(void) +{ + gp_rank_e grade = GRADE_E; + char gradeChar = '?'; + + if (cv_debugrank.value == 0) + { + return; + } + + if (R_GetViewNumber() != 0) // only for p1 + { + return; + } + + if (grandprixinfo.gp == false) + { + return; + } + + grade = K_CalculateGPGrade(&grandprixinfo.rank); + + V_DrawThinString(0, 0, V_SNAPTOTOP|V_SNAPTOLEFT, + va("POS: %d / %d", grandprixinfo.rank.position, RANK_NEUTRAL_POSITION)); + V_DrawThinString(0, 10, V_SNAPTOTOP|V_SNAPTOLEFT, + va("PTS: %d / %d", grandprixinfo.rank.winPoints, grandprixinfo.rank.totalPoints)); + V_DrawThinString(0, 20, V_SNAPTOTOP|V_SNAPTOLEFT, + va("LAPS: %d / %d", grandprixinfo.rank.laps, grandprixinfo.rank.totalLaps)); + V_DrawThinString(0, 30, V_SNAPTOTOP|V_SNAPTOLEFT, + va("CONTINUES: %d", grandprixinfo.rank.continuesUsed)); + V_DrawThinString(0, 40, V_SNAPTOTOP|V_SNAPTOLEFT, + va("PRISONS: %d / %d", grandprixinfo.rank.prisons, grandprixinfo.rank.totalPrisons)); + V_DrawThinString(0, 50, V_SNAPTOTOP|V_SNAPTOLEFT, + va("RINGS: %d / %d", grandprixinfo.rank.rings, grandprixinfo.rank.totalRings)); + V_DrawThinString(0, 60, V_SNAPTOTOP|V_SNAPTOLEFT, + va("EMERALD: %s", (grandprixinfo.rank.specialWon == true) ? "YES" : "NO")); + + switch (grade) + { + case GRADE_E: { gradeChar = 'E'; break; } + case GRADE_D: { gradeChar = 'D'; break; } + case GRADE_C: { gradeChar = 'C'; break; } + case GRADE_B: { gradeChar = 'B'; break; } + case GRADE_A: { gradeChar = 'A'; break; } + case GRADE_S: { gradeChar = 'S'; break; } + default: { break; } + } + + V_DrawThinString(0, 90, V_SNAPTOTOP|V_SNAPTOLEFT|V_YELLOWMAP, + va(" ** FINAL GRADE: %c", gradeChar)); +} + +typedef enum +{ + MM_IN, + MM_HOLD, + MM_OUT, +} messagemode_t; + +typedef struct +{ + std::string text; + sfxenum_t sound; +} message_t; + +struct messagestate_t +{ + std::deque messages; + std::string objective = ""; + tic_t timer = 0; + boolean persist = false; + messagemode_t mode = MM_IN; + const tic_t speedyswitch = 2*TICRATE; + const tic_t lazyswitch = 4*TICRATE; + + void add(std::string msg) + { + messages.push_back(msg); + } + + void clear() + { + messages.clear(); + switch_mode(MM_IN); + } + + void switch_mode(messagemode_t nextmode) + { + mode = nextmode; + timer = 0; + } + + void tick() + { + if (messages.size() == 0) + { + if (!objective.empty()) + restore(); + else + return; + } + + if (exitcountdown) + return; + + if (timer == 0 && mode == MM_IN) + S_StartSound(NULL, sfx_s3k47); + + timer++; + + switch (mode) + { + case MM_IN: + if (timer > messages[0].length()) + switch_mode(MM_HOLD); + break; + case MM_HOLD: + if (messages.size() > 1 && timer > speedyswitch) // Waiting message, switch to it right away! + next(); + else if (timer > lazyswitch && !persist) // If there's no pending message, we can chill for a bit. + switch_mode(MM_OUT); + break; + case MM_OUT: + if (timer > messages[0].length()) + next(); + break; + } + } + + void restore() + { + switch_mode(MM_IN); + persist = true; + messages.clear(); + messages.push_front(objective); + } + + void next() + { + switch_mode(MM_IN); + persist = false; + if (messages.size() > 0) + messages.pop_front(); + } + +}; + +static std::vector messagestates{MAXSPLITSCREENPLAYERS}; + +void K_AddMessage(const char *msg, boolean interrupt, boolean persist) +{ + for (auto &state : messagestates) + { + if (interrupt) + state.clear(); + + std::string parsedmsg = srb2::Draw::TextElement().parse(msg).string(); + + if (persist) + state.objective = parsedmsg; + else + state.add(parsedmsg); + } +} + +void K_ClearPersistentMessages() +{ + for (auto &state : messagestates) + { + state.objective = ""; + state.clear(); + } +} + +// Return value can be used for "paired" splitscreen messages, true = was displayed +void K_AddMessageForPlayer(player_t *player, const char *msg, boolean interrupt, boolean persist) +{ + if (!player) + return; + + if (player && !P_IsDisplayPlayer(player)) + return; + + if (player && K_PlayerUsesBotMovement(player)) + return; + + messagestate_t *state = &messagestates[G_PartyPosition(player - players)]; + + if (interrupt) + state->clear(); + + std::string parsedmsg = srb2::Draw::TextElement().as(player - players).parse(msg).string(); + + if (persist) + state->objective = parsedmsg; + else + state->add(parsedmsg); +} + +void K_ClearPersistentMessageForPlayer(player_t *player) +{ + if (!player) + return; + + if (player && !P_IsDisplayPlayer(player)) + return; + + messagestate_t *state = &messagestates[G_PartyPosition(player - players)]; + state->objective = ""; +} + +void K_TickMessages() +{ + for (auto &state : messagestates) + { + state.tick(); + } +} + +static void K_DrawMessageFeed(void) +{ + int i; + + if (exitcountdown) + return; + + for (i = 0; i <= r_splitscreen; i++) + { + messagestate_t state = messagestates[i]; + + if (state.messages.size() == 0) + continue; + + std::string msg = state.messages[0]; + + UINT8 sublen = state.timer; + if (state.mode == MM_IN) + sublen = state.timer; + else if (state.mode == MM_HOLD) + sublen = msg.length(); + else if (state.mode == MM_OUT) + sublen = msg.length() - state.timer; + + std::string submsg = msg.substr(0, sublen); + + using srb2::Draw; + + Draw::TextElement text(submsg); + + text.font(Draw::Font::kMenu); + + UINT8 x = BASEVIDWIDTH/2; + UINT8 y = 10; + SINT8 shift = 0; + if (r_splitscreen >= 2) + { + text.font(Draw::Font::kThin); + shift = -2; + + x = BASEVIDWIDTH/4; + y = 5; + + if (i % 2) + x += BASEVIDWIDTH/2; + + if (i >= 2) + y += BASEVIDHEIGHT / 2; + } + else if (r_splitscreen >= 1) + { + y = 5; + + if (i >= 1) + y += BASEVIDHEIGHT / 2; + } + + UINT16 sw = text.width(); + + K_DrawSticker(x - sw/2, y, sw, 0, true); + Draw(x, y+shift).align(Draw::Align::kCenter).text(text); + } +} + +void K_drawKartHUD(void) +{ + boolean islonesome = false; + UINT8 viewnum = R_GetViewNumber(); + boolean freecam = camera[viewnum].freecam; //disable some hud elements w/ freecam + + // Define the X and Y for each drawn object + // This is handled by console/menu values + K_initKartHUD(); + + // Draw that fun first person HUD! Drawn ASAP so it looks more "real". + if (!camera[viewnum].chase && !freecam) + K_drawKartFirstPerson(); + + if (mapreset) + { + // HERE COMES A NEW CHALLENGER + if (R_GetViewNumber() == 0) + K_drawChallengerScreen(); + return; + } + + // Draw full screen stuff that turns off the rest of the HUD + if (R_GetViewNumber() == 0) + { + if (g_emeraldWin) + K_drawEmeraldWin(false); + } + + // In case of font debugging break glass +#if 0 + using srb2::Draw; + + if (1) + { + CV_StealthSetValue(cv_descriptiveinput, 0); + Draw::TextElement text = Draw::TextElement().parse("Hamburger Hamburger\n\nHamburger Hamburger\n\nHamburger \xEB\xEF\xA0\xEB\xEF\xA1\xEB\xEF\xA2\xEB\xEF\xA3\xEB\xEF\xA4\xEB\xEF\xA5\xEB\xEF\xA6\xEB\xEF\xA7\xEB\xEF\xA8\xEB\xEF\xA9\xEB\xEF\xAA\xEB\xEF\xAB\xEB\xEF\xAC Hamburger"); + + UINT8 fakeoff = (stplyr - players)*40; + Draw(5, 5+fakeoff).align((srb2::Draw::Align)0).font(Draw::Font::kMenu).text(text); + Draw(40, 80+fakeoff).align((srb2::Draw::Align)0).font(Draw::Font::kThin).text(text); + } + + if (0) + { + Draw::TextElement text = Draw::TextElement().parse("\xEELEFTSPACE\xEE\n\xEESPC\xEE \xEETAB\xEE\nA \xEF\xA0 A\nB \xEF\xA1 B\nX \xEF\xA2 X\nY \xEF\xA3 Y\nLB \xEF\xA4 LB\nRB \xEF\xA5 RB\nLT \xEF\xA6 LT\nRT \xEF\xA7 RT\nST \xEF\xA8 ST\nBK \xEF\xA9 BK\nLS \xEF\xAA LS\nRS \xEF\xAB RS\n"); + + UINT8 offset = 0; + Draw(160+offset, 5).align((srb2::Draw::Align)1).font(Draw::Font::kThin).text(text); + Draw(55+offset, 5).align((srb2::Draw::Align)1).font(Draw::Font::kMenu).text(text); + } + + if (0) + { + Draw::TextElement text = Draw::TextElement().parse("\xEELEFTSPACE\xEE\n\xEESPC\xEE \xEETAB\xEE\nA \xEB\xEF\xA0 A\nB \xEB\xEF\xA1 B\nX \xEB\xEF\xA2 X\nY \xEB\xEF\xA3 Y\nLB \xEB\xEF\xA4 LB\nRB \xEB\xEF\xA5 RB\nLT \xEB\xEF\xA6 LT\nRT \xEB\xEF\xA7 RT\nST \xEB\xEF\xA8 ST\nBK \xEB\xEF\xA9 BK\nLS \xEB\xEF\xAA LS\nRS \xEB\xEF\xAB RS\n"); + + UINT8 offset = 0; + Draw(160+offset, 5).align((srb2::Draw::Align)1).font(Draw::Font::kThin).text(text); + Draw(55+offset, 5).align((srb2::Draw::Align)1).font(Draw::Font::kMenu).text(text); + } +#endif + + + if (!demo.attract) + { + // Draw the CHECK indicator before the other items, so it's overlapped by everything else + if (LUA_HudEnabled(hud_check)) // delete lua when? + if (!splitscreen && !players[displayplayers[0]].exiting && !freecam) + K_drawKartPlayerCheck(); + + // nametags + if (LUA_HudEnabled(hud_names) && R_DrawPickups()) + K_drawKartNameTags(); + + // Draw WANTED status +#if 0 + if (gametype == GT_BATTLE) + { + if (LUA_HudEnabled(hud_wanted)) + K_drawKartWanted(); + } +#endif + + if (LUA_HudEnabled(hud_minimap)) + K_drawKartMinimap(); + } + + if (demo.attract) + ; + else if (gametype == GT_TUTORIAL) + { + islonesome = true; + } + else if (!r_splitscreen) + { + // Draw the timestamp + if (LUA_HudEnabled(hud_time)) + { + bool ta = modeattacking && !demo.playback; + INT32 flags = V_HUDTRANS|V_SLIDEIN|V_SNAPTOTOP|V_SNAPTORIGHT; + + tic_t realtime = stplyr->realtime; + + if (stplyr->karthud[khud_lapanimation] + && !stplyr->exiting + && stplyr->laptime[LAP_LAST] != 0 + && stplyr->laptime[LAP_LAST] != UINT32_MAX) + { + if ((stplyr->karthud[khud_lapanimation] / 5) & 1) + { + realtime = stplyr->laptime[LAP_LAST]; + } + else + { + realtime = UINT32_MAX; + } + } + + if (modeattacking || (gametyperules & GTR_TIMELIMIT) || cv_drawtimer.value) + K_drawKartTimestamp(realtime, TIME_X, TIME_Y + (ta ? 2 : 0), flags, 0); + + if (modeattacking) + { + if (ta) + { + using srb2::Draw; + Draw::TextElement text = Draw::TextElement().parse(" Restart"); + Draw(BASEVIDWIDTH - 19, 2) + .flags(flags | V_YELLOWMAP) + .align(Draw::Align::kRight) + .text(text.string()); + } + else + { + using srb2::Draw; + Draw row = Draw(BASEVIDWIDTH - 20, TIME_Y + 18).flags(flags).align(Draw::Align::kRight); + auto insert = [&](const char *label, UINT32 tics) + { + Draw::TextElement text = + tics != UINT32_MAX ? + Draw::TextElement( + "{:02}'{:02}\"{:02}", + G_TicsToMinutes(tics, true), + G_TicsToSeconds(tics), + G_TicsToCentiseconds(tics) + ) : + Draw::TextElement("--'--\"--"); + text.font(Draw::Font::kZVote); + row.x(-text.width()).flags(V_ORANGEMAP).text(label); + row.y(1).text(text); + row = row.y(10); + }; + if (modeattacking & ATTACKING_TIME) + insert("Finish: ", hu_demotime); + if (modeattacking & ATTACKING_LAP) + insert("Best Lap: ", hu_demolap); + } + } + } + + islonesome = K_drawKartPositionFaces(); + } + else + { + islonesome = M_NotFreePlay() == false; + + if (r_splitscreen == 1) + { + if (LUA_HudEnabled(hud_time)) + { + K_drawKart2PTimestamp(); + } + + if (viewnum == r_splitscreen && gametyperules & GTR_POINTLIMIT) + { + K_drawKartPositionFaces(); + } + } + else if (viewnum == r_splitscreen) + { + if (LUA_HudEnabled(hud_time)) + { + K_drawKart4PTimestamp(); + } + + if (gametyperules & GTR_POINTLIMIT) + { + K_drawKartPositionFaces(); + } + } + } + + if (!stplyr->spectator && !freecam) // Bottom of the screen elements, don't need in spectate mode + { + if (demo.attract) + { + if (demo.attract == DEMO_ATTRACT_TITLE) // Draw logo on title screen demos + { + INT32 x = BASEVIDWIDTH - 8, y = BASEVIDHEIGHT-8, snapflags = V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_SLIDEIN; + patch_t *pat = static_cast(W_CachePatchName((M_UseAlternateTitleScreen() ? "MTSJUMPR1" : "MTSBUMPR1"), PU_CACHE)); + const UINT8 *colormap = nullptr; + + if (INT32 fade = F_AttractDemoExitFade()) + { + // TODO: Twodee cannot handle + // V_DrawCustomFadeScreen. + // However, since the screen fade just + // uses a colormap, the same colormap can + // be applied on a per-patch basis. + // I'm only bothering to apply this + // colormap to the attract mode sticker, + // since it's the lone HUD element. + if (lighttable_t *clm = V_LoadCustomFadeMap("FADEMAP0")) + { + // This must be statically allocated for Twodee + static UINT8 *colormap_storage; + const UINT8 *fadetable = V_OffsetIntoFadeMap(clm, fade); + + if (!colormap_storage) + Z_MallocAlign(256, PU_STATIC, &colormap_storage, 8); + + memcpy(colormap_storage, fadetable, 256); + colormap = colormap_storage; + + Z_Free(clm); + } + } + + if (r_splitscreen == 3) + { + x = BASEVIDWIDTH/2; + y = BASEVIDHEIGHT/2; + snapflags = 0; + } + + V_DrawMappedPatch(x-(SHORT(pat->width)), y-(SHORT(pat->height)), snapflags, pat, colormap); + } + } + else + { + boolean gametypeinfoshown = false; + + if (K_PlayerTallyActive(stplyr) == true) + { + K_DrawPlayerTally(); + } + + if (LUA_HudEnabled(hud_position)) + { + if (bossinfo.valid) + { + K_drawBossHealthBar(); + } + else if (freecam) + ; + else if ((gametyperules & GTR_POWERSTONES) && !K_PlayerTallyActive(stplyr)) + { + if (!battleprisons) + K_drawKartEmeralds(); + } + else if (!islonesome && !K_Cooperative()) + { + K_DrawKartPositionNum(stplyr->position); + } + } + + if (G_GametypeHasTeams() == true) + { + K_drawKartTeamScores(); + } + + if (LUA_HudEnabled(hud_gametypeinfo)) + { + if (gametyperules & GTR_CIRCUIT) + { + K_drawKartLaps(); + gametypeinfoshown = true; + } + else if (gametyperules & GTR_BUMPERS) + { + K_drawKartBumpersOrKarma(); + gametypeinfoshown = true; + } + } + + // Draw the speedometer and/or accessibility icons + if (cv_kartspeedometer.value && !r_splitscreen && (LUA_HudEnabled(hud_speedometer))) + { + K_drawKartSpeedometer(gametypeinfoshown); + } + else + { + K_drawKartAccessibilityIcons(gametypeinfoshown, 0); + } + + if (gametyperules & GTR_SPHERES) + { + K_drawBlueSphereMeter(gametypeinfoshown); + } + else + { + K_drawRingCounter(gametypeinfoshown); + } + + // Draw the item window + if (LUA_HudEnabled(hud_item) && !freecam) + { + if (stplyr->itemRoulette.ringbox && stplyr->itemamount == 0 && stplyr->itemtype == 0) + { + K_drawKartSlotMachine(); + } + else + { + K_drawKartItem(); + } + } + } + } + + // TODO better voice chat speaking indicator integration for spectators + { + char speakingstring[2048]; + memset(speakingstring, 0, sizeof(speakingstring)); + + for (int i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] && players[i].spectator && S_IsPlayerVoiceActive(i)) + { + strcat(speakingstring, player_names[i]); + strcat(speakingstring, " "); + } + } + + V_DrawThinString(0, 0, V_SNAPTOTOP|V_SNAPTOLEFT, speakingstring); + } + + // Draw the countdowns after everything else. + if (stplyr->lives <= 0 && stplyr->playerstate == PST_DEAD) + { + ; + } + else if (stplyr->karthud[khud_fault] != 0 && stplyr->karthud[khud_finish] == 0) + { + K_drawKartFinish(false); + } + else if (starttime != introtime + && leveltime >= introtime + && leveltime < starttime+TICRATE) + { + K_drawKartStartCountdown(); + } + else if (racecountdown && (!r_splitscreen || !stplyr->exiting)) + { + char *countstr = va("%d", racecountdown/TICRATE); + + if (r_splitscreen > 1) + V_DrawCenteredString(BASEVIDWIDTH/4, LAPS_Y+1, V_SPLITSCREEN, countstr); + else + { + INT32 karlen = strlen(countstr)*6; // half of 12 + V_DrawTimerString((BASEVIDWIDTH/2)-karlen, LAPS_Y+3, V_SPLITSCREEN, countstr); + } + } + + // Race overlays + if (!freecam) + { + if (stplyr->exiting) + K_drawKartFinish(true); + else if (!(gametyperules & GTR_CIRCUIT)) + ; + else if (stplyr->karthud[khud_lapanimation] && !r_splitscreen) + K_drawLapStartAnim(); + } + + // trick panel cool trick + if (stplyr->karthud[khud_trickcool]) + K_drawTrickCool(); + + if ((freecam || stplyr->spectator) && LUA_HudEnabled(hud_textspectator)) + { + K_drawSpectatorHUD(false); + } + + if (R_GetViewNumber() == 0 && g_emeraldWin) + K_drawEmeraldWin(true); + + if (modeattacking || freecam) // everything after here is MP and debug only + { + K_drawInput(); + goto debug; + } + + if ((gametyperules & GTR_KARMA) && !r_splitscreen && (stplyr->karthud[khud_yougotem] % 2)) // * YOU GOT EM * + V_DrawScaledPatch(BASEVIDWIDTH/2 - (SHORT(kp_yougotem->width)/2), 32, V_HUDTRANS, kp_yougotem); + + // Draw FREE PLAY. + K_drawKartFreePlay(); + + if ((netgame || cv_mindelay.value) && r_splitscreen && Playing()) + { + K_drawMiniPing(); + } + + K_drawKartPowerUps(); + + if (K_DirectorIsAvailable(viewnum) == true && LUA_HudEnabled(hud_textspectator)) + { + K_drawSpectatorHUD(true); + } + else + { + K_drawInput(); + } + + if (cv_kartdebugdistribution.value) + K_drawDistributionDebugger(); + + if (cv_kartdebugnodes.value) + { + UINT8 p; + for (p = 0; p < MAXPLAYERS; p++) + V_DrawString(8, 64+(8*p), V_YELLOWMAP, va("%d - %d (%dl)", p, playernode[p], players[p].cmd.latency)); + } + + if (cv_kartdebugcolorize.value && stplyr->mo && stplyr->mo->skin) + { + INT32 x = 0, y = 0; + UINT16 c; + + for (c = 0; c < numskincolors; c++) + { + if (skincolors[c].accessible) + { + UINT8 *cm = R_GetTranslationColormap(TC_RAINBOW, static_cast(c), GTC_CACHE); + V_DrawFixedPatch(x<>1, 0, faceprefix[stplyr->skin][FACE_WANTED], cm); + + x += 16; + if (x > BASEVIDWIDTH-16) + { + x = 0; + y += 16; + } + } + } + } + + if (netgame && cv_voice_servermute.value == 0) + { + if (players[consoleplayer].pflags2 & (PF2_SELFMUTE | PF2_SERVERMUTE | PF2_SELFDEAFEN | PF2_SERVERDEAFEN)) + { + patch_t* micmuted = kp_voice_localmuted; + V_DrawFixedPatch(-1 * FRACUNIT, (BASEVIDHEIGHT - 21) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, micmuted, NULL); + } + else if (S_IsPlayerVoiceActive(consoleplayer)) + { + patch_t* micactivebase = kp_voice_localactive[(leveltime / 2) % 16]; + patch_t* micactivetop = kp_voice_localactiveoverlay[(leveltime / 2) % 16]; + + UINT8* micactivecolormap = NULL; + if (g_local_voice_last_peak < 0.7) + { + micactivecolormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_GREEN, GTC_CACHE); + } + else if (g_local_voice_last_peak < 0.95) + { + micactivecolormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_YELLOW, GTC_CACHE); + } + else + { + micactivecolormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_RED, GTC_CACHE); + } + V_DrawFixedPatch(-15 * FRACUNIT, (BASEVIDHEIGHT - 34) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, micactivebase, micactivecolormap); + V_DrawFixedPatch(-15 * FRACUNIT, (BASEVIDHEIGHT - 34) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, micactivetop, micactivecolormap); + } + else + { + patch_t* micopen = kp_voice_localopen; + V_DrawFixedPatch(-1 * FRACUNIT, (BASEVIDHEIGHT - 21) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, micopen, NULL); + } + + // Deafen indicator + if (players[consoleplayer].pflags2 & (PF2_SELFDEAFEN | PF2_SERVERDEAFEN)) + { + patch_t* deafened = kp_voice_localdeafened; + V_DrawFixedPatch(16 * FRACUNIT, (BASEVIDHEIGHT - 15) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, deafened, NULL); + } + } + +debug: + K_DrawWaypointDebugger(); + K_DrawBotDebugger(); + K_DrawDirectorDebugger(); + K_DrawGPRankDebugger(); + K_DrawMessageFeed(); +} + +void K_DrawSticker(INT32 x, INT32 y, INT32 width, INT32 flags, boolean isSmall) +{ + patch_t *stickerEnd; + INT32 height; + + if (isSmall == true) + { + stickerEnd = static_cast(W_CachePatchName("K_STIKE2", PU_CACHE)); + height = 6; + } + else + { + stickerEnd = static_cast(W_CachePatchName("K_STIKEN", PU_CACHE)); + height = 11; + } + + V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, flags, stickerEnd, NULL); + V_DrawFill(x, y, width, height, 24|flags); + V_DrawFixedPatch((x + width)*FRACUNIT, y*FRACUNIT, FRACUNIT, flags|V_FLIP, stickerEnd, NULL); +} + +void K_DrawMarginSticker(INT32 x, INT32 y, INT32 width, INT32 flags, boolean isSmall, boolean leftedge) +{ + patch_t *stickerEnd; + INT32 height; + + if (isSmall == true) + { + stickerEnd = static_cast(W_CachePatchName("K_STIKE2", PU_CACHE)); + height = 6; + } + else + { + stickerEnd = static_cast(W_CachePatchName("K_STIKEN", PU_CACHE)); + height = 11; + } + + if (leftedge) + V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, flags, stickerEnd, NULL); + V_DrawFill(x, y, width, height, 24|flags); + if (!leftedge) + V_DrawFixedPatch((x + width)*FRACUNIT, y*FRACUNIT, FRACUNIT, flags|V_FLIP, stickerEnd, NULL); +} + +// common fonts: 0 = thin, 8 = menu. sorry we have to launder a C++ enum in here +INT32 K_DrawGameControl(UINT16 x, UINT16 y, UINT8 player, const char *str, UINT8 alignment, UINT8 font, UINT32 flags) +{ + using srb2::Draw; + + Draw::TextElement text = Draw::TextElement().as(player).parse(str).font((Draw::Font)font); + + INT32 width = text.width(); + + Draw(x, y).align((srb2::Draw::Align)alignment).flags(flags).text(text); + + return width; +} diff --git a/src/k_menu.h b/src/k_menu.h index 01681be74..b61b37cd8 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -346,6 +346,7 @@ typedef enum mopt_profiles = 0, mopt_video, mopt_sound, + mopt_voice, mopt_hud, mopt_gameplay, mopt_server, @@ -468,6 +469,9 @@ extern menu_t OPTIONS_VideoAdvancedDef; extern menuitem_t OPTIONS_Sound[]; extern menu_t OPTIONS_SoundDef; +extern menuitem_t OPTIONS_Voice[]; +extern menu_t OPTIONS_VoiceDef; + extern menuitem_t OPTIONS_HUD[]; extern menu_t OPTIONS_HUDDef; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 8e54b48a4..d047e477b 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4178,6 +4178,8 @@ void M_DrawMPServerBrowser(void) servpats[i] = W_CachePatchName(va("M_SERV%c", i + '1'), PU_CACHE); gearpats[i] = W_CachePatchName(va("M_SGEAR%c", i + '1'), PU_CACHE); } + patch_t *voicepat; + voicepat = W_CachePatchName("VOCRMU", PU_CACHE); fixed_t text1loop = SHORT(text1->height)*FRACUNIT; fixed_t text2loop = SHORT(text2->width)*FRACUNIT; @@ -4279,6 +4281,12 @@ void M_DrawMPServerBrowser(void) V_DrawFixedPatch((startx + 251)*FRACUNIT, (starty + ypos + 9)*FRACUNIT, FRACUNIT, transflag, gearpats[speed], NULL); } } + + // voice chat enabled + if (serverlist[i].info.kartvars & SV_VOICEENABLED) + { + V_DrawFixedPatch((startx - 3) * FRACUNIT, (starty + 2) * FRACUNIT, FRACUNIT, 0, voicepat, NULL); + } } ypos += SERVERSPACE; } diff --git a/src/k_profiles.cpp b/src/k_profiles.cpp index e0f35b159..66551a85a 100644 --- a/src/k_profiles.cpp +++ b/src/k_profiles.cpp @@ -310,10 +310,12 @@ void PR_SaveProfiles(void) for (size_t j = 0; j < num_gamecontrols; j++) { + std::vector mappings; for (size_t k = 0; k < MAXINPUTMAPPING; k++) { - jsonprof.controls[j][k] = cprof->controls[j][k]; + mappings.push_back(cprof->controls[j][k]); } + jsonprof.controls.emplace_back(std::move(mappings)); } ng.profiles.emplace_back(std::move(jsonprof)); @@ -498,9 +500,24 @@ void PR_LoadProfiles(void) { for (size_t j = 0; j < num_gamecontrols; j++) { + if (jsprof.controls.size() <= j) + { + for (size_t k = 0; k < MAXINPUTMAPPING; k++) + { + newprof->controls[j][k] = gamecontroldefault[j][k]; + } + continue; + } + + auto& mappings = jsprof.controls.at(j); for (size_t k = 0; k < MAXINPUTMAPPING; k++) { - newprof->controls[j][k] = jsprof.controls.at(j).at(k); + if (mappings.size() <= k) + { + newprof->controls[j][k] = 0; + continue; + } + newprof->controls[j][k] = mappings.at(k); } } } diff --git a/src/k_profiles.h b/src/k_profiles.h index 0a2874dd4..4719b3178 100644 --- a/src/k_profiles.h +++ b/src/k_profiles.h @@ -77,7 +77,7 @@ struct ProfileJson std::string followercolorname; ProfileRecordsJson records; ProfilePreferencesJson preferences; - std::array, gamecontrols_e::num_gamecontrols> controls = {{{{}}}}; + std::vector> controls = {}; NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( ProfileJson, diff --git a/src/k_vote.c b/src/k_vote.c index 9f3ba2c32..33bba9c49 100644 --- a/src/k_vote.c +++ b/src/k_vote.c @@ -1015,6 +1015,23 @@ void Y_VoteDrawer(void) ); } + // TODO better voice chat speaking indicator integration + { + char speakingstring[2048]; + memset(speakingstring, 0, sizeof(speakingstring)); + + for (int i = 0; i < MAXPLAYERS; i++) + { + if (S_IsPlayerVoiceActive(i)) + { + strcat(speakingstring, player_names[i]); + strcat(speakingstring, " "); + } + } + + V_DrawThinString(0, 0, 0, speakingstring); + } + M_DrawMenuForeground(); } diff --git a/src/k_zvote.c b/src/k_zvote.c index 0ba40a708..5e2ff2131 100644 --- a/src/k_zvote.c +++ b/src/k_zvote.c @@ -58,6 +58,25 @@ static void K_MidVoteKick(void) SendKick(g_midVote.victim - players, KICK_MSG_VOTE_KICK); } +/*-------------------------------------------------- + static void K_MidVoteMute(void) + + MVT_MUTE's success function. +--------------------------------------------------*/ +static void K_MidVoteMute(void) +{ + UINT8 buf[2]; + + if (g_midVote.victim == NULL) + { + return; + } + + buf[0] = g_midVote.victim - players; + buf[1] = 1; + SendNetXCmd(XD_SERVERMUTEPLAYER, &buf, 2); +} + /*-------------------------------------------------- static void K_MidVoteRockTheVote(void) @@ -99,6 +118,13 @@ static midVoteTypeDef_t g_midVoteTypeDefs[MVT__MAX] = K_MidVoteKick }, + { // MVT_MUTE + "MUTE", + "Mute Player?", + CVAR_INIT ("zvote_mute_allowed", "Yes", CV_SAVE|CV_NETVAR, CV_YesNo, NULL), + K_MidVoteMute + }, + { // MVT_RTV "RTV", "Skip Level?", @@ -127,6 +153,10 @@ boolean K_MidVoteTypeUsesVictim(midVoteType_e voteType) { return true; } + case MVT_MUTE: + { + return true; + } default: { return false; @@ -1186,6 +1216,7 @@ void K_DrawMidVote(void) switch (g_midVote.type) { case MVT_KICK: + case MVT_MUTE: { // Draw victim name if (g_midVote.victim != NULL) diff --git a/src/k_zvote.h b/src/k_zvote.h index 3aac7f1e2..81baa3039 100644 --- a/src/k_zvote.h +++ b/src/k_zvote.h @@ -27,6 +27,7 @@ extern "C" { typedef enum { MVT_KICK, // Kick another player in the server + MVT_MUTE, // Mute another player in the server (Voice Chat) MVT_RTV, // Exit level early MVT_RUNITBACK, // Restart level fresh MVT__MAX, // Total number of vote types diff --git a/src/m_swap.h b/src/m_swap.h index 23f886401..a5c9ed9b8 100644 --- a/src/m_swap.h +++ b/src/m_swap.h @@ -35,18 +35,39 @@ extern "C" { | \ (((UINT32)(x) & (UINT32)0xff000000UL) >> 24))) +#define SWAP_LONGLONG(x) ((INT64)(\ +(((UINT64)(x) & (UINT64)0x00000000000000ffULL) << 56) \ +| \ +(((UINT64)(x) & (UINT64)0x000000000000ff00ULL) << 40) \ +| \ +(((UINT64)(x) & (UINT64)0x0000000000ff0000ULL) << 24) \ +| \ +(((UINT64)(x) & (UINT64)0x00000000ff000000ULL) << 8) \ +| \ +(((UINT64)(x) & (UINT64)0x000000ff00000000ULL) >> 8) \ +| \ +(((UINT64)(x) & (UINT64)0x0000ff0000000000ULL) >> 24) \ +| \ +(((UINT64)(x) & (UINT64)0x00ff000000000000ULL) >> 40) \ +| \ +(((UINT64)(x) & (UINT64)0xff00000000000000ULL) >> 56))) + // Endianess handling. // WAD files are stored little endian. #ifdef SRB2_BIG_ENDIAN #define SHORT SWAP_SHORT #define LONG SWAP_LONG +#define LONGLON SWAP_LONGLONG #define MSBF_SHORT(x) ((INT16)(x)) #define MSBF_LONG(x) ((INT32)(x)) +#define MSBF_LONGLONG(x) ((INT64)(x)) #else #define SHORT(x) ((INT16)(x)) #define LONG(x) ((INT32)(x)) +#define LONGLONG(x) ((INT64)(x)) #define MSBF_SHORT SWAP_SHORT #define MSBF_LONG SWAP_LONG +#define MSBF_LONGLONG SWAP_LONGLONG #endif // Big to little endian diff --git a/src/menus/CMakeLists.txt b/src/menus/CMakeLists.txt index 80d3c261e..c82b41316 100644 --- a/src/menus/CMakeLists.txt +++ b/src/menus/CMakeLists.txt @@ -29,6 +29,7 @@ target_sources(SRB2SDL2 PRIVATE options-video-1.c options-video-advanced.c options-video-modes.c + options-voice.cpp play-1.c play-char-select.c play-local-1.c diff --git a/src/menus/options-1.c b/src/menus/options-1.c index 3b390abd6..92317444c 100644 --- a/src/menus/options-1.c +++ b/src/menus/options-1.c @@ -31,6 +31,9 @@ menuitem_t OPTIONS_Main[] = {IT_STRING | IT_CALL, "Sound Options", "Adjust the volume.", NULL, {.routine = M_SoundOptions}, 0, 0}, + {IT_STRING | IT_SUBMENU, "Voice Options", "Adjust voice chat.", + NULL, {.submenu = &OPTIONS_VoiceDef}, 0, 0}, + {IT_STRING | IT_SUBMENU, "HUD Options", "Tweak the Heads-Up Display.", NULL, {.submenu = &OPTIONS_HUDDef}, 0, 0}, diff --git a/src/menus/options-profiles-edit-controls.c b/src/menus/options-profiles-edit-controls.c index 1cfa086cf..7c50aba8e 100644 --- a/src/menus/options-profiles-edit-controls.c +++ b/src/menus/options-profiles-edit-controls.c @@ -87,6 +87,9 @@ menuitem_t OPTIONS_ProfileControls[] = { {IT_CONTROL, "OPEN TEAM CHAT", "Opens team-only full chat for online games.", NULL, {.routine = M_ProfileSetControl}, gc_teamtalk, 0}, + {IT_CONTROL, "PUSH-TO-TALK", "Activates voice chat transmission in Push-to-Talk (PTT) mode.", + NULL, {.routine = M_ProfileSetControl}, gc_voicepushtotalk, 0}, + {IT_CONTROL, "LUA/1", "May be used by add-ons.", NULL, {.routine = M_ProfileSetControl}, gc_lua1, 0}, @@ -310,14 +313,14 @@ boolean M_ProfileControlsInputs(INT32 ch) S_StartSound(NULL, sfx_kc69); if (newbuttons & MBT_R) S_StartSound(NULL, sfx_s3ka2); - + if (newbuttons & MBT_A) S_StartSound(NULL, sfx_kc3c); if (newbuttons & MBT_B) S_StartSound(NULL, sfx_3db09); if (newbuttons & MBT_C) S_StartSound(NULL, sfx_s1be); - + if (newbuttons & MBT_X) S_StartSound(NULL, sfx_s1a4); if (newbuttons & MBT_Y) diff --git a/src/menus/options-server-1.c b/src/menus/options-server-1.c index e489045c8..ca7634210 100644 --- a/src/menus/options-server-1.c +++ b/src/menus/options-server-1.c @@ -29,7 +29,7 @@ menuitem_t OPTIONS_Server[] = {IT_HEADER, "Players...", NULL, NULL, {NULL}, 0, 0}, - + {IT_STRING | IT_CVAR, "Maximum Players", "How many players can play at once.", NULL, {.cvar = &cv_maxplayers}, 0, 0}, @@ -40,7 +40,7 @@ menuitem_t OPTIONS_Server[] = NULL, {.cvar = &cv_kartbot}, 0, 0}, {IT_STRING | IT_CVAR, "Use PWR.LV", "Should players should be rated on their performance?", - NULL, {.cvar = &cv_kartusepwrlv}, 0, 0}, + NULL, {.cvar = &cv_kartusepwrlv}, 0, 0}, {IT_STRING | IT_CVAR, "Antigrief Timer (seconds)", "How long can players stop progressing before they're removed?", NULL, {.cvar = &cv_antigrief}, 0, 0}, @@ -78,6 +78,9 @@ menuitem_t OPTIONS_Server[] = {IT_STRING | IT_CVAR, "Mute Chat", "Prevent everyone but admins from sending chat messages.", NULL, {.cvar = &cv_mute}, 0, 0}, + {IT_STRING | IT_CVAR, "Mute Voice Chat", "Prevent everyone from sending voice chat.", + NULL, {.cvar = &cv_voice_servermute}, 0, 0}, + {IT_STRING | IT_CVAR, "Chat Spam Protection", "Prevent too many messages from a single player.", NULL, {.cvar = &cv_chatspamprotection}, 0, 0}, diff --git a/src/menus/options-sound.cpp b/src/menus/options-sound.cpp index e2ad9341b..8a5d6d9a3 100644 --- a/src/menus/options-sound.cpp +++ b/src/menus/options-sound.cpp @@ -38,6 +38,7 @@ struct Slider kMasterVolume, kMusicVolume, kSfxVolume, + kVoiceVolume, kNumSliders }; @@ -120,6 +121,7 @@ std::array sliders{{ n = !n; CV_SetValue(&cv_gamedigimusic, n); CV_SetValue(&cv_gamesounds, n); + CV_SetValue(&cv_voice_chat, n); } return n; @@ -150,6 +152,18 @@ std::array sliders{{ }, cv_soundvolume, }, + { + [](bool toggle) -> bool + { + if (toggle) + { + CV_AddValue(&cv_voice_chat, 1); + } + + return !S_VoiceDisabled(); + }, + cv_voicevolume, + }, }}; void slider_routine(INT32 c) @@ -266,6 +280,9 @@ menuitem_t OPTIONS_Sound[] = {IT_STRING | IT_ARROWS | IT_CV_SLIDER, "Music Volume", "Loudness of music.", NULL, {.routine = slider_routine}, 0, Slider::kMusicVolume}, + {IT_STRING | IT_ARROWS | IT_CV_SLIDER, "Voice Volume", "Loudness of voice chat.", + NULL, {.routine = slider_routine}, 0, Slider::kVoiceVolume}, + {IT_SPACE | IT_NOTHING, NULL, NULL, NULL, {NULL}, 0, 0}, diff --git a/src/menus/options-voice.cpp b/src/menus/options-voice.cpp new file mode 100644 index 000000000..cd9fd6efa --- /dev/null +++ b/src/menus/options-voice.cpp @@ -0,0 +1,91 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by Kart Krew. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file menus/options-voice.cpp +/// \brief Voice Options + +#include "../m_easing.h" +#include "../k_menu.h" +#include "../s_sound.h" // sounds consvars +#include "../v_video.h" + +menuitem_t OPTIONS_Voice[] = +{ + {IT_STRING | IT_CVAR, "Voice Chat", "Turn on or off all voice chat for yourself.", + NULL, {.cvar = &cv_voice_chat}, 0, 0}, + + {IT_STRING | IT_CVAR, "Voice Mode", "When to transmit your own voice.", + NULL, {.cvar = &cv_voice_mode}, 0, 0}, + + {IT_STRING | IT_CVAR, "Input Amplifier", "Amplify your voice, in decibels. Negative values are quieter.", + NULL, {.cvar = &cv_voice_inputamp}, 0, 0}, + + {IT_STRING | IT_CVAR, "Activation Threshold", "Voice higher than this threshold will transmit, in decibels.", + NULL, {.cvar = &cv_voice_activationthreshold}, 0, 0}, + + {IT_STRING | IT_CVAR, "Self Voice Mute", "Whether your voice is transmitted or not.", + NULL, {.cvar = &cv_voice_selfmute}, 0, 0}, + + {IT_STRING | IT_CVAR, "Voice Loopback", "Play your own recording voice back.", + NULL, {.cvar = &cv_voice_loopback}, 0, 0}, + + {IT_SPACE | IT_NOTHING, NULL, NULL, + NULL, {NULL}, 0, 0}, + + {IT_HEADER, "Server Voice Options...", NULL, + NULL, {NULL}, 0, 0}, + + {IT_STRING | IT_CVAR, "Server Voice Mute", "All voice chat will be disabled on your server.", + NULL, {.cvar = &cv_voice_servermute}, 0, 0}, + + {IT_STRING | IT_CVAR, "Proximity Effects", "Player voices will be adjusted relative to you.", + NULL, {.cvar = &cv_voice_proximity}, 0, 0}, +}; + +static void draw_routine() +{ + M_DrawGenericOptions(); + + int x = currentMenu->x - M_EaseWithTransition(Easing_Linear, 5 * 48); + int y = currentMenu->y - 12; + int range = 220; + float last_peak = g_local_voice_last_peak * range; + boolean detected = g_local_voice_detected; + INT32 color = detected ? 65 : 23; + + V_DrawFill(x, y, range + 2, 10, 31); + V_DrawFill(x + 1, y + 1, (int) last_peak, 8, color); +} + +static void tick_routine() +{ + M_OptionsTick(); +} + +static boolean input_routine(INT32) +{ + return false; +} + +menu_t OPTIONS_VoiceDef = { + sizeof (OPTIONS_Voice) / sizeof (menuitem_t), + &OPTIONS_MainDef, + 0, + OPTIONS_Voice, + 48, 80, + SKINCOLOR_ULTRAMARINE, 0, + MBF_DRAWBGWHILEPLAYING, + NULL, + 2, 5, + draw_routine, + M_DrawOptionsCogs, + tick_routine, + NULL, + NULL, + input_routine, +}; diff --git a/src/s_sound.c b/src/s_sound.c index 4369f9250..b27c5b5e4 100644 --- a/src/s_sound.c +++ b/src/s_sound.c @@ -266,6 +266,14 @@ boolean S_SoundDisabled(void) ); } +boolean S_VoiceDisabled(void) +{ + return ( + g_voice_disabled || + (g_fast_forward > 0) + ); +} + // Stop all sounds, load level info, THEN start sounds. void S_StopSounds(void) { @@ -676,6 +684,13 @@ void S_StopSound(void *origin) static INT32 actualsfxvolume; // check for change through console static INT32 actualdigmusicvolume; static INT32 actualmastervolume; +static INT32 actualvoicevolume; + +static boolean PointIsLeft(float ax, float ay, float bx, float by) +{ + // return (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x) > 0; + return ax * by - ay * bx > 0; +} void S_UpdateSounds(void) { @@ -694,6 +709,8 @@ void S_UpdateSounds(void) S_SetMusicVolume(); if (actualmastervolume != cv_mastervolume.value) S_SetMasterVolume(); + if (actualvoicevolume != cv_voicevolume.value) + S_SetVoiceVolume(); // We're done now, if we're not in a level. if (gamestate != GS_LEVEL) @@ -853,6 +870,154 @@ notinlevel: I_UpdateSound(); } +void S_UpdateVoicePositionalProperties(void) +{ + int i; + + if (gamestate != GS_LEVEL) + { + for (i = 0; i < MAXPLAYERS; i++) + { + I_SetPlayerVoiceProperties(i, 1.0f, 0.0f); + } + return; + } + + player_t *consoleplr = &players[consoleplayer]; + listener_t listener = {0}; + mobj_t *listenmobj = NULL; + + if (consoleplr) + { + if (consoleplr->awayview.tics) + { + listenmobj = consoleplr->awayview.mobj; + } + else + { + listenmobj = consoleplr->mo; + } + + if (camera[0].chase && !consoleplr->awayview.tics) + { + listener.x = camera[0].x; + listener.y = camera[0].y; + listener.z = camera[0].z; + listener.angle = camera[0].angle; + } + else if (listenmobj) + { + listener.x = listenmobj->x; + listener.y = listenmobj->y; + listener.z = listenmobj->z; + listener.angle = listenmobj->angle; + } + } + + float playerdistances[MAXPLAYERS]; + for (i = 0; i < MAXPLAYERS; i++) + { + player_t *plr = &players[i]; + mobj_t *mo = plr->mo; + if (plr->spectator || !mo) + { + playerdistances[i] = 0.f; + continue; + } + float px = FixedToFloat(mo->x - listener.x); + float py = FixedToFloat(mo->y - listener.y); + float pz = FixedToFloat(mo->z - listener.z); + playerdistances[i] = sqrtf(px * px + py * py + pz * pz); + } + + // Positional voice audio + boolean voice_proximity_enabled = cv_voice_proximity.value == 1; + float voice_distanceattenuation_distance = FixedToFloat(cv_voice_distanceattenuation_distance.value) * FixedToFloat(mapheaderinfo[gamemap-1]->mobj_scale); + float voice_distanceattenuation_factor = FixedToFloat(cv_voice_distanceattenuation_factor.value); + float voice_stereopanning_factor = FixedToFloat(cv_voice_stereopanning_factor.value); + float voice_concurrentattenuation_min = max(0, min(MAXPLAYERS, cv_voice_concurrentattenuation_min.value)); + float voice_concurrentattenuation_max = max(0, min(MAXPLAYERS, cv_voice_concurrentattenuation_max.value)); + voice_concurrentattenuation_min = min(voice_concurrentattenuation_max, voice_concurrentattenuation_min); + float voice_concurrentattenuation_factor = FixedToFloat(cv_voice_concurrentattenuation_factor.value); + + // Derive concurrent speaker attenuation + float speakingplayers = 0; + for (i = 0; i < MAXPLAYERS; i++) + { + if (S_IsPlayerVoiceActive(i)) + { + if (voice_distanceattenuation_distance > 0) + { + speakingplayers += 1.f - (playerdistances[i] / voice_distanceattenuation_distance); + } + else + { + // invalid distance attenuation + speakingplayers += 1.f; + } + } + } + speakingplayers = min(voice_concurrentattenuation_max, max(0, speakingplayers - voice_concurrentattenuation_min)); + float speakingplayerattenuation = 1.f - (speakingplayers * (voice_concurrentattenuation_factor / voice_concurrentattenuation_max)); + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || !voice_proximity_enabled) + { + I_SetPlayerVoiceProperties(i, speakingplayerattenuation, 0.0f); + continue; + } + + if (i == consoleplayer) + { + I_SetPlayerVoiceProperties(i, speakingplayerattenuation, 0.0f); + continue; + } + + player_t *plr = &players[i]; + if (plr->spectator) + { + I_SetPlayerVoiceProperties(i, speakingplayerattenuation, 0.0f); + continue; + } + + mobj_t *plrmobj = plr->mo; + + if (!plrmobj) + { + I_SetPlayerVoiceProperties(i, 1.0f, 0.0f); + continue; + } + + float lx = FixedToFloat(listener.x); + float ly = FixedToFloat(listener.y); + float lz = FixedToFloat(listener.z); + float px = FixedToFloat(plrmobj->x) - lx; + float py = FixedToFloat(plrmobj->y) - ly; + float pz = FixedToFloat(plrmobj->z) - lz; + + float ldirx = cosf(ANG2RAD(listener.angle)); + float ldiry = sinf(ANG2RAD(listener.angle)); + float pdistance = sqrtf(px * px + py * py + pz * pz); + float p2ddistance = sqrtf(px * px + py * py); + float pdirx = px / p2ddistance; + float pdiry = py / p2ddistance; + float angle = acosf(pdirx * ldirx + pdiry * ldiry); + angle = PointIsLeft(ldirx, ldiry, pdirx, pdiry) ? -angle : angle; + + float plrvolume = 1.0f; + if (voice_distanceattenuation_distance > 0 && voice_distanceattenuation_factor >= 0 && voice_distanceattenuation_factor <= 1.0f) + { + float invfactor = 1.0f - voice_distanceattenuation_factor; + float distfactor = max(0.f, min(voice_distanceattenuation_distance, pdistance)) / voice_distanceattenuation_distance; + plrvolume = max(0.0f, min(1.0f, 1.0f - (invfactor * distfactor))); + } + + float fsep = sinf(angle) * max(0.0f, min(1.0f, voice_stereopanning_factor)); + I_SetPlayerVoiceProperties(i, plrvolume * speakingplayerattenuation, fsep); + } +} + void S_UpdateClosedCaptions(void) { UINT8 i; @@ -887,6 +1052,13 @@ void S_SetSfxVolume(void) I_SetSfxVolume(actualsfxvolume); } +void S_SetVoiceVolume(void) +{ + actualvoicevolume = cv_voicevolume.value; + + I_SetVoiceVolume(actualvoicevolume); +} + void S_SetMasterVolume(void) { actualmastervolume = cv_mastervolume.value; @@ -2639,6 +2811,18 @@ void GameDigiMusic_OnChange(void) } } +void VoiceChat_OnChange(void); +void weaponPrefChange(INT32 ssplayer); +void VoiceChat_OnChange(void) +{ + if (M_CheckParm("-novoice") || M_CheckParm("-noaudio")) + return; + + g_voice_disabled = !cv_voice_chat.value; + + weaponPrefChange(0); +} + void BGAudio_OnChange(void); void BGAudio_OnChange(void) { @@ -2656,3 +2840,53 @@ void BGAudio_OnChange(void) if (window_notinfocus && !(cv_bgaudio.value & 2)) S_StopSounds(); } + + +boolean S_SoundInputIsEnabled(void) +{ + return I_SoundInputIsEnabled(); +} + +boolean S_SoundInputSetEnabled(boolean enabled) +{ + return I_SoundInputSetEnabled(enabled); +} + +UINT32 S_SoundInputDequeueSamples(void *data, UINT32 len) +{ + return I_SoundInputDequeueSamples(data, len); +} + +static INT32 g_playerlastvoiceactive[MAXPLAYERS]; + +void S_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal) +{ + if (dedicated) + { + return; + } + if (cv_voice_chat.value != 0) + { + I_QueueVoiceFrameFromPlayer(playernum, data, len, terminal); + } +} + +void S_SetPlayerVoiceActive(INT32 playernum) +{ + g_playerlastvoiceactive[playernum] = I_GetTime(); +} + +boolean S_IsPlayerVoiceActive(INT32 playernum) +{ + return I_GetTime() - g_playerlastvoiceactive[playernum] < 5; +} + +void S_ResetVoiceQueue(INT32 playernum) +{ + if (dedicated) + { + return; + } + I_ResetVoiceQueue(playernum); + g_playerlastvoiceactive[playernum] = 0; +} diff --git a/src/s_sound.h b/src/s_sound.h index 1a7b6a8c9..73f62577b 100644 --- a/src/s_sound.h +++ b/src/s_sound.h @@ -35,6 +35,7 @@ extern "C" { extern consvar_t stereoreverse; extern consvar_t cv_soundvolume, cv_closedcaptioning, cv_digmusicvolume; +extern consvar_t cv_voicevolume; extern consvar_t surround; extern consvar_t cv_numChannels; @@ -47,6 +48,22 @@ extern consvar_t cv_gamesounds; extern consvar_t cv_bgaudio; extern consvar_t cv_streamersafemusic; +extern consvar_t cv_voice_chat; +extern consvar_t cv_voice_mode; +extern consvar_t cv_voice_selfmute; +extern consvar_t cv_voice_loopback; +extern consvar_t cv_voice_inputamp; +extern consvar_t cv_voice_activationthreshold; +extern consvar_t cv_voice_proximity; +extern consvar_t cv_voice_distanceattenuation_distance; +extern consvar_t cv_voice_distanceattenuation_factor; +extern consvar_t cv_voice_stereopanning_factor; +extern consvar_t cv_voice_concurrentattenuation_factor; +extern consvar_t cv_voice_concurrentattenuation_min; +extern consvar_t cv_voice_concurrentattenuation_max; +extern float g_local_voice_last_peak; +extern boolean g_local_voice_detected; + typedef enum { SF_TOTALLYSINGLE = 1, // Only play one of these sounds at a time...GLOBALLY @@ -122,6 +139,8 @@ lumpnum_t S_GetSfxLumpNum(sfxinfo_t *sfx); boolean S_SoundDisabled(void); +boolean S_VoiceDisabled(void); + // // Start sound for thing at using from sounds.h // @@ -249,6 +268,7 @@ void S_AttemptToRestoreMusic(void); // void S_UpdateSounds(void); void S_UpdateClosedCaptions(void); +void S_UpdateVoicePositionalProperties(void); FUNCMATH fixed_t S_CalculateSoundDistance(fixed_t px1, fixed_t py1, fixed_t pz1, fixed_t px2, fixed_t py2, fixed_t pz2); @@ -257,6 +277,7 @@ INT32 S_GetSoundVolume(sfxinfo_t *sfx, INT32 volume); void S_SetSfxVolume(void); void S_SetMusicVolume(void); void S_SetMasterVolume(void); +void S_SetVoiceVolume(void); INT32 S_OriginPlaying(void *origin); INT32 S_IdPlaying(sfxenum_t id); @@ -270,6 +291,15 @@ void S_StopSoundByNum(sfxenum_t sfxnum); #define S_StartAttackSound S_StartSound #define S_StartScreamSound S_StartSound +boolean S_SoundInputIsEnabled(void); +boolean S_SoundInputSetEnabled(boolean enabled); +UINT32 S_SoundInputDequeueSamples(void *data, UINT32 len); + +void S_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal); +void S_SetPlayerVoiceActive(INT32 playernum); +boolean S_IsPlayerVoiceActive(INT32 playernum); +void S_ResetVoiceQueue(INT32 playernum); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/sdl/new_sound.cpp b/src/sdl/new_sound.cpp index 5330b8aff..6f8835a9c 100644 --- a/src/sdl/new_sound.cpp +++ b/src/sdl/new_sound.cpp @@ -53,6 +53,125 @@ using srb2::audio::Source; using namespace srb2; using namespace srb2::io; +namespace +{ +class SdlAudioStream final +{ + SDL_AudioStream* stream_; +public: + SdlAudioStream(const SDL_AudioFormat format, const Uint8 channels, const int src_rate, const SDL_AudioFormat dst_format, const Uint8 dst_channels, const int dst_rate) noexcept + { + stream_ = SDL_NewAudioStream(format, channels, src_rate, dst_format, dst_channels, dst_rate); + } + SdlAudioStream(const SdlAudioStream&) = delete; + SdlAudioStream(SdlAudioStream&&) = default; + SdlAudioStream& operator=(const SdlAudioStream&) = delete; + SdlAudioStream& operator=(SdlAudioStream&&) = default; + ~SdlAudioStream() + { + SDL_FreeAudioStream(stream_); + } + + void put(tcb::span buf) + { + int result = SDL_AudioStreamPut(stream_, buf.data(), buf.size_bytes()); + if (result < 0) + { + char errbuf[512]; + SDL_GetErrorMsg(errbuf, sizeof(errbuf)); + throw std::runtime_error(errbuf); + } + } + + size_t available() const + { + int result = SDL_AudioStreamAvailable(stream_); + if (result < 0) + { + char errbuf[512]; + SDL_GetErrorMsg(errbuf, sizeof(errbuf)); + throw std::runtime_error(errbuf); + } + return result; + } + + size_t get(tcb::span out) + { + int result = SDL_AudioStreamGet(stream_, out.data(), out.size_bytes()); + if (result < 0) + { + char errbuf[512]; + SDL_GetErrorMsg(errbuf, sizeof(errbuf)); + throw std::runtime_error(errbuf); + } + return result; + } + + void clear() noexcept + { + SDL_AudioStreamClear(stream_); + } +}; + +class SdlVoiceStreamPlayer : public Source<2> +{ + SdlAudioStream stream_; + float volume_ = 1.0f; + float sep_ = 0.0f; + bool terminal_ = true; + +public: + SdlVoiceStreamPlayer() : stream_(AUDIO_F32SYS, 1, 48000, AUDIO_F32SYS, 2, 44100) {} + virtual ~SdlVoiceStreamPlayer() = default; + + virtual std::size_t generate(tcb::span> buffer) override + { + size_t written = stream_.get(tcb::as_writable_bytes(buffer)) / sizeof(Sample<2>); + + for (size_t i = written; i < buffer.size(); i++) + { + buffer[i] = {0.f, 0.f}; + } + + // Apply gain de-popping if the last generation was terminal + if (terminal_) + { + for (size_t i = 0; i < std::min(16, written); i++) + { + buffer[i].amplitudes[0] *= (float)(i) / 16; + buffer[i].amplitudes[1] *= (float)(i) / 16; + } + terminal_ = false; + } + + if (written < buffer.size()) + { + terminal_ = true; + } + + for (size_t i = 0; i < written; i++) + { + float sep_pan = ((sep_ + 1.f) / 2.f) * (3.14159 / 2.f); + + float left_scale = std::cos(sep_pan); + float right_scale = std::sin(sep_pan); + buffer[i] = {buffer[i].amplitudes[0] * volume_ * left_scale, buffer[i].amplitudes[1] * volume_ * right_scale}; + } + + return buffer.size(); + }; + + SdlAudioStream& stream() noexcept { return stream_; } + + void set_properties(float volume, float sep) noexcept + { + volume_ = volume; + sep_ = sep; + } +}; + +} // namespace + // extern in i_sound.h UINT8 sound_started = false; @@ -60,13 +179,16 @@ static unique_ptr> master_gain; static shared_ptr> master; static shared_ptr> mixer_sound_effects; static shared_ptr> mixer_music; +static shared_ptr> mixer_voice; static shared_ptr music_player; static shared_ptr> resample_music_player; static shared_ptr> gain_sound_effects; static shared_ptr> gain_music_player; static shared_ptr> gain_music_channel; +static shared_ptr> gain_voice_channel; static vector> sound_effect_channels; +static vector> player_voice_channels; #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES static shared_ptr av_recorder; @@ -74,6 +196,10 @@ static shared_ptr av_recorder; static void (*music_fade_callback)(); +static SDL_AudioDeviceID g_device_id; +static SDL_AudioDeviceID g_input_device_id; +static boolean g_input_device_paused; + void* I_GetSfx(sfxinfo_t* sfx) { if (sfx->lumpnum == LUMPERROR) @@ -120,8 +246,8 @@ namespace class SdlAudioLockHandle { public: - SdlAudioLockHandle() { SDL_LockAudio(); } - ~SdlAudioLockHandle() { SDL_UnlockAudio(); } + SdlAudioLockHandle() { SDL_LockAudioDevice(g_device_id); } + ~SdlAudioLockHandle() { SDL_UnlockAudioDevice(g_device_id); } }; #ifdef TRACY_ENABLE @@ -180,21 +306,21 @@ void initialize_sound() return; } - SDL_AudioSpec desired; + SDL_AudioSpec desired {}; desired.format = AUDIO_F32SYS; desired.channels = 2; desired.samples = cv_soundmixingbuffersize.value; desired.freq = 44100; desired.callback = audio_callback; - if (SDL_OpenAudio(&desired, NULL) < 0) + if ((g_device_id = SDL_OpenAudioDevice(NULL, SDL_FALSE, &desired, NULL, 0)) == 0) { CONS_Alert(CONS_ERROR, "Failed to open SDL Audio device: %s\n", SDL_GetError()); SDL_QuitSubSystem(SDL_INIT_AUDIO); return; } - SDL_PauseAudio(SDL_FALSE); + SDL_PauseAudioDevice(g_device_id, SDL_FALSE); { SdlAudioLockHandle _; @@ -204,16 +330,20 @@ void initialize_sound() master_gain->bind(master); mixer_sound_effects = make_shared>(); mixer_music = make_shared>(); + mixer_voice = make_shared>(); music_player = make_shared(); resample_music_player = make_shared>(music_player, 1.f); gain_sound_effects = make_shared>(); gain_music_player = make_shared>(); gain_music_channel = make_shared>(); + gain_voice_channel = make_shared>(); gain_sound_effects->bind(mixer_sound_effects); gain_music_player->bind(resample_music_player); gain_music_channel->bind(mixer_music); + gain_voice_channel->bind(mixer_voice); master->add_source(gain_sound_effects); master->add_source(gain_music_channel); + master->add_source(gain_voice_channel); mixer_music->add_source(gain_music_player); sound_effect_channels.clear(); for (size_t i = 0; i < static_cast(cv_numChannels.value); i++) @@ -222,6 +352,13 @@ void initialize_sound() sound_effect_channels.push_back(player); mixer_sound_effects->add_source(player); } + player_voice_channels.clear(); + for (size_t i = 0; i < MAXPLAYERS; i++) + { + shared_ptr player = make_shared(); + player_voice_channels.push_back(player); + mixer_voice->add_source(player); + } } sound_started = true; @@ -237,7 +374,17 @@ void I_StartupSound(void) void I_ShutdownSound(void) { - SDL_CloseAudio(); + if (g_device_id) + { + SDL_CloseAudioDevice(g_device_id); + g_device_id = 0; + } + if (g_input_device_id) + { + SDL_CloseAudioDevice(g_input_device_id); + g_input_device_id = 0; + } + SDL_QuitSubSystem(SDL_INIT_AUDIO); sound_started = false; @@ -380,6 +527,17 @@ void I_SetSfxVolume(int volume) } } +void I_SetVoiceVolume(int volume) +{ + SdlAudioLockHandle _; + float vol = static_cast(volume) / 100.f; + + if (gain_voice_channel) + { + gain_voice_channel->gain(std::clamp(vol * vol * vol, 0.f, 1.f)); + } +} + void I_SetMasterVolume(int volume) { SdlAudioLockHandle _; @@ -824,3 +982,93 @@ void I_UpdateAudioRecorder(void) av_recorder = g_av_recorder; #endif } + +boolean I_SoundInputIsEnabled(void) +{ + return g_input_device_id != 0 && !g_input_device_paused; +} + +boolean I_SoundInputSetEnabled(boolean enabled) +{ + if (g_input_device_id == 0 && enabled) + { + SDL_AudioSpec input_desired {}; + input_desired.format = AUDIO_F32SYS; + input_desired.channels = 1; + input_desired.samples = 2048; + input_desired.freq = 48000; + SDL_AudioSpec input_obtained {}; + g_input_device_id = SDL_OpenAudioDevice(nullptr, SDL_TRUE, &input_desired, &input_obtained, 0); + if (!g_input_device_id) + { + CONS_Alert(CONS_WARNING, "Failed to open input audio device: %s\n", SDL_GetError()); + return false; + } + g_input_device_paused = true; + } + + if (enabled && g_input_device_paused) + { + SDL_PauseAudioDevice(g_input_device_id, SDL_FALSE); + g_input_device_paused = false; + } + else if (!enabled && !g_input_device_paused) + { + SDL_PauseAudioDevice(g_input_device_id, SDL_TRUE); + SDL_ClearQueuedAudio(g_input_device_id); + g_input_device_paused = true; + } + return !g_input_device_paused; +} + +UINT32 I_SoundInputDequeueSamples(void *data, UINT32 len) +{ + if (!g_input_device_id) + { + return 0; + } + UINT32 avail = SDL_GetQueuedAudioSize(g_input_device_id); + if (avail == 0) + { + return 0; + } + + UINT32 ret = SDL_DequeueAudio(g_input_device_id, data, std::min(len, avail)); + return ret; +} + +void I_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal) +{ + if (!sound_started) + { + return; + } + + SdlAudioLockHandle _; + SdlVoiceStreamPlayer* player = player_voice_channels.at(playernum).get(); + player->stream().put(tcb::span((std::byte*)data, len)); +} + +void I_SetPlayerVoiceProperties(INT32 playernum, float volume, float sep) +{ + if (!sound_started) + { + return; + } + + SdlAudioLockHandle _; + SdlVoiceStreamPlayer* player = player_voice_channels.at(playernum).get(); + player->set_properties(volume * volume * volume, sep); +} + +void I_ResetVoiceQueue(INT32 playernum) +{ + if (!sound_started) + { + return; + } + + SdlAudioLockHandle _; + SdlVoiceStreamPlayer* player = player_voice_channels.at(playernum).get(); + player->stream().clear(); +} diff --git a/src/typedef.h b/src/typedef.h index 256eddb52..75e4cedce 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -85,6 +85,7 @@ TYPEDEF (resultsall_pak); TYPEDEF (say_pak); TYPEDEF (reqmapqueue_pak); TYPEDEF (netinfo_pak); +TYPEDEF (voice_pak); // d_event.h TYPEDEF (event_t); diff --git a/src/y_inter.cpp b/src/y_inter.cpp index f4c809e1a..a1c3fc796 100644 --- a/src/y_inter.cpp +++ b/src/y_inter.cpp @@ -799,6 +799,39 @@ void Y_PlayerStandingsDrawer(y_data_t *standings, INT32 xoffset) player_names[pnum] ); + { + patch_t *voxpat; + int voxxoffs = 0; + int voxyoffs = 0; + if (players[pnum].pflags2 & (PF2_SELFDEAFEN | PF2_SERVERDEAFEN)) + { + voxpat = (patch_t*) W_CachePatchName("VOXCRD", PU_HUDGFX); + voxxoffs = 1; + voxyoffs = -5; + } + else if (players[pnum].pflags2 & (PF2_SELFMUTE | PF2_SERVERMUTE)) + { + voxpat = (patch_t*) W_CachePatchName("VOXCRM", PU_HUDGFX); + voxxoffs = 1; + voxyoffs = -6; + } + else if (S_IsPlayerVoiceActive(pnum)) + { + voxpat = (patch_t*) W_CachePatchName("VOXCRA", PU_HUDGFX); + voxyoffs = -4; + } + else + { + voxpat = NULL; + } + + if (voxpat) + { + int namewidth = V_ThinStringWidth(player_names[pnum], 0); + V_DrawFixedPatch((x + 27 + namewidth + voxxoffs) * FRACUNIT, (y + voxyoffs) * FRACUNIT, FRACUNIT, 0, voxpat, NULL); + } + } + V_DrawRightAlignedThinString( x+118, y-2, 0, diff --git a/vcpkg.json b/vcpkg.json index 7d58a74aa..08f7e8a91 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -10,6 +10,7 @@ "libvpx", "libvorbis", "libyuv", + "opus", "zlib" ], "builtin-baseline": "c591ac6466a55ef0a05a3d56bb1489ca36e50102"