Added ready system to screen jobs for multiplayer

Readds the feature to allow players to ready up during stat screens and intermissions instead of autoskipping based on whoever closed it. Comes with a variety of ways to tweak this behavior such as percentage-based auto starting (with a timer), the ability to unready as needed, and who can control it. Players will still be able to skip through individual screen jobs within the runner while waiting to ready up.
This commit is contained in:
Boondorl 2025-05-30 23:18:04 -04:00 committed by Ricardo Luís Vaz Silva
commit fae43b8120
16 changed files with 493 additions and 39 deletions

View file

@ -123,12 +123,12 @@ void CallCreateFunction(const char* qname, DObject* runner)
//
//=============================================================================
DObject* CreateRunner(bool clearbefore)
DObject* CreateRunner(bool clearbefore, int skipType)
{
auto obj = cutscene.runnerclass->CreateNew();
auto func = LookupFunction("ScreenJobRunner.Init", false);
VMValue val[3] = { obj, clearbefore, false };
VMCall(func, val, 3, nullptr, 0);
VMValue val[4] = { obj, clearbefore, false, skipType };
VMCall(func, val, 4, nullptr, 0);
return obj;
}

View file

@ -19,6 +19,13 @@ enum
SJ_BLOCKUI = 1,
};
enum
{
ST_VOTE,
ST_MUST_BE_SKIPPABLE,
ST_UNSKIPPABLE,
};
struct CutsceneDef
{
FString video;
@ -47,7 +54,7 @@ bool CanWipe();
VMFunction* LookupFunction(const char* qname, bool validate = true);
void CallCreateFunction(const char* qname, DObject* runner);
DObject* CreateRunner(bool clearbefore = true);
DObject* CreateRunner(bool clearbefore = true, int skipType = ST_VOTE);
void AddGenericVideo(DObject* runner, const FString& fn, int soundid, int fps);
struct CutsceneState

View file

@ -96,6 +96,13 @@ enum ELevelStartStatus
LST_WAITING,
};
enum EReadyType
{
RT_VOTE,
RT_ANYONE,
RT_HOST_ONLY,
};
// NETWORKING
//
// gametic is the tic about to (or currently being) run.
@ -120,6 +127,9 @@ static uint8_t CurrentLobbyID = 0u; // Ignore commands not from this lobby (usef
static int LastGameUpdate = 0; // Track the last time the game actually ran the world.
static uint64_t MutedClients = 0u; // Ignore messages from these clients.
static int CutsceneCountdown = 0; // If enough people are ready, count down the timer. This won't reset between unreadies, only on intermission entrance.
static uint64_t CutsceneReady = 0u; // If in a cutscene, check if we're ready to move to move past it.
static int LevelStartDebug = 0;
static int LevelStartDelay = 0; // While this is > 0, don't start generating packets yet.
static ELevelStartStatus LevelStartStatus = LST_READY; // Listen for when to actually start making tics.
@ -151,6 +161,21 @@ CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently de
CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE)
CVAR(Bool, net_disablepause, false, CVAR_SERVERINFO | CVAR_NOSAVE)
CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE)
CUSTOM_CVAR(Int, net_cutscenereadytype, RT_VOTE, CVAR_SERVERINFO | CVAR_NOSAVE)
{
if (self < RT_VOTE)
self = RT_VOTE;
else if (self > RT_HOST_ONLY)
self = RT_HOST_ONLY;
}
CUSTOM_CVAR(Float, net_cutscenereadypercent, 0.5f, CVAR_SERVERINFO | CVAR_NOSAVE)
{
if (self < 0.0f)
self = 0.0f;
else if (self > 1.0f)
self = 1.0f;
}
CVAR(Float, net_cutscenecountdown, 30.0f, CVAR_SERVERINFO | CVAR_NOSAVE)
CVAR(Bool, cl_noboldchat, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
CVAR(Bool, cl_nochatsound, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
@ -363,6 +388,9 @@ void Net_ClearBuffers()
gametic = ClientTic = 0;
SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0;
NetEvents.ResetStream();
CutsceneReady = 0u;
CutsceneCountdown = 0;
bCommandsReset = false;
LevelStartAck = 0u;
@ -376,6 +404,91 @@ void Net_ClearBuffers()
NetworkClients += 0;
}
bool Net_IsPlayerReady(int player)
{
if (demoplayback || net_cutscenereadytype != RT_VOTE)
return false;
if (cutscene.runner)
{
int type = ST_VOTE;
IFVM(ScreenJobRunner, GetSkipType)
type = VMCallSingle<int>(func, cutscene.runner);
if (type == ST_UNSKIPPABLE)
return false;
}
return players[player].Bot != nullptr || (CutsceneReady & ((uint64_t)1u << player));
}
// Check if every client is ready to move on from the current cutscene.
void Net_PlayerReadiedUp(int player)
{
if (!netgame || demoplayback)
return;
// Allow unreadying in case a player needs to leave momentarily.
if (Net_IsPlayerReady(player))
CutsceneReady &= ~((uint64_t)1u << player);
else
CutsceneReady |= (uint64_t)1u << player;
}
void Net_StartCutscene()
{
CutsceneCountdown = netgame && !demoplayback && net_cutscenecountdown > 0.0f ? static_cast<int>(ceil(net_cutscenecountdown * TICRATE)) : 0;
}
// Allow the game to automatically start after a set amount of time.
bool Net_CheckCutsceneReady()
{
if (!cutscene.runner)
return false;
int type = ST_VOTE;
IFVM(ScreenJobRunner, GetSkipType)
type = VMCallSingle<int>(func, cutscene.runner);
if (type == ST_UNSKIPPABLE)
return false;
if (net_cutscenereadytype == RT_ANYONE)
return CutsceneReady != 0;
if (net_cutscenereadytype == RT_HOST_ONLY)
return (CutsceneReady & ((uint64_t)1u << Net_Arbitrator));
uint64_t mask = 0u;
int totalReady = 0;
// Bots will be automatically assumed to be ready, so we don't include them.
for (auto client : NetworkClients)
{
mask |= (uint64_t)1u << client;
totalReady += Net_IsPlayerReady(client);
}
if ((CutsceneReady & mask) == mask)
return true;
if ((float)totalReady / NetworkClients.Size() < net_cutscenereadypercent)
return false;
if (CutsceneCountdown <= 0)
return true;
--CutsceneCountdown;
return false;
}
void Net_AdvanceCutscene()
{
CutsceneReady = 0u;
CutsceneCountdown = 0;
if (consoleplayer == Net_Arbitrator)
Net_WriteInt8(DEM_ENDSCREENJOB);
}
void Net_ResetCommands(bool midTic)
{
bCommandsReset = midTic;
@ -554,7 +667,10 @@ static void ClientConnecting(int client)
static void DisconnectClient(int clientNum)
{
NetworkClients -= clientNum;
MutedClients &= ~((uint64_t)1u << clientNum);
const uint64_t mask = ~((uint64_t)1u << clientNum);
MutedClients &= mask;
CutsceneReady &= mask;
LevelStartAck &= mask;
I_ClearClient(clientNum);
// Capture the pawn leaving in the next world tick.
players[clientNum].playerstate = PST_GONE;
@ -1965,7 +2081,8 @@ void TryRunTics()
// Listen for other clients and send out data as needed. This is also
// needed for singleplayer! But is instead handled entirely through local
// buffers. This has a limit of 17 tics that can be generated.
// buffers. This has a limit of one seconds worth of commands that can be
// generated in advanced from the last time the game updated.
NetUpdate(totalTics);
LastEnterTic = EnterTic;
@ -2746,6 +2863,10 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player)
EndScreenJob();
break;
case DEM_READIED:
Net_PlayerReadiedUp(player);
break;
case DEM_ZSC_CMD:
{
FName cmd = ReadStringConst(stream);
@ -2987,6 +3108,55 @@ int Net_GetLatency(int* localDelay, int* arbitratorDelay)
//
//==========================================================================
// Intermission lobby info
static int IsPlayerReady(int player)
{
return Net_IsPlayerReady(player);
}
DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, IsPlayerReady, IsPlayerReady)
{
PARAM_PROLOGUE;
PARAM_INT(player);
ACTION_RETURN_BOOL(IsPlayerReady(player));
}
static void ReadyPlayer()
{
if (netgame && !demoplayback)
Net_WriteInt8(DEM_READIED);
}
DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, ReadyPlayer, ReadyPlayer)
{
PARAM_PROLOGUE;
ReadyPlayer();
return 0;
}
static void ResetReadyTimer()
{
Net_StartCutscene();
}
DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, ResetReadyTimer, ResetReadyTimer)
{
PARAM_PROLOGUE;
ResetReadyTimer();
return 0;
}
static int GetReadyTimer()
{
return CutsceneCountdown;
}
DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, GetReadyTimer, GetReadyTimer)
{
PARAM_PROLOGUE;
ACTION_RETURN_INT(GetReadyTimer());
}
// [RH] List "ping" times
CCMD(pings)
{

View file

@ -151,6 +151,8 @@ void Net_WriteBytes(const uint8_t *, int len);
void Net_DoCommand(int cmd, uint8_t **stream, int player);
void Net_SkipCommand(int cmd, uint8_t **stream);
bool Net_CheckCutsceneReady();
void Net_AdvanceCutscene();
void Net_ResetCommands(bool midTic);
void Net_SetWaiting();
void Net_ClearBuffers();

View file

@ -163,10 +163,11 @@ enum EDemoCommand
DEM_NETEVENT, // 70 String: Event name, Byte: Arg count; each arg is a 4-byte int
DEM_MDK, // 71 String: Damage type
DEM_SETINV, // 72 SetInventory
DEM_ENDSCREENJOB,
DEM_ENDSCREENJOB, // 73
DEM_ZSC_CMD, // 74 String: Command, Word: Byte size of command
DEM_CHANGESKILL, // 75 Int: Skill
DEM_KICK, // 76 Byte: Player number
DEM_READIED, // 77
};
// The following are implemented by cht_DoCheat in m_cheat.cpp

