Skip to content
51 changes: 41 additions & 10 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,9 @@ if(NOT MSVC AND NOT HAIKU)
add_cxx_compiler_flag_if_supported(OUR_FLAGS_OWN -Wdynamic-class-memaccess) # clang
add_cxx_compiler_flag_if_supported(OUR_FLAGS_OWN -Wlogical-op)
add_cxx_compiler_flag_if_supported(OUR_FLAGS_OWN -Wno-missing-field-initializers)
add_cxx_compiler_flag_if_supported(OUR_FLAGS_OWN -Wno-nullability-completeness) # Mac OS build on github
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_cxx_compiler_flag_if_supported(OUR_FLAGS_OWN -Wno-nullability-completeness) # Mac OS build on GitHub
endif()
add_cxx_compiler_flag_if_supported(OUR_FLAGS_OWN -Wno-psabi) # parameter passing for argument of type ‘__gnu_cxx::__normal_iterator<CCommandProcessorFragment_Vulkan::SMemoryBlock<1>*, std::vector<CCommandProcessorFragment_Vulkan::SMemoryBlock<1>, std::allocator<CCommandProcessorFragment_Vulkan::SMemoryBlock<1> > > >’ changed in GCC 7.1
add_cxx_compiler_flag_if_supported(OUR_FLAGS_OWN -Wno-unused-parameter)
add_cxx_compiler_flag_if_supported(OUR_FLAGS_OWN -Wrestrict)
Expand Down Expand Up @@ -614,6 +616,14 @@ find_package(Python3)
find_package(Rust)
find_package(SDL2)
find_package(SQLite3)
if(CLIENT AND TARGET_OS STREQUAL "linux")
# The Music Player reads MPRIS sessions over D-Bus and uses PulseAudio or
# PipeWire's PulseAudio compatibility server for its realtime visualizer.
# Both integrations remain optional so minimal Linux installations keep
# building with the passive player fallback.
find_package(DBus)
find_package(PulseAudio)
endif()
if(DISCORD)
find_package(DiscordSdk)
endif()
Expand Down Expand Up @@ -2479,6 +2489,7 @@ set_src(GAME_SHARED GLOB src/game
team_state.h
teamscore.cpp
teamscore.h
tune_zone_colors.h
tuning.h
version.h
voting.h
Expand Down Expand Up @@ -2781,12 +2792,19 @@ if(CLIENT)
components/tclient/fast_practice.h
components/tclient/inputs.cpp
components/tclient/inputs.h
components/tclient/menus_tclient.cpp
components/tclient/mod.cpp
components/tclient/mod.h
components/tclient/moving_tiles.cpp
components/tclient/moving_tiles.h
components/tclient/mumble.cpp
components/tclient/mumble.h
components/tclient/music_player/media_decoder.cpp
components/tclient/music_player/media_decoder.h
components/tclient/music_player/music_player.cpp
components/tclient/music_player/music_player.h
components/tclient/music_player/music_player_lyrics.cpp
components/tclient/music_player/music_player_lyrics.h
components/tclient/music_player/media_decoder.cpp
components/tclient/music_player/media_decoder.h
components/tclient/music_player/visualizer/analyzer.cpp
components/tclient/music_player/visualizer/analyzer.h
components/tclient/music_player/visualizer/service.cpp
Expand All @@ -2799,13 +2817,6 @@ if(CLIENT)
components/tclient/music_player/visualizer/source_pulse.cpp
components/tclient/music_player/visualizer/source_wasapi.cpp
components/tclient/music_player/visualizer/types.h
components/tclient/menus_tclient.cpp
components/tclient/mod.cpp
components/tclient/mod.h
components/tclient/moving_tiles.cpp
components/tclient/moving_tiles.h
components/tclient/mumble.cpp
components/tclient/mumble.h
components/tclient/outlines.cpp
components/tclient/outlines.h
components/tclient/pet.cpp
Expand Down Expand Up @@ -3030,6 +3041,15 @@ if(CLIENT)
${LIBS}
)

if(TARGET_OS STREQUAL "linux")
if(DBus_FOUND)
list(APPEND LIBS_CLIENT ${DBUS_LIBRARIES})
endif()
if(PulseAudio_FOUND)
list(APPEND LIBS_CLIENT ${PULSEAUDIO_LIBRARIES})
endif()
endif()

if(DISCORD)
if(NOT DISCORD_DYNAMIC)
list(APPEND LIBS_CLIENT discord-shared)
Expand Down Expand Up @@ -3166,6 +3186,7 @@ if(CLIENT)

