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])); + } }