Update the director to the saturn version

This commit is contained in:
NepDisk 2025-08-17 10:43:13 -04:00
parent ef07c79f86
commit 88ecd2f5d6
9 changed files with 288 additions and 44 deletions

View file

@ -87,8 +87,8 @@
#define ASSET_HASH_TEXTURES_KART 0xb4211b2f32b6a291
#define ASSET_HASH_CHARS_KART 0x1e68a3e01aa5c68b
#define ASSET_HASH_MAPS_KART 0x38558ed00da41ce9
#define ASSET_HASH_MAIN_PK3 0x2152f84c950bca6b
#define ASSET_HASH_MAPPATCH_PK3 0x42de80e07ca851be
#define ASSET_HASH_MAIN_PK3 0x0b1ca4fd1a704d9c
#define ASSET_HASH_MAPPATCH_PK3 0x19ebc8123bd51191
#define ASSET_HASH_BONUSCHARS_KART 0x60e6f13d822a7461
#ifdef USE_PATCH_FILE
#define ASSET_HASH_PATCH_PK3 0x0000000000000000

View file

@ -1448,6 +1448,7 @@ struct int_const_s const INT_CONST[] = {
{"GC_CUSTOM2",gc_custom2},
{"GC_CUSTOM3",gc_custom3},
{"GC_RESPAWN",gc_respawn},
{"GC_DIRECTOR",gc_director},
{"NUM_GAMECONTROLS",num_gamecontrols},
// screen.h constants

View file

@ -1784,6 +1784,12 @@ boolean G_Responder(event_t *ev)
COM_ImmedExecute(commandname);
}
}
if (G_ControlBoundToKey(i, gc_director, ev->data1, false))
{
K_ToggleDirector();
}
}
return true;

View file

@ -408,6 +408,7 @@ static const char *gamecontrolname[num_gamecontrols] =
"custom2",
"custom3",
"respawn",
"director",
};
#define NUMKEYNAMES (sizeof (keynames)/sizeof (keyname_t))

View file

@ -86,6 +86,7 @@ typedef enum
gc_custom2, // Lua scriptable
gc_custom3, // Lua scriptable
gc_respawn,
gc_director,
num_gamecontrols
} gamecontrols_e;

View file

