#include "gui/components/wxTitleManagerList.h" #include "gui/helpers/wxHelpers.h" #include "util/helpers/SystemException.h" #include "Cafe/TitleList/GameInfo.h" #include "Cafe/TitleList/TitleInfo.h" #include "Cafe/TitleList/TitleList.h" #include "gui/components/wxGameList.h" #include "gui/helpers/wxCustomEvents.h" #include "gui/helpers/wxHelpers.h" #include #include #include #include #include #include #include #include #include #include "../wxHelper.h" #include #include "config/ActiveSettings.h" #include "gui/ChecksumTool.h" #include "gui/MainWindow.h" #include "Cafe/TitleList/TitleId.h" #include "Cafe/TitleList/SaveList.h" #include "Cafe/TitleList/TitleList.h" #include #include #include "Common/FileStream.h" wxDEFINE_EVENT(wxEVT_TITLE_FOUND, wxCommandEvent); wxDEFINE_EVENT(wxEVT_TITLE_REMOVED, wxCommandEvent); wxDEFINE_EVENT(wxEVT_REMOVE_ENTRY, wxCommandEvent); wxTitleManagerList::wxTitleManagerList(wxWindow* parent, wxWindowID id) : wxListCtrl(parent, id, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_VIRTUAL) { AddColumns(); // tooltip TODO: extract class mb wxPanelTooltip m_tooltip_window = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNO_BORDER); auto* tooltip_sizer = new wxBoxSizer(wxVERTICAL); m_tooltip_text = new wxStaticText(m_tooltip_window, wxID_ANY, wxEmptyString); tooltip_sizer->Add(m_tooltip_text , 0, wxALL, 5); m_tooltip_window->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK)); m_tooltip_window->SetSizerAndFit(tooltip_sizer); m_tooltip_window->Hide(); m_tooltip_timer = new wxTimer(this); Bind(wxEVT_LIST_COL_CLICK, &wxTitleManagerList::OnColumnClick, this); Bind(wxEVT_CONTEXT_MENU, &wxTitleManagerList::OnContextMenu, this); Bind(wxEVT_LIST_ITEM_SELECTED, &wxTitleManagerList::OnItemSelected, this); Bind(wxEVT_TIMER, &wxTitleManagerList::OnTimer, this); Bind(wxEVT_REMOVE_ITEM, &wxTitleManagerList::OnRemoveItem, this); Bind(wxEVT_REMOVE_ENTRY, &wxTitleManagerList::OnRemoveEntry, this); Bind(wxEVT_TITLE_FOUND, &wxTitleManagerList::OnTitleDiscovered, this); Bind(wxEVT_TITLE_REMOVED, &wxTitleManagerList::OnTitleRemoved, this); Bind(wxEVT_CLOSE_WINDOW, &wxTitleManagerList::OnClose, this); m_callbackIdTitleList = CafeTitleList::RegisterCallback([](CafeTitleListCallbackEvent* evt, void* ctx) { ((wxTitleManagerList*)ctx)->HandleTitleListCallback(evt); }, this); m_callbackIdSaveList = CafeSaveList::RegisterCallback([](CafeSaveListCallbackEvent* evt, void* ctx) { ((wxTitleManagerList*)ctx)->HandleSaveListCallback(evt); }, this); } wxTitleManagerList::~wxTitleManagerList() { CafeSaveList::UnregisterCallback(m_callbackIdSaveList); CafeTitleList::UnregisterCallback(m_callbackIdTitleList); } boost::optional wxTitleManagerList::GetSelectedTitleEntry() const { const auto selection = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); if (selection != wxNOT_FOUND) { const auto tmp = GetTitleEntry(selection); if (tmp.has_value()) return tmp.value(); } return {}; } boost::optional wxTitleManagerList::GetSelectedTitleEntry() { const auto selection = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); if (selection != wxNOT_FOUND) { const auto tmp = GetTitleEntry(selection); if (tmp.has_value()) return tmp.value(); } return {}; } //boost::optional wxTitleManagerList::GetTitleEntry(EntryType type, uint64 titleid) //{ // for(const auto& v : m_data) // { // if (v->entry.title_id == titleid && v->entry.type == type) // return v->entry; // } // // return {}; //} boost::optional wxTitleManagerList::GetTitleEntryByUID(uint64 uid) { for (const auto& v : m_data) { if (v->entry.location_uid == uid) return v->entry; } return {}; } void wxTitleManagerList::AddColumns() { wxListItem col0; col0.SetId(ColumnTitleId); col0.SetText(_("Title ID")); col0.SetWidth(120); InsertColumn(ColumnTitleId, col0); wxListItem col1; col1.SetId(ColumnName); col1.SetText(_("Name")); col1.SetWidth(435); InsertColumn(ColumnName, col1); wxListItem col2; col2.SetId(ColumnType); col2.SetText(_("Type")); col2.SetWidth(65); InsertColumn(ColumnType, col2); wxListItem col3; col3.SetId(ColumnVersion); col3.SetText(_("Version")); col3.SetWidth(40); InsertColumn(ColumnVersion, col3); wxListItem col4; col4.SetId(ColumnRegion); col4.SetText(_("Region")); col4.SetWidth(60); InsertColumn(ColumnRegion, col4); wxListItem col5; col5.SetId(ColumnFormat); col5.SetText(_("Format")); col5.SetWidth(63); InsertColumn(ColumnFormat, col5); wxListItem col6; col6.SetId(ColumnLocation); col6.SetText(_("Location")); col6.SetWidth(63); InsertColumn(ColumnLocation, col6); } wxString wxTitleManagerList::OnGetItemText(long item, long column) const { if (item >= GetItemCount()) return wxEmptyString; const auto entry = GetTitleEntry(item); if (entry.has_value()) return GetTitleEntryText(entry.value(), (ItemColumn)column); return wxEmptyString; } wxItemAttr* wxTitleManagerList::OnGetItemAttr(long item) const { static wxColour bgColourPrimary = GetBackgroundColour(); static wxColour bgColourSecondary = wxHelper::CalculateAccentColour(bgColourPrimary); static wxListItemAttr s_primary_attr(GetTextColour(), bgColourPrimary, GetFont()); static wxListItemAttr s_secondary_attr(GetTextColour(), bgColourSecondary, GetFont()); return item % 2 == 0 ? &s_primary_attr : &s_secondary_attr; } boost::optional wxTitleManagerList::GetTitleEntry(long item) { long counter = 0; for (const auto& data : m_sorted_data) { if (!data.get().visible) continue; if (item != counter++) continue; return data.get().entry; } return {}; } boost::optional wxTitleManagerList::GetTitleEntry(const fs::path& path) const { const auto tmp = _pathToUtf8(path); for (const auto& data : m_data) { if (boost::iequals(_pathToUtf8(data->entry.path), tmp)) return data->entry; } return {}; } boost::optional wxTitleManagerList::GetTitleEntry(const fs::path& path) { const auto tmp = _pathToUtf8(path); for (const auto& data : m_data) { if (boost::iequals(_pathToUtf8(data->entry.path), tmp)) return data->entry; } return {}; } boost::optional wxTitleManagerList::GetTitleEntry(long item) const { long counter = 0; for (const auto& data : m_sorted_data) { if (!data.get().visible) continue; if (item != counter++) continue; return data.get().entry; } return {}; } void wxTitleManagerList::OnConvertToCompressedFormat(uint64 titleId, uint64 rightClickedUID) { TitleInfo titleInfo_base; TitleInfo titleInfo_update; TitleInfo titleInfo_aoc; titleId = TitleIdParser::MakeBaseTitleId(titleId); // if the titleId of a separate update is selected, this converts it back to the base titleId TitleIdParser titleIdParser(titleId); bool hasBaseTitleId = titleIdParser.GetType() != TitleIdParser::TITLE_TYPE::AOC; bool hasUpdateTitleId = titleIdParser.CanHaveSeparateUpdateTitleId(); TitleId updateTitleId = hasUpdateTitleId ? titleIdParser.GetSeparateUpdateTitleId() : 0; // todo - AOC titleIds might differ from the base/update game in other bits than the type. We have to use the meta data from the base/update to match aoc to the base title id // for now we just assume they match TitleId aocTitleId; if (hasBaseTitleId) aocTitleId = (titleId & (uint64)~0xFF00000000) | (uint64)0xC00000000; else aocTitleId = titleId; // find base and update for (const auto& data : m_data) { if (hasBaseTitleId && data->entry.title_id == titleId) { if (!titleInfo_base.IsValid()) { titleInfo_base = CafeTitleList::GetTitleInfoByUID(data->entry.location_uid); if(data->entry.location_uid == rightClickedUID) break; // prefer the users selection } } if (hasUpdateTitleId && data->entry.title_id == updateTitleId) { if (!titleInfo_update.IsValid()) { titleInfo_update = CafeTitleList::GetTitleInfoByUID(data->entry.location_uid); if(data->entry.location_uid == rightClickedUID) break; } else { // if multiple updates are present use the newest one if (titleInfo_update.GetAppTitleVersion() < data->entry.version) titleInfo_update = CafeTitleList::GetTitleInfoByUID(data->entry.location_uid); if(data->entry.location_uid == rightClickedUID) break; } } } // find AOC for (const auto& data : m_data) { if (data->entry.title_id == aocTitleId) { titleInfo_aoc = CafeTitleList::GetTitleInfoByUID(data->entry.location_uid); if(data->entry.location_uid == rightClickedUID) break; } } wxString msg = _("The following content will be converted to a compressed Wii U archive file (.wua):"); msg.append("\n \n"); if (titleInfo_base.IsValid()) msg.append(formatWxString(_("Base game:\n{}"), titleInfo_base.GetPrintPath())); else msg.append(_("Base game:\nNot installed")); msg.append("\n\n"); if (titleInfo_update.IsValid()) msg.append(formatWxString(_("Update:\n{}"), titleInfo_update.GetPrintPath())); else msg.append(_("Update:\nNot installed")); msg.append("\n\n"); if (titleInfo_aoc.IsValid()) msg.append(formatWxString(_("DLC:\n{}"), titleInfo_aoc.GetPrintPath())); else msg.append(_("DLC:\nNot installed")); const int answer = wxMessageBox(msg, _("Confirmation"), wxOK | wxCANCEL | wxCENTRE | wxICON_QUESTION, this); if (answer != wxOK) return; std::vector titlesToConvert; if (titleInfo_base.IsValid()) titlesToConvert.emplace_back(&titleInfo_base); if (titleInfo_update.IsValid()) titlesToConvert.emplace_back(&titleInfo_update); if (titleInfo_aoc.IsValid()) titlesToConvert.emplace_back(&titleInfo_aoc); if (titlesToConvert.empty()) return; // get short name CafeConsoleLanguage languageId = CafeConsoleLanguage::EN; // todo - use user's locale std::string shortName; if (titleInfo_base.IsValid()) shortName = titleInfo_base.GetMetaInfo()->GetShortName(languageId); else if (titleInfo_update.IsValid()) shortName = titleInfo_update.GetMetaInfo()->GetShortName(languageId); else if (titleInfo_aoc.IsValid()) shortName = titleInfo_aoc.GetMetaInfo()->GetShortName(languageId); if (!shortName.empty()) { boost::replace_all(shortName, ":", ""); } // for the default output directory we use the first game path configured by the user std::wstring defaultDir = L""; if (!GetConfig().game_paths.empty()) defaultDir = GetConfig().game_paths.front(); // get the short name, which we will use as a suggested default file name std::string defaultFileName = std::move(shortName); boost::replace_all(defaultFileName, "/", ""); boost::replace_all(defaultFileName, "\\", ""); CafeConsoleRegion region = CafeConsoleRegion::Auto; if (titleInfo_base.IsValid() && titleInfo_base.HasValidXmlInfo()) region = titleInfo_base.GetMetaInfo()->GetRegion(); else if (titleInfo_update.IsValid() && titleInfo_update.HasValidXmlInfo()) region = titleInfo_update.GetMetaInfo()->GetRegion(); if (region == CafeConsoleRegion::JPN) defaultFileName.append(" (JP)"); else if (region == CafeConsoleRegion::EUR) defaultFileName.append(" (EU)"); else if (region == CafeConsoleRegion::USA) defaultFileName.append(" (US)"); if (titleInfo_update.IsValid()) { defaultFileName.append(fmt::format(" (v{})", titleInfo_update.GetAppTitleVersion())); } defaultFileName.append(".wua"); // ask the user to provide a path for the output file wxFileDialog saveFileDialog(this, _("Save Wii U game archive file"), defaultDir, wxHelper::FromUtf8(defaultFileName), "WUA files (*.wua)|*.wua", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (saveFileDialog.ShowModal() == wxID_CANCEL || saveFileDialog.GetPath().IsEmpty()) return; fs::path outputPath(wxHelper::MakeFSPath(saveFileDialog.GetPath())); fs::path outputPathTmp(wxHelper::MakeFSPath(saveFileDialog.GetPath().append("__tmp"))); struct ZArchiveWriterContext { static void NewOutputFile(const int32_t partIndex, void* _ctx) { ZArchiveWriterContext* ctx = (ZArchiveWriterContext*)_ctx; ctx->fs = FileStream::createFile2(ctx->outputPath); if (!ctx->fs) ctx->isValid = false; } static void WriteOutputData(const void* data, size_t length, void* _ctx) { ZArchiveWriterContext* ctx = (ZArchiveWriterContext*)_ctx; if (ctx->fs) ctx->fs->writeData(data, length); } bool RecursivelyCountFiles(const std::string& fscPath) { sint32 fscStatus; std::unique_ptr vfDir(fsc_openDirIterator(fscPath.c_str(), &fscStatus)); if (!vfDir) return false; if (cancelled) return false; FSCDirEntry dirEntry; while (fsc_nextDir(vfDir.get(), &dirEntry)) { if (dirEntry.isFile) { totalInputFileSize += (uint64)dirEntry.fileSize; totalFileCount++; } else if (dirEntry.isDirectory) { if (!RecursivelyCountFiles(fmt::format("{}{}/", fscPath, dirEntry.path))) { return false; } } else cemu_assert_unimplemented(); } return true; } bool RecursivelyAddFiles(std::string archivePath, std::string fscPath) { sint32 fscStatus; std::unique_ptr vfDir(fsc_openDirIterator(fscPath.c_str(), &fscStatus)); if (!vfDir) return false; if (cancelled) return false; zaWriter->MakeDir(archivePath.c_str(), false); FSCDirEntry dirEntry; while (fsc_nextDir(vfDir.get(), &dirEntry)) { if (dirEntry.isFile) { zaWriter->StartNewFile((archivePath + dirEntry.path).c_str()); std::unique_ptr vFile(fsc_open((fscPath + dirEntry.path).c_str(), FSC_ACCESS_FLAG::OPEN_FILE | FSC_ACCESS_FLAG::READ_PERMISSION, &fscStatus)); if (!vFile) return false; transferBuffer.resize(32 * 1024); // 32KB uint32 readBytes; while (true) { readBytes = vFile->fscReadData(transferBuffer.data(), transferBuffer.size()); if(readBytes == 0) break; zaWriter->AppendData(transferBuffer.data(), readBytes); if (cancelled) return false; transferredInputBytes += readBytes; } currentFileIndex++; } else if (dirEntry.isDirectory) { if (!RecursivelyAddFiles(fmt::format("{}{}/", archivePath, dirEntry.path), fmt::format("{}{}/", fscPath, dirEntry.path))) return false; } else cemu_assert_unimplemented(); } return true; } bool LoadTitleMetaAndCountFiles(TitleInfo* titleInfo) { std::string temporaryMountPath = TitleInfo::GetUniqueTempMountingPath(); titleInfo->Mount(temporaryMountPath.c_str(), "", FSC_PRIORITY_BASE); bool r = RecursivelyCountFiles(temporaryMountPath); titleInfo->Unmount(temporaryMountPath.c_str()); return r; } bool StoreTitle(TitleInfo* titleInfo) { std::string temporaryMountPath = TitleInfo::GetUniqueTempMountingPath(); titleInfo->Mount(temporaryMountPath.c_str(), "", FSC_PRIORITY_BASE); bool r = RecursivelyAddFiles(fmt::format("{:016x}_v{}/", titleInfo->GetAppTitleId(), titleInfo->GetAppTitleVersion()), temporaryMountPath); titleInfo->Unmount(temporaryMountPath.c_str()); return r; } bool AddTitles(TitleInfo** titles, size_t count) { currentFileIndex = 0; totalFileCount = 0; // count files for (size_t i = 0; i < count; i++) { if (!LoadTitleMetaAndCountFiles(titles[i])) return false; if (cancelled) return false; } // store files for (size_t i = 0; i < count; i++) { if (!StoreTitle(titles[i])) return false; } return true; } ~ZArchiveWriterContext() { delete fs; delete zaWriter; }; fs::path outputPath; FileStream* fs; ZArchiveWriter* zaWriter{}; bool isValid{false}; std::vector transferBuffer; std::atomic_bool cancelled{false}; // progress std::atomic_uint32_t totalFileCount{}; std::atomic_uint32_t currentFileIndex{}; std::atomic_uint64_t totalInputFileSize{}; std::atomic_uint64_t transferredInputBytes{}; }writerContext; // mount and store writerContext.isValid = true; writerContext.outputPath = outputPathTmp; writerContext.zaWriter = new ZArchiveWriter(&ZArchiveWriterContext::NewOutputFile, &ZArchiveWriterContext::WriteOutputData, &writerContext); if (!writerContext.isValid) { // failed to create file wxMessageBox(_("Unable to create file"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); return; } // open progress dialog wxGenericProgressDialog progressDialog("Converting to .wua", _("Counting files..."), 100, // range this, // parent wxPD_CAN_ABORT ); progressDialog.Show(); auto asyncWorker = std::async(std::launch::async, &ZArchiveWriterContext::AddTitles, &writerContext, titlesToConvert.data(), titlesToConvert.size()); while (!future_is_ready(asyncWorker)) { if (writerContext.cancelled) { progressDialog.Update(0, _("Stopping...")); } else if (writerContext.currentFileIndex != 0) { uint64 numSizeCompleted = writerContext.transferredInputBytes; uint64 numSizeTotal = writerContext.totalInputFileSize; uint32 pct = (uint32)(numSizeCompleted * (uint64)100 / numSizeTotal); pct = std::min(pct, (uint32)100); if (pct >= 100) pct = 99; // never set it to 100 as progress == total will make .Update() call ShowModal() and lock up this loop std::string textSuffix = fmt::format(" ({}MiB/{}MiB)", numSizeCompleted / 1024 / 1024, numSizeTotal / 1024 / 1024); progressDialog.Update(pct, _("Converting files...") + textSuffix); } else { progressDialog.Update(0, _("Collecting list of files..." + fmt::format(" ({})", writerContext.totalFileCount.load()))); } if (progressDialog.WasCancelled()) writerContext.cancelled.store(true); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } progressDialog.Update(100-1, _("Finalizing...")); bool r = asyncWorker.get(); if (!r) { delete writerContext.fs; writerContext.fs = nullptr; std::error_code ec; fs::remove(outputPathTmp, ec); return; } writerContext.zaWriter->Finalize(); delete writerContext.fs; writerContext.fs = nullptr; // verify the created WUA file ZArchiveReader* zreader = ZArchiveReader::OpenFromFile(outputPathTmp); if (!zreader) { wxMessageBox(_("Conversion failed\n"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); std::error_code ec; fs::remove(outputPathTmp, ec); return; } // todo - do a quick verification here delete zreader; // finish progressDialog.Hide(); fs::rename(outputPathTmp, outputPath); // ask user if they want to delete the original titles // todo CafeTitleList::Refresh(); wxMessageBox(_("Conversion finished\n"), _("Complete"), wxOK | wxCENTRE | wxICON_INFORMATION, this); } void wxTitleManagerList::OnClose(wxCloseEvent& event) { event.Skip(); // wait until all tasks are complete if (m_context_worker.valid()) m_context_worker.get(); } void wxTitleManagerList::OnColumnClick(wxListEvent& event) { const int column = event.GetColumn(); SortEntries(column); event.Skip(); } void wxTitleManagerList::RemoveItem(long item) { const int item_count = GetItemCount(); const ItemData* ref = nullptr; long counter = 0; for(auto it = m_sorted_data.begin(); it != m_sorted_data.end(); ++it) { if (!it->get().visible) continue; if (item != counter++) continue; ref = &(it->get()); m_sorted_data.erase(it); break; } // shouldn't happen if (ref == nullptr) return; for(auto it = m_data.begin(); it != m_data.end(); ++it) { if (ref != (*it).get()) continue; m_data.erase(it); break; } SetItemCount(std::max(0, item_count - 1)); RefreshPage(); } void wxTitleManagerList::RemoveItem(const TitleEntry& entry) { const int item_count = GetItemCount(); const TitleEntry* ref = &entry; for (auto it = m_sorted_data.begin(); it != m_sorted_data.end(); ++it) { if (ref != &it->get().entry) continue; m_sorted_data.erase(it); break; } for (auto it = m_data.begin(); it != m_data.end(); ++it) { if (ref != &(*it).get()->entry) continue; m_data.erase(it); break; } SetItemCount(std::max(0, item_count - 1)); RefreshPage(); } void wxTitleManagerList::OnItemSelected(wxListEvent& event) { event.Skip(); m_tooltip_timer->Stop(); const auto selection = event.GetIndex(); if (selection == wxNOT_FOUND) { m_tooltip_window->Hide(); return; } const auto entry = GetTitleEntry(selection); if (!entry.has_value()) { m_tooltip_window->Hide(); return; } m_tooltip_window->Hide(); return; //const auto mouse_position = wxGetMousePosition() - GetScreenPosition(); //m_tooltip_window->SetPosition(wxPoint(mouse_position.x + 15, mouse_position.y + 15)); //wxString msg; //switch(entry->error) //{ //case TitleError::WrongBaseLocation: // msg = _("This base game is installed at the wrong location."); // break; //case TitleError::WrongUpdateLocation: // msg = _("This update is installed at the wrong location."); // break; //case TitleError::WrongDlcLocation: // msg = _("This DLC is installed at the wrong location."); // break; //default: // return;; //} //m_tooltip_text->SetLabel(formatWxString("{}\n{}", msg, _("You can use the context menu to fix it."))); //m_tooltip_window->Fit(); //m_tooltip_timer->StartOnce(250); } enum ContextMenuEntries { kContextMenuOpenDirectory = wxID_HIGHEST + 1, kContextMenuDelete, kContextMenuLaunch, kContextMenuVerifyGameFiles, kContextMenuConvertToWUA, }; void wxTitleManagerList::OnContextMenu(wxContextMenuEvent& event) { // still doing work if (m_context_worker.valid() && !future_is_ready(m_context_worker)) return; wxMenu menu; menu.Bind(wxEVT_COMMAND_MENU_SELECTED, &wxTitleManagerList::OnContextMenuSelected, this); const auto selection = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); if (selection == wxNOT_FOUND) return; const auto entry = GetTitleEntry(selection); if (!entry.has_value()) return; if(entry->type == EntryType::Base) menu.Append(kContextMenuLaunch, _("&Launch title")); menu.Append(kContextMenuOpenDirectory, _("&Open directory")); if (entry->type != EntryType::Save) menu.Append(kContextMenuVerifyGameFiles, _("&Verify integrity of game files")); menu.AppendSeparator(); if (entry->type != EntryType::Save && entry->format != EntryFormat::WUA) { menu.Append(kContextMenuConvertToWUA, _("Convert to compressed Wii U archive (.wua)")); menu.AppendSeparator(); } menu.Append(kContextMenuDelete, _("&Delete")); PopupMenu(&menu); // TODO: fix tooltip position } bool wxTitleManagerList::DeleteEntry(long index, const TitleEntry& entry) { wxDTorFunc reset_text(wxQueueEvent, this, new wxSetStatusBarTextEvent(wxEmptyString, 1)); wxQueueEvent(this, new wxSetStatusBarTextEvent("Deleting entry...", 1)); wxString msg; const bool is_directory = fs::is_directory(entry.path); if(is_directory) msg = formatWxString(_("Are you really sure that you want to delete the following folder:\n{}"), _pathToUtf8(entry.path)); else msg = formatWxString(_("Are you really sure that you want to delete the following file:\n{}"), _pathToUtf8(entry.path)); const auto result = wxMessageBox(msg, _("Warning"), wxYES_NO | wxCENTRE | wxICON_EXCLAMATION, this); if (result == wxNO) return false; std::error_code ec; if (is_directory) { if (entry.type != EntryType::Save) { // delete content, meta, code folders first const auto content = entry.path / "content"; fs::remove_all(content, ec); const auto meta = entry.path / "meta"; fs::remove_all(meta, ec); const auto code = entry.path / "code"; fs::remove_all(code, ec); } else { // delete meta, user folders first const auto meta = entry.path / "meta"; fs::remove_all(meta, ec); const auto user = entry.path / "user"; fs::remove_all(user, ec); } // check if folder is empty if(fs::is_empty(entry.path, ec)) fs::remove_all(entry.path, ec); } else // simply remove file fs::remove(entry.path, ec); if(ec) { const auto error_msg = formatWxString(_("Error when trying to delete the entry:\n{}"), GetSystemErrorMessage(ec)); wxMessageBox(error_msg, _("Error"), wxOK|wxCENTRE, this); return false; } // thread safe request to delete entry const auto evt = new wxCommandEvent(wxEVT_REMOVE_ITEM); evt->SetInt(index); wxQueueEvent(this, evt); return true; } void wxTitleManagerList::OnContextMenuSelected(wxCommandEvent& event) { // still doing work if (m_context_worker.valid() && !future_is_ready(m_context_worker)) return; const auto selection = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); if (selection == wxNOT_FOUND) return; const auto entry = GetTitleEntry(selection); if (!entry.has_value()) return; switch (event.GetId()) { case kContextMenuOpenDirectory: { const auto path = fs::is_directory(entry->path) ? entry->path : entry->path.parent_path(); wxLaunchDefaultBrowser(wxHelper::FromUtf8(fmt::format("file:{}", _pathToUtf8(path)))); } break; case kContextMenuDelete: m_context_worker = std::async(std::launch::async, &wxTitleManagerList::DeleteEntry, this, selection, entry.value()); break; case kContextMenuLaunch: { try { MainWindow::RequestLaunchGame(entry->path, wxLaunchGameEvent::INITIATED_BY::TITLE_MANAGER); Close(); } catch (const std::exception& ex) { cemuLog_log(LogType::Force, "wxTitleManagerList::OnContextMenuSelected: can't launch title: {}", ex.what()); } } break; case kContextMenuVerifyGameFiles: (new ChecksumTool(this, entry.value()))->Show(); break; case kContextMenuConvertToWUA: OnConvertToCompressedFormat(entry.value().title_id, entry.value().location_uid); break; } } void wxTitleManagerList::OnTimer(wxTimerEvent& event) { if(event.GetTimer().GetId() != m_tooltip_timer->GetId()) { event.Skip(); return; } m_tooltip_window->Show(); } void wxTitleManagerList::OnRemoveItem(wxCommandEvent& event) { RemoveItem(event.GetInt()); } void wxTitleManagerList::OnRemoveEntry(wxCommandEvent& event) { wxASSERT(event.GetClientData() != nullptr); RemoveItem(*(TitleEntry*)event.GetClientData()); } wxString wxTitleManagerList::GetTitleEntryText(const TitleEntry& entry, ItemColumn column) { switch (column) { case ColumnTitleId: return formatWxString("{:08x}-{:08x}", (uint32) (entry.title_id >> 32), (uint32) (entry.title_id & 0xFFFFFFFF)); case ColumnName: return entry.name; case ColumnType: return GetTranslatedTitleEntryType(entry.type); case ColumnVersion: return formatWxString("{}", entry.version); case ColumnRegion: return wxGetTranslation(fmt::format("{}", entry.region)); case ColumnFormat: { if (entry.type == EntryType::Save) return _("Save folder"); switch (entry.format) { case wxTitleManagerList::EntryFormat::Folder: return _("Folder"); case wxTitleManagerList::EntryFormat::WUD: return _("WUD"); case wxTitleManagerList::EntryFormat::WUA: return _("WUA"); } return ""; } case ColumnLocation: { const auto relative_mlc_path = entry.path.lexically_relative(ActiveSettings::GetMlcPath()).string(); if (relative_mlc_path.starts_with("usr") || relative_mlc_path.starts_with("sys")) return _("MLC"); else return _("Game Paths"); } default: UNREACHABLE; } return wxEmptyString; } wxString wxTitleManagerList::GetTranslatedTitleEntryType(EntryType type) { switch (type) { case EntryType::Base: return _("base"); case EntryType::Update: return _("update"); case EntryType::Dlc: return _("DLC"); case EntryType::Save: return _("save"); case EntryType::System: return _("system"); default: return std::to_string(static_cast>(type)); } } void wxTitleManagerList::HandleTitleListCallback(CafeTitleListCallbackEvent* evt) { if (evt->eventType != CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED && evt->eventType != CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED) return; auto& titleInfo = *evt->titleInfo; wxTitleManagerList::EntryType entryType; switch (titleInfo.GetTitleType()) { case TitleIdParser::TITLE_TYPE::BASE_TITLE_UPDATE: entryType = EntryType::Update; break; case TitleIdParser::TITLE_TYPE::AOC: entryType = EntryType::Dlc; break; case TitleIdParser::TITLE_TYPE::SYSTEM_DATA: case TitleIdParser::TITLE_TYPE::SYSTEM_OVERLAY_TITLE: case TitleIdParser::TITLE_TYPE::SYSTEM_TITLE: entryType = EntryType::System; break; default: entryType = EntryType::Base; } wxTitleManagerList::EntryFormat entryFormat; switch (titleInfo.GetFormat()) { case TitleInfo::TitleDataFormat::HOST_FS: default: entryFormat = EntryFormat::Folder; break; case TitleInfo::TitleDataFormat::WUD: entryFormat = EntryFormat::WUD; break; case TitleInfo::TitleDataFormat::WIIU_ARCHIVE: entryFormat = EntryFormat::WUA; break; } if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED) { if (titleInfo.IsCached()) return; // the title list only displays non-cached entries wxTitleManagerList::TitleEntry entry(entryType, entryFormat, titleInfo.GetPath()); ParsedMetaXml* metaInfo = titleInfo.GetMetaInfo(); if(titleInfo.IsSystemDataTitle()) return; // dont show system data titles for now entry.location_uid = titleInfo.GetUID(); entry.title_id = titleInfo.GetAppTitleId(); std::string name = metaInfo->GetLongName(GetConfig().console_language.GetValue()); const auto nl = name.find(L'\n'); if (nl != std::string::npos) name.replace(nl, 1, " - "); entry.name = wxString::FromUTF8(name); entry.version = titleInfo.GetAppTitleVersion(); entry.region = metaInfo->GetRegion(); auto* cmdEvt = new wxCommandEvent(wxEVT_TITLE_FOUND); cmdEvt->SetClientObject(new wxCustomData(entry)); wxQueueEvent(this, cmdEvt); } else if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED) { wxTitleManagerList::TitleEntry entry(entryType, entryFormat, titleInfo.GetPath()); entry.location_uid = titleInfo.GetUID(); entry.title_id = titleInfo.GetAppTitleId(); auto* cmdEvt = new wxCommandEvent(wxEVT_TITLE_REMOVED); cmdEvt->SetClientObject(new wxCustomData(entry)); wxQueueEvent(this, cmdEvt); } } void wxTitleManagerList::HandleSaveListCallback(struct CafeSaveListCallbackEvent* evt) { if (evt->eventType == CafeSaveListCallbackEvent::TYPE::SAVE_DISCOVERED) { ParsedMetaXml* metaInfo = evt->saveInfo->GetMetaInfo(); if (!metaInfo) return; auto& saveInfo = *evt->saveInfo; wxTitleManagerList::TitleEntry entry(EntryType::Save, EntryFormat::Folder, saveInfo.GetPath()); entry.location_uid = std::hash() ( metaInfo->GetTitleId() ); entry.title_id = metaInfo->GetTitleId(); std::string name = metaInfo->GetLongName(GetConfig().console_language.GetValue()); const auto nl = name.find(L'\n'); if (nl != std::string::npos) name.replace(nl, 1, " - "); entry.name = wxString::FromUTF8(name); entry.version = metaInfo->GetTitleVersion(); entry.region = metaInfo->GetRegion(); auto* cmdEvt = new wxCommandEvent(wxEVT_TITLE_FOUND); cmdEvt->SetClientObject(new wxCustomData(entry)); wxQueueEvent(this, cmdEvt); } } void wxTitleManagerList::OnTitleDiscovered(wxCommandEvent& event) { auto* obj = dynamic_cast(event.GetClientObject()); wxASSERT(obj); AddTitle(obj); } void wxTitleManagerList::OnTitleRemoved(wxCommandEvent& event) { auto* obj = dynamic_cast(event.GetClientObject()); wxASSERT(obj); for (auto& itr : m_data) { if (itr.get()->entry.location_uid == obj->get().location_uid) { RemoveItem(itr.get()->entry); break; } } } void wxTitleManagerList::AddTitle(TitleEntryData_t* obj) { const auto& data = obj->GetData(); if (GetTitleEntryByUID(data.location_uid).has_value()) return; // already in list m_data.emplace_back(std::make_unique(true, data)); m_sorted_data.emplace_back(*m_data[m_data.size() - 1]); SetItemCount(m_data.size()); } int wxTitleManagerList::AddImage(const wxImage& image) const { return -1; // m_image_list->Add(image.Scale(kListIconWidth, kListIconWidth, wxIMAGE_QUALITY_BICUBIC)); } // return < bool wxTitleManagerList::SortFunc(int column, const Type_t& v1, const Type_t& v2) { // last sort option if (column == -1) return v1.get().entry.path.compare(v2.get().entry.path) < 0; // visible have always priority if (!v1.get().visible && v2.get().visible) return false; else if (v1.get().visible && !v2.get().visible) return true; const auto& entry1 = v1.get().entry; const auto& entry2 = v2.get().entry; // check column: title id -> type -> path if (column == ColumnTitleId) { // ensure strong ordering -> use type since only one entry should be now (should be changed if every save for every user is displayed spearately?) if (entry1.title_id == entry2.title_id) return SortFunc(ColumnType, v1, v2); return entry1.title_id < entry2.title_id; } else if (column == ColumnName) { const int tmp = entry1.name.CmpNoCase(entry2.name); if(tmp == 0) return SortFunc(ColumnTitleId, v1, v2); return tmp < 0; } else if (column == ColumnType) { if(std::underlying_type_t(entry1.type) == std::underlying_type_t(entry2.type)) return SortFunc(-1, v1, v2); return std::underlying_type_t(entry1.type) < std::underlying_type_t(entry2.type); } else if (column == ColumnVersion) { if(entry1.version == entry2.version) return SortFunc(ColumnTitleId, v1, v2); return std::underlying_type_t(entry1.version) < std::underlying_type_t(entry2.version); } else if (column == ColumnRegion) { if(entry1.region == entry2.region) return SortFunc(ColumnTitleId, v1, v2); return std::underlying_type_t(entry1.region) < std::underlying_type_t(entry2.region); } return false; } void wxTitleManagerList::SortEntries(int column) { if(column == -1) { column = m_last_column_sorted; m_last_column_sorted = -1; if (column == -1) column = ColumnTitleId; } if (column != ColumnTitleId && column != ColumnName && column != ColumnType && column != ColumnVersion && column != ColumnRegion && column != ColumnFormat) return; if (m_last_column_sorted != column) { m_last_column_sorted = column; m_sort_less = true; } else m_sort_less = !m_sort_less; std::sort(m_sorted_data.begin(), m_sorted_data.end(), [this, column](const Type_t& v1, const Type_t& v2) -> bool { const bool result = SortFunc(column, v1, v2); return m_sort_less ? result : !result; }); RefreshPage(); } void wxTitleManagerList::RefreshPage() { long item_count = GetItemCount(); if (item_count > 0) RefreshItems(GetTopItem(), std::min(item_count - 1, GetTopItem() + GetCountPerPage() + 1)); } int wxTitleManagerList::Filter(const wxString& filter, const wxString& prefix, ItemColumn column) { if (prefix.empty()) return -1; if (!filter.StartsWith(prefix)) return -1; int counter = 0; const auto tmp_filter = filter.substr(prefix.size() - 1).Trim(false); for (auto&& data : m_data) { if (GetTitleEntryText(data->entry, column).Upper().Contains(tmp_filter)) { data->visible = true; ++counter; } else data->visible = false; } return counter; } void wxTitleManagerList::Filter(const wxString& filter) { if(filter.empty()) { std::for_each(m_data.begin(), m_data.end(), [](ItemDataPtr& data) { data->visible = true; }); SetItemCount(m_data.size()); RefreshPage(); return; } const auto filter_upper = filter.Upper().Trim(false).Trim(true); int counter = 0; if (const auto result = Filter(filter_upper, "TITLEID:", ColumnTitleId) != -1) counter = result; else if (const auto result = Filter(filter_upper, "NAME:", ColumnName) != -1) counter = result; else if (const auto result = Filter(filter_upper, "TYPE:", ColumnType) != -1) counter = result; else if (const auto result = Filter(filter_upper, "REGION:", ColumnRegion) != -1) counter = result; else if (const auto result = Filter(filter_upper, "VERSION:", ColumnVersion) != -1) counter = result; else if (const auto result = Filter(filter_upper, "FORMAT:", ColumnFormat) != -1) counter = result; else if(filter_upper == "ERROR") { for (auto&& data : m_data) { bool visible = true; data->visible = visible; if (visible) ++counter; } } else { for (auto&& data : m_data) { bool visible = false; if (data->entry.name.Upper().Contains(filter_upper)) visible = true; else if (GetTitleEntryText(data->entry, ColumnTitleId).Upper().Contains(filter_upper)) visible = true; else if (GetTitleEntryText(data->entry, ColumnType).Upper().Contains(filter_upper)) visible = true; data->visible = visible; if (visible) ++counter; } } SetItemCount(counter); RefreshPage(); } size_t wxTitleManagerList::GetCountByType(EntryType type) const { size_t result = 0; for(const auto& data : m_data) { if (data->entry.type == type) ++result; } return result; } void wxTitleManagerList::ClearItems() { m_sorted_data.clear(); m_data.clear(); SetItemCount(0); RefreshPage(); } void wxTitleManagerList::AutosizeColumns() { wxAutosizeColumns(this, ColumnTitleId, ColumnMAX - 1); }