target_include_directories(game-client SYSTEM PRIVATE
# tidy-alphabetical-start
${DBUS_INCLUDE_DIRS}
${DISCORDSDK_INCLUDE_DIRS}
${FFMPEG_INCLUDE_DIRS}
${FREETYPE_INCLUDE_DIRS}
Expand All @@ -3174,6 +3195,7 @@ if(CLIENT)
${OPUSFILE_INCLUDE_DIRS}
${OPUS_INCLUDE_DIRS}
${PNG_INCLUDE_DIRS}
${PULSEAUDIO_INCLUDE_DIRS}
${SDL2_INCLUDE_DIRS}
${WAVPACK_INCLUDE_DIRS}
# tidy-alphabetical-end
Expand Down Expand Up @@ -3216,6 +3238,15 @@ if(CLIENT)
target_compile_definitions(game-client PRIVATE CONF_BACKEND_VULKAN)
endif()

if(TARGET_OS STREQUAL "linux")
if(DBus_FOUND)
target_compile_definitions(game-client PRIVATE BC_MUSICPLAYER_HAS_DBUS=1)
endif()
if(PulseAudio_FOUND)
target_compile_definitions(game-client PRIVATE BC_MUSICPLAYER_HAS_PULSE=1)
endif()
endif()

if(WIN32)
# Keep the updater a separately built, versioned executable. It is the
# only process that replaces DDNet.exe after the main client exits.
Expand Down
27 changes: 27 additions & 0 deletions cmake/FindDBus.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
if(NOT CMAKE_CROSSCOMPILING)
find_package(PkgConfig QUIET)
pkg_check_modules(PC_DBUS QUIET dbus-1)
endif()

find_library(DBUS_LIBRARY
NAMES dbus-1
HINTS ${PC_DBUS_LIBDIR} ${PC_DBUS_LIBRARY_DIRS}
${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH}
)
find_path(DBUS_INCLUDEDIR
NAMES dbus/dbus.h
HINTS ${PC_DBUS_INCLUDEDIR} ${PC_DBUS_INCLUDE_DIRS}
${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH}
)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(DBus DEFAULT_MSG DBUS_LIBRARY DBUS_INCLUDEDIR)

mark_as_advanced(DBUS_LIBRARY DBUS_INCLUDEDIR)

if(DBUS_FOUND)
set(DBUS_LIBRARIES ${DBUS_LIBRARY})
# dbus/dbus-arch-deps.h is architecture-specific and is not necessarily in
# the same include directory as dbus/dbus.h.
set(DBUS_INCLUDE_DIRS ${DBUS_INCLUDEDIR} ${PC_DBUS_INCLUDE_DIRS})
endif()
36 changes: 36 additions & 0 deletions cmake/FindPulseAudio.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
if(NOT CMAKE_CROSSCOMPILING)
find_package(PkgConfig QUIET)
pkg_check_modules(PC_PULSEAUDIO QUIET libpulse)
pkg_check_modules(PC_PULSEAUDIO_SIMPLE QUIET libpulse-simple)
endif()

find_library(PULSEAUDIO_LIBRARY
NAMES pulse
HINTS ${PC_PULSEAUDIO_LIBDIR} ${PC_PULSEAUDIO_LIBRARY_DIRS}
${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH}
)
find_library(PULSEAUDIO_SIMPLE_LIBRARY
NAMES pulse-simple
HINTS ${PC_PULSEAUDIO_SIMPLE_LIBDIR} ${PC_PULSEAUDIO_SIMPLE_LIBRARY_DIRS}
${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH}
)
find_path(PULSEAUDIO_INCLUDEDIR
NAMES pulse/pulseaudio.h
HINTS ${PC_PULSEAUDIO_INCLUDEDIR} ${PC_PULSEAUDIO_INCLUDE_DIRS}
${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH}
)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(PulseAudio DEFAULT_MSG
PULSEAUDIO_LIBRARY
PULSEAUDIO_SIMPLE_LIBRARY
PULSEAUDIO_INCLUDEDIR
)

mark_as_advanced(PULSEAUDIO_LIBRARY PULSEAUDIO_SIMPLE_LIBRARY PULSEAUDIO_INCLUDEDIR)

if(PulseAudio_FOUND)
# libpulse-simple depends on libpulse, so keep the dependent library first.
set(PULSEAUDIO_LIBRARIES ${PULSEAUDIO_SIMPLE_LIBRARY} ${PULSEAUDIO_LIBRARY})
set(PULSEAUDIO_INCLUDE_DIRS ${PULSEAUDIO_INCLUDEDIR} ${PC_PULSEAUDIO_INCLUDE_DIRS})
endif()
3 changes: 3 additions & 0 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ FLUSH PRIVILEGES;
You need to install the [Vulkan SDK](https://vulkan.lunarg.com/sdk/home) and set the `VULKAN_SDK` environment flag accordingly.
Default value is ON for Linux, and OFF for Windows and macOS.

* **Linux Music Player support** <br>
The music player reads MPRIS-compatible players (such as Spotify, VLC, mpv, and browser media sessions) from the desktop session bus. Its realtime visualizer uses PulseAudio; this also works with PipeWire when its PulseAudio compatibility service is enabled. Install `libdbus-1-dev libpulse-dev` on Debian/Ubuntu, `dbus libpulse` on Arch Linux, or `dbus-devel pulseaudio-libs-devel` on Fedora. These are optional: when unavailable, the client still builds, but the music player uses its passive fallback.

* **-GNinja** <br>
Use the Ninja build system instead of Make. This automatically parallelizes the build and is generally faster. Compile with `ninja` instead of `make`. Install Ninja with `sudo apt install ninja-build` on Debian, `sudo pacman -S --needed ninja` on Arch Linux.

Expand Down
4 changes: 4 additions & 0 deletions src/engine/client/updater.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ void BuildReleasesUrl(char *pBuf, int BufSize)
str_format(pBuf, BufSize, "%s%ct=%lld", pSource, str_find(pSource, "?") ? '&' : '?', (long long)time_timestamp());
}

#if defined(CONF_FAMILY_WINDOWS)
bool StrEndsWithNoCase(const char *pStr, const char *pSuffix)
{
if(!pStr || !pSuffix)
Expand All @@ -42,6 +43,7 @@ bool StrEndsWithNoCase(const char *pStr, const char *pSuffix)
const int SuffixLen = str_length(pSuffix);
return SuffixLen <= StrLen && str_comp_nocase(pStr + StrLen - SuffixLen, pSuffix) == 0;
}
#endif

std::string ToLowerAscii(const char *pStr)
{
Expand Down Expand Up @@ -216,6 +218,7 @@ bool ParseLatestRelease(json_value *pJson, char *pVersion, int VersionSize, char
return true;
}

#if defined(CONF_FAMILY_WINDOWS)
void StripFilename(char *pPath)
{
for(int i = str_length(pPath) - 1; i >= 0; --i)
Expand Down Expand Up @@ -250,6 +253,7 @@ bool IsLegacyPortableDirectory(const char *pDirectory)
str_comp_nocase_num(pName, pPrefix, str_length(pPrefix)) == 0 &&
StrEndsWithNoCase(pName, pSuffix);
}
#endif
} // namespace

CUpdater::CUpdater()
Expand Down
12 changes: 7 additions & 5 deletions src/engine/shared/storage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,14 @@ class CStorage : public IStorage
// Development builds place the executable in a configuration
// subdirectory (for example, build/release) and the data directory
// next to it (build/data).
fs_parent_dir(aDir);
str_format(aBuf, sizeof(aBuf), "%s/data/mapres", aDir);
if(fs_is_dir(aBuf))
if(fs_parent_dir(aDir) == 0)
{
str_format(m_aDatadir, sizeof(m_aDatadir), "%s/data", aDir);
return;
str_format(aBuf, sizeof(aBuf), "%s/data/mapres", aDir);
if(fs_is_dir(aBuf))
{
str_format(m_aDatadir, sizeof(m_aDatadir), "%s/data", aDir);
return;
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/game/client/components/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ void CChat::ExecuteSmoothHudContextAction(int Action)

if(Action == 0) // Copy
{
char aText[MAX_NAME_LENGTH + MAX_LINE_LENGTH + 4];
char aText[static_cast<int>(MAX_NAME_LENGTH) + MAX_LINE_LENGTH + 4];
str_format(aText, sizeof(aText), "%s%s%s", Line.m_aName, Line.m_ClientId >= 0 ? ": " : "", Line.m_aText);
Input()->SetClipboardText(aText);
str_copy(m_aSmoothHudNotification, TCLocalize("Message copied", "AMF Client"), sizeof(m_aSmoothHudNotification));
Expand Down
6 changes: 3 additions & 3 deletions src/game/client/components/menus_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4749,9 +4749,9 @@ struct SAssetOrganizerPopupContext : public SPopupMenuId
const ColorRGBA PopupBorder = ColorRGBA(0.10f, 0.13f, 0.17f, 0.78f);
auto DoOrganizerPopupButton = [pUi, &PopupAccentPressed, &PopupAccentSoft, &PopupNeutral, &PopupNeutralHover, &PopupNeutralDisabled, &PopupBorder](CButtonContainer *pButton, const char *pText, const CUIRect *pRect, float FontSize, bool Enabled = true) {
const bool Hovered = Enabled && pUi->MouseHovered(pRect);
const bool Active = Enabled && pUi->CheckActiveItem(pButton);
const ColorRGBA Fill = !Enabled ? PopupNeutralDisabled : (Active ? PopupAccentPressed : (Hovered ? PopupNeutralHover : PopupNeutral));
const ColorRGBA Border = !Enabled ? PopupBorder : (Active ? PopupAccentPressed : (Hovered ? PopupAccentSoft : PopupBorder));
const bool IsActive = Enabled && pUi->CheckActiveItem(pButton);
const ColorRGBA Fill = !Enabled ? PopupNeutralDisabled : (IsActive ? PopupAccentPressed : (Hovered ? PopupNeutralHover : PopupNeutral));
const ColorRGBA Border = !Enabled ? PopupBorder : (IsActive ? PopupAccentPressed : (Hovered ? PopupAccentSoft : PopupBorder));
pRect->Draw(Border, IGraphics::CORNER_ALL, 3.0f);
CUIRect Inner = *pRect;
Inner.Margin(1.0f, &Inner);
Expand Down
1 change: 0 additions & 1 deletion src/game/client/components/scoreboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,6 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart,
CountryOffset, Row.y + (Spacing + TeeSizeMod * 5.0f) / 2.0f, CountryLength, Row.h - Spacing - TeeSizeMod * 5.0f);

// ping
ColorRGBA PingColor = TextRender()->DefaultTextColor();
if(g_Config.m_ClEnablePingColor)
{
TextRender()->TextColor(WithRenderAlpha(color_cast<ColorRGBA>(ColorHSLA((300.0f - std::clamp(pInfo->m_Latency, 0, 300)) / 1000.0f, 1.0f, 0.5f))));
Expand Down
40 changes: 17 additions & 23 deletions src/game/client/components/tclient/amf_hud_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,27 +222,27 @@ namespace
// the HUD element is very wide, tall or tiny.
DrawRoundedRectOutline(pGraphics, Rect, IGraphics::CORNER_NONE, 0.0f, Color);

const float ShortSide = std::min(Rect.w, Rect.h);
const float CornerSize = std::min(std::clamp(ShortSide * 0.14f, 0.8f, 5.0f), ShortSide * 0.25f);
const float Outset = std::min(CornerSize * 0.22f, 0.8f);
if(CornerSize <= 0.0f)
return;
const float ShortSide = std::min(Rect.w, Rect.h);
const float CornerSize = std::min(std::clamp(ShortSide * 0.14f, 0.8f, 5.0f), ShortSide * 0.25f);
const float Outset = std::min(CornerSize * 0.22f, 0.8f);
if(CornerSize <= 0.0f)
return;
const float Right = Rect.x + Rect.w;
const float Bottom = Rect.y + Rect.h;

pGraphics->TextureClear();
pGraphics->QuadsBegin();
pGraphics->SetColor(Color);
// Four identical, mirrored triangular markers. They are decoration only;
// no input state or resize hitbox is attached to any corner.
IGraphics::CFreeformItem aCorners[] = {
{Rect.x - Outset, Rect.y - Outset, Rect.x - Outset + CornerSize, Rect.y - Outset, Rect.x - Outset, Rect.y - Outset + CornerSize, Rect.x - Outset, Rect.y - Outset + CornerSize},
{Right + Outset, Rect.y - Outset, Right + Outset - CornerSize, Rect.y - Outset, Right + Outset, Rect.y - Outset + CornerSize, Right + Outset, Rect.y - Outset + CornerSize},
{Rect.x - Outset, Bottom + Outset, Rect.x - Outset + CornerSize, Bottom + Outset, Rect.x - Outset, Bottom + Outset - CornerSize, Rect.x - Outset, Bottom + Outset - CornerSize},
{Right + Outset, Bottom + Outset, Right + Outset - CornerSize, Bottom + Outset, Right + Outset, Bottom + Outset - CornerSize, Right + Outset, Bottom + Outset - CornerSize},
};
pGraphics->QuadsDrawFreeform(aCorners, sizeof(aCorners) / sizeof(aCorners[0]));
pGraphics->QuadsEnd();
pGraphics->QuadsBegin();
pGraphics->SetColor(Color);
// Four identical, mirrored triangular markers. They are decoration only;
// no input state or resize hitbox is attached to any corner.
IGraphics::CFreeformItem aCorners[] = {
{Rect.x - Outset, Rect.y - Outset, Rect.x - Outset + CornerSize, Rect.y - Outset, Rect.x - Outset, Rect.y - Outset + CornerSize, Rect.x - Outset, Rect.y - Outset + CornerSize},
{Right + Outset, Rect.y - Outset, Right + Outset - CornerSize, Rect.y - Outset, Right + Outset, Rect.y - Outset + CornerSize, Right + Outset, Rect.y - Outset + CornerSize},
{Rect.x - Outset, Bottom + Outset, Rect.x - Outset + CornerSize, Bottom + Outset, Rect.x - Outset, Bottom + Outset - CornerSize, Rect.x - Outset, Bottom + Outset - CornerSize},
{Right + Outset, Bottom + Outset, Right + Outset - CornerSize, Bottom + Outset, Right + Outset, Bottom + Outset - CornerSize, Right + Outset, Bottom + Outset - CornerSize},
};
pGraphics->QuadsDrawFreeform(aCorners, sizeof(aCorners) / sizeof(aCorners[0]));
pGraphics->QuadsEnd();
}

CUIRect ClampToBounds(CUIRect Rect, float Width, float Height)
Expand All @@ -252,12 +252,6 @@ namespace
return Rect;
}

float ChatInputBottomExtra(const CChat &Chat)
{
const float ScaledFontSize = Chat.FontSize() * (8.0f / 6.0f);
return std::max(2.25f * ScaledFontSize, std::max(ScaledFontSize + 4.0f, 16.0f));
}

} // namespace

void CAmfHudEditor::OnConsoleInit()
Expand Down
12 changes: 6 additions & 6 deletions src/game/client/components/tclient/menus_tclient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2317,12 +2317,12 @@ void CMenus::RenderSettingsAmfClient(CUIRect MainView)

// Both module-local buttons use the same activation path; their owning
// card decides where the row is placed.
auto RenderModuleHudEditorButton = [&](CUIRect &HudEditorButton, CButtonContainer *pButtonContainer) {
auto RenderModuleHudEditorButton = [&](CUIRect &HudEditorButtonRect, CButtonContainer *pButtonContainer) {
if(DoButtonLineSize_Menu(
pButtonContainer,
TCLocalize("HUD Editor", "AMF Client"),
0,
&HudEditorButton,
&HudEditorButtonRect,
LineSize,
false,
nullptr,
Expand All @@ -2335,7 +2335,7 @@ void CMenus::RenderSettingsAmfClient(CUIRect MainView)
}
GameClient()->m_Tooltips.DoToolTip(
pButtonContainer,
&HudEditorButton,
&HudEditorButtonRect,
CanOpenHudEditor ? TCLocalize("Drag, scale, reset and save the position of AMF HUD elements.", "AMF Client") : TCLocalize("Connect to a server first to open the HUD Editor.", "AMF Client"));
};

Expand Down Expand Up @@ -2770,9 +2770,9 @@ void CMenus::RenderSettingsAmfClient(CUIRect MainView)
auto DoMusicPlayerDropDown = [&](const char *pLabel, int &Value, const char **ppNames, int NumNames, CUi::SDropDownState &State, CScrollRegion &ScrollRegion) {
MusicPlayerContent.HSplitTop(MarginExtraSmall, nullptr, &MusicPlayerContent);
MusicPlayerContent.HSplitTop(LineSize, &Button, &MusicPlayerContent);
CUIRect Label, Select;
Button.VSplitLeft(std::min(150.0f, Button.w * 0.45f), &Label, &Select);
Ui()->DoLabel(&Label, pLabel, 12.0f, TEXTALIGN_ML);
CUIRect DropDownLabel, Select;
Button.VSplitLeft(std::min(150.0f, Button.w * 0.45f), &DropDownLabel, &Select);
Ui()->DoLabel(&DropDownLabel, pLabel, 12.0f, TEXTALIGN_ML);
State.m_SelectionPopupContext.m_pScrollRegion = &ScrollRegion;
Value = Ui()->DoDropDown(&Select, std::clamp(Value, 0, NumNames - 1), ppNames, NumNames, State);
};
Expand Down
Loading