Skip to content

Commit 8cf6af6

Browse files
committed
Improve user experience when joining Tutorial server
Previously, joining a Tutorial server was handled in the background and the retry/failure was not indicated to the user. Now, a fullscreen popup is rendered in the menu while trying to join a Tutorial server, which shows progress and error messages. This also allows to cancel joining the Tutorial, which was not possible previously. As before, if no Tutorial server is available immediately when trying to join, the server browser is refreshed after 5 seconds to try again. Now, additionally, if still no Tutorial server is available, the client will automatically attempt to start a local server with the Tutorial map. Furthermore, the local server's MOTD is overridden to indicate to the player that the server is not online and that the record is only saved locally. Finally, if starting and connecting to a local server also failed, an error message popup is shown.
1 parent 73bbb34 commit 8cf6af6

File tree

3 files changed

+253
-48
lines changed

3 files changed

+253
-48
lines changed

src/game/client/components/menus.cpp

Lines changed: 224 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,33 +1025,32 @@ void CMenus::Render()
10251025
m_CommunityIcons.Update();
10261026
}
10271027

1028-
if(ServerBrowser()->DDNetInfoAvailable())
1028+
// Initially add DDNet as favorite community and select its tab.
1029+
// This must be delayed until the DDNet info is available.
1030+
if(m_CreateDefaultFavoriteCommunities &&
1031+
ServerBrowser()->DDNetInfoAvailable())
10291032
{
1030-
// Initially add DDNet as favorite community and select its tab.
1031-
// This must be delayed until the DDNet info is available.
1032-
if(m_CreateDefaultFavoriteCommunities)
1033+
m_CreateDefaultFavoriteCommunities = false;
1034+
if(ServerBrowser()->Community(IServerBrowser::COMMUNITY_DDNET) != nullptr)
10331035
{
1034-
m_CreateDefaultFavoriteCommunities = false;
1035-
if(ServerBrowser()->Community(IServerBrowser::COMMUNITY_DDNET) != nullptr)
1036-
{
1037-
ServerBrowser()->FavoriteCommunitiesFilter().Clear();
1038-
ServerBrowser()->FavoriteCommunitiesFilter().Add(IServerBrowser::COMMUNITY_DDNET);
1039-
SetMenuPage(PAGE_FAVORITE_COMMUNITY_1);
1040-
ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITE_COMMUNITY_1);
1041-
}
1036+
ServerBrowser()->FavoriteCommunitiesFilter().Clear();
1037+
ServerBrowser()->FavoriteCommunitiesFilter().Add(IServerBrowser::COMMUNITY_DDNET);
1038+
SetMenuPage(PAGE_FAVORITE_COMMUNITY_1);
1039+
ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITE_COMMUNITY_1);
10421040
}
1043-
1044-
if(m_JoinTutorial && m_Popup == POPUP_NONE && !ServerBrowser()->IsGettingServerlist())
1041+
}
1042+
if(m_JoinTutorial.m_Queued && m_Popup == POPUP_NONE)
1043+
{
1044+
const char *pAddr = ServerBrowser()->GetTutorialServer();
1045+
if(pAddr)
10451046
{
1046-
m_JoinTutorial = false;
1047-
// This is only reached on first launch, when the DDNet community tab has been created and
1048-
// activated by default, so the server info for the tutorial server should be available.
1049-
const char *pAddr = ServerBrowser()->GetTutorialServer();
1050-
if(pAddr)
1051-
{
1052-
Client()->Connect(pAddr);
1053-
}
1047+
Client()->Connect(pAddr);
1048+
}
1049+
else
1050+
{
1051+
m_Popup = POPUP_JOIN_TUTORIAL;
10541052
}
1053+
m_JoinTutorial.m_Queued = false;
10551054
}
10561055

