RingRacers/src/y_inter.cpp
Eidolon 22b20b5877 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.
2024-12-13 17:12:14 -06:00

2534 lines
56 KiB
C++

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2024 by Vivian "toastergrl" Grannell.
// Copyright (C) 2024 by Kart Krew.
// Copyright (C) 2020 by Sonic Team Junior.
//
// 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 y_inter.cpp
/// \brief Tally screens, or "Intermissions" as they were formally called in Doom
#include <algorithm>
#include "doomdef.h"
#include "doomstat.h"
#include "d_main.h"
#include "f_finale.h"
#include "g_game.h"
#include "hu_stuff.h"
#include "i_net.h"
#include "i_video.h"
#include "p_tick.h"
#include "r_defs.h"
#include "r_skins.h"
#include "s_sound.h"
#include "st_stuff.h"
#include "v_video.h"
#include "w_wad.h"
#include "y_inter.h"
#include "z_zone.h"
#include "k_menu.h"
#include "m_misc.h"
#include "i_system.h"
#include "p_setup.h"
#include "r_local.h"
#include "r_fps.h"
#include "p_local.h"
#include "m_cond.h" // condition sets
#include "lua_hook.h" // IntermissionThinker hook
#include "lua_hud.h"
#include "lua_hudlib_drawlist.h"
#include "m_random.h" // M_RandomKey
#include "g_input.h" // G_PlayerInputDown
#include "k_hud.h" // K_DrawMapThumbnail
#include "k_battle.h"
#include "k_boss.h"
#include "k_pwrlv.h"
#include "k_grandprix.h"
#include "k_serverstats.h" // SV_BumpMatchStats
#include "m_easing.h"
#include "music.h"
#include "v_draw.hpp"
#ifdef HWRENDER
#include "hardware/hw_main.h"
#endif
typedef struct
{
char patch[9];
INT32 points;
UINT8 display;
} y_bonus_t;
static y_data_t data;
// graphics
static patch_t *bgpatch = NULL; // INTERSCR
static patch_t *widebgpatch = NULL;
static patch_t *bgtile = NULL; // SPECTILE/SRB2BACK
static patch_t *interpic = NULL; // custom picture defined in map header
#define INFINITE_TIMER (INT16_MAX) // just some arbitrarily large value that won't easily overflow
static INT32 timer;
static INT32 powertype = PWRLV_DISABLED;
static INT32 intertic;
static INT32 endtic = -1;
static INT32 sorttic = -1;
static fixed_t mqscroll = 0;
static fixed_t chkscroll = 0;
static fixed_t ttlscroll = 0;
intertype_t intertype = int_none;
static huddrawlist_h luahuddrawlist_intermission;
static boolean Y_CanSkipIntermission(void)
{
if (!netgame)
{
return true;
}
return false;
}
boolean Y_IntermissionPlayerLock(void)
{
return (gamestate == GS_INTERMISSION && data.rankingsmode == false);
}
static void Y_UnloadData(void);
//
// SRB2Kart - Y_CalculateMatchData and ancillary functions
//
static void Y_CompareTime(INT32 i)
{
UINT32 val = ((players[i].pflags & PF_NOCONTEST || players[i].realtime == UINT32_MAX)
? (UINT32_MAX-1) : players[i].realtime);
if (!(val < data.val[data.numplayers]))
return;
data.val[data.numplayers] = val;
data.num[data.numplayers] = i;
}
static void Y_CompareScore(INT32 i)
{
UINT32 val = ((players[i].pflags & PF_NOCONTEST)
? (UINT32_MAX-1) : players[i].roundscore);
if (!(data.val[data.numplayers] == UINT32_MAX
|| (!(players[i].pflags & PF_NOCONTEST) && val > data.val[data.numplayers])))
return;
data.val[data.numplayers] = val;
data.num[data.numplayers] = i;
}
static void Y_CompareRank(INT32 i)
{
INT16 increase = ((data.increase[i] == INT16_MIN) ? 0 : data.increase[i]);
UINT32 score = players[i].score;
if (powertype != PWRLV_DISABLED)
{
score = clientpowerlevels[i][powertype];
}
if (!(data.val[data.numplayers] == UINT32_MAX || (score - increase) > data.val[data.numplayers]))
return;
data.val[data.numplayers] = (score - increase);
data.num[data.numplayers] = i;
}
static void Y_CalculateMatchData(UINT8 rankingsmode, void (*comparison)(INT32))
{
INT32 i, j;
boolean completed[MAXPLAYERS];
INT32 numplayersingame = 0;
boolean getmainplayer = false;
// Initialize variables
if (rankingsmode > 1)
;
else if ((data.rankingsmode = (boolean)rankingsmode))
{
sprintf(data.headerstring, "Total Rankings");
data.gotthrough = false;
}
else
{
getmainplayer = true;
data.encore = encoremode;
memset(data.jitter, 0, sizeof (data.jitter));
}
for (i = 0; i < MAXPLAYERS; i++)
{
data.val[i] = UINT32_MAX;
data.grade[i] = GRADE_INVALID;
if (!playeringame[i] || players[i].spectator)
{
data.increase[i] = INT16_MIN;
continue;
}
if (!rankingsmode)
data.increase[i] = INT16_MIN;
numplayersingame++;
}
memset(completed, 0, sizeof (completed));
data.numplayers = 0;
data.showroundnum = false;
data.isduel = (numplayersingame <= 2);
srb2::StandingsJson standings {};
bool savestandings = (!rankingsmode && demo.recording);
// Team stratification (this code only barely supports more than 2 teams)
data.winningteam = TEAM_UNASSIGNED;
data.halfway = UINT8_MAX;
UINT8 countteam[TEAM__MAX];
UINT8 smallestteam = UINT8_MAX;
memset(countteam, 0, sizeof(countteam));
if (rankingsmode == 0 && G_GametypeHasTeams())
{
for (i = data.winningteam+1; i < TEAM__MAX; i++)
{
countteam[i] = G_CountTeam(i);
if (g_teamscores[data.winningteam] < g_teamscores[i])
{
data.winningteam = i;
}
if (smallestteam > countteam[i])
{
smallestteam = countteam[i];
}
}
if (countteam[data.winningteam])
{
data.halfway = countteam[data.winningteam] - 1;
}
}
for (j = 0; j < numplayersingame; j++)
{
i = 0;
if (data.winningteam != TEAM_UNASSIGNED)
{
for (; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator || completed[i])
continue;
if (players[i].team != data.winningteam)
continue;
comparison(i);
}
if (data.val[data.numplayers] == UINT32_MAX)
{
// Only run the un-teamed loop if everybody
// on the winning team was previously placed
i = 0;
}
}
for (; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator || completed[i])
continue;
comparison(i);
}
i = data.num[data.numplayers];
completed[i] = true;
data.grade[i] = K_PlayerTallyActive(&players[i]) ? players[i].tally.rank : GRADE_INVALID;
if (data.numplayers && (data.val[data.numplayers] == data.val[data.numplayers-1]))
{
data.pos[data.numplayers] = data.pos[data.numplayers-1];
}
else
{
data.pos[data.numplayers] = data.numplayers+1;
}
#define strtime data.strval[data.numplayers]
strtime[0] = '\0';
if (!rankingsmode)
{
// Online rank is handled further below in this file.
if (powertype == PWRLV_DISABLED)
{
if (data.winningteam != TEAM_UNASSIGNED)
{
// TODO ASK TYRON
if (smallestteam != 0
&& players[i].team == data.winningteam)
{
data.increase[i] = 1;
}
}
else
{
UINT8 pointgetters = numplayersingame + spectateGriefed;
if (data.pos[data.numplayers] < pointgetters
&& !(players[i].pflags & PF_NOCONTEST))
{
data.increase[i] = K_CalculateGPRankPoints(data.pos[data.numplayers], pointgetters);
}
}
if (data.increase[i] > 0)
{
players[i].score += data.increase[i];
}
}
if (savestandings)
{
srb2::StandingJson standing {};
standing.ranking = data.pos[data.numplayers];
standing.name = std::string(player_names[i]);
standing.demoskin = players[i].skin;
standing.skincolor = std::string(skincolors[players[i].skincolor].name);
standing.timeorscore = data.val[data.numplayers];
standings.standings.emplace_back(std::move(standing));
}
if (data.val[data.numplayers] == (UINT32_MAX-1))
STRBUFCPY(strtime, "RETIRED.");
else
{
if (intertype == int_time)
{
snprintf(strtime, sizeof strtime, "%i'%02i\"%02i", G_TicsToMinutes(data.val[data.numplayers], true),
G_TicsToSeconds(data.val[data.numplayers]), G_TicsToCentiseconds(data.val[data.numplayers]));
}
else
{
snprintf(strtime, sizeof strtime, "%d", data.val[data.numplayers]);
}
}
}
else
{
if (powertype != PWRLV_DISABLED && !clientpowerlevels[i][powertype])
{
// No power level (guests)
STRBUFCPY(strtime, "----");
}
else
{
snprintf(strtime, sizeof strtime, "%d", data.val[data.numplayers]);
}
}
strtime[sizeof strtime - 1] = '\0';
#undef strtime
data.numplayers++;
}
if (data.numplayers <= 2
|| data.halfway == UINT8_MAX
|| data.halfway >= 8
|| (data.numplayers - data.halfway) >= 8)
{
data.halfway = (data.numplayers-1)/2;
}
if (savestandings)
{
srb2::write_current_demo_end_marker();
srb2::write_current_demo_standings(standings);
}
if (getmainplayer == true)
{
// Okay, player scores have been set now - we can calculate GP-relevant material.
{
if (grandprixinfo.gp == true)
{
K_UpdateGPRank(&grandprixinfo.rank);
}
// See also G_GetNextMap, M_DrawPause
data.showrank = false;
if (grandprixinfo.gp == true
&& netgame == false // TODO netgame Special Mode support
&& grandprixinfo.gamespeed >= KARTSPEED_NORMAL
&& roundqueue.size > 1
&& roundqueue.entries[roundqueue.size - 1].rankrestricted == true
)
{
if (roundqueue.position == roundqueue.size-1)
{
// On A rank pace? Then you get a chance for S rank!
gp_rank_e rankforline = K_CalculateGPGrade(&grandprixinfo.rank);
data.showrank = (rankforline >= GRADE_A);
data.linemeter =
(std::min(rankforline, GRADE_A)
* (2 * TICRATE)
) / GRADE_A;
// G_NextMap will float you to rank-restricted stages on Master wins.
// Fudge the rank display.
if (grandprixinfo.masterbots && grandprixinfo.rank.position <= 1)
{
data.showrank = true;
data.linemeter = 2*TICRATE;
}
// A little extra time to take it all in
timer += TICRATE;
}
if (gamedata->everseenspecial == true
|| roundqueue.position == roundqueue.size)
{
// Additional cases in which it should always be shown.
data.showrank = true;
}
}
}
i = MAXPLAYERS;
for (j = 0; j < data.numplayers; j++)
{
i = data.num[j];
if (i >= MAXPLAYERS
|| playeringame[i] == false
|| players[i].spectator == true)
{
continue;
}
if (demo.playback)
{
if (!P_IsDisplayPlayer(&players[i]))
{
continue;
}
break;
}
if (!P_IsPartyPlayer(&players[i]))
{
continue;
}
break;
}
data.headerstring[0] = '\0';
data.gotthrough = false;
data.mainplayer = MAXPLAYERS;
if (j < data.numplayers)
{
data.mainplayer = i;
if (data.winningteam != TEAM_UNASSIGNED
&& players[i].team != TEAM_UNASSIGNED)
{
data.gotthrough = true;
snprintf(data.headerstring,
sizeof data.headerstring,
"%s TEAM",
g_teaminfo[players[i].team].name);
data.showroundnum = true;
}
else if (!(players[i].pflags & PF_NOCONTEST))
{
data.gotthrough = true;
if (players[i].skin < numskins)
{
snprintf(data.headerstring,
sizeof data.headerstring,
"%s",
R_CanShowSkinInDemo(players[i].skin) ? skins[players[i].skin].realname : "???");
}
data.showroundnum = true;
}
else
{
snprintf(data.headerstring,
sizeof data.headerstring,
"NO CONTEST...");
}
}
else
{
if (roundqueue.position > 0 && roundqueue.position <= roundqueue.size
&& (grandprixinfo.gp == false || grandprixinfo.eventmode == GPEVENT_NONE))
{
snprintf(data.headerstring,
sizeof data.headerstring,
"ROUND");
data.showroundnum = true;
}
else if (K_CheckBossIntro() == true && bossinfo.enemyname)
{
snprintf(data.headerstring,
sizeof data.headerstring,
"%s",
bossinfo.enemyname);
}
else if (battleprisons == true)
{
snprintf(data.headerstring,
sizeof data.headerstring,
"PRISON BREAK");
}
else
{
snprintf(data.headerstring,
sizeof data.headerstring,
"%s STAGE",
gametypes[gametype]->name);
}
}
data.headerstring[sizeof data.headerstring - 1] = '\0';
}
}
typedef enum
{
BPP_AHEAD,
BPP_DONE,
BPP_MAIN,
BPP_SHADOW = BPP_MAIN,
BPP_MAX
} bottomprogressionpatch_t;
//
// Y_PlayerStandingsDrawer
//
// Handles drawing the center-of-screen player standings.
//
void Y_PlayerStandingsDrawer(y_data_t *standings, INT32 xoffset)
{
if (standings->numplayers == 0)
{
return;
}
UINT8 i;
SINT8 yspacing = 14;
INT32 heightcount = (standings->numplayers - 1);
INT32 x, y;
INT32 x2, returny, inwardshim = 0;
boolean verticalresults = (standings->numplayers < 4 && (standings->numplayers == 1 || standings->isduel == false));
boolean datarightofcolumn = false;
boolean drawping = (netgame && gamestate == GS_LEVEL);
INT32 hilicol = highlightflags;
patch_t *resbar = static_cast<patch_t*>(W_CachePatchName("R_RESBAR", PU_PATCH)); // Results bars for players
patch_t *cpu = static_cast<patch_t*>(W_CachePatchName("K_CPU", PU_PATCH));
if (drawping || standings->rankingsmode != 0)
{
inwardshim = 8;
}
if (verticalresults)
{
x = (BASEVIDWIDTH/2) - 61;
}
else
{
x = 29;
inwardshim /= 2;
heightcount /= 2;
}
x += xoffset + inwardshim;
x2 = x;
if (drawping)
{
x2 -= 9;
}
UINT8 halfway = standings->halfway;
if (halfway > 4)
{
yspacing--;
}
else if (halfway <= 2)
{
yspacing++;
if (verticalresults)
{
yspacing++;
}
}
y = 106 - (heightcount * yspacing)/2;
if (standings->isduel)
{
y += 38;
}
else if (y < 70)
{
// One sanity check.
y = 70;
}
returny = y;
boolean (*_isHighlightedPlayer)(const player_t *) =
(demo.playback
? P_IsDisplayPlayer
: P_IsPartyPlayer
);
boolean doreverse = (
standings->isduel && standings->numplayers == 2
&& standings->num[0] > standings->num[1]
);
i = 0;
if (doreverse)
{
i = standings->numplayers-1;
halfway++;
}
do // don't use "continue" in this loop just for sanity's sake
{
const UINT8 pnum = standings->num[i];
if (pnum == MAXPLAYERS)
;
else if (!playeringame[pnum] || players[pnum].spectator == true)
standings->num[i] = MAXPLAYERS; // this should be the only field setting in this function
else
{
UINT8 *charcolormap = NULL;
if (!R_CanShowSkinInDemo(players[pnum].skin))
{
charcolormap = R_GetTranslationColormap(TC_BLINK, static_cast<skincolornum_t>(players[pnum].skincolor), GTC_CACHE);
}
else
{
charcolormap = R_GetTranslationColormap(players[pnum].skin, static_cast<skincolornum_t>(players[pnum].skincolor), GTC_CACHE);
}
if (standings->isduel)
{
INT32 duelx = x + 22 + (datarightofcolumn ? inwardshim : -inwardshim);
INT32 duely = y - 80;
V_DrawScaledPatch(duelx, duely, 0, static_cast<patch_t*>(W_CachePatchName("DUELGRPH", PU_CACHE)));
V_DrawScaledPatch(duelx + 8, duely + 9, V_TRANSLUCENT, static_cast<patch_t*>(W_CachePatchName("PREVBACK", PU_CACHE)));
UINT8 spr2 = SPR2_STIN;
if (standings->pos[i] == 2)
{
spr2 = (datarightofcolumn ? SPR2_STGR : SPR2_STGL);
}
M_DrawCharacterSprite(
duelx + 40, duely + 78,
players[pnum].skin,
spr2,
(datarightofcolumn ? 1 : 7),
0,
0,
charcolormap
);
duelx += 8;
duely += 5;
UINT8 j;
for (j = 0; j <= splitscreen; j++)
{
if (pnum == g_localplayers[j])
break;
}
INT32 letterpos = duelx + (datarightofcolumn ? 44 : 0);
if (j > splitscreen || demo.playback)
{
// TODO: EGGA isn't strictly correct for demo playback since they're not really network players, but it's better than displaying local profile.
V_DrawScaledPatch(letterpos, duely, 0, static_cast<patch_t*>(W_CachePatchName(va("CHAR%s", (players[pnum].bot ? "CPU" : "EGGA")), PU_CACHE)));
}
else
{
duelx += (datarightofcolumn ? -1 : 11);
UINT8 profilen = cv_lastprofile[j].value;
V_DrawScaledPatch(duelx, duely, 0, static_cast<patch_t*>(W_CachePatchName("FILEBACK", PU_CACHE)));
if (datarightofcolumn && j == 0)
letterpos++; // A is one pixel thinner
V_DrawScaledPatch(letterpos, duely, 0, static_cast<patch_t*>(W_CachePatchName(va("CHARSEL%c", 'A' + j), PU_CACHE)));
profile_t *pr = PR_GetProfile(profilen);
V_DrawCenteredFileString(duelx+26, duely, 0, pr ? pr->profilename : "PLAYER");
}
}
// Apply the jitter offset (later reversed)
if (standings->jitter[pnum] > 0)
y--;
V_DrawMappedPatch(x, y, 0, resbar, NULL);
V_DrawRightAlignedThinString(x+13, y-2, 0, va("%d", standings->pos[i]));
//if (players[pnum].skincolor != SKINCOLOR_NONE)
{
if ((players[pnum].pflags & PF_NOCONTEST) && players[pnum].bot)
{
// RETIRED !!
V_DrawMappedPatch(
x+14, y-5,
0,
static_cast<patch_t*>(W_CachePatchName("MINIDEAD", PU_CACHE)),
R_GetTranslationColormap(TC_DEFAULT, static_cast<skincolornum_t>(players[pnum].skincolor), GTC_CACHE)
);
}
else
{
charcolormap = R_GetTranslationColormap(players[pnum].skin, static_cast<skincolornum_t>(players[pnum].skincolor), GTC_CACHE);
V_DrawMappedPatch(x+14, y-5, 0,
R_CanShowSkinInDemo(players[pnum].skin) ?
faceprefix[players[pnum].skin][FACE_MINIMAP] : kp_unknownminimap,
charcolormap);
}
}
/* y2 = y;
if ((netgame || (demo.playback && demo.netgame)) && playerconsole[pnum] == 0 && server_lagless && !players[pnum].bot)
{
static UINT8 alagles_timer = 0;
patch_t *alagles;
y2 = ( y - 4 );
V_DrawScaledPatch(x + 36, y2, 0, W_CachePatchName(va("BLAGLES%d", (intertic / 3) % 6), PU_CACHE));
// every 70 tics
if (( leveltime % 70 ) == 0)
{
alagles_timer = 9;
}
if (alagles_timer > 0)
{
alagles = W_CachePatchName(va("ALAGLES%d", alagles_timer), PU_CACHE);
V_DrawScaledPatch(x + 36, y2, 0, alagles);
if (( leveltime % 2 ) == 0)
alagles_timer--;
}
else
{
alagles = W_CachePatchName("ALAGLES0", PU_CACHE);
V_DrawScaledPatch(x + 36, y2, 0, alagles);
}
y2 += SHORT (alagles->height) + 1;
}*/
V_DrawThinString(
x+27, y-2,
(
_isHighlightedPlayer(&players[pnum])
? hilicol
: 0
),
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,
standings->strval[i]
);
if (drawping)
{
if (players[pnum].bot)
{
V_DrawScaledPatch(
x2-2 + (datarightofcolumn ? 2 : -2), y-2,
0,
cpu
);
}
else
{
HU_drawPing(
(x2 - 2) * FRACUNIT, (y-2) * FRACUNIT,
playerpingtable[pnum],
playerdelaytable[pnum],
playerpacketlosstable[pnum],
0,
(datarightofcolumn ? 1 : -1)
);
}
}
else if (gamestate == GS_LEVEL)
;
else if (standings->rankingsmode != 0)
{
char *increasenum = NULL;
if (standings->increase[pnum] != INT16_MIN)
{
increasenum = va(
"(%d)",
standings->increase[pnum]
);
}
if (increasenum)
{
if (datarightofcolumn)
{
V_DrawThinString(
x2, y-2,
0,
increasenum
);
}
else
{
V_DrawRightAlignedThinString(
x2, y-2,
0,
increasenum
);
}
}
}
else if (standings->grade[pnum] != GRADE_INVALID)
{
patch_t *gradePtc = static_cast<patch_t*>(W_CachePatchName(va("R_INRNK%c", K_GetGradeChar(static_cast<gp_rank_e>(standings->grade[pnum]))), PU_PATCH));
patch_t *gradeBG = NULL;
UINT16 gradeColor = SKINCOLOR_NONE;
UINT8 *gradeClm = NULL;
gradeColor = K_GetGradeColor(static_cast<gp_rank_e>(standings->grade[pnum]));
if (gradeColor != SKINCOLOR_NONE)
{
gradeClm = R_GetTranslationColormap(TC_DEFAULT, static_cast<skincolornum_t>(gradeColor), GTC_CACHE);
}
if (datarightofcolumn)
{
gradeBG = static_cast<patch_t*>(W_CachePatchName("R_INRNKR", PU_PATCH));
V_DrawMappedPatch(x + 118, y, 0, gradeBG, gradeClm);
V_DrawMappedPatch(x + 118 + 4, y - 1, 0, gradePtc, gradeClm);
}
else
{
gradeBG = static_cast<patch_t*>(W_CachePatchName("R_INRNKL", PU_PATCH));
V_DrawMappedPatch(x - 12, y, 0, gradeBG, gradeClm);
V_DrawMappedPatch(x - 12 + 3, y - 1, 0, gradePtc, gradeClm);
}
}
// Reverse the jitter offset
if (standings->jitter[pnum] > 0)
y++;
}
y += yspacing;
if (verticalresults == false && i == halfway)
{
x = 169 + xoffset - inwardshim;
y = returny;
datarightofcolumn = true;
x2 = x + 118 + 5;
}
if (!doreverse)
{
if (++i < standings->numplayers)
continue;
break;
}
if (i == 0)
break;
i--;
}
while (true);
}
//
// Y_RoundQueueDrawer
//
// Handles drawing the bottom-of-screen progression.
// Currently requires intermission y_data for animation only.
//
void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, boolean widescreen)
{
if (roundqueue.size == 0)
{
return;
}
// The following is functionally a hack.
// Due to how interpolation works, it's functionally one frame behind.
// So we offset certain interpolated timers by this to make our lives easier!
// This permits cues handled in the ticker and visuals to match up,
// like the player pin reaching the Sealed Star the frame of the fade.
// We also do this rather than doing extrapoleration because that would
// still put 35fps in the future. ~toast 100523
SINT8 interpoffs = (R_UsingFrameInterpolation() ? 1 : 0);
UINT8 i;
UINT8 *greymap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_GREY, GTC_CACHE);
INT32 baseflags = 0;
INT32 bufferspace = 0;
if (widescreen)
{
baseflags |= V_SNAPTOBOTTOM;
bufferspace = ((vid.width/vid.dupx) - BASEVIDWIDTH) / 2;
}
// Background pieces
patch_t *queuebg_flat = static_cast<patch_t*>(W_CachePatchName("R_RMBG1", PU_PATCH));
patch_t *queuebg_upwa = static_cast<patch_t*>(W_CachePatchName("R_RMBG2", PU_PATCH));
patch_t *queuebg_down = static_cast<patch_t*>(W_CachePatchName("R_RMBG3", PU_PATCH));
patch_t *queuebg_prize = static_cast<patch_t*>(W_CachePatchName("R_RMBG4", PU_PATCH));
// Progression lines
patch_t *line_upwa[BPP_MAX];
patch_t *line_down[BPP_MAX];
patch_t *line_flat[BPP_MAX];
line_upwa[BPP_AHEAD] = static_cast<patch_t*>(W_CachePatchName("R_RRMLN1", PU_PATCH));
line_upwa[BPP_DONE] = static_cast<patch_t*>(W_CachePatchName("R_RRMLN3", PU_PATCH));
line_upwa[BPP_SHADOW] = static_cast<patch_t*>(W_CachePatchName("R_RRMLS1", PU_PATCH));
line_down[BPP_AHEAD] = static_cast<patch_t*>(W_CachePatchName("R_RRMLN2", PU_PATCH));
line_down[BPP_DONE] = static_cast<patch_t*>(W_CachePatchName("R_RRMLN4", PU_PATCH));
line_down[BPP_SHADOW] = static_cast<patch_t*>(W_CachePatchName("R_RRMLS2", PU_PATCH));
line_flat[BPP_AHEAD] = static_cast<patch_t*>(W_CachePatchName("R_RRMLN5", PU_PATCH));
line_flat[BPP_DONE] = static_cast<patch_t*>(W_CachePatchName("R_RRMLN6", PU_PATCH));
line_flat[BPP_SHADOW] = static_cast<patch_t*>(W_CachePatchName("R_RRMLS3", PU_PATCH));
// Progress markers
patch_t *level_dot[BPP_MAIN];
patch_t *bonus_dot[BPP_MAIN];
patch_t *capsu_dot[BPP_MAIN];
patch_t *prize_dot[BPP_MAIN];
level_dot[BPP_AHEAD] = static_cast<patch_t*>(W_CachePatchName("R_RRMRK2", PU_PATCH));
level_dot[BPP_DONE] = static_cast<patch_t*>(W_CachePatchName("R_RRMRK1", PU_PATCH));
bonus_dot[BPP_AHEAD] = static_cast<patch_t*>(W_CachePatchName("R_RRMRK7", PU_PATCH));
bonus_dot[BPP_DONE] = static_cast<patch_t*>(W_CachePatchName("R_RRMRK8", PU_PATCH));
capsu_dot[BPP_AHEAD] = static_cast<patch_t*>(W_CachePatchName("R_RRMRK3", PU_PATCH));
capsu_dot[BPP_DONE] = static_cast<patch_t*>(W_CachePatchName("R_RRMRK5", PU_PATCH));
prize_dot[BPP_AHEAD] = static_cast<patch_t*>(W_CachePatchName("R_RRMRK4", PU_PATCH));
prize_dot[BPP_DONE] = static_cast<patch_t*>(W_CachePatchName("R_RRMRK6", PU_PATCH));
UINT8 *colormap = NULL, *oppositemap = NULL;
fixed_t playerx = 0, playery = 0;
UINT8 pskin = MAXSKINS;
UINT16 pcolor = SKINCOLOR_WHITE;
if (standings->mainplayer == MAXPLAYERS)
{
;
}
else if (playeringame[standings->mainplayer] == false)
{
standings->mainplayer = MAXPLAYERS;
}
else if (players[standings->mainplayer].spectator == false
&& players[standings->mainplayer].skin < numskins
&& players[standings->mainplayer].skincolor != SKINCOLOR_NONE
&& players[standings->mainplayer].skincolor < numskincolors
)
{
pskin = players[standings->mainplayer].skin;
pcolor = players[standings->mainplayer].skincolor;
}
colormap = R_GetTranslationColormap(TC_DEFAULT, static_cast<skincolornum_t>(pcolor), GTC_CACHE);
oppositemap = R_GetTranslationColormap(TC_DEFAULT, static_cast<skincolornum_t>(skincolors[pcolor].invcolor), GTC_CACHE);
UINT8 workingqueuesize = roundqueue.size;
boolean upwa = false;
if (roundqueue.size > 1
&& roundqueue.entries[roundqueue.size - 1].rankrestricted == true
)
{
if (roundqueue.size & 1)
{
upwa = true;
}
workingqueuesize--;
}
INT32 widthofroundqueue = 24*(workingqueuesize - 1);
INT32 x = (BASEVIDWIDTH - widthofroundqueue) / 2;
INT32 y, basey = 167 + offset;
INT32 spacetospecial = 0;
// The following block handles horizontal easing of the
// progression bar on the last non-rankrestricted round.
if (standings->showrank == true)
{
fixed_t percentslide = 0;
SINT8 deferxoffs = 0;
const INT32 desiredx2 = (290 + bufferspace);
spacetospecial = std::max<INT32>(desiredx2 - widthofroundqueue - (24 - bufferspace), 16);
if (roundqueue.position == roundqueue.size)
{
percentslide = FRACUNIT;
}
else if (doanimations
&& roundqueue.position == roundqueue.size-1
&& timer - interpoffs <= 3*TICRATE)
{
const INT32 through = (3*TICRATE) - (timer - interpoffs - 1);
const INT32 slidetime = (TICRATE/2);
if (through >= slidetime)
{
percentslide = FRACUNIT;
}
else
{
percentslide = R_InterpolateFixed(
(through - 1) * FRACUNIT,
(through * FRACUNIT)
) / slidetime;
}
}
if (percentslide != 0)
{
const INT32 differencetocover = (x + widthofroundqueue + spacetospecial - desiredx2);
if (percentslide == FRACUNIT)
{
x -= (differencetocover + deferxoffs);
}
else
{
x -= Easing_OutCubic(
percentslide,
0,
differencetocover * FRACUNIT
) / FRACUNIT;
}
}
}
// Fill in background to left edge of screen
fixed_t xiter = x;
if (upwa == true)
{
xiter -= 24;
V_DrawMappedPatch(xiter, basey, baseflags, queuebg_upwa, greymap);
}
while (xiter > -bufferspace)
{
xiter -= 24;
V_DrawMappedPatch(xiter, basey, baseflags, queuebg_flat, greymap);
}
for (i = 0; i < workingqueuesize; i++)
{
// Draw the background, and grab the appropriate line, to the right of the dot
patch_t **choose_line = NULL;
upwa ^= true;
if (upwa == false)
{
y = basey + 4;
V_DrawMappedPatch(x, basey, baseflags, queuebg_down, greymap);
if (i+1 != workingqueuesize) // no more line?
{
choose_line = line_down;
}
}
else
{
y = basey + 12;
if (i+1 != workingqueuesize) // no more line?
{
V_DrawMappedPatch(x, basey, baseflags, queuebg_upwa, greymap);
choose_line = line_upwa;
}
else
{
V_DrawMappedPatch(x, basey, baseflags, queuebg_flat, greymap);
}
}
if (roundqueue.position == i+1)
{
playerx = (x * FRACUNIT);
playery = (y * FRACUNIT);
// If there's standard progression ahead of us, visibly move along it.
if (
doanimations
&& choose_line != NULL
&& timer - interpoffs <= 2*TICRATE
)
{
// 8 tics is chosen because it plays nice
// with both the x and y distance to cover.
fixed_t through = (2*TICRATE) - (timer - interpoffs - 1);;
if (through > 8)
{
if (through == 9 + interpoffs)
{
// Impactful landing
playery += FRACUNIT;
}
through = 8 * FRACUNIT;
}
else
{
through = R_InterpolateFixed(
(through - 1) * FRACUNIT,
(through * FRACUNIT)
);
}
// 24 pixels when all is said and done
if (!nextmapoverride)
playerx += through * 3;
if (upwa == false)
{
playery += through;
}
else
{
playery -= through;
}
if (through > 0 && through < 8 * FRACUNIT)
{
// Hoparabola and a skip.
const fixed_t jumpfactor = through - (4 * FRACUNIT);
// jumpfactor squared goes through 36 -> 0 -> 36.
// 12 pixels is an arbitrary jump height, but we match it to invert the parabola.
playery -= ((12 * FRACUNIT)
- (FixedMul(jumpfactor, jumpfactor) / 3)
);
}
}
// End of the moving along
}
if (choose_line != NULL)
{
// Draw the line to the right of the dot
V_DrawMappedPatch(
x - 1, basey + 11,
baseflags,
choose_line[BPP_SHADOW],
NULL
);
boolean lineisfull = false, recttoclear = false;
if (roundqueue.position > i+1)
{
lineisfull = true;
}
else if (
doanimations == true
&& roundqueue.position == i+1
&& timer - interpoffs <= 2*TICRATE
)
{
// 8 tics is chosen because it plays nice
// with both the x and y distance to cover.
const INT32 through = (2*TICRATE) - (timer - interpoffs - 1);
if (through == 0)
{
; // no change...
}
else if (through > 8)
{
lineisfull = true;
}
else
{
V_DrawMappedPatch(
x - 1, basey + 12,
baseflags,
choose_line[BPP_DONE],
colormap
);
V_SetClipRect(
playerx + FRACUNIT,
0,
(BASEVIDWIDTH + bufferspace) << FRACBITS,
BASEVIDHEIGHT << FRACBITS,
baseflags
);
recttoclear = true;
}
}
V_DrawMappedPatch(
x - 1, basey + 12,
baseflags,
choose_line[lineisfull ? BPP_DONE : BPP_AHEAD],
lineisfull ? colormap : NULL
);
if (recttoclear == true)
{
V_ClearClipRect();
}
}
else
{
// No more line! Fill in background to right edge of screen
xiter = x;
while (xiter < BASEVIDWIDTH + bufferspace)
{
xiter += 24;
V_DrawMappedPatch(xiter, basey, baseflags, queuebg_flat, greymap);
}
// Handle special entry on the end
// (has to be drawn before the semifinal dot due to overlap)
if (standings->showrank == true)
{
const fixed_t x2 = x + spacetospecial;
if (roundqueue.position == roundqueue.size)
{
playerx = (x2 * FRACUNIT);
playery = (y * FRACUNIT);
}
else if (
doanimations == true
&& roundqueue.position == roundqueue.size-1
&& timer - interpoffs <= 2*TICRATE
)
{
const INT32 through = ((2*TICRATE) - (timer - interpoffs - 1));
fixed_t linefill;
if (through > standings->linemeter)
{
linefill = standings->linemeter * FRACUNIT;
// Small judder if there's enough time for it
if (timer <= 2)
{
;
}
else if (through == (standings->linemeter + 1 + interpoffs))
{
playerx += FRACUNIT;
}
else if (through == (standings->linemeter + 2 + interpoffs))
{
playerx -= FRACUNIT;
}
}
else
{
linefill = R_InterpolateFixed(
(through - 1) * FRACUNIT,
(through * FRACUNIT)
);
}
const fixed_t percent = FixedDiv(
linefill,
(2*TICRATE) * FRACUNIT
);
playerx +=
FixedMul(
(x2 - x) * FRACUNIT,
percent
);
}
// Special background bump
V_DrawMappedPatch(x2 - 13, basey, baseflags, queuebg_prize, greymap);
// Draw the final line
const fixed_t barstart = x + 6;
const fixed_t barend = x2 - 6;
if (barend - 2 >= barstart)
{
boolean lineisfull = false, recttoclear = false;
xiter = barstart;
if (playerx >= (barend + 1) * FRACUNIT)
{
lineisfull = true;
}
else if (playerx <= (barstart - 1) * FRACUNIT)
{
;
}
else
{
const fixed_t fillend = std::min((playerx / FRACUNIT) + 2, barend);
while (xiter < fillend)
{
V_DrawMappedPatch(
xiter - 1, basey + 10,
baseflags,
line_flat[BPP_SHADOW],
NULL
);
V_DrawMappedPatch(
xiter - 1, basey + 12,
baseflags,
line_flat[BPP_DONE],
colormap
);
xiter += 2;
}
// Undo the last step so we can draw the unfilled area of the patch.
xiter -= 2;
V_SetClipRect(
playerx,
0,
(BASEVIDWIDTH + bufferspace) << FRACBITS,
BASEVIDHEIGHT << FRACBITS,
baseflags
);
recttoclear = true;
}
while (xiter < barend)
{
V_DrawMappedPatch(
xiter - 1, basey + 10,
baseflags,
line_flat[BPP_SHADOW],
NULL
);
V_DrawMappedPatch(
xiter - 1, basey + 12,
baseflags,
line_flat[lineisfull ? BPP_DONE : BPP_AHEAD],
lineisfull ? colormap : NULL
);
xiter += 2;
}
if (recttoclear == true)
{
V_ClearClipRect();
}
}
// Draw the final dot
V_DrawMappedPatch(
x2 - 8, y,
baseflags,
prize_dot[roundqueue.position == roundqueue.size ? BPP_DONE : BPP_AHEAD],
roundqueue.position == roundqueue.size ? oppositemap : colormap
);
}
// End of the special entry handling
}
// Now draw the dot
patch_t **chose_dot = NULL;
if (roundqueue.entries[i].rankrestricted == true)
{
// This shouldn't show up in regular play, but don't hide it entirely.
chose_dot = prize_dot;
}
else if (
roundqueue.entries[i].overridden == true
|| (grandprixinfo.gp == true
&& roundqueue.entries[i].gametype != GT_RACE) // roundqueue.entries[0].gametype
)
{
if ((gametypes[roundqueue.entries[i].gametype]->rules & GTR_PRISONS) == GTR_PRISONS)
{
chose_dot = capsu_dot;
}
else
{
chose_dot = bonus_dot;
}
}
else
{
chose_dot = level_dot;
}
if (chose_dot)
{
V_DrawMappedPatch(
x - 8, y,
baseflags,
chose_dot[roundqueue.position >= i+1 ? BPP_DONE : BPP_AHEAD],
roundqueue.position == i+1 ? oppositemap : colormap
);
}
x += 24;
}
// Draw the player position through the round queue!
if (playery != 0)
{
patch_t *rpmark[2];
rpmark[0] = static_cast<patch_t*>(W_CachePatchName("R_RPMARK", PU_PATCH));
rpmark[1] = static_cast<patch_t*>(W_CachePatchName("R_R2MARK", PU_PATCH));
// Change alignment
playerx -= (10 * FRACUNIT);
playery -= (14 * FRACUNIT);
if (pskin < numskins)
{
// Draw outline for rank icon
V_DrawFixedPatch(
playerx, playery,
FRACUNIT,
baseflags,
rpmark[0],
NULL
);
// Draw the player's rank icon
V_DrawFixedPatch(
playerx + FRACUNIT, playery + FRACUNIT,
FRACUNIT,
baseflags,
faceprefix[pskin][FACE_RANK],
R_GetTranslationColormap(pskin, static_cast<skincolornum_t>(pcolor), GTC_CACHE)
);
}
else
{
// Draw mini arrow
V_DrawFixedPatch(
playerx, playery,
FRACUNIT,
baseflags,
rpmark[1],
NULL
);
}
}
}
#define INTERBUTTONSLIDEIN (TICRATE/2)
//
// Y_DrawIntermissionButton
//
// It's a button that slides at the given time
//
void Y_DrawIntermissionButton(INT32 startslide, INT32 through, boolean widescreen)
{
INT32 percentslide = 0;
const INT32 slidetime = (TICRATE/4);
boolean pressed = false;
if (startslide >= 0)
{
through = startslide;
}
else
{
through -= ((TICRATE/2) + 1);
pressed = (!menuactive && M_MenuConfirmHeld(0));
}
if (through >= 0)
{
if (through >= slidetime)
{
percentslide = FRACUNIT;
}
else
{
percentslide = R_InterpolateFixed(
(through - 1) * FRACUNIT,
(through * FRACUNIT)
) / slidetime;
}
}
if (percentslide < FRACUNIT)
{
INT32 offset = 0;
if (percentslide)
{
offset = Easing_InCubic(
percentslide,
0,
16 * FRACUNIT
);
}
using srb2::Draw;
Draw::TextElement text = Draw::TextElement().parse(pressed ? "<a_pressed>" : "<a>");
Draw draw = Draw(FixedToFloat(2*FRACUNIT - offset), FixedToFloat((BASEVIDHEIGHT - 16)*FRACUNIT)).flags(widescreen ? (V_SNAPTOLEFT|V_SNAPTOBOTTOM) : 0);
draw.text(text.string());
/*
K_drawButton(
2*FRACUNIT - offset,
(BASEVIDHEIGHT - 16)*FRACUNIT,
(widescreen
? (V_SNAPTOLEFT|V_SNAPTOBOTTOM)
: 0
),
kp_button_a[1],
pressed
);
*/
}
}
void Y_DrawIntermissionHeader(fixed_t x, fixed_t y, boolean gotthrough, const char *headerstring, boolean showroundnum, boolean small)
{
const INT32 v_width = (small ? BASEVIDWIDTH/2 : BASEVIDWIDTH);
const fixed_t frac = (small ? FRACUNIT/2 : FRACUNIT);
const INT32 small_flag = (small ? V_SPLITSCREEN : 0);
if (small && r_splitscreen > 1)
{
V_SetClipRect(
0,
0,
v_width << FRACBITS,
BASEVIDHEIGHT << FRACBITS,
V_SPLITSCREEN
);
}
// Header bar
patch_t *rtpbr = static_cast<patch_t*>(W_CachePatchName((small ? "R_RTPB4" : "R_RTPBR"), PU_PATCH));
V_DrawFixedPatch((20 * frac) + x, (24 * frac) + y, FRACUNIT, small_flag, rtpbr, NULL);
fixed_t headerx, headery, headerwidth = 0;
if (gotthrough)
{
headerx = (51 * frac);
headery = (7 * frac);
}
else
{
headerwidth = V_TitleCardStringWidth(headerstring, small);
headerx = (v_width - headerwidth) * (FRACUNIT / 2);
headery = 17 * frac;
}
// Draw round numbers
if (showroundnum == true)
{
patch_t *roundpatch = ST_getRoundPicture(small);
if (roundpatch)
{
fixed_t roundx = (v_width * 3 * FRACUNIT) / 4;
if (headerwidth != 0)
{
const fixed_t roundoffset = (8 * frac) + (roundpatch->width * FRACUNIT);
roundx = headerx + roundoffset;
headerx -= roundoffset/2;
}
V_DrawFixedPatch(x + roundx, (39 * frac) + y, FRACUNIT, small_flag, roundpatch, NULL);
}
}
V_DrawTitleCardStringFixed(x + headerx, y + headery, FRACUNIT, headerstring, small_flag, false, 0, 0, small);
if (gotthrough)
{
// GOT THROUGH ROUND
patch_t *gthro = static_cast<patch_t*>(W_CachePatchName((small ? "R_GTHR4" : "R_GTHRO"), PU_PATCH));
V_DrawFixedPatch((50 * frac) + x, (42 * frac) + y, FRACUNIT, small_flag, gthro, NULL);
}
V_ClearClipRect();
}
static void Y_DrawMapTitleString(fixed_t x, const char *name)
{
V_DrawStringScaled(
x - ttlscroll,
(BASEVIDHEIGHT - 73) * FRACUNIT,
FRACUNIT,
FRACUNIT,
FRACUNIT,
V_SUBTRACT | V_60TRANS,
NULL,
LSHI_FONT,
name
);
}
static fixed_t Y_DrawMapTitle(void)
{
const char *name = bossinfo.valid && bossinfo.enemyname ?
bossinfo.enemyname : mapheaderinfo[prevmap]->menuttl;
char *buf = NULL;
if (!name[0])
{
buf = G_BuildMapTitle(prevmap + 1);
name = buf;
}
fixed_t w = V_StringScaledWidth(
FRACUNIT,
FRACUNIT,
FRACUNIT,
0,
LSHI_FONT,
name
) + (16 * FRACUNIT);
fixed_t x = BASEVIDWIDTH * FRACUNIT;
while (x > -w)
{
Y_DrawMapTitleString(x, name);
x -= w;
}
Z_Free(buf);
return w;
}
//
// Y_IntermissionDrawer
//
// Called by D_Display. Nothing is modified here; all it does is draw. (SRB2Kart: er, about that...)
// Neat concept, huh?
//
void Y_IntermissionDrawer(void)
{
// INFO SEGMENT
// Numbers are V_DrawRightAlignedThinString as flags
// resbar 1 (48,82) 5 (176, 82)
// 2 (48, 96)
//player icon 1 (55,79) 2 (55,93) 5 (183,79)
if (intertype == int_none || rendermode == render_none)
return;
fixed_t x;
// Checker scroll
patch_t *rbgchk = static_cast<patch_t*>(W_CachePatchName("R_RBGCHK", PU_PATCH));
// Scrolling marquee
patch_t *rrmq = static_cast<patch_t*>(W_CachePatchName("R_RRMQ", PU_PATCH));
fixed_t mqloop = SHORT(rrmq->width)*FRACUNIT;
fixed_t chkloop = SHORT(rbgchk->width)*FRACUNIT;
UINT8 *bgcolor = R_GetTranslationColormap(TC_INTERMISSION, static_cast<skincolornum_t>(0), GTC_CACHE);
// Draw the background
K_DrawMapThumbnail(0, 0, BASEVIDWIDTH<<FRACBITS, (data.encore ? V_FLIP : 0), prevmap, bgcolor);
for (x = -mqscroll; x < (BASEVIDWIDTH * FRACUNIT); x += mqloop)
{
V_DrawFixedPatch(x, 154<<FRACBITS, FRACUNIT, V_SUBTRACT, rrmq, NULL);
}
V_DrawFixedPatch(chkscroll, 0, FRACUNIT, V_SUBTRACT, rbgchk, NULL);
V_DrawFixedPatch(chkscroll - chkloop, 0, FRACUNIT, V_SUBTRACT, rbgchk, NULL);
fixed_t ttlloop = Y_DrawMapTitle();
// Animate scrolling elements if relevant
if (!paused && !P_AutoPause())
{
mqscroll += renderdeltatics;
if (mqscroll > mqloop)
mqscroll %= mqloop;
chkscroll += renderdeltatics;
if (chkscroll > chkloop)
chkscroll %= chkloop;
ttlscroll += renderdeltatics * 2;
if (ttlscroll > ttlloop)
ttlscroll %= ttlloop;
}
if (renderisnewtic)
{
LUA_HUD_ClearDrawList(luahuddrawlist_intermission);
LUA_HookHUD(luahuddrawlist_intermission, HUD_HOOK(intermission));
}
LUA_HUD_DrawList(luahuddrawlist_intermission);
if (!LUA_HudEnabled(hud_intermissiontally))
goto skiptallydrawer;
x = 0;
if (sorttic != -1 && intertic > sorttic)
{
const INT32 count = (intertic - sorttic);
if (count < 8)
x = -((count * BASEVIDWIDTH) / 8);
else if (count == 8)
goto skiptallydrawer;
else if (count < 16)
x = (((16 - count) * BASEVIDWIDTH) / 8);
}
// Draw the header bar
Y_DrawIntermissionHeader(x << FRACBITS, 0, data.gotthrough, data.headerstring, data.showroundnum, false);
// Returns early if there's no players to draw
Y_PlayerStandingsDrawer(&data, x);
// Draw bottom (and top) pieces
skiptallydrawer:
if (!LUA_HudEnabled(hud_intermissionmessages))
goto finalcounter;
// Returns early if there's no roundqueue entries to draw
Y_RoundQueueDrawer(&data, 0, true, false);
if (netgame)
{
if (speedscramble != -1 && speedscramble != gamespeed)
{
V_DrawCenteredThinString(BASEVIDWIDTH/2, 154, highlightflags,
va(M_GetText("Next race will be %s Speed!"), kartspeed_cons_t[1+speedscramble].strvalue));
}
}
finalcounter:
if ((modeattacking == ATTACKING_NONE) && demo.recording)
ST_DrawSaveReplayHint(0);
if (Y_CanSkipIntermission())
{
const tic_t end = roundqueue.size != 0 ? 3*TICRATE : TICRATE;
Y_DrawIntermissionButton(INTERBUTTONSLIDEIN - intertic, end - timer, false);
}
else
{
const INT32 tickDown = (timer + 1)/TICRATE;
// See also k_vote.c
V__DrawOneScaleString(
2*FRACUNIT,
(BASEVIDHEIGHT - (2+8))*FRACUNIT,
FRACUNIT,
0, NULL,
OPPRF_FONT,
va("%d", tickDown)
);
}
M_DrawMenuForeground();
}
//
// Y_Ticker
//
// Manages fake score tally for single player end of act, and decides when intermission is over.
//
void Y_Ticker(void)
{
if (intertype == int_none)
return;
if (demo.recording)
G_CheckDemoTitleEntry();
// Check for pause or menu up in single player
if (paused || P_AutoPause())
return;
LUA_HOOK(IntermissionThinker);
if (Y_CanSkipIntermission())
{
if (intertic < INTERBUTTONSLIDEIN)
{
intertic++;
return;
}
boolean preventintertic = (intertic == INTERBUTTONSLIDEIN);
if (!menuactive && M_MenuConfirmPressed(0))
{
// If there is a roundqueue, make time for it.
// Else, end instantly on button press.
// Actually, give it a slight delay, so the "kaching" sound isn't cut off.
const tic_t end = roundqueue.size != 0 ? 3*TICRATE : TICRATE;
if (intertic == INTERBUTTONSLIDEIN) // card flip hasn't started
{
if (sorttic != -1)
{
intertic = sorttic;
}
else
{
timer = end;
}
preventintertic = false;
}
else if (timer >= INFINITE_TIMER && intertic >= sorttic + 16) // card done flipping
{
const INT32 kaching = sorttic + 16 + (2*TICRATE);
if (intertic < kaching)
{
intertic = kaching; // kaching immediately
}
timer = end;
}
}
if (preventintertic)
{
return;
}
}
intertic++;
// Team scramble code for team match and CTF.
// Don't do this if we're going to automatically scramble teams next round.
/*
if (G_GametypeHasTeams() && cv_teamscramble.value && !cv_scrambleonchange.value && server)
{
// If we run out of time in intermission, the beauty is that
// the P_Ticker() team scramble code will pick it up.
if ((intertic % (TICRATE/7)) == 0)
P_DoTeamscrambling();
}
*/
if ((timer < INFINITE_TIMER && --timer <= 0)
|| (intertic == endtic))
{
Y_EndIntermission();
G_AfterIntermission();
return;
}
// Animation sounds for roundqueue, see Y_RoundQueueDrawer
if (roundqueue.size > 1
&& roundqueue.position != 0
&& (timer - 1) <= 2*TICRATE)
{
const INT32 through = ((2*TICRATE) - (timer - 1));
UINT8 workingqueuesize = roundqueue.size - 1;
if (data.showrank == true
&& roundqueue.position == workingqueuesize)
{
// Handle special entry on the end
if (through == data.linemeter && timer > 2)
{
S_StopSoundByID(NULL, sfx_gpmetr);
S_StartSound(NULL, sfx_kc50);
}
else if (through == 0)
{
S_StartSound(NULL, sfx_gpmetr);
}
}
else
{
if (data.showrank == false
&& roundqueue.entries[workingqueuesize].rankrestricted == true)
{
workingqueuesize--;
}
if (through == 9
&& roundqueue.position <= workingqueuesize)
{
// Impactful landing
S_StartSound(NULL, sfx_kc50);
}
}
}
if (intertic < TICRATE || endtic != -1)
{
return;
}
if (data.rankingsmode && intertic & 1)
{
memset(data.jitter, 0, sizeof (data.jitter));
return;
}
if (intertype == int_time || intertype == int_score)
{
{
if (!data.rankingsmode && sorttic != -1 && (intertic >= sorttic + 8))
{
Y_MidIntermission();
Y_CalculateMatchData(1, Y_CompareRank);
}
if (data.rankingsmode && intertic > sorttic+16+(2*TICRATE))
{
INT32 q=0,r=0;
boolean kaching = true;
for (q = 0; q < data.numplayers; q++)
{
if (data.num[q] == MAXPLAYERS
|| !data.increase[data.num[q]]
|| data.increase[data.num[q]] == INT16_MIN)
{
continue;
}
r++;
data.jitter[data.num[q]] = 1;
// Player can skip the tally, kaching!
if (Y_CanSkipIntermission() && timer < INFINITE_TIMER)
{
data.increase[data.num[q]] = 0;
}
if (powertype != PWRLV_DISABLED)
{
// Power Levels
if (abs(data.increase[data.num[q]]) < 10)
{
// Not a lot of point increase left, just set to 0 instantly
data.increase[data.num[q]] = 0;
}
else
{
SINT8 remove = 0; // default (should not happen)
if (data.increase[data.num[q]] < 0)
remove = -10;
else if (data.increase[data.num[q]] > 0)
remove = 10;
// Remove 10 points at a time
data.increase[data.num[q]] -= remove;
// Still not zero, no kaching yet
if (data.increase[data.num[q]] != 0)
kaching = false;
}
}
else
{
// Basic bitch points
if (data.increase[data.num[q]])
{
if (--data.increase[data.num[q]])
kaching = false;
}
}
}
if (r)
{
S_StartSound(NULL, (kaching ? sfx_chchng : sfx_ptally));
Y_CalculateMatchData(2, Y_CompareRank);
}
/*else -- This is how to define an endtic, but we currently use timer for both SP and MP.
endtic = intertic + 3*TICRATE;*/
}
}
}
}
boolean Y_ShouldDoIntermission(void)
{
// no intermission for GP events
if ((grandprixinfo.gp == true && grandprixinfo.eventmode != GPEVENT_NONE)
// or for failing in time attack mode
|| (modeattacking && (players[consoleplayer].pflags & PF_NOCONTEST))
// or for explicit requested skip (outside of modeattacking)
|| (modeattacking == ATTACKING_NONE && skipstats != 0)
// or tutorial skip material
|| (nextmapoverride == NEXTMAP_TUTORIALCHALLENGE+1 || tutorialchallenge != TUTORIALSKIP_NONE)
// or title screen attract demos
|| (demo.playback && demo.attract == DEMO_ATTRACT_TITLE))
{
return false;
}
return true;
}
//
// Y_DetermineIntermissionType
//
// Determines the intermission type from the current gametype.
//
void Y_DetermineIntermissionType(void)
{
// no intermission for GP events
if (!Y_ShouldDoIntermission())
{
intertype = int_none;
return;
}
// set initially
intertype = static_cast<intertype_t>(gametypes[gametype]->intermission);
// special cases
if (intertype == int_scoreortimeattack)
{
UINT8 i = 0, nump = 0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
continue;
nump++;
}
intertype = (nump < 2 ? int_time : int_score);
}
}
static UINT8 Y_PlayersBestPossiblePosition(player_t *const player)
{
UINT8 bestPossiblePosition = MAXPLAYERS + 1;
UINT8 i = UINT8_MAX;
if ((player->pflags & PF_NOCONTEST) == 0)
{
if (player->exiting)
{
// They are finished, so their position is set in stone.
bestPossiblePosition = player->position;
}
else
{
// If they're NOT finished, then check what their points could be
// if they finished in the first available position.
bestPossiblePosition = 1;
for (i = 0; i < MAXPLAYERS; i++)
{
player_t *const other = &players[i];
if (!playeringame[i] || other->spectator)
{
continue;
}
if (other == player)
{
continue;
}
if (other->exiting)
{
bestPossiblePosition = std::max<UINT8>(bestPossiblePosition, other->position + 1);
}
}
}
}
return bestPossiblePosition;
}
static UINT32 Y_EstimatePodiumScore(player_t *const player, UINT8 numPlaying)
{
UINT8 pos = Y_PlayersBestPossiblePosition(player);
UINT32 ourScore = player->score;
if (pos < numPlaying)
{
ourScore += K_CalculateGPRankPoints(pos, numPlaying);
}
return ourScore;
}
static boolean Y_GuaranteedGPFirstPlace(void)
{
player_t *bestInParty = nullptr;
UINT32 bestPartyScore = 0;
UINT8 numPlaying = spectateGriefed;
UINT8 i = UINT8_MAX;
// Quick first loop to count players.
for (i = 0; i < MAXPLAYERS; i++)
{
player_t *const comparePlayer = &players[i];
if (!playeringame[i] || comparePlayer->spectator)
{
continue;
}
numPlaying++;
}
// Iterate our party, estimate the best possible exiting score out of all of them.
for (i = 0; i <= r_splitscreen; i++)
{
player_t *const comparePlayer = &players[displayplayers[i]];
if (comparePlayer->spectator)
{
continue;
}
if (!comparePlayer->exiting)
{
continue;
}
UINT32 newScore = Y_EstimatePodiumScore(comparePlayer, numPlaying);
if (bestInParty == nullptr || newScore > bestPartyScore)
{
bestInParty = comparePlayer;
bestPartyScore = newScore;
}
}
if (bestInParty == nullptr)
{
// No partied players are actually available,
// so always use the regular intermission music.
return false;
}
// Iterate through all players not belonging to our party.
// Estimate the possible scores that they could get.
// Play special music only if none of these scores beat ours!
for (i = 0; i < MAXPLAYERS; i++)
{
player_t *comparePlayer = &players[i];
if (!playeringame[i] || comparePlayer->spectator)
{
continue;
}
if (P_IsPartyPlayer(comparePlayer))
{
continue;
}
if (Y_EstimatePodiumScore(comparePlayer, numPlaying) >= bestPartyScore)
{
// NO, there is a chance that we will NOT finish first!
// You may still be able to finish first, but it is NOT guaranteed.
return false;
}
}
// There is an overwhelmingly good chance
// that we are finishing in first place.
return true;
}
void Y_PlayIntermissionMusic(void)
{
if (modeattacking != ATTACKING_NONE)
{
Music_Remap("intermission", "timent");
}
else if (grandprixinfo.gp == true
&& grandprixinfo.cup != nullptr
&& roundqueue.size > 0
&& roundqueue.roundnum >= grandprixinfo.cup->numlevels)
{
if (Y_GuaranteedGPFirstPlace())
{
Music_Remap("intermission", "gprnds");
}
else
{
Music_Remap("intermission", "gprnd5");
}
}
else
{
Music_Remap("intermission", "racent");
}
if (!Music_Playing("intermission"))
Music_Play("intermission");
}
//
// Y_StartIntermission
//
// Called by G_DoCompleted. Sets up data for intermission drawer/ticker.
//
void Y_StartIntermission(void)
{
UINT8 i = 0, nump = 0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
continue;
nump++;
}
intertic = -1;
#ifdef PARANOIA
if (endtic != -1)
I_Error("endtic is dirty");
#endif
// set player Power Level type
powertype = K_UsingPowerLevels();
// determine the tic the intermission ends
// Technically cv_inttime is saved to demos... but this permits having extremely long timers for post-netgame chatting without stranding you on the intermission in netreplays.
if (!K_CanChangeRules(false))
{
timer = 10*TICRATE;
}
else
{
timer = cv_inttime.value*TICRATE;
}
// determine the tic everybody's scores/PWR starts getting sorted
sorttic = -1;
if (!timer)
;
else if (
( // Match Race or Time Attack
netgame == false
&& grandprixinfo.gp == false
)
&& (
modeattacking != ATTACKING_NONE // Definitely never another map
|| ( // Any level sequence?
roundqueue.size == 0 // No maps queued, points aren't relevant
|| roundqueue.position == 0 // OR points from this round will be discarded
)
)
)
{
// No PWR/global score, skip it
// (the above is influenced by G_GetNextMap)
timer /= 2;
}
else
{
// Minimum two seconds for match results, then two second slideover approx halfway through
sorttic = std::max<INT32>((timer/2) - 2*TICRATE, 2*TICRATE);
}
// TODO: code's a mess, I'm just making it extra clear
// that this piece of code is supposed to take priority
// over the above. :)
if (Y_CanSkipIntermission())
{
timer = INFINITE_TIMER; // doesn't count down
if (sorttic != -1)
{
// Will start immediately, but must be triggered.
// Needs to be TICRATE to bypass a condition in Y_Ticker.
sorttic = TICRATE;
}
}
// We couldn't display the intermission even if we wanted to.
// But we still need to give the players their score bonuses, dummy.
//if (dedicated) return;
// This should always exist, but just in case...
if (prevmap >= nummapheaders || !mapheaderinfo[prevmap])
I_Error("Y_StartIntermission: Internal map ID %d not found (nummapheaders = %d)", prevmap, nummapheaders);
switch (intertype)
{
case int_score:
{
// Calculate who won
Y_CalculateMatchData(0, Y_CompareScore);
break;
}
case int_time:
{
// Calculate who won
Y_CalculateMatchData(0, Y_CompareTime);
break;
}
case int_none:
default:
break;
}
if (powertype != PWRLV_DISABLED)
{
for (i = 0; i < MAXPLAYERS; i++)
{
// Kind of a hack to do this here,
// but couldn't think of a better way.
data.increase[i] = K_FinalPowerIncrement(
&players[i],
clientpowerlevels[i][powertype],
clientPowerAdd[i]
);
}
K_CashInPowerLevels();
SV_BumpMatchStats();
}
if (!timer)
{
Y_EndIntermission();
return;
}
G_SetGamestate(GS_INTERMISSION);
if (demo.playback)
{
// Replay menu is inacessible here.
// Press A to continue!
M_ClearMenus(true);
}
if (musiccountdown == 0)
{
Y_PlayIntermissionMusic();
}
S_ShowMusicCredit(); // Always call
LUA_HUD_DestroyDrawList(luahuddrawlist_intermission);
luahuddrawlist_intermission = LUA_HUD_CreateDrawList();
if (roundqueue.size > 0 && roundqueue.position == roundqueue.size)
{
Automate_Run(AEV_QUEUEEND);
}
Automate_Run(AEV_INTERMISSIONSTART);
bgpatch = static_cast<patch_t*>(W_CachePatchName("MENUBG", PU_STATIC));
widebgpatch = static_cast<patch_t*>(W_CachePatchName("WEIRDRES", PU_STATIC));
}
// ======
//
// Y_MidIntermission
//
void Y_MidIntermission(void)
{
// Replacing bots that fail out of play
K_RetireBots();
// If tournament play is not in action...
if (roundqueue.position == 0)
{
// Unset player teams in anticipation of P_ShuffleTeams
UINT8 i;
for (i = 0; i < MAXPLAYERS; i++)
{
players[i].team = TEAM_UNASSIGNED;
}
}
}
//
// Y_EndIntermission
//
void Y_EndIntermission(void)
{
if (!data.rankingsmode)
{
Y_MidIntermission();
}
Y_UnloadData();
endtic = -1;
sorttic = -1;
intertype = int_none;
}
#define UNLOAD(x) if (x) {Patch_Free(x);} x = NULL;
#define CLEANUP(x) x = NULL;
//
// Y_UnloadData
//
static void Y_UnloadData(void)
{
// In hardware mode, don't Z_ChangeTag a pointer returned by W_CachePatchName().
// It doesn't work and is unnecessary.
if (rendermode != render_soft)
return;
// unload the background patches
UNLOAD(bgpatch);
UNLOAD(widebgpatch);
UNLOAD(bgtile);
UNLOAD(interpic);
}