diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 3f9e788e13..6bc9a3d1d8 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -13,6 +13,7 @@ - Fix: [#24975] The Corkscrew and LIM Launched (original bug) roller coaster quarter loop tunnels are too high. - Fix: [#25139] Steam locomotive and tram claxons can sound louder than normal when approaching crossings that span multiple tiles. - Fix: [#25190] Inserting a block brake while a coaster is simulating will cause the simulation to behave strangely. +- Fix: [#25206] System file picker does not show up on the Steam Deck. - Fix: [#25272] Text colour dropdown in the Banner window is too narrow, resulting in truncated labels. - Fix: [#25299] The Mine Train Coaster left large helix draws incorrect sprites at certain angles (original bug). - Fix: [#25328] Spiral Slide in Blackpool Pleasure Beach has its entrance and exit the wrong way round (bug in the original scenario). diff --git a/src/openrct2-ui/UiContext.Linux.cpp b/src/openrct2-ui/UiContext.Linux.cpp index b7ab3c0b94..7b2978d93a 100644 --- a/src/openrct2-ui/UiContext.Linux.cpp +++ b/src/openrct2-ui/UiContext.Linux.cpp @@ -20,6 +20,7 @@ #include #include #include + #include #include #include #include @@ -102,21 +103,22 @@ namespace OpenRCT2::Ui std::string executablePath; DialogType dtype = GetDialogApp(&executablePath); - switch (dtype) { case DialogType::kdialog: { - std::string cmd = String::stdFormat( - "%s --title \"OpenRCT2\" --msgbox \"%s\"", executablePath.c_str(), message.c_str()); - Platform::Execute(cmd); + const char* args[] = { + executablePath.c_str(), "--title", "OpenRCT2", "--msgbox", message.c_str(), nullptr + }; + Platform::Execute(args); break; } case DialogType::zenity: { - std::string cmd = String::stdFormat( - "%s --title=\"OpenRCT2\" --info --text=\"%s\"", executablePath.c_str(), message.c_str()); - Platform::Execute(cmd); + const char* args[] = { + executablePath.c_str(), "--title=OpenRCT2", "--info", "--text", message.c_str(), nullptr + }; + Platform::Execute(args); break; } default: @@ -127,15 +129,15 @@ namespace OpenRCT2::Ui void OpenFolder(const std::string& path) override { - std::string cmd = String::stdFormat("xdg-open %s", EscapePathForShell(path).c_str()); - Platform::Execute(cmd); + const char* args[] = { "xdg-open", path.c_str(), nullptr }; + Platform::Execute(args); } void OpenURL(const std::string& url) override { #ifndef __EMSCRIPTEN__ - std::string cmd = String::stdFormat("xdg-open %s", url.c_str()); - Platform::Execute(cmd); + const char* args[] = { "xdg-open", url.c_str(), nullptr }; + Platform::Execute(args); #else MAIN_THREAD_EM_ASM({ window.open(UTF8ToString($0)); }, url.c_str()); #endif @@ -145,19 +147,28 @@ namespace OpenRCT2::Ui { std::string result; std::string executablePath; - u8string directory = EscapePathForShell(desc.InitialDirectory + '/'); + u8string directory = desc.InitialDirectory + '/'; DialogType dtype = GetDialogApp(&executablePath); switch (dtype) { case DialogType::kdialog: { std::string action = (desc.Type == FileDialogType::Open) ? "--getopenfilename" : "--getsavefilename"; - std::string filter = GetKDialogFilterString(desc.Filters); - std::string cmd = String::stdFormat( - "%s --title '%s' %s %s '%s'", executablePath.c_str(), desc.Title.c_str(), action.c_str(), - directory.c_str(), filter.c_str()); + std::vector filters = GetKDialogFilterString(desc.Filters); + std::vector argsVec = { executablePath, "--title", desc.Title, action, directory }; + for (const auto& filter : filters) + { + argsVec.push_back(filter); + } + std::vector args; + for (const auto& arg : argsVec) + { + args.push_back(arg.c_str()); + } + args.push_back(nullptr); + std::string output; - if (Platform::Execute(cmd, &output) == 0) + if (Platform::Execute(args.data(), &output) == 0) { result = output; } @@ -166,17 +177,30 @@ namespace OpenRCT2::Ui case DialogType::zenity: { std::string action = "--file-selection"; - std::string flags; + std::vector filters = GetZenityFilterString(desc.Filters); + u8string directoryarg = String::stdFormat("--filename=%s", directory.c_str()); + std::vector argsVec = { executablePath, action, directoryarg }; if (desc.Type == FileDialogType::Save) { - flags = "--confirm-overwrite --save"; + argsVec.push_back("--confirm-overwrite"); + argsVec.push_back("--save"); } - std::string filters = GetZenityFilterString(desc.Filters); - std::string cmd = String::stdFormat( - "%s %s --filename=%s %s --title='%s' / %s", executablePath.c_str(), action.c_str(), directory.c_str(), - flags.c_str(), desc.Title.c_str(), filters.c_str()); + argsVec.push_back("--title"); + argsVec.push_back(desc.Title); + argsVec.push_back("/"); + for (const auto& filter : filters) + { + argsVec.push_back(filter); + } + std::vector args; + for (const auto& arg : argsVec) + { + args.push_back(arg.c_str()); + } + args.push_back(nullptr); + std::string output; - if (Platform::Execute(cmd, &output) == 0) + if (Platform::Execute(args.data(), &output) == 0) { if (desc.Type == FileDialogType::Save) { @@ -219,8 +243,9 @@ namespace OpenRCT2::Ui } if (desc.Type == FileDialogType::Save && access(result.c_str(), F_OK) != -1 && dtype == DialogType::kdialog) { - std::string cmd = String::stdFormat("%s --yesno \"Overwrite %s?\"", executablePath.c_str(), result.c_str()); - if (Platform::Execute(cmd) != 0) + u8string overwrite = String::stdFormat("Overwrite %s?", result.c_str()); + const char* args[] = { executablePath.c_str(), "--yesno", overwrite.c_str(), nullptr }; + if (Platform::Execute(args) != 0) { result = std::string(); } @@ -241,7 +266,9 @@ namespace OpenRCT2::Ui std::string output; std::string cmd = String::stdFormat( "%s --title '%s' --getexistingdirectory /", executablePath.c_str(), title.c_str()); - if (Platform::Execute(cmd, &output) == 0) + const char* args[] = { executablePath.c_str(), "--title", title.c_str(), + "--getexistingdirectory", "/", nullptr }; + if (Platform::Execute(args, &output) == 0) { result = output; } @@ -252,7 +279,9 @@ namespace OpenRCT2::Ui std::string output; std::string cmd = String::stdFormat( "%s --title='%s' --file-selection --directory /", executablePath.c_str(), title.c_str()); - if (Platform::Execute(cmd, &output) == 0) + const char* args[] = { executablePath.c_str(), "--title", title.c_str(), "--file-selection", + "--directory", "/", nullptr }; + if (Platform::Execute(args, &output) == 0) { result = output; } @@ -306,30 +335,49 @@ namespace OpenRCT2::Ui { case DialogType::zenity: { - auto sb = StringBuilder(); - sb.Append(String::stdFormat("zenity --list --column '' --width=%d --height=%d", width, height)); + std::vector argsVec = { executablePath, + "--list", + "--column", + "", + String::stdFormat("--width=%d", width), + String::stdFormat("--height=%d", height) }; for (const auto& option : options) { - sb.Append(String::stdFormat(" '%s'", option.c_str())); + argsVec.push_back(option); } - sb.Append(String::stdFormat(" --title '%s' --text '%s'", title.c_str(), text.c_str())); - + argsVec.push_back("--title"); + argsVec.push_back(title); + argsVec.push_back("--text"); + argsVec.push_back(text); + std::vector args; + for (const auto& arg : argsVec) + { + args.push_back(arg.c_str()); + } + args.push_back(nullptr); std::string buff; - Platform::Execute(sb.GetBuffer(), &buff); + Platform::Execute(args.data(), &buff); return std::find(options.begin(), options.end(), buff) - options.begin(); } case DialogType::kdialog: { - auto sb = StringBuilder(); - sb.Append(String::stdFormat("kdialog --geometry %dx%d --title '%s' --menu ", width, height, title.c_str())); - sb.Append(String::stdFormat(" '%s'", text.c_str())); + std::vector argsVec = { + executablePath, "--geometry", String::stdFormat("%dx%d", width, height), "--title", title, + "--menu", text + }; for (const auto& option : options) { - sb.Append(String::stdFormat(" '%s' '%s'", option.c_str(), option.c_str())); + argsVec.push_back(option); + argsVec.push_back(option); } - + std::vector args; + for (const auto& arg : argsVec) + { + args.push_back(arg.c_str()); + } + args.push_back(nullptr); std::string buff; - Platform::Execute(sb.GetBuffer(), &buff); + Platform::Execute(args.data(), &buff); return std::find(options.begin(), options.end(), buff) - options.begin(); } default: @@ -346,8 +394,6 @@ namespace OpenRCT2::Ui { // Prefer zenity as it offers more required features, e.g., overwrite // confirmation and selecting only existing files. - // Silence error output with 2> /dev/null to avoid confusion in the - // case where a user does not have zenity and/or kdialog. // OpenRCT2 will fall back to an SDL pop-up if the user has neither. if (Platform::FindApp("zenity", executablePath)) { @@ -360,35 +406,31 @@ namespace OpenRCT2::Ui return DialogType::none; } - static std::string GetKDialogFilterString(const std::vector filters) + static std::vector GetKDialogFilterString(const std::vector& filters) { - std::stringstream filtersb; - bool first = true; + std::vector filterStrings; for (const auto& filter : filters) { - if (!first) - { - filtersb << "\\n"; - } - first = false; - + std::stringstream filtersb; filtersb << filter.Name.c_str() << " ("; AddFilterCaseInsensitive(filtersb, filter.Pattern); filtersb << ")"; + filterStrings.push_back(filtersb.str()); } - return filtersb.str(); + return filterStrings; } - static std::string GetZenityFilterString(const std::vector filters) + static std::vector GetZenityFilterString(const std::vector& filters) { - std::stringstream filtersb; + std::vector filterStrings; for (const auto& filter : filters) { - filtersb << " --file-filter='" << filter.Name << " | "; + std::stringstream filtersb; + filtersb << "--file-filter=" << filter.Name << " | "; AddFilterCaseInsensitive(filtersb, filter.Pattern); - filtersb << "'"; + filterStrings.push_back(filtersb.str()); } - return filtersb.str(); + return filterStrings; } static void AddFilterCaseInsensitive(std::stringstream& stream, u8string pattern) @@ -418,15 +460,6 @@ namespace OpenRCT2::Ui uiContext.ShowMessageBox(dialogMissingWarning); throw std::runtime_error(dialogMissingWarning); } - - static std::string EscapePathForShell(std::string path) - { - for (size_t index = 0; (index = path.find('"', index)) != std::string::npos; index += 2) - { - path.replace(index, 1, "\\\""); - } - return '"' + path + '"'; - } }; std::unique_ptr CreatePlatformUiContext() diff --git a/src/openrct2/config/Config.cpp b/src/openrct2/config/Config.cpp index 417113b643..b3dcc5d27b 100644 --- a/src/openrct2/config/Config.cpp +++ b/src/openrct2/config/Config.cpp @@ -807,10 +807,9 @@ namespace OpenRCT2::Config LOG_ERROR("Please install innoextract to extract files from GOG."); return false; } - int32_t exit_status = Platform::Execute( - String::stdFormat( - "%s '%s' --exclude-temp --output-dir '%s'", path.c_str(), installerPath.c_str(), targetPath.c_str()), - &output); + const char* args[] = { path.c_str(), installerPath.c_str(), "--exclude-temp", "--silent", + "--output-dir", targetPath.c_str(), nullptr }; + int32_t exit_status = Platform::Execute(args, &output); LOG_INFO("Exit status %d", exit_status); return exit_status == 0; } diff --git a/src/openrct2/platform/Platform.Posix.cpp b/src/openrct2/platform/Platform.Posix.cpp index dfeda0de99..5329756f19 100644 --- a/src/openrct2/platform/Platform.Posix.cpp +++ b/src/openrct2/platform/Platform.Posix.cpp @@ -29,6 +29,7 @@ #include #include #include + #include #include // The name of the mutex used to prevent multiple instances of the game from running @@ -113,55 +114,101 @@ namespace OpenRCT2::Platform bool FindApp(std::string_view app, std::string* output) { - return Execute(String::stdFormat("which %s 2> /dev/null", std::string(app).c_str()), output) == 0; + std::string appStr = std::string(app); + const char* args[] = { appStr.c_str(), "--version", nullptr }; + int result = Execute(args, nullptr); + if (result == 0 && output) + { + *output = appStr; + } + return result == 0; } - int32_t Execute(std::string_view command, std::string* output) + int32_t Execute(const char* args[], std::string* output) { - #ifndef __EMSCRIPTEN__ - LOG_VERBOSE("executing \"%s\"...", std::string(command).c_str()); - FILE* fpipe = popen(std::string(command).c_str(), "r"); - if (fpipe == nullptr) + // Build the command string from args for logging + std::string commandLine; + if (args && args[0]) { + for (size_t i = 0; args[i]; ++i) + { + if (i > 0) + commandLine += " "; + commandLine += args[i]; + } + } + #ifndef __EMSCRIPTEN__ + int status; + pid_t pid1; + + LOG_INFO("Executing command: %s", commandLine.c_str()); + int fd_pipe[2]; + if (pipe(fd_pipe) != 0) + { /* create a pipe */ return -1; } - if (output != nullptr) - { - // Read output into buffer - std::vector outputBuffer; - char buffer[1024]; - size_t readBytes; - while ((readBytes = fread(buffer, 1, sizeof(buffer), fpipe)) > 0) + + pid1 = fork(); + if (pid1 == 0) + { /* child process */ + close(fd_pipe[0]); /* no reading from pipe */ + /* write stdout in pipe */ + if (dup2(fd_pipe[1], STDOUT_FILENO) == -1) { - outputBuffer.insert(outputBuffer.begin(), buffer, buffer + readBytes); + _exit(128); } - // Trim line breaks - size_t outputLength = outputBuffer.size(); - for (size_t i = outputLength - 1; i != SIZE_MAX; i--) - { - if (outputBuffer[i] == '\n') - { - outputLength = i; - } - else - { - break; - } - } - - // Convert to string - *output = std::string(outputBuffer.data(), outputLength); + /* const casting argv is fine: + * https://pubs.opengroup.org/onlinepubs/9699919799/functions/fexecve.html -> rational + */ + execvp(args[0], const_cast(args)); + _exit(129); + } + else if (pid1 < 0) + { /* fork() failed */ + return -128; } else - { - fflush(fpipe); - } + { /* parent process */ + close(fd_pipe[1]); /* no writing to the pipe */ + if (waitpid(pid1, &status, 0) != pid1) + { + return -errno; + } - // Return exit code - return pclose(fpipe); + if (!WIFEXITED(status)) + { + return -129; + } + + if (WEXITSTATUS(status) >= 128) + { + return -130 - WEXITSTATUS(status); + } + + // Optionally, read output from fd_pipe[0] if output != nullptr + if (output != nullptr) + { + std::vector outputBuffer; + char buffer[1024]; + ssize_t readBytes; + while ((readBytes = read(fd_pipe[0], buffer, sizeof(buffer))) > 0) + { + outputBuffer.insert(outputBuffer.end(), buffer, buffer + readBytes); + } + // Trim line breaks + size_t outputLength = outputBuffer.size(); + while (outputLength > 0 && outputBuffer[outputLength - 1] == '\n') + { + --outputLength; + } + *output = std::string(outputBuffer.data(), outputLength); + } + close(fd_pipe[0]); + return 0; /* success! */ + } #else - LOG_WARNING("Emscripten cannot execute processes. The commandline was '%s'.", std::string(command).c_str()); + LOG_WARNING("Emscripten cannot execute processes. The commandline was '%s'.", commandLine.c_str()); return -1; #endif // __EMSCRIPTEN__ } diff --git a/src/openrct2/platform/Platform.Win32.cpp b/src/openrct2/platform/Platform.Win32.cpp index 8da9216b3b..47e835a15f 100644 --- a/src/openrct2/platform/Platform.Win32.cpp +++ b/src/openrct2/platform/Platform.Win32.cpp @@ -494,7 +494,7 @@ namespace OpenRCT2::Platform return false; } - int32_t Execute(std::string_view command, std::string* output) + int32_t Execute(const char* args[], std::string* output) { LOG_WARNING("Execute() not implemented for Windows!"); return -1; diff --git a/src/openrct2/platform/Platform.h b/src/openrct2/platform/Platform.h index b2f3ccbdeb..00603ad076 100644 --- a/src/openrct2/platform/Platform.h +++ b/src/openrct2/platform/Platform.h @@ -92,7 +92,7 @@ namespace OpenRCT2::Platform RealWorldDate GetDateLocal(); bool FindApp(std::string_view app, std::string* output); - int32_t Execute(std::string_view command, std::string* output = nullptr); + int32_t Execute(const char* args[], std::string* output = nullptr); bool ProcessIsElevated(); float GetDefaultScale();