10571056
// Determine the client state once before rendering because it can change
@@ -1284,6 +1283,10 @@ void CMenus::RenderPopupFullscreen(CUIRect Screen)
12841283
pButtonText = Localize("Ok");
12851284
TopAlign = true;
12861285
}
1286+
else if(m_Popup == POPUP_JOIN_TUTORIAL)
1287+
{
1288+
pTitle = Localize("Joining Tutorial server");
1289+
}
12871290
else if(m_Popup == POPUP_POINTS)
12881291
{
12891292
pTitle = Localize("Existing Player");
@@ -1338,6 +1341,7 @@ void CMenus::RenderPopupFullscreen(CUIRect Screen)
13381341
}
13391342

13401343
// Extra text (optional)
1344+
if(m_Popup != POPUP_JOIN_TUTORIAL)
13411345
{
13421346
CUIRect ExtraText;
13431347
Box.HSplitTop(24.0f, &ExtraText, &Box);
@@ -1761,15 +1765,14 @@ void CMenus::RenderPopupFullscreen(CUIRect Screen)
17611765
static CButtonContainer s_JoinTutorialButton;
17621766
if(DoButton_Menu(&s_JoinTutorialButton, Localize("Join Tutorial Server"), 0, &Join) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))
17631767
{
1764-
m_JoinTutorial = true;
17651768
Client()->RequestDDNetInfo();
17661769
m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE;
1770+
JoinTutorial();
17671771
}
17681772

