1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-23 14:54:30 +01:00
Files
OpenRCT2/src/openrct2/platform/Platform.Linux.cpp
2025-05-18 21:32:48 +02:00

460 lines
14 KiB
C++

/*****************************************************************************
* Copyright (c) 2014-2025 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#if defined(__unix__) && !defined(__ANDROID__) && !defined(__APPLE__)
#include "../Diagnostic.h"
#include <cstring>
#include <fnmatch.h>
#include <limits.h>
#include <locale.h>
#include <pwd.h>
#include <stdlib.h>
#include <unistd.h>
#include <vector>
#if defined(__FreeBSD__) || defined(__NetBSD__)
#include <stddef.h>
#include <sys/param.h>
#include <sys/sysctl.h>
#endif // __FreeBSD__ || __NetBSD__
#if defined(__linux__)
// for PATH_MAX
#include <linux/limits.h>
#endif // __linux__
#ifndef DISABLE_TTF
#include <fontconfig/fontconfig.h>
#endif // DISABLE_TTF
#include "../Date.h"
#include "../OpenRCT2.h"
#include "../core/Path.hpp"
#include "../localisation/Language.h"
#include "Platform.h"
namespace OpenRCT2::Platform
{
// EnvLangGuard allows us to temporarily set the user's locale
// to the generic C locale, in order to trick fontconfig into
// returning an English font face name, while using RAII to avoid
// changing locale settings in other parts of the program
class EnvLangGuard
{
public:
EnvLangGuard();
~EnvLangGuard();
private:
// GNU recommends scripts/programs set LC_ALL to override
// locales for uniform testing, clearing it after should let
// LANG and other locale settings operate normally
static constexpr const char* _kOverrideVarName{ "LC_ALL" };
static constexpr const char* _kTargetLocale{ "C.UTF-8" };
};
EnvLangGuard::EnvLangGuard()
{
int overwrite = 1;
int result = setenv(_kOverrideVarName, _kTargetLocale, overwrite);
if (result != 0)
{
LOG_VERBOSE("Could not update locale for font selection, some fonts may display incorrectly");
}
}
EnvLangGuard::~EnvLangGuard()
{
int result = unsetenv(_kOverrideVarName);
if (result != 0)
{
LOG_VERBOSE("Could not restore user locale");
}
}
std::string GetFolderPath(SpecialFolder folder)
{
switch (folder)
{
case SpecialFolder::userCache:
case SpecialFolder::userConfig:
case SpecialFolder::userData:
{
auto path = GetEnvironmentPath("XDG_CONFIG_HOME");
if (path.empty())
{
auto home = GetFolderPath(SpecialFolder::userHome);
path = Path::Combine(home, u8".config");
}
return path;
}
case SpecialFolder::userHome:
return GetHomePath();
default:
return std::string();
}
}
std::string GetDocsPath()
{
static const utf8* searchLocations[] = {
"./doc",
"/usr/share/doc/openrct2",
DOCDIR,
};
for (auto searchLocation : searchLocations)
{
LOG_VERBOSE("Looking for OpenRCT2 doc path at %s", searchLocation);
if (Path::DirectoryExists(searchLocation))
{
return searchLocation;
}
}
return std::string();
}
static std::string GetCurrentWorkingDirectory()
{
char cwdPath[PATH_MAX];
if (getcwd(cwdPath, sizeof(cwdPath)) != nullptr)
{
return cwdPath;
}
return std::string();
}
std::string GetInstallPath()
{
// 1. Try command line argument
if (!gCustomOpenRCT2DataPath.empty())
{
return Path::GetAbsolute(gCustomOpenRCT2DataPath);
}
// 2. Try {${exeDir},${cwd},/}/{data,standard system app directories}
// exeDir should come first to allow installing into build dir
// clang-format off
const std::string prefixes[]{
Path::GetDirectory(Platform::GetCurrentExecutablePath()),
GetCurrentWorkingDirectory(),
"/"
};
static constexpr u8string_view SearchLocations[] = {
"/data",
"../share/openrct2",
"/usr/local/share/openrct2",
"/var/lib/openrct2",
"/usr/share/openrct2",
};
// clang-format on
for (const auto& prefix : prefixes)
{
for (const auto searchLocation : SearchLocations)
{
auto prefixedPath = Path::Combine(prefix, searchLocation);
LOG_VERBOSE("Looking for OpenRCT2 data in %s", prefixedPath.c_str());
if (Path::DirectoryExists(prefixedPath))
{
return prefixedPath;
}
}
}
return "/";
}
std::string GetCurrentExecutablePath()
{
char exePath[PATH_MAX] = { 0 };
#ifdef __linux__
auto bytesRead = readlink("/proc/self/exe", exePath, sizeof(exePath));
if (bytesRead == -1)
{
LOG_FATAL("failed to read /proc/self/exe");
}
#elif defined(__FreeBSD__) || defined(__NetBSD__)
#if defined(__FreeBSD__)
const int32_t mib[] = {
CTL_KERN,
KERN_PROC,
KERN_PROC_PATHNAME,
-1,
};
#else
const int32_t mib[] = {
CTL_KERN,
KERN_PROC_ARGS,
-1,
KERN_PROC_PATHNAME,
};
#endif
auto exeLen = sizeof(exePath);
if (sysctl(mib, 4, exePath, &exeLen, nullptr, 0) == -1)
{
LOG_FATAL("failed to get process path");
}
#elif defined(__OpenBSD__) || defined(__EMSCRIPTEN__)
// There is no way to get the path name of a running executable.
// If you are not using the port or package, you may have to change this line!
strlcpy(exePath, "/usr/local/bin/", sizeof(exePath));
#else
#error "Platform does not support full path exe retrieval"
#endif
return exePath;
}
u8string StrDecompToPrecomp(u8string_view input)
{
return u8string(input);
}
bool HandleSpecialCommandLineArgument(const char* argument)
{
return false;
}
uint16_t GetLocaleLanguage()
{
const char* langString = setlocale(LC_MESSAGES, "");
if (langString != nullptr)
{
// The locale has the following form:
// language[_territory[.codeset]][@modifier]
// (see https://www.gnu.org/software/libc/manual/html_node/Locale-Names.html)
// longest on my system is 29 with codeset and modifier, so 32 for the pattern should be more than enough
char pattern[32];
// strip the codeset and modifier part
int32_t length = strlen(langString);
{
for (int32_t i = 0; i < length; ++i)
{
if (langString[i] == '.' || langString[i] == '@')
{
length = i;
break;
}
}
} // end strip
std::memcpy(pattern, langString, length); // copy all until first '.' or '@'
pattern[length] = '\0';
// find _ if present
const char* strip = strchr(pattern, '_');
if (strip != nullptr)
{
// could also use '-', but '?' is more flexible. Maybe LanguagesDescriptors will change.
// pattern is now "language?territory"
pattern[strip - pattern] = '?';
}
// Iterate through all available languages
for (int32_t i = 1; i < LANGUAGE_COUNT; ++i)
{
if (!fnmatch(pattern, LanguagesDescriptors[i].locale, 0))
{
return i;
}
}
// special case
if (fnmatch(pattern, "en_CA", 0) == 0)
{
return LANGUAGE_ENGLISH_US;
}
// no exact match found trying only language part
if (strip != nullptr)
{
pattern[strip - pattern] = '*';
pattern[strip - pattern + 1] = '\0'; // pattern is now "language*"
for (int32_t i = 1; i < LANGUAGE_COUNT; ++i)
{
if (!fnmatch(pattern, LanguagesDescriptors[i].locale, 0))
{
return i;
}
}
}
}
return LANGUAGE_ENGLISH_UK;
}
CurrencyType GetLocaleCurrency()
{
char* langstring = setlocale(LC_MONETARY, "");
if (langstring == nullptr)
{
return Platform::GetCurrencyValue(NULL);
}
struct lconv* lc = localeconv();
return Platform::GetCurrencyValue(lc->int_curr_symbol);
}
MeasurementFormat GetLocaleMeasurementFormat()
{
// LC_MEASUREMENT is GNU specific.
#ifdef LC_MEASUREMENT
const char* langstring = setlocale(LC_MEASUREMENT, "");
#else
const char* langstring = setlocale(LC_ALL, "");
#endif
if (langstring != nullptr)
{
// using https://en.wikipedia.org/wiki/Metrication#Chronology_and_status_of_conversion_by_country as reference
if (!fnmatch("*_US*", langstring, 0) || !fnmatch("*_MM*", langstring, 0) || !fnmatch("*_LR*", langstring, 0))
{
return MeasurementFormat::Imperial;
}
}
return MeasurementFormat::Metric;
}
std::string GetSteamPath()
{
const char* steamRoot = getenv("STEAMROOT");
if (steamRoot != nullptr)
{
return Path::Combine(steamRoot, u8"ubuntu12_32/steamapps/content");
}
const char* localSharePath = getenv("XDG_DATA_HOME");
if (localSharePath != nullptr)
{
auto steamPath = Path::Combine(localSharePath, u8"Steam/ubuntu12_32/steamapps/content");
if (Path::DirectoryExists(steamPath))
{
return steamPath;
}
}
const char* homeDir = getpwuid(getuid())->pw_dir;
if (homeDir == nullptr)
{
return {};
}
// Prefer new path for Steam, which is the default when using with Proton
auto steamPath = Path::Combine(homeDir, u8".local/share/Steam/steamapps/common");
if (Path::DirectoryExists(steamPath))
{
return steamPath;
}
// Fallback paths
steamPath = Path::Combine(homeDir, u8".local/share/Steam/ubuntu12_32/steamapps/content");
if (Path::DirectoryExists(steamPath))
{
return steamPath;
}
steamPath = Path::Combine(homeDir, u8".steam/steam/ubuntu12_32/steamapps/content");
if (Path::DirectoryExists(steamPath))
{
return steamPath;
}
return {};
}
u8string GetRCT1SteamDir()
{
return u8"Rollercoaster Tycoon Deluxe";
}
u8string GetRCT2SteamDir()
{
return u8"Rollercoaster Tycoon 2";
}
u8string GetRCTClassicSteamDir()
{
return u8"RollerCoaster Tycoon Classic";
}
std::vector<std::string_view> GetSearchablePathsRCT1()
{
return {
// game-data-packager uses this path when installing game files
"/usr/share/games/roller-coaster-tycoon",
};
}
std::vector<std::string_view> GetSearchablePathsRCT2()
{
return {
// game-data-packager uses this path when installing game files
"/usr/share/games/roller-coaster-tycoon2",
};
}
#ifndef DISABLE_TTF
std::string GetFontPath(const TTFFontDescriptor& font)
{
// set LANG to portable C.UTF-8 so font face names from fontconfig
// are reported in English
EnvLangGuard elg;
LOG_VERBOSE("Looking for font %s with FontConfig.", font.font_name);
FcConfig* config = FcInitLoadConfigAndFonts();
if (!config)
{
LOG_ERROR("Failed to initialize FontConfig library");
FcFini();
return {};
}
FcPattern* pat = FcNameParse(reinterpret_cast<const FcChar8*>(font.font_name));
FcConfigSubstitute(config, pat, FcMatchPattern);
FcDefaultSubstitute(pat);
std::string path = "";
FcResult result = FcResultNoMatch;
FcPattern* match = FcFontMatch(config, pat, &result);
if (match)
{
bool is_substitute = false;
// FontConfig implicitly falls back to any default font it is configured to handle.
// In our implementation, this cannot account for supported character sets, leading
// to unrendered characters (tofu) when trying to render e.g. CJK characters using a
// Western (sans-)serif font. We therefore ignore substitutions FontConfig provides,
// and instead rely on exact matches on the fonts predefined for each font family.
FcChar8* matched_font_face = nullptr;
if (FcPatternGetString(match, FC_FULLNAME, 0, &matched_font_face) == FcResultMatch
&& strcmp(font.font_name, reinterpret_cast<const char*>(matched_font_face)) != 0)
{
LOG_VERBOSE("FontConfig provided substitute font %s -- disregarding.", matched_font_face);
is_substitute = true;
}
FcChar8* filename = nullptr;
if (!is_substitute && FcPatternGetString(match, FC_FILE, 0, &filename) == FcResultMatch)
{
path = reinterpret_cast<utf8*>(filename);
LOG_VERBOSE("FontConfig provided font %s", filename);
}
FcPatternDestroy(match);
}
else
{
LOG_WARNING("Failed to find required font.");
}
FcPatternDestroy(pat);
FcConfigDestroy(config);
FcFini();
return path;
}
#endif // DISABLE_TTF
} // namespace OpenRCT2::Platform
#endif