Add race lap splits display from Ring Racers

Very neat feature
This commit is contained in:
Anonimus 2025-09-13 23:03:17 -04:00
parent ab5ded6ae9
commit c906ccc7a1
10 changed files with 306 additions and 10 deletions

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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++)

View file

@ -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();
}

View file

@ -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]--;

View file

@ -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);

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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;
}