From c906ccc7a15def2638c80c5cd4552f605cb1ebab Mon Sep 17 00:00:00 2001 From: Anonimus Date: Sat, 13 Sep 2025 23:03:17 -0400 Subject: [PATCH] Add race lap splits display from Ring Racers Very neat feature --- src/d_netcmd.c | 8 +++- src/d_netcmd.h | 1 + src/d_player.h | 25 +++++++++++ src/g_game.c | 20 ++++++++- src/k_hud.c | 113 ++++++++++++++++++++++++++++++++++++++++++++++++- src/k_kart.c | 109 +++++++++++++++++++++++++++++++++++++++++++++++ src/k_kart.h | 1 + src/p_saveg.c | 8 ++++ src/p_spec.c | 28 +++++++++--- src/p_user.c | 3 ++ 10 files changed, 306 insertions(+), 10 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 514b61738..725e50a6c 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -648,7 +648,12 @@ consvar_t cv_showping = CVAR_INIT ("showping", "Always", CV_SAVE, showping_cons_ static CV_PossibleValue_t pingmeasurement_cons_t[] = {{0, "Frames"}, {1, "Milliseconds"}, {0, NULL}}; consvar_t cv_pingmeasurement = CVAR_INIT ("pingmeasurement", "Frames", CV_SAVE, pingmeasurement_cons_t, NULL); -consvar_t cv_showlapemblem = CVAR_INIT ("showlapemblem", "On", CV_SAVE, CV_OnOff, NULL); +static CV_PossibleValue_t showlapemblem_cons_t[] = {{0, "Off"}, {1, "Emblem Only"}, {2, "Splits Only"}, {3, "All"}, {0, NULL}}; +consvar_t cv_showlapemblem = CVAR_INIT ("showlapemblem", "All", CV_SAVE, showlapemblem_cons_t, NULL); + +// Race splits rely on showlapemblem, so toggling them off isn't really necessary. +static CV_PossibleValue_t racesplits_cons_t[] = {{1, "Next"}, {2, "Leader"}, {0, NULL}}; +consvar_t cv_racesplits = CVAR_INIT ("racesplits", "Leader", CV_SAVE, racesplits_cons_t, NULL); consvar_t cv_showminimapnames = CVAR_INIT ("showminimapnames", "Off", CV_SAVE, CV_OnOff, NULL); static CV_PossibleValue_t minimapdot_cons_t[] = {{0, "Off"}, {1, "Dot"}, {2, "Headlight"}, {0, NULL}}; @@ -967,6 +972,7 @@ void D_RegisterServerCommands(void) CV_RegisterVar(&cv_showminimapangle); CV_RegisterVar(&cv_minihead); CV_RegisterVar(&cv_showlapemblem); + CV_RegisterVar(&cv_racesplits); CV_RegisterVar(&cv_showviewpointtext); CV_RegisterVar(&cv_schedule); diff --git a/src/d_netcmd.h b/src/d_netcmd.h index 448b60a00..de6cfdeaf 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -228,6 +228,7 @@ extern consvar_t cv_showminimapangle; extern consvar_t cv_minihead; extern consvar_t cv_showlapemblem; +extern consvar_t cv_racesplits; extern consvar_t cv_showviewpointtext; extern consvar_t cv_skipmapcheck; diff --git a/src/d_player.h b/src/d_player.h index 1f345a272..4c68c2c45 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -44,6 +44,9 @@ typedef enum // free up to and including 1<<31 } skinflags_t; +// Splits are per-lap in Blankart, but let's keep this as-is. +#define MAXRACESPLITS 32 + // // Player states. // @@ -268,6 +271,14 @@ typedef enum khud_cardanimation, // Used to determine the position of some full-screen Battle Mode graphics khud_yougotem, // "You Got Em" gfx when hitting someone as a karma player via a method that gets you back in the game instantly + // Splits + khud_splittime, // Delta between you and highest split + khud_splitwin, // How to color/flag the split based on gaining/losing | ahead/behind + khud_splittimer, // How long to show splits HUD + khud_splitskin, // Skin index of the leading player + khud_splitcolor, // Skincolor of the leading player + khud_splitposition, // Who are we comparing to? + NUMKARTHUD } karthudtype_t; @@ -480,6 +491,15 @@ struct saltyhop_t fixed_t momz, zoffset; // erm... the mechanism.... }; +// enum for saved lap times +typedef enum +{ + LAP_CUR, + LAP_BEST, + LAP_LAST, + LAP__MAX +} laptime_e; + // ======================================================================== // PLAYER STRUCTURE // ======================================================================== @@ -568,6 +588,7 @@ struct player_t boolean jitterlegacy; // If true, makes Ring Racers characters jitter during drifts like // SRB2Kart. + // Basic gameplay things UINT8 position; // Used for Kart positions, mostly for deterministic stuff UINT8 oldposition; // Used for taunting when you pass someone @@ -723,6 +744,9 @@ struct player_t UINT8 lastsafelap; UINT8 lastsafestarpost; + tic_t splits[MAXRACESPLITS]; // Times we crossed checkpoint + INT32 pace; // Last split delta, used for checking whether gaining or losing time + // SINT8 lives; @@ -745,6 +769,7 @@ struct player_t INT16 totalring; // Total number of rings obtained for GP tic_t realtime; // integer replacement for leveltime + tic_t laptime[LAP__MAX]; UINT8 laps; // Number of laps (optional) UINT8 latestlap; diff --git a/src/g_game.c b/src/g_game.c index 5b14d1317..26a21d50a 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -2402,6 +2402,8 @@ static inline void G_PlayerFinishLevel(INT32 player) void G_PlayerReborn(INT32 player, boolean betweenmaps) { player_t *p; + + UINT8 i; INT32 score, roundscore; INT32 lives; @@ -2480,6 +2482,8 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) UINT16 bigwaypointgap; boolean jitterlegacy = false; + tic_t laptime[LAP__MAX]; + score = players[player].score; lives = players[player].lives; ctfteam = players[player].ctfteam; @@ -2558,6 +2562,10 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) nextcheck = 0; xtralife = 0; + for (i = 0; i < LAP__MAX; i++) + { + laptime[i] = 0; + } } else { @@ -2614,6 +2622,11 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) lastsafelap = players[player].lastsafelap; lastsafestarpost = players[player].lastsafestarpost; bigwaypointgap = players[player].bigwaypointgap; + + for (i = 0; i < LAP__MAX; i++) + { + laptime[i] = players[player].laptime[i]; + } } if (!betweenmaps) @@ -2679,6 +2692,11 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) p->latestlap = latestlap; p->totalring = totalring; + for (i = 0; i < LAP__MAX; i++) + { + p->laptime[i] = laptime[i]; + } + p->bot = bot; p->botvars.style = style; p->botvars.difficulty = botdifficulty; @@ -2733,8 +2751,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) // Check to make sure their color didn't change somehow... if (G_GametypeHasTeams()) { - UINT8 i; - if (p->ctfteam == 1 && p->skincolor != skincolor_redteam) { for (i = 0; i <= splitscreen; i++) diff --git a/src/k_hud.c b/src/k_hud.c index b1d4ebc1f..8a295175f 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -4776,9 +4776,14 @@ static void K_drawChallengerScreen(void) V_DrawScaledPatch(0, 0, 0, kp_challenger[anim[offset]]); } +static boolean K_DisplayingLapEmblem(void) +{ + return ((cv_showlapemblem.value & 1) && (stplyr->karthud[khud_lapanimation])); +} + static void K_drawLapStartAnim(void) { - if (!cv_showlapemblem.value) + if (!K_DisplayingLapEmblem()) return; // This is an EVEN MORE insanely complicated animation. @@ -4884,6 +4889,109 @@ static void K_drawLapStartAnim(void) } } +// Redundant copy from k_kart. +static INT32 K_GetRaceSplitsToggle(void) +{ + if (cv_showlapemblem.value < 2) + { + // Splits aren't on. + return 0; + } + + // Not taking any chances with this stupid goddamn engine. + return max((INT32)(cv_racesplits.value), 1); +} + +static void K_drawLapSplitComparison(void) +{ + if (!K_GetRaceSplitsToggle()) + { + return; + } + + if (!r_splitscreen) + { + // Draw the timestamp + if (LUA_HudEnabled(hud_time)) + { + boolean debug_alwaysdrawsplits = false; + + if ( + ( + (!K_DisplayingLapEmblem()) && + stplyr->karthud[khud_splittimer] && + (stplyr->karthud[khud_splittimer] > TICRATE/3 || stplyr->karthud[khud_splittimer]%2) + ) + || debug_alwaysdrawsplits + ) + { + INT32 split = stplyr->karthud[khud_splittime]; + INT32 skin = stplyr->karthud[khud_splitskin]; + INT32 color = stplyr->karthud[khud_splitcolor]; + INT32 ahead = stplyr->karthud[khud_splitwin]; + INT32 pos = stplyr->karthud[khud_splitposition]; + + // debug + if (!stplyr->karthud[khud_splittimer]) + { + ahead = ((leveltime/17)%5) - 2; + split = leveltime; + skin = stplyr->skin; + color = stplyr->skincolor; + } + + split = abs(split); + + UINT8 *skincolor = R_GetTranslationColormap(skin, color, GTC_CACHE); + + UINT32 textcolor = 0; + switch (ahead) + { + case 2: + textcolor = V_BLUEMAP; // leading and gaining + break; + case 1: + textcolor = V_SKYMAP; // leading and losing + break; + case -1: + textcolor = V_ORANGEMAP; // trailing and gaining + break; + case -2: + textcolor = V_REDMAP; // trailing and losing + break; + } + + fixed_t row_position[2] = {BASEVIDWIDTH/2, BASEVIDHEIGHT/4}; + UINT32 splitflags = V_30TRANS; + + const char *arrow = (ahead == 1 || ahead == -2) ? "\x1B" : "\x1A"; + char buffer[256]; + snprintf(buffer, 256, "%s%02i'%02i\"%02i%s", + ahead >= 0 ? "-" : "+", + G_TicsToMinutes(split, true), + G_TicsToSeconds(split), + G_TicsToCentiseconds(split), + arrow + ); + + INT32 stwidth = V_StringWidth(buffer, splitflags) / 2; + + // vibes offset + V_DrawMappedPatch(row_position[0] - stwidth - 35, row_position[1], splitflags, faceprefix[skin][FACE_MINIMAP], skincolor); + + if (pos > 1) + { + V_DrawPingNum(row_position[0] - stwidth - 35, row_position[1], splitflags, pos, NULL); + } + + // vibes offset TWO + row_position[0] += 15; + V_DrawCenteredString(row_position[0], row_position[1], splitflags|textcolor, buffer); + } + } + } +} + void K_drawKartFreePlay(void) { // Doesn't support splitscreens higher than 2 for real estate reasons. @@ -5363,6 +5471,9 @@ void K_drawKartHUD(void) if (LUA_HudEnabled(hud_time)) K_drawKartTimestamp(stplyr->realtime, TIME_X, TIME_Y, gamemap, 0); + // Draw splits + K_drawLapSplitComparison(); + islonesome = K_drawKartPositionFaces(); } diff --git a/src/k_kart.c b/src/k_kart.c index d421da59d..885d773a8 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -2968,6 +2968,103 @@ void K_AwardPlayerRings(player_t *player, UINT16 rings, boolean overload) player->superring = superring; } +static INT32 K_GetRaceSplitsToggle(void) +{ + if (cv_showlapemblem.value < 2) + { + // Splits aren't on. + return 0; + } + + // Not taking any chances with this stupid goddamn engine. + return max((INT32)(cv_racesplits.value), 1); +} + +static void K_SetupSplitForPlayer(player_t *us, player_t *them, tic_t ourtime, tic_t theirtime) +{ + us->karthud[khud_splittimer] = 3*TICRATE; + + INT32 delta = (INT32)theirtime - (INT32)ourtime; // how ahead are we? bigger number = more ahead, negative = behind + us->karthud[khud_splittime] = -1 * delta; // (HUD expects this to be backwards, but this is how i felt today!) + + INT32 winning = 0; + + if (delta > 0) + winning = 2; // winning aid gaining + else if (delta < 0) + winning = -2; // behind and falling + + if (winning > 0 && delta < us->pace) + winning = 1; // winning but falling + else if (winning < 0 && delta > us->pace) + winning = -1; // behind but gaiming + + us->pace = delta; + + us->karthud[khud_splitwin] = winning; + us->karthud[khud_splitskin] = them->skin; + us->karthud[khud_splitcolor] = them->skincolor; + if (us->position != 1) + us->karthud[khud_splitposition] = them->position; + else + us->karthud[khud_splitposition] = 0; +} + +void K_HandleRaceSplits(player_t *player, tic_t time, UINT8 checkpoint) +{ + if (checkpoint >= MAXRACESPLITS) + return; + + const INT32 splitstoggle = K_GetRaceSplitsToggle(); + + player->splits[checkpoint] = time; + + player_t *lowest = player; + player_t *next = player; + UINT8 numrealsplits = 0; + + // find fastest player for this checkpoint and # players who have already crossed + for (UINT8 i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i]) + continue; + + player_t *check = &players[i]; + + if (check == player) + continue; + if (check->spectator) + continue; + if (check->splits[checkpoint] == 0) + continue; + + numrealsplits++; + + if (check->splits[checkpoint] < lowest->splits[checkpoint]) + lowest = check; + + if (check->splits[checkpoint] > next->splits[checkpoint] || next == player) + next = check; + } + + // no one to compare against yet + if (lowest == player || numrealsplits == 0) + return; + + // if there's exactly one player ahead of us, they need a blue split generated + // so they can see how far behind we are + if (lowest != player && numrealsplits == 1) + { + K_SetupSplitForPlayer(lowest, player, lowest->splits[checkpoint], player->splits[checkpoint]); + } + + if (numrealsplits && splitstoggle) + { + player_t *target = (splitstoggle == 2) ? lowest : next; + K_SetupSplitForPlayer(player, target, player->splits[checkpoint], target->splits[checkpoint]); + } +} + void K_DoInstashield(player_t *player) { mobj_t *layera; @@ -6025,10 +6122,22 @@ static void K_UpdateInvincibilitySounds(player_t *player) #undef STOPTHIS } +static boolean K_DisplayingLapEmblem(player_t *player) +{ + return ((cv_showlapemblem.value & 1) && (player->karthud[khud_lapanimation])) & 1; +} + void K_KartPlayerHUDUpdate(player_t *player) { if (player->karthud[khud_lapanimation]) + { player->karthud[khud_lapanimation]--; + } + + if (player->karthud[khud_splittimer] && (!K_DisplayingLapEmblem(player))) + { + player->karthud[khud_splittimer]--; + } if (player->karthud[khud_yougotem]) player->karthud[khud_yougotem]--; diff --git a/src/k_kart.h b/src/k_kart.h index 282a08a0c..b0f55468c 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -205,6 +205,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd); void K_KartPlayerAfterThink(player_t *player); angle_t K_MomentumAngle(mobj_t *mo); void K_AwardPlayerRings(player_t *player, UINT16 rings, boolean overload); +void K_HandleRaceSplits(player_t *player, tic_t time, UINT8 checkpoint); void K_DoInstashield(player_t *player); void K_BattleAwardHit(player_t *player, player_t *victim, mobj_t *inflictor, UINT8 bumpersRemoved); void K_SpinPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, INT32 type); diff --git a/src/p_saveg.c b/src/p_saveg.c index 7dfb12891..33d7ac1fb 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -162,6 +162,10 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEINT16(save->p, players[i].totalring); WRITEUINT32(save->p, players[i].realtime); + for (j = 0; j < LAP__MAX; j++) + { + WRITEUINT32(save->p, players[i].laptime[j]); + } WRITEUINT8(save->p, players[i].laps); WRITEUINT8(save->p, players[i].latestlap); @@ -517,6 +521,10 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].totalring = READINT16(save->p); // Total number of rings obtained for GP players[i].realtime = READUINT32(save->p); // integer replacement for leveltime + for (j = 0; j < LAP__MAX; j++) + { + players[i].laptime[j] = READUINT32(save->p); + } players[i].laps = READUINT8(save->p); // Number of laps (optional) players[i].latestlap = READUINT8(save->p); diff --git a/src/p_spec.c b/src/p_spec.c index 6bd2aaa71..5a1cf291d 100644 --- a/src/p_spec.c +++ b/src/p_spec.c @@ -1996,6 +1996,9 @@ static void K_HandleLapIncrement(player_t *player) player->starpostnum = 0; player->laps++; + // Calculate splits. + // Nice-to-have: Halfway-through-lap splits? + if (!K_UsingLegacyCheckpoints()) { // do nothing, let waypoint code handle respawn positions @@ -2042,6 +2045,10 @@ static void K_HandleLapIncrement(player_t *player) player->karthud[khud_laphand] = 0; // No hands in FREE PLAY player->karthud[khud_lapanimation] = 80; + + const UINT8 fakegradingpoint = ((UINT8)(player->laps - 1) & 7); + + K_HandleRaceSplits(player, leveltime - starttime, fakegradingpoint); } if (P_IsDisplayPlayer(player)) @@ -2080,14 +2087,19 @@ static void K_HandleLapIncrement(player_t *player) if (player->laps > 1) { // save best lap for record attack + if (player->laptime[LAP_CUR] < player->laptime[LAP_BEST] || player->laptime[LAP_BEST] == 0) + { + player->laptime[LAP_BEST] = player->laptime[LAP_CUR]; + } + + player->laptime[LAP_LAST] = player->laptime[LAP_CUR]; + player->laptime[LAP_CUR] = 0; + if (modeattacking && player == &players[consoleplayer]) { - if (curlap < bestlap || bestlap == 0) - { - bestlap = curlap; - } - - curlap = 0; + // No need for redundant bullshit, just feed the calculated values. + bestlap = player->laptime[LAP_BEST]; + curlap = player->laptime[LAP_CUR]; } // Update power levels for this lap. @@ -2136,7 +2148,11 @@ static void K_HandleLapDecrement(player_t *player) player->starpostnum = numstarposts; player->laps--; K_UpdateAllPlayerPositions(); + curlap = UINT32_MAX; + + // Redundant, but we're only using laptimes for the timestamps as-is. + player->laptime[LAP_CUR] = UINT32_MAX; } } } diff --git a/src/p_user.c b/src/p_user.c index 0451cfa8f..5e2921dd2 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -2557,11 +2557,14 @@ static void P_DeathThink(player_t *player) curlap = 0; else if (curlap != UINT32_MAX) curlap++; // This is too complicated to sync to realtime, just sorta hope for the best :V + + player->laptime[LAP_CUR] = curlap; } } else { player->realtime = 0; + player->laptime[LAP_CUR] = 0; if (player == &players[consoleplayer]) curlap = 0; }