From d910d1380191eaebfb7b3b3eff0e8eea8dcb579c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Wed, 18 Sep 2024 13:36:05 +0200 Subject: [PATCH 01/58] Overhaul console design: replace background, improve contrast Replace the dotted background texture of the console with a less noticeable noise texture. Replace the large striped bar at the bottom of the console with a small bar with a slightly darker color than the respective console background. Remove the shadow above the striped bar. Improve the spacing between the console backlog and input with the additional free space from the replaced striped bar. The total console height is not changed. Improve the contrast of the console completion options by slightly increasing the saturation/brightness of the text color and by coloring the selection rectangle black instead of using an orange color which was very similar to the highlight color. Also highlight the matching string of the selected completion option instead of only highlighting it for all other options. --- CMakeLists.txt | 3 +- data/background_noise.png | Bin 0 -> 3347 bytes data/console.png | Bin 679 -> 0 bytes data/console_bar.png | Bin 3822 -> 0 bytes datasrc/content.py | 3 +- src/game/client/components/console.cpp | 110 +++++++++++-------------- src/game/client/components/console.h | 1 + 7 files changed, 52 insertions(+), 65 deletions(-) create mode 100644 data/background_noise.png delete mode 100644 data/console.png delete mode 100644 data/console_bar.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 436c8cf53a1..ba403347f62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1133,11 +1133,10 @@ set(EXPECTED_DATA audio/wp_switch-02.wv audio/wp_switch-03.wv autoexec_server.cfg + background_noise.png blob.png censorlist.txt communityicons/none.png - console.png - console_bar.png countryflags/AD.png countryflags/AE.png countryflags/AF.png diff --git a/data/background_noise.png b/data/background_noise.png new file mode 100644 index 0000000000000000000000000000000000000000..9465570ce0679bc8023ec2c454024054cad63987 GIT binary patch literal 3347 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 L%}lQh?h^eE$vA?* literal 0 HcmV?d00001 diff --git a/data/console.png b/data/console.png deleted file mode 100644 index d4d664715fc742041273487e38ad935e6b133ccc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 679 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!3HERU8}EWU|`Dgba4!+V0<&nbGKyN`Z2AeM8gKVB}L!D6o8MurC6 znEi`D&v&pk9IxBYfA8mw2ZvtH^?PnO`|N~x&+>H#x6WZ`_;7*o0Nd=o_g||l z{1}a{Z{v2@ew$a0A!ozGiYGZ{(pzm$I7}01V7l!P_iy&uW!*iTGf!M&zVQBgGb_X0 zsps;yMmanaDkz!2bijG@mY37LeolJtxKl>+k+WpGH~RVoR2_fxLi^WxePpM*ub8JjZfTY5NTPp7b}eN{F1 zs(WFH1dC{c-7lsYp0>PRw!CJl%}UD~6*paUjJ?gjMIdZNc|y}IdDfM2ua*`ByZ^o% zX6$lbZfiWlW$`0N81}rEU2$vSt6O!a-WDi#%*mGS*l_WZZ>_?y74C~>oKC*~FIlB4 z+2YzSkC0c&h4PFi_@8}R#K;hB#BfxOC*6&iVOt6Vm)*R#cikBo90U$I{9pIc@mXWz z>r_s|A`Cy^En z&S0Ts8ej5%hKsG!nPb>u*kama+G9Dza*FL7TN#B%#Y_%I9F8=&G`bu-a_|VFE0e3! zQKzF#uFbB8j~+hC-6J5OXylgCb3sE$+Pz8HvqXopU+v?^Lf-g9!B@(qPb+~5kipZ{ K&t;ucLK6T-e<<$& diff --git a/data/console_bar.png b/data/console_bar.png deleted file mode 100644 index 546ce941973d4efc2e58923bec84b77246e68aef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3822 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%0AEpNb7 z?)_zMCWAux&0@;1s+*ge-xnMAomZ=GK{&cL$2sGMuirb$;_tP(UJ?50Sf#V6*~l>% zSSdW<#;l3&7~zG=iyYsL)kokByZPDK#=@61c5K)EA4l)b+eOB)I=C8NxBEEdEhGR# z&ve+KAgPN~swb#N^)*)3K4)S5j{-N~PI!)+nU`s}O|AO6Yh|bc#g(W!{;n<3;D_bW zZc6qFp$@HoF2yQ9&|7P@vu8-WRZ(o#S4|ML_Ue={RxVj~0)Jr1{xX}!O44eVTG60J hdijw@9M-u!VOx&&!?$SU&7I?{y{)qi=Iph5{{f}C5tIM` diff --git a/datasrc/content.py b/datasrc/content.py index 7cb5fbe1a9e..8eee9776b36 100644 --- a/datasrc/content.py +++ b/datasrc/content.py @@ -245,8 +245,7 @@ def FileList(fmt, num): container.images.Add(Image("cursor", "gui_cursor.png")) container.images.Add(Image("banner", "gui_logo.png")) container.images.Add(image_emoticons) -container.images.Add(Image("console_bg", "console.png")) -container.images.Add(Image("console_bar", "console_bar.png")) +container.images.Add(Image("background_noise", "background_noise.png")) container.images.Add(image_speedup_arrow) container.images.Add(image_guibuttons) container.images.Add(image_guiicons) diff --git a/src/game/client/components/console.cpp b/src/game/client/components/console.cpp index 803ff59292b..a3257a0b1c8 100644 --- a/src/game/client/components/console.cpp +++ b/src/game/client/components/console.cpp @@ -918,41 +918,45 @@ void CGameConsole::PossibleCommandsRenderCallback(int Index, const char *pStr, v { CCompletionOptionRenderInfo *pInfo = static_cast(pUser); + ColorRGBA TextColor; if(Index == pInfo->m_WantedCompletion) { - float TextWidth = pInfo->m_pSelf->TextRender()->TextWidth(pInfo->m_Cursor.m_FontSize, pStr, -1, -1.0f); - const CUIRect Rect = {pInfo->m_Cursor.m_X - 2.5f, pInfo->m_Cursor.m_Y - 4.f / 2.f, TextWidth + 5.f, pInfo->m_Cursor.m_FontSize + 4.f}; - Rect.Draw(ColorRGBA(229.0f / 255.0f, 185.0f / 255.0f, 4.0f / 255.0f, 0.85f), IGraphics::CORNER_ALL, pInfo->m_Cursor.m_FontSize / 3.f); + TextColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + const float TextWidth = pInfo->m_pSelf->TextRender()->TextWidth(pInfo->m_Cursor.m_FontSize, pStr); + const CUIRect Rect = {pInfo->m_Cursor.m_X - 2.0f, pInfo->m_Cursor.m_Y - 2.0f, TextWidth + 4.0f, pInfo->m_Cursor.m_FontSize + 4.0f}; + Rect.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.85f), IGraphics::CORNER_ALL, 2.0f); // scroll when out of sight const bool MoveLeft = Rect.x - *pInfo->m_pOffsetChange < 0.0f; const bool MoveRight = Rect.x + Rect.w - *pInfo->m_pOffsetChange > pInfo->m_Width; if(MoveLeft && !MoveRight) + { *pInfo->m_pOffsetChange -= -Rect.x + pInfo->m_Width / 4.0f; + } else if(!MoveLeft && MoveRight) + { *pInfo->m_pOffsetChange += Rect.x + Rect.w - pInfo->m_Width + pInfo->m_Width / 4.0f; - - pInfo->m_pSelf->TextRender()->TextColor(0.05f, 0.05f, 0.05f, 1); - pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pStr, -1); + } } else { - const char *pMatchStart = str_find_nocase(pStr, pInfo->m_pCurrentCmd); + TextColor = ColorRGBA(0.75f, 0.75f, 0.75f, 1.0f); + } - if(pMatchStart) - { - pInfo->m_pSelf->TextRender()->TextColor(0.5f, 0.5f, 0.5f, 1); - pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pStr, pMatchStart - pStr); - pInfo->m_pSelf->TextRender()->TextColor(229.0f / 255.0f, 185.0f / 255.0f, 4.0f / 255.0f, 1); - pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pMatchStart, str_length(pInfo->m_pCurrentCmd)); - pInfo->m_pSelf->TextRender()->TextColor(0.5f, 0.5f, 0.5f, 1); - pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pMatchStart + str_length(pInfo->m_pCurrentCmd), -1); - } - else - { - pInfo->m_pSelf->TextRender()->TextColor(0.75f, 0.75f, 0.75f, 1); - pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pStr, -1); - } + const char *pMatchStart = str_find_nocase(pStr, pInfo->m_pCurrentCmd); + if(pMatchStart) + { + pInfo->m_pSelf->TextRender()->TextColor(TextColor); + pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pStr, pMatchStart - pStr); + pInfo->m_pSelf->TextRender()->TextColor(1.0f, 0.75f, 0.0f, 1.0f); + pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pMatchStart, str_length(pInfo->m_pCurrentCmd)); + pInfo->m_pSelf->TextRender()->TextColor(TextColor); + pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pMatchStart + str_length(pInfo->m_pCurrentCmd)); + } + else + { + pInfo->m_pSelf->TextRender()->TextColor(TextColor); + pInfo->m_pSelf->TextRender()->TextEx(&pInfo->m_Cursor, pStr); } pInfo->m_Cursor.m_X += 7.0f; @@ -1029,69 +1033,53 @@ void CGameConsole::OnRender() const float ConsoleHeight = ConsoleHeightScale * MaxConsoleHeight; - Ui()->MapScreen(); + const ColorRGBA ShadowColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f); + const ColorRGBA TransparentColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + const ColorRGBA aBackgroundColors[NUM_CONSOLETYPES] = {ColorRGBA(0.2f, 0.2f, 0.2f, 0.9f), ColorRGBA(0.4f, 0.2f, 0.2f, 0.9f)}; + const ColorRGBA aBorderColors[NUM_CONSOLETYPES] = {ColorRGBA(0.1f, 0.1f, 0.1f, 0.9f), ColorRGBA(0.2f, 0.1f, 0.1f, 0.9f)}; - // do console shadow - Graphics()->TextureClear(); - Graphics()->QuadsBegin(); - IGraphics::CColorVertex Array[4] = { - IGraphics::CColorVertex(0, 0, 0, 0, 0.5f), - IGraphics::CColorVertex(1, 0, 0, 0, 0.5f), - IGraphics::CColorVertex(2, 0, 0, 0, 0.0f), - IGraphics::CColorVertex(3, 0, 0, 0, 0.0f)}; - Graphics()->SetColorVertex(Array, 4); - IGraphics::CQuadItem QuadItem(0, ConsoleHeight, Screen.w, 10.0f); - Graphics()->QuadsDrawTL(&QuadItem, 1); - Graphics()->QuadsEnd(); + Ui()->MapScreen(); - // do background - Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CONSOLE_BG].m_Id); + // background + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id); Graphics()->QuadsBegin(); - Graphics()->SetColor(0.2f, 0.2f, 0.2f, 0.9f); - if(m_ConsoleType == CONSOLETYPE_REMOTE) - Graphics()->SetColor(0.4f, 0.2f, 0.2f, 0.9f); - Graphics()->QuadsSetSubset(0, -ConsoleHeight * 0.075f, Screen.w * 0.075f * 0.5f, 0); - QuadItem = IGraphics::CQuadItem(0, 0, Screen.w, ConsoleHeight); - Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->SetColor(aBackgroundColors[m_ConsoleType]); + Graphics()->QuadsSetSubset(0, 0, Screen.w / 80.0f, ConsoleHeight / 80.0f); + IGraphics::CQuadItem QuadItemBackground(0.0f, 0.0f, Screen.w, ConsoleHeight); + Graphics()->QuadsDrawTL(&QuadItemBackground, 1); Graphics()->QuadsEnd(); - // do small bar shadow + // bottom border Graphics()->TextureClear(); Graphics()->QuadsBegin(); - Array[0] = IGraphics::CColorVertex(0, 0, 0, 0, 0.0f); - Array[1] = IGraphics::CColorVertex(1, 0, 0, 0, 0.0f); - Array[2] = IGraphics::CColorVertex(2, 0, 0, 0, 0.25f); - Array[3] = IGraphics::CColorVertex(3, 0, 0, 0, 0.25f); - - Graphics()->SetColorVertex(Array, 4); - QuadItem = IGraphics::CQuadItem(0, ConsoleHeight - 20, Screen.w, 10); - Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->SetColor(aBorderColors[m_ConsoleType]); + IGraphics::CQuadItem QuadItemBorder(0.0f, ConsoleHeight, Screen.w, 1.0f); + Graphics()->QuadsDrawTL(&QuadItemBorder, 1); Graphics()->QuadsEnd(); - // do the lower bar - Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CONSOLE_BAR].m_Id); + // bottom shadow + Graphics()->TextureClear(); Graphics()->QuadsBegin(); - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.9f); - Graphics()->QuadsSetSubset(0, 0.1f, Screen.w * 0.015f, 1 - 0.1f); - QuadItem = IGraphics::CQuadItem(0, ConsoleHeight - 10.0f, Screen.w, 10.0f); - Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->SetColor4(ShadowColor, ShadowColor, TransparentColor, TransparentColor); + IGraphics::CQuadItem QuadItemShadow(0.0f, ConsoleHeight + 1.0f, Screen.w, 10.0f); + Graphics()->QuadsDrawTL(&QuadItemShadow, 1); Graphics()->QuadsEnd(); { // Get height of 1 line const float LineHeight = TextRender()->TextBoundingBox(FONT_SIZE, " ", -1, -1.0f, LINE_SPACING).m_H; - const float RowHeight = FONT_SIZE * 1.5f; + const float RowHeight = FONT_SIZE * 2.0f; float x = 3; - float y = ConsoleHeight - RowHeight - 27.0f; + float y = ConsoleHeight - RowHeight - 18.0f; const float InitialX = x; const float InitialY = y; // render prompt CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, x, y, FONT_SIZE, TEXTFLAG_RENDER); + TextRender()->SetCursor(&Cursor, x, y + FONT_SIZE / 2.0f, FONT_SIZE, TEXTFLAG_RENDER); char aPrompt[32]; Prompt(aPrompt); @@ -1165,7 +1153,7 @@ void CGameConsole::OnRender() { pConsole->m_Input.Activate(EInputPriority::CONSOLE); // Ensure that the input is active } - const CUIRect InputCursorRect = {x, y + FONT_SIZE, 0.0f, 0.0f}; + const CUIRect InputCursorRect = {x, y + FONT_SIZE * 1.5f, 0.0f, 0.0f}; const bool WasChanged = pConsole->m_Input.WasChanged(); const bool WasCursorChanged = pConsole->m_Input.WasCursorChanged(); const bool Changed = WasChanged || WasCursorChanged; diff --git a/src/game/client/components/console.h b/src/game/client/components/console.h index e8ab47900f4..f411d0323d4 100644 --- a/src/game/client/components/console.h +++ b/src/game/client/components/console.h @@ -176,6 +176,7 @@ class CGameConsole : public CComponent { CONSOLETYPE_LOCAL = 0, CONSOLETYPE_REMOTE, + NUM_CONSOLETYPES }; CGameConsole(); From 72892e87fbb7052d244df13c2246c1a8dd11bd6c Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Tue, 12 Nov 2024 08:51:33 +0800 Subject: [PATCH 02/58] Use uint64_t for snapshot rates to avoid overflows See also https://github.com/ddnet/ddnet/commit/65cb2ed7de102f155cde64b4e75839d15b6fe35a And #5646 --- src/engine/client/client.cpp | 26 +++++++++++++++++++++++--- src/engine/shared/snapshot.cpp | 4 ++-- src/engine/shared/snapshot.h | 10 +++++----- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index b9242e42b28..0f1688dc0fd 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -898,7 +898,13 @@ void CClient::DebugRender() { if(m_SnapshotDelta.GetDataRate(i)) { - str_format(aBuffer, sizeof(aBuffer), "%5d %20s: %8d %8d %8d", i, GameClient()->GetItemName(i), m_SnapshotDelta.GetDataRate(i) / 8, m_SnapshotDelta.GetDataUpdates(i), + str_format( + aBuffer, + sizeof(aBuffer), + "%5d %20s: %8" PRIu64 " %8" PRIu64 " %8" PRIu64, + i, + GameClient()->GetItemName(i), + m_SnapshotDelta.GetDataRate(i) / 8, m_SnapshotDelta.GetDataUpdates(i), (m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8); Graphics()->QuadsText(2, 100 + y * 12, 16, aBuffer); y++; @@ -911,14 +917,28 @@ void CClient::DebugRender() int Type = m_aapSnapshots[g_Config.m_ClDummy][IClient::SNAP_CURRENT]->m_pAltSnap->GetExternalItemType(i); if(Type == UUID_INVALID) { - str_format(aBuffer, sizeof(aBuffer), "%5d %20s: %8d %8d %8d", i, "Unknown UUID", m_SnapshotDelta.GetDataRate(i) / 8, m_SnapshotDelta.GetDataUpdates(i), + str_format( + aBuffer, + sizeof(aBuffer), + "%5d %20s: %8" PRIu64 " %8" PRIu64 " %8" PRIu64, + i, + "Unknown UUID", + m_SnapshotDelta.GetDataRate(i) / 8, + m_SnapshotDelta.GetDataUpdates(i), (m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8); Graphics()->QuadsText(2, 100 + y * 12, 16, aBuffer); y++; } else if(Type != i) { - str_format(aBuffer, sizeof(aBuffer), "%5d %20s: %8d %8d %8d", Type, GameClient()->GetItemName(Type), m_SnapshotDelta.GetDataRate(i) / 8, m_SnapshotDelta.GetDataUpdates(i), + str_format( + aBuffer, + sizeof(aBuffer), + "%5d %20s: %8" PRIu64 " %8" PRIu64 " %8" PRIu64, + Type, + GameClient()->GetItemName(Type), + m_SnapshotDelta.GetDataRate(i) / 8, + m_SnapshotDelta.GetDataUpdates(i), (m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8); Graphics()->QuadsText(2, 100 + y * 12, 16, aBuffer); y++; diff --git a/src/engine/shared/snapshot.cpp b/src/engine/shared/snapshot.cpp index 99d2b50ccc7..7f9706de930 100644 --- a/src/engine/shared/snapshot.cpp +++ b/src/engine/shared/snapshot.cpp @@ -230,7 +230,7 @@ int CSnapshotDelta::DiffItem(const int *pPast, const int *pCurrent, int *pOut, i return Needed; } -void CSnapshotDelta::UndiffItem(const int *pPast, const int *pDiff, int *pOut, int Size, int *pDataRate) +void CSnapshotDelta::UndiffItem(const int *pPast, const int *pDiff, int *pOut, int Size, uint64_t *pDataRate) { while(Size) { @@ -243,7 +243,7 @@ void CSnapshotDelta::UndiffItem(const int *pPast, const int *pDiff, int *pOut, i { unsigned char aBuf[CVariableInt::MAX_BYTES_PACKED]; unsigned char *pEnd = CVariableInt::Pack(aBuf, *pDiff, sizeof(aBuf)); - *pDataRate += (int)(pEnd - (unsigned char *)aBuf) * 8; + *pDataRate += (uint64_t)(pEnd - (unsigned char *)aBuf) * 8; } pOut++; diff --git a/src/engine/shared/snapshot.h b/src/engine/shared/snapshot.h index 7a001221d5d..105f05d1f9b 100644 --- a/src/engine/shared/snapshot.h +++ b/src/engine/shared/snapshot.h @@ -89,18 +89,18 @@ class CSnapshotDelta }; short m_aItemSizes[MAX_NETOBJSIZES]; short m_aItemSizes7[MAX_NETOBJSIZES]; - int m_aSnapshotDataRate[CSnapshot::MAX_TYPE + 1]; - int m_aSnapshotDataUpdates[CSnapshot::MAX_TYPE + 1]; + uint64_t m_aSnapshotDataRate[CSnapshot::MAX_TYPE + 1]; + uint64_t m_aSnapshotDataUpdates[CSnapshot::MAX_TYPE + 1]; CData m_Empty; - static void UndiffItem(const int *pPast, const int *pDiff, int *pOut, int Size, int *pDataRate); + static void UndiffItem(const int *pPast, const int *pDiff, int *pOut, int Size, uint64_t *pDataRate); public: static int DiffItem(const int *pPast, const int *pCurrent, int *pOut, int Size); CSnapshotDelta(); CSnapshotDelta(const CSnapshotDelta &Old); - int GetDataRate(int Index) const { return m_aSnapshotDataRate[Index]; } - int GetDataUpdates(int Index) const { return m_aSnapshotDataUpdates[Index]; } + uint64_t GetDataRate(int Index) const { return m_aSnapshotDataRate[Index]; } + uint64_t GetDataUpdates(int Index) const { return m_aSnapshotDataUpdates[Index]; } void SetStaticsize(int ItemType, size_t Size); void SetStaticsize7(int ItemType, size_t Size); const CData *EmptyDelta() const; From 956b8ef902045f2099e6bdd18e30ce690242cd50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Tue, 22 Oct 2024 20:56:32 +0200 Subject: [PATCH 03/58] Add parameter `AllowTruncation` to `CPacker::AddString` function Previously, the `CPacker::AddString` function would truncate strings if the `int Limit` parameter or the packet buffer size were exceeded. When the packer buffer size is exceeded the packer would also enter the error state. The parameter `bool AllowTruncation` is added to control whether the string is allowed to be truncated when using the `Limit` parameter. If truncation is not allowed then the packer will instead enter the error state and the truncated string will not be packed. The parameter `AllowTruncation` has the default value `true` to preserve the behavior of existing function calls, where the parameter is used to truncate strings. The behavior of the `AddString` function when it enters the error state is changed so the packer position is not advanced to the end of the truncated string. This makes it consistent with the behavior of the other packer functions which either pack the entire given argument or do not change the packer position. This allows using the `Limit` parameter to pack a message with a specific maximum size (i.e. the maximum payload size) without depending on the protocol specifics in the packer implementation. In particular, this is intended for cases where as many strings as possible are packed into a message, in which case the strings should not be truncated. --- src/engine/shared/packer.cpp | 27 ++++++++----- src/engine/shared/packer.h | 2 +- src/test/packer.cpp | 77 +++++++++++++++++++++++------------- 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/src/engine/shared/packer.cpp b/src/engine/shared/packer.cpp index 3593929ea68..eaab9528365 100644 --- a/src/engine/shared/packer.cpp +++ b/src/engine/shared/packer.cpp @@ -26,39 +26,48 @@ void CPacker::AddInt(int i) m_pCurrent = pNext; } -void CPacker::AddString(const char *pStr, int Limit) +void CPacker::AddString(const char *pStr, int Limit, bool AllowTruncation) { if(m_Error) return; + unsigned char *const pPrevCurrent = m_pCurrent; if(Limit <= 0) { Limit = PACKER_BUFFER_SIZE; } - while(*pStr && Limit != 0) + while(*pStr) { int Codepoint = str_utf8_decode(&pStr); if(Codepoint == -1) { Codepoint = 0xfffd; // Unicode replacement character. } - char aGarbage[4]; - int Length = str_utf8_encode(aGarbage, Codepoint); + char aEncoded[4]; + const int Length = str_utf8_encode(aEncoded, Codepoint); + // Limit must ensure space for null termination if desired. if(Limit < Length) { - break; + if(AllowTruncation) + { + break; + } + m_Error = true; + m_pCurrent = pPrevCurrent; + return; } // Ensure space for the null termination. - if(m_pEnd - m_pCurrent < Length + 1) + if(m_pCurrent + Length + 1 > m_pEnd) { m_Error = true; - break; + m_pCurrent = pPrevCurrent; + return; } - Length = str_utf8_encode((char *)m_pCurrent, Codepoint); + mem_copy(m_pCurrent, aEncoded, Length); m_pCurrent += Length; Limit -= Length; } - *m_pCurrent++ = 0; + *m_pCurrent++ = '\0'; } void CPacker::AddRaw(const void *pData, int Size) diff --git a/src/engine/shared/packer.h b/src/engine/shared/packer.h index 3ee622f6fd9..d69a5ce1b2c 100644 --- a/src/engine/shared/packer.h +++ b/src/engine/shared/packer.h @@ -20,7 +20,7 @@ class CPacker public: void Reset(); void AddInt(int i); - void AddString(const char *pStr, int Limit = PACKER_BUFFER_SIZE); + void AddString(const char *pStr, int Limit = PACKER_BUFFER_SIZE, bool AllowTruncation = true); void AddRaw(const void *pData, int Size); int Size() const { return (int)(m_pCurrent - m_aBuffer); } diff --git a/src/test/packer.cpp b/src/test/packer.cpp index 6b4e133698f..5dd76a9393b 100644 --- a/src/test/packer.cpp +++ b/src/test/packer.cpp @@ -5,24 +5,24 @@ #include // pExpected is NULL if an error is expected -static void ExpectAddString5(const char *pString, int Limit, const char *pExpected) +static void ExpectAddString5(const char *pString, int Limit, bool AllowTruncation, const char *pExpected) { static char ZEROS[CPacker::PACKER_BUFFER_SIZE] = {0}; static const int OFFSET = CPacker::PACKER_BUFFER_SIZE - 5; CPacker Packer; Packer.Reset(); Packer.AddRaw(ZEROS, OFFSET); - Packer.AddString(pString, Limit); + Packer.AddString(pString, Limit, AllowTruncation); - EXPECT_EQ(pExpected == 0, Packer.Error()); + EXPECT_EQ(pExpected == 0, Packer.Error()) << "for String='" << pString << "', Limit='" << Limit << "', AllowTruncation='" << AllowTruncation << "'"; if(pExpected) { // Include null termination. int ExpectedLength = str_length(pExpected) + 1; - EXPECT_EQ(ExpectedLength, Packer.Size() - OFFSET); + EXPECT_EQ(ExpectedLength, Packer.Size() - OFFSET) << "for String='" << pString << "', Limit='" << Limit << "', AllowTruncation='" << AllowTruncation << "'"; if(ExpectedLength == Packer.Size() - OFFSET) { - EXPECT_STREQ(pExpected, (const char *)Packer.Data() + OFFSET); + EXPECT_STREQ(pExpected, (const char *)Packer.Data() + OFFSET) << "for String='" << pString << "', Limit='" << Limit << "', AllowTruncation='" << AllowTruncation << "'"; } } } @@ -207,40 +207,63 @@ TEST(Packer, AddExtendedInt) TEST(Packer, AddString) { - ExpectAddString5("", 0, ""); - ExpectAddString5("a", 0, "a"); - ExpectAddString5("abcd", 0, "abcd"); - ExpectAddString5("abcde", 0, 0); + ExpectAddString5("", 0, true, ""); + ExpectAddString5("a", 0, true, "a"); + ExpectAddString5("abcd", 0, true, "abcd"); + ExpectAddString5("abcde", 0, true, nullptr); } TEST(Packer, AddStringLimit) { - ExpectAddString5("", 1, ""); - ExpectAddString5("a", 1, "a"); - ExpectAddString5("aa", 1, "a"); - ExpectAddString5("ä", 1, ""); + ExpectAddString5("", 1, true, ""); + ExpectAddString5("a", 1, true, "a"); + ExpectAddString5("aa", 1, true, "a"); + ExpectAddString5("ä", 1, true, ""); - ExpectAddString5("", 10, ""); - ExpectAddString5("a", 10, "a"); - ExpectAddString5("abcd", 10, "abcd"); - ExpectAddString5("abcde", 10, 0); + ExpectAddString5("", 10, true, ""); + ExpectAddString5("a", 10, true, "a"); + ExpectAddString5("abcd", 10, true, "abcd"); + ExpectAddString5("abcde", 10, true, nullptr); - ExpectAddString5("äöü", 5, "äö"); - ExpectAddString5("äöü", 6, 0); + ExpectAddString5("äöü", 4, true, "äö"); + ExpectAddString5("äöü", 5, true, "äö"); + ExpectAddString5("äöü", 6, true, nullptr); + + ExpectAddString5("", 1, false, ""); + ExpectAddString5("a", 1, false, "a"); + ExpectAddString5("aa", 1, false, nullptr); + ExpectAddString5("ä", 1, false, nullptr); + + ExpectAddString5("", 10, false, ""); + ExpectAddString5("a", 10, false, "a"); + ExpectAddString5("abcd", 10, false, "abcd"); + ExpectAddString5("abcde", 10, false, nullptr); + + ExpectAddString5("äöü", 4, false, nullptr); + ExpectAddString5("äöü", 5, false, nullptr); + ExpectAddString5("äöü", 6, false, nullptr); } TEST(Packer, AddStringBroken) { - ExpectAddString5("\x80", 0, "�"); - ExpectAddString5("\x80\x80", 0, 0); - ExpectAddString5("a\x80", 0, "a�"); + ExpectAddString5("\x80", 0, true, "�"); + ExpectAddString5("\x80\x80", 0, true, nullptr); + ExpectAddString5("a\x80", 0, true, "a�"); ExpectAddString5("\x80" "a", - 0, "�a"); - ExpectAddString5("\x80", 1, ""); - ExpectAddString5("\x80\x80", 3, "�"); - ExpectAddString5("\x80\x80", 5, "�"); - ExpectAddString5("\x80\x80", 6, 0); + 0, true, "�a"); + + ExpectAddString5("\x80", 1, true, ""); + ExpectAddString5("\x80", 3, true, "�"); + ExpectAddString5("\x80\x80", 3, true, "�"); + ExpectAddString5("\x80\x80", 5, true, "�"); + ExpectAddString5("\x80\x80", 6, true, nullptr); + + ExpectAddString5("\x80", 1, false, nullptr); + ExpectAddString5("\x80", 3, false, "�"); + ExpectAddString5("\x80\x80", 3, false, nullptr); + ExpectAddString5("\x80\x80", 5, false, nullptr); + ExpectAddString5("\x80\x80", 6, false, nullptr); } TEST(Packer, Error) From c83d79ec5c6da194b6007775cad0dd6b363d0dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Thu, 21 Nov 2024 18:18:53 +0100 Subject: [PATCH 04/58] Use separate packer with larger buffer only for Teehistorian Revert buffer size for regular `CPacker` to 2 KiB to avoid stack overflows on systems with only 1 MiB of stack memory (e.g. Windows with MSVC and Android). Add separate `CTeehistorianPacker` with a larger buffer to be used only for Teehistorian chunks. To avoid duplicate code the existing `CPacker` is renamed to `CAbstractPacker` and the buffer and its size are supplied by the concrete subclasses `CPacker` and `CTeehistorianPacker`. --- src/engine/shared/packer.cpp | 20 ++++--- src/engine/shared/packer.h | 44 ++++++++++---- src/game/server/teehistorian.cpp | 72 +++++++++++++---------- src/test/packer.cpp | 98 +++++++++++++++++--------------- 4 files changed, 141 insertions(+), 93 deletions(-) diff --git a/src/engine/shared/packer.cpp b/src/engine/shared/packer.cpp index eaab9528365..6ec54c60a41 100644 --- a/src/engine/shared/packer.cpp +++ b/src/engine/shared/packer.cpp @@ -5,14 +5,20 @@ #include "compression.h" #include "packer.h" -void CPacker::Reset() +CAbstractPacker::CAbstractPacker(unsigned char *pBuffer, size_t Size) : + m_pBuffer(pBuffer), + m_BufferSize(Size) +{ +} + +void CAbstractPacker::Reset() { m_Error = false; - m_pCurrent = m_aBuffer; - m_pEnd = m_pCurrent + PACKER_BUFFER_SIZE; + m_pCurrent = m_pBuffer; + m_pEnd = m_pCurrent + m_BufferSize; } -void CPacker::AddInt(int i) +void CAbstractPacker::AddInt(int i) { if(m_Error) return; @@ -26,7 +32,7 @@ void CPacker::AddInt(int i) m_pCurrent = pNext; } -void CPacker::AddString(const char *pStr, int Limit, bool AllowTruncation) +void CAbstractPacker::AddString(const char *pStr, int Limit, bool AllowTruncation) { if(m_Error) return; @@ -34,7 +40,7 @@ void CPacker::AddString(const char *pStr, int Limit, bool AllowTruncation) unsigned char *const pPrevCurrent = m_pCurrent; if(Limit <= 0) { - Limit = PACKER_BUFFER_SIZE; + Limit = m_BufferSize; } while(*pStr) { @@ -70,7 +76,7 @@ void CPacker::AddString(const char *pStr, int Limit, bool AllowTruncation) *m_pCurrent++ = '\0'; } -void CPacker::AddRaw(const void *pData, int Size) +void CAbstractPacker::AddRaw(const void *pData, int Size) { if(m_Error) return; diff --git a/src/engine/shared/packer.h b/src/engine/shared/packer.h index d69a5ce1b2c..d08b74c7bb5 100644 --- a/src/engine/shared/packer.h +++ b/src/engine/shared/packer.h @@ -3,31 +3,53 @@ #ifndef ENGINE_SHARED_PACKER_H #define ENGINE_SHARED_PACKER_H -class CPacker -{ -public: - enum - { - PACKER_BUFFER_SIZE = 1024 * 64 - }; +#include +/** + * Abstract packer implementation. Subclasses must supply the buffer. + */ +class CAbstractPacker +{ private: - unsigned char m_aBuffer[PACKER_BUFFER_SIZE]; + unsigned char *const m_pBuffer; + const size_t m_BufferSize; unsigned char *m_pCurrent; unsigned char *m_pEnd; bool m_Error; +protected: + CAbstractPacker(unsigned char *pBuffer, size_t Size); + public: void Reset(); void AddInt(int i); - void AddString(const char *pStr, int Limit = PACKER_BUFFER_SIZE, bool AllowTruncation = true); + void AddString(const char *pStr, int Limit = 0, bool AllowTruncation = true); void AddRaw(const void *pData, int Size); - int Size() const { return (int)(m_pCurrent - m_aBuffer); } - const unsigned char *Data() const { return m_aBuffer; } + int Size() const { return (int)(m_pCurrent - m_pBuffer); } + const unsigned char *Data() const { return m_pBuffer; } bool Error() const { return m_Error; } }; +/** + * Default packer with buffer for networking. + */ +class CPacker : public CAbstractPacker +{ +public: + enum + { + PACKER_BUFFER_SIZE = 1024 * 2 + }; + CPacker() : + CAbstractPacker(m_aBuffer, sizeof(m_aBuffer)) + { + } + +private: + unsigned char m_aBuffer[PACKER_BUFFER_SIZE]; +}; + class CUnpacker { const unsigned char *m_pStart; diff --git a/src/game/server/teehistorian.cpp b/src/game/server/teehistorian.cpp index 16c43b29086..c2e67c5f0e7 100644 --- a/src/game/server/teehistorian.cpp +++ b/src/game/server/teehistorian.cpp @@ -1,12 +1,26 @@ #include "teehistorian.h" #include -#include + #include #include +#include #include + #include +class CTeehistorianPacker : public CAbstractPacker +{ +public: + CTeehistorianPacker() : + CAbstractPacker(m_aBuffer, sizeof(m_aBuffer)) + { + } + +private: + unsigned char m_aBuffer[1024 * 64]; +}; + static const char TEEHISTORIAN_NAME[] = "teehistorian@ddnet.tw"; static const CUuid TEEHISTORIAN_UUID = CalculateUuid(TEEHISTORIAN_NAME); static const char TEEHISTORIAN_VERSION[] = "2"; @@ -215,7 +229,7 @@ void CTeeHistorian::WriteExtra(CUuid Uuid, const void *pData, int DataSize) { EnsureTickWritten(); - CPacker Ex; + CTeehistorianPacker Ex; Ex.Reset(); Ex.AddInt(-TEEHISTORIAN_EX); Ex.AddRaw(&Uuid, sizeof(Uuid)); @@ -277,7 +291,7 @@ void CTeeHistorian::RecordPlayer(int ClientId, const CNetObj_CharacterCore *pCha { EnsureTickWrittenPlayerData(ClientId); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); if(pPrev->m_Alive) { @@ -320,7 +334,7 @@ void CTeeHistorian::RecordDeadPlayer(int ClientId) { EnsureTickWrittenPlayerData(ClientId); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_PLAYER_OLD); Buffer.AddInt(ClientId); @@ -341,7 +355,7 @@ void CTeeHistorian::RecordPlayerTeam(int ClientId, int Team) EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); Buffer.AddInt(Team); @@ -363,7 +377,7 @@ void CTeeHistorian::RecordTeamPractice(int Team, bool Practice) EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(Team); Buffer.AddInt(Practice); @@ -392,7 +406,7 @@ void CTeeHistorian::EnsureTickWritten() void CTeeHistorian::WriteTick() { - CPacker TickPacker; + CTeehistorianPacker TickPacker; TickPacker.Reset(); int dt = m_Tick - m_LastWrittenTick - 1; @@ -424,7 +438,7 @@ void CTeeHistorian::BeginInputs() void CTeeHistorian::RecordPlayerInput(int ClientId, uint32_t UniqueClientId, const CNetObj_PlayerInput *pInput) { - CPacker Buffer; + CTeehistorianPacker Buffer; CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientId]; CNetObj_PlayerInput DiffInput; @@ -473,7 +487,7 @@ void CTeeHistorian::RecordPlayerMessage(int ClientId, const void *pMsg, int MsgS { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_MESSAGE); Buffer.AddInt(ClientId); @@ -499,7 +513,7 @@ void CTeeHistorian::RecordPlayerJoin(int ClientId, int Protocol) EnsureTickWritten(); { - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); if(m_Debug) @@ -510,7 +524,7 @@ void CTeeHistorian::RecordPlayerJoin(int ClientId, int Protocol) WriteExtra(Uuid, Buffer.Data(), Buffer.Size()); } - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_JOIN); Buffer.AddInt(ClientId); @@ -527,7 +541,7 @@ void CTeeHistorian::RecordPlayerRejoin(int ClientId) { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); @@ -543,7 +557,7 @@ void CTeeHistorian::RecordPlayerReady(int ClientId) { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); @@ -559,7 +573,7 @@ void CTeeHistorian::RecordPlayerDrop(int ClientId, const char *pReason) { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_DROP); Buffer.AddInt(ClientId); @@ -577,7 +591,7 @@ void CTeeHistorian::RecordPlayerName(int ClientId, const char *pName) { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); Buffer.AddString(pName, 0); @@ -594,7 +608,7 @@ void CTeeHistorian::RecordConsoleCommand(int ClientId, int FlagMask, const char { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_CONSOLE_COMMAND); Buffer.AddInt(ClientId); @@ -628,7 +642,7 @@ void CTeeHistorian::RecordPlayerSwap(int ClientId1, int ClientId2) { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId1); Buffer.AddInt(ClientId2); @@ -640,7 +654,7 @@ void CTeeHistorian::RecordTeamSaveSuccess(int Team, CUuid SaveId, const char *pT { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(Team); Buffer.AddRaw(&SaveId, sizeof(SaveId)); @@ -660,7 +674,7 @@ void CTeeHistorian::RecordTeamSaveFailure(int Team) { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(Team); @@ -676,7 +690,7 @@ void CTeeHistorian::RecordTeamLoadSuccess(int Team, CUuid SaveId, const char *pT { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(Team); Buffer.AddRaw(&SaveId, sizeof(SaveId)); @@ -696,7 +710,7 @@ void CTeeHistorian::RecordTeamLoadFailure(int Team) { EnsureTickWritten(); - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(Team); @@ -723,7 +737,7 @@ void CTeeHistorian::EndTick() void CTeeHistorian::RecordDDNetVersionOld(int ClientId, int DDNetVersion) { - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); Buffer.AddInt(DDNetVersion); @@ -738,7 +752,7 @@ void CTeeHistorian::RecordDDNetVersionOld(int ClientId, int DDNetVersion) void CTeeHistorian::RecordDDNetVersion(int ClientId, CUuid ConnectionId, int DDNetVersion, const char *pDDNetVersionStr) { - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); Buffer.AddRaw(&ConnectionId, sizeof(ConnectionId)); @@ -757,7 +771,7 @@ void CTeeHistorian::RecordDDNetVersion(int ClientId, CUuid ConnectionId, int DDN void CTeeHistorian::RecordAuthInitial(int ClientId, int Level, const char *pAuthName) { - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); Buffer.AddInt(Level); @@ -773,7 +787,7 @@ void CTeeHistorian::RecordAuthInitial(int ClientId, int Level, const char *pAuth void CTeeHistorian::RecordAuthLogin(int ClientId, int Level, const char *pAuthName) { - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); Buffer.AddInt(Level); @@ -789,7 +803,7 @@ void CTeeHistorian::RecordAuthLogin(int ClientId, int Level, const char *pAuthNa void CTeeHistorian::RecordAuthLogout(int ClientId) { - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); @@ -813,7 +827,7 @@ void CTeeHistorian::RecordAntibot(const void *pData, int DataSize) void CTeeHistorian::RecordPlayerFinish(int ClientId, int TimeTicks) { - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(ClientId); Buffer.AddInt(TimeTicks); @@ -827,7 +841,7 @@ void CTeeHistorian::RecordPlayerFinish(int ClientId, int TimeTicks) void CTeeHistorian::RecordTeamFinish(int TeamId, int TimeTicks) { - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(TeamId); Buffer.AddInt(TimeTicks); @@ -852,7 +866,7 @@ void CTeeHistorian::Finish() EndTick(); } - CPacker Buffer; + CTeehistorianPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_FINISH); diff --git a/src/test/packer.cpp b/src/test/packer.cpp index 5dd76a9393b..90a3a7cad15 100644 --- a/src/test/packer.cpp +++ b/src/test/packer.cpp @@ -266,56 +266,62 @@ TEST(Packer, AddStringBroken) ExpectAddString5("\x80\x80", 6, false, nullptr); } -TEST(Packer, Error) +TEST(Packer, Error1) { char aData[CPacker::PACKER_BUFFER_SIZE]; mem_zero(aData, sizeof(aData)); - { - CPacker Packer; - Packer.Reset(); - EXPECT_EQ(Packer.Error(), false); - Packer.AddRaw(aData, sizeof(aData) - 1); - EXPECT_EQ(Packer.Error(), false); - EXPECT_EQ(Packer.Size(), sizeof(aData) - 1); - Packer.AddInt(1); - EXPECT_EQ(Packer.Error(), false); - EXPECT_EQ(Packer.Size(), sizeof(aData)); - Packer.AddInt(2); - EXPECT_EQ(Packer.Error(), true); - Packer.AddInt(3); - EXPECT_EQ(Packer.Error(), true); - } + CPacker Packer; + Packer.Reset(); + EXPECT_EQ(Packer.Error(), false); + Packer.AddRaw(aData, sizeof(aData) - 1); + EXPECT_EQ(Packer.Error(), false); + EXPECT_EQ(Packer.Size(), sizeof(aData) - 1); + Packer.AddInt(1); + EXPECT_EQ(Packer.Error(), false); + EXPECT_EQ(Packer.Size(), sizeof(aData)); + Packer.AddInt(2); + EXPECT_EQ(Packer.Error(), true); + Packer.AddInt(3); + EXPECT_EQ(Packer.Error(), true); +} - { - CPacker Packer; - Packer.Reset(); - EXPECT_EQ(Packer.Error(), false); - Packer.AddRaw(aData, sizeof(aData) - 1); - EXPECT_EQ(Packer.Error(), false); - EXPECT_EQ(Packer.Size(), sizeof(aData) - 1); - Packer.AddRaw(aData, 1); - EXPECT_EQ(Packer.Error(), false); - EXPECT_EQ(Packer.Size(), sizeof(aData)); - Packer.AddRaw(aData, 1); - EXPECT_EQ(Packer.Error(), true); - Packer.AddRaw(aData, 1); - EXPECT_EQ(Packer.Error(), true); - } +TEST(Packer, Error2) +{ + char aData[CPacker::PACKER_BUFFER_SIZE]; + mem_zero(aData, sizeof(aData)); - { - CPacker Packer; - Packer.Reset(); - EXPECT_EQ(Packer.Error(), false); - Packer.AddRaw(aData, sizeof(aData) - 5); - EXPECT_EQ(Packer.Error(), false); - EXPECT_EQ(Packer.Size(), sizeof(aData) - 5); - Packer.AddString("test"); - EXPECT_EQ(Packer.Error(), false); - EXPECT_EQ(Packer.Size(), sizeof(aData)); - Packer.AddString("test"); - EXPECT_EQ(Packer.Error(), true); - Packer.AddString("test"); - EXPECT_EQ(Packer.Error(), true); - } + CPacker Packer; + Packer.Reset(); + EXPECT_EQ(Packer.Error(), false); + Packer.AddRaw(aData, sizeof(aData) - 1); + EXPECT_EQ(Packer.Error(), false); + EXPECT_EQ(Packer.Size(), sizeof(aData) - 1); + Packer.AddRaw(aData, 1); + EXPECT_EQ(Packer.Error(), false); + EXPECT_EQ(Packer.Size(), sizeof(aData)); + Packer.AddRaw(aData, 1); + EXPECT_EQ(Packer.Error(), true); + Packer.AddRaw(aData, 1); + EXPECT_EQ(Packer.Error(), true); +} + +TEST(Packer, Error3) +{ + char aData[CPacker::PACKER_BUFFER_SIZE]; + mem_zero(aData, sizeof(aData)); + + CPacker Packer; + Packer.Reset(); + EXPECT_EQ(Packer.Error(), false); + Packer.AddRaw(aData, sizeof(aData) - 5); + EXPECT_EQ(Packer.Error(), false); + EXPECT_EQ(Packer.Size(), sizeof(aData) - 5); + Packer.AddString("test"); + EXPECT_EQ(Packer.Error(), false); + EXPECT_EQ(Packer.Size(), sizeof(aData)); + Packer.AddString("test"); + EXPECT_EQ(Packer.Error(), true); + Packer.AddString("test"); + EXPECT_EQ(Packer.Error(), true); } From ee61be2d83c24b40c1f3e5df24e31c4e03f42388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sun, 24 Nov 2024 14:48:44 +0100 Subject: [PATCH 05/58] Show size of images in editor images list and file browser --- src/game/editor/editor.cpp | 37 ++++++++++++++++++++++++++++++------- src/game/editor/editor.h | 1 + 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 9c85e7a9968..1175c29f774 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -1406,6 +1406,20 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) } } +void CEditor::DoToolbarImages(CUIRect ToolBar) +{ + CUIRect ToolBarTop, ToolBarBottom; + ToolBar.HSplitMid(&ToolBarTop, &ToolBarBottom, 5.0f); + + if(m_SelectedImage >= 0 && (size_t)m_SelectedImage < m_Map.m_vpImages.size()) + { + const std::shared_ptr pSelectedImage = m_Map.m_vpImages[m_SelectedImage]; + char aLabel[64]; + str_format(aLabel, sizeof(aLabel), "Size: %" PRIzu " × %" PRIzu, pSelectedImage->m_Width, pSelectedImage->m_Height); + Ui()->DoLabel(&ToolBarBottom, aLabel, 12.0f, TEXTALIGN_ML); + } +} + void CEditor::DoToolbarSounds(CUIRect ToolBar) { CUIRect ToolBarTop, ToolBarBottom; @@ -5275,23 +5289,30 @@ void CEditor::RenderFileDialog() Preview.Margin(10.0f, &Preview); if(m_FilePreviewState == PREVIEW_LOADED) { + CUIRect PreviewLabel, PreviewImage; + Preview.HSplitTop(20.0f, &PreviewLabel, &PreviewImage); + + char aLabel[64]; + str_format(aLabel, sizeof(aLabel), "Size: %d × %d", m_FilePreviewImageWidth, m_FilePreviewImageHeight); + Ui()->DoLabel(&PreviewLabel, aLabel, 12.0f, TEXTALIGN_ML); + int w = m_FilePreviewImageWidth; int h = m_FilePreviewImageHeight; - if(m_FilePreviewImageWidth > Preview.w) + if(m_FilePreviewImageWidth > PreviewImage.w) { - h = m_FilePreviewImageHeight * Preview.w / m_FilePreviewImageWidth; - w = Preview.w; + h = m_FilePreviewImageHeight * PreviewImage.w / m_FilePreviewImageWidth; + w = PreviewImage.w; } - if(h > Preview.h) + if(h > PreviewImage.h) { - w = w * Preview.h / h; - h = Preview.h; + w = w * PreviewImage.h / h; + h = PreviewImage.h; } Graphics()->TextureSet(m_FilePreviewImage); Graphics()->BlendNormal(); Graphics()->QuadsBegin(); - IGraphics::CQuadItem QuadItem(Preview.x, Preview.y, w, h); + IGraphics::CQuadItem QuadItem(PreviewImage.x, PreviewImage.y, w, h); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } @@ -7934,6 +7955,8 @@ void CEditor::Render() // do the toolbar if(m_Mode == MODE_LAYERS) DoToolbarLayers(ToolBar); + else if(m_Mode == MODE_IMAGES) + DoToolbarImages(ToolBar); else if(m_Mode == MODE_SOUNDS) DoToolbarSounds(ToolBar); diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index a32c9d7ca4f..45e34dfdfcc 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -973,6 +973,7 @@ class CEditor : public IEditor }; void DoMapEditor(CUIRect View); void DoToolbarLayers(CUIRect Toolbar); + void DoToolbarImages(CUIRect Toolbar); void DoToolbarSounds(CUIRect Toolbar); void DoQuad(int LayerIndex, const std::shared_ptr &pLayer, CQuad *pQuad, int Index); void PreparePointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex); From 0c37800e443b78bf063f4d6d70570ba4ae547452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Tue, 26 Nov 2024 21:01:00 +0100 Subject: [PATCH 06/58] Remove unused graphics functions --- src/engine/client/graphics_threaded.h | 10 ---------- src/engine/graphics.h | 15 --------------- 2 files changed, 25 deletions(-) diff --git a/src/engine/client/graphics_threaded.h b/src/engine/client/graphics_threaded.h index 1a7b023eb74..06b841c3c5e 100644 --- a/src/engine/client/graphics_threaded.h +++ b/src/engine/client/graphics_threaded.h @@ -1107,16 +1107,6 @@ class CGraphics_Threaded : public IEngineGraphics void DrawRect4(float x, float y, float w, float h, ColorRGBA ColorTopLeft, ColorRGBA ColorTopRight, ColorRGBA ColorBottomLeft, ColorRGBA ColorBottomRight, int Corners, float Rounding) override; void DrawCircle(float CenterX, float CenterY, float Radius, int Segments) override; - const GL_STexCoord *GetCurTextureCoordinates() override - { - return m_aTexture; - } - - const GL_SColor *GetCurColor() override - { - return m_aColor; - } - int CreateQuadContainer(bool AutomaticUpload = true) override; void QuadContainerChangeAutomaticUpload(int ContainerIndex, bool AutomaticUpload) override; void QuadContainerUpload(int ContainerIndex) override; diff --git a/src/engine/graphics.h b/src/engine/graphics.h index 610f46e3082..a4f44119552 100644 --- a/src/engine/graphics.h +++ b/src/engine/graphics.h @@ -369,27 +369,12 @@ class IGraphics : public IInterface CQuadItem() {} CQuadItem(float x, float y, float w, float h) : m_X(x), m_Y(y), m_Width(w), m_Height(h) {} - void Set(float x, float y, float w, float h) - { - m_X = x; - m_Y = y; - m_Width = w; - m_Height = h; - } - - CFreeformItem ToFreeForm() const - { - return CFreeformItem(m_X, m_Y, m_X + m_Width, m_Y, m_X, m_Y + m_Height, m_X + m_Width, m_Y + m_Height); - } }; virtual void QuadsDraw(CQuadItem *pArray, int Num) = 0; virtual void QuadsDrawTL(const CQuadItem *pArray, int Num) = 0; virtual void QuadsTex3DDrawTL(const CQuadItem *pArray, int Num) = 0; - virtual const GL_STexCoord *GetCurTextureCoordinates() = 0; - virtual const GL_SColor *GetCurColor() = 0; - virtual int CreateQuadContainer(bool AutomaticUpload = true) = 0; virtual void QuadContainerChangeAutomaticUpload(int ContainerIndex, bool AutomaticUpload) = 0; virtual void QuadContainerUpload(int ContainerIndex) = 0; From 62ef7ed711959fc366bd610508573705ae8c98d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Tue, 26 Nov 2024 21:23:43 +0100 Subject: [PATCH 07/58] Fix gametime debug graph when dummy is connected Use separate graphs for the main and dummy gametimes to prevent the graph from using an incorrect time scale due to values for both main and dummy connection being added to it. --- src/engine/client/client.cpp | 12 +++++++----- src/engine/client/client.h | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index d334c0613e4..236331f6d21 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -86,7 +86,7 @@ static const ColorRGBA gs_ClientNetworkErrPrintColor{1.0f, 0.25f, 0.25f, 1.0f}; CClient::CClient() : m_DemoPlayer(&m_SnapshotDelta, true, [&]() { UpdateDemoIntraTimers(); }), m_InputtimeMarginGraph(128), - m_GametimeMarginGraph(128), + m_aGametimeMarginGraphs{128, 128}, m_FpsGraph(4096) { m_StateStartTime = time_get(); @@ -634,7 +634,7 @@ void CClient::Connect(const char *pAddress, const char *pPassword) SetState(IClient::STATE_CONNECTING); m_InputtimeMarginGraph.Init(-150.0f, 150.0f); - m_GametimeMarginGraph.Init(-150.0f, 150.0f); + m_aGametimeMarginGraphs[CONN_MAIN].Init(-150.0f, 150.0f); GenerateTimeoutCodes(aConnectAddrs, NumConnectAddrs); } @@ -753,6 +753,8 @@ void CClient::DummyConnect() m_aNetClient[CONN_DUMMY].Connect7(m_aNetClient[CONN_MAIN].ServerAddress(), 1); else m_aNetClient[CONN_DUMMY].Connect(m_aNetClient[CONN_MAIN].ServerAddress(), 1); + + m_aGametimeMarginGraphs[CONN_DUMMY].Init(-150.0f, 150.0f); } void CClient::DummyDisconnect(const char *pReason) @@ -963,8 +965,8 @@ void CClient::DebugRender() m_FpsGraph.Render(Graphics(), TextRender(), x, sp * 5, w, h, "FPS"); m_InputtimeMarginGraph.Scale(5 * time_freq()); m_InputtimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 6 + h, w, h, "Prediction Margin"); - m_GametimeMarginGraph.Scale(5 * time_freq()); - m_GametimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 7 + h * 2, w, h, "Gametime Margin"); + m_aGametimeMarginGraphs[g_Config.m_ClDummy].Scale(5 * time_freq()); + m_aGametimeMarginGraphs[g_Config.m_ClDummy].Render(Graphics(), TextRender(), x, sp * 7 + h * 2, w, h, "Gametime Margin"); } } @@ -2085,7 +2087,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) int64_t Now = m_aGameTime[Conn].Get(time_get()); int64_t TickStart = GameTick * time_freq() / GameTickSpeed(); int64_t TimeLeft = (TickStart - Now) * 1000 / time_freq(); - m_aGameTime[Conn].Update(&m_GametimeMarginGraph, (GameTick - 1) * time_freq() / GameTickSpeed(), TimeLeft, CSmoothTime::ADJUSTDIRECTION_DOWN); + m_aGameTime[Conn].Update(&m_aGametimeMarginGraphs[Conn], (GameTick - 1) * time_freq() / GameTickSpeed(), TimeLeft, CSmoothTime::ADJUSTDIRECTION_DOWN); } if(m_aReceivedSnapshots[Conn] > GameTickSpeed() && !m_aCodeRunAfterJoin[Conn]) diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 9ca2699fd76..56e92ac05cf 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -187,7 +187,7 @@ class CClient : public IClient, public CDemoPlayer::IListener // graphs CGraph m_InputtimeMarginGraph; - CGraph m_GametimeMarginGraph; + CGraph m_aGametimeMarginGraphs[NUM_DUMMIES]; CGraph m_FpsGraph; // the game snapshots are modifiable by the game From 40aa83f74e6a22d93da6b3ccc37e3baebbc1648f Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Wed, 27 Nov 2024 23:08:55 +0800 Subject: [PATCH 08/58] Allow decimal value for demo_speed command --- src/engine/client/client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 731227b151a..719987b6db5 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -4373,7 +4373,7 @@ void CClient::RegisterCommands() m_pConsole->Register("demo_slice_start", "", CFGFLAG_CLIENT, Con_DemoSliceBegin, this, "Mark the beginning of a demo cut"); m_pConsole->Register("demo_slice_end", "", CFGFLAG_CLIENT, Con_DemoSliceEnd, this, "Mark the end of a demo cut"); m_pConsole->Register("demo_play", "", CFGFLAG_CLIENT, Con_DemoPlay, this, "Play/pause the current demo"); - m_pConsole->Register("demo_speed", "i[speed]", CFGFLAG_CLIENT, Con_DemoSpeed, this, "Set current demo speed"); + m_pConsole->Register("demo_speed", "f[speed]", CFGFLAG_CLIENT, Con_DemoSpeed, this, "Set current demo speed"); m_pConsole->Register("save_replay", "?i[length] ?r[filename]", CFGFLAG_CLIENT, Con_SaveReplay, this, "Save a replay of the last defined amount of seconds"); m_pConsole->Register("benchmark_quit", "i[seconds] r[file]", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_BenchmarkQuit, this, "Benchmark frame times for number of seconds to file, then quit"); From 68f1bf5e95c886f1e74be6d749081d6958d38d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Wed, 27 Nov 2024 20:25:16 +0100 Subject: [PATCH 09/58] Fix comment about resending frequency for unchanged input The resending frequency for unchanged inputs was changed from 10 Hz to 25 Hz in 24899a13e8863e59027e993a61e9b2ef6787b74b but the comment was not adjusted accordingly. --- src/game/client/components/controls.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp index 2ee2fe3db52..07c56d9c6f9 100644 --- a/src/game/client/components/controls.cpp +++ b/src/game/client/components/controls.cpp @@ -311,7 +311,7 @@ int CControls::SnapInput(int *pData) Send = Send || m_aInputData[g_Config.m_ClDummy].m_WantedWeapon != m_aLastData[g_Config.m_ClDummy].m_WantedWeapon; Send = Send || m_aInputData[g_Config.m_ClDummy].m_NextWeapon != m_aLastData[g_Config.m_ClDummy].m_NextWeapon; Send = Send || m_aInputData[g_Config.m_ClDummy].m_PrevWeapon != m_aLastData[g_Config.m_ClDummy].m_PrevWeapon; - Send = Send || time_get() > m_LastSendTime + time_freq() / 25; // send at least 10hz + Send = Send || time_get() > m_LastSendTime + time_freq() / 25; // send at least 25 Hz Send = Send || (m_pClient->m_Snap.m_pLocalCharacter && m_pClient->m_Snap.m_pLocalCharacter->m_Weapon == WEAPON_NINJA && (m_aInputData[g_Config.m_ClDummy].m_Direction || m_aInputData[g_Config.m_ClDummy].m_Jump || m_aInputData[g_Config.m_ClDummy].m_Hook)); } From b5fa4029b40ce96df3cff9ab2a4ecaffb6831cd0 Mon Sep 17 00:00:00 2001 From: SollyBunny Date: Thu, 28 Nov 2024 06:24:14 +0000 Subject: [PATCH 10/58] Allow float zoom --- src/game/client/components/camera.cpp | 23 ++++++++++++++--------- src/game/client/gameclient.cpp | 4 ++-- src/game/client/gameclient.h | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/game/client/components/camera.cpp b/src/game/client/components/camera.cpp index d57840a0dcc..2abf3af2cc2 100644 --- a/src/game/client/components/camera.cpp +++ b/src/game/client/components/camera.cpp @@ -276,9 +276,9 @@ void CCamera::OnRender() void CCamera::OnConsoleInit() { - Console()->Register("zoom+", "", CFGFLAG_CLIENT, ConZoomPlus, this, "Zoom increase"); - Console()->Register("zoom-", "", CFGFLAG_CLIENT, ConZoomMinus, this, "Zoom decrease"); - Console()->Register("zoom", "?i", CFGFLAG_CLIENT, ConZoom, this, "Change zoom"); + Console()->Register("zoom+", "?f[amount]", CFGFLAG_CLIENT, ConZoomPlus, this, "Zoom increase"); + Console()->Register("zoom-", "?f[amount]", CFGFLAG_CLIENT, ConZoomMinus, this, "Zoom decrease"); + Console()->Register("zoom", "?f", CFGFLAG_CLIENT, ConZoom, this, "Change zoom"); Console()->Register("set_view", "i[x]i[y]", CFGFLAG_CLIENT, ConSetView, this, "Set camera position to x and y in the map"); Console()->Register("set_view_relative", "i[x]i[y]", CFGFLAG_CLIENT, ConSetViewRelative, this, "Set camera position relative to current view in the map"); Console()->Register("goto_switch", "i[number]?i[offset]", CFGFLAG_CLIENT, ConGotoSwitch, this, "View switch found (at offset) with given number"); @@ -299,10 +299,12 @@ void CCamera::ConZoomPlus(IConsole::IResult *pResult, void *pUserData) if(!pSelf->ZoomAllowed()) return; - pSelf->ScaleZoom(CCamera::ZOOM_STEP); + float ZoomAmount = pResult->NumArguments() ? pResult->GetFloat(0) : 1.0f; + + pSelf->ScaleZoom(std::pow(CCamera::ZOOM_STEP, ZoomAmount)); if(pSelf->GameClient()->m_MultiViewActivated) - pSelf->GameClient()->m_MultiViewPersonalZoom++; + pSelf->GameClient()->m_MultiViewPersonalZoom += ZoomAmount; } void CCamera::ConZoomMinus(IConsole::IResult *pResult, void *pUserData) { @@ -310,10 +312,13 @@ void CCamera::ConZoomMinus(IConsole::IResult *pResult, void *pUserData) if(!pSelf->ZoomAllowed()) return; - pSelf->ScaleZoom(1 / CCamera::ZOOM_STEP); + float ZoomAmount = pResult->NumArguments() ? pResult->GetFloat(0) : 1.0f; + ZoomAmount *= -1.0f; + + pSelf->ScaleZoom(std::pow(CCamera::ZOOM_STEP, ZoomAmount)); if(pSelf->GameClient()->m_MultiViewActivated) - pSelf->GameClient()->m_MultiViewPersonalZoom--; + pSelf->GameClient()->m_MultiViewPersonalZoom += ZoomAmount; } void CCamera::ConZoom(IConsole::IResult *pResult, void *pUserData) { @@ -322,10 +327,10 @@ void CCamera::ConZoom(IConsole::IResult *pResult, void *pUserData) return; float TargetLevel = pResult->NumArguments() ? pResult->GetFloat(0) : g_Config.m_ClDefaultZoom; - pSelf->ChangeZoom(std::pow(CCamera::ZOOM_STEP, TargetLevel - 10), pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active && pSelf->GameClient()->m_MultiViewActivated ? g_Config.m_ClMultiViewZoomSmoothness : g_Config.m_ClSmoothZoomTime); + pSelf->ChangeZoom(std::pow(CCamera::ZOOM_STEP, TargetLevel - 10.0f), pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active && pSelf->GameClient()->m_MultiViewActivated ? g_Config.m_ClMultiViewZoomSmoothness : g_Config.m_ClSmoothZoomTime); if(pSelf->GameClient()->m_MultiViewActivated && pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active) - pSelf->GameClient()->m_MultiViewPersonalZoom = 0; + pSelf->GameClient()->m_MultiViewPersonalZoom = TargetLevel - 10.0f; } void CCamera::ConSetView(IConsole::IResult *pResult, void *pUserData) { diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 7a3ebda2057..fbdb7eb2786 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -647,7 +647,7 @@ void CGameClient::OnReset() m_LastScreenAspect = 0.0f; m_LastDummyConnected = false; - m_MultiViewPersonalZoom = 0; + m_MultiViewPersonalZoom = 0.0f; m_MultiViewActivated = false; m_MultiView.m_IsInit = false; @@ -4221,7 +4221,7 @@ float CGameClient::MapValue(float MaxValue, float MinValue, float MaxRange, floa void CGameClient::ResetMultiView() { m_Camera.SetZoom(std::pow(CCamera::ZOOM_STEP, g_Config.m_ClDefaultZoom - 10), g_Config.m_ClSmoothZoomTime); - m_MultiViewPersonalZoom = 0; + m_MultiViewPersonalZoom = 0.0f; m_MultiViewActivated = false; m_MultiView.m_Solo = false; m_MultiView.m_IsInit = false; diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 8bda5b80e71..665049e947b 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -780,7 +780,7 @@ class CGameClient : public IGameClient const std::vector &SnapEntities() { return m_vSnapEntities; } int m_MultiViewTeam; - int m_MultiViewPersonalZoom; + float m_MultiViewPersonalZoom; bool m_MultiViewShowHud; bool m_MultiViewActivated; bool m_aMultiViewId[MAX_CLIENTS]; From 07ca6d280a5471e7d4ee5b9fbc80a4fad608c0d2 Mon Sep 17 00:00:00 2001 From: Valentin Bashkirov Date: Thu, 28 Nov 2024 11:51:07 +0100 Subject: [PATCH 11/58] increase register content limit to 32 KiB --- src/mastersrv/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mastersrv/src/main.rs b/src/mastersrv/src/main.rs index c53fae69ff9..d19f48c8488 100644 --- a/src/mastersrv/src/main.rs +++ b/src/mastersrv/src/main.rs @@ -1039,7 +1039,7 @@ async fn main() { .and(warp::post()) .and(warp::header::headers_cloned()) .and(warp::addr::remote()) - .and(warp::body::content_length_limit(16 * 1024)) // limit body size to 16 KiB + .and(warp::body::content_length_limit(32 * 1024)) // limit body size to 32 KiB .and(warp::body::bytes()) .map( move |headers: warp::http::HeaderMap, addr: Option, info: bytes::Bytes| { From b60e75dfec1717fb7daea2cd93ddb38d75c778a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sun, 29 Sep 2024 15:15:18 +0200 Subject: [PATCH 12/58] Minor refactoring of server announcement loading Move `reload_announcement` command handling from `CGameContext` to `CServer`. This is consistent with the conchain for `sv_announcement_filename` also being registered in `CServer`. Furthermore, this allows making the `ReadAnnouncementsFile` function non-`virtual` as it does not need to be exposed to the gameserver anymore. Remove unnecessary filename argument of `ReadAnnouncementsFile` function, which is always set to the config variable. Log number of loaded announcements on success. Use error log level on error. --- src/engine/server.h | 1 - src/engine/server/server.cpp | 23 ++++++++++++++++------- src/engine/server/server.h | 4 +++- src/game/server/ddracecommands.cpp | 6 ------ src/game/server/gamecontext.cpp | 1 - src/game/server/gamecontext.h | 1 - 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/engine/server.h b/src/engine/server.h index f6d950e5f29..a459f8d8a5e 100644 --- a/src/engine/server.h +++ b/src/engine/server.h @@ -273,7 +273,6 @@ class IServer : public IInterface virtual bool DnsblPending(int ClientId) = 0; virtual bool DnsblBlack(int ClientId) = 0; virtual const char *GetAnnouncementLine() = 0; - virtual void ReadAnnouncementsFile(const char *pFileName) = 0; virtual bool ClientPrevIngame(int ClientId) = 0; virtual const char *GetNetErrorString(int ClientId) = 0; virtual void ResetNetErrorString(int ClientId) = 0; diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 19d00781d7e..b2c0e93b67e 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -2773,7 +2773,7 @@ int CServer::Run() Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); } - ReadAnnouncementsFile(g_Config.m_SvAnnouncementFileName); + ReadAnnouncementsFile(); // process pending commands m_pConsole->StoreCommands(false); @@ -3618,6 +3618,12 @@ void CServer::ConDumpSqlServers(IConsole::IResult *pResult, void *pUserData) } } +void CServer::ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData) +{ + CServer *pThis = static_cast(pUserData); + pThis->ReadAnnouncementsFile(); +} + void CServer::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { pfnCallback(pResult, pCallbackUserData); @@ -3809,7 +3815,7 @@ void CServer::ConchainAnnouncementFileName(IConsole::IResult *pResult, void *pUs pfnCallback(pResult, pCallbackUserData); if(Changed) { - pSelf->ReadAnnouncementsFile(g_Config.m_SvAnnouncementFileName); + pSelf->ReadAnnouncementsFile(); } } @@ -3873,6 +3879,8 @@ void CServer::RegisterCommands() Console()->Register("auth_remove", "s[ident]", CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConAuthRemove, this, "Remove a rcon key"); Console()->Register("auth_list", "", CFGFLAG_SERVER, ConAuthList, this, "List all rcon keys"); + Console()->Register("reload_announcement", "", CFGFLAG_SERVER, ConReloadAnnouncement, this, "Reload the announcements"); + RustVersionRegister(*Console()); Console()->Chain("sv_name", ConchainSpecialInfoupdate, this); @@ -3936,17 +3944,17 @@ void CServer::GetClientAddr(int ClientId, NETADDR *pAddr) const } } -void CServer::ReadAnnouncementsFile(const char *pFileName) +void CServer::ReadAnnouncementsFile() { m_vAnnouncements.clear(); - if(pFileName[0] == '\0') + if(g_Config.m_SvAnnouncementFileName[0] == '\0') return; CLineReader LineReader; - if(!LineReader.OpenFile(m_pStorage->OpenFile(pFileName, IOFLAG_READ, IStorage::TYPE_ALL))) + if(!LineReader.OpenFile(m_pStorage->OpenFile(g_Config.m_SvAnnouncementFileName, IOFLAG_READ, IStorage::TYPE_ALL))) { - dbg_msg("announcements", "failed to open '%s'", pFileName); + log_error("server", "Failed load announcements from '%s'", g_Config.m_SvAnnouncementFileName); return; } while(const char *pLine = LineReader.Get()) @@ -3956,13 +3964,14 @@ void CServer::ReadAnnouncementsFile(const char *pFileName) m_vAnnouncements.emplace_back(pLine); } } + log_info("server", "Loaded %" PRIzu " announcements", m_vAnnouncements.size()); } const char *CServer::GetAnnouncementLine() { if(m_vAnnouncements.empty()) { - return 0; + return nullptr; } else if(m_vAnnouncements.size() == 1) { diff --git a/src/engine/server/server.h b/src/engine/server/server.h index 9e5b2c2e48f..36ba63a42ac 100644 --- a/src/engine/server/server.h +++ b/src/engine/server/server.h @@ -411,6 +411,8 @@ class CServer : public IServer static void ConAddSqlServer(IConsole::IResult *pResult, void *pUserData); static void ConDumpSqlServers(IConsole::IResult *pResult, void *pUserData); + static void ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData); + static void ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainMaxclientsperipUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainCommandAccessUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); @@ -444,7 +446,7 @@ class CServer : public IServer void GetClientAddr(int ClientId, NETADDR *pAddr) const override; int m_aPrevStates[MAX_CLIENTS]; const char *GetAnnouncementLine() override; - void ReadAnnouncementsFile(const char *pFileName) override; + void ReadAnnouncementsFile(); int *GetIdMap(int ClientId) override; diff --git a/src/game/server/ddracecommands.cpp b/src/game/server/ddracecommands.cpp index 26f817314d7..d1329645698 100644 --- a/src/game/server/ddracecommands.cpp +++ b/src/game/server/ddracecommands.cpp @@ -917,12 +917,6 @@ void CGameContext::ConReloadCensorlist(IConsole::IResult *pResult, void *pUserDa pSelf->ReadCensorList(); } -void CGameContext::ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData) -{ - CGameContext *pSelf = (CGameContext *)pUserData; - pSelf->Server()->ReadAnnouncementsFile(g_Config.m_SvAnnouncementFileName); -} - void CGameContext::ConDumpAntibot(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index b957f82f09e..8d08bdc89bc 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -3643,7 +3643,6 @@ void CGameContext::OnConsoleInit() Console()->Register("set_team_all", "i[team-id]", CFGFLAG_SERVER, ConSetTeamAll, this, "Set team of all players to team"); Console()->Register("hot_reload", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConHotReload, this, "Reload the map while preserving the state of tees and teams"); Console()->Register("reload_censorlist", "", CFGFLAG_SERVER, ConReloadCensorlist, this, "Reload the censorlist"); - Console()->Register("reload_announcement", "", CFGFLAG_SERVER, ConReloadAnnouncement, this, "Reload the announcements"); Console()->Register("add_vote", "s[name] r[command]", CFGFLAG_SERVER, ConAddVote, this, "Add a voting option"); Console()->Register("remove_vote", "r[name]", CFGFLAG_SERVER, ConRemoveVote, this, "remove a voting option"); diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index 85055d7d38e..8ca8027218f 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -528,7 +528,6 @@ class CGameContext : public IGameServer static void ConUnFreezeHammer(IConsole::IResult *pResult, void *pUserData); static void ConReloadCensorlist(IConsole::IResult *pResult, void *pUserData); - static void ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData); CCharacter *GetPracticeCharacter(IConsole::IResult *pResult); From 1b85bf294a8664e104d3a6662282017daa08b9d2 Mon Sep 17 00:00:00 2001 From: SollyBunny Date: Fri, 29 Nov 2024 06:28:50 +0000 Subject: [PATCH 13/58] Spikes freeze instead of kill in practice --- src/game/server/entities/character.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/game/server/entities/character.cpp b/src/game/server/entities/character.cpp index 00ccfadc6df..983caab9150 100644 --- a/src/game/server/entities/character.cpp +++ b/src/game/server/entities/character.cpp @@ -1386,7 +1386,10 @@ void CCharacter::HandleSkippableTiles(int Index) Collision()->GetFCollisionAt(m_Pos.x - GetProximityRadius() / 3.f, m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH) && !m_Core.m_Super && !m_Core.m_Invincible && !(Team() && Teams()->TeeFinished(m_pPlayer->GetCid()))) { - Die(m_pPlayer->GetCid(), WEAPON_WORLD); + if(Team() && Teams()->IsPractice(Team())) + Freeze(); + else + Die(m_pPlayer->GetCid(), WEAPON_WORLD); return; } From a03f642e4d36425df82958037d034a63e13ecd0a Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Sat, 30 Nov 2024 01:04:26 +0800 Subject: [PATCH 14/58] fix cursor drifting when using smooth dyncam and zoom at the same time --- src/game/client/components/camera.cpp | 124 +++++++++++++----------- src/game/client/components/camera.h | 11 +++ src/game/client/components/controls.cpp | 11 ++- src/game/client/gameclient.cpp | 3 + 4 files changed, 94 insertions(+), 55 deletions(-) diff --git a/src/game/client/components/camera.cpp b/src/game/client/components/camera.cpp index 2abf3af2cc2..5b6607dcbb7 100644 --- a/src/game/client/components/camera.cpp +++ b/src/game/client/components/camera.cpp @@ -35,6 +35,11 @@ CCamera::CCamera() m_WasSpectating = false; m_CameraSmoothing = false; + + m_LastMousePos = vec2(0, 0); + m_DyncamTargetCameraOffset = vec2(0, 0); + mem_zero(m_aDyncamCurrentCameraOffset, sizeof(m_aDyncamCurrentCameraOffset)); + m_DyncamSmoothingSpeedBias = 0.5f; } float CCamera::CameraSmoothingProgress(float CurrentTime) const @@ -89,7 +94,7 @@ void CCamera::ChangeZoom(float Target, int Smoothness) m_Zooming = true; } -void CCamera::OnRender() +void CCamera::UpdateCamera() { if(m_Zooming) { @@ -112,6 +117,58 @@ void CCamera::OnRender() m_Zoom = clamp(m_Zoom, MinZoomLevel(), MaxZoomLevel()); } + if(!ZoomAllowed()) + { + m_ZoomSet = false; + m_Zoom = 1.0f; + m_Zooming = false; + } + else if(!m_ZoomSet && g_Config.m_ClDefaultZoom != 10) + { + m_ZoomSet = true; + OnReset(); + } + + if(m_pClient->m_Snap.m_SpecInfo.m_Active && !m_pClient->m_Snap.m_SpecInfo.m_UsePosition) + return; + + float DeltaTime = Client()->RenderFrameTime(); + + if(g_Config.m_ClDyncamSmoothness > 0) + { + float CameraSpeed = (1.0f - (g_Config.m_ClDyncamSmoothness / 100.0f)) * 9.5f + 0.5f; + float CameraStabilizingFactor = 1 + g_Config.m_ClDyncamStabilizing / 100.0f; + + m_DyncamSmoothingSpeedBias += CameraSpeed * DeltaTime; + if(g_Config.m_ClDyncam) + { + m_DyncamSmoothingSpeedBias -= length(m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy] - m_LastMousePos) * std::log10(CameraStabilizingFactor) * 0.02f; + m_DyncamSmoothingSpeedBias = clamp(m_DyncamSmoothingSpeedBias, 0.5f, CameraSpeed); + } + else + { + m_DyncamSmoothingSpeedBias = maximum(5.0f, CameraSpeed); // make sure toggle back is fast + } + } + + m_DyncamTargetCameraOffset = vec2(0, 0); + vec2 MousePos = m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy]; + float l = length(MousePos); + if(l > 0.0001f) // make sure that this isn't 0 + { + float OffsetAmount = maximum(l - Deadzone(), 0.0f) * (FollowFactor() / 100.0f); + m_DyncamTargetCameraOffset = normalize(MousePos) * OffsetAmount; + } + + m_LastMousePos = MousePos; + if(g_Config.m_ClDyncamSmoothness > 0) + m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy] += (m_DyncamTargetCameraOffset - m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy]) * minimum(DeltaTime * m_DyncamSmoothingSpeedBias, 1.0f); + else + m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy] = m_DyncamTargetCameraOffset; +} + +void CCamera::OnRender() +{ if(m_CameraSmoothing) { if(!m_pClient->m_Snap.m_SpecInfo.m_Active) @@ -139,18 +196,6 @@ void CCamera::OnRender() } } - if(!ZoomAllowed()) - { - m_ZoomSet = false; - m_Zoom = 1.0f; - m_Zooming = false; - } - else if(!m_ZoomSet && g_Config.m_ClDefaultZoom != 10) - { - m_ZoomSet = true; - OnReset(); - } - // update camera center if(m_pClient->m_Snap.m_SpecInfo.m_Active && !m_pClient->m_Snap.m_SpecInfo.m_UsePosition) { @@ -172,49 +217,10 @@ void CCamera::OnRender() m_CamType = CAMTYPE_PLAYER; } - float DeltaTime = Client()->RenderFrameTime(); - static vec2 s_LastMousePos(0, 0); - static vec2 s_aCurrentCameraOffset[NUM_DUMMIES] = {vec2(0, 0), vec2(0, 0)}; - static float s_SpeedBias = 0.5f; - - if(g_Config.m_ClDyncamSmoothness > 0) - { - float CameraSpeed = (1.0f - (g_Config.m_ClDyncamSmoothness / 100.0f)) * 9.5f + 0.5f; - float CameraStabilizingFactor = 1 + g_Config.m_ClDyncamStabilizing / 100.0f; - - s_SpeedBias += CameraSpeed * DeltaTime; - if(g_Config.m_ClDyncam) - { - s_SpeedBias -= length(m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy] - s_LastMousePos) * std::log10(CameraStabilizingFactor) * 0.02f; - s_SpeedBias = clamp(s_SpeedBias, 0.5f, CameraSpeed); - } - else - { - s_SpeedBias = maximum(5.0f, CameraSpeed); // make sure toggle back is fast - } - } - - vec2 TargetCameraOffset(0, 0); - s_LastMousePos = m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy]; - float l = length(s_LastMousePos); - if(l > 0.0001f) // make sure that this isn't 0 - { - float DeadZone = g_Config.m_ClDyncam ? g_Config.m_ClDyncamDeadzone : g_Config.m_ClMouseDeadzone; - float FollowFactor = (g_Config.m_ClDyncam ? g_Config.m_ClDyncamFollowFactor : g_Config.m_ClMouseFollowfactor) / 100.0f; - float OffsetAmount = maximum(l - DeadZone, 0.0f) * FollowFactor; - - TargetCameraOffset = normalize(m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy]) * OffsetAmount; - } - - if(g_Config.m_ClDyncamSmoothness > 0) - s_aCurrentCameraOffset[g_Config.m_ClDummy] += (TargetCameraOffset - s_aCurrentCameraOffset[g_Config.m_ClDummy]) * minimum(DeltaTime * s_SpeedBias, 1.0f); - else - s_aCurrentCameraOffset[g_Config.m_ClDummy] = TargetCameraOffset; - if(m_pClient->m_Snap.m_SpecInfo.m_Active) - m_Center = m_pClient->m_Snap.m_SpecInfo.m_Position + s_aCurrentCameraOffset[g_Config.m_ClDummy]; + m_Center = m_pClient->m_Snap.m_SpecInfo.m_Position + m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy]; else - m_Center = m_pClient->m_LocalCharacterPos + s_aCurrentCameraOffset[g_Config.m_ClDummy]; + m_Center = m_pClient->m_LocalCharacterPos + m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy]; } if(m_ForceFreeview && m_CamType == CAMTYPE_SPEC) @@ -478,3 +484,13 @@ bool CCamera::ZoomAllowed() const GameClient()->m_GameInfo.m_AllowZoom || Client()->State() == IClient::STATE_DEMOPLAYBACK; } + +int CCamera::Deadzone() const +{ + return g_Config.m_ClDyncam ? g_Config.m_ClDyncamDeadzone : g_Config.m_ClMouseDeadzone; +} + +int CCamera::FollowFactor() const +{ + return g_Config.m_ClDyncam ? g_Config.m_ClDyncamFollowFactor : g_Config.m_ClMouseFollowfactor; +} diff --git a/src/game/client/components/camera.h b/src/game/client/components/camera.h index 72b42230f91..b487c680b5b 100644 --- a/src/game/client/components/camera.h +++ b/src/game/client/components/camera.h @@ -50,6 +50,9 @@ class CCamera : public CComponent float MinZoomLevel(); float MaxZoomLevel(); + vec2 m_LastMousePos; + float m_DyncamSmoothingSpeedBias; + public: static constexpr float ZOOM_STEP = 0.866025f; @@ -59,6 +62,9 @@ class CCamera : public CComponent float m_Zoom; float m_ZoomSmoothingTarget; + vec2 m_DyncamTargetCameraOffset; + vec2 m_aDyncamCurrentCameraOffset[NUM_DUMMIES]; + CCamera(); virtual int Sizeof() const override { return sizeof(*this); } virtual void OnRender() override; @@ -75,6 +81,11 @@ class CCamera : public CComponent void SetZoom(float Target, int Smoothness); bool ZoomAllowed() const; + int Deadzone() const; + int FollowFactor() const; + + void UpdateCamera(); + private: static void ConZoomPlus(IConsole::IResult *pResult, void *pUserData); static void ConZoomMinus(IConsole::IResult *pResult, void *pUserData); diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp index 07c56d9c6f9..7969b109dbd 100644 --- a/src/game/client/components/controls.cpp +++ b/src/game/client/components/controls.cpp @@ -356,11 +356,20 @@ void CControls::OnRender() // update target pos if(m_pClient->m_Snap.m_pGameInfoObj && !m_pClient->m_Snap.m_SpecInfo.m_Active) - m_aTargetPos[g_Config.m_ClDummy] = m_pClient->m_LocalCharacterPos + m_aMousePos[g_Config.m_ClDummy]; + { + // make sure to compensate for smooth dyncam to ensure the cursor stays still in world space if zoomed + vec2 DyncamOffsetDelta = m_pClient->m_Camera.m_DyncamTargetCameraOffset - m_pClient->m_Camera.m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy]; + float Zoom = m_pClient->m_Camera.m_Zoom; + m_aTargetPos[g_Config.m_ClDummy] = m_pClient->m_LocalCharacterPos + m_aMousePos[g_Config.m_ClDummy] - DyncamOffsetDelta + DyncamOffsetDelta / Zoom; + } else if(m_pClient->m_Snap.m_SpecInfo.m_Active && m_pClient->m_Snap.m_SpecInfo.m_UsePosition) + { m_aTargetPos[g_Config.m_ClDummy] = m_pClient->m_Snap.m_SpecInfo.m_Position + m_aMousePos[g_Config.m_ClDummy]; + } else + { m_aTargetPos[g_Config.m_ClDummy] = m_aMousePos[g_Config.m_ClDummy]; + } } bool CControls::OnCursorMove(float x, float y, IInput::ECursorType CursorType) diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index fbdb7eb2786..734d940708e 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -757,6 +757,9 @@ void CGameClient::OnRender() } } + // update camera data prior to CControls::OnRender to allow CControls::m_aTargetPos to compensate using camera data + m_Camera.UpdateCamera(); + // render all systems for(auto &pComponent : m_vpAll) pComponent->OnRender(); From a2e390592e955678de311a7e395f6d1bc357b62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Thu, 21 Nov 2024 22:09:28 +0100 Subject: [PATCH 15/58] Fix crashes with whisper command The client state was not checked when searching through the list of client names for the whisper target and when handling 0.7 whisper messages. The invalid whisper target ID being used was causing crashes in the following cases: - When using `/whisper (invalid) message`, if there is any free slot on the server. - When using `/whisper (connecting) message`, if there is any player currently not ingame, e.g. connecting or redirected. - When sending specially crafted 0.7 whisper messages with invalid target IDs or when sending a 0.7 whisper message to a client that is about to disconnect. Refactoring: - Add assertion to catch all incorrect uses of the `CGameContext::WhisperId` function. - Remove redundant `Victim >= MAX_CLIENTS` check which is included in the call to the `CheckClientId2` function. - Use `nullptr` and `'\0'` instead of `0`. - Use templated `str_copy` function instead of passing size as number. - Remove obsolete debug message for client command messages. --- src/game/server/gamecontext.cpp | 81 +++++++++++++++++---------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 8d08bdc89bc..2c340288329 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -1853,13 +1853,18 @@ bool CGameContext::OnClientDDNetVersionKnown(int ClientId) return false; } +bool CheckClientId2(int ClientId) +{ + return ClientId >= 0 && ClientId < MAX_CLIENTS; +} + void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientId) { if(Server()->IsSixup(ClientId) && *pMsgId < OFFSET_UUID) { void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(*pMsgId, pUnpacker); if(!pRawMsg) - return 0; + return nullptr; CPlayer *pPlayer = m_apPlayers[ClientId]; static char s_aRawMsg[1024]; @@ -1870,18 +1875,21 @@ void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientI // Should probably use a placement new to start the lifetime of the object to avoid future weirdness ::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg; - if(pMsg7->m_Target >= 0) + if(pMsg7->m_Mode == protocol7::CHAT_WHISPER) { + if(!CheckClientId2(pMsg7->m_Target) || !Server()->ClientIngame(pMsg7->m_Target)) + return nullptr; if(ProcessSpamProtection(ClientId)) - return 0; + return nullptr; - // Should we maybe recraft the message so that it can go through the usual path? WhisperId(ClientId, pMsg7->m_Target, pMsg7->m_pMessage); - return 0; + return nullptr; + } + else + { + pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM; + pMsg->m_pMessage = pMsg7->m_pMessage; } - - pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM; - pMsg->m_pMessage = pMsg7->m_pMessage; } else if(*pMsgId == protocol7::NETMSGTYPE_CL_STARTINFO) { @@ -1908,7 +1916,7 @@ void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientI protocol7::CNetMsg_Cl_SkinChange *pMsg = (protocol7::CNetMsg_Cl_SkinChange *)pRawMsg; if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick()) - return 0; + return nullptr; pPlayer->m_LastChangeInfo = Server()->Tick(); @@ -1927,7 +1935,7 @@ void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientI Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1); - return 0; + return nullptr; } else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETSPECTATORMODE) { @@ -1955,7 +1963,6 @@ void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientI str_format(s_aRawMsg + sizeof(*pMsg), sizeof(s_aRawMsg) - sizeof(*pMsg), "/%s %s", pMsg7->m_pName, pMsg7->m_pArguments); pMsg->m_pMessage = s_aRawMsg + sizeof(*pMsg); - dbg_msg("debug", "line='%s'", s_aRawMsg + sizeof(*pMsg)); pMsg->m_Team = 0; *pMsgId = NETMSGTYPE_CL_SAY; @@ -1973,7 +1980,7 @@ void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientI Console()->SetAccessLevel(Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : IConsole::ACCESS_LEVEL_HELPER); Console()->ExecuteLine(s_aRawMsg, ClientId, false); Console()->SetAccessLevel(IConsole::ACCESS_LEVEL_ADMIN); - return 0; + return nullptr; } pMsg->m_pValue = pMsg7->m_pValue; @@ -2148,25 +2155,25 @@ void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientId, con if(str_startswith_nocase(pMsg->m_pMessage + 1, "w ")) { char aWhisperMsg[256]; - str_copy(aWhisperMsg, pMsg->m_pMessage + 3, 256); + str_copy(aWhisperMsg, pMsg->m_pMessage + 3); Whisper(pPlayer->GetCid(), aWhisperMsg); } else if(str_startswith_nocase(pMsg->m_pMessage + 1, "whisper ")) { char aWhisperMsg[256]; - str_copy(aWhisperMsg, pMsg->m_pMessage + 9, 256); + str_copy(aWhisperMsg, pMsg->m_pMessage + 9); Whisper(pPlayer->GetCid(), aWhisperMsg); } else if(str_startswith_nocase(pMsg->m_pMessage + 1, "c ")) { char aWhisperMsg[256]; - str_copy(aWhisperMsg, pMsg->m_pMessage + 3, 256); + str_copy(aWhisperMsg, pMsg->m_pMessage + 3); Converse(pPlayer->GetCid(), aWhisperMsg); } else if(str_startswith_nocase(pMsg->m_pMessage + 1, "converse ")) { char aWhisperMsg[256]; - str_copy(aWhisperMsg, pMsg->m_pMessage + 10, 256); + str_copy(aWhisperMsg, pMsg->m_pMessage + 10); Converse(pPlayer->GetCid(), aWhisperMsg); } else @@ -4559,11 +4566,6 @@ void CGameContext::ResetTuning() SendTuningParams(-1); } -bool CheckClientId2(int ClientId) -{ - return ClientId >= 0 && ClientId < MAX_CLIENTS; -} - void CGameContext::Whisper(int ClientId, char *pStr) { if(ProcessSpamProtection(ClientId)) @@ -4571,7 +4573,7 @@ void CGameContext::Whisper(int ClientId, char *pStr) pStr = str_skip_whitespaces(pStr); - char *pName; + const char *pName; int Victim; bool Error = false; @@ -4608,14 +4610,16 @@ void CGameContext::Whisper(int ClientId, char *pStr) if(!Error) { - // write null termination - *pDst = 0; - + *pDst = '\0'; pStr++; for(Victim = 0; Victim < MAX_CLIENTS; Victim++) - if(str_comp(pName, Server()->ClientName(Victim)) == 0) + { + if(Server()->ClientIngame(Victim) && str_comp(pName, Server()->ClientName(Victim)) == 0) + { break; + } + } } } else @@ -4623,20 +4627,23 @@ void CGameContext::Whisper(int ClientId, char *pStr) pName = pStr; while(true) { - if(pStr[0] == 0) + if(pStr[0] == '\0') { Error = true; break; } if(pStr[0] == ' ') { - pStr[0] = 0; + pStr[0] = '\0'; for(Victim = 0; Victim < MAX_CLIENTS; Victim++) - if(str_comp(pName, Server()->ClientName(Victim)) == 0) + { + if(Server()->ClientIngame(Victim) && str_comp(pName, Server()->ClientName(Victim)) == 0) + { break; + } + } pStr[0] = ' '; - if(Victim < MAX_CLIENTS) break; } @@ -4649,7 +4656,7 @@ void CGameContext::Whisper(int ClientId, char *pStr) Error = true; } - *pStr = 0; + *pStr = '\0'; pStr++; if(Error) @@ -4658,7 +4665,7 @@ void CGameContext::Whisper(int ClientId, char *pStr) return; } - if(Victim >= MAX_CLIENTS || !CheckClientId2(Victim)) + if(!CheckClientId2(Victim)) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "No player with name \"%s\" found", pName); @@ -4671,14 +4678,10 @@ void CGameContext::Whisper(int ClientId, char *pStr) void CGameContext::WhisperId(int ClientId, int VictimId, const char *pMessage) { - if(!CheckClientId2(ClientId)) - return; + dbg_assert(CheckClientId2(ClientId) && m_apPlayers[ClientId] != nullptr, "ClientId invalid"); + dbg_assert(CheckClientId2(VictimId) && m_apPlayers[VictimId] != nullptr, "VictimId invalid"); - if(!CheckClientId2(VictimId)) - return; - - if(m_apPlayers[ClientId]) - m_apPlayers[ClientId]->m_LastWhisperTo = VictimId; + m_apPlayers[ClientId]->m_LastWhisperTo = VictimId; char aCensoredMessage[256]; CensorMessage(aCensoredMessage, pMessage, sizeof(aCensoredMessage)); From eb541f87ad842cd39b2cd64d5ac8bc1294c85923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Wed, 21 Aug 2024 00:47:54 +0200 Subject: [PATCH 16/58] Support handling touch state as part of the client update So the touch state can be handled by gameclient components independently from their rendering, so touch inputs are not skipped when frames are. Also store the time when touch fingers are first pressed down. --- src/engine/client/input.cpp | 14 ++++++++++---- src/engine/client/input.h | 1 + src/engine/input.h | 11 +++++++++++ src/game/client/component.h | 8 ++++++++ src/game/client/gameclient.cpp | 17 +++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/engine/client/input.cpp b/src/engine/client/input.cpp index f590ba8c025..cb4ec78b503 100644 --- a/src/engine/client/input.cpp +++ b/src/engine/client/input.cpp @@ -301,6 +301,14 @@ const std::vector &CInput::TouchFingerStates() const return m_vTouchFingerStates; } +void CInput::ClearTouchDeltas() +{ + for(CTouchFingerState &TouchFingerState : m_vTouchFingerStates) + { + TouchFingerState.m_Delta = vec2(0.0f, 0.0f); + } +} + std::string CInput::GetClipboardText() { char *pClipboardText = SDL_GetClipboardText(); @@ -348,10 +356,7 @@ void CInput::Clear() mem_zero(m_aInputState, sizeof(m_aInputState)); mem_zero(m_aInputCount, sizeof(m_aInputCount)); m_vInputEvents.clear(); - for(CTouchFingerState &TouchFingerState : m_vTouchFingerStates) - { - TouchFingerState.m_Delta = vec2(0.0f, 0.0f); - } + ClearTouchDeltas(); } float CInput::GetUpdateTime() const @@ -565,6 +570,7 @@ void CInput::HandleTouchDownEvent(const SDL_TouchFingerEvent &Event) TouchFingerState.m_Finger.m_FingerId = Event.fingerId; TouchFingerState.m_Position = vec2(Event.x, Event.y); TouchFingerState.m_Delta = vec2(Event.dx, Event.dy); + TouchFingerState.m_PressTime = time_get_nanoseconds(); m_vTouchFingerStates.emplace_back(TouchFingerState); } diff --git a/src/engine/client/input.h b/src/engine/client/input.h index d0098abf1b5..7d491571e68 100644 --- a/src/engine/client/input.h +++ b/src/engine/client/input.h @@ -146,6 +146,7 @@ class CInput : public IEngineInput bool NativeMousePressed(int Index) const override; const std::vector &TouchFingerStates() const override; + void ClearTouchDeltas() override; std::string GetClipboardText() override; void SetClipboardText(const char *pText) override; diff --git a/src/engine/input.h b/src/engine/input.h index 9aa87335826..873423b8085 100644 --- a/src/engine/input.h +++ b/src/engine/input.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -128,6 +129,10 @@ class IInput : public IInterface * @remark This is reset to zero at the end of each frame. */ vec2 m_Delta; + /** + * The time when this finger was first pressed down. + */ + std::chrono::nanoseconds m_PressTime; }; /** * Returns a vector of the states of all touch fingers currently being pressed down on touch devices. @@ -137,6 +142,12 @@ class IInput : public IInterface * @return vector of all touch finger states */ virtual const std::vector &TouchFingerStates() const = 0; + /** + * Must be called after the touch finger states have been used during the client update to ensure that + * touch deltas are only accumulated until the next update. If the touch states are only used during + * rendering, i.e. for user interfaces, then this is called automatically by calling @link Clear @endlink. + */ + virtual void ClearTouchDeltas() = 0; // clipboard virtual std::string GetClipboardText() = 0; diff --git a/src/game/client/component.h b/src/game/client/component.h index 639c8b140ad..9d14615a058 100644 --- a/src/game/client/component.h +++ b/src/game/client/component.h @@ -212,6 +212,14 @@ class CComponent * @param Event The input event. */ virtual bool OnInput(const IInput::CEvent &Event) { return false; } + /** + * Called with all current touch finger states. + * + * @param vTouchFingerStates The touch finger states to be handled. + * + * @return `true` if the component used the touch events, `false` otherwise + */ + virtual bool OnTouchState(const std::vector &vTouchFingerStates) { return false; } }; #endif diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 734d940708e..ff733b30db8 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -444,6 +444,23 @@ void CGameClient::OnUpdate() } } + // handle touch events + const std::vector &vTouchFingerStates = Input()->TouchFingerStates(); + bool TouchHandled = false; + for(auto &pComponent : m_vpInput) + { + if(TouchHandled) + { + // Also update inactive components so they can handle touch fingers being released. + pComponent->OnTouchState({}); + } + else if(pComponent->OnTouchState(vTouchFingerStates)) + { + Input()->ClearTouchDeltas(); + TouchHandled = true; + } + } + // handle key presses Input()->ConsumeEvents([&](const IInput::CEvent &Event) { for(auto &pComponent : m_vpInput) From 377664fdaa6daaf6468bd873cda499ce17a15486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sat, 13 Jul 2024 22:25:34 +0200 Subject: [PATCH 17/58] Add ingame touch controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new client component `CTouchControls` for playing the game with touch inputs. The touch controls can be enabled/disabled with the config variable `cl_touch_controls`, which defaults to `1` on Android and `0` on other platforms. The touch controls consist of various on-screen touch buttons. The different buttons are only shown when they are usable depending on the context. The touch button configuration is stored in `touch_controls.json`. Closes #8359. Default touch button configuration ---------------------------------- Movement buttons for left, right and jump actions are arranged in a `⊥`-pattern similar to WASD controls. For the fire and hook action, two modes are implemented: 1. Direct touch input: the mouse is moved exactly where the player touches on the screen. 2. Virtual joystick: a button is used to emulate a joystick, which moves the mouse relative to the center of the screen. In either mode, a button is used to switch between the active actions (fire and hook). While the virtual joystick is being held down, this button uses the other action directly instead of switching it. The direct touch input mode can be adjusted separately for ingame/spectating, to prevent accidental direct touch input when using a joystick. When spectating, direct touch input is used to allow panning the map directly like in an image/map viewer. Virtual joysticks can also be used to pan the map while spectating. Two separate buttons are shown to switch to the previous and next weapons. A hamburger menu button `☰` is used to toggle the visibility of lesser used touch buttons. This includes buttons for showing the scoreboard, showing the emoticon selector, showing the spectator menu, opening team and team chat, voting yes/no, and zooming. Long pressing the hamburger menu button will open the ingame menu. When the dummy is connected, a button for swapping between main and dummy is shown. The emoticon selector and spectator menu are activated with the respective touch buttons and can be deactivated by touching outside of them or by using the back-button, as toggling them while the ingame touch button is pressed down is currently not feasible and also inconvenient at least for using the spectator menu. Touch button configuration format --------------------------------- The default button layout described above is loaded from `data/touch_controls.json`. This layout can be overridden by creating `touch_controls.json` in the user directory. The attribute `"touch-buttons"` of the root object in this file specifies an array of touch button objects. Each touch button object has the following adjustable properties: - Position and size (attributes `x`, `y`, `w`, `h`): the X/Y position and width/height are integers on a 1,000,000² grid. The unit grid values are converted to screen grid values at runtime in relation to the aspect ratio of the screen. This means buttons may appear stretched if the resolution is changed, but it allows us to provide a reasonable default for slightly different aspect ratios. If we save the absolute position instead, we would have the problem of buttons appearing outside the screen or not being aligned on different aspect ratios. - Shape (attribute `shape`): determines the shape of the button being rendered. - `"rect"`: rectangle shape. - `"circle"`: circle shape. The button size will automatically be adjusted so that width and height are identical. - Visibility (attribute `visibilities`): an array of predefined visibility classes can be selected and the button is only shown if all conditions are satisfied. An empty array means that the button is always shown. The following visibility classes are defined: - `"ingame"`: player is ingame, i.e. not spectating. - `"extra-menu"`, `"extra-menu-2"`, ..., `"extra-menu-5"`: the extra menu with the given number is activated. - `"zoom-allowed"`: zoom is allowed on this server. - `"vote-active"`: a vote is currently active. - `"dummy-allowed"`: dummy is allowed on this server. - `"dummy-connected"`: dummy is currently connected. - `"rcon-authed"`: player is currently authed in rcon. - `"demo-player"`: demo player is currently active. - All visibility classes can be inverted by prefixing them with `-`, e.g. `"-ingame"` is satisfied when the player is not ingame, i.e. spectating. - Behavior (attribute `behavior`): an object which describes the behavior of this touch button when it is activated/deactivated as well as its label. The attribute `type` is used to differentiate which type of behavior is used. The behavior is either predefined (hard-coded) or based on generic console commands (like bind). Predefined behavior is only used where necessary, all other buttons are represented as generic binds. - Predefined behavior (attribute `type` set to `"predefined"`): The attribute `id` is set to a fixed string value which determines the specific predefined behavior. The following predefined behaviors exist: - `"ingame-menu"`: Opens the ingame menu immediately when released. - `"extra-menu"`: The extra menu button which toggles visibility of buttons with `"extra-menu"`, `"extra-menu-2"`, `"extra-menu-3"` etc. visibilities. Also opens the ingame menu on long press. - The attribute `"number"` specifies an integer between 1 and 5 to associate this button with the respective visibilities `"extra-menu"`, `"extra-menu-2"`, ..., `"extra-menu-5"`. - `"emoticon"`: Opens the emoticon selector (this does not work with binds). - `"spectate"`: Opens the spectator menu (this does not work with binds). - `"swap-action"`: Swaps the active action (fire and hook) for direct touch input and virtual joysticks. - `"use-action"`: Uses the active action with the current aiming position. - `"joystick-action"`: Virtual joystick which uses the active action. - `"joystick-aim"`: Virtual joystick which only aims without using an action. - `"joystick-fire"`: Virtual joystick which always uses fire. - `"joystick-hook"`: Virtual joystick which always uses hook. - Bind behavior (attribute `type` set to `"bind"`). Buttons with this behavior work like regular key binds. - The attribute `"label"` specifies as a string the label of the button. - The attribute `"label-type"` specifies as a string the type of the label of the button, i.e. how the attribute `"label"` is interpreted: - `"plain"`: Label is used as is. - `"localized"`: Label is localized. Only usable for the default buttons for which there are translations available. - `"icon"`: Icon font is used for the label. Icons must be encoded in UTF-16 using `\uXXXX`, e.g. `\uf3ce` for the mobile phone icon. - The attribute `"command"` specifies as a string the command to execute in the console like a bind when this button is used. - Bind toggle behavior (attribute `type` set to `"bind-toggle"`). Buttons with this behavior cycle between executing one of two or more specified commands. - The attribute `"commands"` specifies an array of two or more command in the order in which they are shown and executed. Each command is an object with the attributes `"label"`, `"label-type"` and `"command"` which are defined the same as for the bind behavior described above. At least two commands must be specified. The root object additionally has the following attributes: - `"direct-touch-ingame"`: specifies the mode of direct touch input while ingame. Possible values: - `"disabled"`: Direct touch input is not used while ingame. This means a virtual joystick is necessary. - `"action"`: Direct touch input uses the active action (see above). - `"aim"`: Direct touch input only changes the aiming position without using an action. This means separate buttons for using the actions are necessary. - `"fire"`: Direct touch input always uses fire. - `"hook"`: Direct touch input always uses hook. - `"direct-touch-spectate"`: specifies the mode of direct touch input while ingame. Possible values: - `"disabled"`: Direct touch input is not used while spectating. This means a virtual joystick is necessary. - `"aim"`: Direct touch input is used for spectating. Ingame menu buttons ------------------- In addition to the separate on-screen touch controls, a second row is added to the main page of the ingame menu when `cl_touch_controls` is enabled for less frequently used functions which are otherwise not usable without a keyboard: - Buttons to open the local and remote consoles. Opening the local console without the touch controls is useful because error messages would be shown there if the touch controls configuration could not be loaded. - Button to close the menu, which is more convenient than using the virtual back button if it's not always shown. - Checkbox for toggling the touch controls editor UI (see below). Ingame touch controls editor ---------------------------- The user interface to adjust the touch controls is rendered over the main screen of the ingame menu when enabled. For now, the implementation of the touch controls editor is limited to basic configuration management functions. - Saving the configuration to the `touch_controls.json` file in the config directory. - Discarding the current changes by reloading the `touch_controls.json` file from the config directory. - Restoring the default configuration by reloading the `touch_controls.json` file from the data directory. - Displaying whether there are unsaved changes. - Importing and exporting the configuration from and to the clipboard. This is the only way to edit the configuration on newer Android versions, as editing files within apps' storage is not possible anymore. Furthermore, the global touch controls settings can be adjusted in this UI: - The direct touch input modes while ingame and spectating (see Touch button configuration format) can be adjusted using dropdown menus. While the touch controls editor is active, all touch buttons are shown regardless of their visibilities, to better support arranging the buttons. Future tasks and ideas ---------------------- - Implement more usable UI for adjusting the button layout. - Add `color` property to touch buttons to adjust the default background color? - Add `choice` predefined behavior which shows a selection popup for 2 or more other behaviors? - Add combined weapon picker button that shows all currently available weapons. - Support zooming gesture while spectating. - Optimize touch button rendering using text and quad containers. --- CMakeLists.txt | 3 + data/touch_controls.json | 311 ++++ src/engine/shared/config_variables.h | 5 + src/game/client/components/controls.h | 2 +- src/game/client/components/menus.h | 7 +- src/game/client/components/menus_ingame.cpp | 245 ++- src/game/client/components/touch_controls.cpp | 1568 +++++++++++++++++ src/game/client/components/touch_controls.h | 548 ++++++ src/game/client/gameclient.cpp | 2 + src/game/client/gameclient.h | 2 + 10 files changed, 2679 insertions(+), 14 deletions(-) create mode 100644 data/touch_controls.json create mode 100644 src/game/client/components/touch_controls.cpp create mode 100644 src/game/client/components/touch_controls.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ba403347f62..1ff72da59a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1846,6 +1846,7 @@ set(EXPECTED_DATA themes/winter.png themes/winter_day.map themes/winter_night.map + touch_controls.json wordlist.txt ) @@ -2402,6 +2403,8 @@ if(CLIENT) components/statboard.h components/tooltips.cpp components/tooltips.h + components/touch_controls.cpp + components/touch_controls.h components/voting.cpp components/voting.h gameclient.cpp diff --git a/data/touch_controls.json b/data/touch_controls.json new file mode 100644 index 00000000000..5d4aa431b6c --- /dev/null +++ b/data/touch_controls.json @@ -0,0 +1,311 @@ +{ + "direct-touch-ingame": "action", + "direct-touch-spectate": "aim", + "touch-buttons": [ + { + "x": 0, + "y": 833333, + "w": 200000, + "h": 166667, + "shape": "rect", + "visibilities": [ + "ingame" + ], + "behavior": { + "type": "bind", + "label": "Move left", + "label-type": "localized", + "command": "+left" + } + }, + { + "x": 200000, + "y": 833333, + "w": 200000, + "h": 166667, + "shape": "rect", + "visibilities": [ + "ingame" + ], + "behavior": { + "type": "bind", + "label": "Move right", + "label-type": "localized", + "command": "+right" + } + }, + { + "x": 100000, + "y": 666667, + "w": 200000, + "h": 166667, + "shape": "rect", + "visibilities": [ + "ingame" + ], + "behavior": { + "type": "bind", + "label": "Jump", + "label-type": "localized", + "command": "+jump" + } + }, + { + "x": 116667, + "y": 16667, + "w": 83333, + "h": 83333, + "shape": "rect", + "visibilities": [ + "ingame" + ], + "behavior": { + "type": "bind", + "label": "Prev. weapon", + "label-type": "localized", + "command": "+prevweapon" + } + }, + { + "x": 200000, + "y": 16667, + "w": 83333, + "h": 83333, + "shape": "rect", + "visibilities": [ + "ingame" + ], + "behavior": { + "type": "bind", + "label": "Next weapon", + "label-type": "localized", + "command": "+nextweapon" + } + }, + { + "x": 16667, + "y": 16667, + "w": 83333, + "h": 83333, + "shape": "rect", + "visibilities": [ + ], + "behavior": { + "type": "predefined", + "id": "extra-menu", + "number": 1 + } + }, + { + "x": 300000, + "y": 16667, + "w": 83333, + "h": 83333, + "shape": "rect", + "visibilities": [ + "extra-menu", + "zoom-allowed" + ], + "behavior": { + "type": "bind", + "label": "Zoom out", + "label-type": "localized", + "command": "zoom-" + } + }, + { + "x": 383333, + "y": 16667, + "w": 83333, + "h": 83333, + "shape": "rect", + "visibilities": [ + "extra-menu", + "zoom-allowed" + ], + "behavior": { + "type": "bind", + "label": "Default zoom", + "label-type": "localized", + "command": "zoom" + } + }, + { + "x": 466666, + "y": 16667, + "w": 83333, + "h": 83333, + "shape": "rect", + "visibilities": [ + "extra-menu", + "zoom-allowed" + ], + "behavior": { + "type": "bind", + "label": "Zoom in", + "label-type": "localized", + "command": "zoom+" + } + }, + { + "x": 16667, + "y": 133333, + "w": 83333, + "h": 66667, + "shape": "rect", + "visibilities": [ + "extra-menu" + ], + "behavior": { + "type": "bind", + "label": "Scoreboard", + "label-type": "localized", + "command": "+scoreboard" + } + }, + { + "x": 116667, + "y": 133333, + "w": 83333, + "h": 66667, + "shape": "rect", + "visibilities": [ + "ingame", + "extra-menu" + ], + "behavior": { + "type": "predefined", + "id": "emoticon" + } + }, + { + "x": 116667, + "y": 133333, + "w": 83333, + "h": 66667, + "shape": "rect", + "visibilities": [ + "-ingame", + "extra-menu" + ], + "behavior": { + "type": "predefined", + "id": "spectate" + } + }, + { + "x": 216667, + "y": 133333, + "w": 83333, + "h": 66667, + "shape": "rect", + "visibilities": [ + "extra-menu", + "-demo-player" + ], + "behavior": { + "type": "bind", + "label": "Chat", + "label-type": "localized", + "command": "chat all" + } + }, + { + "x": 316667, + "y": 133333, + "w": 83333, + "h": 66667, + "shape": "rect", + "visibilities": [ + "extra-menu", + "-demo-player" + ], + "behavior": { + "type": "bind", + "label": "Team chat", + "label-type": "localized", + "command": "chat team" + } + }, + { + "x": 16667, + "y": 333333, + "w": 83333, + "h": 66667, + "shape": "rect", + "visibilities": [ + "extra-menu", + "vote-active", + "-demo-player" + ], + "behavior": { + "type": "bind", + "label": "Vote yes", + "label-type": "localized", + "command": "vote yes" + } + }, + { + "x": 116667, + "y": 333333, + "w": 83333, + "h": 66667, + "shape": "rect", + "visibilities": [ + "extra-menu", + "vote-active", + "-demo-player" + ], + "behavior": { + "type": "bind", + "label": "Vote no", + "label-type": "localized", + "command": "vote no" + } + }, + { + "x": 766667, + "y": 16667, + "w": 100000, + "h": 100000, + "shape": "rect", + "visibilities": [ + "dummy-connected" + ], + "behavior": { + "type": "bind", + "label": "Toggle dummy", + "label-type": "localized", + "command": "toggle cl_dummy 0 1" + } + }, + { + "x": 883333, + "y": 16667, + "w": 100000, + "h": 100000, + "shape": "rect", + "visibilities": [ + "ingame" + ], + "behavior": { + "type": "predefined", + "id": "swap-action" + } + }, + { + "x": 755000, + "y": 580000, + "w": 225000, + "h": 400000, + "shape": "circle", + "visibilities": [ + "ingame" + ], + "behavior": { + "type": "predefined", + "id": "joystick-action" + } + } + ] +} diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 73bf017ca92..d676b4e8a82 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -22,6 +22,11 @@ MACRO_CONFIG_INT(ClAntiPingSmooth, cl_antiping_smooth, 0, 0, 1, CFGFLAG_CLIENT | MACRO_CONFIG_INT(ClAntiPingGunfire, cl_antiping_gunfire, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Predict gunfire and show predicted weapon physics (with cl_antiping_grenade 1 and cl_antiping_weapons 1)") MACRO_CONFIG_INT(ClPredictionMargin, cl_prediction_margin, 10, 1, 300, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Prediction margin in ms (adds latency, can reduce lag from ping jumps)") MACRO_CONFIG_INT(ClSubTickAiming, cl_sub_tick_aiming, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Send aiming data at sub-tick accuracy") +#if defined(CONF_PLATFORM_ANDROID) +MACRO_CONFIG_INT(ClTouchControls, cl_touch_controls, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable ingame touch controls") +#else +MACRO_CONFIG_INT(ClTouchControls, cl_touch_controls, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable ingame touch controls") +#endif MACRO_CONFIG_INT(ClNameplates, cl_nameplates, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show name plates") MACRO_CONFIG_INT(ClAfkEmote, cl_afk_emote, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show zzz emote next to afk players") diff --git a/src/game/client/components/controls.h b/src/game/client/components/controls.h index ce6bc664dd1..11eecdde03d 100644 --- a/src/game/client/components/controls.h +++ b/src/game/client/components/controls.h @@ -12,10 +12,10 @@ class CControls : public CComponent { +public: float GetMinMouseDistance() const; float GetMaxMouseDistance() const; -public: vec2 m_aMousePos[NUM_DUMMIES]; vec2 m_aMousePosOnAction[NUM_DUMMIES]; vec2 m_aTargetPos[NUM_DUMMIES]; diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index f8e4afccdc8..1afda415090 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -479,8 +479,12 @@ class CMenus : public CComponent // found in menus_ingame.cpp STextContainerIndex m_MotdTextContainerIndex; void RenderGame(CUIRect MainView); + void RenderTouchControlsEditor(CUIRect MainView); void PopupConfirmDisconnect(); void PopupConfirmDisconnectDummy(); + void PopupConfirmDiscardTouchControlsChanges(); + void PopupConfirmResetTouchControls(); + void PopupConfirmImportTouchControlsClipboard(); void RenderPlayers(CUIRect MainView); void RenderServerInfo(CUIRect MainView); void RenderServerInfoMotd(CUIRect Motd); @@ -641,7 +645,6 @@ class CMenus : public CComponent static CUi::EPopupMenuFunctionResult PopupMapPicker(void *pContext, CUIRect View, bool Active); void SetNeedSendInfo(); - void SetActive(bool Active); void UpdateColors(); IGraphics::CTextureHandle m_TextureBlob; @@ -661,6 +664,8 @@ class CMenus : public CComponent bool IsInit() { return m_IsInit; } bool IsActive() const { return m_MenuActive; } + void SetActive(bool Active); + void KillServer(); virtual void OnInit() override; diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp index fdc8459e347..b68361a6429 100644 --- a/src/game/client/components/menus_ingame.cpp +++ b/src/game/client/components/menus_ingame.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -40,21 +41,19 @@ using namespace std::chrono_literals; void CMenus::RenderGame(CUIRect MainView) { - CUIRect Button, ButtonBar, ButtonBar2; + CUIRect Button, ButtonBars, ButtonBar, ButtonBar2; bool ShowDDRaceButtons = MainView.w > 855.0f; - MainView.HSplitTop(45.0f, &ButtonBar, &MainView); - ButtonBar.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); - - // button bar - ButtonBar.HSplitTop(10.0f, 0, &ButtonBar); - ButtonBar.HSplitTop(25.0f, &ButtonBar, 0); - ButtonBar.VMargin(10.0f, &ButtonBar); - - ButtonBar.HSplitTop(30.0f, 0, &ButtonBar2); - ButtonBar2.HSplitTop(25.0f, &ButtonBar2, 0); + MainView.HSplitTop(45.0f + (g_Config.m_ClTouchControls ? 35.0f : 0.0f), &ButtonBars, &MainView); + ButtonBars.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); + ButtonBars.Margin(10.0f, &ButtonBars); + ButtonBars.HSplitTop(25.0f, &ButtonBar, &ButtonBars); + if(g_Config.m_ClTouchControls) + { + ButtonBars.HSplitTop(10.0f, nullptr, &ButtonBars); + ButtonBars.HSplitTop(25.0f, &ButtonBar2, &ButtonBars); + } ButtonBar.VSplitRight(120.0f, &ButtonBar, &Button); - static CButtonContainer s_DisconnectButton; if(DoButton_Menu(&s_DisconnectButton, Localize("Disconnect"), 0, &Button)) { @@ -214,6 +213,173 @@ void CMenus::RenderGame(CUIRect MainView) } } } + + if(g_Config.m_ClTouchControls) + { + ButtonBar2.VSplitLeft(200.0f, &Button, &ButtonBar2); + static char s_TouchControlsEditCheckbox; + if(DoButton_CheckBox(&s_TouchControlsEditCheckbox, Localize("Edit touch controls"), GameClient()->m_TouchControls.IsEditingActive(), &Button)) + { + GameClient()->m_TouchControls.SetEditingActive(!GameClient()->m_TouchControls.IsEditingActive()); + } + + ButtonBar2.VSplitRight(80.0f, &ButtonBar2, &Button); + static CButtonContainer s_CloseButton; + if(DoButton_Menu(&s_CloseButton, Localize("Close"), 0, &Button)) + { + SetActive(false); + } + + ButtonBar2.VSplitRight(5.0f, &ButtonBar2, nullptr); + ButtonBar2.VSplitRight(160.0f, &ButtonBar2, &Button); + static CButtonContainer s_RemoveConsoleButton; + if(DoButton_Menu(&s_RemoveConsoleButton, Localize("Remote console"), 0, &Button)) + { + Console()->ExecuteLine("toggle_remote_console"); + } + + ButtonBar2.VSplitRight(5.0f, &ButtonBar2, nullptr); + ButtonBar2.VSplitRight(120.0f, &ButtonBar2, &Button); + static CButtonContainer s_LocalConsoleButton; + if(DoButton_Menu(&s_LocalConsoleButton, Localize("Console"), 0, &Button)) + { + Console()->ExecuteLine("toggle_local_console"); + } + + if(GameClient()->m_TouchControls.IsEditingActive()) + { + CUIRect TouchControlsEditor; + MainView.VMargin((MainView.w - 505.0f) / 2.0f, &TouchControlsEditor); + TouchControlsEditor.HMargin((TouchControlsEditor.h - 230.0f) / 2.0f, &TouchControlsEditor); + RenderTouchControlsEditor(TouchControlsEditor); + } + } +} + +void CMenus::RenderTouchControlsEditor(CUIRect MainView) +{ + CUIRect Label, Button, Row; + MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_ALL, 10.0f); + MainView.Margin(10.0f, &MainView); + + MainView.HSplitTop(25.0f, &Label, &MainView); + MainView.HSplitTop(5.0f, nullptr, &MainView); + Ui()->DoLabel(&Label, Localize("Edit touch controls"), 20.0f, TEXTALIGN_MC); + + MainView.HSplitTop(25.0f, &Row, &MainView); + MainView.HSplitTop(5.0f, nullptr, &MainView); + + Row.VSplitLeft(240.0f, &Button, &Row); + static CButtonContainer s_SaveConfigurationButton; + if(DoButton_Menu(&s_SaveConfigurationButton, Localize("Save changes"), GameClient()->m_TouchControls.HasEditingChanges() ? 0 : 1, &Button)) + { + if(GameClient()->m_TouchControls.SaveConfigurationToFile()) + { + GameClient()->m_TouchControls.SetEditingChanges(false); + } + else + { + SWarning Warning(Localize("Error saving touch controls"), Localize("Could not save touch controls to file. See local console for details.")); + Warning.m_AutoHide = false; + Client()->AddWarning(Warning); + } + } + + Row.VSplitLeft(5.0f, nullptr, &Row); + Row.VSplitLeft(240.0f, &Button, &Row); + if(GameClient()->m_TouchControls.HasEditingChanges()) + { + TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f)); + Ui()->DoLabel(&Button, Localize("Unsaved changes"), 14.0f, TEXTALIGN_MC); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + + MainView.HSplitTop(25.0f, &Row, &MainView); + MainView.HSplitTop(5.0f, nullptr, &MainView); + + Row.VSplitLeft(240.0f, &Button, &Row); + static CButtonContainer s_DiscardChangesButton; + if(DoButton_Menu(&s_DiscardChangesButton, Localize("Discard changes"), GameClient()->m_TouchControls.HasEditingChanges() ? 0 : 1, &Button)) + { + PopupConfirm(Localize("Discard changes"), + Localize("Are you sure that you want to discard the current changes to the touch controls?"), + Localize("Yes"), Localize("No"), + &CMenus::PopupConfirmDiscardTouchControlsChanges); + } + + Row.VSplitLeft(5.0f, nullptr, &Row); + Row.VSplitLeft(240.0f, &Button, &Row); + static CButtonContainer s_ResetButton; + if(DoButton_Menu(&s_ResetButton, Localize("Reset to default"), 0, &Button)) + { + PopupConfirm(Localize("Reset to default"), + Localize("Are you sure that you want to reset the touch controls to default?"), + Localize("Yes"), Localize("No"), + &CMenus::PopupConfirmResetTouchControls); + } + + MainView.HSplitTop(25.0f, &Row, &MainView); + MainView.HSplitTop(10.0f, nullptr, &MainView); + + Row.VSplitLeft(240.0f, &Button, &Row); + static CButtonContainer s_ClipboardImportButton; + if(DoButton_Menu(&s_ClipboardImportButton, Localize("Import from clipboard"), 0, &Button)) + { + PopupConfirm(Localize("Import from clipboard"), + Localize("Are you sure that you want to import the touch controls from the clipboard? The will overwrite your current touch controls."), + Localize("Yes"), Localize("No"), + &CMenus::PopupConfirmImportTouchControlsClipboard); + } + + Row.VSplitLeft(5.0f, nullptr, &Row); + Row.VSplitLeft(240.0f, &Button, &Row); + static CButtonContainer s_ClipboardExportButton; + if(DoButton_Menu(&s_ClipboardExportButton, Localize("Export to clipboard"), 0, &Button)) + { + GameClient()->m_TouchControls.SaveConfigurationToClipboard(); + } + + MainView.HSplitTop(25.0f, &Label, &MainView); + MainView.HSplitTop(5.0f, nullptr, &MainView); + Ui()->DoLabel(&Label, Localize("Settings"), 20.0f, TEXTALIGN_MC); + + MainView.HSplitTop(25.0f, &Row, &MainView); + MainView.HSplitTop(5.0f, nullptr, &MainView); + + Row.VSplitLeft(300.0f, &Label, &Row); + Ui()->DoLabel(&Label, Localize("Direct touch input while ingame"), 16.0f, TEXTALIGN_ML); + + Row.VSplitLeft(5.0f, nullptr, &Row); + Row.VSplitLeft(180.0f, &Button, &Row); + const char *apIngameTouchModes[(int)CTouchControls::EDirectTouchIngameMode::NUM_STATES] = {Localize("Disabled", "Direct touch input"), Localize("Active action", "Direct touch input"), Localize("Aim", "Direct touch input"), Localize("Fire", "Direct touch input"), Localize("Hook", "Direct touch input")}; + const CTouchControls::EDirectTouchIngameMode OldDirectTouchIngame = GameClient()->m_TouchControls.DirectTouchIngame(); + static CUi::SDropDownState s_DirectTouchIngameDropDownState; + static CScrollRegion s_DirectTouchIngameDropDownScrollRegion; + s_DirectTouchIngameDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_DirectTouchIngameDropDownScrollRegion; + const CTouchControls::EDirectTouchIngameMode NewDirectTouchIngame = (CTouchControls::EDirectTouchIngameMode)Ui()->DoDropDown(&Button, (int)OldDirectTouchIngame, apIngameTouchModes, std::size(apIngameTouchModes), s_DirectTouchIngameDropDownState); + if(OldDirectTouchIngame != NewDirectTouchIngame) + { + GameClient()->m_TouchControls.SetDirectTouchIngame(NewDirectTouchIngame); + } + + MainView.HSplitTop(25.0f, &Row, &MainView); + MainView.HSplitTop(5.0f, nullptr, &MainView); + + Row.VSplitLeft(300.0f, &Label, &Row); + Ui()->DoLabel(&Label, Localize("Direct touch input while spectating"), 16.0f, TEXTALIGN_ML); + + Row.VSplitLeft(5.0f, nullptr, &Row); + Row.VSplitLeft(180.0f, &Button, &Row); + const char *apSpectateTouchModes[(int)CTouchControls::EDirectTouchSpectateMode::NUM_STATES] = {Localize("Disabled", "Direct touch input"), Localize("Aim", "Direct touch input")}; + const CTouchControls::EDirectTouchSpectateMode OldDirectTouchSpectate = GameClient()->m_TouchControls.DirectTouchSpectate(); + static CUi::SDropDownState s_DirectTouchSpectateDropDownState; + static CScrollRegion s_DirectTouchSpectateDropDownScrollRegion; + s_DirectTouchSpectateDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_DirectTouchSpectateDropDownScrollRegion; + const CTouchControls::EDirectTouchSpectateMode NewDirectTouchSpectate = (CTouchControls::EDirectTouchSpectateMode)Ui()->DoDropDown(&Button, (int)OldDirectTouchSpectate, apSpectateTouchModes, std::size(apSpectateTouchModes), s_DirectTouchSpectateDropDownState); + if(OldDirectTouchSpectate != NewDirectTouchSpectate) + { + GameClient()->m_TouchControls.SetDirectTouchSpectate(NewDirectTouchSpectate); + } } void CMenus::PopupConfirmDisconnect() @@ -227,6 +393,57 @@ void CMenus::PopupConfirmDisconnectDummy() SetActive(false); } +void CMenus::PopupConfirmDiscardTouchControlsChanges() +{ + if(GameClient()->m_TouchControls.LoadConfigurationFromFile(IStorage::TYPE_ALL)) + { + GameClient()->m_TouchControls.SetEditingChanges(false); + } + else + { + SWarning Warning(Localize("Error loading touch controls"), Localize("Could not load touch controls from file. See local console for details.")); + Warning.m_AutoHide = false; + Client()->AddWarning(Warning); + } +} + +void CMenus::PopupConfirmResetTouchControls() +{ + bool Success = false; + for(int StorageType = IStorage::TYPE_SAVE + 1; StorageType < Storage()->NumPaths(); ++StorageType) + { + if(GameClient()->m_TouchControls.LoadConfigurationFromFile(StorageType)) + { + Success = true; + break; + } + } + if(Success) + { + GameClient()->m_TouchControls.SetEditingChanges(true); + } + else + { + SWarning Warning(Localize("Error loading touch controls"), Localize("Could not load default touch controls from file. See local console for details.")); + Warning.m_AutoHide = false; + Client()->AddWarning(Warning); + } +} + +void CMenus::PopupConfirmImportTouchControlsClipboard() +{ + if(GameClient()->m_TouchControls.LoadConfigurationFromClipboard()) + { + GameClient()->m_TouchControls.SetEditingChanges(true); + } + else + { + SWarning Warning(Localize("Error loading touch controls"), Localize("Could not load touch controls from clipboard. See local console for details.")); + Warning.m_AutoHide = false; + Client()->AddWarning(Warning); + } +} + void CMenus::RenderPlayers(CUIRect MainView) { CUIRect Button, Button2, ButtonBar, PlayerList, Player; @@ -1281,6 +1498,10 @@ void CMenus::RenderGhost(CUIRect MainView) void CMenus::RenderIngameHint() { + // With touch controls enabled there is a Close button in the menu and usually no Escape key available. + if(g_Config.m_ClTouchControls) + return; + float Width = 300 * Graphics()->ScreenAspect(); Graphics()->MapScreen(0, 0, Width, 300); TextRender()->TextColor(1, 1, 1, 1); diff --git a/src/game/client/components/touch_controls.cpp b/src/game/client/components/touch_controls.cpp new file mode 100644 index 00000000000..07942591658 --- /dev/null +++ b/src/game/client/components/touch_controls.cpp @@ -0,0 +1,1568 @@ +#include "touch_controls.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +// TODO: Add user interface to adjust button layout +// TODO: Add "color" property for touch buttons? +// TODO: Add combined weapon picker button that shows all currently available weapons +// TODO: Add "joystick-aim-relative", a virtual joystick that moves the mouse pointer relatively. And add "aim-relative" ingame direct touch input. +// TODO: Add "choice" predefined behavior which shows a selection popup for 2 or more other behaviors? +// TODO: Support changing labels of menu buttons (or support overriding label for all predefined button behaviors)? + +static constexpr const char *const ACTION_NAMES[] = {Localizable("Aim"), Localizable("Fire"), Localizable("Hook")}; +static constexpr const char *const ACTION_SWAP_NAMES[] = {/* unused */ "", Localizable("Active: Fire"), Localizable("Active: Hook")}; +static constexpr const char *const ACTION_COMMANDS[] = {/* unused */ "", "+fire", "+hook"}; + +static constexpr std::chrono::milliseconds LONG_TOUCH_DURATION = 500ms; +static constexpr std::chrono::milliseconds BIND_REPEAT_INITIAL_DELAY = 250ms; +static constexpr std::chrono::nanoseconds BIND_REPEAT_RATE = std::chrono::nanoseconds(1s) / 15; + +static constexpr const char *const CONFIGURATION_FILENAME = "touch_controls.json"; +static constexpr int BUTTON_SIZE_SCALE = 1000000; +static constexpr int BUTTON_SIZE_MINIMUM = 50000; +static constexpr int BUTTON_SIZE_MAXIMUM = 500000; + +/* This is required for the localization script to find the labels of the default bind buttons specified in the configuration file: +Localizable("Move left") Localizable("Move right") Localizable("Jump") Localizable("Prev. weapon") Localizable("Next weapon") +Localizable("Zoom out") Localizable("Default zoom") Localizable("Zoom in") Localizable("Scoreboard") Localizable("Chat") Localizable("Team chat") +Localizable("Vote yes") Localizable("Vote no") Localizable("Toggle dummy") +*/ + +CTouchControls::CTouchButton::CTouchButton(CTouchControls *pTouchControls) : + m_pTouchControls(pTouchControls), + m_VisibilityCached(false) +{ +} + +CTouchControls::CTouchButton::CTouchButton(CTouchButton &&Other) noexcept : + m_pTouchControls(Other.m_pTouchControls), + m_UnitRect(Other.m_UnitRect), + m_Shape(Other.m_Shape), + m_vVisibilities(Other.m_vVisibilities), + m_pBehavior(std::move(Other.m_pBehavior)), + m_VisibilityCached(false) +{ + Other.m_pTouchControls = nullptr; +} + +CTouchControls::CTouchButton &CTouchControls::CTouchButton::operator=(CTouchButton &&Other) noexcept +{ + m_pTouchControls = Other.m_pTouchControls; + Other.m_pTouchControls = nullptr; + m_UnitRect = Other.m_UnitRect; + m_Shape = Other.m_Shape; + m_vVisibilities = Other.m_vVisibilities; + m_pBehavior = std::move(Other.m_pBehavior); + m_VisibilityCached = false; + return *this; +} + +void CTouchControls::CTouchButton::UpdatePointers() +{ + m_pBehavior->Init(this); +} + +void CTouchControls::CTouchButton::UpdateScreenFromUnitRect() +{ + const vec2 ScreenSize = m_pTouchControls->CalculateScreenSize(); + m_ScreenRect.x = m_UnitRect.m_X * ScreenSize.x / BUTTON_SIZE_SCALE; + m_ScreenRect.y = m_UnitRect.m_Y * ScreenSize.y / BUTTON_SIZE_SCALE; + m_ScreenRect.w = m_UnitRect.m_W * ScreenSize.x / BUTTON_SIZE_SCALE; + m_ScreenRect.h = m_UnitRect.m_H * ScreenSize.y / BUTTON_SIZE_SCALE; + + // Enforce circle shape so the screen rect can be used for mapping the touch input position + if(m_Shape == EButtonShape::CIRCLE) + { + if(m_ScreenRect.h > m_ScreenRect.w) + { + m_ScreenRect.y += (m_ScreenRect.h - m_ScreenRect.w) / 2.0f; + m_ScreenRect.h = m_ScreenRect.w; + } + else if(m_ScreenRect.w > m_ScreenRect.h) + { + m_ScreenRect.x += (m_ScreenRect.w - m_ScreenRect.h) / 2.0f; + m_ScreenRect.w = m_ScreenRect.h; + } + } +} + +void CTouchControls::CTouchButton::UpdateBackgroundCorners() +{ + if(m_Shape != EButtonShape::RECT) + { + m_BackgroundCorners = IGraphics::CORNER_NONE; + return; + } + + // Determine rounded corners based on button layout + m_BackgroundCorners = IGraphics::CORNER_ALL; + + if(m_UnitRect.m_X == 0) + { + m_BackgroundCorners &= ~IGraphics::CORNER_L; + } + if(m_UnitRect.m_X + m_UnitRect.m_W == BUTTON_SIZE_SCALE) + { + m_BackgroundCorners &= ~IGraphics::CORNER_R; + } + if(m_UnitRect.m_Y == 0) + { + m_BackgroundCorners &= ~IGraphics::CORNER_T; + } + if(m_UnitRect.m_Y + m_UnitRect.m_H == BUTTON_SIZE_SCALE) + { + m_BackgroundCorners &= ~IGraphics::CORNER_B; + } + + const auto &&PointInOrOnRect = [](ivec2 Point, CUnitRect Rect) { + return Point.x >= Rect.m_X && Point.x <= Rect.m_X + Rect.m_W && Point.y >= Rect.m_Y && Point.y <= Rect.m_Y + Rect.m_H; + }; + for(const CTouchButton &OtherButton : m_pTouchControls->m_vTouchButtons) + { + if(&OtherButton == this || OtherButton.m_Shape != EButtonShape::RECT) + continue; + // TODO: This does not consider that button visibilities can change independently, also update corners when any visibility changed + const bool ExcludingVisibilities = std::any_of(OtherButton.m_vVisibilities.begin(), OtherButton.m_vVisibilities.end(), [&](const CButtonVisibility &OtherVisibility) { + return std::any_of(m_vVisibilities.begin(), m_vVisibilities.end(), [&](const CButtonVisibility &OurVisibility) { + return OtherVisibility.m_Type == OurVisibility.m_Type && OtherVisibility.m_Parity != OurVisibility.m_Parity; + }); + }); + if(ExcludingVisibilities) + continue; + + if((m_BackgroundCorners & IGraphics::CORNER_TL) && PointInOrOnRect(ivec2(m_UnitRect.m_X, m_UnitRect.m_Y), OtherButton.m_UnitRect)) + { + m_BackgroundCorners &= ~IGraphics::CORNER_TL; + } + if((m_BackgroundCorners & IGraphics::CORNER_TR) && PointInOrOnRect(ivec2(m_UnitRect.m_X + m_UnitRect.m_W, m_UnitRect.m_Y), OtherButton.m_UnitRect)) + { + m_BackgroundCorners &= ~IGraphics::CORNER_TR; + } + if((m_BackgroundCorners & IGraphics::CORNER_BL) && PointInOrOnRect(ivec2(m_UnitRect.m_X, m_UnitRect.m_Y + m_UnitRect.m_H), OtherButton.m_UnitRect)) + { + m_BackgroundCorners &= ~IGraphics::CORNER_BL; + } + if((m_BackgroundCorners & IGraphics::CORNER_BR) && PointInOrOnRect(ivec2(m_UnitRect.m_X + m_UnitRect.m_W, m_UnitRect.m_Y + m_UnitRect.m_H), OtherButton.m_UnitRect)) + { + m_BackgroundCorners &= ~IGraphics::CORNER_BR; + } + if(m_BackgroundCorners == IGraphics::CORNER_NONE) + { + break; + } + } +} + +vec2 CTouchControls::CTouchButton::ClampTouchPosition(vec2 TouchPosition) const +{ + switch(m_Shape) + { + case EButtonShape::RECT: + { + TouchPosition.x = clamp(TouchPosition.x, m_ScreenRect.x, m_ScreenRect.x + m_ScreenRect.w); + TouchPosition.y = clamp(TouchPosition.y, m_ScreenRect.y, m_ScreenRect.y + m_ScreenRect.h); + break; + } + case EButtonShape::CIRCLE: + { + const vec2 Center = m_ScreenRect.Center(); + const float MaxLength = minimum(m_ScreenRect.w, m_ScreenRect.h) / 2.0f; + const vec2 TouchDirection = TouchPosition - Center; + const float Length = length(TouchDirection); + if(Length > MaxLength) + { + TouchPosition = normalize_pre_length(TouchDirection, Length) * MaxLength + Center; + } + break; + } + default: + dbg_assert(false, "Unhandled shape"); + break; + } + return TouchPosition; +} + +bool CTouchControls::CTouchButton::IsInside(vec2 TouchPosition) const +{ + switch(m_Shape) + { + case EButtonShape::RECT: + return m_ScreenRect.Inside(TouchPosition); + case EButtonShape::CIRCLE: + return distance(TouchPosition, m_ScreenRect.Center()) <= minimum(m_ScreenRect.w, m_ScreenRect.h) / 2.0f; + default: + dbg_assert(false, "Unhandled shape"); + return false; + } +} + +void CTouchControls::CTouchButton::UpdateVisibility() +{ + const bool PrevVisibility = m_VisibilityCached; + m_VisibilityCached = m_pTouchControls->m_EditingActive || std::all_of(m_vVisibilities.begin(), m_vVisibilities.end(), [&](CButtonVisibility Visibility) { + return m_pTouchControls->m_aVisibilityFunctions[(int)Visibility.m_Type].m_Function() == Visibility.m_Parity; + }); + if(m_VisibilityCached && !PrevVisibility) + { + m_VisibilityStartTime = time_get_nanoseconds(); + } +} + +bool CTouchControls::CTouchButton::IsVisible() const +{ + return m_VisibilityCached; +} + +// TODO: Optimization: Use text and quad containers for rendering +void CTouchControls::CTouchButton::Render() const +{ + const ColorRGBA ButtonColor = m_pBehavior->IsActive() ? ColorRGBA(0.2f, 0.2f, 0.2f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f); + + switch(m_Shape) + { + case EButtonShape::RECT: + { + m_ScreenRect.Draw(ButtonColor, m_BackgroundCorners, 10.0f); + break; + } + case EButtonShape::CIRCLE: + { + const vec2 Center = m_ScreenRect.Center(); + const float Radius = minimum(m_ScreenRect.w, m_ScreenRect.h) / 2.0f; + m_pTouchControls->Graphics()->TextureClear(); + m_pTouchControls->Graphics()->QuadsBegin(); + m_pTouchControls->Graphics()->SetColor(ButtonColor); + m_pTouchControls->Graphics()->DrawCircle(Center.x, Center.y, Radius, maximum(round_truncate(Radius / 4.0f) & ~1, 32)); + m_pTouchControls->Graphics()->QuadsEnd(); + break; + } + default: + dbg_assert(false, "Unhandled shape"); + break; + } + + const float FontSize = 22.0f; + CButtonLabel LabelData = m_pBehavior->GetLabel(); + CUIRect LabelRect; + m_ScreenRect.Margin(10.0f, &LabelRect); + SLabelProperties LabelProps; + LabelProps.m_MaxWidth = LabelRect.w; + if(LabelData.m_Type == CButtonLabel::EType::ICON) + { + m_pTouchControls->TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + m_pTouchControls->TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); + m_pTouchControls->Ui()->DoLabel(&LabelRect, LabelData.m_pLabel, FontSize, TEXTALIGN_MC, LabelProps); + m_pTouchControls->TextRender()->SetRenderFlags(0); + m_pTouchControls->TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + } + else + { + const char *pLabel = LabelData.m_Type == CButtonLabel::EType::LOCALIZED ? Localize(LabelData.m_pLabel) : LabelData.m_pLabel; + m_pTouchControls->Ui()->DoLabel(&LabelRect, pLabel, FontSize, TEXTALIGN_MC, LabelProps); + } +} + +void CTouchControls::CTouchButton::WriteToConfiguration(CJsonWriter *pWriter) +{ + char aBuf[256]; + + pWriter->BeginObject(); + + pWriter->WriteAttribute("x"); + pWriter->WriteIntValue(m_UnitRect.m_X); + pWriter->WriteAttribute("y"); + pWriter->WriteIntValue(m_UnitRect.m_Y); + pWriter->WriteAttribute("w"); + pWriter->WriteIntValue(m_UnitRect.m_W); + pWriter->WriteAttribute("h"); + pWriter->WriteIntValue(m_UnitRect.m_H); + + pWriter->WriteAttribute("shape"); + pWriter->WriteStrValue(SHAPE_NAMES[(int)m_Shape]); + + pWriter->WriteAttribute("visibilities"); + pWriter->BeginArray(); + for(CButtonVisibility Visibility : m_vVisibilities) + { + str_format(aBuf, sizeof(aBuf), "%s%s", Visibility.m_Parity ? "" : "-", m_pTouchControls->m_aVisibilityFunctions[(int)Visibility.m_Type].m_pId); + pWriter->WriteStrValue(aBuf); + } + pWriter->EndArray(); + + pWriter->WriteAttribute("behavior"); + pWriter->BeginObject(); + m_pBehavior->WriteToConfiguration(pWriter); + pWriter->EndObject(); + + pWriter->EndObject(); +} + +void CTouchControls::CTouchButtonBehavior::Init(CTouchButton *pTouchButton) +{ + m_pTouchButton = pTouchButton; + m_pTouchControls = pTouchButton->m_pTouchControls; +} + +void CTouchControls::CTouchButtonBehavior::Reset() +{ + m_Active = false; +} + +void CTouchControls::CTouchButtonBehavior::SetActive(const IInput::CTouchFingerState &FingerState) +{ + const vec2 ScreenSize = m_pTouchControls->CalculateScreenSize(); + const CUIRect ButtonScreenRect = m_pTouchButton->m_ScreenRect; + const vec2 Position = (m_pTouchButton->ClampTouchPosition(FingerState.m_Position * ScreenSize) - ButtonScreenRect.TopLeft()) / ButtonScreenRect.Size(); + const vec2 Delta = FingerState.m_Delta * ScreenSize / ButtonScreenRect.Size(); + if(!m_Active) + { + m_Active = true; + m_ActivePosition = Position; + m_AccumulatedDelta = Delta; + m_ActivationStartTime = time_get_nanoseconds(); + m_Finger = FingerState.m_Finger; + OnActivate(); + } + else if(m_Finger == FingerState.m_Finger) + { + m_ActivePosition = Position; + m_AccumulatedDelta += Delta; + OnUpdate(); + } + else + { + dbg_assert(false, "Touch button must be inactive or use same finger"); + } +} + +void CTouchControls::CTouchButtonBehavior::SetInactive() +{ + if(m_Active) + { + m_Active = false; + OnDeactivate(); + } +} + +bool CTouchControls::CTouchButtonBehavior::IsActive() const +{ + return m_Active; +} + +bool CTouchControls::CTouchButtonBehavior::IsActive(const IInput::CTouchFinger &Finger) const +{ + return m_Active && m_Finger == Finger; +} + +void CTouchControls::CPredefinedTouchButtonBehavior::WriteToConfiguration(CJsonWriter *pWriter) +{ + pWriter->WriteAttribute("type"); + pWriter->WriteStrValue(BEHAVIOR_TYPE); + + pWriter->WriteAttribute("id"); + pWriter->WriteStrValue(m_pId); +} + +// Ingame menu button: always opens ingame menu. +CTouchControls::CButtonLabel CTouchControls::CIngameMenuTouchButtonBehavior::GetLabel() const +{ + return {CButtonLabel::EType::ICON, "\xEF\x85\x8E"}; +} + +void CTouchControls::CIngameMenuTouchButtonBehavior::OnDeactivate() +{ + m_pTouchControls->GameClient()->m_Menus.SetActive(true); +} + +// Extra menu button: +// - Short press: show/hide additional buttons (toggle extra-menu visibilities) +// - Long press: open ingame menu +CTouchControls::CExtraMenuTouchButtonBehavior::CExtraMenuTouchButtonBehavior(int Number) : + CPredefinedTouchButtonBehavior(BEHAVIOR_ID), + m_Number(Number) +{ + if(m_Number == 0) + { + str_copy(m_aLabel, "\xEF\x83\x89"); + } + else + { + str_format(m_aLabel, sizeof(m_aLabel), "\xEF\x83\x89%d", m_Number + 1); + } +} + +CTouchControls::CButtonLabel CTouchControls::CExtraMenuTouchButtonBehavior::GetLabel() const +{ + if(m_Active && time_get_nanoseconds() - m_ActivationStartTime >= LONG_TOUCH_DURATION) + { + return {CButtonLabel::EType::ICON, "\xEF\x95\x90"}; + } + else + { + return {CButtonLabel::EType::ICON, m_aLabel}; + } +} + +void CTouchControls::CExtraMenuTouchButtonBehavior::OnDeactivate() +{ + if(time_get_nanoseconds() - m_ActivationStartTime >= LONG_TOUCH_DURATION) + { + m_pTouchControls->GameClient()->m_Menus.SetActive(true); + } + else + { + m_pTouchControls->m_aExtraMenuActive[m_Number] = !m_pTouchControls->m_aExtraMenuActive[m_Number]; + } +} + +void CTouchControls::CExtraMenuTouchButtonBehavior::WriteToConfiguration(CJsonWriter *pWriter) +{ + CPredefinedTouchButtonBehavior::WriteToConfiguration(pWriter); + + pWriter->WriteAttribute("number"); + pWriter->WriteIntValue(m_Number + 1); +} + +// Emoticon button: keeps the emoticon HUD open, next touch in emoticon HUD will close it again. +CTouchControls::CButtonLabel CTouchControls::CEmoticonTouchButtonBehavior::GetLabel() const +{ + return {CButtonLabel::EType::LOCALIZED, Localizable("Emoticon")}; +} + +void CTouchControls::CEmoticonTouchButtonBehavior::OnDeactivate() +{ + m_pTouchControls->Console()->ExecuteLineStroked(1, "+emote"); +} + +// Spectate button: keeps the spectate menu open, next touch in spectate menu will close it again. +CTouchControls::CButtonLabel CTouchControls::CSpectateTouchButtonBehavior::GetLabel() const +{ + return {CButtonLabel::EType::LOCALIZED, Localizable("Spectator mode")}; +} + +void CTouchControls::CSpectateTouchButtonBehavior::OnDeactivate() +{ + m_pTouchControls->Console()->ExecuteLineStroked(1, "+spectate"); +} + +// Swap action button: +// - If joystick is currently active with one action: activate the other action. +// - Else: swap active action. +CTouchControls::CButtonLabel CTouchControls::CSwapActionTouchButtonBehavior::GetLabel() const +{ + if(m_ActiveAction != NUM_ACTIONS) + { + return {CButtonLabel::EType::LOCALIZED, ACTION_NAMES[m_ActiveAction]}; + } + else if(m_pTouchControls->m_pPrimaryJoystickTouchButtonBehavior != nullptr && + m_pTouchControls->m_pPrimaryJoystickTouchButtonBehavior->ActiveAction() != NUM_ACTIONS) + { + return {CButtonLabel::EType::LOCALIZED, ACTION_NAMES[m_pTouchControls->NextActiveAction(m_pTouchControls->m_pPrimaryJoystickTouchButtonBehavior->ActiveAction())]}; + } + return {CButtonLabel::EType::LOCALIZED, ACTION_SWAP_NAMES[m_pTouchControls->m_ActionSelected]}; +} + +void CTouchControls::CSwapActionTouchButtonBehavior::OnActivate() +{ + if(m_pTouchControls->m_pPrimaryJoystickTouchButtonBehavior != nullptr && + m_pTouchControls->m_pPrimaryJoystickTouchButtonBehavior->ActiveAction() != NUM_ACTIONS) + { + m_ActiveAction = m_pTouchControls->NextActiveAction(m_pTouchControls->m_pPrimaryJoystickTouchButtonBehavior->ActiveAction()); + m_pTouchControls->Console()->ExecuteLineStroked(1, ACTION_COMMANDS[m_ActiveAction]); + } + else + { + m_pTouchControls->m_ActionSelected = m_pTouchControls->NextActiveAction(m_pTouchControls->m_ActionSelected); + } +} + +void CTouchControls::CSwapActionTouchButtonBehavior::OnDeactivate() +{ + if(m_ActiveAction != NUM_ACTIONS) + { + m_pTouchControls->Console()->ExecuteLineStroked(0, ACTION_COMMANDS[m_ActiveAction]); + m_ActiveAction = NUM_ACTIONS; + } +} + +// Use action button: always uses the active action. +CTouchControls::CButtonLabel CTouchControls::CUseActionTouchButtonBehavior::GetLabel() const +{ + if(m_ActiveAction != NUM_ACTIONS) + { + return {CButtonLabel::EType::LOCALIZED, ACTION_NAMES[m_ActiveAction]}; + } + return {CButtonLabel::EType::LOCALIZED, ACTION_NAMES[m_pTouchControls->m_ActionSelected]}; +} + +void CTouchControls::CUseActionTouchButtonBehavior::OnActivate() +{ + m_ActiveAction = m_pTouchControls->m_ActionSelected; + m_pTouchControls->Console()->ExecuteLineStroked(1, ACTION_COMMANDS[m_ActiveAction]); +} + +void CTouchControls::CUseActionTouchButtonBehavior::OnDeactivate() +{ + m_pTouchControls->Console()->ExecuteLineStroked(0, ACTION_COMMANDS[m_ActiveAction]); + m_ActiveAction = NUM_ACTIONS; +} + +// Generic joystick button behavior: aim with virtual joystick and use action (defined by subclass). +CTouchControls::CButtonLabel CTouchControls::CJoystickTouchButtonBehavior::GetLabel() const +{ + if(m_ActiveAction != NUM_ACTIONS) + { + return {CButtonLabel::EType::LOCALIZED, ACTION_NAMES[m_ActiveAction]}; + } + return {CButtonLabel::EType::LOCALIZED, ACTION_NAMES[SelectedAction()]}; +} + +void CTouchControls::CJoystickTouchButtonBehavior::OnActivate() +{ + m_ActiveAction = SelectedAction(); + OnUpdate(); + if(m_ActiveAction != ACTION_AIM) + { + m_pTouchControls->Console()->ExecuteLineStroked(1, ACTION_COMMANDS[m_ActiveAction]); + } +} + +void CTouchControls::CJoystickTouchButtonBehavior::OnDeactivate() +{ + if(m_ActiveAction != ACTION_AIM) + { + m_pTouchControls->Console()->ExecuteLineStroked(0, ACTION_COMMANDS[m_ActiveAction]); + } + m_ActiveAction = NUM_ACTIONS; +} + +void CTouchControls::CJoystickTouchButtonBehavior::OnUpdate() +{ + CControls &Controls = m_pTouchControls->GameClient()->m_Controls; + if(m_pTouchControls->GameClient()->m_Snap.m_SpecInfo.m_Active) + { + vec2 WorldScreenSize; + m_pTouchControls->RenderTools()->CalcScreenParams(m_pTouchControls->Graphics()->ScreenAspect(), m_pTouchControls->GameClient()->m_Camera.m_Zoom, &WorldScreenSize.x, &WorldScreenSize.y); + Controls.m_aMousePos[g_Config.m_ClDummy] += -m_AccumulatedDelta * WorldScreenSize; + Controls.m_aMousePos[g_Config.m_ClDummy].x = clamp(Controls.m_aMousePos[g_Config.m_ClDummy].x, -201.0f * 32, (m_pTouchControls->Collision()->GetWidth() + 201.0f) * 32.0f); + Controls.m_aMousePos[g_Config.m_ClDummy].y = clamp(Controls.m_aMousePos[g_Config.m_ClDummy].y, -201.0f * 32, (m_pTouchControls->Collision()->GetHeight() + 201.0f) * 32.0f); + m_AccumulatedDelta = vec2(0.0f, 0.0f); + } + else + { + const vec2 AbsolutePosition = (m_ActivePosition - vec2(0.5f, 0.5f)) * 2.0f; + Controls.m_aMousePos[g_Config.m_ClDummy] = AbsolutePosition * (Controls.GetMaxMouseDistance() - Controls.GetMinMouseDistance()) + normalize(AbsolutePosition) * Controls.GetMinMouseDistance(); + if(length(Controls.m_aMousePos[g_Config.m_ClDummy]) < 0.001f) + { + Controls.m_aMousePos[g_Config.m_ClDummy].x = 0.001f; + Controls.m_aMousePos[g_Config.m_ClDummy].y = 0.0f; + } + } +} + +// Joystick that uses the active action. Registers itself as the primary joystick. +void CTouchControls::CJoystickActionTouchButtonBehavior::Init(CTouchButton *pTouchButton) +{ + CPredefinedTouchButtonBehavior::Init(pTouchButton); + m_pTouchControls->m_pPrimaryJoystickTouchButtonBehavior = this; +} + +int CTouchControls::CJoystickActionTouchButtonBehavior::SelectedAction() const +{ + return m_pTouchControls->m_ActionSelected; +} + +// Joystick that only aims. +int CTouchControls::CJoystickAimTouchButtonBehavior::SelectedAction() const +{ + return ACTION_AIM; +} + +// Joystick that always uses fire. +int CTouchControls::CJoystickFireTouchButtonBehavior::SelectedAction() const +{ + return ACTION_FIRE; +} + +// Joystick that always uses hook. +int CTouchControls::CJoystickHookTouchButtonBehavior::SelectedAction() const +{ + return ACTION_HOOK; +} + +// Bind button behavior that executes a command like a bind. +CTouchControls::CButtonLabel CTouchControls::CBindTouchButtonBehavior::GetLabel() const +{ + return {m_LabelType, m_Label.c_str()}; +} + +void CTouchControls::CBindTouchButtonBehavior::OnActivate() +{ + m_pTouchControls->Console()->ExecuteLineStroked(1, m_Command.c_str()); + m_Repeating = false; +} + +void CTouchControls::CBindTouchButtonBehavior::OnDeactivate() +{ + m_pTouchControls->Console()->ExecuteLineStroked(0, m_Command.c_str()); +} + +void CTouchControls::CBindTouchButtonBehavior::OnUpdate() +{ + const auto Now = time_get_nanoseconds(); + if(m_Repeating) + { + m_AccumulatedRepeatingTime += Now - m_LastUpdateTime; + m_LastUpdateTime = Now; + if(m_AccumulatedRepeatingTime >= BIND_REPEAT_RATE) + { + m_AccumulatedRepeatingTime -= BIND_REPEAT_RATE; + m_pTouchControls->Console()->ExecuteLineStroked(1, m_Command.c_str()); + } + } + else if(Now - m_ActivationStartTime >= BIND_REPEAT_INITIAL_DELAY) + { + m_Repeating = true; + m_LastUpdateTime = Now; + m_AccumulatedRepeatingTime = 0ns; + } +} + +void CTouchControls::CBindTouchButtonBehavior::WriteToConfiguration(CJsonWriter *pWriter) +{ + pWriter->WriteAttribute("type"); + pWriter->WriteStrValue(BEHAVIOR_TYPE); + + pWriter->WriteAttribute("label"); + pWriter->WriteStrValue(m_Label.c_str()); + + pWriter->WriteAttribute("label-type"); + pWriter->WriteStrValue(LABEL_TYPE_NAMES[(int)m_LabelType]); + + pWriter->WriteAttribute("command"); + pWriter->WriteStrValue(m_Command.c_str()); +} + +// Bind button behavior that switches between executing one of two or more console commands. +CTouchControls::CButtonLabel CTouchControls::CBindToggleTouchButtonBehavior::GetLabel() const +{ + const auto &ActiveCommand = m_vCommands[m_ActiveCommandIndex]; + return {ActiveCommand.m_LabelType, ActiveCommand.m_Label.c_str()}; +} + +void CTouchControls::CBindToggleTouchButtonBehavior::OnActivate() +{ + m_pTouchControls->Console()->ExecuteLine(m_vCommands[m_ActiveCommandIndex].m_Command.c_str()); + m_ActiveCommandIndex = (m_ActiveCommandIndex + 1) % m_vCommands.size(); +} + +void CTouchControls::CBindToggleTouchButtonBehavior::WriteToConfiguration(CJsonWriter *pWriter) +{ + pWriter->WriteAttribute("type"); + pWriter->WriteStrValue(BEHAVIOR_TYPE); + + pWriter->WriteAttribute("commands"); + pWriter->BeginArray(); + + for(const auto &Command : m_vCommands) + { + pWriter->BeginObject(); + + pWriter->WriteAttribute("label"); + pWriter->WriteStrValue(Command.m_Label.c_str()); + + pWriter->WriteAttribute("label-type"); + pWriter->WriteStrValue(LABEL_TYPE_NAMES[(int)Command.m_LabelType]); + + pWriter->WriteAttribute("command"); + pWriter->WriteStrValue(Command.m_Command.c_str()); + + pWriter->EndObject(); + } + + pWriter->EndArray(); +} + +void CTouchControls::OnInit() +{ + InitVisibilityFunctions(); + if(!LoadConfigurationFromFile(IStorage::TYPE_ALL)) + { + Client()->AddWarning(SWarning(Localize("Error loading touch controls"), Localize("Could not load touch controls from file. See local console for details."))); + } +} + +void CTouchControls::OnReset() +{ + ResetButtons(); + m_EditingActive = false; +} + +void CTouchControls::OnWindowResize() +{ + ResetButtons(); + for(CTouchButton &TouchButton : m_vTouchButtons) + { + TouchButton.UpdateScreenFromUnitRect(); + } +} + +bool CTouchControls::OnTouchState(const std::vector &vTouchFingerStates) +{ + if(!g_Config.m_ClTouchControls) + return false; + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return false; + if(GameClient()->m_Chat.IsActive() || + !GameClient()->m_GameConsole.IsClosed() || + GameClient()->m_Menus.IsActive() || + GameClient()->m_Emoticon.IsActive() || + GameClient()->m_Spectator.IsActive()) + { + ResetButtons(); + return false; + } + + UpdateButtons(vTouchFingerStates); + return true; +} + +void CTouchControls::OnRender() +{ + if(!g_Config.m_ClTouchControls) + return; + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(GameClient()->m_Chat.IsActive() || + GameClient()->m_Emoticon.IsActive() || + GameClient()->m_Spectator.IsActive()) + { + return; + } + + const vec2 ScreenSize = CalculateScreenSize(); + Graphics()->MapScreen(0.0f, 0.0f, ScreenSize.x, ScreenSize.y); + + RenderButtons(); +} + +bool CTouchControls::LoadConfigurationFromFile(int StorageType) +{ + void *pFileData; + unsigned FileLength; + if(!Storage()->ReadFile(CONFIGURATION_FILENAME, StorageType, &pFileData, &FileLength)) + { + log_error("touch_controls", "Failed to read configuration from '%s'", CONFIGURATION_FILENAME); + return false; + } + + const bool Result = ParseConfiguration(pFileData, FileLength); + free(pFileData); + return Result; +} + +bool CTouchControls::LoadConfigurationFromClipboard() +{ + std::string Clipboard = Input()->GetClipboardText(); + return ParseConfiguration(Clipboard.c_str(), Clipboard.size()); +} + +bool CTouchControls::SaveConfigurationToFile() +{ + IOHANDLE File = Storage()->OpenFile(CONFIGURATION_FILENAME, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(!File) + { + log_error("touch_controls", "Failed to open '%s' for writing configuration", CONFIGURATION_FILENAME); + return false; + } + + CJsonFileWriter Writer(File); + WriteConfiguration(&Writer); + return true; +} + +void CTouchControls::SaveConfigurationToClipboard() +{ + CJsonStringWriter Writer; + WriteConfiguration(&Writer); + std::string ConfigurationString = Writer.GetOutputString(); + Input()->SetClipboardText(ConfigurationString.c_str()); +} + +void CTouchControls::InitVisibilityFunctions() +{ + m_aVisibilityFunctions[(int)EButtonVisibility::INGAME].m_pId = "ingame"; + m_aVisibilityFunctions[(int)EButtonVisibility::INGAME].m_Function = [&]() { + return !GameClient()->m_Snap.m_SpecInfo.m_Active; + }; + m_aVisibilityFunctions[(int)EButtonVisibility::ZOOM_ALLOWED].m_pId = "zoom-allowed"; + m_aVisibilityFunctions[(int)EButtonVisibility::ZOOM_ALLOWED].m_Function = [&]() { + return GameClient()->m_Camera.ZoomAllowed(); + }; + m_aVisibilityFunctions[(int)EButtonVisibility::VOTE_ACTIVE].m_pId = "vote-active"; + m_aVisibilityFunctions[(int)EButtonVisibility::VOTE_ACTIVE].m_Function = [&]() { + return GameClient()->m_Voting.IsVoting(); + }; + m_aVisibilityFunctions[(int)EButtonVisibility::DUMMY_ALLOWED].m_pId = "dummy-allowed"; + m_aVisibilityFunctions[(int)EButtonVisibility::DUMMY_ALLOWED].m_Function = [&]() { + return Client()->DummyAllowed(); + }; + m_aVisibilityFunctions[(int)EButtonVisibility::DUMMY_CONNECTED].m_pId = "dummy-connected"; + m_aVisibilityFunctions[(int)EButtonVisibility::DUMMY_CONNECTED].m_Function = [&]() { + return Client()->DummyConnected(); + }; + m_aVisibilityFunctions[(int)EButtonVisibility::RCON_AUTHED].m_pId = "rcon-authed"; + m_aVisibilityFunctions[(int)EButtonVisibility::RCON_AUTHED].m_Function = [&]() { + return Client()->RconAuthed(); + }; + m_aVisibilityFunctions[(int)EButtonVisibility::DEMO_PLAYER].m_pId = "demo-player"; + m_aVisibilityFunctions[(int)EButtonVisibility::DEMO_PLAYER].m_Function = [&]() { + return Client()->State() == IClient::STATE_DEMOPLAYBACK; + }; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_1].m_pId = "extra-menu"; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_1].m_Function = [&]() { + return m_aExtraMenuActive[0]; + }; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_2].m_pId = "extra-menu-2"; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_2].m_Function = [&]() { + return m_aExtraMenuActive[1]; + }; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_3].m_pId = "extra-menu-3"; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_3].m_Function = [&]() { + return m_aExtraMenuActive[2]; + }; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_4].m_pId = "extra-menu-4"; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_4].m_Function = [&]() { + return m_aExtraMenuActive[3]; + }; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_5].m_pId = "extra-menu-5"; + m_aVisibilityFunctions[(int)EButtonVisibility::EXTRA_MENU_5].m_Function = [&]() { + return m_aExtraMenuActive[4]; + }; +} + +int CTouchControls::NextActiveAction(int Action) const +{ + switch(Action) + { + case ACTION_FIRE: + return ACTION_HOOK; + case ACTION_HOOK: + return ACTION_FIRE; + default: + dbg_assert(false, "Action invalid for NextActiveAction"); + return NUM_ACTIONS; + } +} + +int CTouchControls::NextDirectTouchAction() const +{ + if(m_pClient->m_Snap.m_SpecInfo.m_Active) + { + switch(m_DirectTouchSpectate) + { + case EDirectTouchSpectateMode::DISABLED: + return NUM_ACTIONS; + case EDirectTouchSpectateMode::AIM: + return ACTION_AIM; + default: + dbg_assert(false, "m_DirectTouchSpectate invalid"); + return NUM_ACTIONS; + } + } + else + { + switch(m_DirectTouchIngame) + { + case EDirectTouchIngameMode::DISABLED: + return NUM_ACTIONS; + case EDirectTouchIngameMode::ACTION: + return m_ActionSelected; + case EDirectTouchIngameMode::AIM: + return ACTION_AIM; + case EDirectTouchIngameMode::FIRE: + return ACTION_FIRE; + case EDirectTouchIngameMode::HOOK: + return ACTION_HOOK; + default: + dbg_assert(false, "m_DirectTouchIngame invalid"); + return NUM_ACTIONS; + } + } +} + +void CTouchControls::UpdateButtons(const std::vector &vTouchFingerStates) +{ + // Update cached button visibilities and store time that buttons become visible. + for(CTouchButton &TouchButton : m_vTouchButtons) + { + TouchButton.UpdateVisibility(); + } + + const int DirectTouchAction = NextDirectTouchAction(); + const vec2 ScreenSize = CalculateScreenSize(); + + std::vector vRemainingTouchFingerStates = vTouchFingerStates; + + // Remove remaining finger states for fingers which are responsible for active actions + // and release action when the finger responsible for it is not pressed down anymore. + bool GotDirectFingerState = false; // Whether DirectFingerState is valid + IInput::CTouchFingerState DirectFingerState{}; // The finger that will be used to update the mouse position + for(int Action = ACTION_AIM; Action < NUM_ACTIONS; ++Action) + { + if(!m_aDirectTouchActionStates[Action].m_Active) + { + continue; + } + + const auto ActiveFinger = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchFingerState.m_Finger == m_aDirectTouchActionStates[Action].m_Finger; + }); + if(ActiveFinger == vRemainingTouchFingerStates.end() || DirectTouchAction == NUM_ACTIONS) + { + m_aDirectTouchActionStates[Action].m_Active = false; + if(Action != ACTION_AIM) + { + Console()->ExecuteLineStroked(0, ACTION_COMMANDS[Action]); + } + } + else + { + if(Action == m_DirectTouchLastAction) + { + GotDirectFingerState = true; + DirectFingerState = *ActiveFinger; + } + vRemainingTouchFingerStates.erase(ActiveFinger); + } + } + + // Update touch button states after the active action fingers were removed from the vector + // so that current cursor movement can cross over touch buttons without activating them. + + // Activate visible, inactive buttons with hovered finger. Deactivate previous button being + // activated by the same finger. Touch buttons are only activated if they became visible + // before the respective touch finger was pressed down, to prevent repeatedly activating + // overlapping buttons of excluding visibilities. + for(CTouchButton &TouchButton : m_vTouchButtons) + { + if(!TouchButton.IsVisible() || TouchButton.m_pBehavior->IsActive()) + { + continue; + } + const auto FingerInsideButton = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchButton.m_VisibilityStartTime < TouchFingerState.m_PressTime && + TouchButton.IsInside(TouchFingerState.m_Position * ScreenSize); + }); + if(FingerInsideButton == vRemainingTouchFingerStates.end()) + { + continue; + } + const auto OtherHoveredTouchButton = std::find_if(m_vTouchButtons.begin(), m_vTouchButtons.end(), [&](const CTouchButton &Button) { + return &Button != &TouchButton && Button.IsVisible() && Button.IsInside(FingerInsideButton->m_Position * ScreenSize); + }); + if(OtherHoveredTouchButton != m_vTouchButtons.end()) + { + // Do not activate any button if multiple overlapping buttons are hovered. + // TODO: Prevent overlapping buttons entirely when parsing the button configuration? + vRemainingTouchFingerStates.erase(FingerInsideButton); + continue; + } + auto PrevActiveTouchButton = std::find_if(m_vTouchButtons.begin(), m_vTouchButtons.end(), [&](const CTouchButton &Button) { + return Button.m_pBehavior->IsActive(FingerInsideButton->m_Finger); + }); + if(PrevActiveTouchButton != m_vTouchButtons.end()) + { + PrevActiveTouchButton->m_pBehavior->SetInactive(); + } + TouchButton.m_pBehavior->SetActive(*FingerInsideButton); + } + + // Deactivate touch buttons only when the respective finger is released, so touch buttons + // are kept active also if the finger is moved outside the button. + for(CTouchButton &TouchButton : m_vTouchButtons) + { + if(!TouchButton.IsVisible()) + { + TouchButton.m_pBehavior->SetInactive(); + continue; + } + if(!TouchButton.m_pBehavior->IsActive()) + { + continue; + } + const auto ActiveFinger = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchFingerState.m_Finger == TouchButton.m_pBehavior->m_Finger; + }); + if(ActiveFinger == vRemainingTouchFingerStates.end()) + { + TouchButton.m_pBehavior->SetInactive(); + } + else + { + // Update the already active touch button with the current finger state + TouchButton.m_pBehavior->SetActive(*ActiveFinger); + } + } + + // Remove remaining fingers for active buttons after updating the buttons. + for(CTouchButton &TouchButton : m_vTouchButtons) + { + if(!TouchButton.m_pBehavior->IsActive()) + { + continue; + } + const auto ActiveFinger = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchFingerState.m_Finger == TouchButton.m_pBehavior->m_Finger; + }); + dbg_assert(ActiveFinger != vRemainingTouchFingerStates.end(), "Active button finger not found"); + vRemainingTouchFingerStates.erase(ActiveFinger); + } + + // TODO: Support standard gesture to zoom (enabled separately for ingame and spectator) + + // Activate action if there is an unhandled pressed down finger. + int ActivateAction = NUM_ACTIONS; + if(DirectTouchAction != NUM_ACTIONS && !vRemainingTouchFingerStates.empty() && !m_aDirectTouchActionStates[DirectTouchAction].m_Active) + { + GotDirectFingerState = true; + DirectFingerState = vRemainingTouchFingerStates[0]; + vRemainingTouchFingerStates.erase(vRemainingTouchFingerStates.begin()); + m_aDirectTouchActionStates[DirectTouchAction].m_Active = true; + m_aDirectTouchActionStates[DirectTouchAction].m_Finger = DirectFingerState.m_Finger; + m_DirectTouchLastAction = DirectTouchAction; + ActivateAction = DirectTouchAction; + } + + // Update mouse position based on the finger responsible for the last active action. + if(GotDirectFingerState) + { + const float Zoom = m_pClient->m_Snap.m_SpecInfo.m_Active ? m_pClient->m_Camera.m_Zoom : 1.0f; + vec2 WorldScreenSize; + RenderTools()->CalcScreenParams(Graphics()->ScreenAspect(), Zoom, &WorldScreenSize.x, &WorldScreenSize.y); + CControls &Controls = GameClient()->m_Controls; + if(m_pClient->m_Snap.m_SpecInfo.m_Active) + { + Controls.m_aMousePos[g_Config.m_ClDummy] += -DirectFingerState.m_Delta * WorldScreenSize; + Controls.m_aMousePos[g_Config.m_ClDummy].x = clamp(Controls.m_aMousePos[g_Config.m_ClDummy].x, -201.0f * 32, (Collision()->GetWidth() + 201.0f) * 32.0f); + Controls.m_aMousePos[g_Config.m_ClDummy].y = clamp(Controls.m_aMousePos[g_Config.m_ClDummy].y, -201.0f * 32, (Collision()->GetHeight() + 201.0f) * 32.0f); + } + else + { + Controls.m_aMousePos[g_Config.m_ClDummy] = (DirectFingerState.m_Position - vec2(0.5f, 0.5f)) * WorldScreenSize; + } + } + + // Activate action after the mouse position is set. + if(ActivateAction != ACTION_AIM && ActivateAction != NUM_ACTIONS) + { + Console()->ExecuteLineStroked(1, ACTION_COMMANDS[ActivateAction]); + } +} + +void CTouchControls::ResetButtons() +{ + for(CTouchButton &TouchButton : m_vTouchButtons) + { + TouchButton.m_pBehavior->Reset(); + } + for(CActionState &ActionState : m_aDirectTouchActionStates) + { + ActionState.m_Active = false; + } +} + +void CTouchControls::RenderButtons() +{ + for(CTouchButton &TouchButton : m_vTouchButtons) + { + TouchButton.UpdateVisibility(); + if(!TouchButton.IsVisible()) + { + continue; + } + TouchButton.Render(); + } +} + +vec2 CTouchControls::CalculateScreenSize() const +{ + const float ScreenHeight = 400.0f * 3.0f; + const float ScreenWidth = ScreenHeight * Graphics()->ScreenAspect(); + return vec2(ScreenWidth, ScreenHeight); +} + +bool CTouchControls::ParseConfiguration(const void *pFileData, unsigned FileLength) +{ + json_settings JsonSettings{}; + char aError[256]; + json_value *pConfiguration = json_parse_ex(&JsonSettings, static_cast(pFileData), FileLength, aError); + + if(pConfiguration == nullptr) + { + log_error("touch_controls", "Failed to parse configuration (invalid json): '%s'", aError); + return false; + } + if(pConfiguration->type != json_object) + { + log_error("touch_controls", "Failed to parse configuration: root must be an object"); + json_value_free(pConfiguration); + return false; + } + + std::optional ParsedDirectTouchIngame = ParseDirectTouchIngameMode(&(*pConfiguration)["direct-touch-ingame"]); + if(!ParsedDirectTouchIngame.has_value()) + { + json_value_free(pConfiguration); + return false; + } + + std::optional ParsedDirectTouchSpectate = ParseDirectTouchSpectateMode(&(*pConfiguration)["direct-touch-spectate"]); + if(!ParsedDirectTouchSpectate.has_value()) + { + json_value_free(pConfiguration); + return false; + } + + const json_value &TouchButtons = (*pConfiguration)["touch-buttons"]; + if(TouchButtons.type != json_array) + { + log_error("touch_controls", "Failed to parse configuration: attribute 'touch-buttons' must specify an array"); + json_value_free(pConfiguration); + return false; + } + + std::vector vParsedTouchButtons; + vParsedTouchButtons.reserve(TouchButtons.u.array.length); + for(unsigned ButtonIndex = 0; ButtonIndex < TouchButtons.u.array.length; ++ButtonIndex) + { + std::optional ParsedButton = ParseButton(&TouchButtons[ButtonIndex]); + if(!ParsedButton.has_value()) + { + log_error("touch_controls", "Failed to parse configuration: could not parse button at index '%d'", ButtonIndex); + json_value_free(pConfiguration); + return false; + } + + vParsedTouchButtons.push_back(std::move(ParsedButton.value())); + } + + // Parsing successful. Apply parsed configuration. + m_DirectTouchIngame = ParsedDirectTouchIngame.value(); + m_DirectTouchSpectate = ParsedDirectTouchSpectate.value(); + + m_pPrimaryJoystickTouchButtonBehavior = nullptr; + m_vTouchButtons = std::move(vParsedTouchButtons); + for(CTouchButton &TouchButton : m_vTouchButtons) + { + TouchButton.UpdatePointers(); + TouchButton.UpdateScreenFromUnitRect(); + TouchButton.UpdateBackgroundCorners(); + } + + json_value_free(pConfiguration); + + return true; +} + +std::optional CTouchControls::ParseDirectTouchIngameMode(const json_value *pModeValue) +{ + // TODO: Remove json_boolean backwards compatibility + const json_value &DirectTouchIngame = *pModeValue; + if(DirectTouchIngame.type != json_boolean && DirectTouchIngame.type != json_string) + { + log_error("touch_controls", "Failed to parse configuration: attribute 'direct-touch-ingame' must specify a string"); + return {}; + } + if(DirectTouchIngame.type == json_boolean) + { + return DirectTouchIngame.u.boolean ? EDirectTouchIngameMode::ACTION : EDirectTouchIngameMode::DISABLED; + } + EDirectTouchIngameMode ParsedDirectTouchIngame = EDirectTouchIngameMode::NUM_STATES; + for(int CurrentMode = (int)EDirectTouchIngameMode::DISABLED; CurrentMode < (int)EDirectTouchIngameMode::NUM_STATES; ++CurrentMode) + { + if(str_comp(DirectTouchIngame.u.string.ptr, DIRECT_TOUCH_INGAME_MODE_NAMES[CurrentMode]) == 0) + { + ParsedDirectTouchIngame = (EDirectTouchIngameMode)CurrentMode; + break; + } + } + if(ParsedDirectTouchIngame == EDirectTouchIngameMode::NUM_STATES) + { + log_error("touch_controls", "Failed to parse configuration: attribute 'direct-touch-ingame' specifies unknown value '%s'", DirectTouchIngame.u.string.ptr); + return {}; + } + return ParsedDirectTouchIngame; +} + +std::optional CTouchControls::ParseDirectTouchSpectateMode(const json_value *pModeValue) +{ + // TODO: Remove json_boolean backwards compatibility + const json_value &DirectTouchSpectate = *pModeValue; + if(DirectTouchSpectate.type != json_boolean && DirectTouchSpectate.type != json_string) + { + log_error("touch_controls", "Failed to parse configuration: attribute 'direct-touch-spectate' must specify a string"); + return {}; + } + if(DirectTouchSpectate.type == json_boolean) + { + return DirectTouchSpectate.u.boolean ? EDirectTouchSpectateMode::AIM : EDirectTouchSpectateMode::DISABLED; + } + EDirectTouchSpectateMode ParsedDirectTouchSpectate = EDirectTouchSpectateMode::NUM_STATES; + for(int CurrentMode = (int)EDirectTouchSpectateMode::DISABLED; CurrentMode < (int)EDirectTouchSpectateMode::NUM_STATES; ++CurrentMode) + { + if(str_comp(DirectTouchSpectate.u.string.ptr, DIRECT_TOUCH_SPECTATE_MODE_NAMES[CurrentMode]) == 0) + { + ParsedDirectTouchSpectate = (EDirectTouchSpectateMode)CurrentMode; + break; + } + } + if(ParsedDirectTouchSpectate == EDirectTouchSpectateMode::NUM_STATES) + { + log_error("touch_controls", "Failed to parse configuration: attribute 'direct-touch-spectate' specifies unknown value '%s'", DirectTouchSpectate.u.string.ptr); + return {}; + } + return ParsedDirectTouchSpectate; +} + +std::optional CTouchControls::ParseButton(const json_value *pButtonObject) +{ + const json_value &ButtonObject = *pButtonObject; + if(ButtonObject.type != json_object) + { + log_error("touch_controls", "Failed to parse touch button: must be an object"); + return {}; + } + + const auto &&ParsePositionSize = [&](const char *pAttribute, int &ParsedValue, int Min, int Max) { + const json_value &AttributeValue = ButtonObject[pAttribute]; + if(AttributeValue.type != json_integer || !in_range(AttributeValue.u.integer, Min, Max)) + { + log_error("touch_controls", "Failed to parse touch button: attribute '%s' must specify an integer between '%d' and '%d'", pAttribute, Min, Max); + return false; + } + ParsedValue = AttributeValue.u.integer; + return true; + }; + CUnitRect ParsedUnitRect; + if(!ParsePositionSize("w", ParsedUnitRect.m_W, BUTTON_SIZE_MINIMUM, BUTTON_SIZE_MAXIMUM) || + !ParsePositionSize("h", ParsedUnitRect.m_H, BUTTON_SIZE_MINIMUM, BUTTON_SIZE_MAXIMUM)) + { + return {}; + } + if(!ParsePositionSize("x", ParsedUnitRect.m_X, 0, BUTTON_SIZE_SCALE - ParsedUnitRect.m_W) || + !ParsePositionSize("y", ParsedUnitRect.m_Y, 0, BUTTON_SIZE_SCALE - ParsedUnitRect.m_H)) + { + return {}; + } + + const json_value &Shape = ButtonObject["shape"]; + if(Shape.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button: attribute 'shape' must specify a string"); + return {}; + } + EButtonShape ParsedShape = EButtonShape::NUM_SHAPES; + for(int CurrentShape = (int)EButtonShape::RECT; CurrentShape < (int)EButtonShape::NUM_SHAPES; ++CurrentShape) + { + if(str_comp(Shape.u.string.ptr, SHAPE_NAMES[CurrentShape]) == 0) + { + ParsedShape = (EButtonShape)CurrentShape; + break; + } + } + if(ParsedShape == EButtonShape::NUM_SHAPES) + { + log_error("touch_controls", "Failed to parse touch button: attribute 'shape' specifies unknown value '%s'", Shape.u.string.ptr); + return {}; + } + + const json_value &Visibilities = ButtonObject["visibilities"]; + if(Visibilities.type != json_array) + { + log_error("touch_controls", "Failed to parse touch button: attribute 'visibilities' must specify an array"); + return {}; + } + std::vector vParsedVisibilities; + for(unsigned VisibilityIndex = 0; VisibilityIndex < Visibilities.u.array.length; ++VisibilityIndex) + { + const json_value &Visibility = Visibilities[VisibilityIndex]; + if(Visibility.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button: attribute 'visibilities' does not specify string at index '%d'", VisibilityIndex); + return {}; + } + EButtonVisibility ParsedVisibility = EButtonVisibility::NUM_VISIBILITIES; + const bool ParsedParity = Visibility.u.string.ptr[0] != '-'; + const char *pVisibilityString = ParsedParity ? Visibility.u.string.ptr : &Visibility.u.string.ptr[1]; + for(int CurrentVisibility = (int)EButtonVisibility::INGAME; CurrentVisibility < (int)EButtonVisibility::NUM_VISIBILITIES; ++CurrentVisibility) + { + if(str_comp(pVisibilityString, m_aVisibilityFunctions[CurrentVisibility].m_pId) == 0) + { + ParsedVisibility = (EButtonVisibility)CurrentVisibility; + break; + } + } + if(ParsedVisibility == EButtonVisibility::NUM_VISIBILITIES) + { + log_error("touch_controls", "Failed to parse touch button: attribute 'visibilities' specifies unknown value '%s' at index '%d'", pVisibilityString, VisibilityIndex); + return {}; + } + const bool VisibilityAlreadyUsed = std::any_of(vParsedVisibilities.begin(), vParsedVisibilities.end(), [&](CButtonVisibility OtherParsedVisibility) { + return OtherParsedVisibility.m_Type == ParsedVisibility; + }); + if(VisibilityAlreadyUsed) + { + log_error("touch_controls", "Failed to parse touch button: attribute 'visibilities' specifies duplicate value '%s' at '%d'", pVisibilityString, VisibilityIndex); + return {}; + } + vParsedVisibilities.emplace_back(ParsedVisibility, ParsedParity); + } + + std::unique_ptr pParsedBehavior = ParseBehavior(&ButtonObject["behavior"]); + if(pParsedBehavior == nullptr) + { + log_error("touch_controls", "Failed to parse touch button: failed to parse attribute 'behavior' (see details above)"); + return {}; + } + + CTouchButton Button(this); + Button.m_UnitRect = ParsedUnitRect; + Button.m_Shape = ParsedShape; + Button.m_vVisibilities = std::move(vParsedVisibilities); + Button.m_pBehavior = std::move(pParsedBehavior); + return Button; +} + +std::unique_ptr CTouchControls::ParseBehavior(const json_value *pBehaviorObject) +{ + const json_value &BehaviorObject = *pBehaviorObject; + if(BehaviorObject.type != json_object) + { + log_error("touch_controls", "Failed to parse touch button behavior: must be an object"); + return nullptr; + } + + const json_value &BehaviorType = BehaviorObject["type"]; + if(BehaviorType.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button behavior: attribute 'type' must specify a string"); + return nullptr; + } + + if(str_comp(BehaviorType.u.string.ptr, CPredefinedTouchButtonBehavior::BEHAVIOR_TYPE) == 0) + { + return ParsePredefinedBehavior(&BehaviorObject); + } + else if(str_comp(BehaviorType.u.string.ptr, CBindTouchButtonBehavior::BEHAVIOR_TYPE) == 0) + { + return ParseBindBehavior(&BehaviorObject); + } + else if(str_comp(BehaviorType.u.string.ptr, CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE) == 0) + { + return ParseBindToggleBehavior(&BehaviorObject); + } + else + { + log_error("touch_controls", "Failed to parse touch button behavior: attribute 'type' specifies unknown value '%s'", BehaviorType.u.string.ptr); + return nullptr; + } +} + +std::unique_ptr CTouchControls::ParsePredefinedBehavior(const json_value *pBehaviorObject) +{ + const json_value &BehaviorObject = *pBehaviorObject; + const json_value &PredefinedId = BehaviorObject["id"]; + if(PredefinedId.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': attribute 'id' must specify a string", CPredefinedTouchButtonBehavior::BEHAVIOR_TYPE); + return nullptr; + } + + class CBehaviorFactory + { + public: + const char *m_pId; + std::function(const json_value *pBehaviorObject)> m_Factory; + }; + static const CBehaviorFactory BEHAVIOR_FACTORIES[] = { + {CIngameMenuTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}, + {CExtraMenuTouchButtonBehavior::BEHAVIOR_ID, [&](const json_value *pBehavior) { return ParseExtraMenuBehavior(pBehavior); }}, + {CEmoticonTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}, + {CSpectateTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}, + {CSwapActionTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}, + {CUseActionTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}, + {CJoystickActionTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}, + {CJoystickAimTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}, + {CJoystickFireTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}, + {CJoystickHookTouchButtonBehavior::BEHAVIOR_ID, [](const json_value *pBehavior) { return std::make_unique(); }}}; + for(const CBehaviorFactory &BehaviorFactory : BEHAVIOR_FACTORIES) + { + if(str_comp(PredefinedId.u.string.ptr, BehaviorFactory.m_pId) == 0) + { + return BehaviorFactory.m_Factory(&BehaviorObject); + } + } + + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': attribute 'id' specifies unknown value '%s'", CPredefinedTouchButtonBehavior::BEHAVIOR_TYPE, PredefinedId.u.string.ptr); + return nullptr; +} + +std::unique_ptr CTouchControls::ParseExtraMenuBehavior(const json_value *pBehaviorObject) +{ + const json_value &BehaviorObject = *pBehaviorObject; + const json_value &MenuNumber = BehaviorObject["number"]; + // TODO: Remove json_none backwards compatibility + const int MaxNumber = (int)EButtonVisibility::EXTRA_MENU_5 - (int)EButtonVisibility::EXTRA_MENU_1 + 1; + if(MenuNumber.type != json_none && (MenuNumber.type != json_integer || !in_range(MenuNumber.u.integer, 1, MaxNumber))) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s' and ID '%s': attribute 'number' must specify an integer between '%d' and '%d'", + CPredefinedTouchButtonBehavior::BEHAVIOR_TYPE, CExtraMenuTouchButtonBehavior::BEHAVIOR_ID, 1, MaxNumber); + return nullptr; + } + int ParsedMenuNumber = MenuNumber.type == json_none ? 0 : (MenuNumber.u.integer - 1); + + return std::make_unique(ParsedMenuNumber); +} + +std::unique_ptr CTouchControls::ParseBindBehavior(const json_value *pBehaviorObject) +{ + const json_value &BehaviorObject = *pBehaviorObject; + const json_value &Label = BehaviorObject["label"]; + if(Label.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': attribute 'label' must specify a string", CBindTouchButtonBehavior::BEHAVIOR_TYPE); + return nullptr; + } + + const json_value &LabelType = BehaviorObject["label-type"]; + if(LabelType.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': attribute 'label-type' must specify a string", CBindTouchButtonBehavior::BEHAVIOR_TYPE); + return {}; + } + CButtonLabel::EType ParsedLabelType = CButtonLabel::EType::NUM_TYPES; + for(int CurrentType = (int)CButtonLabel::EType::PLAIN; CurrentType < (int)CButtonLabel::EType::NUM_TYPES; ++CurrentType) + { + if(str_comp(LabelType.u.string.ptr, LABEL_TYPE_NAMES[CurrentType]) == 0) + { + ParsedLabelType = (CButtonLabel::EType)CurrentType; + break; + } + } + if(ParsedLabelType == CButtonLabel::EType::NUM_TYPES) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': attribute 'label-type' specifies unknown value '%s'", CBindTouchButtonBehavior::BEHAVIOR_TYPE, LabelType.u.string.ptr); + return {}; + } + + const json_value &Command = BehaviorObject["command"]; + if(Command.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': attribute 'command' must specify a string", CBindTouchButtonBehavior::BEHAVIOR_TYPE); + return nullptr; + } + + return std::make_unique(Label.u.string.ptr, ParsedLabelType, Command.u.string.ptr); +} + +std::unique_ptr CTouchControls::ParseBindToggleBehavior(const json_value *pBehaviorObject) +{ + const json_value &CommandsObject = (*pBehaviorObject)["commands"]; + if(CommandsObject.type != json_array || CommandsObject.u.array.length < 2) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': attribute 'commands' must specify an array with at least 2 entries", CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE); + return {}; + } + + std::vector vCommands; + vCommands.reserve(CommandsObject.u.array.length); + for(unsigned CommandIndex = 0; CommandIndex < CommandsObject.u.array.length; ++CommandIndex) + { + const json_value &CommandObject = CommandsObject[CommandIndex]; + if(CommandObject.type != json_object) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': failed to parse command at index '%d': attribute 'commands' must specify an array of objects", CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE, CommandIndex); + return nullptr; + } + + const json_value &Label = CommandObject["label"]; + if(Label.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': failed to parse command at index '%d': attribute 'label' must specify a string", CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE, CommandIndex); + return nullptr; + } + + const json_value &LabelType = CommandObject["label-type"]; + if(LabelType.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': failed to parse command at index '%d': attribute 'label-type' must specify a string", CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE, CommandIndex); + return {}; + } + CButtonLabel::EType ParsedLabelType = CButtonLabel::EType::NUM_TYPES; + for(int CurrentType = (int)CButtonLabel::EType::PLAIN; CurrentType < (int)CButtonLabel::EType::NUM_TYPES; ++CurrentType) + { + if(str_comp(LabelType.u.string.ptr, LABEL_TYPE_NAMES[CurrentType]) == 0) + { + ParsedLabelType = (CButtonLabel::EType)CurrentType; + break; + } + } + if(ParsedLabelType == CButtonLabel::EType::NUM_TYPES) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': failed to parse command at index '%d': attribute 'label-type' specifies unknown value '%s'", CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE, CommandIndex, LabelType.u.string.ptr); + return {}; + } + + const json_value &Command = CommandObject["command"]; + if(Command.type != json_string) + { + log_error("touch_controls", "Failed to parse touch button behavior of type '%s': failed to parse command at index '%d': attribute 'command' must specify a string", CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE, CommandIndex); + return nullptr; + } + vCommands.emplace_back(Label.u.string.ptr, ParsedLabelType, Command.u.string.ptr); + } + return std::make_unique(std::move(vCommands)); +} + +void CTouchControls::WriteConfiguration(CJsonWriter *pWriter) +{ + pWriter->BeginObject(); + + pWriter->WriteAttribute("direct-touch-ingame"); + pWriter->WriteStrValue(DIRECT_TOUCH_INGAME_MODE_NAMES[(int)m_DirectTouchIngame]); + + pWriter->WriteAttribute("direct-touch-spectate"); + pWriter->WriteStrValue(DIRECT_TOUCH_SPECTATE_MODE_NAMES[(int)m_DirectTouchSpectate]); + + pWriter->WriteAttribute("touch-buttons"); + pWriter->BeginArray(); + for(CTouchButton &TouchButton : m_vTouchButtons) + { + TouchButton.WriteToConfiguration(pWriter); + } + pWriter->EndArray(); + + pWriter->EndObject(); +} diff --git a/src/game/client/components/touch_controls.h b/src/game/client/components/touch_controls.h new file mode 100644 index 00000000000..e13c4a35b87 --- /dev/null +++ b/src/game/client/components/touch_controls.h @@ -0,0 +1,548 @@ +#ifndef GAME_CLIENT_COMPONENTS_TOUCH_CONTROLS_H +#define GAME_CLIENT_COMPONENTS_TOUCH_CONTROLS_H + +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +class CJsonWriter; +typedef struct _json_value json_value; + +class CTouchControls : public CComponent +{ +public: + enum class EDirectTouchIngameMode + { + DISABLED, + ACTION, + AIM, + FIRE, + HOOK, + NUM_STATES + }; + enum class EDirectTouchSpectateMode + { + DISABLED, + AIM, + NUM_STATES + }; + + int Sizeof() const override { return sizeof(*this); } + void OnInit() override; + void OnReset() override; + void OnWindowResize() override; + bool OnTouchState(const std::vector &vTouchFingerStates) override; + void OnRender() override; + + bool LoadConfigurationFromFile(int StorageType); + bool LoadConfigurationFromClipboard(); + bool SaveConfigurationToFile(); + void SaveConfigurationToClipboard(); + + EDirectTouchIngameMode DirectTouchIngame() const { return m_DirectTouchIngame; } + void SetDirectTouchIngame(EDirectTouchIngameMode DirectTouchIngame) + { + m_DirectTouchIngame = DirectTouchIngame; + m_EditingChanges = true; + } + EDirectTouchSpectateMode DirectTouchSpectate() const { return m_DirectTouchSpectate; } + void SetDirectTouchSpectate(EDirectTouchSpectateMode DirectTouchSpectate) + { + m_DirectTouchSpectate = DirectTouchSpectate; + m_EditingChanges = true; + } + bool IsEditingActive() const { return m_EditingActive; } + void SetEditingActive(bool EditingActive) { m_EditingActive = EditingActive; } + bool HasEditingChanges() const { return m_EditingChanges; } + void SetEditingChanges(bool EditingChanges) { m_EditingChanges = EditingChanges; } + +private: + static constexpr const char *const DIRECT_TOUCH_INGAME_MODE_NAMES[(int)EDirectTouchIngameMode::NUM_STATES] = {"disabled", "action", "aim", "fire", "hook"}; + static constexpr const char *const DIRECT_TOUCH_SPECTATE_MODE_NAMES[(int)EDirectTouchSpectateMode::NUM_STATES] = {"disabled", "aim"}; + + enum class EButtonShape + { + RECT, + CIRCLE, + NUM_SHAPES + }; + + static constexpr const char *const SHAPE_NAMES[(int)EButtonShape::NUM_SHAPES] = {"rect", "circle"}; + + enum class EButtonVisibility + { + INGAME, + ZOOM_ALLOWED, + VOTE_ACTIVE, + DUMMY_ALLOWED, + DUMMY_CONNECTED, + RCON_AUTHED, + DEMO_PLAYER, + EXTRA_MENU_1, + EXTRA_MENU_2, + EXTRA_MENU_3, + EXTRA_MENU_4, + EXTRA_MENU_5, + NUM_VISIBILITIES + }; + + class CButtonVisibility + { + public: + EButtonVisibility m_Type; + bool m_Parity; + + CButtonVisibility(EButtonVisibility Type, bool Parity) : + m_Type(Type), m_Parity(Parity) {} + }; + + class CButtonVisibilityData + { + public: + const char *m_pId; + std::function m_Function; + }; + + CButtonVisibilityData m_aVisibilityFunctions[(int)EButtonVisibility::NUM_VISIBILITIES]; + + enum + { + ACTION_AIM, + ACTION_FIRE, + ACTION_HOOK, + NUM_ACTIONS + }; + + class CButtonLabel + { + public: + enum class EType + { + /** + * Label is used as is. + */ + PLAIN, + /** + * Label is localized. Only usable for default button labels for which there must be + * corresponding `Localizable`-calls in code and string in the translation files. + */ + LOCALIZED, + /** + * Icon font is used for the label. + */ + ICON, + /** + * Number of label types. + */ + NUM_TYPES + }; + + EType m_Type; + const char *m_pLabel; + }; + + static constexpr const char *const LABEL_TYPE_NAMES[(int)CButtonLabel::EType::NUM_TYPES] = {"plain", "localized", "icon"}; + + class CUnitRect + { + public: + int m_X; + int m_Y; + int m_W; + int m_H; + }; + + class CTouchButtonBehavior; + + class CTouchButton + { + public: + CTouchButton(CTouchControls *pTouchControls); + CTouchButton(CTouchButton &&Other) noexcept; + CTouchButton(const CTouchButton &Other) = delete; + + CTouchButton &operator=(const CTouchButton &Other) = delete; + CTouchButton &operator=(CTouchButton &&Other) noexcept; + + CTouchControls *m_pTouchControls; + + CUnitRect m_UnitRect; + CUIRect m_ScreenRect; + + EButtonShape m_Shape; + int m_BackgroundCorners; // only used with EButtonShape::RECT + + std::vector m_vVisibilities; + std::unique_ptr m_pBehavior; + + bool m_VisibilityCached; + std::chrono::nanoseconds m_VisibilityStartTime; + + void UpdatePointers(); + void UpdateScreenFromUnitRect(); + void UpdateBackgroundCorners(); + + vec2 ClampTouchPosition(vec2 TouchPosition) const; + bool IsInside(vec2 TouchPosition) const; + void UpdateVisibility(); + bool IsVisible() const; + void Render() const; + void WriteToConfiguration(CJsonWriter *pWriter); + }; + + class CTouchButtonBehavior + { + public: + CTouchButton *m_pTouchButton; + CTouchControls *m_pTouchControls; + + bool m_Active; // variables below must only be used when active + IInput::CTouchFinger m_Finger; + vec2 m_ActivePosition; + vec2 m_AccumulatedDelta; + std::chrono::nanoseconds m_ActivationStartTime; + + virtual ~CTouchButtonBehavior() = default; + virtual void Init(CTouchButton *pTouchButton); + + void Reset(); + void SetActive(const IInput::CTouchFingerState &FingerState); + void SetInactive(); + bool IsActive() const; + bool IsActive(const IInput::CTouchFinger &Finger) const; + + virtual CButtonLabel GetLabel() const = 0; + virtual void OnActivate() {} + virtual void OnDeactivate() {} + virtual void OnUpdate() {} + virtual void WriteToConfiguration(CJsonWriter *pWriter) = 0; + }; + + /** + * Abstract class for predefined behaviors. + * + * Subclasses must implemented the concrete behavior and provide the label. + */ + class CPredefinedTouchButtonBehavior : public CTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_TYPE = "predefined"; + + CPredefinedTouchButtonBehavior(const char *pId) : + m_pId(pId) {} + + /** + * Implements the serialization for predefined behaviors. Subclasses + * may override this, but they should call the parent function first. + */ + void WriteToConfiguration(CJsonWriter *pWriter) override; + + private: + const char *m_pId; + }; + + class CIngameMenuTouchButtonBehavior : public CPredefinedTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "ingame-menu"; + + CIngameMenuTouchButtonBehavior() : + CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {} + + CButtonLabel GetLabel() const override; + void OnDeactivate() override; + }; + + class CExtraMenuTouchButtonBehavior : public CPredefinedTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "extra-menu"; + + CExtraMenuTouchButtonBehavior(int Number); + + CButtonLabel GetLabel() const override; + void OnDeactivate() override; + void WriteToConfiguration(CJsonWriter *pWriter) override; + + private: + int m_Number; + char m_aLabel[16]; + }; + + class CEmoticonTouchButtonBehavior : public CPredefinedTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "emoticon"; + + CEmoticonTouchButtonBehavior() : + CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {} + + CButtonLabel GetLabel() const override; + void OnDeactivate() override; + }; + + class CSpectateTouchButtonBehavior : public CPredefinedTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "spectate"; + + CSpectateTouchButtonBehavior() : + CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {} + + CButtonLabel GetLabel() const override; + void OnDeactivate() override; + }; + + class CSwapActionTouchButtonBehavior : public CPredefinedTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "swap-action"; + + CSwapActionTouchButtonBehavior() : + CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {} + + CButtonLabel GetLabel() const override; + void OnActivate() override; + void OnDeactivate() override; + + private: + int m_ActiveAction = NUM_ACTIONS; + }; + + class CUseActionTouchButtonBehavior : public CPredefinedTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "use-action"; + + CUseActionTouchButtonBehavior() : + CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {} + + CButtonLabel GetLabel() const override; + void OnActivate() override; + void OnDeactivate() override; + + private: + int m_ActiveAction = NUM_ACTIONS; + }; + + class CJoystickTouchButtonBehavior : public CPredefinedTouchButtonBehavior + { + public: + CJoystickTouchButtonBehavior(const char *pId) : + CPredefinedTouchButtonBehavior(pId) {} + + CButtonLabel GetLabel() const override; + void OnActivate() override; + void OnDeactivate() override; + void OnUpdate() override; + int ActiveAction() const { return m_ActiveAction; } + virtual int SelectedAction() const = 0; + + private: + int m_ActiveAction = NUM_ACTIONS; + }; + + class CJoystickActionTouchButtonBehavior : public CJoystickTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "joystick-action"; + + CJoystickActionTouchButtonBehavior() : + CJoystickTouchButtonBehavior(BEHAVIOR_ID) {} + + void Init(CTouchButton *pTouchButton) override; + int SelectedAction() const override; + }; + + class CJoystickAimTouchButtonBehavior : public CJoystickTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "joystick-aim"; + + CJoystickAimTouchButtonBehavior() : + CJoystickTouchButtonBehavior(BEHAVIOR_ID) {} + + int SelectedAction() const override; + }; + + class CJoystickFireTouchButtonBehavior : public CJoystickTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "joystick-fire"; + + CJoystickFireTouchButtonBehavior() : + CJoystickTouchButtonBehavior(BEHAVIOR_ID) {} + + int SelectedAction() const override; + }; + + class CJoystickHookTouchButtonBehavior : public CJoystickTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_ID = "joystick-hook"; + + CJoystickHookTouchButtonBehavior() : + CJoystickTouchButtonBehavior(BEHAVIOR_ID) {} + + int SelectedAction() const override; + }; + + /** + * Generic behavior implementation that executes a console command like a bind. + */ + class CBindTouchButtonBehavior : public CTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_TYPE = "bind"; + + CBindTouchButtonBehavior(const char *pLabel, CButtonLabel::EType LabelType, const char *pCommand) : + m_Label(pLabel), + m_LabelType(LabelType), + m_Command(pCommand) {} + + CButtonLabel GetLabel() const override; + void OnActivate() override; + void OnDeactivate() override; + void OnUpdate() override; + void WriteToConfiguration(CJsonWriter *pWriter) override; + + private: + std::string m_Label; + CButtonLabel::EType m_LabelType; + std::string m_Command; + + bool m_Repeating = false; + std::chrono::nanoseconds m_LastUpdateTime; + std::chrono::nanoseconds m_AccumulatedRepeatingTime; + }; + + /** + * Generic behavior implementation that switches between executing one of two or more console commands. + */ + class CBindToggleTouchButtonBehavior : public CTouchButtonBehavior + { + public: + static constexpr const char *const BEHAVIOR_TYPE = "bind-toggle"; + + class CCommand + { + public: + std::string m_Label; + CButtonLabel::EType m_LabelType; + std::string m_Command; + + CCommand(const char *pLabel, CButtonLabel::EType LabelType, const char *pCommand) : + m_Label(pLabel), + m_LabelType(LabelType), + m_Command(pCommand) {} + }; + + CBindToggleTouchButtonBehavior(std::vector &&vCommands) : + m_vCommands(std::move(vCommands)) {} + + CButtonLabel GetLabel() const override; + void OnActivate() override; + void WriteToConfiguration(CJsonWriter *pWriter) override; + + private: + std::vector m_vCommands; + size_t m_ActiveCommandIndex = 0; + }; + + /** + * Mode of direct touch input while ingame. + * + * Saved to the touch controls configuration. + */ + EDirectTouchIngameMode m_DirectTouchIngame = EDirectTouchIngameMode::ACTION; + + /** + * Mode of direct touch input while spectating. + * + * Saved to the touch controls configuration. + */ + EDirectTouchSpectateMode m_DirectTouchSpectate = EDirectTouchSpectateMode::AIM; + + /** + * All touch buttons. + * + * Saved to the touch controls configuration. + */ + std::vector m_vTouchButtons; + + /** + * The activation states of the different extra menus which are toggle by the extra menu button behavior. + */ + bool m_aExtraMenuActive[(int)EButtonVisibility::EXTRA_MENU_5 - (int)EButtonVisibility::EXTRA_MENU_1 + 1] = {false}; + + /** + * The currently selected action which is used for direct touch and is changed and used by some button behaviors. + */ + int m_ActionSelected = ACTION_FIRE; + + /** + * The action that was last activated with direct touch input, which will determine the finger that will + * be used to update the mouse position from direct touch input. + */ + int m_DirectTouchLastAction = ACTION_FIRE; + + class CActionState + { + public: + bool m_Active = false; + IInput::CTouchFinger m_Finger; + }; + + /** + * The states of the different actions for direct touch input. + */ + CActionState m_aDirectTouchActionStates[NUM_ACTIONS]; + + /** + * A pointer to the action joystick, if any exists in the current configuration, or `nullptr` if none. + * This is set by @link CJoystickActionTouchButtonBehavior @endlink when it is initialized and always + * cleared before loading a new touch button configuration. + */ + CJoystickActionTouchButtonBehavior *m_pPrimaryJoystickTouchButtonBehavior; + + /** + * Whether editing mode is currently active. + */ + bool m_EditingActive = false; + + /** + * Whether there are changes to the current configuration in editing mode. + */ + bool m_EditingChanges = false; + + void InitVisibilityFunctions(); + int NextActiveAction(int Action) const; + int NextDirectTouchAction() const; + void UpdateButtons(const std::vector &vTouchFingerStates); + void ResetButtons(); + void RenderButtons(); + vec2 CalculateScreenSize() const; + + bool ParseConfiguration(const void *pFileData, unsigned FileLength); + std::optional ParseDirectTouchIngameMode(const json_value *pModeValue); + std::optional ParseDirectTouchSpectateMode(const json_value *pModeValue); + std::optional ParseButton(const json_value *pButtonObject); + std::unique_ptr ParseBehavior(const json_value *pBehaviorObject); + std::unique_ptr ParsePredefinedBehavior(const json_value *pBehaviorObject); + std::unique_ptr ParseExtraMenuBehavior(const json_value *pBehaviorObject); + std::unique_ptr ParseBindBehavior(const json_value *pBehaviorObject); + std::unique_ptr ParseBindToggleBehavior(const json_value *pBehaviorObject); + void WriteConfiguration(CJsonWriter *pWriter); +}; + +#endif diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index ff733b30db8..bb088c66710 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -146,6 +146,7 @@ void CGameClient::OnConsoleInit() &m_Chat, &m_Broadcast, &m_DebugHud, + &m_TouchControls, &m_Scoreboard, &m_Statboard, &m_Motd, @@ -165,6 +166,7 @@ void CGameClient::OnConsoleInit() &m_Emoticon, &m_Menus, &m_Controls, + &m_TouchControls, &m_Binds}); // add basic console commands diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 665049e947b..f5a1e2eefab 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -58,6 +58,7 @@ #include "components/spectator.h" #include "components/statboard.h" #include "components/tooltips.h" +#include "components/touch_controls.h" #include "components/voting.h" class CGameInfo @@ -146,6 +147,7 @@ class CGameClient : public IGameClient CSounds m_Sounds; CEmoticon m_Emoticon; CDamageInd m_DamageInd; + CTouchControls m_TouchControls; CVoting m_Voting; CSpectator m_Spectator; From c77415e6a5fefe204bffb94aabcb71b41cb1c3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sat, 30 Nov 2024 13:51:31 +0100 Subject: [PATCH 18/58] Fix clipboard not being sanitized when pasting multiple lines For example when pasting multiple lines of text containing Windows line endings, the `\r` characters were not being removed from lines except the first one. --- src/game/client/lineinput.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/game/client/lineinput.cpp b/src/game/client/lineinput.cpp index e27353b0d23..f2f113f624d 100644 --- a/src/game/client/lineinput.cpp +++ b/src/game/client/lineinput.cpp @@ -358,6 +358,7 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) Line = GetString(); } Begin = i + 1; + str_sanitize_cc(Line.data()); m_pfnClipboardLineCallback(Line.c_str()); } } From 307ad7c9b627948fa7eccd631df714629a5e7418 Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Sun, 1 Dec 2024 07:36:34 +0100 Subject: [PATCH 19/58] android build: Don't parallelize twice Every build itself already uses all cores. By running 4 builds at once my system gets overloaded and goes OoM --- scripts/android/cmake_android.sh | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/scripts/android/cmake_android.sh b/scripts/android/cmake_android.sh index d541250a366..e577307fe6b 100755 --- a/scripts/android/cmake_android.sh +++ b/scripts/android/cmake_android.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e # $HOME must be used instead of ~ else cargo-ndk cannot find the folder export ANDROID_HOME=$HOME/Android/Sdk @@ -157,40 +158,19 @@ function build_for_type() { mkdir -p "${BUILD_FOLDER}" if [[ "${ANDROID_BUILD}" == "arm" || "${ANDROID_BUILD}" == "all" ]]; then - build_for_type arm armeabi-v7a armv7-linux-androideabi & - PID_BUILD_ARM=$! + build_for_type arm armeabi-v7a armv7-linux-androideabi fi if [[ "${ANDROID_BUILD}" == "arm64" || "${ANDROID_BUILD}" == "all" ]]; then - build_for_type arm64 arm64-v8a aarch64-linux-android & - PID_BUILD_ARM64=$! + build_for_type arm64 arm64-v8a aarch64-linux-android fi if [[ "${ANDROID_BUILD}" == "x86" || "${ANDROID_BUILD}" == "all" ]]; then - build_for_type x86 x86 i686-linux-android & - PID_BUILD_X86=$! + build_for_type x86 x86 i686-linux-android fi if [[ "${ANDROID_BUILD}" == "x86_64" || "${ANDROID_BUILD}" == "all" ]]; then - build_for_type x86_64 x86_64 x86_64-linux-android & - PID_BUILD_X86_64=$! -fi - -if [ -n "$PID_BUILD_ARM" ] && ! wait "$PID_BUILD_ARM"; then - printf "${COLOR_RED}%s${COLOR_RESET}\n" "Building for arm failed" - exit 1 -fi -if [ -n "$PID_BUILD_ARM64" ] && ! wait "$PID_BUILD_ARM64"; then - printf "${COLOR_RED}%s${COLOR_RESET}\n" "Building for arm64 failed" - exit 1 -fi -if [ -n "$PID_BUILD_X86" ] && ! wait "$PID_BUILD_X86"; then - printf "${COLOR_RED}%s${COLOR_RESET}\n" "Building for x86 failed" - exit 1 -fi -if [ -n "$PID_BUILD_X86_64" ] && ! wait "$PID_BUILD_X86_64"; then - printf "${COLOR_RED}%s${COLOR_RESET}\n" "Building for x86_64 failed" - exit 1 + build_for_type x86_64 x86_64 x86_64-linux-android fi printf "${COLOR_CYAN}%s${COLOR_RESET}\n" "Copying project files..." From b4e4e5d7f9f1b15517ec2099399c665d26b90243 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Sun, 1 Dec 2024 16:56:01 +0800 Subject: [PATCH 20/58] remove self from spectate selector --- src/game/client/components/spectator.cpp | 3 +++ src/game/client/gameclient.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp index 1e29fccacb6..9682c61ba21 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -426,6 +426,9 @@ void CSpectator::OnRender() if(!m_pClient->m_Snap.m_apInfoByDDTeamName[i] || m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_Team == TEAM_SPECTATORS) continue; + if(Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId == m_pClient->m_Snap.m_LocalClientId) + continue; + ++Count; if(Count == PerLine + 1 || (Count > PerLine + 1 && (Count - 1) % PerLine == 0)) diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 734d940708e..619a0817e7e 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -4084,6 +4084,9 @@ bool CGameClient::InitMultiView(int Team) int Count = 0; for(int i = 0; i < MAX_CLIENTS; i++) { + if(Client()->State() != IClient::STATE_DEMOPLAYBACK && m_Snap.m_apPlayerInfos[i] && m_Snap.m_apPlayerInfos[i]->m_ClientId == m_Snap.m_LocalClientId) + continue; + vec2 PlayerPos; // get the position of the player From 2464b46da94c83c76d49e7e857d6f5358adce908 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Sun, 1 Dec 2024 18:07:11 +0800 Subject: [PATCH 21/58] make demo spectator id changes consistent --- src/game/client/components/spectator.cpp | 91 +++++++++++++----------- src/game/client/components/spectator.h | 4 +- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp index 1e29fccacb6..053f144d25a 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -18,10 +18,17 @@ #include -bool CSpectator::CanChangeSpectator() +bool CSpectator::CanChangeSpectatorId() { - // Don't change SpectatorId when not spectating - return m_pClient->m_Snap.m_SpecInfo.m_Active; + // don't change SpectatorId when not spectating + if(!m_pClient->m_Snap.m_SpecInfo.m_Active) + return false; + + // stop follow mode from changing SpectatorId + if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecId == SPEC_FOLLOW) + return false; + + return true; } void CSpectator::SpectateNext(bool Reverse) @@ -89,7 +96,7 @@ void CSpectator::ConKeySpectator(IConsole::IResult *pResult, void *pUserData) void CSpectator::ConSpectate(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; - if(!pSelf->CanChangeSpectator()) + if(!pSelf->CanChangeSpectatorId()) return; pSelf->Spectate(pResult->GetInteger(0)); @@ -98,7 +105,7 @@ void CSpectator::ConSpectate(IConsole::IResult *pResult, void *pUserData) void CSpectator::ConSpectateNext(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; - if(!pSelf->CanChangeSpectator()) + if(!pSelf->CanChangeSpectatorId()) return; pSelf->SpectateNext(false); @@ -107,7 +114,7 @@ void CSpectator::ConSpectateNext(IConsole::IResult *pResult, void *pUserData) void CSpectator::ConSpectatePrevious(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; - if(!pSelf->CanChangeSpectator()) + if(!pSelf->CanChangeSpectatorId()) return; pSelf->SpectateNext(true); @@ -116,37 +123,7 @@ void CSpectator::ConSpectatePrevious(IConsole::IResult *pResult, void *pUserData void CSpectator::ConSpectateClosest(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; - if(!pSelf->CanChangeSpectator()) - return; - - const CGameClient::CSnapState &Snap = pSelf->m_pClient->m_Snap; - int SpectatorId = Snap.m_SpecInfo.m_SpectatorId; - - int NewSpectatorId = -1; - - vec2 CurPosition(pSelf->m_pClient->m_Camera.m_Center); - if(SpectatorId != SPEC_FREEVIEW) - { - const CNetObj_Character &CurCharacter = Snap.m_aCharacters[SpectatorId].m_Cur; - CurPosition.x = CurCharacter.m_X; - CurPosition.y = CurCharacter.m_Y; - } - - int ClosestDistance = std::numeric_limits::max(); - for(int i = 0; i < MAX_CLIENTS; i++) - { - if(i == SpectatorId || !Snap.m_aCharacters[i].m_Active || !Snap.m_apPlayerInfos[i] || Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS || (SpectatorId == SPEC_FREEVIEW && i == Snap.m_LocalClientId)) - continue; - const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[i].m_Cur; - int Distance = distance(CurPosition, vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y)); - if(NewSpectatorId == -1 || Distance < ClosestDistance) - { - NewSpectatorId = i; - ClosestDistance = Distance; - } - } - if(NewSpectatorId > -1) - pSelf->Spectate(NewSpectatorId); + pSelf->SpectateClosest(true); } void CSpectator::ConMultiView(IConsole::IResult *pResult, void *pUserData) @@ -204,7 +181,7 @@ bool CSpectator::OnInput(const IInput::CEvent &Event) if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) Spectate(SPEC_FREEVIEW); else - SpectateClosest(); + SpectateClosest(Client()->State() == IClient::STATE_DEMOPLAYBACK); return true; } } @@ -641,7 +618,41 @@ void CSpectator::Spectate(int SpectatorId) Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); } -void CSpectator::SpectateClosest() +void CSpectator::SpectateClosest(bool AllowSelf) { - ConSpectateClosest(NULL, this); + if(CanChangeSpectatorId()) + return; + + const CGameClient::CSnapState &Snap = m_pClient->m_Snap; + int SpectatorId = Snap.m_SpecInfo.m_SpectatorId; + + int NewSpectatorId = -1; + + vec2 CurPosition(m_pClient->m_Camera.m_Center); + if(SpectatorId != SPEC_FREEVIEW) + { + const CNetObj_Character &CurCharacter = Snap.m_aCharacters[SpectatorId].m_Cur; + CurPosition.x = CurCharacter.m_X; + CurPosition.y = CurCharacter.m_Y; + } + + int ClosestDistance = std::numeric_limits::max(); + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(i == SpectatorId || !Snap.m_aCharacters[i].m_Active || !Snap.m_apPlayerInfos[i] || Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS) + continue; + + if(!AllowSelf && i == Snap.m_LocalClientId) + continue; + + const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[i].m_Cur; + int Distance = distance(CurPosition, vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y)); + if(NewSpectatorId == -1 || Distance < ClosestDistance) + { + NewSpectatorId = i; + ClosestDistance = Distance; + } + } + if(NewSpectatorId > -1) + Spectate(NewSpectatorId); } diff --git a/src/game/client/components/spectator.h b/src/game/client/components/spectator.h index 25dddfb60e4..cf34196c502 100644 --- a/src/game/client/components/spectator.h +++ b/src/game/client/components/spectator.h @@ -26,7 +26,7 @@ class CSpectator : public CComponent float m_MultiViewActivateDelay; - bool CanChangeSpectator(); + bool CanChangeSpectatorId(); void SpectateNext(bool Reverse); static void ConKeySpectator(IConsole::IResult *pResult, void *pUserData); @@ -48,7 +48,7 @@ class CSpectator : public CComponent virtual void OnReset() override; void Spectate(int SpectatorId); - void SpectateClosest(); + void SpectateClosest(bool AllowSelf); bool IsActive() const { return m_Active; } }; From a892a21d20fb96d513650b123d61ea98e5f106f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sun, 1 Dec 2024 11:29:12 +0100 Subject: [PATCH 22/58] Fix menus skin list initially being empty, refactor skin refreshing The skin list in the menus was initially empty because `s_SkinLastRefreshTime` is initialized to the current refresh time, which prevents the list from being updated unless the user already has favorite skins, as adding these also cause the list to be updated. The static `s_SkinLastRefreshTime` variable is replaced with a member variable `m_SkinListNeedsUpdate` and an empty optional is used as initial value to always refresh the list. The separate `m_SkinListNeedsUpdate` and `m_SkinFavoritesChanged` variables are removed and instead the `m_SkinListLastRefreshTime` variable is used to handle all updates of the skin list. Separate refresh time variables `m_SkinList7LastRefreshTime` and `m_SkinPartsList7LastRefreshTime` are added for the 0.7 skin menu to avoid interference with 0.6 skins. --- src/game/client/components/menus.cpp | 2 +- src/game/client/components/menus.h | 7 ++++--- src/game/client/components/menus_settings.cpp | 20 ++++++------------- .../client/components/menus_settings7.cpp | 20 ++++++------------- src/game/client/components/skins7.cpp | 2 ++ src/game/client/components/skins7.h | 4 ++++ 6 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index 8e95087f8c8..e5d75ef52d4 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -1815,7 +1815,7 @@ void CMenus::RenderPopupFullscreen(CUIRect Screen) if(m_pClient->m_Skins7.SaveSkinfile(m_SkinNameInput.GetString(), m_Dummy)) { m_Popup = POPUP_NONE; - m_SkinListNeedsUpdate = true; + m_SkinList7LastRefreshTime = std::nullopt; } else PopupMessage(Localize("Error"), Localize("Unable to save the skin"), Localize("Ok"), POPUP_SAVE_SKIN); diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index f8e4afccdc8..38ef11e5254 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -94,8 +95,10 @@ class CMenus : public CComponent void DoJoystickAxisPicker(CUIRect View); void DoJoystickBar(const CUIRect *pRect, float Current, float Tolerance, bool Active); - bool m_SkinListNeedsUpdate = false; + std::optional m_SkinListLastRefreshTime; bool m_SkinListScrollToSelected = false; + std::optional m_SkinList7LastRefreshTime; + std::optional m_SkinPartsList7LastRefreshTime; int m_DirectionQuadContainerIndex; @@ -585,7 +588,6 @@ class CMenus : public CComponent void UpdateCommunityIcons(); // skin favorite list - bool m_SkinFavoritesChanged = false; std::unordered_set m_SkinFavorites; static void Con_AddFavoriteSkin(IConsole::IResult *pResult, void *pUserData); static void Con_RemFavoriteSkin(IConsole::IResult *pResult, void *pUserData); @@ -668,7 +670,6 @@ class CMenus : public CComponent virtual void OnStateChange(int NewState, int OldState) override; virtual void OnWindowResize() override; - virtual void OnRefreshSkins() override; virtual void OnReset() override; virtual void OnRender() override; virtual bool OnInput(const IInput::CEvent &Event) override; diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 13273bf6e9a..82b280c9a36 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -401,11 +401,6 @@ struct CUISkin bool operator==(const char *pOther) const { return !str_comp_nocase(m_pSkin->GetName(), pOther); } }; -void CMenus::OnRefreshSkins() -{ - m_SkinListNeedsUpdate = true; -} - void CMenus::Con_AddFavoriteSkin(IConsole::IResult *pResult, void *pUserData) { auto *pSelf = (CMenus *)pUserData; @@ -417,7 +412,7 @@ void CMenus::Con_AddFavoriteSkin(IConsole::IResult *pResult, void *pUserData) return; } pSelf->m_SkinFavorites.emplace(pStr); - pSelf->m_SkinFavoritesChanged = true; + pSelf->m_SkinListLastRefreshTime = std::nullopt; } void CMenus::Con_RemFavoriteSkin(IConsole::IResult *pResult, void *pUserData) @@ -427,7 +422,7 @@ void CMenus::Con_RemFavoriteSkin(IConsole::IResult *pResult, void *pUserData) if(it != pSelf->m_SkinFavorites.end()) { pSelf->m_SkinFavorites.erase(it); - pSelf->m_SkinFavoritesChanged = true; + pSelf->m_SkinListLastRefreshTime = std::nullopt; } } @@ -713,14 +708,12 @@ void CMenus::RenderSettingsTee(CUIRect MainView) static CListBox s_ListBox; // be nice to the CPU - static std::chrono::nanoseconds s_SkinLastRefreshTime = m_pClient->m_Skins.LastRefreshTime(); - if(m_SkinListNeedsUpdate || m_SkinFavoritesChanged || s_SkinLastRefreshTime != m_pClient->m_Skins.LastRefreshTime()) + if(!m_SkinListLastRefreshTime.has_value() || m_SkinListLastRefreshTime.value() != m_pClient->m_Skins.LastRefreshTime()) { - s_SkinLastRefreshTime = m_pClient->m_Skins.LastRefreshTime(); + m_SkinListLastRefreshTime = m_pClient->m_Skins.LastRefreshTime(); s_vSkinList.clear(); s_vSkinListHelper.clear(); s_vFavoriteSkinListHelper.clear(); - m_SkinFavoritesChanged = false; auto &&SkinNotFiltered = [&](const CSkin *pSkinToBeSelected) { // filter quick search @@ -756,7 +749,6 @@ void CMenus::RenderSettingsTee(CUIRect MainView) std::sort(s_vFavoriteSkinListHelper.begin(), s_vFavoriteSkinListHelper.end()); s_vSkinList = s_vFavoriteSkinListHelper; s_vSkinList.insert(s_vSkinList.end(), s_vSkinListHelper.begin(), s_vSkinListHelper.end()); - m_SkinListNeedsUpdate = false; } int OldSelected = -1; @@ -820,7 +812,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) { m_SkinFavorites.emplace(pSkinToBeDraw->GetName()); } - m_SkinListNeedsUpdate = true; + m_SkinListLastRefreshTime = std::nullopt; } } } @@ -835,7 +827,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString)); if(Ui()->DoEditBox_Search(&s_SkinFilterInput, &QuickSearch, 14.0f, !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed())) { - m_SkinListNeedsUpdate = true; + m_SkinListLastRefreshTime = std::nullopt; } static CButtonContainer s_SkinDatabaseButton; diff --git a/src/game/client/components/menus_settings7.cpp b/src/game/client/components/menus_settings7.cpp index 13c804d3669..9c5cf8751cd 100644 --- a/src/game/client/components/menus_settings7.cpp +++ b/src/game/client/components/menus_settings7.cpp @@ -207,8 +207,8 @@ void CMenus::RenderSettingsTee7(CUIRect MainView) static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString)); if(Ui()->DoEditBox_Search(&s_SkinFilterInput, &QuickSearch, 14.0f, !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed())) { - m_SkinListNeedsUpdate = true; - m_SkinPartListNeedsUpdate = true; + m_SkinList7LastRefreshTime = std::nullopt; + m_SkinPartsList7LastRefreshTime = std::nullopt; } static CButtonContainer s_DirectoryButton; @@ -317,14 +317,11 @@ void CMenus::RenderSkinSelection7(CUIRect MainView) static float s_LastSelectionTime = -10.0f; static std::vector s_vpSkinList; static CListBox s_ListBox; - static size_t s_SkinCount = 0; - const std::vector &vCurrentSkins = GameClient()->m_Skins7.GetSkins(); - if(m_SkinListNeedsUpdate || vCurrentSkins.size() != s_SkinCount) + if(!m_SkinList7LastRefreshTime.has_value() || m_SkinList7LastRefreshTime.value() != m_SkinList7LastRefreshTime) { s_vpSkinList.clear(); - s_SkinCount = vCurrentSkins.size(); - for(const CSkins7::CSkin &Skin : vCurrentSkins) + for(const CSkins7::CSkin &Skin : GameClient()->m_Skins7.GetSkins()) { if((Skin.m_Flags & CSkins7::SKINFLAG_SPECIAL) != 0) continue; @@ -333,7 +330,6 @@ void CMenus::RenderSkinSelection7(CUIRect MainView) s_vpSkinList.emplace_back(&Skin); } - m_SkinListNeedsUpdate = false; } m_pSelectedSkin = nullptr; @@ -405,15 +401,12 @@ void CMenus::RenderSkinPartSelection7(CUIRect MainView) { static std::vector s_paList[protocol7::NUM_SKINPARTS]; static CListBox s_ListBox; - static size_t s_aSkinPartCount[protocol7::NUM_SKINPARTS] = {0}; for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) { - const std::vector &vCurrentSkinParts = GameClient()->m_Skins7.GetSkinParts(Part); - if(m_SkinPartListNeedsUpdate || vCurrentSkinParts.size() != s_aSkinPartCount[Part]) + if(!m_SkinList7LastRefreshTime.has_value() || m_SkinList7LastRefreshTime.value() != GameClient()->m_Skins7.LastRefreshTime()) { s_paList[Part].clear(); - s_aSkinPartCount[Part] = vCurrentSkinParts.size(); - for(const CSkins7::CSkinPart &SkinPart : vCurrentSkinParts) + for(const CSkins7::CSkinPart &SkinPart : GameClient()->m_Skins7.GetSkinParts(Part)) { if((SkinPart.m_Flags & CSkins7::SKINFLAG_SPECIAL) != 0) continue; @@ -425,7 +418,6 @@ void CMenus::RenderSkinPartSelection7(CUIRect MainView) } } } - m_SkinPartListNeedsUpdate = false; static int s_OldSelected = -1; s_ListBox.DoBegin(&MainView); diff --git a/src/game/client/components/skins7.cpp b/src/game/client/components/skins7.cpp index 9287180f346..494c0f02ceb 100644 --- a/src/game/client/components/skins7.cpp +++ b/src/game/client/components/skins7.cpp @@ -307,6 +307,8 @@ void CSkins7::OnInit() LoadXmasHat(); LoadBotDecoration(); GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); + + m_LastRefreshTime = time_get_nanoseconds(); } void CSkins7::InitPlaceholderSkinParts() diff --git a/src/game/client/components/skins7.h b/src/game/client/components/skins7.h index 4f37630ea4a..54e3b3dcdda 100644 --- a/src/game/client/components/skins7.h +++ b/src/game/client/components/skins7.h @@ -13,6 +13,7 @@ #include #include +#include #include class CSkins7 : public CComponent @@ -66,6 +67,8 @@ class CSkins7 : public CComponent int Sizeof() const override { return sizeof(*this); } void OnInit() override; + std::chrono::nanoseconds LastRefreshTime() const { return m_LastRefreshTime; } + const std::vector &GetSkins() const; const std::vector &GetSkinParts(int Part) const; const CSkinPart *FindSkinPartOrNullptr(int Part, const char *pName, bool AllowSpecialPart) const; @@ -87,6 +90,7 @@ class CSkins7 : public CComponent private: int m_ScanningPart; + std::chrono::nanoseconds m_LastRefreshTime; std::vector m_avSkinParts[protocol7::NUM_SKINPARTS]; CSkinPart m_aPlaceholderSkinParts[protocol7::NUM_SKINPARTS]; From b9c18add48b23509f8551004252c3e588921cbb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sun, 10 Nov 2024 22:49:30 +0100 Subject: [PATCH 23/58] Replace editor background texture with noise texture Use the new console background noise texture also instead of the old editor background texture. --- CMakeLists.txt | 1 - data/editor/background.png | Bin 56183 -> 0 bytes src/game/editor/editor.cpp | 14 +++++++------- src/game/editor/editor.h | 2 -- 4 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 data/editor/background.png diff --git a/CMakeLists.txt b/CMakeLists.txt index ba403347f62..b49ca88920e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1408,7 +1408,6 @@ set(EXPECTED_DATA editor/automap/round_tiles.rules editor/automap/water.rules editor/automap/winter_main.rules - editor/background.png editor/checker.png editor/cursor.png editor/cursor_resize.png diff --git a/data/editor/background.png b/data/editor/background.png deleted file mode 100644 index 7d83990e6ef21aeaee8f34acc1aeb73402e034f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56183 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

tZ}FFbDtc$yuekv?eA>}unyC8Q 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 mG6-E~bq9{Z)qJk5&Ho2bsK0tE diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 1175c29f774..318585d526d 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -7934,21 +7934,22 @@ void CEditor::Render() } } - float Brightness = 0.25f; + const float BackgroundBrightness = 0.26f; + const float BackgroundScale = 80.0f; if(m_GuiActive) { - RenderBackground(MenuBar, m_BackgroundTexture, 128.0f, Brightness * 0); + RenderBackground(MenuBar, IGraphics::CTextureHandle(), BackgroundScale, 0.0f); MenuBar.Margin(2.0f, &MenuBar); - RenderBackground(ToolBox, m_BackgroundTexture, 128.0f, Brightness); + RenderBackground(ToolBox, g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, BackgroundScale, BackgroundBrightness); ToolBox.Margin(2.0f, &ToolBox); - RenderBackground(ToolBar, m_BackgroundTexture, 128.0f, Brightness); + RenderBackground(ToolBar, g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, BackgroundScale, BackgroundBrightness); ToolBar.Margin(2.0f, &ToolBar); ToolBar.VSplitLeft(m_ToolBoxWidth, &ModeBar, &ToolBar); - RenderBackground(StatusBar, m_BackgroundTexture, 128.0f, Brightness); + RenderBackground(StatusBar, g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, BackgroundScale, BackgroundBrightness); StatusBar.Margin(2.0f, &StatusBar); } @@ -8079,7 +8080,7 @@ void CEditor::Render() { if(m_ActiveExtraEditor != EXTRAEDITOR_NONE) { - RenderBackground(ExtraEditor, m_BackgroundTexture, 128.0f, Brightness); + RenderBackground(ExtraEditor, g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, BackgroundScale, BackgroundBrightness); ExtraEditor.HMargin(2.0f, &ExtraEditor); ExtraEditor.VSplitRight(2.0f, &ExtraEditor, nullptr); } @@ -8480,7 +8481,6 @@ void CEditor::Init() Component.OnInit(this); m_CheckerTexture = Graphics()->LoadTexture("editor/checker.png", IStorage::TYPE_ALL); - m_BackgroundTexture = Graphics()->LoadTexture("editor/background.png", IStorage::TYPE_ALL); m_aCursorTextures[CURSOR_NORMAL] = Graphics()->LoadTexture("editor/cursor.png", IStorage::TYPE_ALL); m_aCursorTextures[CURSOR_RESIZE_H] = Graphics()->LoadTexture("editor/cursor_resize.png", IStorage::TYPE_ALL); m_aCursorTextures[CURSOR_RESIZE_V] = m_aCursorTextures[CURSOR_RESIZE_H]; diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 45e34dfdfcc..32557f476f7 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -439,7 +439,6 @@ class CEditor : public IEditor } m_CheckerTexture.Invalidate(); - m_BackgroundTexture.Invalidate(); for(auto &CursorTexture : m_aCursorTextures) CursorTexture.Invalidate(); @@ -819,7 +818,6 @@ class CEditor : public IEditor bool m_ColorPipetteActive = false; IGraphics::CTextureHandle m_CheckerTexture; - IGraphics::CTextureHandle m_BackgroundTexture; enum ECursorType { From e0a47f3046339aff9cef534b7f7923513f42830b Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Sun, 1 Dec 2024 22:46:34 +0100 Subject: [PATCH 24/58] Update translations for upcoming 18.8 --- data/languages/arabic.txt | 98 ++++++++++++++++- data/languages/azerbaijani.txt | 110 +++++++++++++++++-- data/languages/belarusian.txt | 110 +++++++++++++++++-- data/languages/bosnian.txt | 98 ++++++++++++++++- data/languages/brazilian_portuguese.txt | 110 +++++++++++++++++-- data/languages/bulgarian.txt | 98 ++++++++++++++++- data/languages/catalan.txt | 98 ++++++++++++++++- data/languages/chuvash.txt | 98 ++++++++++++++++- data/languages/czech.txt | 110 +++++++++++++++++-- data/languages/danish.txt | 98 ++++++++++++++++- data/languages/dutch.txt | 98 ++++++++++++++++- data/languages/esperanto.txt | 116 ++++++++++++++++++-- data/languages/estonian.txt | 110 +++++++++++++++++-- data/languages/finnish.txt | 110 +++++++++++++++++-- data/languages/french.txt | 110 +++++++++++++++++-- data/languages/galician.txt | 110 +++++++++++++++++-- data/languages/german.txt | 112 +++++++++++++++++-- data/languages/greek.txt | 98 ++++++++++++++++- data/languages/hungarian.txt | 102 ++++++++++++++++- data/languages/italian.txt | 106 ++++++++++++++++-- data/languages/japanese.txt | 98 ++++++++++++++++- data/languages/korean.txt | 102 ++++++++++++++++- data/languages/kyrgyz.txt | 98 ++++++++++++++++- data/languages/norwegian.txt | 98 ++++++++++++++++- data/languages/persian.txt | 110 +++++++++++++++++-- data/languages/polish.txt | 110 +++++++++++++++++-- data/languages/portuguese.txt | 98 ++++++++++++++++- data/languages/romanian.txt | 98 ++++++++++++++++- data/languages/russian.txt | 110 +++++++++++++++++-- data/languages/serbian.txt | 110 +++++++++++++++++-- data/languages/serbian_cyrillic.txt | 102 ++++++++++++++++- data/languages/simplified_chinese.txt | 110 +++++++++++++++++-- data/languages/slovak.txt | 110 +++++++++++++++++-- data/languages/spanish.txt | 110 +++++++++++++++++-- data/languages/swedish.txt | 110 +++++++++++++++++-- data/languages/traditional_chinese.txt | 110 +++++++++++++++++-- data/languages/turkish.txt | 110 +++++++++++++++++-- data/languages/ukrainian.txt | 110 +++++++++++++++++-- src/game/client/components/menus_ingame.cpp | 6 +- 39 files changed, 3753 insertions(+), 257 deletions(-) diff --git a/data/languages/arabic.txt b/data/languages/arabic.txt index 9bc3f350205..0b251a761c7 100644 --- a/data/languages/arabic.txt +++ b/data/languages/arabic.txt @@ -1348,7 +1348,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1367,7 +1367,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1505,6 +1511,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1748,7 +1828,10 @@ Always show own player's hook collision line Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1933,6 +2016,15 @@ Eyes Loading sound files == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/azerbaijani.txt b/data/languages/azerbaijani.txt index b777b359d0d..fae7f75d2a6 100644 --- a/data/languages/azerbaijani.txt +++ b/data/languages/azerbaijani.txt @@ -1159,9 +1159,6 @@ Leak IP No server selected == Heç bir server seçilmədi -Online players (%d) -== Onlayn oyunçular (%d) - Online clanmates (%d) == Onlayn klandaşlar (%d) @@ -1178,9 +1175,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Bu klanı dostlarınızın siyahısından silmək üçün klikləyin. -None -== Yox - Are you sure that you want to remove the player '%s' from your friends list? == '%s' oyunçu dostlarınız siyahısından silmək istədiyinizə əminsiniz? @@ -1483,9 +1477,6 @@ Show local player's key presses Hook collision line == Qarmaq izi -Hook collision line width -== Qarmaq izinin qalınlığı - Hook collision line opacity == Kanca çizgisi opaklığı @@ -1943,3 +1934,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == Bəzi yazı fontları yüklənə bilmir. Ətraflı məlumat üçün əsas konsolu yoxlayın + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/belarusian.txt b/data/languages/belarusian.txt index b54a463539f..852aaa927e9 100644 --- a/data/languages/belarusian.txt +++ b/data/languages/belarusian.txt @@ -1236,9 +1236,6 @@ Hook collision line Show other players' hook collision lines == Паказваць сутыкненні крука іншых гульцоў -Hook collision line width -== Шырыня лініі сутыкнення крука - Hook collision line opacity == Непразрыстасьць лініі сутыкнення крука @@ -1534,9 +1531,6 @@ A demo with this name already exists No server selected == Сервер не абраны -Online players (%d) -== Гульцоў анлайн (%d) - Online clanmates (%d) == Членаў клана анлайн (%d) @@ -1553,9 +1547,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Націсніце, как выдаліць гэты клан са свайго спіса сяброў. -None -== Пуста - Add Clan == Дадаць клан @@ -1894,12 +1885,98 @@ Unable to save the skin with a reserved name No local servers found (ports %d-%d) == +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + Dummy is not allowed on this server == Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + [Hertz] Hz == @@ -1907,6 +1984,12 @@ Hz Show client IDs (scoreboard, chat, spectator) == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Basic == @@ -1945,3 +2028,12 @@ Feet [skins] Eyes == + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/bosnian.txt b/data/languages/bosnian.txt index e68a22d938e..a1c9519089a 100644 --- a/data/languages/bosnian.txt +++ b/data/languages/bosnian.txt @@ -1246,7 +1246,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1265,7 +1265,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1412,6 +1418,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1697,7 +1777,10 @@ Always show own player's hook collision line Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1940,6 +2023,15 @@ Spree Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/brazilian_portuguese.txt b/data/languages/brazilian_portuguese.txt index 4139e87525a..c347ab97ecf 100644 --- a/data/languages/brazilian_portuguese.txt +++ b/data/languages/brazilian_portuguese.txt @@ -1382,9 +1382,6 @@ Show jumps indicator Hook collision line == Linha de colisão do gancho -Hook collision line width -== Largura da linha de colisão do gancho - Hook collision line opacity == Opacidade da linha de colisão do gancho @@ -1580,9 +1577,6 @@ A demo with this name already exists No server selected == Nenhum serviço selecionado -Online players (%d) -== Jogadores online (%d) - Online clanmates (%d) == Colegas de clã online (%d) @@ -1599,9 +1593,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Clique para remover este clã a partir de sua lista de amigos. -None -== Ninguém - Add Clan == Adicionar clã @@ -1974,3 +1965,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == Algumas fontes não puderam ser carregadas. Verifique o console local para detalhes. + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/bulgarian.txt b/data/languages/bulgarian.txt index 7058e24f937..b5749f41225 100644 --- a/data/languages/bulgarian.txt +++ b/data/languages/bulgarian.txt @@ -928,7 +928,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -947,7 +947,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1139,6 +1145,80 @@ Kill Pause == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1589,7 +1669,10 @@ Show other players' hook collision lines Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1928,6 +2011,15 @@ Best Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + 1 new mention == diff --git a/data/languages/catalan.txt b/data/languages/catalan.txt index 1fe4ce1162d..a0be38044a2 100644 --- a/data/languages/catalan.txt +++ b/data/languages/catalan.txt @@ -1411,7 +1411,7 @@ Copy info No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1430,7 +1430,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1568,6 +1574,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1769,7 +1849,10 @@ Always show own player's hook collision line Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1930,6 +2013,15 @@ Eyes Loading sound files == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/chuvash.txt b/data/languages/chuvash.txt index c525505dbc1..55ef81af7e5 100644 --- a/data/languages/chuvash.txt +++ b/data/languages/chuvash.txt @@ -931,7 +931,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -950,7 +950,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1142,6 +1148,80 @@ Kill Pause == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1589,7 +1669,10 @@ Show other players' hook collision lines Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1928,6 +2011,15 @@ Best Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + 1 new mention == diff --git a/data/languages/czech.txt b/data/languages/czech.txt index 1d250ac6dec..d45e6aa6fba 100644 --- a/data/languages/czech.txt +++ b/data/languages/czech.txt @@ -1316,9 +1316,6 @@ Leak IP No server selected == Není vybrán žádný server -Online players (%d) -== Online hráči (%d) - Online clanmates (%d) == Online členové klanu (%d) @@ -1335,9 +1332,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Kliknutím odeberete tento klan ze seznamu přátel. -None -== Žádný - Are you sure that you want to remove the player '%s' from your friends list? == Opravdu chcete odstranit hráče '%s' ze seznamu přátel? @@ -1673,9 +1667,6 @@ Same clan color in scoreboard Hook collision line == Čára kolize háku -Hook collision line width -== Šířka čáry kolize háku - Hook collision line opacity == Neprůhlednost čáry kolize háku @@ -1946,3 +1937,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/danish.txt b/data/languages/danish.txt index 86f092c08a9..bf68c7728a0 100644 --- a/data/languages/danish.txt +++ b/data/languages/danish.txt @@ -1356,7 +1356,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1375,7 +1375,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1513,6 +1519,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1756,7 +1836,10 @@ Always show own player's hook collision line Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1938,6 +2021,15 @@ Eyes Loading sound files == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/dutch.txt b/data/languages/dutch.txt index cf3c53a80ea..98b77a85b97 100644 --- a/data/languages/dutch.txt +++ b/data/languages/dutch.txt @@ -1454,7 +1454,7 @@ Copy info No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1473,7 +1473,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1611,6 +1617,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1806,7 +1886,10 @@ Always show own player's hook collision line Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1949,6 +2032,15 @@ Eyes Loading sound files == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/esperanto.txt b/data/languages/esperanto.txt index 2419b375614..51516742bfb 100644 --- a/data/languages/esperanto.txt +++ b/data/languages/esperanto.txt @@ -881,7 +881,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -900,7 +900,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1089,6 +1095,89 @@ Stop record Record demo == +Edit touch controls +== + +Close +== + +Remote console +== + +Console +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Reset to defaults +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Score limit == @@ -1291,12 +1380,6 @@ Spectate next Spectate previous == -Console -== - -Remote console -== - Screenshot == @@ -1362,9 +1445,6 @@ UI mouse sens. Movement == -Reset to defaults -== - Reset controls == @@ -1623,7 +1703,10 @@ Show other players' hook collision lines Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1928,6 +2011,15 @@ Best Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/estonian.txt b/data/languages/estonian.txt index 429e4b09237..7ad3eeec297 100644 --- a/data/languages/estonian.txt +++ b/data/languages/estonian.txt @@ -1269,9 +1269,6 @@ Copy info No server selected == Serverit pole valitud -Online players (%d) -== Võrgus olevaid mängijaid (%d) - Online clanmates (%d) == Võrgus olevaid klannikaaslasi (%d) @@ -1288,9 +1285,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Klõpsake, et eemaldada see klann oma sõbranimekirjast. -None -== Mitte ühtegi - Are you sure that you want to remove the player '%s' from your friends list? == Kas olete kindel, et soovite eemaldada mängija '%s' oma sõbranimekirjast? @@ -1641,9 +1635,6 @@ Same clan color in scoreboard Hook collision line == Konksu kokkupõrkejoon -Hook collision line width -== Konksu kokkupõrkejoone laius - Hook collision line opacity == Konksu kokkupõrkejoone läbipaistvus @@ -1890,12 +1881,98 @@ Unable to save the skin with a reserved name No local servers found (ports %d-%d) == +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + Dummy is not allowed on this server == Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + [Hertz] Hz == @@ -1903,6 +1980,12 @@ Hz Show client IDs (scoreboard, chat, spectator) == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Basic == @@ -1942,5 +2025,14 @@ Feet Eyes == +Aim +== + +Active: Fire +== + +Active: Hook +== + https://wiki.ddnet.org/wiki/Mapping == diff --git a/data/languages/finnish.txt b/data/languages/finnish.txt index b4412c8e1c3..d733e3cfdec 100644 --- a/data/languages/finnish.txt +++ b/data/languages/finnish.txt @@ -1271,9 +1271,6 @@ Copy info No server selected == Palvelinta ei ole valittu -Online players (%d) -== Online pelaajat (%d) - Online clanmates (%d) == Online klaanikaverit (%d) @@ -1290,9 +1287,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Klikkaa poistaaksesi tämän klaanin ystävälistaltasi. -None -== Ei mitään - Are you sure that you want to remove the player '%s' from your friends list? == Oletko varma että haluat poistaa pelaajan '%s' ystävälistaltasi? @@ -1670,9 +1664,6 @@ Same clan color in scoreboard Hook collision line == Koukku törmäysviiva -Hook collision line width -== Koukku törmäysviivan paksuus - Hook collision line opacity == Koukku törmäysviivan läpinäkyvyys @@ -1860,12 +1851,98 @@ No local servers found (ports %d-%d) Example of usage == +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + Dummy is not allowed on this server == Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + [Hertz] Hz == @@ -1882,6 +1959,12 @@ Show client IDs (scoreboard, chat, spectator) Show only chat messages from team members == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Basic == @@ -1941,5 +2024,14 @@ Spree Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + https://wiki.ddnet.org/wiki/Mapping == diff --git a/data/languages/french.txt b/data/languages/french.txt index 28c148f81c9..168a091213d 100644 --- a/data/languages/french.txt +++ b/data/languages/french.txt @@ -1384,9 +1384,6 @@ Name Plate Hook Collisions == Collisions du grappin -Hook collision line width -== Largeur de la ligne de collision du grappin - Hook collision line opacity == Opacité de la ligne de collision du grappin @@ -1564,9 +1561,6 @@ Could not save downloaded map. Try manually deleting this file: %s Copy info == Copier les infos -Online players (%d) -== Joueurs en ligne (%d) - Online clanmates (%d) == Membres du clan en ligne (%d) @@ -1583,9 +1577,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Cliquez pour retirer ce clan de votre liste d'amis -None -== Aucuns - Add Clan == Ajouter clan @@ -1899,12 +1890,98 @@ No local servers found (ports %d-%d) Example of usage == +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + Dummy is not allowed on this server == Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + [Hertz] Hz == @@ -1912,6 +1989,12 @@ Hz Show client IDs (scoreboard, chat, spectator) == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Basic == @@ -1965,5 +2048,14 @@ Feet Eyes == +Aim +== + +Active: Fire +== + +Active: Hook +== + https://wiki.ddnet.org/wiki/Mapping == diff --git a/data/languages/galician.txt b/data/languages/galician.txt index d6eb655f1d8..8acc8237690 100644 --- a/data/languages/galician.txt +++ b/data/languages/galician.txt @@ -1361,9 +1361,6 @@ Show jumps indicator Hook collision line == Liña de colisión do gancho -Hook collision line width -== Ancho da liña de colisión do gancho - Hook collision line opacity == Opacidade da liña de colisión do gancho @@ -1529,9 +1526,6 @@ Copy info Create a random skin == Crear skin aleatoria -Online players (%d) -== Xogadores en línea (%d) - Online clanmates (%d) == Compañeiro de clan en liña (%d) @@ -1548,9 +1542,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Haz clic para quitar este clan da túa lista de amigos. -None -== Ningún - Add Clan == Agregar clan @@ -1710,6 +1701,18 @@ Communities No server selected == +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + Server filter == @@ -1791,6 +1794,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1864,6 +1941,12 @@ Always show own player's hook collision line Always show other players' hook collision lines == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Show finish messages == @@ -1935,6 +2018,15 @@ Feet Eyes == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/german.txt b/data/languages/german.txt index 5fbea807add..c00f51e020c 100644 --- a/data/languages/german.txt +++ b/data/languages/german.txt @@ -90,7 +90,7 @@ Client == Client Colors of the hook collision line, in case of a possible collision with: -== Die Farbe der Hakenkollisionlinie bei einer möglichen Kollision mit: +== Die Farbe der Hakenkollisionslinie bei einer möglichen Kollision mit: Connecting to == Verbinden mit @@ -848,9 +848,6 @@ Hook collision line Hook collision line opacity == Deckkraft der Hakenkollisionslinie -Hook collision line width -== Breite der Hakenkollisionslinie - Fetch Info == Neu laden @@ -1559,9 +1556,6 @@ Open the settings file Create a random skin == Erstelle einen zufälligen Skin -Online players (%d) -== Online-Spieler (%d) - Online clanmates (%d) == Online-Clanmitglieder (%d) @@ -1578,9 +1572,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Klicke um diesen Clan aus deiner Freundesliste zu entfernen. -None -== Keine - Add Clan == Clan hinzufügen @@ -1959,3 +1950,104 @@ https://wiki.ddnet.org/wiki/Mapping Some fonts could not be loaded. Check the local console for details. == Einige Schriften konnten nicht geladen werden. Prüf die lokale Konsole für Details. + +Online friends (%d) +== Online-Freunde (%d) + +Add friends by entering their name below or by clicking their name in the player list. +== Freunde hinzufügen durch Eingabe des Namens oder durch Klicken auf ihren Namen in Spielerliste. + +Add clanmates by entering their clan below and leaving the name blank. +== Clanmitglieder hinzufügen durch Eingabe des Clans und Leerlassen des Namens. + +Offline friends and clanmates will appear here. +== Offline-Freunde und Clanmitglieder erscheinen hier. + +Edit touch controls +== Berührungseingaben bearbeiten + +Close +== Schließen + +Save changes +== Änderungen speichern + +Error saving touch controls +== Berührungseingaben konnten nicht gespeichert werden + +Could not save touch controls to file. See local console for details. +== Konnte Berührungseingaben nicht in Datei speichern. Siehe lokale Konsole für Details. + +Unsaved changes +== Ungespeicherte Änderungen + +Discard changes +== Änderungen verwerfen + +Are you sure that you want to discard the current changes to the touch controls? +== Willst du wirklich die aktuellen Änderungen zu Berührungseingaben verwerfen? + +Are you sure that you want to reset the touch controls to default? +== Willst du wirklich die Berührungseingaben zurücksetzen? + +Import from clipboard +== Aus Zwischenablage importieren + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== Willst du wirklich Berührungseingaben aus der Zwischenablage importieren? Dies überschreibt deine aktuellen Berührungseingaben. + +Export to clipboard +== In Zwischenablage exportieren + +Direct touch input while ingame +== Direkte Berührungseingaben beim Spielen + +[Direct touch input] +Disabled +== Deaktiviert + +[Direct touch input] +Active action +== Aktive Aktion + +[Direct touch input] +Aim +== Zielen + +[Direct touch input] +Fire +== Feuern + +[Direct touch input] +Hook +== Haken + +Direct touch input while spectating +== Direkte Berührungseingaben beim Zuschauen + +Error loading touch controls +== Konnte Berührungssteuerung nicht laden + +Could not load touch controls from file. See local console for details. +== Konnte Berührungssteuerung nicht von Datei laden. Siehe Details in der lokalen Konsole. + +Could not load default touch controls from file. See local console for details. +== Konnte Standard-Berührungssteuerung nicht von Datei laden. Siehe Details in der lokalen Konsole. + +Could not load touch controls from clipboard. See local console for details. +== Konnte Berührungssteuerung nicht von Zwischenablage laden. Siehe Details in der lokalen Konsole. + +Width of your own hook collision line +== Breite der eigenen Hakenkollisionslinie + +Width of others' hook collision line +== Breite der Hakenkollisionslinie anderer + +Aim +== Zielen + +Active: Fire +== Aktiv: Feuern + +Active: Hook +== Aktiv: Haken diff --git a/data/languages/greek.txt b/data/languages/greek.txt index cff01a43ebf..77dc974a713 100644 --- a/data/languages/greek.txt +++ b/data/languages/greek.txt @@ -934,7 +934,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -953,7 +953,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1145,6 +1151,80 @@ Kill Pause == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1589,7 +1669,10 @@ Show other players' hook collision lines Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1928,6 +2011,15 @@ Best Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + 1 new mention == diff --git a/data/languages/hungarian.txt b/data/languages/hungarian.txt index f0a848f09da..df11f05a2f8 100644 --- a/data/languages/hungarian.txt +++ b/data/languages/hungarian.txt @@ -1344,9 +1344,6 @@ Opacity of freeze bars inside freeze Hook collision line == Horog érintkező vonal indikátora -Hook collision line width -== Horog érintkező vonal szélessége - Hook collision line opacity == Horog érintkező vonal láthatósága @@ -1690,7 +1687,7 @@ Communities No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1709,7 +1706,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Add Clan @@ -1796,6 +1799,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1869,6 +1946,12 @@ Always show own player's hook collision line Always show other players' hook collision lines == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Show finish messages == @@ -1937,6 +2020,15 @@ Feet Eyes == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/italian.txt b/data/languages/italian.txt index 42afef5ff91..e8a3ce85e81 100644 --- a/data/languages/italian.txt +++ b/data/languages/italian.txt @@ -1185,9 +1185,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Fai click per rimuovere questo clan dall'elenco dei tuoi amici. -None -== Nessuno - Are you sure that you want to remove the player '%s' from your friends list? == Sei sicuro di voler rimuovere il giocatore '%s' dalla tua lista amici? @@ -1300,9 +1297,6 @@ Show local player's key presses Hook collision line == Hook line collisione -Hook collision line width -== Larghezza hook line collisione - Hook collision line opacity == Opacità hook line di collisione @@ -1568,13 +1562,22 @@ Copy info Leak IP == -Online players (%d) +Online friends (%d) == [friends (server browser)] Offline (%d) == +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + Server filter == @@ -1704,6 +1707,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1850,6 +1927,12 @@ Always show own player's hook collision line Always show other players' hook collision lines == +Width of your own hook collision line +== + +Width of others' hook collision line +== + A Tee == @@ -1975,5 +2058,14 @@ Eyes FPM == +Aim +== + +Active: Fire +== + +Active: Hook +== + https://wiki.ddnet.org/wiki/Mapping == diff --git a/data/languages/japanese.txt b/data/languages/japanese.txt index 1af876f322b..a1c33cb81f8 100644 --- a/data/languages/japanese.txt +++ b/data/languages/japanese.txt @@ -1382,7 +1382,7 @@ Copy info No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1401,7 +1401,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1539,6 +1545,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1764,7 +1844,10 @@ Always show own player's hook collision line Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1937,6 +2020,15 @@ Eyes Loading sound files == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/korean.txt b/data/languages/korean.txt index 6c036f9682b..b6dbb414264 100644 --- a/data/languages/korean.txt +++ b/data/languages/korean.txt @@ -1359,9 +1359,6 @@ Show jumps indicator Hook collision line == 갈고리 보조선 -Hook collision line width -== 갈고리 보조선 폭 - Hook collision line opacity == 갈고리 보조선 불투명도 @@ -1702,7 +1699,7 @@ Communities No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1721,7 +1718,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Add Clan @@ -1808,6 +1811,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1881,6 +1958,12 @@ Always show own player's hook collision line Always show other players' hook collision lines == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Show finish messages == @@ -1949,6 +2032,15 @@ Feet Eyes == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/kyrgyz.txt b/data/languages/kyrgyz.txt index 1bb30df3645..685a9066b78 100644 --- a/data/languages/kyrgyz.txt +++ b/data/languages/kyrgyz.txt @@ -925,7 +925,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -944,7 +944,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1136,6 +1142,80 @@ Kill Pause == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1580,7 +1660,10 @@ Show other players' hook collision lines Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1919,6 +2002,15 @@ Best Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + 1 new mention == diff --git a/data/languages/norwegian.txt b/data/languages/norwegian.txt index eea7f05af6b..5dc467d0f8d 100644 --- a/data/languages/norwegian.txt +++ b/data/languages/norwegian.txt @@ -1357,7 +1357,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1376,7 +1376,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1514,6 +1520,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1757,7 +1837,10 @@ Always show own player's hook collision line Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1939,6 +2022,15 @@ Eyes Loading sound files == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/persian.txt b/data/languages/persian.txt index bd254c2e8b9..9a6ce8093ac 100644 --- a/data/languages/persian.txt +++ b/data/languages/persian.txt @@ -1286,9 +1286,6 @@ Hook collision line Show other players' hook collision lines == ﺮﮕﯾﺩ ﻥﺎﻨﮑﯾﺯﺎﺑ ﺏﻼﻗ ﺩﺭﻮﺧﺮﺑ ﻁﻮﻄﺧ ﺶﯾﺎﻤﻧ -Hook collision line width -== ﺏﻼﻗ ﺩﺭﻮﺧﺮﺑ ﻂﺧ ﺽﺮﻋ - Hook collision line opacity == ﺏﻼﻗ ﺩﺭﻮﺧﺮﺑ ﻂﺧ ﺖﯿﻓﺎﻔﺷ @@ -1667,9 +1664,6 @@ Copy info No server selected == ﺖﺳﺍ ﻩﺪﺸﻧ ﺏﺎﺨﺘﻧﺍ ىﺭﻭﺮﺳ ﭻﯿﻫ -Online players (%d) -== (%d) ﻦﯾﻼﻧﺁ ﻥﺎﻨﻜﯾﺯﺎﺑ - Online clanmates (%d) == (%d) ﻦﯾﻼﻧﺁ ىﺎﻫ ﯽﻨﻠﻛ ﻢﻫ @@ -1686,9 +1680,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == .ﺪﯿﻨﻛ ﻚﯿﻠﻛ ﺩﻮﺧ ﻥﺎﺘﺳﻭﺩ ﺖﺴﯿﻟ ﺯﺍ ﻦﻠﻛ ﻦﯾﺍ ﻑﺬﺣ ىﺍﺮﺑ -None -== ﻡﺍﺪﻛ ﭻﯿﻫ - Add Clan == ﻦﻠﻛ ﻥﺩﺮﻛ ﻪﻓﺎﺿﺍ @@ -1941,3 +1932,104 @@ The width of texture %s is not divisible by %d, or the height is not divisible b Some fonts could not be loaded. Check the local console for details. == + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/polish.txt b/data/languages/polish.txt index 03a1e80fe26..c71f7e0050a 100644 --- a/data/languages/polish.txt +++ b/data/languages/polish.txt @@ -1386,9 +1386,6 @@ Copy info No server selected == Nie wybrano serwera -Online players (%d) -== Dostępni gracze (%d) - Online clanmates (%d) == Dostępni członkowie klanu (%d) @@ -1405,9 +1402,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Kliknij, aby usunąć ten klan z listy znajomych. -None -== Nikt - Are you sure that you want to remove the player '%s' from your friends list? == Jesteś pewny, że chcesz usunąć gracza '%s' z listy znajomych? @@ -1694,9 +1688,6 @@ Always show own player's hook collision line Always show other players' hook collision lines == Zawsze pokazuj linię kolizyjną haka innych -Hook collision line width -== Szerokość linii kolizji haka - Hook collision line opacity == Przezroczystość linii kolizji haka @@ -1948,3 +1939,104 @@ https://wiki.ddnet.org/wiki/Mapping Some fonts could not be loaded. Check the local console for details. == + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/portuguese.txt b/data/languages/portuguese.txt index 851d0809133..5a17acd6da2 100644 --- a/data/languages/portuguese.txt +++ b/data/languages/portuguese.txt @@ -1182,7 +1182,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1201,7 +1201,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1363,6 +1369,80 @@ Kill Pause == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1675,7 +1755,10 @@ Show other players' hook collision lines Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1930,6 +2013,15 @@ Spree Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + 1 new mention == diff --git a/data/languages/romanian.txt b/data/languages/romanian.txt index f62b920ad68..081459b1e85 100644 --- a/data/languages/romanian.txt +++ b/data/languages/romanian.txt @@ -940,7 +940,7 @@ Leak IP No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -959,7 +959,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1151,6 +1157,80 @@ Kill Pause == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1595,7 +1675,10 @@ Show other players' hook collision lines Always show other players' hook collision lines == -Hook collision line width +Width of your own hook collision line +== + +Width of others' hook collision line == Hook collision line opacity @@ -1934,6 +2017,15 @@ Best Grabs == +Aim +== + +Active: Fire +== + +Active: Hook +== + 1 new mention == diff --git a/data/languages/russian.txt b/data/languages/russian.txt index 85c4f30ab83..5cf8dc14ab7 100644 --- a/data/languages/russian.txt +++ b/data/languages/russian.txt @@ -1366,9 +1366,6 @@ Show jumps indicator Hook collision line == Линия коллизии крюка -Hook collision line width -== Ширина линии - Hook collision line opacity == Прозрачность линии @@ -1561,9 +1558,6 @@ Copy info Create a random skin == Создать случайный скин -Online players (%d) -== Друзья (%d) - Online clanmates (%d) == Участники клана (%d) @@ -1580,9 +1574,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Нажмите, чтобы исключить клан из списка. -None -== Пусто - Add Clan == Добавить клан @@ -1958,3 +1949,104 @@ Feet [skins] Eyes == Глаза + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/serbian.txt b/data/languages/serbian.txt index df9130b9d9f..9ee98eabccc 100644 --- a/data/languages/serbian.txt +++ b/data/languages/serbian.txt @@ -1294,9 +1294,6 @@ Show local player's key presses Hook collision line == Dodir kuke -Hook collision line width -== Debljina linije za dodir kuke - Hook collision line opacity == Providnost linije za dodir kuke @@ -1440,9 +1437,6 @@ Copy info No server selected == Није изабран сервер -Online players (%d) -== Играчи на мрежи (%d) - Online clanmates (%d) == Чланови клана на мрежи (%d) @@ -1459,9 +1453,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Кликните да бисте уклонили овај клан са листе пријатеља. -None -== Ништа - Are you sure that you want to remove the player '%s' from your friends list? == Да ли сте сигурни да желите да уклоните играча '%s' са листе пријатеља? @@ -1761,6 +1752,18 @@ No login required Communities == +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + Server filter == @@ -1809,6 +1812,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1876,6 +1953,12 @@ Always show own player's hook collision line Always show other players' hook collision lines == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Show finish messages == @@ -1944,5 +2027,14 @@ Feet Eyes == +Aim +== + +Active: Fire +== + +Active: Hook +== + https://wiki.ddnet.org/wiki/Mapping == diff --git a/data/languages/serbian_cyrillic.txt b/data/languages/serbian_cyrillic.txt index b58eb080d08..946408f7730 100644 --- a/data/languages/serbian_cyrillic.txt +++ b/data/languages/serbian_cyrillic.txt @@ -1293,9 +1293,6 @@ Show local player's key presses Hook collision line == Додир куке -Hook collision line width -== Дебљина линије за додир куке - Hook collision line opacity == Провидност линије за додир куке @@ -1573,7 +1570,7 @@ Copy info No server selected == -Online players (%d) +Online friends (%d) == Online clanmates (%d) @@ -1592,7 +1589,13 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == -None +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. == Are you sure that you want to remove the player '%s' from your friends list? @@ -1727,6 +1730,80 @@ Dummy is not allowed on this server Please wait… == +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + Loading… == @@ -1833,6 +1910,12 @@ Always show own player's hook collision line Always show other players' hook collision lines == +Width of your own hook collision line +== + +Width of others' hook collision line +== + Show finish messages == @@ -1937,6 +2020,15 @@ Feet Eyes == +Aim +== + +Active: Fire +== + +Active: Hook +== + Moved ingame == diff --git a/data/languages/simplified_chinese.txt b/data/languages/simplified_chinese.txt index cde20f5430c..0b7caebcb17 100644 --- a/data/languages/simplified_chinese.txt +++ b/data/languages/simplified_chinese.txt @@ -1395,9 +1395,6 @@ Show jumps indicator Hook collision line == 钩索辅助线 -Hook collision line width -== 辅助线宽度 - Hook collision line opacity == 辅助线不透明度 @@ -1587,9 +1584,6 @@ Copy info Create a random skin == 随机创造皮肤 -Online players (%d) -== 在线玩家 (%d人) - Online clanmates (%d) == 在线战队成员 (%d人) @@ -1606,9 +1600,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == 单击从你的好友列表移除此战队 -None -== 无 - Add Clan == 添加战队 @@ -1987,3 +1978,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == 未能加载某些字体。检查本地控制台以获取详情。 + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/slovak.txt b/data/languages/slovak.txt index 29dbf781197..12cbbf5a321 100644 --- a/data/languages/slovak.txt +++ b/data/languages/slovak.txt @@ -889,9 +889,6 @@ Leak IP No server selected == Nie je vybraný žiadny server -Online players (%d) -== Online hráči (%d) - Online clanmates (%d) == Online členovia klanu (%d) @@ -908,9 +905,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Kliknutím odoberiete tento klan zo zoznamu priateľov. -None -== Žiadny - Are you sure that you want to remove the player '%s' from your friends list? == Naozaj chcete odstrániť hráča '%s' zo zoznamu priateľov? @@ -1504,9 +1498,6 @@ Hook collision line Show other players' hook collision lines == Zobraziť čiaru kolízie hákov ostatných hráčov -Hook collision line width -== Šírka čiary kolízie háku - Hook collision line opacity == Nepriehľadnosť čiary kolízie háku @@ -1943,3 +1934,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/spanish.txt b/data/languages/spanish.txt index 1612ed35dc6..0d439ca553d 100644 --- a/data/languages/spanish.txt +++ b/data/languages/spanish.txt @@ -1393,9 +1393,6 @@ Show jumps indicator Hook collision line == Línea de colisión del gancho -Hook collision line width -== Ancho de la línea de colisión del gancho - Hook collision line opacity == Opacidad de la lína de colisión del gancho @@ -1561,9 +1558,6 @@ Copy info Create a random skin == Crear skin aleatoria -Online players (%d) -== Jugadores en línea (%d) - Online clanmates (%d) == Compañeros de clan en línea (%d) @@ -1580,9 +1574,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Haz clic para quitar este clan de tu lista de amigos. -None -== Ninguno - Add Clan == Agregar clan @@ -1961,3 +1952,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/swedish.txt b/data/languages/swedish.txt index 1e8e4bc1723..f565e8c236b 100644 --- a/data/languages/swedish.txt +++ b/data/languages/swedish.txt @@ -1279,9 +1279,6 @@ Copy info No server selected == Ingen server vald -Online players (%d) -== Online spelare (%d) - Online clanmates (%d) == Online klanmedlemmar (%d) @@ -1298,9 +1295,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Klicka för att ta bort denna klan från din kompis lista. -None -== Ingen - Are you sure that you want to remove the player '%s' from your friends list? == Är du säker på att du vill ta bort spelare '%s' från din kompis lista? @@ -1537,9 +1531,6 @@ Opacity of freeze bars inside freeze Hook collision line == Hook kollisions linje -Hook collision line width -== Hook kollisions linje bredd - Hook collision line opacity == Hook kollisions linje opacitet @@ -1945,3 +1936,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == Vissa typsnitt kunde inte laddas in. Se den lokala konsolen för detaljer. + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/traditional_chinese.txt b/data/languages/traditional_chinese.txt index b969f1c669b..1deeab44bc3 100644 --- a/data/languages/traditional_chinese.txt +++ b/data/languages/traditional_chinese.txt @@ -1384,9 +1384,6 @@ Show jumps indicator Hook collision line == 鉤索輔助線 -Hook collision line width -== 輔助線寬度 - Hook collision line opacity == 輔助線不透明度 @@ -1576,9 +1573,6 @@ Copy info Create a random skin == 隨機創造外觀 -Online players (%d) -== 在綫玩家 (%d) - Online clanmates (%d) == 在綫戰隊隊友 (%d) @@ -1595,9 +1589,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == 點擊以將此戰隊從好友列表中移除 -None -== 無 - Add Clan == 新增戰隊 @@ -1976,3 +1967,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == 未能載入某些字體。檢查本機控制台以取得詳情。 + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/turkish.txt b/data/languages/turkish.txt index 77f6146d3c7..bddb2e8821f 100644 --- a/data/languages/turkish.txt +++ b/data/languages/turkish.txt @@ -1173,9 +1173,6 @@ Leak IP No server selected == Sunucu seçilmedi -Online players (%d) -== Çevrim içi oyuncular (%d) - Online clanmates (%d) == Çevrim içi klan arkadaşları (%d) @@ -1192,9 +1189,6 @@ Click to remove this player from your friends list. Click to remove this clan from your friends list. == Bu klanı arkadaş listesinden silmek için tıkla. -None -== Yok - Are you sure that you want to remove the player '%s' from your friends list? == '%s' oyuncusunu arkadaş listenizden silmek istediğinize emin misiniz? @@ -1497,9 +1491,6 @@ Show local player's key presses Hook collision line == Kanca çizgisi -Hook collision line width -== Kanca çizgisi kalınlığı - Hook collision line opacity == Kanca çizgisi opaklığı @@ -1957,3 +1948,104 @@ Eyes Some fonts could not be loaded. Check the local console for details. == Bazı fontlar yüklenemiyor. Detaylar için ana konsolu kontrol edin + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/data/languages/ukrainian.txt b/data/languages/ukrainian.txt index 07df531f449..9699ef5c7de 100644 --- a/data/languages/ukrainian.txt +++ b/data/languages/ukrainian.txt @@ -830,9 +830,6 @@ Hook collision line Hook collision line opacity == Непрозорість лінії зіткнення гака -Hook collision line width -== Товщина лінії зіткнення гака - Hook collisions == Зіткнення гака @@ -1127,9 +1124,6 @@ No servers match your filter criteria No updates available == Немає доступних оновлень -None -== Немає - Normal Color == Колір звичайних повідомлень @@ -1152,9 +1146,6 @@ Ok Online clanmates (%d) == Співклановці в мережі (%d) -Online players (%d) -== Гравці в мережі (%d) - Only save improvements == Зберігати лише покращення @@ -1942,3 +1933,104 @@ Zoom in Zoom out == Віддалити + +Online friends (%d) +== + +Add friends by entering their name below or by clicking their name in the player list. +== + +Add clanmates by entering their clan below and leaving the name blank. +== + +Offline friends and clanmates will appear here. +== + +Edit touch controls +== + +Close +== + +Save changes +== + +Error saving touch controls +== + +Could not save touch controls to file. See local console for details. +== + +Unsaved changes +== + +Discard changes +== + +Are you sure that you want to discard the current changes to the touch controls? +== + +Are you sure that you want to reset the touch controls to default? +== + +Import from clipboard +== + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== + +Export to clipboard +== + +Direct touch input while ingame +== + +[Direct touch input] +Disabled +== + +[Direct touch input] +Active action +== + +[Direct touch input] +Aim +== + +[Direct touch input] +Fire +== + +[Direct touch input] +Hook +== + +Direct touch input while spectating +== + +Error loading touch controls +== + +Could not load touch controls from file. See local console for details. +== + +Could not load default touch controls from file. See local console for details. +== + +Could not load touch controls from clipboard. See local console for details. +== + +Width of your own hook collision line +== + +Width of others' hook collision line +== + +Aim +== + +Active: Fire +== + +Active: Hook +== diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp index b68361a6429..beba28fc118 100644 --- a/src/game/client/components/menus_ingame.cpp +++ b/src/game/client/components/menus_ingame.cpp @@ -310,9 +310,9 @@ void CMenus::RenderTouchControlsEditor(CUIRect MainView) Row.VSplitLeft(5.0f, nullptr, &Row); Row.VSplitLeft(240.0f, &Button, &Row); static CButtonContainer s_ResetButton; - if(DoButton_Menu(&s_ResetButton, Localize("Reset to default"), 0, &Button)) + if(DoButton_Menu(&s_ResetButton, Localize("Reset to defaults"), 0, &Button)) { - PopupConfirm(Localize("Reset to default"), + PopupConfirm(Localize("Reset to defaults"), Localize("Are you sure that you want to reset the touch controls to default?"), Localize("Yes"), Localize("No"), &CMenus::PopupConfirmResetTouchControls); @@ -326,7 +326,7 @@ void CMenus::RenderTouchControlsEditor(CUIRect MainView) if(DoButton_Menu(&s_ClipboardImportButton, Localize("Import from clipboard"), 0, &Button)) { PopupConfirm(Localize("Import from clipboard"), - Localize("Are you sure that you want to import the touch controls from the clipboard? The will overwrite your current touch controls."), + Localize("Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls."), Localize("Yes"), Localize("No"), &CMenus::PopupConfirmImportTouchControlsClipboard); } From b452636d522cf3d072e286733a67cdc45fc3b30c Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Sun, 1 Dec 2024 23:19:07 +0100 Subject: [PATCH 25/58] Bump nightly version to 18.9 --- src/game/version.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/version.h b/src/game/version.h index cdab44c3914..5b48443e4bb 100644 --- a/src/game/version.h +++ b/src/game/version.h @@ -3,7 +3,7 @@ #ifndef GAME_VERSION_H #define GAME_VERSION_H #ifndef GAME_RELEASE_VERSION -#define GAME_RELEASE_VERSION "18.8" +#define GAME_RELEASE_VERSION "18.9" #endif // teeworlds @@ -13,7 +13,7 @@ #define GAME_NETVERSION7 "0.7 802f1be60a05665f" // ddnet -#define DDNET_VERSION_NUMBER 18080 +#define DDNET_VERSION_NUMBER 18090 extern const char *GIT_SHORTREV_HASH; #define GAME_NAME "DDNet" #endif From 962ae09014ed4b67eb2a22b0abc1d39606b5a9b4 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Mon, 2 Dec 2024 16:56:17 +0800 Subject: [PATCH 26/58] use remote tee angle when spectating --- src/game/client/components/players.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/client/components/players.cpp b/src/game/client/components/players.cpp index 100e9824d7c..525f13612ed 100644 --- a/src/game/client/components/players.cpp +++ b/src/game/client/components/players.cpp @@ -453,7 +453,7 @@ void CPlayers::RenderPlayer( float AttackTicksPassed = AttackTime * (float)Client()->GameTickSpeed(); float Angle; - if(Local && (!m_pClient->m_Snap.m_SpecInfo.m_Active || m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) && Client()->State() != IClient::STATE_DEMOPLAYBACK) + if(Local && !m_pClient->m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK) { // just use the direct input if it's the local player we are rendering Angle = angle(m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy] * m_pClient->m_Camera.m_Zoom); From 884de60978f79eb853e57258e9dc0f2270756080 Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Mon, 2 Dec 2024 12:28:02 +0100 Subject: [PATCH 27/58] Update credits --- src/game/server/ddracechat.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/game/server/ddracechat.cpp b/src/game/server/ddracechat.cpp index 2bfe8ce84cc..f8a6025bd23 100644 --- a/src/game/server/ddracechat.cpp +++ b/src/game/server/ddracechat.cpp @@ -56,7 +56,9 @@ void CGameContext::ConCredits(IConsole::IResult *pResult, void *pUserData) pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "DynamoFox, MilkeeyCat, iMilchshake, SchrodingerZhu,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "catseyenebulous, Rei-Tw, Matodor, Emilcha, art0007i & others"); + "catseyenebulous, Rei-Tw, Matodor, Emilcha, art0007i, SollyBunny,"); + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", + "0xfaulty & others"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Based on DDRace by the DDRace developers,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", From 84a0303467f2b8e2cc52f6a266f7e6e7197e4553 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Mon, 2 Dec 2024 19:47:58 +0800 Subject: [PATCH 28/58] send camera type flag to prevent unexpected viewpos and target update --- datasrc/network.py | 2 +- src/game/client/components/camera.h | 3 +++ src/game/client/components/controls.cpp | 6 +++++- src/game/server/player.cpp | 11 ++++++++--- src/game/server/player.h | 2 ++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/datasrc/network.py b/datasrc/network.py index 08fa59c5437..ad7f76abd04 100644 --- a/datasrc/network.py +++ b/datasrc/network.py @@ -4,7 +4,7 @@ from datatypes import Enum, Flags, NetArray, NetBool, NetEvent, NetEventEx, NetIntAny, NetIntRange, NetMessage, NetMessageEx, NetObject, NetObjectEx, NetString, NetStringHalfStrict, NetStringStrict, NetTick Emotes = ["NORMAL", "PAIN", "HAPPY", "SURPRISE", "ANGRY", "BLINK"] -PlayerFlags = ["PLAYING", "IN_MENU", "CHATTING", "SCOREBOARD", "AIM"] +PlayerFlags = ["PLAYING", "IN_MENU", "CHATTING", "SCOREBOARD", "AIM", "SPEC_CAM"] GameFlags = ["TEAMS", "FLAGS"] GameStateFlags = ["GAMEOVER", "SUDDENDEATH", "PAUSED", "RACETIME"] CharacterFlags = ["SOLO", "JETPACK", "COLLISION_DISABLED", "ENDLESS_HOOK", "ENDLESS_JUMP", "SUPER", diff --git a/src/game/client/components/camera.h b/src/game/client/components/camera.h index b487c680b5b..32d7c3a10c5 100644 --- a/src/game/client/components/camera.h +++ b/src/game/client/components/camera.h @@ -14,6 +14,7 @@ class CCamera : public CComponent { friend class CMenuBackground; +public: enum { CAMTYPE_UNDEFINED = -1, @@ -21,6 +22,7 @@ class CCamera : public CComponent CAMTYPE_PLAYER, }; +private: int m_CamType; vec2 m_aLastPos[NUM_DUMMIES]; vec2 m_PrevCenter; @@ -83,6 +85,7 @@ class CCamera : public CComponent int Deadzone() const; int FollowFactor() const; + int CamType() const { return m_CamType; } void UpdateCamera(); diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp index 7969b109dbd..d464a9799f2 100644 --- a/src/game/client/components/controls.cpp +++ b/src/game/client/components/controls.cpp @@ -191,9 +191,13 @@ int CControls::SnapInput(int *pData) if(m_pClient->m_Scoreboard.Active()) m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_SCOREBOARD; - if(m_pClient->m_Controls.m_aShowHookColl[g_Config.m_ClDummy] && Client()->ServerCapAnyPlayerFlag()) + if(Client()->ServerCapAnyPlayerFlag() && m_pClient->m_Controls.m_aShowHookColl[g_Config.m_ClDummy]) m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_AIM; + // force send spec cam flag on first tick to let server know that it is supported + if(Client()->ServerCapAnyPlayerFlag() && (m_pClient->m_Camera.CamType() == CCamera::CAMTYPE_SPEC || m_LastSendTime == 0)) + m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_SPEC_CAM; + bool Send = m_aLastData[g_Config.m_ClDummy].m_PlayerFlags != m_aInputData[g_Config.m_ClDummy].m_PlayerFlags; m_aLastData[g_Config.m_ClDummy].m_PlayerFlags = m_aInputData[g_Config.m_ClDummy].m_PlayerFlags; diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index 12767386b48..ab7d72c3f17 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -146,6 +146,8 @@ void CPlayer::Reset() m_SwapTargetsClientId = -1; m_BirthdayAnnounced = false; m_RescueMode = RESCUEMODE_AUTO; + + m_CanUseSpectatingPlayerFlag = false; } static int PlayerFlags_SixToSeven(int Flags) @@ -511,7 +513,7 @@ void CPlayer::OnPredictedInput(CNetObj_PlayerInput *pNewInput) m_NumInputs++; - if(m_pCharacter && !m_Paused) + if(m_pCharacter && !m_Paused && !(pNewInput->m_PlayerFlags & PLAYERFLAG_SPEC_CAM)) m_pCharacter->OnPredictedInput(pNewInput); // Magic number when we can hope that client has successfully identified itself @@ -527,7 +529,7 @@ void CPlayer::OnDirectInput(CNetObj_PlayerInput *pNewInput) AfkTimer(); - if(((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorId == SPEC_FREEVIEW) + if(((pNewInput->m_PlayerFlags & PLAYERFLAG_SPEC_CAM) || !m_CanUseSpectatingPlayerFlag) && ((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorId == SPEC_FREEVIEW) m_ViewPos = vec2(pNewInput->m_TargetX, pNewInput->m_TargetY); // check for activity @@ -546,6 +548,9 @@ void CPlayer::OnPredictedEarlyInput(CNetObj_PlayerInput *pNewInput) { m_PlayerFlags = pNewInput->m_PlayerFlags; + // enable spectating flag feature if the player has ever used it + m_CanUseSpectatingPlayerFlag = m_CanUseSpectatingPlayerFlag || (m_PlayerFlags & PLAYERFLAG_SPEC_CAM); + if(!m_pCharacter && m_Team != TEAM_SPECTATORS && (pNewInput->m_Fire & 1)) m_Spawning = true; @@ -553,7 +558,7 @@ void CPlayer::OnPredictedEarlyInput(CNetObj_PlayerInput *pNewInput) if(m_PlayerFlags & PLAYERFLAG_CHATTING) return; - if(m_pCharacter && !m_Paused) + if(m_pCharacter && !m_Paused && !(m_PlayerFlags & PLAYERFLAG_SPEC_CAM)) m_pCharacter->OnDirectInput(pNewInput); } diff --git a/src/game/server/player.h b/src/game/server/player.h index c281ac5b144..ff3929333d0 100644 --- a/src/game/server/player.h +++ b/src/game/server/player.h @@ -128,6 +128,8 @@ class CPlayer int m_Max; } m_Latency; + bool m_CanUseSpectatingPlayerFlag; + private: const uint32_t m_UniqueClientId; CCharacter *m_pCharacter; From 69bd36d06ec7bdef92620c3dc0b8c7636dc4bdc7 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Mon, 2 Dec 2024 22:40:53 +0800 Subject: [PATCH 29/58] fix spectate closest --- src/game/client/components/spectator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp index 5ad32199b63..490309a16f0 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -623,7 +623,7 @@ void CSpectator::Spectate(int SpectatorId) void CSpectator::SpectateClosest(bool AllowSelf) { - if(CanChangeSpectatorId()) + if(!CanChangeSpectatorId()) return; const CGameClient::CSnapState &Snap = m_pClient->m_Snap; From fb640a64450bc5635ece19af28ef5ae31c647110 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Sat, 30 Nov 2024 15:23:38 +0800 Subject: [PATCH 30/58] rework pointer --- datasrc/network.py | 6 ++ src/game/client/components/camera.cpp | 2 +- src/game/client/components/controls.cpp | 14 ----- src/game/client/gameclient.cpp | 77 +++++++++++++++++++++---- src/game/client/gameclient.h | 3 + src/game/server/ddracechat.cpp | 6 +- src/game/server/ddracecommands.cpp | 8 ++- src/game/server/gamecontext.cpp | 9 +++ src/game/server/gamecontext.h | 1 + src/game/server/player.cpp | 31 +++++++++- src/game/server/player.h | 13 +++++ 11 files changed, 137 insertions(+), 33 deletions(-) diff --git a/datasrc/network.py b/datasrc/network.py index ad7f76abd04..d03edce9795 100644 --- a/datasrc/network.py +++ b/datasrc/network.py @@ -540,6 +540,12 @@ NetIntRange("m_Show", 0, 2), ]), + NetMessageEx("Cl_CameraInfo", "camera-info@netmsg.ddnet.org", [ + NetIntAny("m_Zoom"), + NetIntAny("m_Deadzone"), + NetIntAny("m_FollowFactor"), + ]), + NetMessageEx("Sv_TeamsState", "teamsstate@netmsg.ddnet.tw", []), NetMessageEx("Sv_DDRaceTime", "ddrace-time@netmsg.ddnet.tw", [ diff --git a/src/game/client/components/camera.cpp b/src/game/client/components/camera.cpp index 5b6607dcbb7..2d64955bec5 100644 --- a/src/game/client/components/camera.cpp +++ b/src/game/client/components/camera.cpp @@ -157,7 +157,7 @@ void CCamera::UpdateCamera() if(l > 0.0001f) // make sure that this isn't 0 { float OffsetAmount = maximum(l - Deadzone(), 0.0f) * (FollowFactor() / 100.0f); - m_DyncamTargetCameraOffset = normalize(MousePos) * OffsetAmount; + m_DyncamTargetCameraOffset = normalize_pre_length(MousePos, l) * OffsetAmount; } m_LastMousePos = MousePos; diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp index d464a9799f2..e9cd545f091 100644 --- a/src/game/client/components/controls.cpp +++ b/src/game/client/components/controls.cpp @@ -215,13 +215,6 @@ int CControls::SnapInput(int *pData) m_aInputData[g_Config.m_ClDummy].m_TargetX = (int)m_aMousePos[g_Config.m_ClDummy].x; m_aInputData[g_Config.m_ClDummy].m_TargetY = (int)m_aMousePos[g_Config.m_ClDummy].y; - // scale TargetX, TargetY by zoom. - if(!m_pClient->m_Snap.m_SpecInfo.m_Active) - { - m_aInputData[g_Config.m_ClDummy].m_TargetX *= m_pClient->m_Camera.m_Zoom; - m_aInputData[g_Config.m_ClDummy].m_TargetY *= m_pClient->m_Camera.m_Zoom; - } - // send once a second just to be sure Send = Send || time_get() > m_LastSendTime + time_freq(); } @@ -250,13 +243,6 @@ int CControls::SnapInput(int *pData) if(!m_aInputDirectionLeft[g_Config.m_ClDummy] && m_aInputDirectionRight[g_Config.m_ClDummy]) m_aInputData[g_Config.m_ClDummy].m_Direction = 1; - // scale TargetX, TargetY by zoom. - if(!m_pClient->m_Snap.m_SpecInfo.m_Active) - { - m_aInputData[g_Config.m_ClDummy].m_TargetX *= m_pClient->m_Camera.m_Zoom; - m_aInputData[g_Config.m_ClDummy].m_TargetY *= m_pClient->m_Camera.m_Zoom; - } - // dummy copy moves if(g_Config.m_ClDummyCopyMoves) { diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 26f495a2ed7..4a01e989018 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -662,8 +662,11 @@ void CGameClient::OnReset() // m_aTuningList is reset in LoadMapSettings + m_LastShowDistanceZoom = 0.0f; m_LastZoom = 0.0f; m_LastScreenAspect = 0.0f; + m_LastDeadzone = 0.0f; + m_LastFollowFactor = 0.0f; m_LastDummyConnected = false; m_MultiViewPersonalZoom = 0.0f; @@ -2048,32 +2051,82 @@ void CGameClient::OnNewSnapshot() m_aShowOthers[g_Config.m_ClDummy] = g_Config.m_ClShowOthers; } - float ZoomToSend = m_Camera.m_Zoom; + float ShowDistanceZoom = m_Camera.m_Zoom; + float Zoom = m_Camera.m_Zoom; if(m_Camera.m_Zooming) { if(m_Camera.m_ZoomSmoothingTarget > m_Camera.m_Zoom) // Zooming out - ZoomToSend = m_Camera.m_ZoomSmoothingTarget; - else if(m_Camera.m_ZoomSmoothingTarget < m_Camera.m_Zoom && m_LastZoom > 0) // Zooming in - ZoomToSend = m_LastZoom; + ShowDistanceZoom = m_Camera.m_ZoomSmoothingTarget; + else if(m_Camera.m_ZoomSmoothingTarget < m_Camera.m_Zoom && m_LastShowDistanceZoom > 0) // Zooming in + ShowDistanceZoom = m_LastShowDistanceZoom; + + Zoom = m_Camera.m_ZoomSmoothingTarget; + } + + float Deadzone = m_Camera.Deadzone(); + float FollowFactor = m_Camera.FollowFactor(); + + // initialize dummy vital when first connected + if(Client()->DummyConnected() && !m_LastDummyConnected) + { + { + CNetMsg_Cl_ShowDistance Msg; + float x, y; + RenderTools()->CalcScreenParams(Graphics()->ScreenAspect(), ShowDistanceZoom, &x, &y); + Msg.m_X = x; + Msg.m_Y = y; + CMsgPacker Packer(&Msg); + Msg.Pack(&Packer); + Client()->SendMsg(IClient::CONN_DUMMY, &Packer, MSGFLAG_VITAL); + } + { + CNetMsg_Cl_CameraInfo Msg; + Msg.m_Zoom = round_truncate(Zoom * 1000.f); + Msg.m_Deadzone = Deadzone; + Msg.m_FollowFactor = FollowFactor; + CMsgPacker Packer(&Msg); + Msg.Pack(&Packer); + Client()->SendMsg(IClient::CONN_DUMMY, &Packer, MSGFLAG_VITAL); + } } - if(ZoomToSend != m_LastZoom || Graphics()->ScreenAspect() != m_LastScreenAspect || (Client()->DummyConnected() && !m_LastDummyConnected)) + // send show distance + if(ShowDistanceZoom != m_LastShowDistanceZoom || Graphics()->ScreenAspect() != m_LastScreenAspect) { CNetMsg_Cl_ShowDistance Msg; float x, y; - RenderTools()->CalcScreenParams(Graphics()->ScreenAspect(), ZoomToSend, &x, &y); + RenderTools()->CalcScreenParams(Graphics()->ScreenAspect(), ShowDistanceZoom, &x, &y); Msg.m_X = x; Msg.m_Y = y; - Client()->ChecksumData()->m_Zoom = ZoomToSend; + Client()->ChecksumData()->m_Zoom = ShowDistanceZoom; CMsgPacker Packer(&Msg); Msg.Pack(&Packer); - if(ZoomToSend != m_LastZoom) - Client()->SendMsg(IClient::CONN_MAIN, &Packer, MSGFLAG_VITAL); - if(Client()->DummyConnected()) + + Client()->SendMsg(IClient::CONN_MAIN, &Packer, MSGFLAG_VITAL); + if(Client()->DummyConnected() && m_LastDummyConnected) Client()->SendMsg(IClient::CONN_DUMMY, &Packer, MSGFLAG_VITAL); - m_LastZoom = ZoomToSend; - m_LastScreenAspect = Graphics()->ScreenAspect(); } + + // send camera info + if(Zoom != m_LastZoom || Deadzone != m_LastDeadzone || FollowFactor != m_LastFollowFactor) + { + CNetMsg_Cl_CameraInfo Msg; + Msg.m_Zoom = round_truncate(Zoom * 1000.f); + Msg.m_Deadzone = Deadzone; + Msg.m_FollowFactor = FollowFactor; + CMsgPacker Packer(&Msg); + Msg.Pack(&Packer); + + Client()->SendMsg(IClient::CONN_MAIN, &Packer, MSGFLAG_VITAL); + if(Client()->DummyConnected() && m_LastDummyConnected) + Client()->SendMsg(IClient::CONN_DUMMY, &Packer, MSGFLAG_VITAL); + } + + m_LastShowDistanceZoom = ShowDistanceZoom; + m_LastZoom = Zoom; + m_LastScreenAspect = Graphics()->ScreenAspect(); + m_LastDeadzone = Deadzone; + m_LastFollowFactor = FollowFactor; m_LastDummyConnected = Client()->DummyConnected(); for(auto &pComponent : m_vpAll) diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index f5a1e2eefab..6b42d95cb51 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -819,8 +819,11 @@ class CGameClient : public IGameClient CTuningParams m_aTuningList[NUM_TUNEZONES]; CTuningParams *TuningList() { return m_aTuningList; } + float m_LastShowDistanceZoom; float m_LastZoom; float m_LastScreenAspect; + float m_LastDeadzone; + float m_LastFollowFactor; bool m_LastDummyConnected; void HandleMultiView(); diff --git a/src/game/server/ddracechat.cpp b/src/game/server/ddracechat.cpp index 2bfe8ce84cc..e63fd0a4d29 100644 --- a/src/game/server/ddracechat.cpp +++ b/src/game/server/ddracechat.cpp @@ -1920,10 +1920,12 @@ void CGameContext::ConTeleCursor(IConsole::IResult *pResult, void *pUserData) return; } + // default to view pos when character is not available vec2 Pos = pPlayer->m_ViewPos; - if(pResult->NumArguments() == 0 && !pPlayer->IsPaused()) + if(pResult->NumArguments() == 0 && !pPlayer->IsPaused() && pPlayer->GetCharacter() && pPlayer->GetCharacter()->IsAlive()) { - Pos += vec2(pChr->Core()->m_Input.m_TargetX, pChr->Core()->m_Input.m_TargetY); + vec2 Target = vec2(pChr->Core()->m_Input.m_TargetX, pChr->Core()->m_Input.m_TargetY); + Pos = pPlayer->m_CameraInfo.ConvertTargetToWorld(pPlayer->GetCharacter()->GetPos(), Target); } else if(pResult->NumArguments() > 0) { diff --git a/src/game/server/ddracecommands.cpp b/src/game/server/ddracecommands.cpp index d1329645698..4b8fd2d2042 100644 --- a/src/game/server/ddracecommands.cpp +++ b/src/game/server/ddracecommands.cpp @@ -439,10 +439,12 @@ void CGameContext::ConTeleport(IConsole::IResult *pResult, void *pUserData) if(pChr && pPlayer && pSelf->GetPlayerChar(TeleTo)) { - vec2 Pos = pSelf->m_apPlayers[TeleTo]->m_ViewPos; - if(!pPlayer->IsPaused() && !pResult->NumArguments()) + // default to view pos when character is not available + vec2 Pos = pPlayer->m_ViewPos; + if(pResult->NumArguments() == 0 && !pPlayer->IsPaused() && pChr->IsAlive()) { - Pos += vec2(pChr->Core()->m_Input.m_TargetX, pChr->Core()->m_Input.m_TargetY); + vec2 Target = vec2(pChr->Core()->m_Input.m_TargetX, pChr->Core()->m_Input.m_TargetY); + Pos = pPlayer->m_CameraInfo.ConvertTargetToWorld(pChr->GetPos(), Target); } pSelf->Teleport(pChr, Pos); pChr->ResetJumps(); diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 2c340288329..6c319fbac9f 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -2075,6 +2075,9 @@ void CGameContext::OnMessage(int MsgId, CUnpacker *pUnpacker, int ClientId) case NETMSGTYPE_CL_SHOWDISTANCE: OnShowDistanceNetMessage(static_cast(pRawMsg), ClientId); break; + case NETMSGTYPE_CL_CAMERAINFO: + OnCameraInfoNetMessage(static_cast(pRawMsg), ClientId); + break; case NETMSGTYPE_CL_SETSPECTATORMODE: OnSetSpectatorModeNetMessage(static_cast(pRawMsg), ClientId); break; @@ -2585,6 +2588,12 @@ void CGameContext::OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, pPlayer->m_ShowDistance = vec2(pMsg->m_X, pMsg->m_Y); } +void CGameContext::OnCameraInfoNetMessage(const CNetMsg_Cl_CameraInfo *pMsg, int ClientId) +{ + CPlayer *pPlayer = m_apPlayers[ClientId]; + pPlayer->m_CameraInfo.Write(pMsg); +} + void CGameContext::OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientId) { if(m_World.m_Paused) diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index 8ca8027218f..c5e6b19f83b 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -309,6 +309,7 @@ class CGameContext : public IGameServer void OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientId); void OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientId); void OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientId); + void OnCameraInfoNetMessage(const CNetMsg_Cl_CameraInfo *pMsg, int ClientId); void OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientId); void OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientId); void OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientId); diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index ab7d72c3f17..740736d423a 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -146,8 +146,9 @@ void CPlayer::Reset() m_SwapTargetsClientId = -1; m_BirthdayAnnounced = false; m_RescueMode = RESCUEMODE_AUTO; - m_CanUseSpectatingPlayerFlag = false; + + m_CameraInfo.Reset(); } static int PlayerFlags_SixToSeven(int Flags) @@ -959,3 +960,31 @@ void CPlayer::ProcessScoreResult(CScorePlayerResult &Result) } } } + +vec2 CPlayer::CCameraInfo::ConvertTargetToWorld(vec2 Position, vec2 Target) const +{ + vec2 TargetCameraOffset(0, 0); + float l = length(Target); + + if(l > 0.0001f) // make sure that this isn't 0 + { + float OffsetAmount = maximum(l - m_Deadzone, 0.0f) * (m_FollowFactor / 100.0f); + TargetCameraOffset = normalize_pre_length(Target, l) * OffsetAmount; + } + + return Position + (Target - TargetCameraOffset) * m_Zoom + TargetCameraOffset; +} + +void CPlayer::CCameraInfo::Write(const CNetMsg_Cl_CameraInfo *Msg) +{ + m_Zoom = Msg->m_Zoom / 1000.0f; + m_Deadzone = Msg->m_Deadzone; + m_FollowFactor = Msg->m_FollowFactor; +} + +void CPlayer::CCameraInfo::Reset() +{ + m_Zoom = 1.0f; + m_Deadzone = 0.0f; + m_FollowFactor = 0.0f; +} diff --git a/src/game/server/player.h b/src/game/server/player.h index ff3929333d0..660f2cfa82d 100644 --- a/src/game/server/player.h +++ b/src/game/server/player.h @@ -193,6 +193,19 @@ class CPlayer bool m_SpecTeam; bool m_NinjaJetpack; + // camera info is used sparingly for converting aim target to absolute world coordinates + class CCameraInfo + { + float m_Zoom; + int m_Deadzone; + int m_FollowFactor; + + public: + vec2 ConvertTargetToWorld(vec2 Position, vec2 Target) const; + void Write(const CNetMsg_Cl_CameraInfo *pMsg); + void Reset(); + } m_CameraInfo; + int m_ChatScore; bool m_Moderating; From cdb983fdf80f9b9180866a5d5fc18f156606930a Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Tue, 3 Dec 2024 00:12:02 +0800 Subject: [PATCH 31/58] Revert "remove self from spectate selector" This reverts commit b4e4e5d7f9f1b15517ec2099399c665d26b90243. --- src/game/client/components/spectator.cpp | 3 --- src/game/client/gameclient.cpp | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp index 490309a16f0..2077bf1da09 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -403,9 +403,6 @@ void CSpectator::OnRender() if(!m_pClient->m_Snap.m_apInfoByDDTeamName[i] || m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_Team == TEAM_SPECTATORS) continue; - if(Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId == m_pClient->m_Snap.m_LocalClientId) - continue; - ++Count; if(Count == PerLine + 1 || (Count > PerLine + 1 && (Count - 1) % PerLine == 0)) diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 26f495a2ed7..bb088c66710 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -4103,9 +4103,6 @@ bool CGameClient::InitMultiView(int Team) int Count = 0; for(int i = 0; i < MAX_CLIENTS; i++) { - if(Client()->State() != IClient::STATE_DEMOPLAYBACK && m_Snap.m_apPlayerInfos[i] && m_Snap.m_apPlayerInfos[i]->m_ClientId == m_Snap.m_LocalClientId) - continue; - vec2 PlayerPos; // get the position of the player From cb623cbc3f8aea12ecebd963ba2245a70f2938e3 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Tue, 3 Dec 2024 00:21:51 +0800 Subject: [PATCH 32/58] fully allow clicking on yourself to spectate --- src/game/client/components/spectator.cpp | 9 +++------ src/game/client/components/spectator.h | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp index 2077bf1da09..6c7f0b81c34 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -123,7 +123,7 @@ void CSpectator::ConSpectatePrevious(IConsole::IResult *pResult, void *pUserData void CSpectator::ConSpectateClosest(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; - pSelf->SpectateClosest(true); + pSelf->SpectateClosest(); } void CSpectator::ConMultiView(IConsole::IResult *pResult, void *pUserData) @@ -181,7 +181,7 @@ bool CSpectator::OnInput(const IInput::CEvent &Event) if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) Spectate(SPEC_FREEVIEW); else - SpectateClosest(Client()->State() == IClient::STATE_DEMOPLAYBACK); + SpectateClosest(); return true; } } @@ -618,7 +618,7 @@ void CSpectator::Spectate(int SpectatorId) Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); } -void CSpectator::SpectateClosest(bool AllowSelf) +void CSpectator::SpectateClosest() { if(!CanChangeSpectatorId()) return; @@ -642,9 +642,6 @@ void CSpectator::SpectateClosest(bool AllowSelf) if(i == SpectatorId || !Snap.m_aCharacters[i].m_Active || !Snap.m_apPlayerInfos[i] || Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS) continue; - if(!AllowSelf && i == Snap.m_LocalClientId) - continue; - const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[i].m_Cur; int Distance = distance(CurPosition, vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y)); if(NewSpectatorId == -1 || Distance < ClosestDistance) diff --git a/src/game/client/components/spectator.h b/src/game/client/components/spectator.h index cf34196c502..c454c0cf5dc 100644 --- a/src/game/client/components/spectator.h +++ b/src/game/client/components/spectator.h @@ -48,7 +48,7 @@ class CSpectator : public CComponent virtual void OnReset() override; void Spectate(int SpectatorId); - void SpectateClosest(bool AllowSelf); + void SpectateClosest(); bool IsActive() const { return m_Active; } }; From 1ea9d868ca921fe4a62fc09017d3ecc06b7a8ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Mon, 2 Dec 2024 21:58:10 +0100 Subject: [PATCH 33/58] Fix hook collisions preview string not being localizable Use single quotes as the localization script does not support strings containing escaped double quotes. --- data/languages/arabic.txt | 3 +++ data/languages/azerbaijani.txt | 3 +++ data/languages/belarusian.txt | 3 +++ data/languages/bosnian.txt | 3 +++ data/languages/brazilian_portuguese.txt | 3 +++ data/languages/bulgarian.txt | 3 +++ data/languages/catalan.txt | 3 +++ data/languages/chuvash.txt | 3 +++ data/languages/czech.txt | 3 +++ data/languages/danish.txt | 3 +++ data/languages/dutch.txt | 3 +++ data/languages/esperanto.txt | 3 +++ data/languages/estonian.txt | 3 +++ data/languages/finnish.txt | 3 +++ data/languages/french.txt | 3 +++ data/languages/galician.txt | 3 +++ data/languages/german.txt | 3 +++ data/languages/greek.txt | 3 +++ data/languages/hungarian.txt | 3 +++ data/languages/italian.txt | 3 +++ data/languages/japanese.txt | 3 +++ data/languages/korean.txt | 3 +++ data/languages/kyrgyz.txt | 3 +++ data/languages/norwegian.txt | 3 +++ data/languages/persian.txt | 3 +++ data/languages/polish.txt | 3 +++ data/languages/portuguese.txt | 3 +++ data/languages/romanian.txt | 3 +++ data/languages/russian.txt | 3 +++ data/languages/serbian.txt | 3 +++ data/languages/serbian_cyrillic.txt | 3 +++ data/languages/simplified_chinese.txt | 3 +++ data/languages/slovak.txt | 3 +++ data/languages/spanish.txt | 3 +++ data/languages/swedish.txt | 3 +++ data/languages/traditional_chinese.txt | 3 +++ data/languages/turkish.txt | 3 +++ data/languages/ukrainian.txt | 3 +++ src/game/client/components/menus_settings.cpp | 2 +- 39 files changed, 115 insertions(+), 1 deletion(-) diff --git a/data/languages/arabic.txt b/data/languages/arabic.txt index 0b251a761c7..744c666e9f2 100644 --- a/data/languages/arabic.txt +++ b/data/languages/arabic.txt @@ -1852,6 +1852,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/azerbaijani.txt b/data/languages/azerbaijani.txt index fae7f75d2a6..37854268c69 100644 --- a/data/languages/azerbaijani.txt +++ b/data/languages/azerbaijani.txt @@ -2027,6 +2027,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/belarusian.txt b/data/languages/belarusian.txt index 852aaa927e9..6cb7a8bc800 100644 --- a/data/languages/belarusian.txt +++ b/data/languages/belarusian.txt @@ -1990,6 +1990,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Basic == diff --git a/data/languages/bosnian.txt b/data/languages/bosnian.txt index a1c9519089a..dcc4037608a 100644 --- a/data/languages/bosnian.txt +++ b/data/languages/bosnian.txt @@ -1801,6 +1801,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/brazilian_portuguese.txt b/data/languages/brazilian_portuguese.txt index c347ab97ecf..a7ec8face18 100644 --- a/data/languages/brazilian_portuguese.txt +++ b/data/languages/brazilian_portuguese.txt @@ -2058,6 +2058,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/bulgarian.txt b/data/languages/bulgarian.txt index b5749f41225..fcffffdbc78 100644 --- a/data/languages/bulgarian.txt +++ b/data/languages/bulgarian.txt @@ -1693,6 +1693,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show kill messages == diff --git a/data/languages/catalan.txt b/data/languages/catalan.txt index a0be38044a2..cc2acb78f1f 100644 --- a/data/languages/catalan.txt +++ b/data/languages/catalan.txt @@ -1873,6 +1873,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/chuvash.txt b/data/languages/chuvash.txt index 55ef81af7e5..ea60ba0661a 100644 --- a/data/languages/chuvash.txt +++ b/data/languages/chuvash.txt @@ -1693,6 +1693,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show kill messages == diff --git a/data/languages/czech.txt b/data/languages/czech.txt index d45e6aa6fba..670746c328d 100644 --- a/data/languages/czech.txt +++ b/data/languages/czech.txt @@ -2030,6 +2030,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/danish.txt b/data/languages/danish.txt index bf68c7728a0..fc989d26c81 100644 --- a/data/languages/danish.txt +++ b/data/languages/danish.txt @@ -1860,6 +1860,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/dutch.txt b/data/languages/dutch.txt index 98b77a85b97..410adbedee5 100644 --- a/data/languages/dutch.txt +++ b/data/languages/dutch.txt @@ -1910,6 +1910,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/esperanto.txt b/data/languages/esperanto.txt index 51516742bfb..13df65b9f95 100644 --- a/data/languages/esperanto.txt +++ b/data/languages/esperanto.txt @@ -1724,6 +1724,9 @@ Nothing hookable Something hookable == +Preview 'Hook collisions' being pressed +== + Show kill messages == diff --git a/data/languages/estonian.txt b/data/languages/estonian.txt index 7ad3eeec297..a4a0c2afd50 100644 --- a/data/languages/estonian.txt +++ b/data/languages/estonian.txt @@ -1986,6 +1986,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Basic == diff --git a/data/languages/finnish.txt b/data/languages/finnish.txt index d733e3cfdec..3ac49e14156 100644 --- a/data/languages/finnish.txt +++ b/data/languages/finnish.txt @@ -1965,6 +1965,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Basic == diff --git a/data/languages/french.txt b/data/languages/french.txt index 168a091213d..f08074e470a 100644 --- a/data/languages/french.txt +++ b/data/languages/french.txt @@ -1995,6 +1995,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Basic == diff --git a/data/languages/galician.txt b/data/languages/galician.txt index 8acc8237690..a667fa3494f 100644 --- a/data/languages/galician.txt +++ b/data/languages/galician.txt @@ -1947,6 +1947,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/german.txt b/data/languages/german.txt index c00f51e020c..2af1614650e 100644 --- a/data/languages/german.txt +++ b/data/languages/german.txt @@ -2051,3 +2051,6 @@ Active: Fire Active: Hook == Aktiv: Haken + +Preview 'Hook collisions' being pressed +== Vorschau für 'Hakenkollisionen' gedrückt diff --git a/data/languages/greek.txt b/data/languages/greek.txt index 77dc974a713..131e9131e17 100644 --- a/data/languages/greek.txt +++ b/data/languages/greek.txt @@ -1693,6 +1693,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show kill messages == diff --git a/data/languages/hungarian.txt b/data/languages/hungarian.txt index df11f05a2f8..b584323917d 100644 --- a/data/languages/hungarian.txt +++ b/data/languages/hungarian.txt @@ -1952,6 +1952,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/italian.txt b/data/languages/italian.txt index e8a3ce85e81..f3db9483a50 100644 --- a/data/languages/italian.txt +++ b/data/languages/italian.txt @@ -1936,6 +1936,9 @@ Width of others' hook collision line A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/japanese.txt b/data/languages/japanese.txt index a1c33cb81f8..092639bfea2 100644 --- a/data/languages/japanese.txt +++ b/data/languages/japanese.txt @@ -1868,6 +1868,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/korean.txt b/data/languages/korean.txt index b6dbb414264..82a1809b780 100644 --- a/data/languages/korean.txt +++ b/data/languages/korean.txt @@ -1964,6 +1964,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/kyrgyz.txt b/data/languages/kyrgyz.txt index 685a9066b78..ea457d86281 100644 --- a/data/languages/kyrgyz.txt +++ b/data/languages/kyrgyz.txt @@ -1684,6 +1684,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show kill messages == diff --git a/data/languages/norwegian.txt b/data/languages/norwegian.txt index 5dc467d0f8d..d0f4cd4237a 100644 --- a/data/languages/norwegian.txt +++ b/data/languages/norwegian.txt @@ -1861,6 +1861,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/persian.txt b/data/languages/persian.txt index 9a6ce8093ac..1ba26366d63 100644 --- a/data/languages/persian.txt +++ b/data/languages/persian.txt @@ -2025,6 +2025,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/polish.txt b/data/languages/polish.txt index c71f7e0050a..e669e128e23 100644 --- a/data/languages/polish.txt +++ b/data/languages/polish.txt @@ -2032,6 +2032,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/portuguese.txt b/data/languages/portuguese.txt index 5a17acd6da2..83919357ae2 100644 --- a/data/languages/portuguese.txt +++ b/data/languages/portuguese.txt @@ -1779,6 +1779,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/romanian.txt b/data/languages/romanian.txt index 081459b1e85..6ff656e6610 100644 --- a/data/languages/romanian.txt +++ b/data/languages/romanian.txt @@ -1699,6 +1699,9 @@ Something hookable A Tee == +Preview 'Hook collisions' being pressed +== + Show kill messages == diff --git a/data/languages/russian.txt b/data/languages/russian.txt index 5cf8dc14ab7..1adb0c44d57 100644 --- a/data/languages/russian.txt +++ b/data/languages/russian.txt @@ -2042,6 +2042,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/serbian.txt b/data/languages/serbian.txt index 9ee98eabccc..b5cd34ad48f 100644 --- a/data/languages/serbian.txt +++ b/data/languages/serbian.txt @@ -1959,6 +1959,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/serbian_cyrillic.txt b/data/languages/serbian_cyrillic.txt index 946408f7730..c265e5230a9 100644 --- a/data/languages/serbian_cyrillic.txt +++ b/data/languages/serbian_cyrillic.txt @@ -1916,6 +1916,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Show finish messages == diff --git a/data/languages/simplified_chinese.txt b/data/languages/simplified_chinese.txt index 0b7caebcb17..edfe6a0f5d8 100644 --- a/data/languages/simplified_chinese.txt +++ b/data/languages/simplified_chinese.txt @@ -2071,6 +2071,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/slovak.txt b/data/languages/slovak.txt index 12cbbf5a321..934620aa21f 100644 --- a/data/languages/slovak.txt +++ b/data/languages/slovak.txt @@ -2027,6 +2027,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/spanish.txt b/data/languages/spanish.txt index 0d439ca553d..ab307ea5821 100644 --- a/data/languages/spanish.txt +++ b/data/languages/spanish.txt @@ -2045,6 +2045,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/swedish.txt b/data/languages/swedish.txt index f565e8c236b..d454acf0242 100644 --- a/data/languages/swedish.txt +++ b/data/languages/swedish.txt @@ -2029,6 +2029,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/traditional_chinese.txt b/data/languages/traditional_chinese.txt index 1deeab44bc3..4dca27b4ed1 100644 --- a/data/languages/traditional_chinese.txt +++ b/data/languages/traditional_chinese.txt @@ -2060,6 +2060,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/turkish.txt b/data/languages/turkish.txt index bddb2e8821f..99035426b82 100644 --- a/data/languages/turkish.txt +++ b/data/languages/turkish.txt @@ -2041,6 +2041,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/data/languages/ukrainian.txt b/data/languages/ukrainian.txt index 9699ef5c7de..10d934dabcc 100644 --- a/data/languages/ukrainian.txt +++ b/data/languages/ukrainian.txt @@ -2026,6 +2026,9 @@ Width of your own hook collision line Width of others' hook collision line == +Preview 'Hook collisions' being pressed +== + Aim == diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 82b280c9a36..b8f8f7823bd 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -3053,7 +3053,7 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) // ***** Preview +hookcoll pressed toggle ***** RightView.HSplitTop(LineSize, &Button, &RightView); - if(DoButton_CheckBox(&s_HookCollPressed, Localize("Preview \"Hook collisions\" being pressed"), s_HookCollPressed, &Button)) + if(DoButton_CheckBox(&s_HookCollPressed, Localize("Preview 'Hook collisions' being pressed"), s_HookCollPressed, &Button)) s_HookCollPressed = !s_HookCollPressed; } else if(s_CurTab == APPEARANCE_TAB_INFO_MESSAGES) From 75f9694e10117ca276b6a3d86b6824ca06a3ee97 Mon Sep 17 00:00:00 2001 From: ASKLL Date: Tue, 3 Dec 2024 00:51:44 +0800 Subject: [PATCH 34/58] Update Simplified & Traditional Chinese translations for upcoming 18.8 --- data/languages/simplified_chinese.txt | 65 +++++++++++++------------- data/languages/traditional_chinese.txt | 65 +++++++++++++------------- 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/data/languages/simplified_chinese.txt b/data/languages/simplified_chinese.txt index 0b7caebcb17..ec6a2ee1c54 100644 --- a/data/languages/simplified_chinese.txt +++ b/data/languages/simplified_chinese.txt @@ -44,6 +44,7 @@ # 2024-08-29 Pioooooo # 2024-09-29 Pioooooo # 2024-11-01 Pioooooo +# 2024-12-03 豆腐渣 & Pioooooo ##### /authors ##### ##### translated strings ##### @@ -1980,102 +1981,102 @@ Some fonts could not be loaded. Check the local console for details. == 未能加载某些字体。检查本地控制台以获取详情。 Online friends (%d) -== +== 在线好友 (%d人) Add friends by entering their name below or by clicking their name in the player list. -== +== 在下方输入好友名称或点击玩家列表中的好友名称即可添加好友 Add clanmates by entering their clan below and leaving the name blank. -== +== 在下方输入战队并留空名称即可添加战队 Offline friends and clanmates will appear here. -== +== 离线好友和战队会显示在这里 Edit touch controls -== +== 编辑触控设置 Close -== +== 关闭 Save changes -== +== 保存更改 Error saving touch controls -== +== 保存触控设置错误 Could not save touch controls to file. See local console for details. -== +== 无法将触控设置保存到文件。检查本地控制台以获取详情。 Unsaved changes -== +== 更改未保存 Discard changes -== +== 放弃更改 Are you sure that you want to discard the current changes to the touch controls? -== +== 确定要放弃当前对触控设置的更改吗? Are you sure that you want to reset the touch controls to default? -== +== 确定要将触控设置重置为默认设置吗? Import from clipboard -== +== 从剪贴板导入 Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== +== 您确定要从剪贴板导入触控设置吗?这将覆盖当前的触控设置。 Export to clipboard -== +== 导出到剪贴板 Direct touch input while ingame -== +== 游戏时直接触摸操作 [Direct touch input] Disabled -== +== 禁用 [Direct touch input] Active action -== +== 当前动作 [Direct touch input] Aim -== +== 瞄准 [Direct touch input] Fire -== +== 开火 [Direct touch input] Hook -== +== 钩索 Direct touch input while spectating -== +== 旁观时直接触摸操作 Error loading touch controls -== +== 加载触控设置错误 Could not load touch controls from file. See local console for details. -== +== 无法从文件加载触控设置。检查本地控制台以获取详情。 Could not load default touch controls from file. See local console for details. -== +== 无法从文件加载默认触控设置。检查本地控制台以获取详情。 Could not load touch controls from clipboard. See local console for details. -== +== 无法从剪贴板加载触控设置。检查本地控制台以获取详情。 Width of your own hook collision line -== +== 自己的钩索辅助线宽度 Width of others' hook collision line -== +== 其他玩家的钩索辅助线宽度 Aim -== +== 瞄准 Active: Fire -== +== 当前:开火 Active: Hook -== +== 当前:钩索 diff --git a/data/languages/traditional_chinese.txt b/data/languages/traditional_chinese.txt index 1deeab44bc3..b159893b5ca 100644 --- a/data/languages/traditional_chinese.txt +++ b/data/languages/traditional_chinese.txt @@ -33,6 +33,7 @@ # 2024-08-29 Pioooooo # 2024-09-29 Pioooooo # 2024-11-01 Pioooooo +# 2024-12-03 豆腐渣 & Pioooooo ##### /authors ##### ##### translated strings ##### @@ -1969,102 +1970,102 @@ Some fonts could not be loaded. Check the local console for details. == 未能載入某些字體。檢查本機控制台以取得詳情。 Online friends (%d) -== +== 在線好友(%d人) Add friends by entering their name below or by clicking their name in the player list. -== +== 在下方輸入好友名稱或點擊玩家列表中的好友的名稱即可添加好友 Add clanmates by entering their clan below and leaving the name blank. -== +== 在下方輸入戰隊名並留空名稱即可添加戰隊 Offline friends and clanmates will appear here. -== +== 離線好友和戰隊會顯示在這裡 Edit touch controls -== +== 編輯觸控設置 Close -== +== 關閉 Save changes -== +== 保存更改 Error saving touch controls -== +== 保存觸控設置錯誤 Could not save touch controls to file. See local console for details. -== +== 無法將觸控設置保存到文件。檢查本地控制台以獲得詳情 Unsaved changes -== +== 更改未保存 Discard changes -== +== 放棄更改 Are you sure that you want to discard the current changes to the touch controls? -== +== 確定要放棄當前對觸控設置的更改嗎? Are you sure that you want to reset the touch controls to default? -== +== 確定要將觸控設置重置為默認設置嗎? Import from clipboard -== +== 從剪貼板導入 Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== +== 確定要從剪貼板導入觸控設置嗎?這將覆蓋當前的觸控設置。 Export to clipboard -== +== 導出到剪貼板 Direct touch input while ingame -== +== 遊戲時直接觸摸操作 [Direct touch input] Disabled -== +== 禁用 [Direct touch input] Active action -== +== 當前行為 [Direct touch input] Aim -== +== 瞄準 [Direct touch input] Fire -== +== 開火 [Direct touch input] Hook -== +== 鉤索 Direct touch input while spectating -== +== 旁觀時直接觸摸操作 Error loading touch controls -== +== 加載觸控設置錯誤 Could not load touch controls from file. See local console for details. -== +== 無法從文件加載觸控設置。查看本地控制台以獲取詳情。 Could not load default touch controls from file. See local console for details. -== +== 無法從文件加載默認觸控設置。查看本地控制台以獲取詳情。 Could not load touch controls from clipboard. See local console for details. -== +== 無法從剪貼板加載觸控設置。查看本地控制台以獲取詳情。 Width of your own hook collision line -== +== 自己的鉤鎖輔助線寬度 Width of others' hook collision line -== +== 其他玩家的鉤鎖輔助線寬度 Aim -== +== 瞄準 Active: Fire -== +== 當前:開火 Active: Hook -== +== 當前:鉤鎖 From 2c4e021d7103b72db31bb89ea216f4c97e964b44 Mon Sep 17 00:00:00 2001 From: Sans3108 Date: Tue, 3 Dec 2024 02:05:33 +0200 Subject: [PATCH 35/58] 100% RO translation (1/2) --- data/languages/romanian.txt | 284 ++++++++++++++++++------------------ 1 file changed, 142 insertions(+), 142 deletions(-) diff --git a/data/languages/romanian.txt b/data/languages/romanian.txt index 6ff656e6610..c84042d3be9 100644 --- a/data/languages/romanian.txt +++ b/data/languages/romanian.txt @@ -17,10 +17,10 @@ == %ds a ieșit %i minute left -== %i minute rămas +== %i minut rămas %i minutes left -== %i minutes rămase +== %i minute rămase %i second left == %i secundă rămasă @@ -47,7 +47,7 @@ All == Toți Are you sure that you want to quit? -== Sigur vrei să ieși? +== Ești sigur că vrei să ieși? Automatically record demos == Înregistrează automat demo-uri @@ -74,10 +74,10 @@ Chat == Chat Clan -== Clanul +== Clan Client -== Clientul +== Client Connecting to == Conectare la @@ -104,13 +104,13 @@ Delete == Șterge Delete demo -== Șterge demonstrația +== Șterge demo-ul Demofile: %s == Fișier demo: %s Demos -== Demo +== Demo-uri Disconnect == Deconectare @@ -128,7 +128,7 @@ Dynamic Camera == Cameră dinamică Emoticon -== Figurine +== Emoticon Error == Eroare @@ -185,7 +185,7 @@ Graphics == Grafică Grenade -== Grenade +== Grenadă Hammer == Ciocan @@ -203,10 +203,10 @@ Invalid Demo == Demo nevalid Join blue -== La albaștri +== Intră la albaștri Join red -== La roșii +== Intră la roșii Jump == Salt @@ -236,7 +236,7 @@ Movement == Mișcare Mute when not active -== Opreşte sunetul la inactivate +== Opreşte sunetul când nu este activ Name == Nume @@ -245,7 +245,7 @@ Next weapon == Arma următoare Nickname -== Pseudonim +== Poreclă No == Nu @@ -257,10 +257,10 @@ No servers found == Nici un server găsit No servers match your filter criteria -== Nici un server nu corespunde criteriilor +== Nici un server nu corespunde criteriilor Ok -== Bine +== Ok Parent Folder == Dosarul părinte @@ -314,13 +314,13 @@ Remove == Șterge Remove friend -== Șterge prietenul +== Șterge prieten Rename demo -== Redenumește demo-ul +== Redenumește demo Reset filter -== Filtru implicit +== Resetează filtrul Score == Scor @@ -329,7 +329,7 @@ Score limit == Limita de scor Scoreboard -== Scoruri +== Tabela de scor Screenshot == Captură de ecran @@ -341,7 +341,7 @@ Server info == Info. server Server not full -== Are locuri libere +== Server-ul nu e plin Shotgun == Pușcă @@ -350,16 +350,16 @@ Show chat == Afișare chat Show friends only -== Arată prietenii +== Arată doar prietenii Show ingame HUD -== Arată scorul în joc +== Afișează HUD în joc Show name plates -== Arată pseudonimele +== Arată poreclele Show only chat messages from friends -== Arată doar chatul prietenilor +== Arată doar mesaje de la prieteni în chat Sound == Sunet @@ -404,7 +404,7 @@ The audio device couldn't be initialised. == Dispozitivul audio nu a putut fi inițializat. The server is running a non-standard tuning on a pure game type. -== Serverul folosește parametri ne-standard într-un joc standard. +== Serverul folosește parametri non-standard într-un joc standard. Time limit == Timp limită @@ -419,13 +419,13 @@ Type == Tip Unable to rename the demo -== Nu pot redenumi demo-ul +== Nu se poate redenumi demo-ul Use sounds -== Folosește sunetul +== Folosește sunete Use team colors for name plates -== Folosește culorile echipelor pe etichetele de nume +== Folosește culorile echipelor pentru etichetele de nume V-Sync == Sincronizare verticală (V-Sync) @@ -466,7 +466,7 @@ New name: == Nume nou: Sat. -== Saturație +== Sat. Miscellaneous == Diverse @@ -484,7 +484,7 @@ Join game == Intră în joc FSAA samples -== Eșantioane FSAA +== Mostre FSAA Sound volume == Volum sunet @@ -493,10 +493,10 @@ Max Screenshots == Număr maxim de capturi de ecran Laser -== Carabină +== Laser Hue -== Tentă +== Nuanță Record demo == Înreg. demo @@ -520,344 +520,344 @@ LAN == Rețea Name plates size -== Dimensiune nume placă +== Dimensiune plăcuțe de nume [Graphics error] Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again. -== +== Eșuat în timpul inițializării. Încercați să schimbați gfx_backend la OpenGL sau Vulkan în settings_ddnet.cfg din dosarul de configurare și încercați din nou. [Graphics error] Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution. -== +== Fără memorie video (VRAM). Încercați să eliminați resursele personalizate (costume, entități etc.), în special cele cu rezoluție mare. [Graphics error] An error during command recording occurred. Try to update your GPU drivers. -== +== A apărut o eroare în timpul înregistrării comenzii. Încercați să vă actualizați driverele plăcii grafice (GPU). [Graphics error] A render command failed. Try to update your GPU drivers. -== +== O comandă de randare a eșuat. Încercați să vă actualizați driverele plăcii grafice (GPU). [Graphics error] Submitting the render commands failed. Try to update your GPU drivers. -== +== Trimiterea comenzilor de randare a eșuat. Încercați să vă actualizați driverele plăcii grafice (GPU). [Graphics error] Failed to swap framebuffers. Try to update your GPU drivers. -== +== A eșuat schimbul tamponelor de cadre. Încercați să vă actualizați driverele plăcii grafice (GPU). [Graphics error] Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again. -== +== Eroare necunoscută. Încercați să schimbați gfx_backend la OpenGL sau Vulkan în settings_ddnet.cfg din dosarul de configurare și încercați din nou. [Graphics error] Could not initialize the given graphics backend, reverting to the default backend now. -== +== Nu s-a putut inițializa backend-ul grafic dat, se revine la backend-ul implicit. [Graphics error] Could not initialize the given graphics backend, this is probably because you didn't install the driver of the integrated graphics card. -== +== Nu s-a putut inițializa backend-ul grafic dat, probabil pentru că nu ați instalat driverul plăcii grafice integrate. Could not resolve connect address '%s'. See local console for details. -== +== Nu s-a putut rezolva adresa de conectare '%s'. Consultați consola pentru detalii. Connect address error -== +== Eroare adresă de conectare Could not save downloaded map. Try manually deleting this file: %s -== +== Nu s-a putut salva harta descărcată. Încercați să ștergeți manual acest fișier: %s Could not connect dummy -== +== Nu s-a putut conecta manechinul Error playing demo -== +== Eroare la redarea demo-ului Successfully saved the replay! -== +== Reluare salvată cu succes! Failed saving the replay! -== +== A eșuat salvarea reluării! Saving settings to '%s' failed -== +== Salvarea setărilor în '%s' a eșuat Error saving settings -== +== Eroare la salvarea setărilor Replay feature is disabled! -== +== Funcția de reluare este dezactivată! The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs. -== +== Lățimea texturii %s nu este divizibilă cu %d, sau înălțimea nu este divizibilă cu %d, ceea ce poate cauza erori vizuale. The format of texture %s is not RGBA which will cause visual bugs. -== +== Formatul texturii %s nu este RGBA, ceea ce va cauza erori vizuale. Preparing demo playback -== +== Se pregătește redarea demo-ului Connected -== +== Conectat Loading map file from storage -== +== Se încarcă fișierul de hartă din stocare Loading demo file from storage -== +== Se încarcă fișierul demo din stocare Some fonts could not be loaded. Check the local console for details. -== +== Unele fonturi nu au putut fi încărcate. Verificați consola pentru detalii. Loading DDNet Client -== +== Se încarcă client-ul DDNet Initializing components -== +== Se inițializează componentele Why are you slowmo replaying to read this? -== +== De ce re-joci în slowmo ca să citești asta? Initializing assets -== +== Se inițializează resursele Initializing map logic -== +== Se inițializează logica hărții Sending initial client info -== +== Se trimit informațiile inițiale despre client Warning -== +== Avertisment Quitting. Please wait… -== +== Ieșire. Așteptați, vă rog… Restarting. Please wait… -== +== Repornire. Așteptați, vă rog… Loading skin files -== +== Se încarcă fișiere costume Search -== +== Caută Searching -== +== Se caută Enter Username -== +== Introduceți numele de utilizator Enter Password -== +== Introduceți parola NOT CONNECTED -== +== NU ESTE CONECTAT Match %d of %d -== +== Corespunde %d din %d No results -== +== Nu există rezultate Lines %d - %d (%s) -== +== Liniile %d - %d (%s) Locked -== +== Blocat Following -== +== Urmărind Loading commands… -== +== Se încarcă comenzile… Debug mode enabled. Press Ctrl+Shift+D to disable debug mode. -== +== Modul de depanare activat. Apăsați Ctrl+Shift+D pentru a dezactiva modul de depanare. Position: -== +== Poziție: Speed: -== +== Viteză: Angle: -== +== Unghi: Multi-View -== +== Multi-vedere [Spectating] Following %s -== +== Urmărind %s Server best: -== +== Record server: Personal best: -== +== Record personal: Team %d -== +== Echipa %d Some map images could not be loaded. Check the local console for details. -== +== Unele imagini ale hărții nu au putut fi încărcate. Verificați consola pentru detalii. Uploading map data to GPU -== +== Se încarcă datele hărții pe placa grafică (GPU) Some map sounds could not be loaded. Check the local console for details. -== +== Unele sunete ale hărții nu au putut fi încărcate. Verificați consola pentru detalii. Loading menu themes -== +== Se încarcă temele meniului Reset -== +== Resetează Press a key… -== +== Apasă o tastă… Settings -== +== Setări Editor -== +== Editor Main menu -== +== Meniul principal Browser -== +== Browser Ghost -== +== Fantomă Reconnect in %d sec -== +== Reconectare în %d sec Rename folder -== +== Redenumește dosarul Render demo -== +== Randează demo Render complete -== +== Randare completă Restart -== +== Repornire Are you sure that you want to restart? -== +== Ești sigur că vrei să repornești? Welcome to DDNet -== +== Bine ai venit pe DDNet DDraceNetwork is a cooperative online game where the goal is for you and your group of tees to reach the finish line of the map. As a newcomer you should start on Novice servers, which host the easiest maps. Consider the ping to choose a server close to you. -== +== DDraceNetwork este un joc online cooperativ în care scopul tău și al grupului tău este să ajungeți la linia de sosire a hărții. Ca începător, ar trebui să începi pe serverele Novice, care găzduiesc cele mai ușoare hărți. Ține cont de ping pentru a alege un server aproape de tine. Use k key to kill (restart), q to pause and watch other players. See settings for other key binds. -== +== Folosește tasta k pentru a te "ucide" (reporni), q pentru a pune pauză și a privi alți jucători. Vezi setările pentru alte taste configurabile. It's recommended that you check the settings to adjust them to your liking before joining a server. -== +== Este recomandat să verifici setările pentru a le ajusta după preferințele tale înainte de a intra pe un server. Please enter your nickname below. -== +== Introdu porecla ta mai jos. Existing Player -== +== Jucător existent Your nickname '%s' is already used (%d points). Do you still want to use it? -== +== Porecla ta '%s' este deja folosită (%d puncte). Vrei totuși să o folosești? Checking for existing player with your name -== +== Se verifică existența unui jucător cu numele tău Save skin -== +== Salvează costumul Are you sure you want to save your skin? If a skin with this name already exists, it will be replaced. -== +== Ești sigur că vrei să salvezi costumul? Dacă un costum cu acest nume există deja, acesta va fi înlocuit. There's an unsaved map in the editor, you might want to save it. -== +== Există o hartă nesalvată în editor, poate vrei să o salvezi. Continue anyway? -== +== Continui oricum? A demo with this name already exists -== +== Un demo cu acest nume există deja A folder with this name already exists -== +== Un dosar cu acest nume există deja Unable to rename the folder -== +== Nu se poate redenumi dosarul File '%s' already exists, do you want to overwrite it? -== +== Fișierul '%s' există deja, vrei să-l suprascrii? Replace video -== +== Înlocuiește videoclipul (paused) -== +== (pauză) Speed -== +== Viteză Video name: -== +== Nume videoclip: Videos directory -== +== Director videoclipuri Video was saved to '%s' -== +== Videoclipul a fost salvat în '%s' Join Tutorial Server -== +== Intră pe serverul de tutorial Skip Tutorial -== +== Sari peste tutorial Show DDNet map finishes in server browser -== +== Afișează finalizările hărților DDNet în browserul de servere transmits your player name to info.ddnet.org -== +== transmite numele tău de jucător către info.ddnet.org Unable to save the skin -== +== Nu se poate salva costumul Unable to save the skin with a reserved name -== +== Nu se poate salva costumul cu un nume rezervat Trying to determine UDP connectivity… -== +== Se încearcă determinarea conectivității UDP… UDP seems to be filtered. -== +== UDP pare să fie filtrat. UDP and TCP IP addresses seem to be different. Try disabling VPN, proxy or network accelerators. -== +== Adresele IP UDP și TCP par să fie diferite. Încercați să dezactivați VPN-ul, proxy-ul sau acceleratoarele de rețea. No answer from server yet. -== +== Încă nu a răspuns serverul. %d/%d KiB (%.1f KiB/s) -== +== %d/%d KiB (%.1f KiB/s) Getting game info -== +== Se obțin informațiile jocului Requesting to join the game -== +== Se solicită intrarea în joc Theme -== +== Temă Loading menu images == From cf277014a6877cecf53b7b2cbeb647c8306b9688 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Mon, 2 Dec 2024 23:47:57 +0800 Subject: [PATCH 36/58] use proper version compatibilty check --- src/engine/shared/protocol.h | 1 + src/game/client/components/controls.cpp | 3 +-- src/game/server/player.cpp | 6 +----- src/game/server/player.h | 2 -- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/engine/shared/protocol.h b/src/engine/shared/protocol.h index 363e35c9412..2608a9297ee 100644 --- a/src/engine/shared/protocol.h +++ b/src/engine/shared/protocol.h @@ -127,6 +127,7 @@ enum VERSION_DDNET_MULTI_LASER = 16040, VERSION_DDNET_ENTITY_NETOBJS = 16200, VERSION_DDNET_REDIRECT = 17020, + VERSION_DDNET_PLAYERFLAG_SPEC_CAM = 18090, }; typedef std::bitset CClientMask; diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp index e9cd545f091..a2a1fc0712c 100644 --- a/src/game/client/components/controls.cpp +++ b/src/game/client/components/controls.cpp @@ -194,8 +194,7 @@ int CControls::SnapInput(int *pData) if(Client()->ServerCapAnyPlayerFlag() && m_pClient->m_Controls.m_aShowHookColl[g_Config.m_ClDummy]) m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_AIM; - // force send spec cam flag on first tick to let server know that it is supported - if(Client()->ServerCapAnyPlayerFlag() && (m_pClient->m_Camera.CamType() == CCamera::CAMTYPE_SPEC || m_LastSendTime == 0)) + if(Client()->ServerCapAnyPlayerFlag() && m_pClient->m_Camera.CamType() == CCamera::CAMTYPE_SPEC) m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_SPEC_CAM; bool Send = m_aLastData[g_Config.m_ClDummy].m_PlayerFlags != m_aInputData[g_Config.m_ClDummy].m_PlayerFlags; diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index 740736d423a..9f280760796 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -146,7 +146,6 @@ void CPlayer::Reset() m_SwapTargetsClientId = -1; m_BirthdayAnnounced = false; m_RescueMode = RESCUEMODE_AUTO; - m_CanUseSpectatingPlayerFlag = false; m_CameraInfo.Reset(); } @@ -530,7 +529,7 @@ void CPlayer::OnDirectInput(CNetObj_PlayerInput *pNewInput) AfkTimer(); - if(((pNewInput->m_PlayerFlags & PLAYERFLAG_SPEC_CAM) || !m_CanUseSpectatingPlayerFlag) && ((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorId == SPEC_FREEVIEW) + if(((pNewInput->m_PlayerFlags & PLAYERFLAG_SPEC_CAM) || GetClientVersion() < VERSION_DDNET_PLAYERFLAG_SPEC_CAM) && ((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorId == SPEC_FREEVIEW) m_ViewPos = vec2(pNewInput->m_TargetX, pNewInput->m_TargetY); // check for activity @@ -549,9 +548,6 @@ void CPlayer::OnPredictedEarlyInput(CNetObj_PlayerInput *pNewInput) { m_PlayerFlags = pNewInput->m_PlayerFlags; - // enable spectating flag feature if the player has ever used it - m_CanUseSpectatingPlayerFlag = m_CanUseSpectatingPlayerFlag || (m_PlayerFlags & PLAYERFLAG_SPEC_CAM); - if(!m_pCharacter && m_Team != TEAM_SPECTATORS && (pNewInput->m_Fire & 1)) m_Spawning = true; diff --git a/src/game/server/player.h b/src/game/server/player.h index 660f2cfa82d..5703a0a765c 100644 --- a/src/game/server/player.h +++ b/src/game/server/player.h @@ -128,8 +128,6 @@ class CPlayer int m_Max; } m_Latency; - bool m_CanUseSpectatingPlayerFlag; - private: const uint32_t m_UniqueClientId; CCharacter *m_pCharacter; From 9e07034716740ab60b395fdd1c1b3b9059262143 Mon Sep 17 00:00:00 2001 From: ASKLL Date: Tue, 3 Dec 2024 18:29:04 +0800 Subject: [PATCH 37/58] Update Simplified Chinese translations for upcoming 18.8 supplement hook collisions preview transl --- data/languages/simplified_chinese.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/languages/simplified_chinese.txt b/data/languages/simplified_chinese.txt index d2167dcf87f..16c439deba5 100644 --- a/data/languages/simplified_chinese.txt +++ b/data/languages/simplified_chinese.txt @@ -2073,7 +2073,7 @@ Width of others' hook collision line == 其他玩家的钩索辅助线宽度 Preview 'Hook collisions' being pressed -== +== 预览按下时的钩索辅助线 Aim == 瞄准 From a54577ba4d0d9bda7cb7056bc526e174c0cc5835 Mon Sep 17 00:00:00 2001 From: ASKLL Date: Tue, 3 Dec 2024 18:30:41 +0800 Subject: [PATCH 38/58] Update Traditional Chinese translations for upcoming 18.8 supplement hook collisions preview transl --- data/languages/traditional_chinese.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/languages/traditional_chinese.txt b/data/languages/traditional_chinese.txt index 2f2150547d7..f1120ebade8 100644 --- a/data/languages/traditional_chinese.txt +++ b/data/languages/traditional_chinese.txt @@ -2062,7 +2062,7 @@ Width of others' hook collision line == 其他玩家的鉤鎖輔助線寬度 Preview 'Hook collisions' being pressed -== +== 預覽按下時的鉤索輔助線 Aim == 瞄準 From a150b857734bce58f4d9930a765a774afcca4899 Mon Sep 17 00:00:00 2001 From: By <130899529+By622@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:57:54 +0800 Subject: [PATCH 39/58] Unite the word used in traditional_chinese.txt --- data/languages/traditional_chinese.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/data/languages/traditional_chinese.txt b/data/languages/traditional_chinese.txt index 2f2150547d7..abe39834b43 100644 --- a/data/languages/traditional_chinese.txt +++ b/data/languages/traditional_chinese.txt @@ -33,7 +33,7 @@ # 2024-08-29 Pioooooo # 2024-09-29 Pioooooo # 2024-11-01 Pioooooo -# 2024-12-03 豆腐渣 & Pioooooo +# 2024-12-03 豆腐渣 & Pioooooo & By ##### /authors ##### ##### translated strings ##### @@ -2009,13 +2009,13 @@ Are you sure that you want to reset the touch controls to default? == 確定要將觸控設置重置為默認設置嗎? Import from clipboard -== 從剪貼板導入 +== 從剪貼簿導入 Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== 確定要從剪貼板導入觸控設置嗎?這將覆蓋當前的觸控設置。 +== 確定要從剪貼簿導入觸控設置嗎?這將覆蓋當前的觸控設置。 Export to clipboard -== 導出到剪貼板 +== 導出到剪貼簿 Direct touch input while ingame == 遊戲時直接觸摸操作 @@ -2047,19 +2047,19 @@ Error loading touch controls == 加載觸控設置錯誤 Could not load touch controls from file. See local console for details. -== 無法從文件加載觸控設置。查看本地控制台以獲取詳情。 +== 無法從文件加載觸控設置。檢查本地控制台以獲取詳情。 Could not load default touch controls from file. See local console for details. -== 無法從文件加載默認觸控設置。查看本地控制台以獲取詳情。 +== 無法從文件加載默認觸控設置。檢查本地控制台以獲取詳情。 Could not load touch controls from clipboard. See local console for details. -== 無法從剪貼板加載觸控設置。查看本地控制台以獲取詳情。 +== 無法從剪貼板加載觸控設置。檢查本地控制台以獲取詳情。 Width of your own hook collision line -== 自己的鉤鎖輔助線寬度 +== 自己的鉤索輔助線寬度 Width of others' hook collision line -== 其他玩家的鉤鎖輔助線寬度 +== 其他玩家的鉤索輔助線寬度 Preview 'Hook collisions' being pressed == From 6f3c4fec4dbc0c66210b3c577a76dccc09cc7f98 Mon Sep 17 00:00:00 2001 From: ASKLL Date: Tue, 3 Dec 2024 19:55:13 +0800 Subject: [PATCH 40/58] pr#9329 supplement --- data/languages/traditional_chinese.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/languages/traditional_chinese.txt b/data/languages/traditional_chinese.txt index 2ddcffe6471..2326a00cd67 100644 --- a/data/languages/traditional_chinese.txt +++ b/data/languages/traditional_chinese.txt @@ -2047,13 +2047,13 @@ Error loading touch controls == 加載觸控設置錯誤 Could not load touch controls from file. See local console for details. -== 無法從文件加載觸控設置。檢查本地控制台以獲取詳情。 +== 無法從文件加載觸控設置。檢查本機控制台以獲取詳情。 Could not load default touch controls from file. See local console for details. -== 無法從文件加載默認觸控設置。檢查本地控制台以獲取詳情。 +== 無法從文件加載默認觸控設置。檢查本機控制台以獲取詳情。 Could not load touch controls from clipboard. See local console for details. -== 無法從剪貼板加載觸控設置。檢查本地控制台以獲取詳情。 +== 無法從剪貼板加載觸控設置。檢查本機控制台以獲取詳情。 Width of your own hook collision line == 自己的鉤索輔助線寬度 From 9c89478f3e88433032bfbba1e1873e1530a2410e Mon Sep 17 00:00:00 2001 From: Rafael Fontenelle Date: Tue, 3 Dec 2024 09:17:48 -0300 Subject: [PATCH 41/58] Update brazilian_portuguese.txt --- data/languages/brazilian_portuguese.txt | 66 ++++++++++++------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/data/languages/brazilian_portuguese.txt b/data/languages/brazilian_portuguese.txt index a7ec8face18..4c737883940 100644 --- a/data/languages/brazilian_portuguese.txt +++ b/data/languages/brazilian_portuguese.txt @@ -1967,105 +1967,105 @@ Some fonts could not be loaded. Check the local console for details. == Algumas fontes não puderam ser carregadas. Verifique o console local para detalhes. Online friends (%d) -== +== Amigos online (%d) Add friends by entering their name below or by clicking their name in the player list. -== +== Adicione amigos digitando o nome deles abaixo ou clicando no nome deles na lista de jogadores. Add clanmates by entering their clan below and leaving the name blank. -== +== Adicione companheiros de clã inserindo o clã deles abaixo e deixando o nome em branco. Offline friends and clanmates will appear here. -== +== Amigos offline e companheiros de clã aparecerão aqui. Edit touch controls -== +== Editar controles de toque Close -== +== Fechar Save changes -== +== Salvar alterações Error saving touch controls -== +== Erro ao salvar controles de toque Could not save touch controls to file. See local console for details. -== +== Não foi possível salvar os controles de toque no arquivo. Veja o console local para detalhes. Unsaved changes -== +== Alterações não salvas Discard changes -== +== Descartar alterações Are you sure that you want to discard the current changes to the touch controls? -== +== Tem certeza de que deseja descartar as alterações atuais nos controles de toque? Are you sure that you want to reset the touch controls to default? -== +== Tem certeza de que deseja redefinir os controles de toque para o padrão? Import from clipboard -== +== Importar da área de transferência Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== +== Tem certeza de que deseja importar os controles de toque da área de transferência? Isso substituirá seus controles de toque atuais. Export to clipboard -== +== Exportar para área de transferência Direct touch input while ingame -== +== Entrada de toque direto durante o jogo [Direct touch input] Disabled -== +== Desabilitada [Direct touch input] Active action -== +== Ação ativa [Direct touch input] Aim -== +== Mira [Direct touch input] Fire -== +== Atirar [Direct touch input] Hook -== +== Gancho Direct touch input while spectating -== +== Entrada de toque direto durante observação Error loading touch controls -== +== Erro ao carregar controles de toque Could not load touch controls from file. See local console for details. -== +== Não foi possível carregar controles de toque do arquivo. Veja o console local para detalhes. Could not load default touch controls from file. See local console for details. -== +== Não foi possível carregar controles de toque padrão do arquivo. Veja o console local para detalhes. Could not load touch controls from clipboard. See local console for details. -== +== Não foi possível carregar controles de toque da área de transferência. Veja o console local para detalhes. Width of your own hook collision line -== +== Largura de sua própria linha de colisão do gancho Width of others' hook collision line -== +== Largura de linha de colisão do gancho dos outros Preview 'Hook collisions' being pressed -== +== Prévia de 'colisões de gancho' sendo pressionada Aim -== +== Mira Active: Fire -== +== Ativo: atirar Active: Hook -== +== Ativo: Gancho From 9684bf2fec7b7d2976fdaa511e3e89032b89bae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Talha=20Ayg=C3=BCn?= Date: Tue, 3 Dec 2024 16:02:22 +0300 Subject: [PATCH 42/58] Update Turkish translation for 18.8 --- data/languages/turkish.txt | 73 +++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/data/languages/turkish.txt b/data/languages/turkish.txt index 99035426b82..f68398e0ebe 100644 --- a/data/languages/turkish.txt +++ b/data/languages/turkish.txt @@ -20,6 +20,7 @@ # Gokturk 2024-09-29 19:23:00 # Gokturk 2024-09-29 19:23:00 # Gokturk 2024-11-04 11:32:00 +# Gokturk 2024-12-03 16:00:00 ##### /authors ##### ##### translated strings ##### @@ -1871,7 +1872,7 @@ https://wiki.ddnet.org/wiki/Mapping == https://wiki.ddnet.org/wiki/Mapping/tr Could not resolve connect address '%s'. See local console for details. -== '%s' Bağlantı adresi çözümlenemedi. Detaylar için yerel konsola bakın +== '%s' bağlantı adresi çözümlenemedi. Detaylar için yerel konsola bakın. Connect address error == Bağlantı adresi hatası @@ -1913,7 +1914,7 @@ Unable to save the skin == Skin kaydedilemiyor Unable to save the skin with a reserved name -== Skin kaydedilemiyor, bu isimde zaten bir skin var. +== Skin kaydedilemiyor bu isimde zaten bir skin var No local servers found (ports %d-%d) == Yerel sunucu bulunamadı (bağlantı noktası %d-%d) @@ -1947,108 +1948,108 @@ Eyes == Göz Some fonts could not be loaded. Check the local console for details. -== Bazı fontlar yüklenemiyor. Detaylar için ana konsolu kontrol edin +== Bazı fontlar yüklenemiyor. Detaylar için konsolu kontrol edin. Online friends (%d) -== +== Çevrim içi arkadaşlar (%d) Add friends by entering their name below or by clicking their name in the player list. -== +== Aşağıya isim girerek veya oyuncu listesinden isme tıklayarak arkadaş ekle. Add clanmates by entering their clan below and leaving the name blank. -== +== Klan arkadaşlarınızı klanlarını aşağıya girerek ve isimlerini boş bırakarak ekleyin. Offline friends and clanmates will appear here. -== +== Çevrim dışı arkadaşlar ve klan arkadaşları burada görünecek. Edit touch controls -== +== Dokunmatik ayarlarını düzelt Close -== +== Kapat Save changes -== +== Değişiklikleri kaydet Error saving touch controls -== +== Dokunmatik ayarları kaydedilirken bir hata oldu Could not save touch controls to file. See local console for details. -== +== Dokunmatik ayarları dosyalara kaydedilemedi. Detaylar için yerel konsola bakın. Unsaved changes -== +== Kaydedilmemiş değişiklikler Discard changes -== +== Değişiklikleri iptal et Are you sure that you want to discard the current changes to the touch controls? -== +== Dokunmatik ayarlarındaki değişiklikleri iptal etmek istediğinden emin misin? Are you sure that you want to reset the touch controls to default? -== +== Dokunmatik ayarlarını varsayılan ayarlarına döndürmek istediğinden emin misin? Import from clipboard -== +== Panodan içeri aktar Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== +== Dokunmatik ayarlarını panodan içeri aktarmak istediğine emin misin? Bu şu anki kontrollerin üzerine yazılacak. Export to clipboard -== +== Panodan dışarı aktar Direct touch input while ingame -== +== Oyun içindeyken doğrudan dokunmatik giriş [Direct touch input] Disabled -== +== Devre dışı [Direct touch input] Active action -== +== Aktif aksiyon [Direct touch input] Aim -== +== Aim [Direct touch input] Fire -== +== Ateş [Direct touch input] Hook -== +== Kanca Direct touch input while spectating -== +== Gezinme modundayken dokunarak doğrudan giriş Error loading touch controls -== +== Dokunmatik ayarları yüklenirken bir hata oluştu Could not load touch controls from file. See local console for details. -== +== Dokunmatik ayarları dosyalardan yüklenemedi. Detaylar için konsola bakın. Could not load default touch controls from file. See local console for details. -== +== Varsayılan dokunmatik ayarları dosyalardan yüklenemedi. Detaylar için konsola bakın. Could not load touch controls from clipboard. See local console for details. -== +== Dokunmatik ayarları panodan yüklenemedi. Detaylar için konsola bakın. Width of your own hook collision line -== +== Kendi kancanın çarpışma çizgisi kalınlığı Width of others' hook collision line -== +== Başkalarının kancalarının çarpışma çizgisi kalınlığı Preview 'Hook collisions' being pressed -== +== Önizleme 'Kanca çarpışmaları' gösteriliyor Aim -== +== Aim Active: Fire -== +== Aktif: Ateş etme Active: Hook -== +== Aktif: Kanca From 9b4e2d17d4276ddad1457aea62bfc3b38dbff36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Talha=20Ayg=C3=BCn?= Date: Tue, 3 Dec 2024 16:36:18 +0300 Subject: [PATCH 43/58] Update Azerbaijani translation for 18.8 --- data/languages/azerbaijani.txt | 69 +++++++++++++++++----------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/data/languages/azerbaijani.txt b/data/languages/azerbaijani.txt index 37854268c69..72fe1c79549 100644 --- a/data/languages/azerbaijani.txt +++ b/data/languages/azerbaijani.txt @@ -7,6 +7,7 @@ # Gokturk 2024-08-29 23:39:00 # Gokturk 2024-09-29 19:24:00 # Gokturk 2024-11-04 11:35:00 +# Gokturk & seishiroon 2024-12-03 16:35:00 ##### /authors ##### ##### translated strings ##### @@ -1933,108 +1934,108 @@ Eyes == Göz Some fonts could not be loaded. Check the local console for details. -== Bəzi yazı fontları yüklənə bilmir. Ətraflı məlumat üçün əsas konsolu yoxlayın +== Bəzi yazı fontları yüklənə bilmir. Ətraflı məlumat üçün əsas konsolu yoxlayın. Online friends (%d) -== +== Onlayn dostlar (%d) Add friends by entering their name below or by clicking their name in the player list. -== +== Aşağıya ad yazaraq və ya oyunçu siyahısından adlarına tıklayaraq dost əlavə edin. Add clanmates by entering their clan below and leaving the name blank. -== +== Klan yoldaşlarınızı klan adını aşağıya yazaraq və ad hissəsini boş buraxaraq əlavə edin. Offline friends and clanmates will appear here. -== +== Oflayn dostlar və klan yoldaşları burada görünəcək. Edit touch controls -== +== Toxunuş idarəetmələrini düzəlt Close -== +== Bağla Save changes -== +== Dəyişiklikləri yadda saxla Error saving touch controls -== +== Toxunuş idarəetmələri saxlanılarkən xəta baş verdi Could not save touch controls to file. See local console for details. -== +== Toxunuş idarəetmələrini fayllara yadda saxlamaq mümkün olmadı. Ətraflı məlumat üçün konsola baxın. Unsaved changes -== +== Yadda saxlanılmamış dəyişikliklər Discard changes -== +== Dəyişiklikləri ləğv et Are you sure that you want to discard the current changes to the touch controls? -== +== Toxunuş idarəetmələrindəki dəyişiklikləri ləğv etmək istədiyinizə əminsinizmi? Are you sure that you want to reset the touch controls to default? -== +== oxunuş idarəetmələrini varsayılan parametrlərə qaytarmaq istədiyinizə əminsinizmi? Import from clipboard -== +== Panodan içəri idxal et Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== +== Toxunuş idarəetmələrini panodan idxal etmək istədiyinizə əminsinizmi? Bu, mövcud idarəetmələrin üzərinə yazılacaq. Export to clipboard -== +== Panoya ixrac et Direct touch input while ingame -== +== Oyun içində olarkən birbaşa toxunuş girişi [Direct touch input] Disabled -== +== Deaktiv edilib [Direct touch input] Active action -== +== Aktiv hərəkət [Direct touch input] Aim -== +== Aim [Direct touch input] Fire -== +== Atəş [Direct touch input] Hook -== +== Qarmaq Direct touch input while spectating -== +== İzləmə rejimində olarkən birbaşa toxunuş girişi Error loading touch controls -== +== Toxunuş idarəetmələri yüklənərkən xəta baş verdi Could not load touch controls from file. See local console for details. -== +== Toxunuş idarəetmələrini fayllardan yükləmək mümkün olmadı. Ətraflı məlumat üçün konsola baxın. Could not load default touch controls from file. See local console for details. -== +== Varsayılan toxunuş idarəetmələrini fayllardan yükləmək mümkün olmadı. Ətraflı məlumat üçün konsola baxın. Could not load touch controls from clipboard. See local console for details. -== +== Toxunuş idarəetmələrini panodan yükləmək mümkün olmadı. Ətraflı məlumat üçün konsola baxın. Width of your own hook collision line -== +== Öz qarmağınızın toqquşma xəttinin qalınlığı Width of others' hook collision line -== +== Başqalarının qarmaqlarının toqquşma xəttinin qalınlığı Preview 'Hook collisions' being pressed -== +== ‘Qarmaq toqquşmaları’ basılarkən önizləmə Aim -== +== Aim Active: Fire -== +== Aktiv: Atəş Active: Hook -== +== Aktiv: Qarmaq From b08f1e251c2347170ad7e4420296ebb4283dcb69 Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Tue, 3 Dec 2024 11:19:55 +0100 Subject: [PATCH 44/58] android build: Allow specifying parallelism via BUILD_FLAGS --- scripts/android/cmake_android.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/android/cmake_android.sh b/scripts/android/cmake_android.sh index e577307fe6b..9c3797bb179 100755 --- a/scripts/android/cmake_android.sh +++ b/scripts/android/cmake_android.sh @@ -3,8 +3,8 @@ set -e # $HOME must be used instead of ~ else cargo-ndk cannot find the folder export ANDROID_HOME=$HOME/Android/Sdk -MAKEFLAGS=-j$(nproc) -export MAKEFLAGS +BUILD_FLAGS="${BUILD_FLAGS:--j$(nproc)}" +export BUILD_FLAGS ANDROID_NDK_VERSION="$(cd "$ANDROID_HOME/ndk" && find . -maxdepth 1 | sort -n | tail -1)" ANDROID_NDK_VERSION="${ANDROID_NDK_VERSION:2}" @@ -151,7 +151,9 @@ function build_for_type() { -DVIDEORECORDER=OFF ( cd "${BUILD_FOLDER}/$ANDROID_SUB_BUILD_DIR/$1" || exit 1 - cmake --build . --target game-client + # We want word splitting + # shellcheck disable=SC2086 + cmake --build . --target game-client $BUILD_FLAGS ) } From 8f4f9041918899b5cb03ec06b9528e65c96bf62e Mon Sep 17 00:00:00 2001 From: Sans3108 Date: Wed, 4 Dec 2024 03:08:11 +0200 Subject: [PATCH 45/58] 100% RO translation (2/2) --- data/languages/romanian.txt | 791 ++++++++++++++++++------------------ 1 file changed, 396 insertions(+), 395 deletions(-) diff --git a/data/languages/romanian.txt b/data/languages/romanian.txt index c84042d3be9..610cffe8388 100644 --- a/data/languages/romanian.txt +++ b/data/languages/romanian.txt @@ -9,12 +9,13 @@ # kneekoo 2011-07-01 18:40:49 # kneekoo 2011-07-05 23:34:34 # kneekoo 2012-05-01 02:01:47 +# Sans 2024-12-04 02:19:00 ##### /authors ##### ##### translated strings ##### %ds left -== %ds a ieșit +== %ds rămase %i minute left == %i minut rămas @@ -158,7 +159,7 @@ Free-View == Vizualizare liberă Fullscreen -== Ecrat complet +== Ecran complet Game == Joc @@ -185,7 +186,7 @@ Graphics == Grafică Grenade -== Grenadă +== Lansator de grenade Hammer == Ciocan @@ -257,7 +258,7 @@ No servers found == Nici un server găsit No servers match your filter criteria -== Nici un server nu corespunde criteriilor +== Nici un server nu corespunde criteriilor Ok == Ok @@ -344,7 +345,7 @@ Server not full == Server-ul nu e plin Shotgun -== Pușcă +== Pușcă de vânatoare Show chat == Afișare chat @@ -353,7 +354,7 @@ Show friends only == Arată doar prietenii Show ingame HUD -== Afișează HUD în joc +== Afișează interfața din joc Show name plates == Arată poreclele @@ -812,7 +813,7 @@ Video name: == Nume videoclip: Videos directory -== Director videoclipuri +== Dosar videoclipuri Video was saved to '%s' == Videoclipul a fost salvat în '%s' @@ -860,1186 +861,1186 @@ Theme == Temă Loading menu images -== +== Se încarcă imaginile meniului AFR -== +== AFR ASI -== +== ASI AUS -== +== AUS EUR -== +== EUR NA -== +== NA SA -== +== SA CHN -== +== CHN Getting server list from master server -== +== Se obține lista serverelor de la serverul principal No local servers found (ports %d-%d) -== +== Nu s-au găsit servere locale (porturi %d-%d) Example of usage -== +== Exemplu de utilizare Exclude -== +== Exclude %d of %d servers -== +== %d din %d servere %d of %d server -== +== %d din %d server %d players -== +== %d jucători %d player -== +== %d jucător Are you sure that you want to disconnect and switch to a different server? -== +== Ești sigur că vrei să te deconectezi și să treci la un alt server? No login required -== +== Nu este necesar să te autentifici Filter connecting players -== +== Filtrează jucătorii care se conectează Indicate map finish -== +== Indică finalizarea hărții Unfinished map -== +== Hartă neterminată Countries -== +== Țări Types -== +== Tipuri Communities -== +== Comunități Copy info -== +== Copiază informațiile Leak IP -== +== Expune IP-ul No server selected -== +== Niciun server selectat Online friends (%d) -== +== Prieteni online (%d) Online clanmates (%d) -== +== Membri de clan online (%d) [friends (server browser)] Offline (%d) -== +== Offline (%d) Click to select server. Double click to join your friend. -== +== Fă clic pentru a selecta serverul. Dublu clic pentru a te alătura prietenului tău. Click to remove this player from your friends list. -== +== Fă clic pentru a elimina acest jucător din lista de prieteni. Click to remove this clan from your friends list. -== +== Fă clic pentru a elimina acest clan din lista de prieteni. Add friends by entering their name below or by clicking their name in the player list. -== +== Adaugă prieteni introducând numele lor mai jos sau făcând clic pe numele lor în lista de jucători. Add clanmates by entering their clan below and leaving the name blank. -== +== Adaugă membri de clan introducând numele clanului mai jos și lăsând numele gol. Offline friends and clanmates will appear here. -== +== Prietenii și membrii de clan offline vor apărea aici. Are you sure that you want to remove the player '%s' from your friends list? -== +== Ești sigur că vrei să elimini jucătorul '%s' din lista de prieteni? Are you sure that you want to remove the clan '%s' from your friends list? -== +== Ești sigur că vrei să elimini clanul '%s' din lista de prieteni? Add Clan -== +== Adaugă Clan Server filter -== +== Filtru server Friends -== +== Prieteni Play the current demo -== +== Redă demo-ul curent Pause the current demo -== +== Pune pe pauză demo-ul curent Stop the current demo -== +== Oprește demo-ul curent Go back the specified duration -== +== Mergi înapoi cu durata specificată [Demo player duration] %d min. -== +== %d min. [Demo player duration] %d sec. -== +== %d sec. Change the skip duration -== +== Schimbă durata de sărire Go forward the specified duration -== +== Mergi înainte cu durata specificată Go back one tick -== +== Mergi înapoi un pas Go forward one tick -== +== Mergi înainte un pas Go back one marker -== +== Mergi înapoi un marcaj Go forward one marker -== +== Mergi înainte un marcaj Slow down the demo -== +== Încetinește demo-ul Speed up the demo -== +== Accelerează demo-ul Mark the beginning of a cut (right click to reset) -== +== Marchează începutul unei secțiuni (clic dreapta pentru resetare) Mark the end of a cut (right click to reset) -== +== Marchează sfârșitul unei secțiuni (clic dreapta pentru resetare) Export cut as a separate demo -== +== Exportă secțiunea ca un demo separat Close the demo player -== +== Închide player-ul demo Toggle keyboard shortcuts -== +== Comutează scurtăturile de tastatură Export demo cut -== +== Exportă secțiunea demo Cut interval -== +== Interval de tăiere Cut length -== +== Lungime secțiune Remove chat -== +== Elimină chatul Render cut to video -== +== Randează secțiunea în video Please use a different filename -== +== Folosește un alt nume de fișier File already exists, do you want to overwrite it? -== +== Fișierul există deja, vrei să-l suprascrii? Loading demo files -== +== Se încarcă fișierele demo All combined -== +== Toate combinate Demo -== +== Demo Length -== +== Lungime Date -== +== Dată No demo selected -== +== Niciun demo selectat Folder Link -== +== Link folder Created -== +== Creat Markers -== +== Marcaje Netversion -== +== Versiune rețea Size -== +== Dimensiune [Demo details] map not included -== +== harta nu este inclusă %.2f MiB -== +== %.2f MiB %.2f KiB -== +== %.2f KiB Fetch Info -== +== Preia informații Demos directory -== +== Dosar demo-uri Open the directory that contains the demo files -== +== Deschide dosarul care conține fișierele demo Are you sure that you want to delete the folder '%s'? -== +== Ești sigur că vrei să ștergi folderul '%s'? Are you sure that you want to delete the demo '%s'? -== +== Ești sigur că vrei să ștergi demo-ul '%s'? Delete folder -== +== Șterge folderul Unable to delete the demo '%s' -== +== Nu se poate șterge demo-ul '%s' Unable to delete the folder '%s'. Make sure it's empty first. -== +== Nu se poate șterge folderul '%s'. Asigură-te mai întâi că este gol. Are you sure that you want to disconnect? -== +== Ești sigur că vrei să te deconectezi? Connect Dummy -== +== Conectează manechinul Dummy is not allowed on this server -== +== Manechinul nu este permis pe acest server Please wait… -== +== Te rog așteaptă… Connecting dummy -== +== Se conectează manechinul Disconnect Dummy -== +== Deconectează manechinul Are you sure that you want to disconnect your dummy? -== +== Ești sigur că vrei să deconectezi manechinul? Kill -== +== Ucide Pause -== +== Pauză Edit touch controls -== +== Editează controalele tactile Close -== +== Închide Save changes -== +== Salvează modificările Error saving touch controls -== +== Eroare la salvarea controalelor tactile Could not save touch controls to file. See local console for details. -== +== Nu s-au putut salva controalele tactile în fișier. Verifică consola locală pentru detalii. Unsaved changes -== +== Modificări nesalvate Discard changes -== +== Renunță la modificări Are you sure that you want to discard the current changes to the touch controls? -== +== Ești sigur că vrei să renunți la modificările curente ale controalelor tactile? Are you sure that you want to reset the touch controls to default? -== +== Ești sigur că vrei să resetezi controalele tactile la valorile implicite? Import from clipboard -== +== Importă din clipboard Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== +== Ești sigur că vrei să imporți controalele tactile din clipboard? Acest lucru va suprascrie setările actuale. Export to clipboard -== +== Exportă în clipboard Direct touch input while ingame -== +== Intrare tactilă directă în timpul jocului [Direct touch input] Disabled -== +== Dezactivat [Direct touch input] Active action -== +== Acțiune activă [Direct touch input] Aim -== +== Țintește [Direct touch input] Fire -== +== Trage [Direct touch input] Hook -== +== Cârlig Direct touch input while spectating -== +== Intrare tactilă directă în timpul vizionării Error loading touch controls -== +== Eroare la încărcarea controalelor tactile Could not load touch controls from file. See local console for details. -== +== Nu s-au putut încărca controalele tactile din fișier. Verifică consola locală pentru detalii. Could not load default touch controls from file. See local console for details. -== +== Nu s-au putut încărca controalele tactile implicite din fișier. Verifică consola locală pentru detalii. Could not load touch controls from clipboard. See local console for details. -== +== Nu s-au putut încărca controalele tactile din clipboard. Verifică consola locală pentru detalii. Loading… -== +== Se încarcă… Loading ghost files -== +== Se încarcă fișierele fantomă Time -== +== Timp Ghosts directory -== +== Dosar fantome Activate all -== +== Activează toate Deactivate all -== +== Dezactivează toate Deactivate -== +== Dezactivează Activate -== +== Activează Save -== +== Salvează Menu opened. Press Esc key again to close menu. -== +== Meniul a fost deschis. Apasă tasta Esc din nou pentru a închide meniul. Smooth Dynamic Camera -== +== Cameră dinamică lină Switch weapon when out of ammo -== +== Schimbă arma când rămâi fără muniție Skip the main menu -== +== Sari peste meniul principal [Hertz] Hz -== +== Hz Refresh Rate -== +== Rata de refresh Save power by lowering refresh rate (higher input latency) -== +== Economisește energie prin reducerea ratei de refresh (latență mai mare la intrări) Settings file -== +== Fișier de setări Open the settings file -== +== Deschide fișierul de setări Config directory -== +== Dosarul de configurare Open the directory that contains the configuration and user files -== +== Deschide dosarul care conține fișierele de configurare și utilizator Themes directory -== +== Dosar teme Open the directory to add custom themes -== +== Deschide dosarul pentru a adăuga teme personalizate Automatically take statboard screenshot -== +== Fă automat captură de ecran pentru tabela de statistici Automatically create statboard csv -== +== Creează automat fișier CSV pentru tabela de statistici Max CSVs -== +== Număr maxim de fișiere CSV Dummy -== +== Manechin Player info change cooldown -== +== Timp de așteptare pentru schimbarea informațiilor jucătorului Download skins -== +== Descarcă costume Download community skins -== +== Descarcă costume comunitare Vanilla skins only -== +== Doar costume de bază (Vanilla) Fat skins (DDFat) -== +== Costume mari (DDFat) Skin prefix -== +== Prefix costum Create a random skin -== +== Creează un costum aleatoriu Choose default eyes when joining a server -== +== Alege ochii implicați la conectarea pe un server Skin Database -== +== Baza de date a costumelor Skins directory -== +== Dosar costume Open the directory to add custom skins -== +== Deschide dosarul pentru a adăuga costume personalizate Hook collisions -== +== Coliziuni cârlig Zoom in -== +== Mărește Zoom out -== +== Micșorează Default zoom -== +== Zoom implicit Show others -== +== Arată alții Show all -== +== Arată toți Toggle dyncam -== +== Comutează camera dinamică Toggle ghost -== +== Comutează fantoma Converse -== +== Conversație Chat command -== +== Comandă chat Toggle dummy -== +== Comutează manechinul Dummy copy -== +== Copiere manechin Hammerfly dummy -== +== Manechin hammerfly Statboard -== +== Tabela de statistici Lock team -== +== Blochează echipa Show entities -== +== Arată entitățile Show HUD -== +== Arată interfața Enable controller -== +== Activează controlerul Controller -== +== Controler Ingame controller mode -== +== Mod controler în joc [Ingame controller mode] Relative -== +== Relativ [Ingame controller mode] Absolute -== +== Absolut Ingame controller sens. -== +== Sensibilitate controler în joc UI controller sens. -== +== Sensibilitate controler interfață Controller jitter tolerance -== +== Toleranță la zgomot controler No controller found. Plug in a controller. -== +== Nu s-a găsit niciun controler. Conectează un controler. Axis -== +== Axă Status -== +== Status Aim bind -== +== Legare țintire Mouse -== +== Mouse Ingame mouse sens. -== +== Sensibilitate mouse în joc UI mouse sens. -== +== Sensibilitate mouse interfață Reset controls -== +== Resetează controalele Are you sure that you want to reset the controls to their defaults? -== +== Ești sigur că vrei să resetezi controalele la valorile implicite? Cancel -== +== Anulează Windowed -== +== Fereastră Windowed borderless -== +== Fereastră fără margini Windowed fullscreen -== +== Fereastră pe tot ecranul Desktop fullscreen -== +== Ecran complet desktop Screen -== +== Ecran may cause delay -== +== poate cauza întârzieri Allows maps to render with more detail -== +== Permite redarea hărților cu mai multe detalii Use high DPI -== +== Folosește DPI înalt Renderer -== +== Motor grafic default -== +== implicit custom -== +== personalizat Graphics card -== +== Placă grafică auto -== +== automat Enable game sounds -== +== Activează sunetele jocului Enable gun sound -== +== Activează sunetul armelor Enable long pain sound (used when shooting in freeze) -== +== Activează sunetul lung de durere (utilizat când tragi în timp ce ești înghețat) Enable server message sound -== +== Activează sunetul mesajelor serverului Enable regular chat sound -== +== Activează sunetul chat-ului obișnuit Enable team chat sound -== +== Activează sunetul chat-ului de echipă Enable highlighted chat sound -== +== Activează sunetul pentru chat-ul evidențiat Game sound volume -== +== Volumul sunetului jocului Chat sound volume -== +== Volumul sunetului din chat Map sound volume -== +== Volumul sunetelor hărții Background music volume -== +== Volumul muzicii de fundal Tee -== +== Tee Appearance -== +== Apariție DDNet -== +== DDNet Assets -== +== Resurse DDNet Client needs to be restarted to complete update! -== +== Clientul DDNet trebuie repornit pentru a finaliza actualizarea! HUD -== +== Interfață Name Plate -== +== Etichetă de nume Hook Collisions -== +== Coliziuni cârlig Info Messages -== +== Mesaje informative Show health, shields and ammo -== +== Arată viața, scuturile și muniția Show score -== +== Arată scorul Show local time always -== +== Arată ora locală mereu Show votes window after voting -== +== Arată fereastra de voturi după votare DDRace HUD -== +== Interfață DDRace Show client IDs (scoreboard, chat, spectator) -== +== Arată ID-urile de client (tabelă, chat, spectator) Show DDRace HUD -== +== Arată interfața DDRace Show jumps indicator -== +== Arată indicatorul săriturilor Show dummy actions -== +== Arată acțiunile manechinului Show player position -== +== Arată poziția jucătorului Show player speed -== +== Arată viteza jucătorului Show player target angle -== +== Arată unghiul țintei jucătorului Show freeze bars -== +== Arată barele de îngheț Opacity of freeze bars inside freeze -== +== Opacitatea barelor de îngheț în stare de îngheț Always show chat -== +== Arată mereu chat-ul Show names in chat in team colors -== +== Arată numele în chat în culorile echipei Show only chat messages from team members -== +== Arată doar mesajele din chat de la membrii echipei Use old chat style -== +== Folosește stilul vechi de chat Chat font size -== +== Dimensiunea fontului în chat Chat width -== +== Lățimea chat-ului Messages -== +== Mesaje System message -== +== Mesaj de sistem Highlighted message -== +== Mesaj evidențiat Team message -== +== Mesaj de echipă Friend message -== +== Mesaj de prieten Normal message -== +== Mesaj normal Client message -== +== Mesaj client Preview -== +== Previzualizare Show clan above name plates -== +== Arată clanul deasupra etichetelor de nume Clan plates size -== +== Dimensiunea etichetelor de clan Show friend mark (♥) in name plates -== +== Arată semnul prietenilor (♥) pe etichetele de nume Show hook strength icon indicator -== +== Arată indicatorul de putere al cârligului Show hook strength number indicator -== +== Arată indicatorul numeric de putere al cârligului Show other players' key presses -== +== Arată tastele apăsate de alți jucători Show local player's key presses -== +== Arată tastele apăsate de jucătorul local Authed name color in scoreboard -== +== Culoarea numelui autentificat în tabela de scor Same clan color in scoreboard -== +== Culoarea comună a clanului în tabela de scor Hook collision line -== +== Linie de coliziune cârlig Show own player's hook collision line -== +== Arată linia de coliziune a cârligului propriu Always show own player's hook collision line -== +== Arată mereu linia de coliziune a cârligului propriu Show other players' hook collision lines -== +== Arată liniile de coliziune ale cârligelor altor jucători Always show other players' hook collision lines -== +== Arată mereu liniile de coliziune ale cârligelor altor jucători Width of your own hook collision line -== +== Lățimea liniei de coliziune a cârligului propriu Width of others' hook collision line -== +== Lățimea liniei de coliziune a cârligelor altor jucători Hook collision line opacity -== +== Opacitatea liniei de coliziune a cârligului Colors of the hook collision line, in case of a possible collision with: -== +== Culorile liniei de coliziune a cârligului, în cazul unei posibile coliziuni cu: Your movements are not taken into account when calculating the line colors -== +== Mișcările tale nu sunt luate în considerare la calcularea culorilor liniei Nothing hookable -== +== Nimic agățabil Something hookable -== +== Ceva agățabil A Tee -== +== Un Tee Preview 'Hook collisions' being pressed -== +== Previzualizare 'Coliziuni cârlig' în timpul apăsării Show kill messages -== +== Arată mesajele de ucidere Show finish messages -== +== Arată mesajele de finalizare Normal Color -== +== Culoare normală Highlight Color -== +== Culoare evidențiată Weapons -== +== Arme Rifle Laser Outline Color -== +== Culoarea conturului puștii Rifle Laser Inner Color -== +== Culoarea interioară a puștii Shotgun Laser Outline Color -== +== Culoarea conturului laserului puștii de vânătoare Shotgun Laser Inner Color -== +== Culoarea interioară a laserului puștii de vânătoare Entities -== +== Entități Door Laser Outline Color -== +== Culoarea conturului laserului ușii Door Laser Inner Color -== +== Culoarea interioară a laserului ușii Freeze Laser Outline Color -== +== Culoarea conturului laserului de îngheț Freeze Laser Inner Color -== +== Culoarea interioară a laserului de îngheț Set all to Rifle -== +== Setează toate pe Pușcă Save the best demo of each race -== +== Salvează cel mai bun demo al fiecărei curse Enable replays -== +== Activează reluările Default length -== +== Lungime implicită Enable ghost -== +== Activează fantoma When you cross the start line, show a ghost tee replicating the movements of your best time -== +== Când treci linia de start, arată un Tee fantomă care reproduce mișcările celui mai bun timp al tău Show ghost -== +== Arată fantoma Opacity -== +== Opacitate Save ghost -== +== Salvează fantoma Only save improvements -== +== Salvează doar îmbunătățirile Gameplay -== +== Gameplay Overlay entities -== +== Suprapune entitățile Show text entities -== +== Arată entitățile text Adjust the opacity of entities belonging to other teams, such as tees and nameplates -== +== Ajustează opacitatea entităților aparținând altor echipe, cum ar fi Tee-urile și etichetele de nume Show others (own team only) -== +== Arată alții (doar echipa proprie) Show quads -== +== Arată quad-urile Quads are used for background decoration -== +== Quad-urile sunt utilizate pentru decorarea fundalului AntiPing -== +== AntiPing Tries to predict other entities to give a feel of low latency -== +== Încearcă să prezică alte entități pentru a oferi senzația de latență scăzută AntiPing: predict other players -== +== AntiPing: prezice alți jucători AntiPing: predict weapons -== +== AntiPing: prezice armele AntiPing: predict grenade paths -== +== AntiPing: prezice traiectoria grenadelor Background -== +== Fundal Regular background color -== +== Culoare de fundal obișnuită Entities background color -== +== Culoarea de fundal a entităților Use current map as background -== +== Folosește harta curentă ca fundal Show tiles layers from BG map -== +== Arată straturile de țigle din harta de fundal New random timeout code -== +== Cod nou de timeout aleatoriu Run on join -== +== Rulează la conectare Chat command (e.g. showall 1) -== +== Comandă chat (ex: showall 1) Unregister protocol and file extensions -== +== Deregistrează protocolul și extensiile fișierelor DDNet %s is available: -== +== DDNet %s este disponibil: Update now -== +== Actualizează acum Updating… -== +== Se actualizează… DDNet Client updated! -== +== Clientul DDNet a fost actualizat! No updates available -== +== Nu există actualizări disponibile Check now -== +== Verifică acum Basic -== +== Bazic Custom -== +== Personalizat Are you sure that you want to delete '%s'? -== +== Ești sigur că vrei să ștergi '%s'? Delete skin -== +== Șterge costumul Unable to delete skin -== +== Nu se poate șterge costumul Emoticons -== +== Emoticoane Particles -== +== Particule Extras -== +== Extra Loading assets -== +== Se încarcă resursele Assets directory -== +== Dosar resurse Open the directory to add custom assets -== +== Deschide dosarul pentru a adăuga resurse personalizate Discord -== +== Discord https://ddnet.org/discord -== +== https://ddnet.org/discord Learn -== +== Învață https://wiki.ddnet.org/ -== +== https://wiki.ddnet.org/ Tutorial -== +== Tutorial Can't find a Tutorial server -== +== Nu se poate găsi un server de Tutorial Website -== +== Website Stop server -== +== Oprește server Run server -== +== Rulează server Server executable not found, can't run server -== +== Executabilul serverului nu a fost găsit, nu se poate rula serverul [Start menu] Play -== +== Joacă DDNet %s is out! -== +== DDNet %s a fost lansat! Downloading %s: -== +== Descarcă %s: Update failed! Check log… -== +== Actualizarea a eșuat! Verifică jurnalul… Loading race demo files -== +== Se încarcă fișierele demo de cursă Round %d/%d -== +== Runda %d/%d [Spectators] %d others… -== +== %d alți spectatori… Super -== +== Super [Team and size] %d\n(%d/%d) -== +== %d\n(%d/%d) Team %d (%d/%d) -== +== Echipa %d (%d/%d) Manual -== +== Manual Race -== +== Cursă Auto -== +== Automat Replay -== +== Repetă [skins] Body -== +== Corp [skins] Marking -== +== Marcaj [skins] Decoration -== +== Decorație [skins] Hands -== +== Mâini [skins] Feet -== +== Picioare [skins] Eyes -== +== Ochii Loading sound files -== +== Se încarcă fișierele audio Follow -== +== Urmărește Frags -== +== Frags Deaths -== +== Decese Suicides -== +== Sinucideri Ratio -== +== Rata Net -== +== Rețea FPM -== +== FPM Spree -== +== Serii de ucideri Best -== +== Cel mai bun Grabs -== +== Prinderi Aim -== +== Țintă Active: Fire -== +== Activ: Foc Active: Hook -== +== Activ: Cârlig 1 new mention -== +== 1 mențiune nouă %d new mentions -== +== %d mențiuni noi 9+ new mentions -== +== 9+ mențiuni noi Moved ingame -== +== Mutat în joc https://wiki.ddnet.org/wiki/Mapping -== +== https://wiki.ddnet.org/wiki/Mapping From 0bcd8b2c0ffca55ddeb3f60f06bf380fa2275a75 Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Wed, 4 Dec 2024 09:22:15 +0800 Subject: [PATCH 46/58] don't allow SpectateClosest to select self --- src/game/client/components/spectator.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp index 6c7f0b81c34..8240361c9e8 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -642,6 +642,9 @@ void CSpectator::SpectateClosest() if(i == SpectatorId || !Snap.m_aCharacters[i].m_Active || !Snap.m_apPlayerInfos[i] || Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS) continue; + if(Client()->State() != IClient::STATE_DEMOPLAYBACK && i == Snap.m_LocalClientId) + continue; + const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[i].m_Cur; int Distance = distance(CurPosition, vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y)); if(NewSpectatorId == -1 || Distance < ClosestDistance) From 1a0a5a4c2204a797ed7e9c710b18a4b1576734fb Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Wed, 4 Dec 2024 10:53:27 +0800 Subject: [PATCH 47/58] allow invalid commands too if unknown commands are allowed --- src/game/editor/editor_server_settings.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/game/editor/editor_server_settings.cpp b/src/game/editor/editor_server_settings.cpp index d91f41ed078..02678474cd5 100644 --- a/src/game/editor/editor_server_settings.cpp +++ b/src/game/editor/editor_server_settings.cpp @@ -325,7 +325,7 @@ void CEditor::DoMapSettingsEditBox(CMapSettingsBackend::CContext *pContext, cons ToolBar.VSplitRight(ToolBar.h, &ToolBar, &Button); // Do the unknown command toggle button - if(DoButton_FontIcon(&Context.m_AllowUnknownCommands, FONT_ICON_QUESTION, Context.m_AllowUnknownCommands, &Button, 0, "Disallow/allow unknown commands", IGraphics::CORNER_R)) + if(DoButton_FontIcon(&Context.m_AllowUnknownCommands, FONT_ICON_QUESTION, Context.m_AllowUnknownCommands, &Button, 0, "Disallow/allow unknown or invalid commands", IGraphics::CORNER_R)) { Context.m_AllowUnknownCommands = !Context.m_AllowUnknownCommands; Context.Update(); @@ -1393,7 +1393,7 @@ void CMapSettingsBackend::CContext::ParseArgs(const char *pLineInputStr, const c // Validate argument from the parsed argument of the current setting. // If current setting is not valid, then there are no arguments which results in an error. - char Type = 'u'; // u = unknown, only possible for unknown commands when m_AllowUnknownCommands is true. + char Type = 'u'; // u = unknown if(ArgIndex < CommandArgCount) { SParsedMapSettingArg &Arg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(ArgIndex); @@ -1479,10 +1479,6 @@ void CMapSettingsBackend::CContext::ParseArgs(const char *pLineInputStr, const c NewArg.m_Error = Error != SCommandParseError::ERROR_NONE || Length == 0 || m_Error.m_Type != SCommandParseError::ERROR_NONE; NewArg.m_ExpectedType = Type; - // Do not emit an error if we allow unknown commands and the current setting is invalid - if(m_AllowUnknownCommands && m_pCurrentSetting == nullptr) - NewArg.m_Error = false; - // Check error and fill the error field with different messages if(Error == SCommandParseError::ERROR_INVALID_VALUE || Error == SCommandParseError::ERROR_UNKNOWN_VALUE || Error == SCommandParseError::ERROR_OUT_OF_RANGE || Error == SCommandParseError::ERROR_INCOMPLETE) { @@ -1524,7 +1520,7 @@ void CMapSettingsBackend::CContext::ParseArgs(const char *pLineInputStr, const c m_Error.m_ArgIndex = ArgIndex; break; } - else if(!m_AllowUnknownCommands) + else { char aFormattedValue[256]; FormatDisplayValue(m_aCommand, aFormattedValue); @@ -1699,7 +1695,7 @@ void CMapSettingsBackend::CContext::UpdatePossibleMatches() } // If there are no matches, then the command is unknown - if(m_vPossibleMatches.empty() && !m_AllowUnknownCommands) + if(m_vPossibleMatches.empty()) { // Fill the error if we do not allow unknown commands char aFormattedValue[256]; @@ -1827,7 +1823,7 @@ void CMapSettingsBackend::CContext::ColorArguments(std::vector if(m_pLineInput && !m_pLineInput->IsEmpty()) { - if(!CommandIsValid() && !m_AllowUnknownCommands && m_CommentOffset != 0) + if(!CommandIsValid() && m_CommentOffset != 0) { // If command is invalid, override color splits with red, but not comment int ErrorLength = m_CommentOffset == -1 ? -1 : m_CommentOffset; @@ -2047,6 +2043,10 @@ bool CMapSettingsBackend::CContext::Valid() const { // Check if the entire setting is valid or not + // We don't need to check whether a command is valid if we allow unknown commands + if(m_AllowUnknownCommands) + return true; + if(m_CommentOffset == 0 || m_aCommand[0] == '\0') return true; // A "comment" setting is considered valid. @@ -2066,9 +2066,7 @@ bool CMapSettingsBackend::CContext::Valid() const } else { - // If we have an invalid setting, then we consider the entire setting as valid if we allow unknown commands - // as we cannot handle them. - return m_AllowUnknownCommands; + return false; } } From 254debc11f5d13fab53f25c690778eb3a68b3310 Mon Sep 17 00:00:00 2001 From: furo Date: Wed, 4 Dec 2024 17:43:43 +0100 Subject: [PATCH 48/58] Update Swedish translations for 18.8 --- data/languages/swedish.txt | 68 +++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/data/languages/swedish.txt b/data/languages/swedish.txt index d454acf0242..214ea656892 100644 --- a/data/languages/swedish.txt +++ b/data/languages/swedish.txt @@ -7,7 +7,7 @@ # 3edcxzaq1 2020-06-25 00:00:00 # cur.ie 2020-09-28 00:00:00 # simpygirl 2022-02-20 00:00:00 -# furo 2024-11-01 00:00:00 +# furo 2024-12-04 00:00:00 ##### /authors ##### ##### translated strings ##### @@ -1938,105 +1938,105 @@ Some fonts could not be loaded. Check the local console for details. == Vissa typsnitt kunde inte laddas in. Se den lokala konsolen för detaljer. Online friends (%d) -== +== Online kompisar (%d) Add friends by entering their name below or by clicking their name in the player list. -== +== Lägg till en kompis genom att skriva in deras namn nedanför eller genom att klicka på deras namn i spellistan. Add clanmates by entering their clan below and leaving the name blank. -== +== Lägg till en klanmedlem genom att skriva in deras klan nedanför och lämna namn fältet blankt. Offline friends and clanmates will appear here. -== +== Offline kompisar och klanmedlemmar kommer att synas här. Edit touch controls -== +== Ändra pekskärms kontrollerna Close -== +== Stäng Save changes -== +== Spara ändringar Error saving touch controls -== +== Misslyckades med att spara pekskärms kontrollerna Could not save touch controls to file. See local console for details. -== +== Kunde inte spara pekskärms kontrollerna till en fil. Se den lokala konsolen för detaljer. Unsaved changes -== +== Osparade ändringar Discard changes -== +== Kasta ändringar Are you sure that you want to discard the current changes to the touch controls? -== +== Är du säker att du vill kasta de nuvarande ändringar till pekskärms kontrollerna? Are you sure that you want to reset the touch controls to default? -== +== Är du säker på att du vill nollställa pekskärms kontrollerna till standardinställningarna? Import from clipboard -== +== Importera från urklipp Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== +== Är du säker att du vill importera pekskärms kontrollerna från urklipp? Detta kommer att skriva över dina nuvarande inställningar. Export to clipboard -== +== Exportera till urklipp Direct touch input while ingame -== +== Direkt pekskärms kontroller medan du är i spel [Direct touch input] Disabled -== +== Inaktiverad [Direct touch input] Active action -== +== Aktivt läge [Direct touch input] Aim -== +== Sikta [Direct touch input] Fire -== +== Skjut [Direct touch input] Hook -== +== Hook Direct touch input while spectating -== +== Direkt pekskärms kontroller medan du är i åskådarläge Error loading touch controls -== +== Misslyckades att ladda in pekskärms kontrollerna Could not load touch controls from file. See local console for details. -== +== Misslyckades att ladda in pekskärms kontrollerna från fil. Se den lokala konsolen för detaljer. Could not load default touch controls from file. See local console for details. -== +== Misslyckades att ladda in standard pekskärms kontrollerna från fil. Se den lokala konsolen för detaljer. Could not load touch controls from clipboard. See local console for details. -== +== Misslyckades att ladda in pekskärms kontrollerna från urkipp. Se den lokala konsolen för detaljer. Width of your own hook collision line -== +== Bredd av din egna hook kollisions linje Width of others' hook collision line -== +== Bredd av andras hook kollisions linjer Preview 'Hook collisions' being pressed -== +== Förhandsvisning när 'Hook kollisions' är aktivt Aim -== +== Sikta Active: Fire -== +== Läge: Skjut Active: Hook -== +== Läge: Hook From 4dc2d35491ba6c91740f00c2e1e84c1379bb70b8 Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Wed, 4 Dec 2024 17:31:17 +0100 Subject: [PATCH 49/58] French translation by Yubel --- data/languages/french.txt | 191 +++++++++++++++++++------------------- 1 file changed, 96 insertions(+), 95 deletions(-) diff --git a/data/languages/french.txt b/data/languages/french.txt index f08074e470a..71602767201 100644 --- a/data/languages/french.txt +++ b/data/languages/french.txt @@ -27,6 +27,7 @@ # Nouaa 2022-10-25 18:00:00 # Sukya 2023-05-20 19:38:00 # Emilcha 2024-06-09 11:31:00 +# Yubel 2024-12-04 16:17:00 ##### /authors ##### @@ -66,7 +67,7 @@ All == Tout le monde Are you sure that you want to quit? -== Êtes-vous sûrs de vouloir quitter ? +== Êtes-vous sûr de vouloir quitter ? Automatically record demos == Enregistrer les démos automatiquement @@ -153,7 +154,7 @@ Error == Erreur Error loading demo -== Erreur pendant le chargement de la démo +== Erreur lors du chargement de la démo Favorite == Favori @@ -605,7 +606,7 @@ AntiPing: predict other players == AntiPing: prédit le déplacement des autres joueurs Are you sure that you want to disconnect your dummy? -== Êtes vous sûrs de vouloir déconnecter votre dummy ? +== Êtes-vous sûr de vouloir déconnecter votre dummy ? Server best: == Meilleur score du serveur @@ -647,7 +648,7 @@ Show text entities == Afficher le texte des entités Connecting dummy -== Connection du dummy +== Connexion du dummy Render demo == Convertir une démo @@ -683,13 +684,13 @@ System message == Message du système Are you sure that you want to disconnect? -== Êtes-vous sûrs de vouloir vous déconnecter ? +== Êtes-vous sûr de vouloir vous déconnecter ? Deactivate == Désactiver Update failed! Check log… -== La mise à jour a échoué ! Vérifier les logs… +== La mise à jour a échouée ! Vérifiez les logs… %.2f KiB == %.2f Kio @@ -734,7 +735,7 @@ Show only chat messages from friends == Ne montrer que les messages des amis Remove chat -== Desactiver le chat +== Désactiver le chat Pause == Pause @@ -764,7 +765,7 @@ Exclude == Exclure Disconnect Dummy -== Deconnecter le dummy +== Déconnecter le dummy Show clan above name plates == Afficher le clan au dessus du pseudonyme @@ -1140,7 +1141,7 @@ https://ddnet.org/discord == https://ddnet.org/discord Are you sure that you want to disconnect and switch to a different server? -== Êtes vous sur de vouloir vous déconnecter et changer de serveur? +== Êtes-vous sûr de vouloir vous déconnecter et changer de serveur ? Show local player's key presses == Montrer les touches appuyées des autres joueurs @@ -1158,7 +1159,7 @@ Run on join == Exécuter en rejoignant Chat command (e.g. showall 1) -== Commande du chat (ex: showall 1) +== Commande du chat (ex : showall 1) The format of texture %s is not RGBA which will cause visual bugs. == La texture %s n'est pas au format RGBA, ce qui causera des bugs visuels. @@ -1188,13 +1189,13 @@ auto == Automatique When you cross the start line, show a ghost tee replicating the movements of your best time -== Lorsque vous franchissez la ligne de départ, montrez un fantôme reproduisant les mouvements de votre meilleur temps +== Lorsque vous franchissez la ligne de départ, montre un fantôme reproduisant les mouvements de votre meilleur temps Opacity == Opacité Adjust the opacity of entities belonging to other teams, such as tees and nameplates -== Ajustez l'opacité des entités appartenant à d'autres équipes, telles que les tees et les noms des joueurs +== Ajustez l'opacité des entités appartenant à d'autres équipes, telle que les tees et les noms des joueurs Quads are used for background decoration == Les quads sont utilisés pour la décoration de l'arrière-plan @@ -1215,16 +1216,16 @@ Team %d == Équipe %d Position: -== Position: +== Position : Speed: -== Vitesse: +== Vitesse : Angle: -== Angle: +== Angle : Trying to determine UDP connectivity… -== Tentative de connection UDP… +== Tentative de connexion UDP… UDP seems to be filtered. == UDP semble filtré. @@ -1435,11 +1436,11 @@ Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in s [Graphics error] Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution. -== Mémoire graphique insuffisante. Essayez de retirer des ressources personnalisées (skins, entités, etc.), spécialement celles de haute résolution. +== Mémoire graphique insuffisante. Essayez de retirer des ressources personnalisées (skins, entités, etc.), en particulier celles de haute résolution. [Graphics error] An error during command recording occurred. Try to update your GPU drivers. -== Une erreur pendant l'enregistrement des commandes est survenue. Essayez de mettre à jour vos pilotes GPU. +== Une erreur est survenue pendant l'enregistrement des commandes. Essayez de mettre à jour vos pilotes GPU. [Graphics error] A render command failed. Try to update your GPU drivers. @@ -1468,10 +1469,10 @@ File '%s' already exists, do you want to overwrite it? == Le fichier '%s' existe déjà, voulez-vous l'écraser ? Are you sure that you want to remove the player '%s' from your friends list? -== Êtes-vous sûrs de vouloir retirer le joueur '%s' de votre liste d'amis ? +== Êtes-vous sûr de vouloir retirer le joueur '%s' de votre liste d'amis ? Are you sure that you want to remove the clan '%s' from your friends list? -== Êtes-vous sûrs de vouloir retirer le clan '%s' de votre liste d'amis ? +== Êtes-vous sûr de vouloir retirer le clan '%s' de votre liste d'amis ? Go back one tick == Reculer d'une image @@ -1489,7 +1490,7 @@ Open the directory that contains the demo files == Ouvrir le dossier contenant les fichiers démo Are you sure that you want to delete the demo '%s'? -== Êtes-vous sûrs de vouloir supprimer la démo '%s' ? +== Êtes-vous sûr de vouloir supprimer la démo '%s' ? Unable to delete the demo '%s' == Impossible de supprimer la démo '%s' @@ -1501,7 +1502,7 @@ Open the settings file == Ouvrir le fichier de configuration Open the directory that contains the configuration and user files -== Ouvrir le dossier de configuration et des fichiers personnalisés +== Ouvrir le dossier contenant les fichiers de configuration et d'utilisateurs Open the directory to add custom themes == Ouvrir le dossier pour ajouter un thème personnalisé @@ -1516,7 +1517,7 @@ Reset controls == Réinitialiser les contrôles Are you sure that you want to reset the controls to their defaults? -== Êtes-vous sûrs de vouloir réinitialiser les contrôles aux valeurs par défaut ? +== Êtes-vous sûr de vouloir réinitialiser les contrôles aux valeurs par défaut ? Rifle Laser Outline Color == Couleur fusil laser (contour) @@ -1553,10 +1554,10 @@ Open the directory to add custom assets [Graphics error] Could not initialize the given graphics backend, this is probably because you didn't install the driver of the integrated graphics card. -== Impossible d'initialiser l'interface graphique, tu n'as probablement pas installé les pilotes de ta carte graphique. +== Impossible d'initialiser l'interface graphique, vous n'avez probablement pas installé les pilotes de votre carte graphique. Could not save downloaded map. Try manually deleting this file: %s -== Impossible de sauvegarder la carte téléchargé. Essayez de supprimer ce fichier manuellement: %s +== Impossible de sauvegarder la carte téléchargée. Essayez de supprimer ce fichier manuellement: %s Copy info == Copier les infos @@ -1578,7 +1579,7 @@ Click to remove this clan from your friends list. == Cliquez pour retirer ce clan de votre liste d'amis Add Clan -== Ajouter clan +== Ajouter un clan Create a random skin == Créer un skin aléatoire @@ -1590,7 +1591,7 @@ Failed saving the replay! == Erreur durant la sauvegarde du replay ! Saving settings to '%s' failed -== La sauvegarde des paramètres vers '%s' à échoué +== La sauvegarde des paramètres vers '%s' a échouée Error saving settings == Erreur de sauvegarde des paramètres @@ -1632,7 +1633,7 @@ Following == Suivi Loading commands… -== Chargement des commandes +== Chargement des commandes… Multi-View == Multi-vues @@ -1668,7 +1669,7 @@ Continue anyway? == Continuer quand même ? A demo with this name already exists -== Une demo avec ce nom existe déjà +== Une démo avec ce nom existe déjà A folder with this name already exists == Un dossier avec ce nom existe déjà @@ -1779,10 +1780,10 @@ Ghosts directory == Dossier des fantômes Activate all -== Tous activer +== Tout activer Deactivate all -== Tous désactiver +== Tout désactiver Player info change cooldown == Délai de changement des informations du joueur @@ -1857,208 +1858,208 @@ Moved ingame == Déplacé en jeu Could not resolve connect address '%s'. See local console for details. -== +== Impossible de résoudre l'adresse de connexion '%s'. Consultez la console locale pour plus de détails. Connect address error -== +== Erreur d'adresse de connexion Could not connect dummy -== +== Impossible de connecter le dummy Some fonts could not be loaded. Check the local console for details. -== +== Certaines polices n'ont pas pu être chargées. Consultez la console locale pour plus de détails. [Spectating] Following %s -== +== Suivi de %s Save skin -== +== Enregistrer le skin Are you sure you want to save your skin? If a skin with this name already exists, it will be replaced. -== +== Êtes-vous sûr de vouloir enregistrer votre skin ? Si un skin déjà existant possède ce nom, il sera remplacé. Unable to save the skin -== +== Impossible d'enregistrer le skin Unable to save the skin with a reserved name -== +== Impossible d'enregistrer le skin avec un nom réservé No local servers found (ports %d-%d) -== +== Aucun serveur local trouvé (ports %d-%d) Example of usage -== +== Exemple d'utilisation Online friends (%d) -== +== Amis connectés (%d) Add friends by entering their name below or by clicking their name in the player list. -== +== Ajoutez des amis en entrant leurs noms ci-dessous ou en cliquant sur leurs noms dans la liste des joueurs. Add clanmates by entering their clan below and leaving the name blank. -== +== Ajoutez des membres du clan en entrant le nom de clan ci-dessous et en laissant le nom de joueur vide. Offline friends and clanmates will appear here. -== +== Les amis hors-lignes et les membres du clan apparaîtront ici. Dummy is not allowed on this server -== +== Le dummy n'est pas autorisé sur ce serveur Please wait… -== +== Veuillez patienter… Edit touch controls -== +== Modifier les contrôles tactiles Close -== +== Fermer Save changes -== +== Enregistrer les modifications Error saving touch controls -== +== Erreur lors de l'enregistrement des contrôles tactiles Could not save touch controls to file. See local console for details. -== +== Impossible d'enregistrer les contrôles tactiles dans le fichier. Consultez la console locale pour plus de détails. Unsaved changes -== +== Modifications non-enregistrées Discard changes -== +== Annuler les modifications Are you sure that you want to discard the current changes to the touch controls? -== +== Êtes-vous sûr de vouloir annuler les modifications actuelles des contrôles tactiles ? Are you sure that you want to reset the touch controls to default? -== +== Êtes-vous sûr de vouloir réinitialiser les contrôles tactiles aux valeurs par défaut ? Import from clipboard -== +== Importer depuis le presse-papiers Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== +== Êtes-vous sûr de vouloir importer les contrôles tactiles depuis le presse-papiers ? Cela va remplacer les contrôles tactiles actuels. Export to clipboard -== +== Exporter au presse-papiers Direct touch input while ingame -== +== Entrée tactile directe en jeu [Direct touch input] Disabled -== +== Désactivée [Direct touch input] Active action -== +== Action active [Direct touch input] Aim -== +== Viser [Direct touch input] Fire -== +== Tirer [Direct touch input] Hook -== +== Grappin Direct touch input while spectating -== +== Entrée tactile directe en mode spec. Error loading touch controls -== +== Erreur lors du chargement des contrôles tactiles Could not load touch controls from file. See local console for details. -== +== Impossible de charger les contrôles tactiles depuis le fichier. Consultez la console locale pour plus de détails. Could not load default touch controls from file. See local console for details. -== +== Impossible de charger les contrôles tactiles par défaut depuis le fichier. Consultez la console locale pour plus de détails. Could not load touch controls from clipboard. See local console for details. -== +== Impossible de charger les contrôles tactiles depuis le presse-papiers. Consultez la console locale pour plus de détails. [Hertz] Hz -== +== Hz Show client IDs (scoreboard, chat, spectator) -== +== Afficher les IDs des clients (tableau des scores, chat, spectateur) Width of your own hook collision line -== +== Largeur de la ligne de collision de votre grappin Width of others' hook collision line -== +== Largeur de la ligne de collision du grappin des autres joueurs Preview 'Hook collisions' being pressed -== +== Prévisualiser lorsque 'Collision du grappin' est utilisée Basic -== +== Basique Custom -== +== Personnalisé Are you sure that you want to delete '%s'? -== +== Êtes-vous sûr de vouloir supprimer '%s' ? Delete skin -== +== Supprimer le skin Unable to delete skin -== +== Impossible de supprimer le skin Round %d/%d -== +== Manche %d/%d [Spectators] %d others… -== +== %d autres… [Team and size] %d\n(%d/%d) -== +== %d\n(%d/%d) Team %d (%d/%d) -== +== Équipe %d (%d/%d) [skins] Body -== +== Corps [skins] Marking -== +== Marquage [skins] Decoration -== +== Décoration [skins] Hands -== +== Mains [skins] Feet -== +== Pieds [skins] Eyes -== +== Yeux Aim -== +== Viser Active: Fire -== +== Actif : Tirer Active: Hook -== +== Actif : Grappin https://wiki.ddnet.org/wiki/Mapping -== +== https://wiki.ddnet.org/wiki/Mapping From 1cb72e5d553081da87b13f7a8f6111bda8f61977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Tue, 3 Dec 2024 22:13:21 +0100 Subject: [PATCH 50/58] Support building Android version on Windows with MSYS2 Support user-defined `ANDROID_HOME` variable in the `cmake_android.sh` script to specify different location for the Android SDK installation. See https://developer.android.com/tools/variables#envar for details. Make generation of data integrity index file in the `cmake_android.sh` script faster by factor >100 in MSYS2 by avoiding the loop over piped output. Locally prepend `/usr/bin/` to the `PATH` variable in the `cmake_android.sh` script to ensure that the MSYS2 binaries are used instead of Windows-native tools with the same names which work differently like `find` and `sort`. Update ddnet-libs reference to include the Android libs (latest ddnet-libs master) which cannot currently be build on Windows. Update README with steps for building the Android version on Windows with MSYS2. --- ddnet-libs | 2 +- scripts/android/README.md | 66 ++++++++++++++++++++++++++++---- scripts/android/cmake_android.sh | 25 ++++++------ 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/ddnet-libs b/ddnet-libs index e78f350e789..7f9799bc0a4 160000 --- a/ddnet-libs +++ b/ddnet-libs @@ -1 +1 @@ -Subproject commit e78f350e7898fc6b0702cdc3b7ce6ee347c97d49 +Subproject commit 7f9799bc0a4d4c21a91e9659799f5ac6076d4367 diff --git a/scripts/android/README.md b/scripts/android/README.md index a34bdc315a9..7bd362affaa 100644 --- a/scripts/android/README.md +++ b/scripts/android/README.md @@ -1,9 +1,8 @@ -Requirements for building for Android -===================================== +Requirements for building for Android on Linux +============================================== - At least 10-15 GiB of free disk space. - First follow the general instructions for setting up https://github.com/ddnet/ddnet for building on Linux. - This guide has only been tested on Linux. - Note: Use a stable version of Rust. Using the nightly version results in linking errors. - Install the Android NDK (version 26) in the same location where Android Studio would unpack it (`~/Android/Sdk/ndk/`): @@ -55,10 +54,6 @@ Requirements for building for Android ```shell sudo apt install openjdk-21-jdk ``` -- Install 7zip for building `ddnet-libs`: - ```shell - sudo apt install p7zip-full - ``` - Install ninja: ```shell sudo apt install ninja-build @@ -72,11 +67,57 @@ Requirements for building for Android brew install coreutils ``` - Build the `ddnet-libs` for Android (see below). Follow all above steps first. + Alternatively, use the precompiled libraries from https://github.com/ddnet/ddnet-libs/. + +Requirements for building for Android on Windows using MSYS2 +============================================================ +- At least 50 GiB of free disk space if you start from scratch. +- First install MSYS2 (https://www.msys2.org/wiki/MSYS2-installation/) as well as all required packages for building DDNet using MSYS2 on Windows. + (There is currently no more detailed guide for this.) +- Install cargo-ndk and add Android targets to rustup to build Rust with the Android NDK: + ```shell + cargo install cargo-ndk + rustup target add armv7-linux-androideabi + rustup target add i686-linux-android + rustup target add aarch64-linux-android + rustup target add x86_64-linux-android + ``` +- Install JDK 21, e.g. from https://adoptium.net/temurin/releases/?package=jdk&os=windows&version=21 +- Install ninja: + ```shell + pacman -S mingw-w64-x86_64-ninja + ``` +- Install curl: + ```shell + pacman -S mingw-w64-x86_64-curl + ``` +- Install coreutils so `nproc` is available: + ```shell + pacman -S coreutils + ``` +- Compiling the libraries is not supported on Windows yet. Use the precompiled libraries from https://github.com/ddnet/ddnet-libs/, + i.e. make sure to also clone the ddnet-libs submodule, or compile the libraries on a separate Linux system. +- Set the `ANDROID_HOME` environment variable to override the location where the Android SDK will be installed, e.g. `C:/Android/SDK`. Make sure to only use forward slashes. +- Install either Android Studio (which includes an SDK manager GUI) from https://developer.android.com/studio or the standalone command-line tools (which include the `sdkmanager` tool) from https://developer.android.com/studio/#command-line-tools-only. +- When using the command-line tools: Ensure the command-line tools are installed at the expected location, so `%ANDROID_HOME%/cmdline-tools/latest/bin` should contain `sdkmanager.bat`. +Accept the licenses using the SDK manager, otherwise the Gradle build will fail if the licenses have not been accepted: + ```shell + yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager.bat --licenses + ``` +- Install the following using the SDK Manager in Android Studio (Tools menu) or the `sdkmanager` command-line tool: + - SDK Platform for API Level 34 + - NDK (Side by side) + - Android SDK Build-Tools (latest version) How to build the `ddnet-libs` for Android ========================================= +- Note: This has only been tested on Linux. +- Install 7-Zip: + ```shell + sudo apt install p7zip-full + ``` - There is a script to automatically download and build all repositories, this requires an active internet connection and can take around 30 minutes: ```shell @@ -94,10 +135,10 @@ How to build the `ddnet-libs` for Android cp -r build-android-libs/ddnet-libs/. ddnet-libs/ ``` - How to build the DDNet client for Android ========================================= +- These steps are identical on Linux and Windows, except on Windows `bash` must be used as terminal and not `cmd.exe` or PowerShell. - Open a terminal inside the `ddnet` project root directory and run the following: ```shell scripts/android/cmake_android.sh @@ -144,3 +185,12 @@ How to build the DDNet client for Android - Note that you should only generate a signing key once (and make backups). Users can only update apps automatically if the same package name and signing key have been used, else they must manually uninstall the old app. + +Common problems and solutions +============================= + +- If the Gradle build fails with errors messages indicating bugs relating to files in the Gradle cache, try to clear the Gradle cache by deleting the contents of the folder `~/.gradle/caches` (`%USERPROFILE%/.gradle/caches` on Windows). +- The Gradle build may show a message that the JDK version could not be determined but this can safely be ignored. +- The Gradle build will fail with errors messages indicating an unsupported class file version if a different version of the JDK is used than specified in `build.gradle`. + When incrementing the supported JDK version, the Gradle version also has to be incremented according to https://docs.gradle.org/current/userguide/compatibility.html. + If you have multiple JDKs installed, you can set the JDK version for Gradle using the property `org.gradle.java.home` in the `gradle.properties` file in your Gradle home directory. diff --git a/scripts/android/cmake_android.sh b/scripts/android/cmake_android.sh index 9c3797bb179..2e91565bc41 100755 --- a/scripts/android/cmake_android.sh +++ b/scripts/android/cmake_android.sh @@ -1,8 +1,15 @@ #!/bin/bash set -e -# $HOME must be used instead of ~ else cargo-ndk cannot find the folder -export ANDROID_HOME=$HOME/Android/Sdk +# Ensure that binaries from MSYS2 are preferred over Windows-native commands like find and sort which work differently. +PATH="/usr/bin/:$PATH" + +# $ANDROID_HOME can be used-defined, else the default location is used. Important notes: +# - The path must not contain spaces on Windows. +# - $HOME must be used instead of ~ else cargo-ndk cannot find the folder. +ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}" +export ANDROID_HOME + BUILD_FLAGS="${BUILD_FLAGS:--j$(nproc)}" export BUILD_FLAGS @@ -123,8 +130,6 @@ fi export TW_VERSION_NAME=$ANDROID_VERSION_NAME -printf "${COLOR_CYAN}%s${COLOR_RESET}\n" "Building cmake..." - function build_for_type() { cmake \ -H. \ @@ -160,18 +165,22 @@ function build_for_type() { mkdir -p "${BUILD_FOLDER}" if [[ "${ANDROID_BUILD}" == "arm" || "${ANDROID_BUILD}" == "all" ]]; then + printf "${COLOR_CYAN}%s${COLOR_RESET}\n" "Building cmake (arm)..." build_for_type arm armeabi-v7a armv7-linux-androideabi fi if [[ "${ANDROID_BUILD}" == "arm64" || "${ANDROID_BUILD}" == "all" ]]; then + printf "${COLOR_CYAN}%s${COLOR_RESET}\n" "Building cmake (arm64)..." build_for_type arm64 arm64-v8a aarch64-linux-android fi if [[ "${ANDROID_BUILD}" == "x86" || "${ANDROID_BUILD}" == "all" ]]; then + printf "${COLOR_CYAN}%s${COLOR_RESET}\n" "Building cmake (x86)..." build_for_type x86 x86 i686-linux-android fi if [[ "${ANDROID_BUILD}" == "x86_64" || "${ANDROID_BUILD}" == "all" ]]; then + printf "${COLOR_CYAN}%s${COLOR_RESET}\n" "Building cmake (x86_64)..." build_for_type x86_64 x86_64 x86_64-linux-android fi @@ -241,14 +250,8 @@ cp ./cacert.pem ./assets/asset_integrity_files/data/cacert.pem || exit 1 printf "${COLOR_CYAN}%s${COLOR_RESET}\n" "Creating integrity index file..." ( cd assets/asset_integrity_files || exit 1 - tmpfile="$(mktemp /tmp/hash_strings.XXX)" - - find data -iname "*" -type f -print0 | while IFS= read -r -d $'\0' file; do - sha_hash=$(sha256sum "$file" | cut -d' ' -f 1) - echo "$file $sha_hash" >> "$tmpfile" - done - + find data -iname "*" -type f -print0 | xargs -0 sha256sum | awk '{gsub(/^\*/, "", $2); print substr($0, index($0, $2)), $1}' > "$tmpfile" full_hash="$(sha256sum "$tmpfile" | cut -d' ' -f 1)" rm -f "integrity.txt" From 853b641a9f73920552017c09fe52f957a21c2906 Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Thu, 5 Dec 2024 09:51:23 +0100 Subject: [PATCH 51/58] CI: Add Android --- .github/workflows/build.yml | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29f0b6bda5d..4f4a0005087 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -216,3 +216,64 @@ jobs: with: name: ddnet-${{ matrix.os }} path: release/artifacts + + build-android: + runs-on: ubuntu-24.04 + env: + CARGO_HTTP_MULTIPLEXING: false + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Prepare + run: | + sudo apt-get update -y + sudo apt-get install cmake ninja-build openjdk-21-jdk p7zip-full curl glslang-tools openssl + cargo install cargo-ndk + rustup target add armv7-linux-androideabi + rustup target add i686-linux-android + rustup target add aarch64-linux-android + rustup target add x86_64-linux-android + mkdir ~/Android + cd ~/Android + mkdir Sdk + cd Sdk + mkdir ndk + cd ndk + wget --quiet https://dl.google.com/android/repository/android-ndk-r26d-linux.zip + unzip android-ndk-r26d-linux.zip + rm android-ndk-r26d-linux.zip + cd ~/Android/Sdk + mkdir build-tools + cd build-tools + wget --quiet https://dl.google.com/android/repository/build-tools_r30.0.3-linux.zip + unzip build-tools_r30.0.3-linux.zip + rm build-tools_r30.0.3-linux.zip + mv android-11 30.0.3 + cd ~/Android/Sdk + mkdir cmdline-tools + cd cmdline-tools + wget --quiet https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip + unzip commandlinetools-linux-11076708_latest.zip + rm commandlinetools-linux-11076708_latest.zip + mv cmdline-tools latest + yes | latest/bin/sdkmanager --licenses + + - name: Build + env: + TW_KEY_NAME: /home/runner/DDNet.jks + TW_KEY_ALIAS: DDNet-Key + run: | + export TW_KEY_PW="$(openssl rand -base64 32)" + keytool -genkey -v -keystore "$TW_KEY_NAME" -keyalg RSA -keysize 2048 -validity 10000 -alias "$TW_KEY_ALIAS" -storepass "$TW_KEY_PW" -dname "CN=DDNet CI, OU=DDNet, O=DDNet" + mkdir build-android + scripts/android/cmake_android.sh all DDNet org.ddnet.client Release build-android + mkdir artifacts + mv build-android/DDNet.apk artifacts + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: ddnet-android + path: artifacts From 9075631a5368a7716c8ae36f636466008049bad3 Mon Sep 17 00:00:00 2001 From: Jupeyy Date: Thu, 5 Dec 2024 16:57:31 +0100 Subject: [PATCH 52/58] Reset color in RenderLoading & HUD --- src/game/client/components/hud.cpp | 2 ++ src/game/client/components/menus.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/game/client/components/hud.cpp b/src/game/client/components/hud.cpp index a4bb1a64125..c4014a8e750 100644 --- a/src/game/client/components/hud.cpp +++ b/src/game/client/components/hud.cpp @@ -86,6 +86,8 @@ void CHud::OnInit() { OnReset(); + Graphics()->SetColor(1.0, 1.0, 1.0, 1.0); + m_HudQuadContainerIndex = Graphics()->CreateQuadContainer(false); Graphics()->QuadsSetSubset(0, 0, 1, 1); PrepareAmmoHealthAndArmorQuads(); diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index e5d75ef52d4..5d5c3bb762b 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -806,6 +806,8 @@ void CMenus::RenderLoading(const char *pCaption, const char *pContent, int Incre Ui()->RenderProgressBar(ProgressBar, CurLoadRenderCount / (float)m_LoadingState.m_Total); } + Graphics()->SetColor(1.0, 1.0, 1.0, 1.0); + Client()->UpdateAndSwap(); } From dcb4034f2bd98b1f7b3f25aa2c92063e99829beb Mon Sep 17 00:00:00 2001 From: Andrii <113429882+EGYT5453@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:47:04 +0200 Subject: [PATCH 53/58] Update Ukrainian l10n (18.8) --- data/languages/ukrainian.txt | 240 +++++++++++++++++------------------ 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/data/languages/ukrainian.txt b/data/languages/ukrainian.txt index 10d934dabcc..11a0d288d04 100644 --- a/data/languages/ukrainian.txt +++ b/data/languages/ukrainian.txt @@ -102,15 +102,31 @@ Activate Activate all == Активувати усіх +[Direct touch input] +Active action +== Поточна дія + +Active: Fire +== Поточне: Вогонь + +Active: Hook +== Поточне: Гак + Add == Додати Add Clan == Додати клан +Add clanmates by entering their clan below and leaving the name blank. +== Додайте співклановців, ввівши їх клан нижче і залишивши поле "Нік" пустим. + Add Friend == Додати друга +Add friends by entering their name below or by clicking their name in the player list. +== Додайте друзів, ввівши їх нікнейми нижче або натиснувши на їх нікнейми у списку гравців. + Address == Адреса @@ -120,6 +136,13 @@ Adjust the opacity of entities belonging to other teams, such as tees and namepl AFR == АФР +[Direct touch input] +Aim +== Прицілювання + +Aim +== Прицілювання + Aim bind == Прив’язка @@ -167,40 +190,49 @@ Appearance == Вигляд Are you sure that you want to delete '%s'? -== Ви дійсно хочете видалити '%s'? +== Ви дійсно бажаєте видалити '%s'? Are you sure that you want to delete the demo '%s'? -== Ви дійсно хочете видалити демо '%s'? +== Ви дійсно бажаєте видалити демо '%s'? Are you sure that you want to delete the folder '%s'? -== Ви дійсно хочете видалити теку '%s'? +== Ви дійсно бажаєте видалити теку '%s'? + +Are you sure that you want to discard the current changes to the touch controls? +== Ви дійсно бажаєте скасувати поточні зміни до сенсорного керування? Are you sure that you want to disconnect? -== Ви дійсно хочете від’єднатися? +== Ви дійсно бажаєте від’єднатися? Are you sure that you want to disconnect and switch to a different server? -== Ви дійсно хочете від’єднатися й приєднатися до іншого сервера? +== Ви дійсно бажаєте від’єднатися й приєднатися до іншого сервера? Are you sure that you want to disconnect your dummy? -== Ви дійсно хочете від’єднати свого даммі? +== Ви дійсно бажаєте від’єднати свого даммі? + +Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. +== Ви дійсно бажаєте імпортувати сенсорне керування з буфера обміну? Це перезапише ваші поточні налаштування. Are you sure that you want to quit? -== Ви дійсно хочете вийти? +== Ви дійсно бажаєте вийти? Are you sure that you want to remove the clan '%s' from your friends list? -== Ви дійсно хочете прибрати клан '%s' зі списку друзів? +== Ви дійсно бажаєте прибрати клан '%s' зі списку друзів? Are you sure that you want to remove the player '%s' from your friends list? -== Ви дійсно хочете прибрати гравця '%s' зі списку друзів? +== Ви дійсно бажаєте прибрати гравця '%s' зі списку друзів? Are you sure that you want to reset the controls to their defaults? -== Ви дійсно хочете скинути налаштування керувань до початкових значень? +== Ви дійсно бажаєте скинути налаштування керувань до початкових значень? + +Are you sure that you want to reset the touch controls to default? +== Ви дійсно бажаєте скинути сенсорне керування до початкових значень? Are you sure that you want to restart? -== Ви дійсно хочете перезапустити? +== Ви дійсно бажаєте перезапустити? Are you sure you want to save your skin? If a skin with this name already exists, it will be replaced. -== Ви дійсно хочете зберегти ваш скін? Якщо скін із цією назвою вже існує, його буде замінено. +== Ви дійсно бажаєте зберегти ваш скін? Якщо скін із цією назвою вже існує, його буде замінено. ASI == АЗІ @@ -332,6 +364,9 @@ Client Client message == Повідомлення клієнта +Close +== Закрити + Close the demo player == Закрити програвач демо @@ -394,12 +429,24 @@ Could not initialize the given graphics backend, reverting to the default backen Could not initialize the given graphics backend, this is probably because you didn't install the driver of the integrated graphics card. == Не вдалося ініціалізувати заданий графічний рушій, імовірно, ви не встановили драйвери на вбудовану відеокарту. +Could not load default touch controls from file. See local console for details. +== Не вдалося завантажити стандартне сенсорне керування з файлу. Див. локальну консоль для подробиць. + +Could not load touch controls from clipboard. See local console for details. +== Не вдалося завантажити сенсорне керування з файлу. Див. локальну консоль для подробиць. + +Could not load touch controls from file. See local console for details. +== Не вдалося завантажити сенсорне керування з файлу. Див. локальну консоль для подробиць. + Could not resolve connect address '%s'. See local console for details. == Не вдалося визначити адресу з’єднання '%s'. Див. локальну консоль для подробиць. Could not save downloaded map. Try manually deleting this file: %s == Не вдалося зберегти завантажену мапу. Спробуйте самостійно видалити цей файл: %s +Could not save touch controls to file. See local console for details. +== Не вдалося зберегти сенсорне керування до файлу. Див. локальну консоль для подробиць. + Count players only == Рахувати лише гравців @@ -506,6 +553,19 @@ Demos directory Desktop fullscreen == Робочий стіл на весь екран +Direct touch input while ingame +== Прямий сенсорний ввід у грі + +Direct touch input while spectating +== Прямий сенсорний ввід під час спостерігання + +[Direct touch input] +Disabled +== Вимкнено + +Discard changes +== Скасувати зміни + Disconnect == Від’єднатися @@ -551,6 +611,9 @@ Dummy is not allowed on this server Dynamic Camera == Динамічна камера +Edit touch controls +== Змінити сенсорне керування + Editor == Редактор @@ -608,12 +671,18 @@ Error Error loading demo == Помилка завантаження демо +Error loading touch controls +== Помилка завантаження сенсорного керування + Error playing demo == Помилка відтворення демо Error saving settings == Помилка збереження налаштувань +Error saving touch controls +== Помилка збереження сенсорного керування + EUR == ЄВР @@ -632,6 +701,9 @@ Export cut as a separate demo Export demo cut == Експортувати фрагмент демо +Export to clipboard +== Експортувати до буфера обміну + Extras == Додатково @@ -681,6 +753,10 @@ Filter connecting players Fire == Вогонь +[Direct touch input] +Fire +== Вогонь + Folder == Тека @@ -821,6 +897,10 @@ Highlighted message Hook == Гак +[Direct touch input] +Hook +== Гак + Hook Collisions == Зіткнення гака @@ -852,6 +932,9 @@ Hue Hz == Гц +Import from clipboard +== Імпортувати з буфера обміну + Indicate map finish == Позначати пройдені мапи @@ -1086,7 +1169,7 @@ Next weapon == Наст. зброя Nickname -== Псевдонім +== Нікнейм No == Ні @@ -1140,12 +1223,18 @@ Nothing hookable Offline (%d) == Не в мережі (%d) +Offline friends and clanmates will appear here. +== Тут з'являться друзі та співклановці не в мережі. + Ok == Гаразд Online clanmates (%d) == Співклановці в мережі (%d) +Online friends (%d) +== Друзі в мережі + Only save improvements == Зберігати лише покращення @@ -1256,6 +1345,9 @@ Prev. weapon Preview == Передперегляд +Preview 'Hook collisions' being pressed +== "Зіткнення гака" натиснуто + Quads are used for background decoration == Квади використовуються для декорацій @@ -1383,6 +1475,9 @@ Sat. Save == Зберегти +Save changes +== Зберегти зміни + Save ghost == Зберігати привида @@ -1489,7 +1584,7 @@ Show freeze bars == Показувати смугу заморозки Show friend mark (♥) in name plates -== Показувати позначку друга (♥) біля псевдонімів +== Показувати позначку друга (♥) біля ніків Show friends only == Показувати лише з друзями @@ -1525,7 +1620,7 @@ Show local time always == Завжди показувати місцевий час Show name plates -== Показувати псевдоніми +== Показувати ніки Show names in chat in team colors == Показувати імена в чаті в кольорах команди @@ -1817,6 +1912,9 @@ Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.c Unregister protocol and file extensions == Розреєструвати протокол і розширення файлів +Unsaved changes +== Незбережені зміни + Update failed! Check log… == Помилка оновлення! Перевірте журнал… @@ -1904,6 +2002,12 @@ When you cross the start line, show a ghost tee replicating the movements of you Why are you slowmo replaying to read this? == Чому ви переглядаєте це у повторі? +Width of others' hook collision line +== Товщина лінії зіткнення гака інших + +Width of your own hook collision line +== Товщина вашої лінії зіткнення гака + Windowed == У вікні @@ -1923,7 +2027,7 @@ You must restart the game for all settings to take effect. == Щоб налаштування набули чинності, перезапустіть гру. Your nickname '%s' is already used (%d points). Do you still want to use it? -== Ваш псевдонім «%s» вже зайнято (%d балів). Усе ще хочете використовувати його? +== Ваш нікнейм «%s» вже зайнято (%d балів). Усе ще бажаєте використовувати його? Your skin == Ваш скін @@ -1933,107 +2037,3 @@ Zoom in Zoom out == Віддалити - -Online friends (%d) -== - -Add friends by entering their name below or by clicking their name in the player list. -== - -Add clanmates by entering their clan below and leaving the name blank. -== - -Offline friends and clanmates will appear here. -== - -Edit touch controls -== - -Close -== - -Save changes -== - -Error saving touch controls -== - -Could not save touch controls to file. See local console for details. -== - -Unsaved changes -== - -Discard changes -== - -Are you sure that you want to discard the current changes to the touch controls? -== - -Are you sure that you want to reset the touch controls to default? -== - -Import from clipboard -== - -Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls. -== - -Export to clipboard -== - -Direct touch input while ingame -== - -[Direct touch input] -Disabled -== - -[Direct touch input] -Active action -== - -[Direct touch input] -Aim -== - -[Direct touch input] -Fire -== - -[Direct touch input] -Hook -== - -Direct touch input while spectating -== - -Error loading touch controls -== - -Could not load touch controls from file. See local console for details. -== - -Could not load default touch controls from file. See local console for details. -== - -Could not load touch controls from clipboard. See local console for details. -== - -Width of your own hook collision line -== - -Width of others' hook collision line -== - -Preview 'Hook collisions' being pressed -== - -Aim -== - -Active: Fire -== - -Active: Hook -== From a771b921c364b82a107c8c7f07e06e5f80d69a42 Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Thu, 5 Dec 2024 21:56:21 +0100 Subject: [PATCH 54/58] fix style --- data/languages/ukrainian.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/languages/ukrainian.txt b/data/languages/ukrainian.txt index 11a0d288d04..38cab31735f 100644 --- a/data/languages/ukrainian.txt +++ b/data/languages/ukrainian.txt @@ -1233,7 +1233,7 @@ Online clanmates (%d) == Співклановці в мережі (%d) Online friends (%d) -== Друзі в мережі +== Друзі в мережі (%d) Only save improvements == Зберігати лише покращення From c79b0329f9a6b70f1e88a1164de8e41a855ffb6f Mon Sep 17 00:00:00 2001 From: TsFreddie Date: Fri, 6 Dec 2024 12:45:40 +0800 Subject: [PATCH 55/58] load localization early --- src/engine/client.h | 2 ++ src/engine/client/client.cpp | 3 +++ src/game/client/gameclient.cpp | 15 +++++++++------ src/game/client/gameclient.h | 1 + 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/engine/client.h b/src/engine/client.h index 54fc73d2d5f..114d6bf418e 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -390,6 +390,8 @@ class IGameClient : public IInterface virtual void ApplySkin7InfoFromSnapObj(const protocol7::CNetObj_De_ClientInfo *pObj, int ClientId) = 0; virtual int OnDemoRecSnap7(class CSnapshot *pFrom, class CSnapshot *pTo, int Conn) = 0; virtual int TranslateSnap(class CSnapshot *pSnapDstSix, class CSnapshot *pSnapSrcSeven, int Conn, bool Dummy) = 0; + + virtual void InitializeLanguage() = 0; }; void SnapshotRemoveExtraProjectileInfo(class CSnapshot *pSnap); diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 41b4db0a906..79c8e03fb3f 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -3017,6 +3017,9 @@ void CClient::Run() Graphics()->Clear(0, 0, 0); Graphics()->Swap(); + // init localization first, making sure all errors during init can be localized + GameClient()->InitializeLanguage(); + // init sound, allowed to fail const bool SoundInitFailed = Sound()->Init() != 0; diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index c49bfcd1401..7a2d88c63dd 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -267,6 +267,15 @@ static void GenerateTimeoutCode(char *pTimeoutCode) } } +void CGameClient::InitializeLanguage() +{ + // set the language + g_Localization.LoadIndexfile(Storage(), Console()); + if(g_Config.m_ClShowWelcome) + g_Localization.SelectDefaultLanguage(Console(), g_Config.m_ClLanguagefile, sizeof(g_Config.m_ClLanguagefile)); + g_Localization.Load(g_Config.m_ClLanguagefile, Storage(), Console()); +} + void CGameClient::OnInit() { const int64_t OnInitStart = time_get(); @@ -313,12 +322,6 @@ void CGameClient::OnInit() str_format(m_aDDNetVersionStr, sizeof(m_aDDNetVersionStr), "%s %s", GAME_NAME, GAME_RELEASE_VERSION); } - // set the language - g_Localization.LoadIndexfile(Storage(), Console()); - if(g_Config.m_ClShowWelcome) - g_Localization.SelectDefaultLanguage(Console(), g_Config.m_ClLanguagefile, sizeof(g_Config.m_ClLanguagefile)); - g_Localization.Load(g_Config.m_ClLanguagefile, Storage(), Console()); - // TODO: this should be different // setup item sizes for(int i = 0; i < NUM_NETOBJTYPES; i++) diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 6b42d95cb51..314308c6dd4 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -537,6 +537,7 @@ class CGameClient : public IGameClient virtual void OnFlagGrab(int TeamId); void OnWindowResize() override; + void InitializeLanguage() override; bool m_LanguageChanged = false; void OnLanguageChange(); void HandleLanguageChanged(); From ad8b230e6392347bb39f0d6d561448747333e406 Mon Sep 17 00:00:00 2001 From: SollyBunny Date: Fri, 6 Dec 2024 07:38:26 +0000 Subject: [PATCH 56/58] Add death sounds/particles on spike in practice --- src/game/server/entities/character.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/game/server/entities/character.cpp b/src/game/server/entities/character.cpp index 983caab9150..0a16aeca018 100644 --- a/src/game/server/entities/character.cpp +++ b/src/game/server/entities/character.cpp @@ -1387,7 +1387,16 @@ void CCharacter::HandleSkippableTiles(int Index) !m_Core.m_Super && !m_Core.m_Invincible && !(Team() && Teams()->TeeFinished(m_pPlayer->GetCid()))) { if(Team() && Teams()->IsPractice(Team())) + { Freeze(); + // Rate limit death effects to once per second + if(Server()->Tick() - m_pPlayer->m_DieTick >= Server()->TickSpeed()) + { + m_pPlayer->m_DieTick = Server()->Tick(); + GameServer()->CreateSound(m_Pos, SOUND_PLAYER_DIE, TeamMask()); + GameServer()->CreateDeath(m_Pos, m_pPlayer->GetCid(), TeamMask()); + } + } else Die(m_pPlayer->GetCid(), WEAPON_WORLD); return; From 41fee04a2f39bb7a215d650ee75041581e12b463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Fri, 6 Dec 2024 21:33:15 +0100 Subject: [PATCH 57/58] Fix clipping of last console line, save background noise as RGBA The last console line was being clipped, which was noticeable because underscore characters were appearing thinner. Prevent warning due to background noise texture being converted to RGBA when launching. --- data/background_noise.png | Bin 3347 -> 9762 bytes src/game/client/components/console.cpp | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/background_noise.png b/data/background_noise.png index 9465570ce0679bc8023ec2c454024054cad63987..31af9896303be51b57666b8982aefcc472d188cd 100644 GIT binary patch literal 9762 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 KVwJo_#Qy+~5x7_Y literal 3347 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 L%}lQh?h^eE$vA?* diff --git a/src/game/client/components/console.cpp b/src/game/client/components/console.cpp index a3257a0b1c8..1d9f4ab45bc 100644 --- a/src/game/client/components/console.cpp +++ b/src/game/client/components/console.cpp @@ -1276,7 +1276,7 @@ void CGameConsole::OnRender() const float YScale = Graphics()->ScreenHeight() / Screen.h; const float CalcOffsetY = LineHeight * std::floor((y - RowHeight) / LineHeight); const float ClipStartY = (y - CalcOffsetY) * YScale; - Graphics()->ClipEnable(0, ClipStartY, Screen.w * XScale, y * YScale - ClipStartY); + Graphics()->ClipEnable(0, ClipStartY, Screen.w * XScale, (y + 2.0f) * YScale - ClipStartY); while(pEntry) { From d9cdd0967c7c82513430d6826f28d1465305662d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sat, 7 Dec 2024 20:16:32 +0100 Subject: [PATCH 58/58] Make client/graphics warnings thread-safe, improve log format Add mutex for client/graphics warnings. Return an optional copy of the current warning and remove it from the vector immediately instead of returning a pointer to it and removing it later. Use atomic for `m_WarnPngliteIncompatibleImages` as this variable is also used in multiple threads. Do not use `CLock` as the thread-safety annotations would otherwise have to be added to a lot of client/graphics functions. Use `client` instead of the warning title as the log system, as the warning title is usually too long. Closes #9354. --- src/engine/client.h | 8 ++-- src/engine/client/client.cpp | 19 +++++----- src/engine/client/client.h | 4 +- src/engine/client/graphics_threaded.cpp | 49 +++++++++++++------------ src/engine/client/graphics_threaded.h | 11 +++++- src/engine/client/warning.cpp | 16 ++++++++ src/engine/graphics.h | 3 +- src/engine/warning.h | 4 +- src/game/client/components/menus.cpp | 6 ++- src/game/client/gameclient.cpp | 15 +++++--- 10 files changed, 85 insertions(+), 50 deletions(-) diff --git a/src/engine/client.h b/src/engine/client.h index 114d6bf418e..753568ffc24 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -8,15 +8,15 @@ #include "message.h" #include +#include +#include #include #include #include -#include #include - -#include +#include struct SWarning; @@ -300,7 +300,7 @@ class IClient : public IInterface virtual void GetSmoothTick(int *pSmoothTick, float *pSmoothIntraTick, float MixAmount) = 0; virtual void AddWarning(const SWarning &Warning) = 0; - virtual SWarning *GetCurWarning() = 0; + virtual std::optional CurrentWarning() = 0; virtual CChecksumData *ChecksumData() = 0; virtual int UdpConnectivity(int NetType) = 0; diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 79c8e03fb3f..97ef74d2b57 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -2651,7 +2651,7 @@ void CClient::Update() { SWarning Warning(Localize("Error playing demo"), m_DemoPlayer.ErrorMessage()); Warning.m_AutoHide = false; - m_vWarnings.emplace_back(Warning); + AddWarning(Warning); } } } @@ -3085,7 +3085,7 @@ void CClient::Run() { SWarning Warning(Localize("Sound error"), Localize("The audio device couldn't be initialised.")); Warning.m_AutoHide = false; - m_vWarnings.emplace_back(Warning); + AddWarning(Warning); } bool LastD = false; @@ -5014,23 +5014,22 @@ void CClient::GetSmoothTick(int *pSmoothTick, float *pSmoothIntraTick, float Mix void CClient::AddWarning(const SWarning &Warning) { + const std::unique_lock Lock(m_WarningsMutex); m_vWarnings.emplace_back(Warning); } -SWarning *CClient::GetCurWarning() +std::optional CClient::CurrentWarning() { + const std::unique_lock Lock(m_WarningsMutex); if(m_vWarnings.empty()) { - return NULL; - } - else if(m_vWarnings[0].m_WasShown) - { - m_vWarnings.erase(m_vWarnings.begin()); - return NULL; + return std::nullopt; } else { - return m_vWarnings.data(); + std::optional Result = std::make_optional(m_vWarnings[0]); + m_vWarnings.erase(m_vWarnings.begin()); + return Result; } } diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 56e92ac05cf..2e9fab96719 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -5,6 +5,7 @@ #include #include +#include #include @@ -235,6 +236,7 @@ class CClient : public IClient, public CDemoPlayer::IListener int m_State = STATE_INIT; } m_VersionInfo; + std::mutex m_WarningsMutex; std::vector m_vWarnings; std::vector m_vQuittingWarnings; @@ -508,7 +510,7 @@ class CClient : public IClient, public CDemoPlayer::IListener void GetSmoothTick(int *pSmoothTick, float *pSmoothIntraTick, float MixAmount) override; void AddWarning(const SWarning &Warning) override; - SWarning *GetCurWarning() override; + std::optional CurrentWarning() override; std::vector &&QuittingWarnings() { return std::move(m_vQuittingWarnings); } CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; } diff --git a/src/engine/client/graphics_threaded.cpp b/src/engine/client/graphics_threaded.cpp index a9bf3d4a50a..6ead9b09fd9 100644 --- a/src/engine/client/graphics_threaded.cpp +++ b/src/engine/client/graphics_threaded.cpp @@ -350,7 +350,7 @@ bool CGraphics_Threaded::IsSpriteTextureFullyTransparent(const CImageInfo &FromI return IsImageSubFullyTransparent(FromImageInfo, x, y, w, h); } -static void LoadTextureAddWarning(size_t Width, size_t Height, int Flags, const char *pTexName, std::vector &vWarnings) +void CGraphics_Threaded::LoadTextureAddWarning(size_t Width, size_t Height, int Flags, const char *pTexName) { if((Flags & IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE) != 0 || (Flags & IGraphics::TEXLOAD_TO_3D_TEXTURE) != 0) { @@ -360,7 +360,7 @@ static void LoadTextureAddWarning(size_t Width, size_t Height, int Flags, const char aText[128]; str_format(aText, sizeof(aText), "\"%s\"", pTexName ? pTexName : "(no name)"); str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), aText, 16, 16); - vWarnings.emplace_back(NewWarning); + AddWarning(NewWarning); } } } @@ -385,7 +385,7 @@ static CCommandBuffer::SCommand_Texture_Create LoadTextureCreateCommand(int Text IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRaw(const CImageInfo &Image, int Flags, const char *pTexName) { - LoadTextureAddWarning(Image.m_Width, Image.m_Height, Flags, pTexName, m_vWarnings); + LoadTextureAddWarning(Image.m_Width, Image.m_Height, Flags, pTexName); if(Image.m_Width == 0 || Image.m_Height == 0) return IGraphics::CTextureHandle(); @@ -416,7 +416,7 @@ IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRawMove(CImageInfo &Ima return TextureHandle; } - LoadTextureAddWarning(Image.m_Width, Image.m_Height, Flags, pTexName, m_vWarnings); + LoadTextureAddWarning(Image.m_Width, Image.m_Height, Flags, pTexName); if(Image.m_Width == 0 || Image.m_Height == 0) return IGraphics::CTextureHandle(); @@ -543,7 +543,7 @@ bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const char *pFilename, int S if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) { - m_vWarnings.emplace_back(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pFilename)); + AddWarning(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pFilename)); } return true; @@ -558,7 +558,7 @@ bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const uint8_t *pData, size_t if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) { - m_vWarnings.emplace_back(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pContextName)); + AddWarning(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pContextName)); } return true; @@ -577,7 +577,7 @@ bool CGraphics_Threaded::CheckImageDivisibility(const char *pContextName, CImage str_format(aContextNameQuoted, sizeof(aContextNameQuoted), "\"%s\"", pContextName); str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), aContextNameQuoted, DivX, DivY); - m_vWarnings.emplace_back(NewWarning); + AddWarning(NewWarning); ImageIsValid = false; } @@ -611,7 +611,7 @@ bool CGraphics_Threaded::IsImageFormatRgba(const char *pContextName, const CImag str_format(aContextNameQuoted, sizeof(aContextNameQuoted), "\"%s\"", pContextName); str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), Localize("The format of texture %s is not RGBA which will cause visual bugs."), aContextNameQuoted); - m_vWarnings.emplace_back(NewWarning); + AddWarning(NewWarning); return false; } return true; @@ -629,7 +629,7 @@ void CGraphics_Threaded::KickCommandBuffer() for(const auto &WarnStr : WarningStrings) WarningStr.append((WarnStr + "\n")); str_copy(NewWarning.m_aWarningMsg, WarningStr.c_str()); - m_vWarnings.emplace_back(NewWarning); + AddWarning(NewWarning); } // swap buffer @@ -2224,11 +2224,11 @@ void CGraphics_Threaded::UpdateViewport(int X, int Y, int W, int H, bool ByResiz void CGraphics_Threaded::AddBackEndWarningIfExists() { const char *pErrStr = m_pBackend->GetErrorString(); - if(pErrStr != NULL) + if(pErrStr != nullptr) { SWarning NewWarning; str_copy(NewWarning.m_aWarningMsg, Localize(pErrStr)); - m_vWarnings.emplace_back(NewWarning); + AddWarning(NewWarning); } } @@ -2708,15 +2708,6 @@ void CGraphics_Threaded::TakeCustomScreenshot(const char *pFilename) void CGraphics_Threaded::Swap() { - if(!m_vWarnings.empty()) - { - SWarning *pCurWarning = GetCurWarning(); - if(pCurWarning->m_WasShown) - { - m_vWarnings.erase(m_vWarnings.begin()); - } - } - bool Swapped = false; ScreenshotDirect(&Swapped); ReadPixelDirect(&Swapped); @@ -2790,14 +2781,24 @@ void CGraphics_Threaded::WaitForIdle() m_pBackend->WaitForIdle(); } -SWarning *CGraphics_Threaded::GetCurWarning() +void CGraphics_Threaded::AddWarning(const SWarning &Warning) { + const std::unique_lock Lock(m_WarningsMutex); + m_vWarnings.emplace_back(Warning); +} + +std::optional CGraphics_Threaded::CurrentWarning() +{ + const std::unique_lock Lock(m_WarningsMutex); if(m_vWarnings.empty()) - return NULL; + { + return std::nullopt; + } else { - SWarning *pCurWarning = m_vWarnings.data(); - return pCurWarning; + std::optional Result = std::make_optional(m_vWarnings[0]); + m_vWarnings.erase(m_vWarnings.begin()); + return Result; } } diff --git a/src/engine/client/graphics_threaded.h b/src/engine/client/graphics_threaded.h index 06b841c3c5e..db820956aab 100644 --- a/src/engine/client/graphics_threaded.h +++ b/src/engine/client/graphics_threaded.h @@ -2,10 +2,13 @@ #define ENGINE_CLIENT_GRAPHICS_THREADED_H #include + #include #include +#include #include +#include #include #include @@ -798,8 +801,9 @@ class CGraphics_Threaded : public IEngineGraphics size_t m_FirstFreeTexture; int m_TextureMemoryUsage; - bool m_WarnPngliteIncompatibleImages = false; + std::atomic m_WarnPngliteIncompatibleImages = false; + std::mutex m_WarningsMutex; std::vector m_vWarnings; // is a non full windowed (in a sense that the viewport won't include the whole window), @@ -945,6 +949,7 @@ class CGraphics_Threaded : public IEngineGraphics IGraphics::CTextureHandle FindFreeTextureIndex(); void FreeTextureIndex(CTextureHandle *pIndex); void UnloadTexture(IGraphics::CTextureHandle *pIndex) override; + void LoadTextureAddWarning(size_t Width, size_t Height, int Flags, const char *pTexName); IGraphics::CTextureHandle LoadTextureRaw(const CImageInfo &Image, int Flags, const char *pTexName = nullptr) override; IGraphics::CTextureHandle LoadTextureRawMove(CImageInfo &Image, int Flags, const char *pTexName = nullptr) override; @@ -1240,7 +1245,9 @@ class CGraphics_Threaded : public IEngineGraphics bool IsIdle() const override; void WaitForIdle() override; - SWarning *GetCurWarning() override; + void AddWarning(const SWarning &Warning); + std::optional CurrentWarning() override; + bool ShowMessageBox(unsigned Type, const char *pTitle, const char *pMsg) override; bool IsBackendInitialized() override; diff --git a/src/engine/client/warning.cpp b/src/engine/client/warning.cpp index b15ed4c1fd8..bb160a09a73 100644 --- a/src/engine/client/warning.cpp +++ b/src/engine/client/warning.cpp @@ -2,12 +2,28 @@ #include +SWarning::SWarning(const SWarning &Other) +{ + str_copy(m_aWarningTitle, Other.m_aWarningTitle); + str_copy(m_aWarningMsg, Other.m_aWarningMsg); + m_AutoHide = Other.m_AutoHide; +} + SWarning::SWarning(const char *pMsg) { str_copy(m_aWarningMsg, pMsg); } + SWarning::SWarning(const char *pTitle, const char *pMsg) { str_copy(m_aWarningTitle, pTitle); str_copy(m_aWarningMsg, pMsg); } + +SWarning &SWarning::operator=(const SWarning &Other) +{ + str_copy(m_aWarningTitle, Other.m_aWarningTitle); + str_copy(m_aWarningMsg, Other.m_aWarningMsg); + m_AutoHide = Other.m_AutoHide; + return *this; +} diff --git a/src/engine/graphics.h b/src/engine/graphics.h index a4f44119552..c3a9439832e 100644 --- a/src/engine/graphics.h +++ b/src/engine/graphics.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #define GRAPHICS_TYPE_UNSIGNED_BYTE 0x1401 @@ -467,7 +468,7 @@ class IGraphics : public IInterface // this function always returns the pixels in RGB virtual TGLBackendReadPresentedImageData &GetReadPresentedImageDataFuncUnsafe() = 0; - virtual SWarning *GetCurWarning() = 0; + virtual std::optional CurrentWarning() = 0; // returns true if the error msg was shown virtual bool ShowMessageBox(unsigned Type, const char *pTitle, const char *pMsg) = 0; diff --git a/src/engine/warning.h b/src/engine/warning.h index 4532a10353a..5eb1ae52945 100644 --- a/src/engine/warning.h +++ b/src/engine/warning.h @@ -4,12 +4,14 @@ struct SWarning { SWarning() = default; + SWarning(const SWarning &Other); SWarning(const char *pMsg); SWarning(const char *pTitle, const char *pMsg); + SWarning &operator=(const SWarning &Other); + char m_aWarningTitle[128] = ""; char m_aWarningMsg[256] = ""; - bool m_WasShown = false; bool m_AutoHide = true; }; diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index 5d5c3bb762b..08ebaa67e40 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -988,10 +989,11 @@ void CMenus::PopupWarning(const char *pTopic, const char *pBody, const char *pBu // no multiline support for console std::string BodyStr = pBody; while(BodyStr.find('\n') != std::string::npos) + { BodyStr.replace(BodyStr.find('\n'), 1, " "); - dbg_msg(pTopic, "%s", BodyStr.c_str()); + } + log_warn("client", "%s: %s", pTopic, BodyStr.c_str()); - // reset active item Ui()->SetActiveItem(nullptr); str_copy(m_aMessageTopic, pTopic); diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 7a2d88c63dd..2ebc4eb5ccb 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -772,13 +772,18 @@ void CGameClient::OnRender() // update the local character and spectate position UpdatePositions(); - // display gfx & client warnings - for(SWarning *pWarning : {Graphics()->GetCurWarning(), Client()->GetCurWarning()}) + // display warnings + if(m_Menus.CanDisplayWarning()) { - if(pWarning != nullptr && m_Menus.CanDisplayWarning()) + std::optional Warning = Graphics()->CurrentWarning(); + if(!Warning.has_value()) { - m_Menus.PopupWarning(pWarning->m_aWarningTitle[0] == '\0' ? Localize("Warning") : pWarning->m_aWarningTitle, pWarning->m_aWarningMsg, Localize("Ok"), pWarning->m_AutoHide ? 10s : 0s); - pWarning->m_WasShown = true; + Warning = Client()->CurrentWarning(); + } + if(Warning.has_value()) + { + const SWarning TheWarning = Warning.value(); + m_Menus.PopupWarning(TheWarning.m_aWarningTitle[0] == '\0' ? Localize("Warning") : TheWarning.m_aWarningTitle, TheWarning.m_aWarningMsg, Localize("Ok"), TheWarning.m_AutoHide ? 10s : 0s); } }