@ -1,24 +1,125 @@
// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 2024 by AJ "Tyron" Martinez.
// Copyright (C) 2024 by James Robert Roman.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file k_director.c
/// \brief SRB2kart automatic spectator camera.
#include "k_kart.h"
#include "doomdef.h"
#include "doomstat.h"
#include "g_game.h"
#include "m_random.h"
#include "v_video.h"
#include "k_director.h"
#include "d_netcmd.h"
#include "p_local.h"
#include "st_stuff.h"
#include "r_fps.h"
#define SWITCHTIME TICRATE * 5 // cooldown between unforced switches
#define BOREDOMTIME 3 * TICRATE / 2 // how long until players considered far apart?
#define TRANSFERTIME TICRATE // how long to delay reaction shots?
#define BREAKAWAYDIST 4000 // how *far* until players considered far apart?
#define WALKBACKDIST 600 // how close should a trailing player be before we switch?
#define PINCHDIST 30000 // how close should the leader be to be considered "end of race"?
#define BREAKAWAYDIST 2000 // how *far* until players considered far apart?
#define WALKBACKDIST 400 // how close should a trailing player be before we switch?
#define PINCHDIST 20000 // how close should the leader be to be considered "end of race"?
struct directorinfo directorinfo;
struct directorinfo
{
player_t* viewplayer;
tic_t cooldown; // how long has it been since we last switched?
tic_t chaosleep;// how long did we watch the same player?
tic_t freeze; // when nonzero, fixed switch pending, freeze logic!
INT32 attacker; // who to switch to when freeze delay elapses
INT32 maxdist; // how far is the closest player from finishing?
INT32 sortedplayers[MAXPLAYERS]; // position-1 goes in, player index comes out.
INT32 gap[MAXPLAYERS]; // gap between a given position and their closest pursuer
INT32 boredom[MAXPLAYERS]; // how long has a given position had no credible attackers?
} directorinfo;
static INT32 K_PlayersPlaying(void)
{
INT32 num = 0, i;
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator || !players[i].mo)
continue;
num++;
}
return num;
}
static inline boolean race_rules(void)
{
return (gametyperules & GTR_CIRCUIT);
}
static fixed_t ScaleFromMap(fixed_t n, fixed_t scale)
{
return FixedMul(n, FixedDiv(scale, mapobjectscale));
}
boolean K_DirectorIsAvailable(void)
{
if (splitscreen || dedicated || (demo.playback && demo.title) || modeattacking)
return false;
return ((gamestate == GS_LEVEL) && ((demo.playback && !camera[0].freecam) || (players[consoleplayer].spectator && (K_PlayersPlaying() > 1))));
}
static boolean K_DirectorIsEnabled(void)
{
return (cv_director.value && K_DirectorIsAvailable());
}
static mobj_t *finishmo = NULL;
// scan for the waypoint on the finish line
// used to get a very approximate distance from player to finishline
// we probably should like, do some maths to find the middle point between multiplayer
// waypoints with the same values, but idk if its really worth it
static void K_SetupFinishMo(void)
{
INT16 maxMoveCount = -1;
INT16 maxAngle = -1;
finishmo = NULL;
if (!(mapheaderinfo[gamemap - 1]->levelflags & LF_SECTIONRACE)) // not a sprint map
{
// waypoint with angle 0 should always be at the finish line
for (finishmo = waypointcap; finishmo != NULL; finishmo = finishmo->tracer)
{
if (finishmo->spawnpoint->angle == 0)
break;
}
}
else // crappy optimization weeeee
{
// sprint maps finishline waypoint is the one with highest movecount AND angle
for (finishmo = waypointcap; finishmo != NULL; finishmo = finishmo->tracer)
{
if (finishmo->movecount > maxMoveCount)
maxMoveCount = finishmo->movecount;
if (finishmo->spawnpoint->angle > maxAngle)
maxAngle = finishmo->spawnpoint->angle;
}
// now actually get the one
for (finishmo = waypointcap; finishmo != NULL; finishmo = finishmo->tracer)
{
if (finishmo->movecount == maxMoveCount && finishmo->spawnpoint->angle == maxAngle)
break; // found it
}
}
}
void K_InitDirector(void)
{
@ -28,6 +129,8 @@ void K_InitDirector(void)
directorinfo.freeze = 0;
directorinfo.attacker = 0;
directorinfo.maxdist = 0;
directorinfo.viewplayer = NULL;
directorinfo.chaosleep = 0;
for (playernum = 0; playernum < MAXPLAYERS; playernum++)
{
@ -35,12 +138,25 @@ void K_InitDirector(void)
directorinfo.gap[playernum] = INT32_MAX;
directorinfo.boredom[playernum] = 0;
}
if (K_UsingLegacyCheckpoints())
K_SetupFinishMo();
}
static fixed_t K_GetLegacyDistanceToFinish(player_t player)
{
if (P_MobjWasRemoved(finishmo))
return 0;
return P_AproxDistance(P_AproxDistance(finishmo->x - player.mo->x,
finishmo->y - player.mo->y),
finishmo->z - player.mo->z) / FRACUNIT;
}
static fixed_t K_GetFinishGap(INT32 leader, INT32 follower)
{
fixed_t dista = players[follower].distancetofinish;
fixed_t distb = players[leader].distancetofinish;
fixed_t dista = K_UsingLegacyCheckpoints() ? K_GetLegacyDistanceToFinish(players[follower]) : (fixed_t)players[follower].distancetofinish;
fixed_t distb = K_UsingLegacyCheckpoints() ? K_GetLegacyDistanceToFinish(players[leader]) : (fixed_t)players[leader].distancetofinish;
if (players[follower].position < players[leader].position)
{
@ -58,10 +174,10 @@ static void K_UpdateDirectorPositions(void)
INT32 position;
player_t* target;
memset(directorinfo.sortedplayers, -1, sizeof(directorinfo.sortedplayers));
for (playernum = 0; playernum < MAXPLAYERS; playernum++)
{
directorinfo.sortedplayers[playernum] = -1;
target = &players[playernum];
if (playeringame[playernum] && !target->spectator && target->position > 0)
@ -79,11 +195,11 @@ static void K_UpdateDirectorPositions(void)
continue;
}
directorinfo.gap[position] = P_ScaleFromMap(K_GetFinishGap(directorinfo.sortedplayers[position], directorinfo.sortedplayers[position + 1]), FRACUNIT);
directorinfo.gap[position] = ScaleFromMap(K_GetFinishGap(directorinfo.sortedplayers[position], directorinfo.sortedplayers[position + 1]), FRACUNIT);
if (directorinfo.gap[position] >= BREAKAWAYDIST)
{
directorinfo.boredom[position] = min(BOREDOMTIME * 2, directorinfo.boredom[position] + 1);
directorinfo.boredom[position] = (INT32)(min(BOREDOMTIME * 2, directorinfo.boredom[position] + 1));
}
else if (directorinfo.boredom[position] > 0)
{
@ -91,12 +207,17 @@ static void K_UpdateDirectorPositions(void)
}
}
directorinfo.maxdist = P_ScaleFromMap(players[directorinfo.sortedplayers[0]].distancetofinish, FRACUNIT);
if (directorinfo.sortedplayers[0] == -1)
{
directorinfo.maxdist = -1;
return;
}
directorinfo.maxdist = ScaleFromMap(K_GetLegacyDistanceToFinish(players[directorinfo.sortedplayers[0]]), FRACUNIT);
}
static boolean K_CanSwitchDirector(void)
{
if (directorinfo.cooldown > 0)
{
return false;
@ -107,7 +228,7 @@ static boolean K_CanSwitchDirector(void)
static void K_DirectorSwitch(INT32 player, boolean force)
{
if (P_IsDisplayPlayer(&players[player]))
if (!K_DirectorIsEnabled())
{
return;
}
@ -124,6 +245,7 @@ static void K_DirectorSwitch(INT32 player, boolean force)
G_ResetView(1, player, true);
directorinfo.cooldown = SWITCHTIME;
directorinfo.chaosleep = 0;
}
static void K_DirectorForceSwitch(INT32 player, INT32 time)
@ -135,11 +257,36 @@ static void K_DirectorForceSwitch(INT32 player, INT32 time)
directorinfo.attacker = player;
directorinfo.freeze = time;
directorinfo.chaosleep = 0;
}
static void K_DirectorSwitchRandom(void)
{
INT32 randomplayer = -1;
// kinda dumb but just check if the random player is existing lmao
for (INT32 h = 0; h < MAXPLAYERS; h++)
{
randomplayer = directorinfo.sortedplayers[M_RandomRange(0, K_PlayersPlaying()-1)]; // switch to someone random
if (randomplayer != -1 && randomplayer != displayplayers[0]) // dont switch to ourselves Zzz...
{
break;
}
}
if (randomplayer != -1)
K_DirectorSwitch(randomplayer, true);
}
void K_DirectorFollowAttack(player_t *player, mobj_t *inflictor, mobj_t *source)
{
if (!P_IsDisplayPlayer(player))
if (!K_DirectorIsEnabled())
{
return;
}
if (directorinfo.viewplayer != player)
{
return;
}
@ -172,6 +319,7 @@ void K_DrawDirectorDebugger(void)
V_DrawThinString(120, 0, V_70TRANS, va("BORED"));
V_DrawThinString(150, 0, V_70TRANS, va("COOLDOWN: %d", directorinfo.cooldown));
V_DrawThinString(230, 0, V_70TRANS, va("MAXDIST: %d", directorinfo.maxdist));
V_DrawThinString(310, 0, V_70TRANS, va("SLEEPTIME: %d", directorinfo.chaosleep));
for (position = 0; position < MAXPLAYERS - 1; position++)
{
@ -208,17 +356,18 @@ void K_DrawDirectorDebugger(void)
void K_UpdateDirector(void)
{
INT32 *displayplayerp = &displayplayers[0];
INT32 targetposition;
directorinfo.viewplayer = &players[displayplayers[0]];
if (!cv_director.value)
if (!K_DirectorIsEnabled())
{
return;
}
K_UpdateDirectorPositions();
if (directorinfo.cooldown > 0) {
if (directorinfo.cooldown > 0)
{
directorinfo.cooldown--;
}
@ -231,6 +380,36 @@ void K_UpdateDirector(void)
return;
}
// if there's only one player left in the list, just switch to that player
if (directorinfo.sortedplayers[0] != -1 && (directorinfo.sortedplayers[1] == -1 ||
// TODO: Battle; I just threw this together quick. Focus on leader.
!race_rules()))
{
K_DirectorSwitch(directorinfo.sortedplayers[0], false);
return;
}
// insta switch if the player were watching finishes
if (players[displayplayers[0]].exiting)
{
K_DirectorSwitchRandom();
return;
}
// begin counting when cooldown wore off
if (!directorinfo.cooldown)
{
directorinfo.chaosleep++;
}
// force switch 10 seconds after the cooldown has ended
// otherwise i fall asleeb zzz...
if (directorinfo.chaosleep > TICRATE*10)
{
K_DirectorSwitchRandom();
return;
}
// aaight, time to walk through the standings to find the first interesting pair
// NB: targetposition/sortedplayers is 0-indexed, aiming at the "back half" of a given pair by default.
// we adjust for this when comparing to player->position or when looking at the leading player, Don't Freak Out
@ -271,25 +450,19 @@ void K_UpdateDirector(void)
target = directorinfo.sortedplayers[targetposition];
// stop here since we're already viewing this player
if (*displayplayerp == target)
if (displayplayers[0] == target)
{
break;
}
// if this is a splitscreen player, try next pair
if (P_IsDisplayPlayer(&players[target]))
{
continue;
}
// if we're certain the back half of the pair is actually in this position, try to switch
if (!players[target].positiondelay)
{
K_DirectorSwitch(target, false);
}
// even if we're not certain, if we're certain we're watching the WRONG player, try to switch
if (players[*displayplayerp].position != targetposition+1 && !players[target].positiondelay)
// even if we're not certain, if we're cetain we're watching the WRONG player, try to switch
if (directorinfo.viewplayer->position != targetposition+1 && !directorinfo.viewplayer->positiondelay)
{
K_DirectorSwitch(target, false);
}
@ -297,3 +470,19 @@ void K_UpdateDirector(void)
break;
}
}
void K_ToggleDirector(void)
{
if (!K_DirectorIsAvailable())
return;
if (!K_DirectorIsEnabled())
{
G_AdjustView(1, 1, true);
directorinfo.cooldown = 0; // switch immediately
}
directortoggletimer = 0;
CV_SetValue(&cv_director, (cv_director.value ^ 1));
}

View file

@ -1,5 +1,12 @@
// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 2024 by AJ "Tyron" Martinez.
// Copyright (C) 2024 by James Robert Roman.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file k_director.h
/// \brief SRB2kart automatic spectator camera.
@ -10,22 +17,12 @@
extern "C" {
#endif
extern struct directorinfo
{
tic_t cooldown; // how long has it been since we last switched?
tic_t freeze; // when nonzero, fixed switch pending, freeze logic!
INT32 attacker; // who to switch to when freeze delay elapses
INT32 maxdist; // how far is the closest player from finishing?
INT32 sortedplayers[MAXPLAYERS]; // position-1 goes in, player index comes out.
INT32 gap[MAXPLAYERS]; // gap between a given position and their closest pursuer
INT32 boredom[MAXPLAYERS]; // how long has a given position had no credible attackers?
} directorinfo;
boolean K_DirectorIsAvailable(void);
void K_InitDirector(void);
void K_UpdateDirector(void);
void K_DrawDirectorDebugger(void);
void K_DirectorFollowAttack(player_t *player, mobj_t *inflictor, mobj_t *source);
void K_ToggleDirector(void);
#ifdef __cplusplus
} // extern "C"

View file

@ -33,8 +33,10 @@
#include "m_anigif.h" // cv_gif_downscale
#include "p_setup.h" // NiGHTS grading
#include "k_grandprix.h" // we need to know grandprix status for titlecards
#include "k_director.h"
#include "k_boss.h"
#include "r_fps.h"
#include "g_input.h"
//random index
#include "m_random.h"
@ -64,6 +66,9 @@ consvar_t cv_stagetitle = CVAR_INIT ("maptitle", "On", CV_SAVE, CV_OnOff, NULL);
UINT16 objectsdrawn = 0;
// dumb fade thing for director toggle
tic_t directortoggletimer = 0;
//
// STATUS BAR DATA
//
@ -725,6 +730,25 @@ void ST_preLevelTitleCardDrawer(void)
I_UpdateNoBlit();
}
// returns the actual button name for any gamecontrol
// if unbound is set, it returns "Unbound" as a string should there be no button bound to a gamecontrol
// if gamectrl is set, it adds an extra "-" for cases where theres also a hardcoded button like accelerate
static const char *ST_GetButtonName(INT32 control, const char *inputtext, boolean unbound, boolean gamectrl)
{
static char buttname[32] = "";
const char *butt1 = (gamecontrol[0][control][0] != 0 ? G_KeynumToString(gamecontrol[0][control][0]) : NULL);
const char *butt2 = (gamecontrol[0][control][1] != 0 ? G_KeynumToString(gamecontrol[0][control][1]) : NULL); // alternative bind
if (butt1 == NULL && butt2 == NULL) // not bound to a button
snprintf(buttname, 32, (unbound ? "%s - %s" : (gamectrl ? "-%s - %s" : "%s - %s")), (unbound ? "Unbound" : ""), inputtext);
else if (butt1 != NULL && butt2 != NULL) // bound to two buttons
snprintf(buttname, 32, "%s/%s - %s", butt1, butt2, inputtext);
else // bound to only one
snprintf(buttname, 32, (gamectrl ? "-%s - %s" : "%s - %s"), (butt1 != NULL ? butt1 : butt2 != NULL ? butt2 : ""), inputtext);
return buttname;
}
//
// Draw the status bar overlay, customisable: the user chooses which
// kind of information to overlay
@ -748,6 +772,29 @@ static void ST_overlayDrawer(void)
if (!hu_showscores) // hide the following if TAB is held
{
// TODO: splitscreen support!
if (!splitscreen && !P_IsLocalPlayer(stplyr) && K_DirectorIsAvailable())
{
char directortext[20] = {0};
snprintf(directortext, 20, "Director: %s", cv_director.value ? "On" : "Off");
if ((!demo.playback && directortoggletimer < 13*TICRATE) || (demo.playback && directortoggletimer < 4*TICRATE))
{
if (renderisnewtic)
directortoggletimer++;
if (directortoggletimer < 4*TICRATE)
V_DrawString(1, BASEVIDHEIGHT-8-1, V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_HUDTRANSHALF|V_ALLOWLOWERCASE, directortext);
else if (cv_translucenthud.value != 0)
V_DrawString(1, BASEVIDHEIGHT-8-1, V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_80TRANS|V_ALLOWLOWERCASE, directortext); // idk if V_80TRANS is good?
}
}
else
{
directortoggletimer = 0;
}
if (cv_showviewpointtext.value)
{
if (!demo.title && !P_IsLocalPlayer(stplyr) && !camera[viewnum].freecam)
@ -806,10 +853,11 @@ static void ST_overlayDrawer(void)
}
else
{
V_DrawString(2, BASEVIDHEIGHT-40, V_HUDTRANSHALF|V_SPLITSCREEN|V_YELLOWMAP|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("- SPECTATING -"));
V_DrawString(2, BASEVIDHEIGHT-30, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, itemtxt);
V_DrawString(2, BASEVIDHEIGHT-20, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Accelerate - Float"));
V_DrawString(2, BASEVIDHEIGHT-10, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Brake - Sink"));
V_DrawString(2, BASEVIDHEIGHT-50, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_HUDTRANSHALF|V_YELLOWMAP, M_GetText("- SPECTATING -"));
V_DrawString(2, BASEVIDHEIGHT-40, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_HUDTRANSHALF, itemtxt);
V_DrawString(2, BASEVIDHEIGHT-30, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_HUDTRANSHALF, va("Accelerate %s", ST_GetButtonName(gc_accelerate, "Float", false, true)));
V_DrawString(2, BASEVIDHEIGHT-20, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_HUDTRANSHALF, va("Brake %s", ST_GetButtonName(gc_brake, "Sink", false, true)));
V_DrawString(2, BASEVIDHEIGHT-10, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_HUDTRANSHALF, ST_GetButtonName(gc_director, "Toggle Director", true, false));
}
}
}

View file

@ -90,6 +90,7 @@ extern lumpnum_t st_borderpatchnum;
extern patch_t *faceprefix[MAXSKINS][NUMFACES];
extern UINT16 objectsdrawn;
extern tic_t directortoggletimer;
// variable to stop mayonaka static from flickering
extern consvar_t cv_lessflicker;