17691773
static CButtonContainer s_SkipTutorialButton;
17701774
if(DoButton_Menu(&s_SkipTutorialButton, Localize("Skip Tutorial"), 0, &Skip) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE))
17711775
{
1772-
m_JoinTutorial = false;
17731776
Client()->RequestDDNetInfo();
17741777
m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE;
17751778
}
@@ -1797,6 +1800,193 @@ void CMenus::RenderPopupFullscreen(CUIRect Screen)
17971800
s_PlayerNameInput.SetEmptyText(Client()->PlayerName());
17981801
Ui()->DoEditBox(&s_PlayerNameInput, &TextBox, 12.0f);
17991802
}
1803+
else if(m_Popup == POPUP_JOIN_TUTORIAL)
1804+
{
1805+
CUIRect ButtonBar, StatusLabel, ProgressLabel, ProgressIndicator;
1806+
Box.HSplitBottom(20.0f, &Box, nullptr);
1807+
Box.HSplitBottom(24.0f, &Box, &ButtonBar);
1808+
ButtonBar.VMargin(120.0f, &ButtonBar);
1809+
Box.HSplitBottom(20.0f, &StatusLabel, nullptr);
1810+
StatusLabel.VMargin(20.0f, &StatusLabel);
1811+
StatusLabel.HSplitMid(&StatusLabel, &ProgressLabel);
1812+
ProgressLabel.VSplitLeft(50.0f, &ProgressIndicator, &ProgressLabel);
1813+
1814+
if(m_JoinTutorial.m_Status == CJoinTutorial::EStatus::REFRESHING)
1815+
{
1816+
if(ServerBrowser()->IsGettingServerlist() ||
1817+
Client()->InfoState() == IClient::EInfoState::LOADING)
1818+
{
1819+
// Still refreshing
1820+
}
1821+
else if(ServerBrowser()->IsServerlistError() ||
1822+
Client()->InfoState() == IClient::EInfoState::ERROR)
1823+
{
1824+
m_JoinTutorial.m_Status = CJoinTutorial::EStatus::SERVER_LIST_ERROR;
1825+
}
1826+
else
1827+
{
1828+
const char *pAddr = ServerBrowser()->GetTutorialServer();
1829+
if(pAddr)
1830+
{
1831+
Client()->Connect(pAddr);
1832+
}
1833+
else
1834+
{
1835+
m_JoinTutorial.m_Status = CJoinTutorial::EStatus::NO_TUTORIAL_AVAILABLE;
1836+
}
1837+
}
1838+
}
1839+
1840+
const char *pStatusLabel = nullptr;
1841+
switch(m_JoinTutorial.m_Status)
1842+
{
1843+
case CJoinTutorial::EStatus::REFRESHING:
1844+
pStatusLabel = Localize("Getting server list from master server");
1845+
break;
1846+
case CJoinTutorial::EStatus::SERVER_LIST_ERROR:
1847+
pStatusLabel = Localize("Could not get server list from master server");
1848+
break;
1849+
case CJoinTutorial::EStatus::NO_TUTORIAL_AVAILABLE:
1850+
pStatusLabel = Localize("There are no Tutorial servers available");
1851+
break;
1852+
}
1853+
if(pStatusLabel != nullptr)
1854+
{
1855+
Ui()->DoLabel(&StatusLabel, pStatusLabel, 20.0f, TEXTALIGN_ML);
1856+
}
1857+
1858+
const char *pProgressLabel = nullptr;
1859+
bool ProgressDeterminate = true;
1860+
const float LastStateChangeSeconds = std::chrono::duration_cast<std::chrono::duration<float>>(time_get_nanoseconds() - m_JoinTutorial.m_StateChange).count();
1861+
constexpr float RefreshDelay = 5.0f;
1862+
1863+
if(m_JoinTutorial.m_Status == CJoinTutorial::EStatus::REFRESHING)
1864+
{
1865+
pProgressLabel = Localize("Please wait…");
1866+
ProgressDeterminate = false;
1867+
}
1868+
else if(!m_JoinTutorial.m_TryRefresh)
1869+
{
1870+
if(!m_JoinTutorial.m_TriedRefresh)
1871+
{
1872+
m_JoinTutorial.m_TryRefresh = true;
1873+
m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1874+
}
1875+
else if(m_JoinTutorial.m_LocalServerState == CJoinTutorial::ELocalServerState::NOT_TRIED)
1876+
{
1877+
m_JoinTutorial.m_LocalServerState = CJoinTutorial::ELocalServerState::TRY;
1878+
m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1879+
}
1880+
}
1881+
1882+
if(m_JoinTutorial.m_TryRefresh)
1883+
{
1884+
if(LastStateChangeSeconds >= RefreshDelay)
1885+
{
1886+
// Activate internet tab before joining tutorial to make sure the server info
1887+
// for the tutorial servers is available.
1888+
GameClient()->m_Menus.SetMenuPage(CMenus::PAGE_INTERNET);
1889+
GameClient()->m_Menus.RefreshBrowserTab(true);
1890+
m_JoinTutorial.m_Status = CJoinTutorial::EStatus::REFRESHING;
1891+
m_JoinTutorial.m_TryRefresh = false;
1892+
m_JoinTutorial.m_TriedRefresh = true;
1893+
m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1894+
}
1895+
else
1896+
{
1897+
pProgressLabel = Localize("Retrying…");
1898+
}
1899+
}
1900+
1901+
const auto &&ShowFinalErrorMessage = [&]() {
1902+
PopupMessage(Localize("Error joining Tutorial server"), Localize("Could not find a Tutorial server. Check your internet connection."), Localize("Ok"));
1903+
};
1904+
const auto &&RunServer = [&]() {
1905+
char aMotd[256];
1906+
str_copy(aMotd, "sv_motd \"");
1907+
char *pDst = aMotd + str_length(aMotd);
1908+
str_escape(&pDst, Localize("You're playing on a local server because no online Tutorial server could be found.\n\nYour record will only be saved locally."), aMotd + sizeof(aMotd) - 1);
1909+
str_append(aMotd, "\"");
1910+
if(GameClient()->m_LocalServer.RunServer({"sv_register 0", "sv_map Tutorial", aMotd}))
1911+
{
1912+
m_JoinTutorial.m_LocalServerState = CJoinTutorial::ELocalServerState::WAITING_START;
1913+
m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1914+
}
1915+
else
1916+
{
1917+
ShowFinalErrorMessage();
1918+
}
1919+
};
1920+
if(m_JoinTutorial.m_LocalServerState == CJoinTutorial::ELocalServerState::TRY)
1921+
{
1922+
if(LastStateChangeSeconds >= RefreshDelay)
1923+
{
1924+
if(GameClient()->m_LocalServer.IsServerRunning())
1925+
{
1926+
GameClient()->m_LocalServer.KillServer();
1927+
m_JoinTutorial.m_LocalServerState = CJoinTutorial::ELocalServerState::WAITING_STOP;
1928+
m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1929+
}
1930+
else
1931+
{
1932+
RunServer();
1933+
}
1934+
}
1935+
else
1936+
{
1937+
pProgressLabel = Localize("Could not find online Tutorial server.\nStarting and connecting to local server…");
1938+
}
1939+
}
1940+
else if(m_JoinTutorial.m_LocalServerState == CJoinTutorial::ELocalServerState::WAITING_STOP)
1941+
{
1942+
if(LastStateChangeSeconds >= 5.0f)
1943+
{
1944+
ShowFinalErrorMessage();
1945+
}
1946+
else
1947+
{
1948+
if(!GameClient()->m_LocalServer.IsServerRunning())
1949+
{
1950+
RunServer();
1951+
}
1952+
1953+
pProgressLabel = Localize("Waiting for local server to stop…");
1954+
ProgressDeterminate = false;
1955+
}
1956+
}
1957+
else if(m_JoinTutorial.m_LocalServerState == CJoinTutorial::ELocalServerState::WAITING_START)
1958+
{
1959+
if(LastStateChangeSeconds >= 5.0f)
1960+
{
1961+
ShowFinalErrorMessage();
1962+
}
1963+
else
1964+
{
1965+
if(LastStateChangeSeconds >= 2.0f &&
1966+
GameClient()->m_LocalServer.IsServerRunning())
1967+
{
1968+
Client()->Connect("localhost");
1969+
}
1970+
1971+
pProgressLabel = Localize("Waiting for local server to start…");
1972+
ProgressDeterminate = false;
1973+
}
1974+
}
1975+
1976+
if(pProgressLabel != nullptr)
1977+
{
1978+
Ui()->RenderProgressSpinner(ProgressIndicator.Center(), 12.0f, {.m_Progress = ProgressDeterminate ? (LastStateChangeSeconds / RefreshDelay) : -1.0f});
1979+
Ui()->DoLabel(&ProgressLabel, pProgressLabel, 20.0f, TEXTALIGN_ML);
1980+
}
1981+
1982+
static CButtonContainer s_Button;
1983+
if(DoButton_Menu(&s_Button, Localize("Cancel"), 0, &ButtonBar) ||
1984+
Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE) ||
1985+
Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))
1986+
{
1987+
m_Popup = POPUP_NONE;
1988+
}
1989+
}
18001990
else if(m_Popup == POPUP_POINTS)
18011991
{
18021992
Box.HSplitBottom(20.0f, &Box, nullptr);
@@ -2576,3 +2766,13 @@ void CMenus::ShowQuitPopup()
25762766
{
25772767
m_Popup = POPUP_QUIT;
25782768
}
2769+
2770+
void CMenus::JoinTutorial()
2771+
{
2772+
m_JoinTutorial.m_Queued = true;
2773+
m_JoinTutorial.m_Status = CJoinTutorial::EStatus::REFRESHING;
2774+
m_JoinTutorial.m_TryRefresh = false;
2775+
m_JoinTutorial.m_TriedRefresh = false;
2776+
m_JoinTutorial.m_LocalServerState = CJoinTutorial::ELocalServerState::NOT_TRIED;
2777+
m_JoinTutorial.m_StateChange = time_get_nanoseconds();
2778+
}

src/game/client/components/menus.h

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,31 @@ class CMenus : public CComponent
145145

146146
bool m_DummyNamePlatePreview = false;
147147

148-
bool m_JoinTutorial = false;
148+
class CJoinTutorial
149+
{
150+
public:
151+
bool m_Queued = false;
152+
enum class EStatus
153+
{
154+
REFRESHING,
155+
SERVER_LIST_ERROR,
156+
NO_TUTORIAL_AVAILABLE,
157+
};
158+
EStatus m_Status = EStatus::REFRESHING;
159+
bool m_TryRefresh = false;
160+
bool m_TriedRefresh = false;
161+
enum class ELocalServerState
162+
{
163+
NOT_TRIED,
164+
TRY,
165+
WAITING_STOP,
166+
WAITING_START,
167+
};
168+
ELocalServerState m_LocalServerState = ELocalServerState::NOT_TRIED;
169+
std::chrono::nanoseconds m_StateChange = std::chrono::nanoseconds(0);
170+
};
171+
CJoinTutorial m_JoinTutorial;
172+
149173
bool m_CreateDefaultFavoriteCommunities = false;
150174
bool m_ForceRefreshLanPage = false;
151175

@@ -765,6 +789,7 @@ class CMenus : public CComponent
765789
POPUP_MESSAGE, // generic message popup (one button)
766790
POPUP_CONFIRM, // generic confirmation popup (two buttons)
767791
POPUP_FIRST_LAUNCH,
792+
POPUP_JOIN_TUTORIAL,
768793
POPUP_POINTS,
769794
POPUP_DISCONNECTED,
770795
POPUP_LANGUAGE,
@@ -790,6 +815,7 @@ class CMenus : public CComponent
790815
void ForceRefreshLanPage();
791816
void SetShowStart(bool ShowStart);
792817
void ShowQuitPopup();
818+
void JoinTutorial();
793819

794820
private:
795821
CCommunityIcons m_CommunityIcons;

src/game/client/components/menus_start.cpp

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -62,30 +62,9 @@ void CMenusStart::RenderStartMenu(CUIRect MainView)
6262
ExtMenu.HSplitBottom(5.0f, &ExtMenu, nullptr); // little space
6363
ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button);
6464
static CButtonContainer s_TutorialButton;
65-
static float s_JoinTutorialTime = 0.0f;
66-
if(GameClient()->m_Menus.DoButton_Menu(&s_TutorialButton, Localize("Tutorial"), 0, &Button, BUTTONFLAG_LEFT, nullptr, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) ||
67-
(s_JoinTutorialTime != 0.0f && Client()->LocalTime() >= s_JoinTutorialTime))
65+
if(GameClient()->m_Menus.DoButton_Menu(&s_TutorialButton, Localize("Tutorial"), 0, &Button, BUTTONFLAG_LEFT, nullptr, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
6866
{
69-
// Activate internet tab before joining tutorial to make sure the server info
70-
// for the tutorial servers is available.
71-
GameClient()->m_Menus.SetMenuPage(CMenus::PAGE_INTERNET);
72-
GameClient()->m_Menus.RefreshBrowserTab(true);
73-
const char *pAddr = ServerBrowser()->GetTutorialServer();
74-
if(pAddr)
75-
{
76-
Client()->Connect(pAddr);
77-
s_JoinTutorialTime = 0.0f;
78-
}
79-
else if(s_JoinTutorialTime == 0.0f)
80-
{
81-
dbg_msg("menus", "couldn't find tutorial server, retrying in 5 seconds");
82-
s_JoinTutorialTime = Client()->LocalTime() + 5.0f;
83-
}
84-
else
85-
{
86-
Client()->AddWarning(SWarning(Localize("Can't find a Tutorial server")));
87-
s_JoinTutorialTime = 0.0f;
88-
}
67+
GameClient()->m_Menus.JoinTutorial();
8968
}
9069

9170
ExtMenu.HSplitBottom(5.0f, &ExtMenu, nullptr); // little space

0 commit comments

Comments
 (0)