From fae43b8120509f3ec3e6a495f0bdb54b96e7db7f Mon Sep 17 00:00:00 2001 From: Boondorl Date: Fri, 30 May 2025 23:18:04 -0400 Subject: [PATCH] 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. --- src/common/cutscenes/screenjob.cpp | 6 +- src/common/cutscenes/screenjob.h | 9 +- src/d_net.cpp | 174 +++++++++++++++++- src/d_net.h | 2 + src/d_protocol.h | 3 +- src/g_game.cpp | 44 ++++- src/g_game.h | 2 +- src/g_level.cpp | 7 +- src/intermission/intermission_parse.cpp | 2 +- src/p_tick.cpp | 5 + wadsrc/static/menudef.txt | 10 + wadsrc/static/zscript/engine/screenjob.zs | 59 +++++- wadsrc/static/zscript/ui/intermission.zs | 82 +++++++++ .../zscript/ui/statscreen/statscreen.zs | 2 +- .../zscript/ui/statscreen/statscreen_coop.zs | 63 ++++++- .../zscript/ui/statscreen/statscreen_dm.zs | 62 ++++++- 16 files changed, 493 insertions(+), 39 deletions(-) diff --git a/src/common/cutscenes/screenjob.cpp b/src/common/cutscenes/screenjob.cpp index 6220291ea..edad781d9 100644 --- a/src/common/cutscenes/screenjob.cpp +++ b/src/common/cutscenes/screenjob.cpp @@ -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; } diff --git a/src/common/cutscenes/screenjob.h b/src/common/cutscenes/screenjob.h index dce55e3b0..e619f46d9 100644 --- a/src/common/cutscenes/screenjob.h +++ b/src/common/cutscenes/screenjob.h @@ -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 diff --git a/src/d_net.cpp b/src/d_net.cpp index 7ffc55510..0095d0f31 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -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(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(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(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) { diff --git a/src/d_net.h b/src/d_net.h index 8b2b6649d..b62f4be05 100644 --- a/src/d_net.h +++ b/src/d_net.h @@ -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(); diff --git a/src/d_protocol.h b/src/d_protocol.h index 10b38f801..f4388d3bf 100644 --- a/src/d_protocol.h +++ b/src/d_protocol.h @@ -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 diff --git a/src/g_game.cpp b/src/g_game.cpp index 703cac8bb..6e4b445dd 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -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(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; diff --git a/src/g_game.h b/src/g_game.h index 208e91dcb..fcb9a1974 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -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 completionf); +void RunIntermission(level_info_t* oldlevel, level_info_t* newlevel, DIntermissionController* intermissionScreen, DObject* statusScreen, bool ending, std::function completionf); extern const AActor *SendItemUse, *SendItemDrop; extern int SendItemDropAmount; diff --git a/src/g_level.cpp b/src/g_level.cpp index 4367d4b1c..f5921948e 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1048,9 +1048,10 @@ DIntermissionController* FLevelLocals::CreateIntermission() // //============================================================================= -void RunIntermission(level_info_t* fromMap, level_info_t* toMap, DIntermissionController* intermissionScreen, DObject* statusScreen, std::function completionf) +void RunIntermission(level_info_t* fromMap, level_info_t* toMap, DIntermissionController* intermissionScreen, DObject* statusScreen, bool ending, std::function 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(); diff --git a/src/intermission/intermission_parse.cpp b/src/intermission/intermission_parse.cpp index c27f99e4d..211653011 100644 --- a/src/intermission/intermission_parse.cpp +++ b/src/intermission/intermission_parse.cpp @@ -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; }); } diff --git a/src/p_tick.cpp b/src/p_tick.cpp index 18589a01b..dee5fd56e 100644 --- a/src/p_tick.cpp +++ b/src/p_tick.cpp @@ -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(); diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 44e324612..4e7413357 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -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" diff --git a/wadsrc/static/zscript/engine/screenjob.zs b/wadsrc/static/zscript/engine/screenjob.zs index dc50f5535..fb9fd9c29 100644 --- a/wadsrc/static/zscript/engine/screenjob.zs +++ b/wadsrc/static/zscript/engine/screenjob.zs @@ -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 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); diff --git a/wadsrc/static/zscript/ui/intermission.zs b/wadsrc/static/zscript/ui/intermission.zs index 657be98ba..b42616c57 100644 --- a/wadsrc/static/zscript/ui/intermission.zs +++ b/wadsrc/static/zscript/ui/intermission.zs @@ -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 { diff --git a/wadsrc/static/zscript/ui/statscreen/statscreen.zs b/wadsrc/static/zscript/ui/statscreen/statscreen.zs index 94343f80d..6603c2e06 100644 --- a/wadsrc/static/zscript/ui/statscreen/statscreen.zs +++ b/wadsrc/static/zscript/ui/statscreen/statscreen.zs @@ -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 diff --git a/wadsrc/static/zscript/ui/statscreen/statscreen_coop.zs b/wadsrc/static/zscript/ui/statscreen/statscreen_coop.zs index 73b397c07..baaa633b4 100644 --- a/wadsrc/static/zscript/ui/statscreen/statscreen_coop.zs +++ b/wadsrc/static/zscript/ui/statscreen/statscreen_coop.zs @@ -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])); + } } diff --git a/wadsrc/static/zscript/ui/statscreen/statscreen_dm.zs b/wadsrc/static/zscript/ui/statscreen/statscreen_dm.zs index e768b9db2..93f3ab8e9 100644 --- a/wadsrc/static/zscript/ui/statscreen/statscreen_dm.zs +++ b/wadsrc/static/zscript/ui/statscreen/statscreen_dm.zs @@ -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])); + } }