/* Copyright 2025 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "settings.h" #include "gui.h" #include "network/ggpo.h" #include "network/ice.h" #include "imgui_stdlib.h" void gui_settings_network() { ImGuiStyle& style = ImGui::GetStyle(); header(T("Network Type")); { DisabledScope scope(game_started); int netType = 0; if (config::GGPOEnable) netType = 1; else if (config::NetworkEnable) netType = 2; else if (config::BattleCableEnable) netType = 3; ImGui::Columns(4, "networkType", false); ImGui::RadioButton(T("Native"), &netType, 0); ImGui::SameLine(0, style.ItemInnerSpacing.x); ShowHelpMarker(T("Use native Dreamcast online features using the modem or Broadband Adapter")); ImGui::NextColumn(); ImGui::RadioButton("GGPO", &netType, 1); ImGui::SameLine(0, style.ItemInnerSpacing.x); ShowHelpMarker(T("Enable networking using GGPO")); ImGui::NextColumn(); ImGui::RadioButton("Naomi", &netType, 2); ImGui::SameLine(0, style.ItemInnerSpacing.x); ShowHelpMarker(T("Enable networking for supported Naomi and Atomiswave games")); ImGui::NextColumn(); ImGui::RadioButton(T("Battle Cable"), &netType, 3); ImGui::SameLine(0, style.ItemInnerSpacing.x); ShowHelpMarker(T("Emulate the Taisen (Battle) null modem cable for games that support it")); ImGui::Columns(1, nullptr, false); config::GGPOEnable = false; config::NetworkEnable = false; config::BattleCableEnable = false; switch (netType) { case 1: config::GGPOEnable = true; break; case 2: config::NetworkEnable = true; break; case 3: config::BattleCableEnable = true; break; } } if (config::GGPOEnable || config::NetworkEnable || config::BattleCableEnable) { ImGui::Spacing(); header(T("Configuration")); } { if (config::GGPOEnable) { config::NetworkEnable = false; OptionCheckbox(T("Play as Player 1"), config::ActAsServer, T("Deselect to play as player 2")); InputText(T("Peer"), &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CallbackCharFilter, dnsCharFilter); ImGui::SameLine(); ShowHelpMarker(T("Your peer IP address and optional port")); OptionSlider(T("Frame Delay"), config::GGPODelay, 0, 20, T("Sets Frame Delay, advisable for sessions with ping >100 ms")); ImGui::Text("%s", T("Left Thumbstick:")); OptionRadioButton((Ts("Disabled") + "##analogaxis").c_str(), config::GGPOAnalogAxes, 0, T("Left thumbstick not used")); ImGui::SameLine(); OptionRadioButton(T("Horizontal"), config::GGPOAnalogAxes, 1, T("Use the left thumbstick horizontal axis only")); ImGui::SameLine(); OptionRadioButton(T("Full"), config::GGPOAnalogAxes, 2, T("Use the left thumbstick horizontal and vertical axes")); OptionCheckbox(T("Enable Chat"), config::GGPOChat, T("Open the chat window when a chat message is received")); if (config::GGPOChat) { OptionCheckbox(T("Enable Chat Window Timeout"), config::GGPOChatTimeoutToggle, T("Automatically close chat window after 20 seconds")); if (config::GGPOChatTimeoutToggle) { char chatTimeout[256]; snprintf(chatTimeout, sizeof(chatTimeout), "%d", (int)config::GGPOChatTimeout); InputText(T("Chat Window Timeout (seconds)"), chatTimeout, sizeof(chatTimeout), ImGuiInputTextFlags_CharsDecimal); ImGui::SameLine(); ShowHelpMarker(T("Sets duration that chat window stays open after new message is received.")); config::GGPOChatTimeout.set(atoi(chatTimeout)); } } OptionCheckbox(T("Network Statistics"), config::NetworkStats, T("Display network statistics on screen")); } else if (config::NetworkEnable) { // Naomi networking ImGui::Text("%s", T("Network Role")); int role = 0; if (!config::ActAsServer) role = config::NaomiSatellite ? 2 : 1; ImGui::Columns(3, "networkRole", false); ImGui::RadioButton(T("Master"), &role, 0); ImGui::SameLine(0, style.ItemInnerSpacing.x); ShowHelpMarker(T("Create a local server for Naomi network games")); ImGui::NextColumn(); ImGui::RadioButton(T("Slave"), &role, 1); ImGui::SameLine(0, style.ItemInnerSpacing.x); ShowHelpMarker(T("Connect to the master server")); ImGui::NextColumn(); ImGui::RadioButton(T("Satellite"), &role, 2); ImGui::SameLine(0, style.ItemInnerSpacing.x); ShowHelpMarker(T("Live monitor for games that support it (Virtual-On Oratorio Tangram and Club Kart)")); ImGui::Columns(1, nullptr, false); if (!config::ActAsServer) { InputText(T("Server"), &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CallbackCharFilter, dnsCharFilter); ImGui::SameLine(); ShowHelpMarker(T("The server to connect to. Leave blank to find a server automatically on the default port")); } char localPort[256]; snprintf(localPort, sizeof(localPort), "%d", (int)config::LocalPort); InputText(T("Local Port"), localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal); ImGui::SameLine(); ShowHelpMarker(T("The local UDP port to use")); config::LocalPort.set(atoi(localPort)); switch (role) { case 0: config::ActAsServer = true; config::NaomiSatellite = false; break; case 1: config::ActAsServer = false; config::NaomiSatellite = false; break; case 2: config::ActAsServer = false; config::NaomiSatellite = true; break; } } else if (config::BattleCableEnable) { #if defined(USE_ICE) && defined(USE_WEBSOCKET) if (ImGui::BeginTabBar("battleMode", ImGuiTabBarFlags_NoTooltip)) { if (ImGui::BeginTabItem(T("Match Code"))) { ice::State state = ice::getState(); ImGuiInputTextFlags textFlags = state == ice::Offline ? ImGuiInputTextFlags_CharsNoBlank : ImGuiInputTextFlags_ReadOnly; static std::string matchCode; InputText(T("Code"), &matchCode, textFlags); ImGui::SameLine(); ShowHelpMarker(T("Choose a unique word or number and share it with your opponent.")); if (state == ice::Offline) { if (ImGui::Button(T("Connect")) && !matchCode.empty()) ice::init(matchCode, true); } else { if (ImGui::Button(T("Disconnect"))) try { ice::term(); } catch (...) {} } std::string status; switch (state) { case ice::Offline: status = ice::getStatusText(); break; case ice::Online: status = T("Waiting at meeting point..."); break; case ice::ChalAccepted: status = T("Preparing game..."); break; case ice::Playing: status = strprintf(T("Playing %s (%s)"), matchCode.c_str(), ice::getStatusText().c_str()); break; default: break; } ImGui::TextDisabled("%s", status.c_str()); OptionCheckbox(T("Network Statistics"), config::NetworkStats, T("Display network statistics on screen")); ImGui::EndTabItem(); } if (ImGui::BeginTabItem(T("Manual"))) { #endif InputText(T("Peer"), &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CallbackCharFilter, dnsCharFilter); ImGui::SameLine(); ShowHelpMarker(T("The peer to connect to. Leave blank to find a player automatically on the default port")); char localPort[256]; snprintf(localPort, sizeof(localPort), "%d", (int)config::LocalPort); InputText(T("Local Port"), localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal); ImGui::SameLine(); ShowHelpMarker(T("The local UDP port to use")); config::LocalPort.set(atoi(localPort)); #if defined(USE_ICE) && defined(USE_WEBSOCKET) ImGui::EndTabItem(); } ImGui::EndTabBar(); } #endif OptionCheckbox(T("Act as Master"), config::ActAsServer, T("Only used for Maximum Speed. One of the peer must be master.")); } } ImGui::Spacing(); header(T("Network Options")); { OptionCheckbox(T("Enable UPnP"), config::EnableUPnP, T("Automatically configure your network router for netplay")); OptionCheckbox(T("Broadcast Digital Outputs"), config::NetworkOutput, T("Broadcast digital outputs and force-feedback state on TCP port 8000. " "Compatible with the \"-output network\" MAME option. Arcade games only.")); { DisabledScope scope(game_started); OptionCheckbox(T("Broadband Adapter Emulation"), config::EmulateBBA, T("Emulate the Ethernet Broadband Adapter (BBA) instead of the Modem")); } OptionCheckbox(T("Use DCNet"), config::UseDCNet, T("Use the DCNet cloud service for Dreamcast Internet access.")); std::string& ispUsername = config::ISPUsername; InputText(T("ISP User Name"), &ispUsername, ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CallbackCharFilter, [](ImGuiInputTextCallbackData *data) { return static_cast(data->EventChar <= ' ' || data->EventChar > '~'); }); // CallbackCharFilter isn't called on switch auto it = std::remove_if(ispUsername.begin(), ispUsername.end(), [](char c) { return c <= ' ' || c > '~'; }); ispUsername.erase(it, ispUsername.end()); ImGui::SameLine(); ShowHelpMarker(T("The ISP user name stored in the console Flash RAM. Used by some online games as the player name. Leave blank to keep the current Flash RAM value.")); #if !defined(NDEBUG) || defined(DEBUGFAST) { DisabledScope scope(config::UseDCNet); InputText("DNS", &config::DNS.get(), ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CallbackCharFilter, dnsCharFilter); ImGui::SameLine(); ShowHelpMarker(T("DNS server name or IP address")); } #endif } #ifdef NAOMI_MULTIBOARD ImGui::Spacing(); header(T("Multiboard Screens")); { OptionRadioButton(T("1 (Twin, Satellite)"), config::MultiboardSlaves, 1, T("One screen configuration (F355 Twin, Derby Owners Club satellite)")); ImGui::SameLine(); OptionRadioButton(T("2+ (Deluxe, Main screen)"), config::MultiboardSlaves, 2, T("Two or three screens configuration (Airline Pilot, Derby Owners Club main screen, F355 Deluxe, Sega Strike Fighter)")); } #endif }