diff --git a/pcsx2/ImGui/FullscreenUI.cpp b/pcsx2/ImGui/FullscreenUI.cpp index a86f9fcfa0..2335588984 100644 --- a/pcsx2/ImGui/FullscreenUI.cpp +++ b/pcsx2/ImGui/FullscreenUI.cpp @@ -22,6 +22,12 @@ #include "USB/USB.h" #include "VMManager.h" #include "ps2/BiosTools.h" +#include "DEV9/ATA/HddCreate.h" +#include "DEV9/pcap_io.h" +#include "DEV9/sockets.h" +#ifdef _WIN32 +#include "DEV9/Win32/tap.h" +#endif #include "common/Console.h" #include "common/Error.h" @@ -48,9 +54,11 @@ #include "fmt/format.h" #include +#include #include #include #include +#include #include #include #include @@ -173,6 +181,7 @@ using ImGuiFullscreen::OpenInfoMessageDialog; using ImGuiFullscreen::OpenInputStringDialog; using ImGuiFullscreen::PopPrimaryColor; using ImGuiFullscreen::PushPrimaryColor; +using ImGuiFullscreen::InputFilterType; using ImGuiFullscreen::QueueResetFocus; using ImGuiFullscreen::ResetFocusHere; using ImGuiFullscreen::RightAlignNavButtons; @@ -184,6 +193,176 @@ using ImGuiFullscreen::WantsToCloseMenu; namespace FullscreenUI { + + class HddCreateInProgress : public HddCreate + { + private: + std::string m_dialogId; + std::atomic_bool m_completed{false}; + std::atomic_bool m_success{false}; + int m_reqMiB = 0; + std::atomic_bool m_dialogClosed{false}; // Check if dialog was already closed + + static std::vector> s_activeOperations; + static std::mutex s_operationsMutex; + static std::atomic_int s_nextOperationId; + + public: + HddCreateInProgress(const std::string& dialogId) + : m_dialogId(dialogId) + { + } + + ~HddCreateInProgress() + { + SafeCloseDialog(); + } + + void SafeCloseDialog() + { + bool expected = false; + if (m_dialogClosed.compare_exchange_strong(expected, true)) + { + ImGuiFullscreen::CloseProgressDialog(m_dialogId.c_str()); + } + } + + static bool StartCreation(const std::string& filePath, int sizeInGB, bool use48BitLBA) + { + if (filePath.empty() || sizeInGB <= 0) + return false; + + std::string dialogId = fmt::format("hdd_create_{}", s_nextOperationId.fetch_add(1, std::memory_order_relaxed)); + + std::shared_ptr instance = std::make_shared(dialogId); + + // Convert GB to bytes + const u64 sizeBytes = static_cast(sizeInGB) * static_cast(_1gb); + + // Make sure the file doesn't already exist (or delete it if it does) + if (FileSystem::FileExists(filePath.c_str())) + { + if (!FileSystem::DeleteFilePath(filePath.c_str())) + { + Host::RunOnCPUThread([filePath]() { + ShowToast( + fmt::format("{} HDD Creation Failed", ICON_FA_TRIANGLE_EXCLAMATION), + fmt::format("Failed to delete existing HDD image file '{}'. Please check file permissions and try again.", Path::GetFileName(filePath)), + 5.0f); + }); + return false; + } + } + + // Setup the creation parameters + instance->filePath = filePath; + instance->neededSize = sizeBytes; + + // Register the operation + { + std::lock_guard lock(s_operationsMutex); + s_activeOperations.push_back(instance); + } + + // Start the HDD creation + std::thread([instance = std::move(instance)]() { + instance->Start(); + + if (!instance->errored) + Host::RunOnCPUThread([size_gb = static_cast(instance->neededSize / static_cast(_1gb))]() { + ShowToast( + ICON_FA_CIRCLE_CHECK, + fmt::format("HDD image ({} GB) created successfully.", size_gb), + 3.0f); + }); + else + Host::RunOnCPUThread([]() { + ShowToast( + ICON_FA_TRIANGLE_EXCLAMATION, + "Failed to create HDD image.", + 3.0f); + }); + + std::lock_guard lock(s_operationsMutex); + for (auto it = s_activeOperations.begin(); it != s_activeOperations.end(); ++it) + { + if (it->get() == instance.get()) + { + s_activeOperations.erase(it); + break; + } + } + }).detach(); + + return true; + } + + static void CancelAllOperations() + { + std::lock_guard lock(s_operationsMutex); + for (auto& operation : s_activeOperations) + { + operation->SetCanceled(); + operation->SafeCloseDialog(); + } + s_activeOperations.clear(); + } + + protected: + virtual void Init() override + { + m_reqMiB = static_cast((neededSize + ((1024 * 1024) - 1)) / (1024 * 1024)); + const std::string message = fmt::format("{} Creating HDD Image\n{} / {} MiB", ICON_FA_HARD_DRIVE, 0, m_reqMiB); + ImGuiFullscreen::OpenProgressDialog(m_dialogId.c_str(), message, 0, m_reqMiB, 0); + } + + virtual void SetFileProgress(u64 currentSize) override + { + const int writtenMiB = static_cast((currentSize + ((1024 * 1024) - 1)) / (1024 * 1024)); + const std::string message = fmt::format("{} Creating HDD Image\n{} / {} MiB", ICON_FA_HARD_DRIVE, writtenMiB, m_reqMiB); + ImGuiFullscreen::UpdateProgressDialog(m_dialogId.c_str(), message, 0, m_reqMiB, writtenMiB); + } + + virtual void SetError() override + { + SafeCloseDialog(); + HddCreate::SetError(); + } + + virtual void Cleanup() override + { + SafeCloseDialog(); + m_success.store(!errored, std::memory_order_release); + m_completed.store(true, std::memory_order_release); + } + }; + + std::vector> HddCreateInProgress::s_activeOperations; + std::mutex HddCreateInProgress::s_operationsMutex; + std::atomic_int HddCreateInProgress::s_nextOperationId{0}; + + bool CreateHardDriveWithProgress(const std::string& filePath, int sizeInGB, bool use48BitLBA) + { + // Validate size limits based on the LBA mode set + const int min_size = use48BitLBA ? 100 : 40; + const int max_size = use48BitLBA ? 2000 : 120; + + if (sizeInGB < min_size || sizeInGB > max_size) + { + Host::RunOnCPUThread([min_size, max_size]() { + ShowToast(std::string(), fmt::format("Invalid HDD size. Size must be between {} and {} GB.", min_size, max_size).c_str()); + }); + return false; + } + + return HddCreateInProgress::StartCreation(filePath, sizeInGB, use48BitLBA); + } + + void CancelAllHddOperations() + { + HddCreateInProgress::CancelAllOperations(); + } + enum class MainWindowType { None, @@ -214,6 +393,7 @@ namespace FullscreenUI Graphics, Audio, MemoryCard, + NetworkHDD, Folders, Achievements, Controller, @@ -232,6 +412,16 @@ namespace FullscreenUI Count }; + enum class IPAddressType + { + PS2IP, + SubnetMask, + Gateway, + DNS1, + DNS2, + Other + }; + ////////////////////////////////////////////////////////////////////////// // Main ////////////////////////////////////////////////////////////////////////// @@ -340,6 +530,7 @@ namespace FullscreenUI static void DrawGraphicsSettingsPage(SettingsInterface* bsi, bool show_advanced_settings); static void DrawAudioSettingsPage(); static void DrawMemoryCardSettingsPage(); + static void DrawNetworkHDDSettingsPage(); static void DrawFoldersSettingsPage(); static void DrawAchievementsSettingsPage(std::unique_lock& settings_lock); static void DrawControllerSettingsPage(); @@ -395,6 +586,10 @@ namespace FullscreenUI static void DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, const char* default_value, SettingInfo::GetOptionsCallback options_callback, bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, std::pair font = g_large_font, std::pair summary_font = g_medium_font); + static void DrawIPAddressSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + const char* default_value, bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, + std::pair font = g_large_font, std::pair summary_font = g_medium_font, + IPAddressType ip_type = IPAddressType::Other); static void DrawFloatListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, float default_value, const char* const* options, const float* option_values, size_t option_count, bool translate_options, bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, std::pair font = g_large_font, @@ -965,6 +1160,7 @@ void FullscreenUI::Shutdown(bool clear_state) { if (clear_state) { + CancelAllHddOperations(); CloseSaveStateSelector(); s_cover_image_map.clear(); s_game_list_sorted_entries = {}; @@ -2897,6 +3093,91 @@ void FullscreenUI::DrawPathSetting(SettingsInterface* bsi, const char* title, co } } +void FullscreenUI::DrawIPAddressSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* key, const char* default_value, bool enabled, float height, std::pair font, std::pair summary_font, IPAddressType ip_type) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional value( + bsi->GetOptionalSmallStringValue(section, key, game_settings ? std::nullopt : std::optional(default_value))); + + const SmallString value_text = value.has_value() ? value.value() : SmallString(FSUI_VSTR("Use Global Setting")); + + static std::array ip_octets = {0, 0, 0, 0}; + + if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) + { + const std::string current_ip = value.has_value() ? std::string(value->c_str()) : std::string(default_value); + std::istringstream iss(current_ip); + std::string segment; + int i = 0; + while (std::getline(iss, segment, '.') && i < 4) + { + ip_octets[i] = std::clamp(std::atoi(segment.c_str()), 0, 255); + i++; + } + for (; i < 4; i++) + ip_octets[i] = 0; + + char ip_str[16]; + std::snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", ip_octets[0], ip_octets[1], ip_octets[2], ip_octets[3]); + + const char* message; + switch (ip_type) + { + case IPAddressType::DNS1: + case IPAddressType::DNS2: + message = FSUI_CSTR("Enter the DNS server address"); + break; + case IPAddressType::Gateway: + message = FSUI_CSTR("Enter the Gateway address"); + break; + case IPAddressType::SubnetMask: + message = FSUI_CSTR("Enter the Subnet Mask"); + break; + case IPAddressType::PS2IP: + message = FSUI_CSTR("Enter the PS2 IP address"); + break; + case IPAddressType::Other: + default: + message = FSUI_CSTR("Enter the IP address"); + break; + } + + ImGuiFullscreen::CloseInputDialog(); + + std::string ip_str_value(ip_str); + + ImGuiFullscreen::OpenInputStringDialog( + title, + message, + "", + std::string(FSUI_ICONSTR(ICON_FA_CHECK, "OK")), + [bsi, section, key, default_value](std::string text) { + // Validate and clean up the IP address + std::array new_octets = {0, 0, 0, 0}; + std::istringstream iss(text); + std::string segment; + int i = 0; + while (std::getline(iss, segment, '.') && i < 4) + { + new_octets[i] = std::clamp(std::atoi(segment.c_str()), 0, 255); + i++; + } + + char ip_str[16]; + std::snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", new_octets[0], new_octets[1], new_octets[2], new_octets[3]); + + if (IsEditingGameSettings(bsi) && strcmp(ip_str, default_value) == 0) + bsi->DeleteValue(section, key); + else + bsi->SetStringValue(section, key, ip_str); + SetSettingsChanged(bsi); + }, + ip_str_value, + ImGuiFullscreen::InputFilterType::IPAddress); + } +} + void FullscreenUI::StartAutomaticBinding(u32 port) { // messy because the enumeration has to happen on the input thread @@ -3127,6 +3408,7 @@ void FullscreenUI::DrawSettingsWindow() ICON_PF_PICTURE, ICON_PF_SOUND, ICON_PF_MEMORY_CARD, + ICON_FA_NETWORK_WIRED, ICON_FA_FOLDER_OPEN, ICON_FA_TROPHY, ICON_PF_GAMEPAD_ALT, @@ -3150,6 +3432,7 @@ void FullscreenUI::DrawSettingsWindow() SettingsPage::Graphics, SettingsPage::Audio, SettingsPage::MemoryCard, + SettingsPage::NetworkHDD, SettingsPage::Folders, SettingsPage::Achievements, SettingsPage::Controller, @@ -3174,6 +3457,7 @@ void FullscreenUI::DrawSettingsWindow() FSUI_NSTR("Graphics Settings"), FSUI_NSTR("Audio Settings"), FSUI_NSTR("Memory Card Settings"), + FSUI_NSTR("Network & HDD Settings"), FSUI_NSTR("Folder Settings"), FSUI_NSTR("Achievements Settings"), FSUI_NSTR("Controller Settings"), @@ -3305,6 +3589,10 @@ void FullscreenUI::DrawSettingsWindow() DrawMemoryCardSettingsPage(); break; + case SettingsPage::NetworkHDD: + DrawNetworkHDDSettingsPage(); + break; + case SettingsPage::Folders: DrawFoldersSettingsPage(); break; @@ -4672,6 +4960,540 @@ void FullscreenUI::DrawMemoryCardSettingsPage() } } + EndMenuButtons(); +} + +void FullscreenUI::DrawNetworkHDDSettingsPage() +{ + + static constexpr const char* dns_options[] = { + FSUI_NSTR("Manual"), + FSUI_NSTR("Auto"), + FSUI_NSTR("Internal"), + }; + + static constexpr const char* dns_values[] = { + "Manual", + "Auto", + "Internal", + }; + + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + MenuHeading(FSUI_CSTR("Network Adapter")); + + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_NETWORK_WIRED, "Enable Network Adapter"), + FSUI_CSTR("Enables the network adapter for online functionality and LAN play."), "DEV9/Eth", "EthEnable", false); + + const bool network_enabled = GetEffectiveBoolSetting(bsi, "DEV9/Eth", "EthEnable", false); + + const std::string current_api = bsi->GetStringValue("DEV9/Eth", "EthApi", "Unset"); + + static std::vector> adapter_lists; + static std::vector api_types; + static std::vector api_display_names; + static bool adapters_loaded = false; + + if (!adapters_loaded && network_enabled) + { + adapter_lists.clear(); + api_types.clear(); + api_display_names.clear(); + + adapter_lists.emplace_back(); + api_types.emplace_back(Pcsx2Config::DEV9Options::NetApi::Unset); + api_display_names.emplace_back("Unset"); + + std::vector pcap_adapters = PCAPAdapter::GetAdapters(); + if (!pcap_adapters.empty()) + { + std::vector pcap_bridged_adapters; + std::vector pcap_switched_adapters; + std::set seen_bridged_guids; + std::set seen_switched_guids; + + for (const auto& adapter : pcap_adapters) + { + if (adapter.type == Pcsx2Config::DEV9Options::NetApi::PCAP_Bridged) + { + if (seen_bridged_guids.find(adapter.guid) == seen_bridged_guids.end()) + { + seen_bridged_guids.insert(adapter.guid); + pcap_bridged_adapters.push_back(adapter); + } + } + else if (adapter.type == Pcsx2Config::DEV9Options::NetApi::PCAP_Switched) + { + if (seen_switched_guids.find(adapter.guid) == seen_switched_guids.end()) + { + seen_switched_guids.insert(adapter.guid); + pcap_switched_adapters.push_back(adapter); + } + } + } + + // Sort adapters alphabetically by name + std::sort(pcap_bridged_adapters.begin(), pcap_bridged_adapters.end(), + [](const AdapterEntry& a, const AdapterEntry& b) { return a.name < b.name; }); + std::sort(pcap_switched_adapters.begin(), pcap_switched_adapters.end(), + [](const AdapterEntry& a, const AdapterEntry& b) { return a.name < b.name; }); + + if (!pcap_bridged_adapters.empty()) + { + adapter_lists.emplace_back(pcap_bridged_adapters); + api_types.emplace_back(Pcsx2Config::DEV9Options::NetApi::PCAP_Bridged); + api_display_names.emplace_back("PCAP Bridged"); + } + + if (!pcap_switched_adapters.empty()) + { + adapter_lists.emplace_back(pcap_switched_adapters); + api_types.emplace_back(Pcsx2Config::DEV9Options::NetApi::PCAP_Switched); + api_display_names.emplace_back("PCAP Switched"); + } + } + +#ifdef _WIN32 + std::vector tap_adapters = TAPAdapter::GetAdapters(); + if (!tap_adapters.empty()) + { + // Sort adapters alphabetically by name + std::sort(tap_adapters.begin(), tap_adapters.end(), + [](const AdapterEntry& a, const AdapterEntry& b) { return a.name < b.name; }); + + adapter_lists.emplace_back(tap_adapters); + api_types.emplace_back(Pcsx2Config::DEV9Options::NetApi::TAP); + api_display_names.emplace_back("TAP"); + } +#endif + + std::vector socket_adapters = SocketAdapter::GetAdapters(); + if (!socket_adapters.empty()) + { + // Sort adapters alphabetically by name + std::sort(socket_adapters.begin(), socket_adapters.end(), + [](const AdapterEntry& a, const AdapterEntry& b) { return a.name < b.name; }); + + adapter_lists.emplace_back(socket_adapters); + api_types.emplace_back(Pcsx2Config::DEV9Options::NetApi::Sockets); + api_display_names.emplace_back("Sockets"); + } + + adapters_loaded = true; + } + + size_t current_api_index = 0; + for (size_t i = 0; i < api_types.size(); i++) + { + if (current_api == Pcsx2Config::DEV9Options::NetApiNames[static_cast(api_types[i])]) + { + current_api_index = i; + break; + } + } + + if (MenuButtonWithValue(FSUI_ICONSTR(ICON_FA_PLUG, "Ethernet Device Type"), + FSUI_CSTR("Determines the simulated Ethernet adapter type."), + current_api_index < api_display_names.size() ? api_display_names[current_api_index].c_str() : "Unset", + network_enabled)) + { + ImGuiFullscreen::ChoiceDialogOptions options; + + for (size_t i = 0; i < api_display_names.size(); i++) + { + options.emplace_back(api_display_names[i], i == current_api_index); + } + + std::vector current_api_types = api_types; + std::vector> current_adapter_lists = adapter_lists; + + OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_PLUG, "Ethernet Device Type"), false, std::move(options), + [bsi, current_api_types, current_adapter_lists](s32 index, const std::string& title, bool checked) { + if (index < 0 || index >= static_cast(current_api_types.size())) + return; + + auto lock = Host::GetSettingsLock(); + const std::string selected_api = Pcsx2Config::DEV9Options::NetApiNames[static_cast(current_api_types[index])]; + const std::string previous_api = bsi->GetStringValue("DEV9/Eth", "EthApi", "Unset"); + const std::string previous_device = bsi->GetStringValue("DEV9/Eth", "EthDevice", ""); + + bsi->SetStringValue("DEV9/Eth", "EthApi", selected_api.c_str()); + + std::string new_device = ""; + if (index < static_cast(current_adapter_lists.size())) + { + const auto& new_adapter_list = current_adapter_lists[index]; + + // Try to find the same GUID in the new adapter list + if (!previous_device.empty()) + { + for (const auto& adapter : new_adapter_list) + { + if (adapter.guid == previous_device) + { + new_device = adapter.guid; + break; + } + } + } + + // If no matching device found, use the first available device + if (new_device.empty() && !new_adapter_list.empty()) + { + new_device = new_adapter_list[0].guid; + } + } + + bsi->SetStringValue("DEV9/Eth", "EthDevice", new_device.c_str()); + SetSettingsChanged(bsi); + + CloseChoiceDialog(); + }); + } + + const std::string current_device = bsi->GetStringValue("DEV9/Eth", "EthDevice", ""); + const bool show_device_setting = (current_api_index > 0 && current_api_index < api_types.size()); + + std::string device_display = ""; + if (show_device_setting && !current_device.empty()) + { + if (current_api_index < adapter_lists.size()) + { + const auto& adapter_list = adapter_lists[current_api_index]; + for (const auto& adapter : adapter_list) + { + if (adapter.guid == current_device) + { + device_display = adapter.name; + break; + } + } + } + + if (device_display.empty()) + device_display = current_device; + } + else if (show_device_setting && current_device.empty()) + { + device_display = "Not Selected"; + } + + if (MenuButtonWithValue(FSUI_ICONSTR(ICON_FA_ETHERNET, "Ethernet Device"), + FSUI_CSTR("Network adapter to use for PS2 network emulation."), + device_display.c_str(), + network_enabled && show_device_setting)) + { + ImGuiFullscreen::ChoiceDialogOptions options; + + if (current_api_index > 0 && current_api_index < adapter_lists.size()) + { + const auto& adapter_list = adapter_lists[current_api_index]; + for (size_t i = 0; i < adapter_list.size(); i++) + { + const auto& adapter = adapter_list[i]; + options.emplace_back(adapter.name, adapter.guid == current_device); + } + } + + if (options.empty()) + { + options.emplace_back("No adapters found", false); + } + + std::vector current_adapter_list; + if (current_api_index > 0 && current_api_index < adapter_lists.size()) + { + current_adapter_list = adapter_lists[current_api_index]; + } + + std::string current_api_choice = bsi->GetStringValue("DEV9/Eth", "EthApi", "Unset"); + + OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_ETHERNET, "Ethernet Device"), false, std::move(options), + [bsi, current_adapter_list, current_api_choice](s32 index, const std::string& title, bool checked) { + if (index < 0 || title == "No adapters found") + return; + + if (index < static_cast(current_adapter_list.size())) + { + const auto& selected_adapter = current_adapter_list[index]; + + auto lock = Host::GetSettingsLock(); + bsi->SetStringValue("DEV9/Eth", "EthApi", current_api_choice.c_str()); + bsi->SetStringValue("DEV9/Eth", "EthDevice", selected_adapter.guid.c_str()); + SetSettingsChanged(bsi); + } + + CloseChoiceDialog(); + }); + } + + AdapterOptions adapter_options = AdapterOptions::None; + const std::string final_api = bsi->GetStringValue("DEV9/Eth", "EthApi", "Unset"); + if (final_api == "PCAP Bridged" || final_api == "PCAP Switched") + adapter_options = PCAPAdapter::GetAdapterOptions(); +#ifdef _WIN32 + else if (final_api == "TAP") + adapter_options = TAPAdapter::GetAdapterOptions(); +#endif + else if (final_api == "Sockets") + adapter_options = SocketAdapter::GetAdapterOptions(); + + const bool dhcp_can_be_disabled = (adapter_options & AdapterOptions::DHCP_ForcedOn) == AdapterOptions::None; + + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_SHIELD_HALVED, "Intercept DHCP"), + FSUI_CSTR("When enabled, DHCP packets will be intercepted and replaced with internal responses."), "DEV9/Eth", "InterceptDHCP", false, network_enabled && dhcp_can_be_disabled); + + MenuHeading(FSUI_CSTR("Network Configuration")); + + const bool intercept_dhcp = GetEffectiveBoolSetting(bsi, "DEV9/Eth", "InterceptDHCP", false); + const bool dhcp_forced_on = (adapter_options & AdapterOptions::DHCP_ForcedOn) == AdapterOptions::DHCP_ForcedOn; + const bool ip_settings_enabled = network_enabled && (intercept_dhcp || dhcp_forced_on); + + const bool ip_can_be_edited = (adapter_options & AdapterOptions::DHCP_OverrideIP) == AdapterOptions::None; + const bool subnet_can_be_edited = (adapter_options & AdapterOptions::DHCP_OverideSubnet) == AdapterOptions::None; + const bool gateway_can_be_edited = (adapter_options & AdapterOptions::DHCP_OverideGateway) == AdapterOptions::None; + + DrawIPAddressSetting(bsi, FSUI_ICONSTR(ICON_FA_NETWORK_WIRED, "Address"), + FSUI_CSTR("IP address for the PS2 virtual network adapter."), "DEV9/Eth", "PS2IP", "0.0.0.0", + ip_settings_enabled && ip_can_be_edited, LAYOUT_MENU_BUTTON_HEIGHT, g_large_font, g_medium_font, IPAddressType::PS2IP); + + const bool mask_auto = GetEffectiveBoolSetting(bsi, "DEV9/Eth", "AutoMask", true); + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_WAND_MAGIC, "Auto Subnet Mask"), + FSUI_CSTR("Automatically determine the subnet mask based on the IP address class."), + "DEV9/Eth", "AutoMask", true, ip_settings_enabled && subnet_can_be_edited); + DrawIPAddressSetting(bsi, FSUI_ICONSTR(ICON_FA_NETWORK_WIRED, "Subnet Mask"), + FSUI_CSTR("Subnet mask for the PS2 virtual network adapter."), "DEV9/Eth", "Mask", "0.0.0.0", + ip_settings_enabled && subnet_can_be_edited && !mask_auto, LAYOUT_MENU_BUTTON_HEIGHT, g_large_font, g_medium_font, IPAddressType::SubnetMask); + + const bool gateway_auto = GetEffectiveBoolSetting(bsi, "DEV9/Eth", "AutoGateway", true); + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_WAND_MAGIC, "Auto Gateway"), + FSUI_CSTR("Automatically determine the gateway address based on the IP address."), + "DEV9/Eth", "AutoGateway", true, ip_settings_enabled && gateway_can_be_edited); + DrawIPAddressSetting(bsi, FSUI_ICONSTR(ICON_FA_NETWORK_WIRED, "Gateway Address"), + FSUI_CSTR("Gateway address for the PS2 virtual network adapter."), "DEV9/Eth", "Gateway", "0.0.0.0", + ip_settings_enabled && gateway_can_be_edited && !gateway_auto, LAYOUT_MENU_BUTTON_HEIGHT, g_large_font, g_medium_font, IPAddressType::Gateway); + + // DNS Configuration + const std::string dns1_mode = bsi->GetStringValue("DEV9/Eth", "ModeDNS1", "Auto"); + const std::string dns2_mode = bsi->GetStringValue("DEV9/Eth", "ModeDNS2", "Auto"); + const bool dns1_editable = dns1_mode == "Manual" && ip_settings_enabled; + const bool dns2_editable = dns2_mode == "Manual" && ip_settings_enabled; + + DrawStringListSetting(bsi, FSUI_ICONSTR(ICON_FA_SERVER, "DNS1 Mode"), + FSUI_CSTR("Determines how primary DNS requests are handled."), "DEV9/Eth", "ModeDNS1", "Auto", + dns_options, dns_values, std::size(dns_options), true, ip_settings_enabled); + + DrawIPAddressSetting(bsi, FSUI_ICONSTR(ICON_FA_SERVER, "DNS1 Address"), + FSUI_CSTR("Primary DNS server address for the PS2 virtual network adapter."), "DEV9/Eth", "DNS1", "0.0.0.0", + dns1_editable, LAYOUT_MENU_BUTTON_HEIGHT, g_large_font, g_medium_font, IPAddressType::DNS1); + + DrawStringListSetting(bsi, FSUI_ICONSTR(ICON_FA_SERVER, "DNS2 Mode"), + FSUI_CSTR("Determines how secondary DNS requests are handled."), "DEV9/Eth", "ModeDNS2", "Auto", + dns_options, dns_values, std::size(dns_options), true, ip_settings_enabled); + + DrawIPAddressSetting(bsi, FSUI_ICONSTR(ICON_FA_SERVER, "DNS2 Address"), + FSUI_CSTR("Secondary DNS server address for the PS2 virtual network adapter."), "DEV9/Eth", "DNS2", "0.0.0.0", + dns2_editable, LAYOUT_MENU_BUTTON_HEIGHT, g_large_font, g_medium_font, IPAddressType::DNS2); + + MenuHeading(FSUI_CSTR("Internal HDD")); + + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_FLOPPY_DISK, "Enable HDD"), + FSUI_CSTR("Enables the internal Hard Disk Drive for expanded storage."), "DEV9/Hdd", "HddEnable", false); + + const bool hdd_enabled = GetEffectiveBoolSetting(bsi, "DEV9/Hdd", "HddEnable", false); + + const SmallString hdd_selection = GetEditingSettingsInterface()->GetSmallStringValue("DEV9/Hdd", "HddFile", ""); + const std::string current_display = hdd_selection.empty() ? std::string(FSUI_CSTR("None")) : std::string(Path::GetFileName(hdd_selection.c_str())); + if (MenuButtonWithValue(FSUI_ICONSTR(ICON_FA_HARD_DRIVE, "HDD Image Selection"), + FSUI_CSTR("Changes the HDD image used for PS2 internal storage."), + current_display.c_str(), hdd_enabled)) + { + ImGuiFullscreen::ChoiceDialogOptions choices; + choices.emplace_back(FSUI_STR("None"), hdd_selection.empty()); + + std::vector values; + values.push_back(""); + + FileSystem::FindResultsArray results; + FileSystem::FindFiles(EmuFolders::DataRoot.c_str(), "*.raw", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &results); + for (const FILESYSTEM_FIND_DATA& fd : results) + { + const std::string full_path = fd.FileName; + const std::string filename = std::string(Path::GetFileName(full_path)); + + // Get file size and determine LBA mode + const s64 file_size = FileSystem::GetPathFileSize(full_path.c_str()); + if (file_size > 0) + { + const int size_gb = static_cast(file_size / _1gb); + const bool uses_lba48 = (file_size > static_cast(120) * _1gb); + const std::string lba_mode = uses_lba48 ? "LBA48" : "LBA28"; + + choices.emplace_back(fmt::format("{} ({} GB, {})", filename, size_gb, lba_mode), + hdd_selection == full_path); + values.emplace_back(full_path); + } + } + + choices.emplace_back(FSUI_STR("Browse..."), false); + values.emplace_back("__browse__"); + + choices.emplace_back(FSUI_STR("Create New..."), false); + values.emplace_back("__create__"); + + OpenChoiceDialog(FSUI_CSTR("HDD Image Selection"), false, std::move(choices), + [game_settings = IsEditingGameSettings(bsi), values = std::move(values)](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + if (values[index] == "__browse__") + { + CloseChoiceDialog(); + + OpenFileSelector(FSUI_ICONSTR(ICON_FA_HARD_DRIVE, "Select HDD Image File"), false, + [game_settings](const std::string& path) { + if (path.empty()) + return; + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("DEV9/Hdd", "HddFile", path.c_str()); + SetSettingsChanged(bsi); + ShowToast(std::string(), fmt::format(FSUI_FSTR("Selected HDD image: {}"), Path::GetFileName(path))); + }, {"*.raw", "*"}, EmuFolders::DataRoot); + } + else if (values[index] == "__create__") + { + CloseChoiceDialog(); + + std::vector> size_options = { + {"40 GB (Recommended)", 40}, + {"80 GB", 80}, + {"120 GB (Max LBA28)", 120}, + {"200 GB", 200}, + {"Custom...", -1} + }; + + ImGuiFullscreen::ChoiceDialogOptions size_choices; + std::vector size_values; + for (const auto& [label, size] : size_options) + { + size_choices.emplace_back(label, false); + size_values.push_back(size); + } + + OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_PLUS, "Select HDD Size"), false, std::move(size_choices), + [game_settings, size_values = std::move(size_values)](s32 size_index, const std::string& size_title, bool size_checked) { + if (size_index < 0) + return; + + if (size_values[size_index] == -1) + { + CloseChoiceDialog(); + + OpenInputStringDialog( + FSUI_ICONSTR(ICON_FA_PEN_TO_SQUARE, "Custom HDD Size"), + FSUI_STR("Enter custom HDD size in gigabytes (40–2000):"), + std::string(), + FSUI_ICONSTR(ICON_FA_CHECK, "Create"), + [game_settings](std::string input) { + if (input.empty()) + return; + + std::optional custom_size_opt = StringUtil::FromChars(input); + if (!custom_size_opt.has_value()) + { + ShowToast(std::string(), FSUI_STR("Invalid size. Please enter a number between 40 and 2000.")); + return; + } + int custom_size_gb = custom_size_opt.value(); + + if (custom_size_gb < 40 || custom_size_gb > 2000) + { + ShowToast(std::string(), FSUI_STR("HDD size must be between 40 GB and 2000 GB.")); + return; + } + + const bool lba48 = (custom_size_gb > 120); + const std::string filename = fmt::format("DEV9hdd_{}GB_{}.raw", custom_size_gb, lba48 ? "LBA48" : "LBA28"); + const std::string filepath = Path::Combine(EmuFolders::DataRoot, filename); + + if (FileSystem::FileExists(filepath.c_str())) + { + OpenConfirmMessageDialog( + FSUI_ICONSTR(ICON_FA_TRIANGLE_EXCLAMATION, "File Already Exists"), + fmt::format(FSUI_FSTR("HDD image '{}' already exists. Do you want to overwrite it?"), filename), + [filepath, custom_size_gb, lba48, game_settings](bool confirmed) { + if (confirmed) + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("DEV9/Hdd", "HddFile", filepath.c_str()); + SetSettingsChanged(bsi); + FullscreenUI::CreateHardDriveWithProgress(filepath, custom_size_gb, lba48); + } + }); + } + else + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("DEV9/Hdd", "HddFile", filepath.c_str()); + SetSettingsChanged(bsi); + FullscreenUI::CreateHardDriveWithProgress(filepath, custom_size_gb, lba48); + } + }, + "40", + InputFilterType::Numeric); + return; + } + + const int size_gb = size_values[size_index]; + const bool lba48 = (size_gb > 120); + + const std::string filename = fmt::format("DEV9hdd_{}GB_{}.raw", size_gb, lba48 ? "LBA48" : "LBA28"); + const std::string filepath = Path::Combine(EmuFolders::DataRoot, filename); + + if (FileSystem::FileExists(filepath.c_str())) + { + OpenConfirmMessageDialog( + FSUI_ICONSTR(ICON_FA_TRIANGLE_EXCLAMATION, "File Already Exists"), + fmt::format(FSUI_FSTR("HDD image '{}' already exists. Do you want to overwrite it?"), filename), + [filepath, size_gb, lba48, game_settings](bool confirmed) { + if (confirmed) + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("DEV9/Hdd", "HddFile", filepath.c_str()); + SetSettingsChanged(bsi); + FullscreenUI::CreateHardDriveWithProgress(filepath, size_gb, lba48); + } + }); + } + else + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("DEV9/Hdd", "HddFile", filepath.c_str()); + SetSettingsChanged(bsi); + FullscreenUI::CreateHardDriveWithProgress(filepath, size_gb, lba48); + } + + CloseChoiceDialog(); + }); + } + else + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("DEV9/Hdd", "HddFile", values[index].c_str()); + SetSettingsChanged(bsi); + CloseChoiceDialog(); + } + }); + } EndMenuButtons(); } diff --git a/pcsx2/ImGui/FullscreenUI.h b/pcsx2/ImGui/FullscreenUI.h index bdf51ff51c..3bdbb5eda3 100644 --- a/pcsx2/ImGui/FullscreenUI.h +++ b/pcsx2/ImGui/FullscreenUI.h @@ -40,6 +40,9 @@ namespace FullscreenUI void Render(); void InvalidateCoverCache(); TinyString TimeToPrintableString(time_t t); + + bool CreateHardDriveWithProgress(const std::string& filePath, int sizeInGB, bool use48BitLBA = true); + void CancelAllHddOperations(); } // namespace FullscreenUI // Host UI triggers from Big Picture mode. diff --git a/pcsx2/ImGui/ImGuiFullscreen.cpp b/pcsx2/ImGui/ImGuiFullscreen.cpp index 9831764cd2..3e0ff697af 100644 --- a/pcsx2/ImGui/ImGuiFullscreen.cpp +++ b/pcsx2/ImGui/ImGuiFullscreen.cpp @@ -121,6 +121,7 @@ namespace ImGuiFullscreen static std::string s_input_dialog_text; static std::string s_input_dialog_ok_text; static InputStringDialogCallback s_input_dialog_callback; + static InputFilterType s_input_dialog_filter_type = InputFilterType::None; static bool s_message_dialog_open = false; static std::string s_message_dialog_title; @@ -2492,14 +2493,17 @@ bool ImGuiFullscreen::IsInputDialogOpen() } void ImGuiFullscreen::OpenInputStringDialog( - std::string title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback) + std::string title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback, + std::string default_value, InputFilterType filter_type) { s_input_dialog_open = true; s_input_dialog_title = std::move(title); s_input_dialog_message = std::move(message); s_input_dialog_caption = std::move(caption); s_input_dialog_ok_text = std::move(ok_button_text); + s_input_dialog_text = std::move(default_value); s_input_dialog_callback = std::move(callback); + s_input_dialog_filter_type = filter_type; QueueResetFocus(FocusResetType::PopupOpened); } @@ -2520,10 +2524,11 @@ void ImGuiFullscreen::DrawInputDialog() ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor); bool is_open = true; if (ImGui::BeginPopupModal(s_input_dialog_title.c_str(), &is_open, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { ResetFocusHere(); ImGui::TextWrapped("%s", s_input_dialog_message.c_str()); @@ -2542,7 +2547,40 @@ void ImGuiFullscreen::DrawInputDialog() { ImGui::SetNextItemWidth(ImGui::GetCurrentWindow()->WorkRect.GetWidth()); } - ImGui::InputText("##input", &s_input_dialog_text); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + + static auto input_callback = [](ImGuiInputTextCallbackData* data) -> int { + InputFilterType* filter_type = static_cast(data->UserData); + + if (data->EventFlag == ImGuiInputTextFlags_CallbackCharFilter) { + char c = static_cast(data->EventChar); + + if (*filter_type == InputFilterType::Numeric) { + if (!std::isdigit(c)) { + return 1; + } + } + else if (*filter_type == InputFilterType::IPAddress) { + if (!std::isdigit(c) && c != '.') { + return 1; + } + } + } + + return 0; + }; + + ImGuiInputTextFlags flags = ImGuiInputTextFlags_None; + if (s_input_dialog_filter_type != InputFilterType::None) + flags |= ImGuiInputTextFlags_CallbackCharFilter; + + if (s_focus_reset_queued != FocusResetType::None) + ImGui::SetKeyboardFocusHere(); + + ImGui::InputText("##input", &s_input_dialog_text, flags, + (s_input_dialog_filter_type != InputFilterType::None) ? input_callback : nullptr, + (s_input_dialog_filter_type != InputFilterType::None) ? static_cast(&s_input_dialog_filter_type) : nullptr); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); @@ -2574,7 +2612,7 @@ void ImGuiFullscreen::DrawInputDialog() else GetInputDialogHelpText(s_fullscreen_footer_text); - ImGui::PopStyleColor(3); + ImGui::PopStyleColor(4); ImGui::PopStyleVar(3); ImGui::PopFont(); } @@ -2591,6 +2629,7 @@ void ImGuiFullscreen::CloseInputDialog() s_input_dialog_ok_text = {}; s_input_dialog_text = {}; s_input_dialog_callback = {}; + s_input_dialog_filter_type = InputFilterType::None; } bool ImGuiFullscreen::IsMessageBoxDialogOpen() @@ -2831,61 +2870,56 @@ void ImGuiFullscreen::DrawBackgroundProgressDialogs(ImVec2& position, float spac if (s_background_progress_dialogs.empty()) return; - const float window_width = LayoutScale(500.0f); - const float window_height = LayoutScale(75.0f); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, UIPrimaryDarkColor); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, UISecondaryStrongColor); - ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, LayoutScale(4.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, LayoutScale(1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(10.0f, 10.0f)); - ImGui::PushFont(g_medium_font.first, g_medium_font.second); - - ImDrawList* dl = ImGui::GetForegroundDrawList(); - for (const BackgroundProgressDialogData& data : s_background_progress_dialogs) { - const float window_pos_x = position.x; - const float window_pos_y = position.y - ((s_notification_vertical_direction < 0.0f) ? window_height : 0.0f); + const std::string popup_id = fmt::format("##background_progress_dialog_{}", data.id); + ImGui::SetNextWindowSize(LayoutScale(600.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(popup_id.c_str()); - dl->AddRectFilled(ImVec2(window_pos_x, window_pos_y), ImVec2(window_pos_x + window_width, window_pos_y + window_height), - IM_COL32(0x11, 0x11, 0x11, 200), LayoutScale(10.0f)); + ImGui::PushFont(g_large_font.first, g_large_font.second); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); + ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, UISecondaryColor); - ImVec2 pos(window_pos_x + LayoutScale(10.0f), window_pos_y + LayoutScale(10.0f)); - dl->AddText(g_medium_font.first, g_medium_font.second, pos, IM_COL32(255, 255, 255, 255), data.message.c_str(), nullptr, 0.0f); - pos.y += g_medium_font.second + LayoutScale(10.0f); + bool is_open = true; + const u32 flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar; - const ImVec2 box_end(pos.x + window_width - LayoutScale(10.0f * 2.0f), pos.y + LayoutScale(25.0f)); - dl->AddRectFilled(pos, box_end, ImGui::GetColorU32(UIPrimaryDarkColor)); - - if (data.min != data.max) + if (ImGui::BeginPopupModal(popup_id.c_str(), &is_open, flags)) { - const float fraction = static_cast(data.value - data.min) / static_cast(data.max - data.min); - dl->AddRectFilled(pos, ImVec2(pos.x + fraction * (box_end.x - pos.x), box_end.y), ImGui::GetColorU32(UISecondaryColor)); + BeginMenuButtons(); + ResetFocusHere(); - const std::string text(fmt::format("{}%", static_cast(std::round(fraction * 100.0f)))); - const ImVec2 text_size(ImGui::CalcTextSize(text.c_str())); - const ImVec2 text_pos( - pos.x + ((box_end.x - pos.x) / 2.0f) - (text_size.x / 2.0f), pos.y + ((box_end.y - pos.y) / 2.0f) - (text_size.y / 2.0f)); - dl->AddText(g_medium_font.first, g_medium_font.second, text_pos, ImGui::GetColorU32(UIPrimaryTextColor), text.c_str()); - } - else - { - // indeterminate, so draw a scrolling bar - const float bar_width = LayoutScale(30.0f); - const float fraction = std::fmod(ImGui::GetTime(), 2.0f) * 0.5f; - const ImVec2 bar_start(pos.x + ImLerp(0.0f, box_end.x, fraction) - bar_width, pos.y); - const ImVec2 bar_end(std::min(bar_start.x + bar_width, box_end.x), pos.y + LayoutScale(25.0f)); - dl->AddRectFilled(ImClamp(bar_start, pos, box_end), ImClamp(bar_end, pos, box_end), ImGui::GetColorU32(UISecondaryColor)); + ImGui::TextWrapped("%s", data.message.c_str()); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(20.0f)); + + if (data.min != data.max) + { + const float progress = static_cast(data.value - data.min) / static_cast(data.max - data.min); + ImGui::ProgressBar(progress, ImVec2(-1.0f, LayoutScale(30.0f))); + } + else + { + const float fraction = std::fmod(ImGui::GetTime(), 2.0f) * 0.5f; + ImGui::ProgressBar(fraction, ImVec2(-1.0f, LayoutScale(30.0f))); + } + + EndMenuButtons(); + + ImGui::EndPopup(); } - position.y += s_notification_vertical_direction * (window_height + spacing); + ImGui::PopStyleColor(5); + ImGui::PopStyleVar(4); + ImGui::PopFont(); + break; } - - ImGui::PopFont(); - ImGui::PopStyleVar(4); - ImGui::PopStyleColor(2); } ////////////////////////////////////////////////////////////////////////// diff --git a/pcsx2/ImGui/ImGuiFullscreen.h b/pcsx2/ImGui/ImGuiFullscreen.h index 736dc7f323..242810658e 100644 --- a/pcsx2/ImGui/ImGuiFullscreen.h +++ b/pcsx2/ImGui/ImGuiFullscreen.h @@ -257,8 +257,16 @@ namespace ImGuiFullscreen using InputStringDialogCallback = std::function; bool IsInputDialogOpen(); + enum class InputFilterType : u8 + { + None, + Numeric, + IPAddress + }; + void OpenInputStringDialog( - std::string title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback); + std::string title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback, + std::string default_value = std::string(), InputFilterType filter_type = InputFilterType::None); void CloseInputDialog(); using ConfirmMessageDialogCallback = std::function;