// SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- // Copyright (C) 2004-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.c /// \brief Tally screens, or "Intermissions" as they were formally called in Doom #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 "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" #ifdef HWRENDER #include "hardware/hw_main.h" #endif typedef struct { char patch[9]; INT32 points; UINT8 display; } y_bonus_t; typedef struct { INT32 *character[MAXPLAYERS]; // Winner's character # UINT16 *color[MAXPLAYERS]; // Winner's color # SINT8 num[MAXPLAYERS]; // Winner's player # char *name[MAXPLAYERS]; // Winner's name UINT8 numplayers; // Number of players being displayed char levelstring[64]; // holds levelnames up to 64 characters // SRB2kart INT16 increase[MAXPLAYERS]; // how much did the score increase by? UINT8 jitter[MAXPLAYERS]; // wiggle UINT32 val[MAXPLAYERS]; // Gametype-specific value UINT8 pos[MAXPLAYERS]; // player positions. used for ties boolean rankingsmode; // rankings mode boolean encore; // encore mode } y_data; static y_data 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 static INT32 timer; static INT32 powertype = PWRLV_DISABLED; static INT32 intertic; static INT32 endtic = -1; static INT32 sorttic = -1; static INT32 replayprompttic; intertype_t intertype = int_none; static huddrawlist_h luahuddrawlist_intermission; 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; // Initialize variables if (rankingsmode > 1) ; else if ((data.rankingsmode = (boolean)rankingsmode)) { sprintf(data.levelstring, "* Total Rankings *"); data.encore = false; } else { // set up the levelstring if (bossinfo.valid == true && bossinfo.enemyname) { snprintf(data.levelstring, sizeof data.levelstring, "* %s *", bossinfo.enemyname); } else if (mapheaderinfo[prevmap]->levelflags & LF_NOZONE) { if (mapheaderinfo[prevmap]->actnum > 0) snprintf(data.levelstring, sizeof data.levelstring, "* %s %d *", mapheaderinfo[prevmap]->lvlttl, mapheaderinfo[prevmap]->actnum); else snprintf(data.levelstring, sizeof data.levelstring, "* %s *", mapheaderinfo[prevmap]->lvlttl); } else { const char *zonttl = (mapheaderinfo[prevmap]->zonttl[0] ? mapheaderinfo[prevmap]->zonttl : "ZONE"); if (mapheaderinfo[prevmap]->actnum > 0) snprintf(data.levelstring, sizeof data.levelstring, "* %s %s %d *", mapheaderinfo[prevmap]->lvlttl, zonttl, mapheaderinfo[prevmap]->actnum); else snprintf(data.levelstring, sizeof data.levelstring, "* %s %s *", mapheaderinfo[prevmap]->lvlttl, zonttl); } data.levelstring[sizeof data.levelstring - 1] = '\0'; data.encore = encoremode; memset(data.jitter, 0, sizeof (data.jitter)); } for (i = 0; i < MAXPLAYERS; i++) { data.val[i] = UINT32_MAX; if (!playeringame[i] || players[i].spectator) { data.increase[i] = INT16_MIN; continue; } if (!rankingsmode) data.increase[i] = INT16_MIN; numplayersingame++; } memset(data.color, 0, sizeof (data.color)); memset(data.character, 0, sizeof (data.character)); memset(completed, 0, sizeof (completed)); data.numplayers = 0; for (j = 0; j < numplayersingame; j++) { for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator || completed[i]) continue; comparison(i); } i = data.num[data.numplayers]; completed[i] = true; data.color[data.numplayers] = &players[i].skincolor; data.character[data.numplayers] = &players[i].skin; data.name[data.numplayers] = player_names[i]; 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; } if (!rankingsmode) { if ((powertype == PWRLV_DISABLED) && !(players[i].pflags & PF_NOCONTEST) && (data.pos[data.numplayers] < (numplayersingame + spectateGriefed))) { // Online rank is handled further below in this file. data.increase[i] = K_CalculateGPRankPoints(data.pos[data.numplayers], numplayersingame + spectateGriefed); players[i].score += data.increase[i]; } if (demo.recording) { G_WriteStanding( data.pos[data.numplayers], data.name[data.numplayers], *data.character[data.numplayers], *data.color[data.numplayers], data.val[data.numplayers] ); } } data.numplayers++; } } // // 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) { INT32 i, whiteplayer = MAXPLAYERS, x = 4, hilicol = highlightflags; // If we early return, skip drawing the 3D scene (software buffer) so it doesn't clobber the frame for the wipe g_wipeskiprender = true; if (intertype == int_none || rendermode == render_none) return; g_wipeskiprender = false; // the merge was kind of a mess, how does this work -- toast 171021 { M_DrawMenuBackground(); } 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; if (!r_splitscreen) whiteplayer = demo.playback ? displayplayers[0] : consoleplayer; if (sorttic != -1 && intertic > sorttic) { INT32 count = (intertic - sorttic); if (count < 8) x -= ((count * vid.width) / (8 * vid.dupx)); else if (count == 8) goto skiptallydrawer; else if (count < 16) x += (((16 - count) * vid.width) / (8 * vid.dupx)); } if (intertype == int_time || intertype == int_score) { #define NUMFORNEWCOLUMN 8 INT32 y = 41, gutter = ((data.numplayers > NUMFORNEWCOLUMN) ? 0 : (BASEVIDWIDTH/2)); INT32 dupadjust = (vid.width/vid.dupx), duptweak = (dupadjust - BASEVIDWIDTH)/2; const char *timeheader; int y2; if (data.rankingsmode) { if (powertype == PWRLV_DISABLED) { timeheader = "RANK"; } else { timeheader = "PWR.LV"; } } else { switch (intertype) { case int_score: timeheader = "SCORE"; break; default: timeheader = "TIME"; break; } } // draw the level name V_DrawCenteredString(-4 + x + BASEVIDWIDTH/2, 12, 0, data.levelstring); V_DrawFill((x-3) - duptweak, 34, dupadjust-2, 1, 0); if (data.encore) V_DrawCenteredString(-4 + x + BASEVIDWIDTH/2, 12-8, hilicol, "ENCORE MODE"); if (data.numplayers > NUMFORNEWCOLUMN) { V_DrawFill(x+156, 24, 1, 158, 0); V_DrawFill((x-3) - duptweak, 182, dupadjust-2, 1, 0); V_DrawCenteredString(x+6+(BASEVIDWIDTH/2), 24, hilicol, "#"); V_DrawString(x+36+(BASEVIDWIDTH/2), 24, hilicol, "NAME"); V_DrawRightAlignedString(x+152, 24, hilicol, timeheader); } V_DrawCenteredString(x+6, 24, hilicol, "#"); V_DrawString(x+36, 24, hilicol, "NAME"); V_DrawRightAlignedString(x+(BASEVIDWIDTH/2)+152, 24, hilicol, timeheader); for (i = 0; i < data.numplayers; i++) { boolean dojitter = data.jitter[data.num[i]]; data.jitter[data.num[i]] = 0; if (data.num[i] != MAXPLAYERS && playeringame[data.num[i]] && !players[data.num[i]].spectator) { char strtime[MAXPLAYERNAME+1]; if (dojitter) y--; V_DrawCenteredString(x+6, y, 0, va("%d", data.pos[i])); if (data.color[i]) { UINT8 *colormap = R_GetTranslationColormap(*data.character[i], *data.color[i], GTC_CACHE); V_DrawMappedPatch(x+16, y-4, 0, faceprefix[*data.character[i]][FACE_RANK], colormap); } if (data.num[i] == whiteplayer) { UINT8 cursorframe = (intertic / 4) % 8; V_DrawScaledPatch(x+16, y-4, 0, W_CachePatchName(va("K_CHILI%d", cursorframe+1), PU_CACHE)); } if ((players[data.num[i]].pflags & PF_NOCONTEST) && players[data.num[i]].bot) { // RETIRED!! V_DrawScaledPatch(x+12, y-7, 0, W_CachePatchName("K_NOBLNS", PU_CACHE)); } STRBUFCPY(strtime, data.name[i]); y2 = y; if ((netgame || (demo.playback && demo.netgame)) && playerconsole[data.num[i]] == 0 && server_lagless && !players[data.num[i]].bot) { static int 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; } if (data.numplayers > NUMFORNEWCOLUMN) V_DrawThinString(x+36, y2-1, ((data.num[i] == whiteplayer) ? hilicol : 0)|V_ALLOWLOWERCASE|V_6WIDTHSPACE, strtime); else V_DrawString(x+36, y2, ((data.num[i] == whiteplayer) ? hilicol : 0)|V_ALLOWLOWERCASE, strtime); if (data.rankingsmode) { if (powertype != PWRLV_DISABLED && !clientpowerlevels[data.num[i]][powertype]) { // No power level (guests) STRBUFCPY(strtime, "----"); } else { if (data.increase[data.num[i]] != INT16_MIN) { snprintf(strtime, sizeof strtime, "(%d)", data.increase[data.num[i]]); if (data.numplayers > NUMFORNEWCOLUMN) V_DrawRightAlignedThinString(x+133+gutter, y-1, V_6WIDTHSPACE, strtime); else V_DrawRightAlignedString(x+118+gutter, y, 0, strtime); } snprintf(strtime, sizeof strtime, "%d", data.val[i]); } if (data.numplayers > NUMFORNEWCOLUMN) V_DrawRightAlignedThinString(x+152+gutter, y-1, V_6WIDTHSPACE, strtime); else V_DrawRightAlignedString(x+152+gutter, y, 0, strtime); } else { if (data.val[i] == (UINT32_MAX-1)) V_DrawRightAlignedThinString(x+152+gutter, y-1, (data.numplayers > NUMFORNEWCOLUMN ? V_6WIDTHSPACE : 0), "NO CONTEST."); else { if (intertype == int_time) { snprintf(strtime, sizeof strtime, "%i'%02i\"%02i", G_TicsToMinutes(data.val[i], true), G_TicsToSeconds(data.val[i]), G_TicsToCentiseconds(data.val[i])); strtime[sizeof strtime - 1] = '\0'; if (data.numplayers > NUMFORNEWCOLUMN) V_DrawRightAlignedThinString(x+152+gutter, y-1, V_6WIDTHSPACE, strtime); else V_DrawRightAlignedString(x+152+gutter, y, 0, strtime); } else { if (data.numplayers > NUMFORNEWCOLUMN) V_DrawRightAlignedThinString(x+152+gutter, y-1, V_6WIDTHSPACE, va("%i", data.val[i])); else V_DrawRightAlignedString(x+152+gutter, y, 0, va("%i", data.val[i])); } } } if (dojitter) y++; } else data.num[i] = MAXPLAYERS; // this should be the only field setting in this function y += 18; if (i == NUMFORNEWCOLUMN-1) { y = 41; x += BASEVIDWIDTH/2; } #undef NUMFORNEWCOLUMN } } skiptallydrawer: if (!LUA_HudEnabled(hud_intermissionmessages)) return; if (timer) { if (netgame || demo.netgame) { char *string; INT32 tickdown = (timer+1)/TICRATE; if (demo.playback) string = va("Replay ends in %d", tickdown); else if ((nextmapoverride != 0) || (roundqueue.size > 0 && roundqueue.position < roundqueue.size)) string = va("Next starts in %d", tickdown); else string = va("%s starts in %d", cv_advancemap.string, tickdown); V_DrawCenteredString(BASEVIDWIDTH/2, 188, hilicol, string); if (speedscramble != -1 && speedscramble != gamespeed) { V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24, hilicol|V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, va(M_GetText("Next race will be %s Speed!"), kartspeed_cons_t[1+speedscramble].strvalue)); } } if ((modeattacking == ATTACKING_NONE) && (demo.recording || demo.savemode == DSM_SAVED) && !demo.playback) { switch (demo.savemode) { case DSM_NOTSAVING: { INT32 buttonx = BASEVIDWIDTH; INT32 buttony = 2; K_drawButtonAnim(buttonx - 76, buttony, V_SNAPTOTOP|V_SNAPTORIGHT, kp_button_b[1], replayprompttic); V_DrawRightAlignedThinString(buttonx - 55, buttony, V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|V_6WIDTHSPACE|hilicol, "or"); K_drawButtonAnim(buttonx - 55, buttony, V_SNAPTOTOP|V_SNAPTORIGHT, kp_button_x[1], replayprompttic); V_DrawRightAlignedThinString(buttonx - 2, buttony, V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|V_6WIDTHSPACE|hilicol, "Save replay"); break; } case DSM_SAVED: V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|V_6WIDTHSPACE|hilicol, "Replay saved!"); break; case DSM_TITLEENTRY: ST_DrawDemoTitleEntry(); break; default: // Don't render any text here break; } } } 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) { if (demo.savemode == DSM_NOTSAVING) { replayprompttic++; G_CheckDemoTitleEntry(); } if (demo.savemode == DSM_WILLSAVE || demo.savemode == DSM_WILLAUTOSAVE) G_SaveDemo(); } // Check for pause or menu up in single player if (paused || P_AutoPause()) return; LUA_HOOK(IntermissionThinker); 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 && !--timer) || (intertic == endtic)) { Y_EndIntermission(); G_AfterIntermission(); return; } if (intertic < TICRATE || intertic & 1 || endtic != -1) return; if (intertype == int_time || intertype == int_score) { { if (!data.rankingsmode && sorttic != -1 && (intertic >= sorttic + 8)) { // Anything with post-intermission consequences here should also occur in Y_EndIntermission. K_RetireBots(); 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; 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;*/ } } } } // // Y_DetermineIntermissionType // // Determines the intermission type from the current gametype. // void Y_DetermineIntermissionType(void) { // no intermission for GP events if (grandprixinfo.gp == true && grandprixinfo.eventmode != GPEVENT_NONE) { intertype = int_none; return; } // set initially intertype = 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); } } // // 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) { // Prevent a weird bug timer = 1; } else if (nump < 2 && !netgame) { // No PWR/global score, skip it timer /= 2; } else { // Minimum two seconds for match results, then two second slideover approx halfway through sorttic = max((timer/2) - 2*TICRATE, 2*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); if (!(gametyperules & GTR_CIRCUIT) && (timer > 1)) S_ChangeMusicInternal("racent", true); // loop it S_ShowMusicCredit(); // Always call 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; } LUA_HUD_DestroyDrawList(luahuddrawlist_intermission); luahuddrawlist_intermission = LUA_HUD_CreateDrawList(); 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(); } Automate_Run(AEV_INTERMISSIONSTART); bgpatch = W_CachePatchName("MENUBG", PU_STATIC); widebgpatch = W_CachePatchName("WEIRDRES", PU_STATIC); M_UpdateMenuBGImage(true); } // ====== // // Y_EndIntermission // void Y_EndIntermission(void) { if (!data.rankingsmode) { K_RetireBots(); } 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); }