From f1c200a01675714e51548137f2365ab2a1a9977b Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Fri, 7 Jul 2023 23:48:41 +0000 Subject: [PATCH] Add '--title-id' parameter and desktop shortcut creation (#889) * Add '--title-id' launch option to launch titles by title id * Add title id column to game list * Add option to create game shortcuts Co-authored-by: Exzap <13877693+Exzap@users.noreply.github.com> --- src/config/CemuConfig.cpp | 2 + src/config/CemuConfig.h | 2 + src/config/LaunchSettings.cpp | 16 ++++ src/config/LaunchSettings.h | 2 + src/gui/MainWindow.cpp | 33 +++++-- src/gui/components/wxGameList.cpp | 138 ++++++++++++++++++++++++++++++ src/gui/components/wxGameList.h | 10 ++- 7 files changed, 195 insertions(+), 8 deletions(-) diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 5d99a23d..833ef1c2 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -100,6 +100,7 @@ void CemuConfig::Load(XMLConfigParser& parser) column_width.game_time = loadColumnSize("game_time_width", DefaultColumnSize::game_time); column_width.game_started = loadColumnSize("game_started_width", DefaultColumnSize::game_started); column_width.region = loadColumnSize("region_width", DefaultColumnSize::region); + column_width.title_id = loadColumnSize("title_id", DefaultColumnSize::title_id); recent_launch_files.clear(); auto launch_parser = parser.get("RecentLaunchFiles"); @@ -398,6 +399,7 @@ void CemuConfig::Save(XMLConfigParser& parser) gamelist.set("game_time_width", column_width.game_time); gamelist.set("game_started_width", column_width.game_started); gamelist.set("region_width", column_width.region); + gamelist.set("title_id", column_width.title_id); auto launch_files_parser = config.set("RecentLaunchFiles"); for (const auto& entry : recent_launch_files) diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index e17a9344..e90874ba 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -340,6 +340,7 @@ namespace DefaultColumnSize { game_time = 140u, game_started = 160u, region = 80u, + title_id = 160u }; }; @@ -427,6 +428,7 @@ struct CemuConfig uint32 game_time = DefaultColumnSize::game_time; uint32 game_started = DefaultColumnSize::game_started; uint32 region = DefaultColumnSize::region; + uint32 title_id = 0; } column_width{}; // graphics diff --git a/src/config/LaunchSettings.cpp b/src/config/LaunchSettings.cpp index 958ba459..92289edd 100644 --- a/src/config/LaunchSettings.cpp +++ b/src/config/LaunchSettings.cpp @@ -60,6 +60,7 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) ("version,v", "Displays the version of Cemu") ("game,g", po::wvalue(), "Path of game to launch") + ("title-id,t", po::value(), "Title ID of the title to be launched (overridden by --game)") ("mlc,m", po::wvalue(), "Custom mlc folder location") ("fullscreen,f", po::value()->implicit_value(true), "Launch games in fullscreen mode") @@ -133,6 +134,21 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) s_load_game_file = tmp; } + if (vm.count("title-id")) + { + auto title_param = vm["title-id"].as(); + try { + + if (title_param.starts_with('=')){ + title_param.erase(title_param.begin()); + } + s_load_title_id = std::stoull(title_param, nullptr, 16); + } + catch (std::invalid_argument const& e) + { + std::cerr << "Expected title_param ID as an unsigned 64-bit hexadecimal string\n"; + } + } if (vm.count("mlc")) { diff --git a/src/config/LaunchSettings.h b/src/config/LaunchSettings.h index a916679c..f87dc609 100644 --- a/src/config/LaunchSettings.h +++ b/src/config/LaunchSettings.h @@ -16,6 +16,7 @@ public: static bool HandleCommandline(const std::vector& args); static std::optional GetLoadFile() { return s_load_game_file; } + static std::optional GetLoadTitleID() {return s_load_title_id;} static std::optional GetMLCPath() { return s_mlc_path; } static std::optional RenderUpsideDownEnabled() { return s_render_upside_down; } @@ -35,6 +36,7 @@ public: private: inline static std::optional s_load_game_file{}; + inline static std::optional s_load_title_id{}; inline static std::optional s_mlc_path{}; inline static std::optional s_render_upside_down{}; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index a2e229c0..142d5ef3 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -305,7 +305,32 @@ MainWindow::MainWindow() #endif auto* main_sizer = new wxBoxSizer(wxVERTICAL); - if (!LaunchSettings::GetLoadFile().has_value()) + auto load_file = LaunchSettings::GetLoadFile(); + auto load_title_id = LaunchSettings::GetLoadTitleID(); + bool quick_launch = false; + + if (load_file) + { + MainWindow::RequestLaunchGame(load_file.value(), wxLaunchGameEvent::INITIATED_BY::COMMAND_LINE); + quick_launch = true; + } + else if (load_title_id) + { + TitleInfo info; + TitleId baseId; + if (CafeTitleList::FindBaseTitleId(load_title_id.value(), baseId) && CafeTitleList::GetFirstByTitleId(baseId, info)) + { + MainWindow::RequestLaunchGame(info.GetPath(), wxLaunchGameEvent::INITIATED_BY::COMMAND_LINE); + quick_launch = true; + } + else + { + wxString errorMsg = fmt::format("Title ID {:016x} not found", load_title_id.value()); + wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + + } + } + if (!quick_launch) { { m_main_panel = new wxPanel(this); @@ -328,7 +353,7 @@ MainWindow::MainWindow() } else { - // launching game via -g option. Dont setup or load game list + // launching game via -g or -t option. Don't set up or load game list m_game_list = nullptr; m_info_bar = nullptr; } @@ -350,10 +375,6 @@ MainWindow::MainWindow() Bind(wxEVT_OPEN_GRAPHIC_PACK, &MainWindow::OnGraphicWindowOpen, this); Bind(wxEVT_LAUNCH_GAME, &MainWindow::OnLaunchFromFile, this); - if (LaunchSettings::GetLoadFile().has_value()) - { - MainWindow::RequestLaunchGame(LaunchSettings::GetLoadFile().value(), wxLaunchGameEvent::INITIATED_BY::COMMAND_LINE); - } if (LaunchSettings::GDBStubEnabled()) { g_gdbstub = std::make_unique(config.gdb_port); diff --git a/src/gui/components/wxGameList.cpp b/src/gui/components/wxGameList.cpp index 6e761358..af992129 100644 --- a/src/gui/components/wxGameList.cpp +++ b/src/gui/components/wxGameList.cpp @@ -13,6 +13,9 @@ #include #include #include +#include +#include +#include #include #include @@ -30,6 +33,17 @@ #include "Cafe/IOSU/PDM/iosu_pdm.h" // for last played and play time +#if BOOST_OS_WINDOWS +// for shortcut creation +#include +#include +#include +#include +#include +#include +#include +#endif + // public events wxDEFINE_EVENT(wxEVT_OPEN_SETTINGS, wxCommandEvent); wxDEFINE_EVENT(wxEVT_GAMELIST_BEGIN_UPDATE, wxCommandEvent); @@ -79,6 +93,7 @@ wxGameList::wxGameList(wxWindow* parent, wxWindowID id) InsertColumn(ColumnGameTime, _("You've played"), wxLIST_FORMAT_LEFT, config.column_width.game_time); InsertColumn(ColumnGameStarted, _("Last played"), wxLIST_FORMAT_LEFT, config.column_width.game_started); InsertColumn(ColumnRegion, _("Region"), wxLIST_FORMAT_LEFT, config.column_width.region); + InsertColumn(ColumnTitleID, _("Title ID"), wxLIST_FORMAT_LEFT, config.column_width.title_id); const char transparent_bitmap[kIconWidth * kIconWidth * 4] = {0}; wxBitmap blank(transparent_bitmap, kIconWidth, kIconWidth); @@ -244,6 +259,8 @@ int wxGameList::GetColumnDefaultWidth(int column) return DefaultColumnSize::game_started; case ColumnRegion: return DefaultColumnSize::region; + case ColumnTitleID: + return DefaultColumnSize::title_id; default: return 80; } @@ -528,6 +545,7 @@ enum ContextMenuEntries kContextMenuStyleList, kContextMenuStyleIcon, kContextMenuStyleIconSmall, + kContextMenuCreateShortcut }; void wxGameList::OnContextMenu(wxContextMenuEvent& event) { @@ -568,6 +586,10 @@ void wxGameList::OnContextMenu(wxContextMenuEvent& event) menu.Append(kContextMenuEditGraphicPacks, _("&Edit graphic packs")); menu.Append(kContextMenuEditGameProfile, _("&Edit game profile")); + menu.AppendSeparator(); +#if BOOST_OS_LINUX || BOOST_OS_WINDOWS + menu.Append(kContextMenuCreateShortcut, _("&Create shortcut")); +#endif menu.AppendSeparator(); } } @@ -687,6 +709,11 @@ void wxGameList::OnContextMenuSelected(wxCommandEvent& event) (new GameProfileWindow(GetParent(), title_id))->Show(); break; } + case kContextMenuCreateShortcut: +#if BOOST_OS_LINUX || BOOST_OS_WINDOWS + CreateShortcut(gameInfo); +#endif + break; } } } @@ -729,6 +756,7 @@ void wxGameList::OnColumnRightClick(wxListEvent& event) ShowGameTime, ShowLastPlayed, ShowRegion, + ShowTitleId }; const int column = event.GetColumn(); wxMenu menu; @@ -744,6 +772,7 @@ void wxGameList::OnColumnRightClick(wxListEvent& event) menu.AppendCheckItem(ShowGameTime, _("Show &game time"))->Check(GetColumnWidth(ColumnGameTime) > 0); menu.AppendCheckItem(ShowLastPlayed, _("Show &last played"))->Check(GetColumnWidth(ColumnGameStarted) > 0); menu.AppendCheckItem(ShowRegion, _("Show ®ion"))->Check(GetColumnWidth(ColumnRegion) > 0); + menu.AppendCheckItem(ShowTitleId, _("Show &title ID"))->Check(GetColumnWidth(ColumnTitleID) > 0); menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { @@ -773,6 +802,9 @@ void wxGameList::OnColumnRightClick(wxListEvent& event) case ShowRegion: config.column_width.region = menu->IsChecked(ShowRegion) ? DefaultColumnSize::region : 0; break; + case ShowTitleId: + config.column_width.title_id = menu->IsChecked(ShowTitleId) ? DefaultColumnSize::title_id : 0; + break; case ResetWidth: { switch (column) @@ -797,6 +829,8 @@ void wxGameList::OnColumnRightClick(wxListEvent& event) case ColumnRegion: config.column_width.region = DefaultColumnSize::region; break; + case ColumnTitleID: + config.column_width.title_id = DefaultColumnSize::title_id; default: return; } @@ -836,6 +870,7 @@ void wxGameList::ApplyGameListColumnWidths() SetColumnWidth(ColumnGameTime, config.column_width.game_time); SetColumnWidth(ColumnGameStarted, config.column_width.game_started); SetColumnWidth(ColumnRegion, config.column_width.region); + SetColumnWidth(ColumnTitleID, config.column_width.title_id); AdjustLastColumnWidth(); } @@ -1003,6 +1038,7 @@ void wxGameList::OnGameEntryUpdatedByTitleId(wxTitleIdEvent& event) const auto region_text = fmt::format("{}", gameInfo.GetRegion()); SetItem(index, ColumnRegion, _(region_text)); + SetItem(index, ColumnTitleID, _(fmt::format("{:016x}", titleId))); } else if (m_style == Style::kIcons) { @@ -1189,3 +1225,105 @@ void wxGameList::DeleteCachedStrings() { m_name_cache.clear(); } + +#if BOOST_OS_LINUX || BOOST_OS_WINDOWS +void wxGameList::CreateShortcut(GameInfo2& gameInfo) { + const auto title_id = gameInfo.GetBaseTitleId(); + const auto title_name = gameInfo.GetTitleName(); + const auto exe_path = ActiveSettings::GetExecutablePath(); + +#if BOOST_OS_LINUX + const wxString desktop_entry_name = wxString::Format("%s.desktop", title_name); + wxFileDialog entry_dialog(this, _("Choose desktop entry location"), "~/.local/share/applications", desktop_entry_name, + "Desktop file (*.desktop)|*.desktop", wxFD_SAVE | wxFD_CHANGE_DIR | wxFD_OVERWRITE_PROMPT); +#elif BOOST_OS_WINDOWS + // Get '%APPDATA%\Microsoft\Windows\Start Menu\Programs' path + PWSTR user_shortcut_folder; + SHGetKnownFolderPath(FOLDERID_Programs, 0, NULL, &user_shortcut_folder); + const wxString shortcut_name = wxString::Format("%s.lnk", title_name); + wxFileDialog entry_dialog(this, _("Choose shortcut location"), _pathToUtf8(user_shortcut_folder), shortcut_name, + "Shortcut (*.lnk)|*.lnk", wxFD_SAVE | wxFD_CHANGE_DIR | wxFD_OVERWRITE_PROMPT); +#endif + const auto result = entry_dialog.ShowModal(); + if (result == wxID_CANCEL) + return; + const auto output_path = entry_dialog.GetPath(); + +#if BOOST_OS_LINUX + std::optional icon_path; + // Obtain and convert icon + { + m_icon_cache_mtx.lock(); + const auto icon_iter = m_icon_cache.find(title_id); + const auto result_index = (icon_iter != m_icon_cache.cend()) ? std::optional(icon_iter->second.first) : std::nullopt; + m_icon_cache_mtx.unlock(); + + // In most cases it should find it + if (!result_index){ + wxMessageBox("Icon is yet to load, so will not be used by the shortcut", "Warning", wxOK | wxCENTRE | wxICON_WARNING); + } + else { + const auto out_icon_dir = ActiveSettings::GetDataPath("icons"); + fs::create_directories(out_icon_dir); + icon_path = out_icon_dir / fmt::format("{:016x}.png", gameInfo.GetBaseTitleId()); + + auto image = m_image_list->GetIcon(result_index.value()).ConvertToImage(); + + wxFileOutputStream png_file(_pathToUtf8(icon_path.value())); + wxPNGHandler pngHandler; + pngHandler.SaveFile(&image, png_file, false); + } + } + const auto desktop_entry_string = + fmt::format("[Desktop Entry]\n" + "Name={}\n" + "Comment=Play {} on Cemu\n" + "Exec={} --title-id {:016x}\n" + "Icon={}\n" + "Terminal=false\n" + "Type=Application\n" + "Categories=Game;", + title_name, + title_name, + _pathToUtf8(exe_path), + title_id, + _pathToUtf8(icon_path.value_or(""))); + + std::ofstream output_stream(output_path); + if (!output_stream.good()) + { + const wxString errorMsg = fmt::format("Failed to save desktop entry to {}", output_path.utf8_string()); + wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + return; + } + output_stream << desktop_entry_string; + +#elif BOOST_OS_WINDOWS + IShellLinkW *shell_link; + HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink, reinterpret_cast(&shell_link)); + if (SUCCEEDED(hres)) + { + const auto description = wxString::Format("Play %s on Cemu", title_name); + const auto args = wxString::Format("-t %016llx", title_id); + + shell_link->SetPath(exe_path.wstring().c_str()); + shell_link->SetDescription(description.wc_str()); + shell_link->SetArguments(args.wc_str()); + shell_link->SetWorkingDirectory(exe_path.parent_path().wstring().c_str()); + // Use icon from Cemu exe for now since we can't embed icons into the shortcut + // in the future we could convert and store icons in AppData or ProgramData + shell_link->SetIconLocation(exe_path.wstring().c_str(), 0); + + IPersistFile *shell_link_file; + // save the shortcut + hres = shell_link->QueryInterface(IID_IPersistFile, reinterpret_cast(&shell_link_file)); + if (SUCCEEDED(hres)) + { + hres = shell_link_file->Save(output_path.wc_str(), TRUE); + shell_link_file->Release(); + } + shell_link->Release(); + } +#endif +} +#endif \ No newline at end of file diff --git a/src/gui/components/wxGameList.h b/src/gui/components/wxGameList.h index 7776d7f6..b285d259 100644 --- a/src/gui/components/wxGameList.h +++ b/src/gui/components/wxGameList.h @@ -10,6 +10,7 @@ #include #include #include +#include #include "util/helpers/Semaphore.h" class wxTitleIdEvent : public wxCommandEvent @@ -52,6 +53,10 @@ public: void ReloadGameEntries(bool cached = false); void DeleteCachedStrings(); +#if BOOST_OS_LINUX || BOOST_OS_WINDOWS + void CreateShortcut(GameInfo2& gameInfo); +#endif + long FindListItemByTitleId(uint64 title_id) const; void OnClose(wxCloseEvent& event); @@ -75,8 +80,9 @@ private: ColumnGameTime, ColumnGameStarted, ColumnRegion, - //ColumnFavorite, - ColumnCounts + ColumnTitleID, + //ColumnFavorite, + ColumnCounts, }; int s_last_column = ColumnName;