From bb8139415e9c7ed2c72f2232446ea7b847b82d60 Mon Sep 17 00:00:00 2001 From: Luminyx <27lumi@protonmail.com> Date: Wed, 8 Apr 2026 21:02:02 -0400 Subject: [PATCH] DownloadCustomGraphicPackWindow --- src/gui/wxgui/CMakeLists.txt | 2 + .../wxgui/DownloadCustomGraphicPackWindow.cpp | 424 ++++++++++++++++++ .../wxgui/DownloadCustomGraphicPackWindow.h | 66 +++ src/gui/wxgui/GraphicPacksWindow2.cpp | 16 + src/gui/wxgui/GraphicPacksWindow2.h | 2 + 5 files changed, 510 insertions(+) create mode 100644 src/gui/wxgui/DownloadCustomGraphicPackWindow.cpp create mode 100644 src/gui/wxgui/DownloadCustomGraphicPackWindow.h diff --git a/src/gui/wxgui/CMakeLists.txt b/src/gui/wxgui/CMakeLists.txt index 12fc62d9..9a2e880d 100644 --- a/src/gui/wxgui/CMakeLists.txt +++ b/src/gui/wxgui/CMakeLists.txt @@ -50,6 +50,8 @@ add_library(CemuWxGui STATIC AudioDebuggerWindow.h DownloadGraphicPacksWindow.cpp DownloadGraphicPacksWindow.h + DownloadCustomGraphicPackWindow.cpp + DownloadCustomGraphicPackWindow.h GameProfileWindow.cpp GameProfileWindow.h GameUpdateWindow.cpp diff --git a/src/gui/wxgui/DownloadCustomGraphicPackWindow.cpp b/src/gui/wxgui/DownloadCustomGraphicPackWindow.cpp new file mode 100644 index 00000000..d136b07f --- /dev/null +++ b/src/gui/wxgui/DownloadCustomGraphicPackWindow.cpp @@ -0,0 +1,424 @@ +#include "wxgui/DownloadCustomGraphicPackWindow.h" +#include "Cafe/CafeSystem.h" +#include "config/ActiveSettings.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static size_t curlDownloadFile_writeData(void *ptr, size_t size, size_t nmemb, DownloadCustomGraphicPackWindow::curlDownloadFileState_t* downloadState) +{ + const size_t writeSize = size * nmemb; + const size_t currentSize = downloadState->fileData.size(); + const size_t newSize = currentSize + writeSize; + auto* bytePtr = static_cast(ptr); + downloadState->fileData.insert(downloadState->fileData.end(), bytePtr, bytePtr + writeSize); + return writeSize; +} + +static int progress_callback(DownloadCustomGraphicPackWindow::curlDownloadFileState_t* downloadState, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) +{ + if (downloadState->isCanceled) + return 1; + + if (dltotal > 1.0) + downloadState->progress = dlnow / dltotal; + else + downloadState->progress = 0.0; + return 0; +} + +static bool curlDownloadFile(const char *url, DownloadCustomGraphicPackWindow::curlDownloadFileState_t* downloadState) +{ + CURL* curl = curl_easy_init(); + if (curl == nullptr) + return false; + + downloadState->progress = 0.0; + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlDownloadFile_writeData); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, downloadState); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); + curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, downloadState); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 30L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 15L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); + downloadState->fileData.resize(0); + const CURLcode res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + return res == CURLE_OK; +} + +DownloadCustomGraphicPackWindow::DownloadCustomGraphicPackWindow(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _("Download Graphic Pack from URL"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX), + m_stage(StageDone), m_currentStage(StageDone) +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + m_urlField = new wxTextCtrl(this, wxID_ANY, wxEmptyString); + m_urlField->SetHint(_("Enter download URL...")); + sizer->Add(m_urlField, 0, wxALL | wxEXPAND, 5); + + m_processBar = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(500, 20), wxGA_HORIZONTAL); + m_processBar->SetValue(0); + m_processBar->SetRange(100); + sizer->Add(m_processBar, 0, wxALL | wxEXPAND, 5); + + auto* buttonSizer = new wxBoxSizer(wxHORIZONTAL); + + m_statusText = new wxStaticText(this, wxID_ANY, _("Ready...")); + buttonSizer->Add(m_statusText, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + + buttonSizer->AddStretchSpacer(1); + + auto* m_closeButton = new wxButton(this, wxID_ANY, _("Close")); + m_closeButton->Bind(wxEVT_BUTTON, &DownloadCustomGraphicPackWindow::OnCancelButton, this); + buttonSizer->Add(m_closeButton, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + + m_downloadButton = new wxButton(this, wxID_ANY, _("Download")); + m_downloadButton->Bind(wxEVT_BUTTON, &DownloadCustomGraphicPackWindow::OnDownloadButton, this); + buttonSizer->Add(m_downloadButton, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + + sizer->Add(buttonSizer, 0, wxEXPAND | wxALL, 5); + + this->SetSizer(sizer); + this->Centre(wxBOTH); + + wxWindowBase::Layout(); + wxWindowBase::Fit(); + + m_timer = new wxTimer(this); + this->Bind(wxEVT_TIMER, &DownloadCustomGraphicPackWindow::OnUpdate, this); + this->Bind(wxEVT_CLOSE_WINDOW, &DownloadCustomGraphicPackWindow::OnClose, this); + m_timer->Start(100); + + m_downloadState = std::make_unique(); +} + +DownloadCustomGraphicPackWindow::~DownloadCustomGraphicPackWindow() +{ + if (m_downloadState) + m_downloadState->isCanceled = true; + + m_timer->Stop(); + if (m_thread.joinable()) + m_thread.join(); +} + +int DownloadCustomGraphicPackWindow::ShowModal() +{ + if (CafeSystem::IsTitleRunning()) + { + wxMessageBox(_("Graphic packs cannot be updated while a game is running."), _("Graphic packs"), 5, this); + return wxID_CANCEL; + } + + wxDialog::ShowModal(); + return wxID_OK; +} + +void DownloadCustomGraphicPackWindow::OnClose(wxCloseEvent& event) +{ + if (m_downloadState) + { + m_downloadState->isCanceled = true; + } + + m_timer->Stop(); + if (m_thread.joinable()) + m_thread.join(); + + event.Skip(); +} + +void DownloadCustomGraphicPackWindow::OnUpdate(const wxTimerEvent& event) +{ + if (m_currentStage >= StageDone) + { + m_downloadButton->Enable(); + m_urlField->Enable(); + } + else + { + m_downloadButton->Disable(); + m_urlField->Disable(); + } + + if (m_currentStage == StageDownloading) + { + const sint32 processedSize = (sint32)(m_downloadState->progress * 100.0f); + if (m_processBar->GetValue() != processedSize) + m_processBar->SetValue(processedSize); + } + else if (m_currentStage == StageExtracting) + { + const sint32 processedSize = (sint32)(m_extractionProgress * 100.0f); + if (m_processBar->GetValue() != processedSize) + m_processBar->SetValue(processedSize); + } + + if (m_currentStage != m_stage) + { + wxString status = "..."; + const wxColour* colour = wxWHITE; + + switch (m_stage) + { + case StageDownloading: + status = "Downloading..."; + colour = wxWHITE; + break; + case StageVerifying: + status = "Verifying..."; + colour = wxWHITE; + break; + case StageExtracting: + status = "Extracting..."; + colour = wxWHITE; + break; + case StageDone: + status = "Done!"; + colour = wxGREEN; + m_processBar->SetValue(100.0); + break; + case StageErrConnectFailed: + if (m_urlField->GetValue().empty()) + { + status = "Please enter a valid URL."; + colour = wxWHITE; + } + else + { + status = "ERROR: Connection failed."; + colour = wxRED; + } + m_processBar->SetValue(0.0); + break; + case StageErrInvalidPack: + status = "ERROR: Invalid pack."; + colour = wxRED; + m_processBar->SetValue(0.0); + break; + case StageErrSourceFailed: + status = "ERROR: Failed to create ZIP source."; + colour = wxRED; + m_processBar->SetValue(0.0); + break; + case StageErrOpenFailed: + status = "ERROR: Failed to open downloaded ZIP."; + colour = wxRED; + m_processBar->SetValue(0.0); + break; + case StageErrConflict: + status = "ERROR: File conflict. Pack already installed?"; + colour = wxRED; + m_processBar->SetValue(0.0); + break; + } + + m_currentStage = m_stage; + m_statusText->SetLabel(status); + m_statusText->SetForegroundColour(*colour); + } +} + +void DownloadCustomGraphicPackWindow::OnCancelButton(const wxCommandEvent& event) +{ + Close(); +} + +void DownloadCustomGraphicPackWindow::OnDownloadButton(const wxCommandEvent& event) +{ + m_downloadButton->Disable(); + m_urlField->Disable(); + + if (m_thread.joinable()) + m_thread.join(); + + m_thread = std::thread(&DownloadCustomGraphicPackWindow::UpdateThread, this); +} + +void DownloadCustomGraphicPackWindow::UpdateThread() +{ + m_stage = StageDownloading; + if (curlDownloadFile(m_urlField->GetValue(), m_downloadState.get()) == false) + { + m_stage = StageErrConnectFailed; + return; + } + + m_extractionProgress = 0.0; + m_stage = StageExtracting; + + zip_source_t *src; + zip_t *za; + zip_error_t error; + + // init zip source + zip_error_init(&error); + if ((src = zip_source_buffer_create(m_downloadState->fileData.data(), m_downloadState->fileData.size(), 0, &error)) == NULL) + { + zip_error_fini(&error); + m_stage = StageErrSourceFailed; + return; + } + + // open zip from source + if ((za = zip_open_from_source(src, 0, &error)) == NULL) + { + zip_source_free(src); + zip_error_fini(&error); + m_stage = StageErrOpenFailed; + return; + } + + auto path = ActiveSettings::GetUserDataPath("graphicPacks/customGraphicPacks"); + fs::create_directories(path); + + // check if zip root directly contains a rules.txt + zip_stat_t zs; + zip_stat_init(&zs); + bool hasRootRulesTxt = (zip_stat(za, "rules.txt", 0, &zs) == 0); + + fs::path extractionRoot = path; + + if (hasRootRulesTxt) + { + // make a folder and extract into that + wxString urlStr = m_urlField->GetValue(); + std::string folderName = "NewCustomPack"; // fallback + + int lastSlash = urlStr.Find('/', true); + wxString fileNameBase = (lastSlash != wxNOT_FOUND) ? urlStr.Mid(lastSlash + 1) : urlStr; + + int lastDot = fileNameBase.Find('.', true); + if (lastDot != wxNOT_FOUND) + { + fileNameBase = fileNameBase.Left(lastDot); + } + + fileNameBase.Trim(true).Trim(false); + if (!fileNameBase.IsEmpty()) + { + folderName = fileNameBase.ToStdString(); + } + + extractionRoot = path / folderName; + fs::create_directories(extractionRoot); + } + else + { + // not a gfxpack + m_stage = StageErrInvalidPack; + zip_close(za); + return; + } + + // extract + bool err = false; + zip_int64_t numEntries = zip_get_num_entries(za, 0); + for (zip_int64_t i = 0; i < numEntries; i++) + { + m_extractionProgress = (double)i / (double)numEntries; + + if (m_downloadState->isCanceled) + { + err = true; + } + + if (err) + { + break; + } + + zip_stat_t sb = { 0 }; + zip_stat_init(&sb); + if (zip_stat_index(za, i, 0, &sb) != 0) + { + assert_dbg(); + continue; + } + + std::string fileName = sb.name; + if (std::strstr(sb.name, "../") != nullptr || + std::strstr(sb.name, "..\\") != nullptr || + (!fileName.empty() && (fileName[0] == '/' || fileName[0] == '\\'))) + { + // bad path, CommunityGraphicPacks silently continues + // but this is a low-trust environment, so we halt + m_stage = StageErrInvalidPack; + err = true; + break; + } + + fs::path targetDest = extractionRoot / fileName; + + if (!fileName.empty() && fileName.back() == '/') + { + fs::create_directories(targetDest); + continue; + } + + if (fs::exists(targetDest) && fs::is_regular_file(targetDest)) + { + m_stage = StageErrConflict; + err = true; + break; + } + + fs::create_directories(targetDest.parent_path()); + + zip_file_t* zf = zip_fopen_index(za, i, 0); + if (!zf) + { + continue; + } + + if (sb.size > 1000000000) // 1GB suspicious limit per file + { + m_stage = StageErrInvalidPack; + err = true; + break; + } + + std::vector fileBuffer(sb.size); + + if (zip_fread(zf, fileBuffer.data(), sb.size) == sb.size) + { + std::unique_ptr fs(FileStream::createFile2(targetDest)); + if (fs) + { + fs->writeData(fileBuffer.data(), sb.size); + } + } + else + { + err = true; + m_stage = StageErrInvalidPack; + } + + zip_fclose(zf); + } + + zip_discard(za); + zip_error_fini(&error); + + if (!err) + { + m_stage = StageDone; + } +} diff --git a/src/gui/wxgui/DownloadCustomGraphicPackWindow.h b/src/gui/wxgui/DownloadCustomGraphicPackWindow.h new file mode 100644 index 00000000..586a8644 --- /dev/null +++ b/src/gui/wxgui/DownloadCustomGraphicPackWindow.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +struct curlDownloadFileState_t; + +class DownloadCustomGraphicPackWindow : public wxDialog +{ +public: + DownloadCustomGraphicPackWindow(wxWindow* parent); + ~DownloadCustomGraphicPackWindow() override; + + int ShowModal() override; + void OnClose(wxCloseEvent& event); + void OnUpdate(const wxTimerEvent& event); + void OnCancelButton(const wxCommandEvent& event); + void OnDownloadButton(const wxCommandEvent& event); + +public: + struct curlDownloadFileState_t + { + std::vector fileData; + std::atomic progress = 0.0; + std::atomic isCanceled = false; + }; + +private: + void UpdateThread(); + + enum DownloadStage_t + { + StageDownloading, + StageExtracting, + StageVerifying, + StageDone, + + // Error stages + StageErrConnectFailed, + StageErrSourceFailed, + StageErrInvalidPack, + StageErrOpenFailed, + StageErrConflict, + }; + + std::unique_ptr m_downloadState; + std::atomic m_stage; + DownloadStage_t m_currentStage; + std::atomic m_extractionProgress; + + wxTimer* m_timer; + wxTextCtrl* m_urlField; + wxGauge* m_processBar; + wxStaticText* m_statusText; + wxButton* m_downloadButton; + std::thread m_thread; +}; diff --git a/src/gui/wxgui/GraphicPacksWindow2.cpp b/src/gui/wxgui/GraphicPacksWindow2.cpp index 236a742f..9ac52304 100644 --- a/src/gui/wxgui/GraphicPacksWindow2.cpp +++ b/src/gui/wxgui/GraphicPacksWindow2.cpp @@ -2,6 +2,7 @@ #include "wxgui/wxgui.h" #include "wxgui/GraphicPacksWindow2.h" #include "wxgui/DownloadGraphicPacksWindow.h" +#include "wxgui/DownloadCustomGraphicPackWindow.h" #include "Cafe/GraphicPack/GraphicPack2.h" #include "config/CemuConfig.h" #include "config/ActiveSettings.h" @@ -302,6 +303,11 @@ GraphicPacksWindow2::GraphicPacksWindow2(wxWindow* parent, uint64_t title_id_fil sizer->Add(new wxStaticLine(m_right_panel, wxID_ANY), 0, wxLEFT|wxRIGHT | wxEXPAND, 3); auto* row = new wxBoxSizer(wxHORIZONTAL); + + m_download_from_url = new wxButton(m_right_panel, wxID_ANY, _("Download pack from URL")); + m_download_from_url->Bind(wxEVT_BUTTON, &GraphicPacksWindow2::OnClickCustomDownload, this); + row->Add(m_download_from_url, 0, wxALL, 5); + m_update_graphicPacks = new wxButton(m_right_panel, wxID_ANY, _("Download latest community graphic packs")); m_update_graphicPacks->Bind(wxEVT_BUTTON, &GraphicPacksWindow2::OnCheckForUpdates, this); row->Add(m_update_graphicPacks, 0, wxALL, 5); @@ -599,6 +605,16 @@ void GraphicPacksWindow2::OnReloadShaders(wxCommandEvent& event) ReloadPack(m_shown_graphic_pack); } +void GraphicPacksWindow2::OnClickCustomDownload(wxCommandEvent& event) +{ + DownloadCustomGraphicPackWindow frame(this); + if (frame.ShowModal() == wxID_OK && !CafeSystem::IsTitleRunning()) + { + RefreshGraphicPacks(); + FillGraphicPackList(); + } +} + void GraphicPacksWindow2::OnCheckForUpdates(wxCommandEvent& event) { DownloadGraphicPacksWindow frame(this); diff --git a/src/gui/wxgui/GraphicPacksWindow2.h b/src/gui/wxgui/GraphicPacksWindow2.h index 435d1031..0d24e87b 100644 --- a/src/gui/wxgui/GraphicPacksWindow2.h +++ b/src/gui/wxgui/GraphicPacksWindow2.h @@ -46,6 +46,7 @@ private: wxBoxSizer* m_preset_sizer; std::vector m_active_preset; wxButton* m_reload_shaders; + wxButton* m_download_from_url; wxButton* m_update_graphicPacks; wxInfoBar* m_info_bar; @@ -64,6 +65,7 @@ private: void OnTreeChoiceChanged(wxTreeEvent& event); void OnActivePresetChanged(wxCommandEvent& event); void OnReloadShaders(wxCommandEvent& event); + void OnClickCustomDownload(wxCommandEvent& event); void OnCheckForUpdates(wxCommandEvent& event); void OnSizeChanged(wxSizeEvent& event); void SashPositionChanged(wxEvent& event);