View file

@ -1104,6 +1104,37 @@ static void G_FullConsole()
}
void D_RunCutscene()
{
// Only single player games can cancel out of the screen job via client-side logic.
if (ScreenJobTick() && !demoplayback)
{
if (netgame)
{
// Only the host can determine this.
if (consoleplayer != Net_Arbitrator)
return;
int type = ST_VOTE;
IFVM(ScreenJobRunner, GetSkipType)
type = VMCallSingle<int>(func, cutscene.runner);
if (type != ST_UNSKIPPABLE)
return;
}
Net_WriteInt8(DEM_ENDSCREENJOB);
}
}
// This is used to allow the server to check for when players are ready to advance. For singleplayer we can just
// use the net message from the cutscene finishing to know when to go.
static void D_CheckCutsceneAdvance()
{
if (netgame && !demoplayback && Net_CheckCutsceneReady())
Net_AdvanceCutscene();
}
//
// G_Ticker
// Make ticcmd_ts for the players.
@ -1205,7 +1236,7 @@ void G_Ticker ()
C_AdjustBottom ();
}
// get commands, check consistancy, and build new consistancy check
// get commands
const int curTic = gametic / TicDup;
//Added by MC: For some of that bot stuff. The main bot function.
@ -1221,9 +1252,6 @@ void G_Ticker ()
G_WriteDemoTiccmd(nextCmd, client, curTic);
players[client].oldbuttons = cmd->buttons;
// If the user alt-tabbed away, paused gets set to -1. In this case,
// we do not want to read more demo commands until paused is no
// longer negative.
if (demoplayback)
G_ReadDemoTiccmd(cmd, client);
else
@ -1261,11 +1289,7 @@ void G_Ticker ()
case GS_CUTSCENE:
case GS_INTRO:
if (ScreenJobTick())
{
// synchronize termination with the playsim.
Net_WriteInt8(DEM_ENDSCREENJOB);
}
D_CheckCutsceneAdvance();
break;
default:
@ -3044,7 +3068,7 @@ void G_StartSlideshow(FLevelLocals *Level, FName whichone, int state)
{
auto SelectedSlideshow = whichone == NAME_None ? Level->info->slideshow : whichone;
auto slide = F_StartIntermission(SelectedSlideshow, state);
RunIntermission(nullptr, nullptr, slide, nullptr, [](bool)
RunIntermission(nullptr, nullptr, slide, nullptr, false, [](bool)
{
primaryLevel->SetMusic();
gamestate = GS_LEVEL;

View file

@ -116,7 +116,7 @@ FBaseCVar* G_GetUserCVar(int playernum, const char* cvarname);
class DIntermissionController;
struct level_info_t;
void RunIntermission(level_info_t* oldlevel, level_info_t* newlevel, DIntermissionController* intermissionScreen, DObject* statusScreen, std::function<void(bool)> completionf);
void RunIntermission(level_info_t* oldlevel, level_info_t* newlevel, DIntermissionController* intermissionScreen, DObject* statusScreen, bool ending, std::function<void(bool)> completionf);
extern const AActor *SendItemUse, *SendItemDrop;
extern int SendItemDropAmount;

View file

@ -1048,9 +1048,10 @@ DIntermissionController* FLevelLocals::CreateIntermission()
//
//=============================================================================
void RunIntermission(level_info_t* fromMap, level_info_t* toMap, DIntermissionController* intermissionScreen, DObject* statusScreen, std::function<void(bool)> completionf)
void RunIntermission(level_info_t* fromMap, level_info_t* toMap, DIntermissionController* intermissionScreen, DObject* statusScreen, bool ending, std::function<void(bool)> completionf)
{
cutscene.runner = CreateRunner(false);
// Make sure the finale can't be skipped, otherwise the intermission always needs to be skippable.
cutscene.runner = CreateRunner(false, ending ? ST_UNSKIPPABLE : ST_MUST_BE_SKIPPABLE);
GC::WriteBarrier(cutscene.runner);
cutscene.completion = std::move(completionf);
@ -1139,7 +1140,7 @@ void G_DoCompleted (void)
bool endgame = strncmp(nextlevel.GetChars(), "enDSeQ", 6) == 0;
intermissionScreen = primaryLevel->CreateIntermission();
auto nextinfo = !playinter || endgame? nullptr : FindLevelInfo(nextlevel.GetChars(), false);
RunIntermission(primaryLevel->info, nextinfo, intermissionScreen, statusScreen, [=](bool)
RunIntermission(primaryLevel->info, nextinfo, intermissionScreen, statusScreen, endgame, [=](bool)
{
if (!endgame) primaryLevel->WorldDone();
else D_StartTitle();

View file

@ -991,6 +991,6 @@ CCMD(testfinale)
}
auto controller = F_StartFinale(gameinfo.finaleMusic.GetChars(), gameinfo.finaleOrder, -1, 0, gameinfo.FinaleFlat.GetChars(), text, false, false, true, true);
RunIntermission(nullptr, nullptr, controller, nullptr, [=](bool) { gameaction = ga_nothing; });
RunIntermission(nullptr, nullptr, controller, nullptr, false, [=](bool) { gameaction = ga_nothing; });
}

View file

@ -46,6 +46,7 @@ extern uint8_t globalfreeze, globalchangefreeze;
void C_Ticker();
void M_Ticker();
void D_RunCutscene();
//==========================================================================
//
@ -92,6 +93,10 @@ void P_RunClientsideLogic()
if (gamestate == GS_LEVEL)
primaryLevel->automap->Ticker();
}
else if (gamestate == GS_CUTSCENE || gamestate == GS_INTRO)
{
D_RunCutscene();
}
// [MK] Additional ticker for UI events right after all others
primaryLevel->localEventManager->PostUiTick();

View file

@ -2441,10 +2441,20 @@ OptionMenu NetworkOptions protected
StaticText "$NETMNU_HOSTOPTIONS", 1
Option "$NETMNU_EXTRATICS", "net_extratic", "OnOff"
Option "$NETMNU_DISABLEPAUSE", "net_disablepause", "OnOff"
Option "$NETMNU_READYTYPE", "net_cutscenereadytype", "ReadyTypes"
Slider "$NETMNU_READYPERCENT", "net_cutscenereadypercent", "0", "1", "0.05", 2
NumberField "$NETMNU_READYCOUNTDOWN", "net_cutscenecountdown", 0, 300, 5
NumberField "$NETMNU_CHATSLOMO", "net_chatslowmode", 0, 300, 5
}
OptionValue "ReadyTypes"
{
0, "$OPTVAL_VOTE"
1, "$OPTVAL_ANYONE"
2, "$OPTVAL_HOSTONLY"
}
OptionValue "ChatTypes"
{
0, "$OPTVAL_DISABLECHAT"

View file

@ -321,6 +321,22 @@ class ScreenJobRunner : Object UI
State_Run,
State_Fadeout,
}
enum ESkipType
{
ST_VOTE,
ST_MUST_BE_SKIPPABLE,
ST_UNSKIPPABLE,
}
enum EInputType
{
INP_KEYBOARD_MOUSE,
INP_CONTROLLER,
INP_JOYSTICK,
}
private ESkipType skipType;
private EInputType lastInput;
Array<ScreenJob> jobs;
//CompletionFunc completion;
int index;
@ -335,13 +351,49 @@ class ScreenJobRunner : Object UI
native static void setTransition(int type);
void Init(bool clearbefore_, bool skipall_)
ESkipType GetSkipType() const
{
return skipType;
}
EInputType GetLastInputType() const
{
return lastInput;
}
protected void SetLastInputType(InputEvent ev)
{
if (ev.Type == InputEvent.Type_Mouse)
{
lastInput = INP_KEYBOARD_MOUSE;
}
else if (ev.Type == InputEvent.Type_KeyDown)
{
if (ev.KeyScan >= InputEvent.Key_Pad_LThumb_Right && ev.KeyScan <= InputEvent.Key_Pad_Y)
{
lastInput = INP_CONTROLLER;
}
else if ((ev.KeyScan >= InputEvent.Key_Joy1 && ev.KeyScan <= InputEvent.Key_JoyPOV4_Up)
|| (ev.KeyScan >= InputEvent.Key_JoyAxis1Plus && ev.KeyScan <= InputEvent.Key_JoyAxis8Minus))
{
lastInput = INP_JOYSTICK;
}
else
{
lastInput = INP_KEYBOARD_MOUSE;
}
}
}
void Init(bool clearbefore_, bool skipall_, ESkipType type = ST_VOTE)
{
clearbefore = clearbefore_;
skipall = skipall_;
index = -1;
fadeticks = 0;
last_paused_tic = -1;
skipType = type;
ResetReadyTimer();
}
override void OnDestroy()
@ -418,6 +470,7 @@ class ScreenJobRunner : Object UI
{
if (jobs.Size() == 0)
{
DrawReadiedPlayers(smoothratio);
return 1;
}
int x = index >= jobs.Size()? jobs.Size()-1 : index;
@ -433,6 +486,7 @@ class ScreenJobRunner : Object UI
}
int state = job.DrawFrame(smoothratio);
Screen.SetScreenFade(1.);
DrawReadiedPlayers(smoothratio);
return state;
}
@ -462,6 +516,9 @@ class ScreenJobRunner : Object UI
virtual bool OnEvent(InputEvent ev)
{
SetLastInputType(ev);
if (ConsumedInput(ev)) return true;
if (paused || index < 0 || index >= jobs.Size()) return false;
if (jobs[index].jobstate != ScreenJob.running) return false;
return jobs[index].OnEvent(ev);

View file

@ -1,4 +1,86 @@
extend class ScreenJobRunner
{
protected native static void ReadyPlayer();
protected native static void ResetReadyTimer();
native static int GetReadyTimer();
native static bool IsPlayerReady(int pNum);
bool ConsumedInput(InputEvent evt)
{
if (netgame && evt.type == InputEvent.Type_KeyDown
&& (evt.KeyScan == InputEvent.Key_Space || evt.KeyScan == InputEvent.Key_Mouse1
|| evt.KeyScan == InputEvent.Key_Pad_A || evt.KeyScan == InputEvent.Key_Joy1))
{
ReadyPlayer();
return true;
}
return false;
}
void DrawReadiedPlayers(double smoothratio)
{
if (!netgame || GetSkipType() == ST_UNSKIPPABLE)
return;
if (net_cutscenereadytype == 0)
{
int totalClients, readyClients;
for (int i; i < MAXPLAYERS; ++i)
{
if (!playerInGame[i] || players[i].Bot)
continue;
++totalClients;
readyClients += IsPlayerReady(i);
}
if (totalClients > 1)
{
TextureID readyico = TexMan.CheckForTexture("READYICO", TexMan.Type_MiscPatch);
Vector2 readysize = TexMan.GetScaledSize(readyico);
if (IsPlayerReady(consoleplayer))
Screen.DrawTexture(readyico, true, 0, 0, DTA_CleanNoMove, true, DTA_TopLeft, true);
Screen.DrawText(ConFont, Font.CR_UNTRANSLATED, (int(readysize.X) + 4) * CleanXFac, CleanYFac, String.Format("%d/%d", readyClients, totalClients), DTA_CleanNoMove, true);
int startTimer = GetReadyTimer();
if (startTimer > 0)
{
int col = Font.CR_UNTRANSLATED;
if (startTimer <= GameTicRate * 5)
col = Font.CR_RED;
Screen.DrawText(ConFont, col, 0, int(readysize.Y) * CleanYFac + CleanYFac, SystemTime.Format("%M:%S", int(ceil(double(startTimer) / GameTicRate))), DTA_CleanNoMove, true);
}
}
}
if (net_cutscenereadytype != 2 || consoleplayer == Net_Arbitrator)
{
string contType;
switch (GetLastInputType())
{
case INP_KEYBOARD_MOUSE:
contType = "$NET_CONTINUE_MKB";
break;
case INP_CONTROLLER:
contType = "$NET_CONTINUE_CONTROLLER";
break;
case INP_JOYSTICK:
contType = "$NET_CONTINUE_JOYSTICK";
break;
}
string contTxt = StringTable.Localize(contType);
int xOfs = (Screen.GetWidth() - ConFont.StringWidth(contTxt) * CleanXFac) / 2;
int yOfs = Screen.GetHeight() - ConFont.GetHeight() * CleanYFac - CleanYFac;
Screen.DrawText(ConFont, Font.CR_GREEN, xOfs, yOfs, contTxt, DTA_CleanNoMove, true);
}
}
}
class IntermissionController native ui
{

View file

@ -80,7 +80,7 @@ class StatusScreen : ScreenJob abstract version("2.5")
InterBackground bg;
int acceleratestage; // used to accelerate or skip a stage
bool playerready[MAXPLAYERS];
bool playerready[MAXPLAYERS]; // This is no longer used since the server needs to track this
int me; // wbs.pnum
int bcnt;
int CurState; // specifies current CurState

View file

@ -27,7 +27,6 @@ class CoopStatusScreen : StatusScreen
for (int i = 0; i < MAXPLAYERS; i++)
{
playerready[i] = false;
cnt_kills[i] = cnt_items[i] = cnt_secret[i] = cnt_frags[i] = 0;
if (!playeringame[i])
@ -216,7 +215,6 @@ class CoopStatusScreen : StatusScreen
}
else if (ng_state == 12)
{
// All players are ready; proceed.
if ((acceleratestage) || autoskip)
{
PlaySound("intermission/pastcoopstats");
@ -239,9 +237,9 @@ class CoopStatusScreen : StatusScreen
//
//====================================================================
override void drawStats ()
protected void DrawScoreboard(int y)
{
int i, x, y, ypadding, height, lineheight;
int i, x, ypadding, height, lineheight;
int maxnamewidth, maxscorewidth, maxiconheight;
int pwidth = IntermissionFont.GetCharWidth("%");
int icon_x, name_x, kills_x, bonus_x, secret_x;
@ -252,8 +250,6 @@ class CoopStatusScreen : StatusScreen
String text_bonus, text_secret, text_kills;
TextureID readyico = TexMan.CheckForTexture("READYICO", TexMan.Type_MiscPatch);
y = drawLF();
[maxnamewidth, maxscorewidth, maxiconheight] = GetPlayerWidths();
// Use the readyico height if it's bigger.
Vector2 readysize = TexMan.GetScaledSize(readyico);
@ -282,7 +278,6 @@ class CoopStatusScreen : StatusScreen
bonus_x += x;
secret_x += x;
drawTextScaled(displayFont, name_x, y, Stringtable.Localize("$SCORE_NAME"), FontScale, textcolor);
drawTextScaled(displayFont, kills_x - displayFont.StringWidth(text_kills) * FontScale, y, text_kills, FontScale, textcolor);
drawTextScaled(displayFont, bonus_x - bonus_len * FontScale, y, text_bonus, FontScale, textcolor);
@ -303,7 +298,7 @@ class CoopStatusScreen : StatusScreen
screen.Dim(player.GetDisplayColor(), 0.8f, x, y - ypadding, (secret_x - x) + (8 * CleanXfac), lineheight);
//if (playerready[i] || player.Bot != NULL) // Bots are automatically assumed ready, to prevent confusion
if (ScreenJobRunner.IsPlayerReady(i)) // Bots are automatically assumed ready, to prevent confusion
screen.DrawTexture(readyico, true, x - (readysize.Y * CleanXfac), y, DTA_CleanNoMove, true);
Color thiscolor = GetRowColor(player, i == consoleplayer);
@ -369,4 +364,56 @@ class CoopStatusScreen : StatusScreen
drawTimeScaled(displayFont, secret_x, y, cnt_total_time, FontScale, textcolor);
}
}
override void drawStats ()
{
DrawScoreboard(drawLF());
}
override void drawShowNextLoc()
{
bg.drawBackground(CurState, true, snl_pointeron);
// This has to be expanded out because drawEL() doesn't return its y offset and it's a virtual
// meaning it's too late to change :(
bool ispatch = TexMan.OkForLocalization(enteringPatch, "$WI_ENTERING");
int oldy = TITLEY * scaleFactorY;
if (!ispatch)
{
let asc = entering.mFont.GetMaxAscender("$WI_ENTERING");
if (asc > TITLEY - 2)
{
oldy = (asc+2) * scaleFactorY;
}
}
int y = DrawPatchOrText(oldy, entering, enteringPatch, "$WI_ENTERING");
// If the displayed info is made of patches we need some additional offsetting here.
if (ispatch)
{
int h1 = BigFont.GetHeight() - BigFont.GetDisplacement();
let size = TexMan.GetScaledSize(enteringPatch);
int h2 = int(size.Y);
let disp = min(h1, h2) / 4;
// The offset getting applied here must at least be as tall as the largest ascender in the following text to avoid overlaps.
if (!wbs.LName1.isValid())
{
disp += mapname.mFont.GetMaxAscender(lnametexts[1]);
}
y += disp * scaleFactorY;
}
y = DrawName(y, wbs.LName1, lnametexts[1]);
if (wbs.LName1.isValid() && authortexts[1].length() > 0)
{
// Consdider the ascender height of the following text.
y += author.mFont.GetMaxAscender(authortexts[1]) * scaleFactorY;
}
DrawScoreboard(DrawAuthor(y, authortexts[1]));
}
}

View file

@ -26,7 +26,6 @@ class DeathmatchStatusScreen : StatusScreen
for(i = 0; i < MAXPLAYERS; i++)
{
playerready[i] = false;
cnt_frags[i] = cnt_deaths[i] = player_deaths[i] = 0;
}
total_frags = 0;
@ -124,7 +123,6 @@ class DeathmatchStatusScreen : StatusScreen
}
else if (ng_state == 6)
{
// All players are ready; proceed.
if ((acceleratestage) || doautoskip)
{
PlaySound("intermission/pastdmstats");
@ -141,9 +139,9 @@ class DeathmatchStatusScreen : StatusScreen
}
}
override void drawStats ()
protected void DrawScoreboard(int y)
{
int i, pnum, x, y, ypadding, height, lineheight;
int i, pnum, x, ypadding, height, lineheight;
int maxnamewidth, maxscorewidth, maxiconheight;
int pwidth = IntermissionFont.GetCharWidth("%");
int icon_x, name_x, frags_x, deaths_x;
@ -151,8 +149,6 @@ class DeathmatchStatusScreen : StatusScreen
String text_deaths, text_frags;
TextureID readyico = TexMan.CheckForTexture("READYICO", TexMan.Type_MiscPatch);
y = drawLF();
[maxnamewidth, maxscorewidth, maxiconheight] = GetPlayerWidths();
// Use the readyico height if it's bigger.
Vector2 readysize = TexMan.GetScaledSize(readyico);
@ -199,7 +195,7 @@ class DeathmatchStatusScreen : StatusScreen
screen.Dim(player.GetDisplayColor(), 0.8, x, y - ypadding, (deaths_x - x) + (8 * CleanXfac), lineheight);
//if (playerready[pnum] || player.Bot != NULL) // Bots are automatically assumed ready, to prevent confusion
if (ScreenJobRunner.IsPlayerReady(pnum)) // Bots are automatically assumed ready, to prevent confusion
screen.DrawTexture(readyico, true, x - (readysize.X * CleanXfac), y, DTA_CleanNoMove, true);
let thiscolor = GetRowColor(player, pnum == consoleplayer);
@ -239,4 +235,56 @@ class DeathmatchStatusScreen : StatusScreen
String leveltime = Stringtable.Localize("$SCORE_LVLTIME") .. ": " .. String.Format("%02i:%02i:%02i", hours, minutes, seconds);
drawTextScaled(displayFont, x, y, leveltime, FontScale, textcolor);
}
override void drawStats ()
{
DrawScoreboard(drawLF());
}
override void drawShowNextLoc()
{
bg.drawBackground(CurState, true, snl_pointeron);
// This has to be expanded out because drawEL() doesn't return its y offset and it's a virtual
// meaning it's too late to change :(
bool ispatch = TexMan.OkForLocalization(enteringPatch, "$WI_ENTERING");
int oldy = TITLEY * scaleFactorY;
if (!ispatch)
{
let asc = entering.mFont.GetMaxAscender("$WI_ENTERING");
if (asc > TITLEY - 2)
{
oldy = (asc+2) * scaleFactorY;
}
}
int y = DrawPatchOrText(oldy, entering, enteringPatch, "$WI_ENTERING");
// If the displayed info is made of patches we need some additional offsetting here.
if (ispatch)
{
int h1 = BigFont.GetHeight() - BigFont.GetDisplacement();
let size = TexMan.GetScaledSize(enteringPatch);
int h2 = int(size.Y);
let disp = min(h1, h2) / 4;
// The offset getting applied here must at least be as tall as the largest ascender in the following text to avoid overlaps.
if (!wbs.LName1.isValid())
{
disp += mapname.mFont.GetMaxAscender(lnametexts[1]);
}
y += disp * scaleFactorY;
}
y = DrawName(y, wbs.LName1, lnametexts[1]);
if (wbs.LName1.isValid() && authortexts[1].length() > 0)
{
// Consdider the ascender height of the following text.
y += author.mFont.GetMaxAscender(authortexts[1]) * scaleFactorY;
}
DrawScoreboard(DrawAuthor(y, authortexts[1]));
}
}