#include #include #include "ppsspp_config.h" #undef new #ifdef SYSTEM_RAPIDJSON #include #else #include "ext/rapidjson/include/rapidjson/document.h" #endif #include "ext/pugixml/pugixml.hpp" #include "Common/DbgNew.h" #include "AdhocServerScreen.h" #include "Core/Util/GameDB.h" #include "Common/Net/Resolve.h" #include "Common/UI/Root.h" #include "Common/UI/PopupScreens.h" #include "Common/Data/Text/Parsers.h" #include "Common/StringUtils.h" #include "Common/Net/HTTPClient.h" #include "Core/HLE/sceNetAdhoc.h" #include "UI/MiscViews.h" static void UpgradeGameName(std::string *str) { if (str->size() == 9) { // TODO: Make a better heuristic, we might make some failed lookup into the DB. // It's probably a game ID. Convert it to a name using the database. std::vector infos; if (g_gameDB.GetGameInfos(*str, &infos)) { *str = infos[0].title; } } } static int ParseUserCountValue(const rapidjson::Value &v) { if (v.IsInt()) return v.GetInt(); else if (v.IsString()) { int value = 0; if (TryParse(v.GetString(), &value)) return value; } return 0; } static int ParsePortValue(const rapidjson::Value &v) { if (v.IsInt()) return v.GetInt(); return -1; } std::vector ParseStatusXML(const std::string& xmlInput) { pugi::xml_document doc; pugi::xml_parse_result result = doc.load_string(xmlInput.c_str()); std::vector gameList; if (!result) { ERROR_LOG(Log::sceNet, "XML Parsing Error: %s", result.description()); return gameList; } // Root is pugi::xml_node prometheus = doc.child("prometheus"); for (pugi::xml_node xmlGame : prometheus.children("game")) { AdhocGame game; game.name = xmlGame.attribute("name").as_string(); game.usercount = xmlGame.attribute("usercount").as_int(); for (pugi::xml_node xmlGroup : xmlGame.children("group")) { AdhocGroup group; group.name = xmlGroup.attribute("name").as_string(); group.usercount = xmlGroup.attribute("usercount").as_int(); for (pugi::xml_node xmlUser : xmlGroup.children("user")) { AdhocUser user; // In XML, the username is the text inside the tag user.name = xmlUser.child_value(); group.users.push_back(user); } game.groups.push_back(group); } gameList.push_back(game); } return gameList; } std::vector ParseDataJson(std::string_view json) { rapidjson::Document d; d.Parse(json.data(), json.size()); std::vector gameList; if (d.HasParseError() || !d.IsObject() || !d.HasMember("games") || !d["games"].IsArray()) return gameList; const auto& gamesArray = d["games"]; for (auto& g : gamesArray.GetArray()) { if (!g.IsObject()) continue; AdhocGame game; if (!g.HasMember("name") || !g["name"].IsString()) continue; game.name = g["name"].GetString(); UpgradeGameName(&game.name); game.usercount = g.HasMember("usercount") ? ParseUserCountValue(g["usercount"]) : 0; if (g.HasMember("game_ids") && g["game_ids"].IsArray()) { for (auto& id : g["game_ids"].GetArray()) { if (!id.IsString()) continue; game.game_ids.push_back(id.GetString()); } } if (g.HasMember("groups") && g["groups"].IsArray()) { for (auto& grp : g["groups"].GetArray()) { if (!grp.IsObject()) continue; AdhocGroup group; if (!grp.HasMember("name") || !grp["name"].IsString()) continue; group.name = grp["name"].GetString(); group.usercount = grp.HasMember("usercount") ? ParseUserCountValue(grp["usercount"]) : 0; if (grp.HasMember("users") && grp["users"].IsArray()) { for (auto& u : grp["users"].GetArray()) { if (!u.IsObject() || !u.HasMember("name") || !u["name"].IsString()) continue; AdhocUser user; user.name = u["name"].GetString(); if (u.HasMember("pdp_ports") && u["pdp_ports"].IsArray()) { for (auto& p : u["pdp_ports"].GetArray()) { int port = ParsePortValue(p); if (port >= 0) user.pdp_ports.push_back(port); } } if (u.HasMember("ptp_ports") && u["ptp_ports"].IsArray()) { for (auto& p : u["ptp_ports"].GetArray()) { int port = ParsePortValue(p); if (port >= 0) user.ptp_ports.push_back(port); } } group.users.push_back(user); } } game.groups.push_back(group); } } gameList.push_back(game); } return gameList; } class AdhocAddServerPopupScreen : public UI::PopupScreen { public: AdhocAddServerPopupScreen(std::string *outEditValue) : PopupScreen(T(I18NCat::NETWORKING, "Add server"), T(I18NCat::DIALOG, "Add"), T(I18NCat::DIALOG, "Cancel")), outEditValue_(outEditValue) { } void CreatePopupContents(UI::ViewGroup *parent) override { using namespace UI; auto ni = GetI18NCategory(I18NCat::NETWORKING); PopupTextInputChoice *textInputChoice = parent->Add(new PopupTextInputChoice(GetRequesterToken(), &editValue_, ni->T("Hostname or IP"), "", 450, screenManager())); textInputChoice->SetShadowText(ni->T("Hostname or IP")); parent->Add(new CheckBox(&hasRelay_, ni->T("Relay server mode"))); } virtual void OnCompleted(DialogResult result) override { if (result == DialogResult::DR_OK) { std::vector servers = AdhocGetServerList(AdhocLoadListMode::CacheOnlySync); bool preset = false; for (auto &iter : servers) { if (equalsNoCase(editValue_, iter.host)) { // We have this predefined. preset = true; } } if (!preset) { if (hasRelay_) { // Insert at the start of the vector. if (!ContainsNoCase(g_Config.vCustomAdhocServerListWithRelay, editValue_) && !ContainsNoCase(g_Config.vCustomAdhocServerList, editValue_)) { g_Config.vCustomAdhocServerListWithRelay.insert(g_Config.vCustomAdhocServerListWithRelay.begin(), editValue_); } } else { if (!ContainsNoCase(g_Config.vCustomAdhocServerList, editValue_) && !ContainsNoCase(g_Config.vCustomAdhocServerListWithRelay, editValue_)) { g_Config.vCustomAdhocServerList.insert(g_Config.vCustomAdhocServerList.begin(), editValue_); } } } *outEditValue_ = editValue_; } } virtual bool CanComplete(DialogResult result) override { return result == DR_OK ? !editValue_.empty() : true; } const char *tag() const override { return "AdhocAddServerPopup"; } private: std::string editValue_; std::string *outEditValue_; bool hasRelay_ = false; }; AdhocServerCompactInfo::AdhocServerCompactInfo(const AdhocServerListEntry &entry, UI::LayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT, UI::Margins(5.0f, 0.0f))), entry_(entry) { using namespace UI; SetSpacing(5.0f); LinearLayout *lines = Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(Margins(5, 5)))); lines->SetSpacing(0.0f); TextView *name = lines->Add(new TextView(entry.name)); std::string secondLine = entry.host; auto n = GetI18NCategory(I18NCat::NETWORKING); if (!entry.location.empty()) { secondLine += ": " + entry.location; } lines->Add(new TextView(secondLine))->SetTextSize(TextSize::Small)->SetWordWrap(); Add(new Spacer(0.0f, new LinearLayoutParams(1.0f, Margins(0.0f, 5.0f)))); if (entry.mode == AdhocDataMode::AemuPostoffice) { TextView *relay = Add(new TextView(n->T("Relay"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(10.0)))); } Add(new Choice(ImageID("I_FILE_COPY"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)))->OnClick.Add([host = entry_.host](UI::EventParams &) { System_CopyStringToClipboard(host); }); } void AdhocServerCompactInfo::Draw(UIContext &dc) { UI::LinearLayout::Draw(dc); // Underline dc.Draw()->DrawImageCenterTexel(dc.GetTheme().whiteImage, bounds_.x, bounds_.y2() - 2, bounds_.x2(), bounds_.y2(), dc.GetTheme().popupTitleStyle.fgColor); } static UI::View *CreateInfoItemWithButton(std::string_view text, ImageID buttonImage, std::function onClick) { using namespace UI; LinearLayout *line = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(12, 0))); line->Add(new TextView(text, new LinearLayoutParams(0.0f, Gravity::G_VCENTER))); line->Add(new Spacer(0, new LinearLayoutParams(1.0f))); line->Add(new Choice(buttonImage, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)))->OnClick.Add(onClick); return line; } static UI::View *CreateLinkButton(std::string url, std::string_view title = "") { using namespace UI; // steal strings from all over the place auto cr = GetI18NCategory(I18NCat::PSPCREDITS); auto st = GetI18NCategory(I18NCat::STORE); ImageID icon = ImageID("I_LINK_OUT_QUESTION"); if (startsWith(url, "https://discord")) { icon = ImageID("I_LOGO_DISCORD"); if (title.empty()) title = cr->T("Discord"); } else { icon = ImageID("I_LINK_OUT"); if (title.empty()) title = st->T("Website"); } Choice *choice = new Choice(title, icon, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)); choice->OnClick.Add([url](UI::EventParams &) { System_LaunchUrl(LaunchUrlType::BROWSER_URL, url); }); return choice; } // Later, this might also show games-in-progress. // For now, it's just a simple metadata viewer. AdhocServerInfoScreen::AdhocServerInfoScreen(const AdhocServerListEntry &entry) : UI::PopupScreen("", T(I18NCat::DIALOG, "Back")), entry_(entry) { std::string dataUrl; if (!entry.dataJsonUrl.empty()) { dataUrl = http::RemoveHttpsIfNeeded(entry.dataJsonUrl); } else if (!entry.statusXmlUrl.empty()) { dataUrl = http::RemoveHttpsIfNeeded(entry.statusXmlUrl); } if (!dataUrl.empty()) { statusRequest_ = g_DownloadManager.StartDownload(dataUrl, Path(), http::RequestFlags::KeepInMemory, nullptr, "status"); } } void CreateAdhocServerGameList(UI::ViewGroup *content, const std::vector &games, bool requestInProgress) { using namespace UI; auto ni = GetI18NCategory(I18NCat::NETWORKING); if (games.empty()) { if (requestInProgress) { // Still loading. Show a spinner. content->Add(new Spinner(nullptr, 0, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Margins(8)))); } else { content->Add(new TextView(ni->T("No games in progress on this server"))); } return; } for (const AdhocGame &game : games) { std::string title = game.name + " - " + ApplySafeSubstitutions(ni->T("players: %1"), game.usercount) + " " + ApplySafeSubstitutions(ni->T("groups: %1"), (int)game.groups.size()); CollapsibleSection *gameSection = content->Add(new CollapsibleSection(title)); gameSection->Header()->SetUnderline(false); for (const AdhocGroup &group : game.groups) { std::string groupName(StripSpaces(group.name)); if (groupName.empty()) { groupName = "???"; } if (group.usercount >= 1 && groupName == "Groupless") { gameSection->Add(new TextView(" " + ApplySafeSubstitutions(ni->T("Players waiting: %1"), group.usercount)))->SetTextSize(TextSize::Small); continue; } if (g_Config.bAdhocServerShowPlayerPorts) { // Show detailed info, each player on their own line. gameSection->Add(new TextView(" " + groupName + " - " + ApplySafeSubstitutions(ni->T("players: %1"), group.usercount)))->SetTextSize(TextSize::Small); for (const AdhocUser &user : group.users) { std::string portInfo; if (!user.pdp_ports.empty()) { portInfo += "PDP: "; for (int port : user.pdp_ports) { portInfo += std::to_string(port) + " "; } } if (!user.ptp_ports.empty()) { portInfo += "PTP: "; for (int port : user.ptp_ports) { portInfo += std::to_string(port) + " "; } } gameSection->Add(new TextView(" " + user.name + " " + portInfo))->SetTextSize(TextSize::Tiny); } } else { // Show each group on a single line. std::string groupString = " " + groupName + ApplySafeSubstitutions(" (%1):", group.usercount); bool first = true; for (const AdhocUser &user : group.users) { if (first) { groupString += " "; first = false; } else { groupString += ", "; } groupString += user.name; } gameSection->Add(new TextView(groupString))->SetTextSize(TextSize::Small)->SetWordWrap(); } } gameSection->SetOpen(false); // NOTE: Must be last! } } void AdhocServerInfoScreen::CreatePopupContents(UI::ViewGroup *parent) { using namespace UI; auto pa = GetI18NCategory(I18NCat::PAUSE); auto di = GetI18NCategory(I18NCat::DIALOG); auto ni = GetI18NCategory(I18NCat::NETWORKING); Margins contentMargins(12, 0); parent->Add(new AdhocServerCompactInfo(entry_, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins))); parent->Add(new Spacer(5.0f)); ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f)); LinearLayout *content = new LinearLayout(ORIENT_VERTICAL); content->SetSpacing(6.0f); if (!entry_.ip.empty()) { content->Add(new InfoItem(entry_.ip, "")); } TextView *desc = content->Add(new TextView(entry_.description, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins))); desc->SetTextSize(TextSize::Small); desc->SetWordWrap(); LinearLayout *buttonStrip = content->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins))); buttonStrip->SetSpacing(8); if (!entry_.web.empty() || !entry_.discord.empty()) { if (!entry_.web.empty()) { buttonStrip->Add(CreateLinkButton(entry_.web)); } if (!entry_.discord.empty()) { buttonStrip->Add(CreateLinkButton(entry_.discord)); } } if (entry_.dataJsonUrl.empty() && entry_.statusXmlUrl.empty()) { content->Add(CreateInfoItemWithButton(ni->T("This server has no data.json status page"), ImageID("I_LINK_OUT_QUESTION"), [](UI::EventParams &e) { System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/docs/multiplayer/adhoc-server-status/"); })); if (!entry_.statusWebUrl.empty()) { buttonStrip->Add(CreateLinkButton(entry_.statusWebUrl, ni->T("Status"))); } if (!entry_.statusXmlUrl.empty()) { buttonStrip->Add(CreateLinkButton(entry_.statusXmlUrl, ni->T("Status"))); } } else { CreateAdhocServerGameList(content, games_, statusRequest_.get() ? true : false); } scroll->Add(content); parent->Add(scroll); } void AdhocServerInfoScreen::update() { UI::PopupScreen::update(); if (statusRequest_ && statusRequest_->Done()) { std::string data; statusRequest_->buffer().TakeAll(&data); if (endsWith(statusRequest_->url(), ".xml")) { games_ = ParseStatusXML(data); } else { games_ = ParseDataJson(data); } statusRequest_.reset(); RecreateViews(); } } void AddDeleteButton(std::string *editValue, ScreenManager *screenManager, UI::ViewGroup *viewGroup, const AdhocServerListEntry &entry) { using namespace UI; Choice *deleteButton = viewGroup->Add(new Choice(ImageID("I_TRASHCAN"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(0, 0, 10, 0)))); deleteButton->OnClick.Add([host = entry.host, screenManager, editValue](UI::EventParams &e) { auto di = GetI18NCategory(I18NCat::DIALOG); const std::string quotedHost = "\"" + host + "\""; const std::string message = ApplySafeSubstitutions(di->T("Are you sure you want to delete %1?"), quotedHost); screenManager->push(new UI::MessagePopupScreen(di->T("Delete"), message, di->T("Delete"), di->T("Cancel"), [host, editValue](bool confirmed) { if (confirmed) { RemoveNoCase(g_Config.vCustomAdhocServerList, host); RemoveNoCase(g_Config.vCustomAdhocServerListWithRelay, host); if (*editValue == host) { // Reset to socom.cc, which will always be in a list. *editValue = DefaultProAdhocServer(); } } })); }); } class AdhocServerRow : public UI::LinearLayout { public: AdhocServerRow(std::string *value, const AdhocServerListEntry &entry, bool showDeleteButton, ScreenManager *screenManager, UI::LayoutParams *layoutParams = nullptr); void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { w = 500; h = 90; } void Draw(UIContext &dc) override { dc.FillRect(dc.GetTheme().itemStyle.background, bounds_); if (*value_ == entry_.host) { // TODO: Make this highlight themable dc.FillRect(UI::Drawable(0x48FFFFFF), GetBounds()); } LinearLayout::Draw(dc); } bool Touch(const TouchInput &input) override { using namespace UI; if (UI::LinearLayout::Touch(input)) { return true; } if (input.flags & TouchInputFlags::DOWN) { if (bounds_.Contains(input.x, input.y)) { dragging_ = true; return true; } } if (dragging_ && (input.flags & TouchInputFlags::UP)) { dragging_ = false; if (!(input.flags & TouchInputFlags::CANCEL) && bounds_.Contains(input.x, input.y)) { EventParams e; e.v = this; OnSelected.Trigger(e); return true; } } return false; } UI::Event OnSelected; private: bool dragging_ = false; std::string *value_; AdhocServerListEntry entry_; }; AdhocServerRow::AdhocServerRow(std::string *editValue, const AdhocServerListEntry &entry, bool showDeleteButton, ScreenManager *screenManager, UI::LayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT, UI::Margins(5.0f, 0.0f))), value_(editValue), entry_(entry) { using namespace UI; SetSpacing(5.0f); // Show as radio button to make it really clear that selection actually is the choice. int number = 0; Add(new ImageView([editValue, host = entry.host]() { return host == *editValue ? ImageID("I_RADIO_SELECTED") : ImageID("I_RADIO_EMPTY"); }, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(5, 0, 0, 0)))); LinearLayout *lines = Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(Margins(5, 5)))); lines->SetSpacing(0.0f); ClickableTextView *name = lines->Add(new ClickableTextView(entry.name)); name->SetFocusable(true); name->OnClick.Add([this](UI::EventParams &e) { EventParams e2; e2.v = this; OnSelected.Trigger(e2); }); std::string secondLine = entry.host; auto n = GetI18NCategory(I18NCat::NETWORKING); if (entry.host == "localhost") { // Special case this to add a hint. secondLine = n->T("Ad hoc server address hint"); } if (!entry.location.empty()) { secondLine += ": " + entry.location; } lines->Add(new TextView(secondLine))->SetTextSize(TextSize::Small)->SetWordWrap(); Add(new Spacer(0.0f, new LinearLayoutParams(1.0f, Margins(0.0f, 5.0f)))); if (entry.mode == AdhocDataMode::AemuPostoffice) { TextView *relay = Add(new TextView(n->T("Relay"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(10.0)))); } if (showDeleteButton) { AddDeleteButton(editValue, screenManager, this, entry); } if (!entry.description.empty()) { Choice *infoButton = Add(new Choice(ImageID("I_INFO"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(0, 0, 10, 0)))); AdhocServerListEntry copy = entry; infoButton->OnClick.Add([this, copy, screenManager](UI::EventParams &e) { e.v = this; screenManager->push(new AdhocServerInfoScreen(copy)); }); } } AdhocServerScreen::AdhocServerScreen(std::string *value, std::string_view title) : UI::PopupScreen(title, T(I18NCat::DIALOG, "OK"), T(I18NCat::DIALOG, "Cancel")), value_(value) { resolver_ = std::thread([](AdhocServerScreen *thiz) { thiz->ResolverThread(); }, this); editValue_ = *value; AdhocLoadServerList(AdhocLoadListMode::AllSourcesAsync); } AdhocServerScreen::~AdhocServerScreen() { { std::unique_lock guard(resolverLock_); resolverState_ = ResolverState::QUIT; resolverCond_.notify_one(); } resolver_.join(); } void AdhocServerScreen::sendMessage(UIMessage message, const char *value) { if (message == UIMessage::ADHOC_SERVER_LIST_CHANGED) { RecreateViews(); } } void AdhocServerScreen::CreatePopupContents(UI::ViewGroup *parent) { using namespace UI; auto sy = GetI18NCategory(I18NCat::SYSTEM); auto di = GetI18NCategory(I18NCat::DIALOG); auto n = GetI18NCategory(I18NCat::NETWORKING); // We already kicked off loading the server list in the constructor, so by now it should be either loaded or in the process of loading. // We can get the cached list immediately, and then update it when the async load finishes. std::vector listItems = AdhocGetServerList(AdhocLoadListMode::CacheOnlySync); Choice *addServer = parent->Add(new Choice(n->T("Add server"), ImageID("I_PLUS"))); addServer->OnClick.Add([this](UI::EventParams &e) { AdhocAddServerPopupScreen *addScreen = new AdhocAddServerPopupScreen(&editValue_); screenManager()->push(addScreen); }); parent->Add(new Spacer(5.0f)); // editValue_ has the currently selected server. On closing the dialog, we copy that to settings. // Start with the downloaded list. std::vector entries = listItems; std::vector localEntries; std::vector customEntries; bool currentServerFound = false; // If the current server is not found, we'll have to add it to one of the lists. for (const auto &iter : entries) { if (iter.host == editValue_) { currentServerFound = true; } } // Add localhost and local IPs, since those are common ones to connect to. { AdhocServerListEntry localhostEntry; localhostEntry.name = "localhost"; localhostEntry.host = "localhost"; localEntries.push_back(localhostEntry); std::vector listIP; net::GetLocalIP4List(listIP); for (const auto &ipAddress : listIP) { if (startsWith(ipAddress, "127.") || startsWith(ipAddress, "169.254.") || startsWith(ipAddress, "0.")) { continue; } AdhocServerListEntry entry; entry.name = ipAddress; entry.host = ipAddress; localEntries.push_back(entry); if (ipAddress == editValue_) { currentServerFound = true; } } } auto hostInEntries = [entries](const std::string &host) { for (const auto &entry : entries) { if (entry.host == host) { return true; } } return false; }; for (auto iter = g_Config.vCustomAdhocServerListWithRelay.begin(); iter != g_Config.vCustomAdhocServerListWithRelay.end();) { // If the host is already in the public list, skip it. We don't want duplicates. if (hostInEntries(*iter) || iter->empty()) { // Remove things that duplicate the public list, or that are empty (probably added by mistake). iter = g_Config.vCustomAdhocServerListWithRelay.erase(iter); recreateParent_ = true; continue; } AdhocServerListEntry entry; entry.name = *iter; entry.host = *iter; entry.mode = AdhocDataMode::AemuPostoffice; customEntries.push_back(entry); if (*iter == editValue_) { currentServerFound = true; } iter++; } for (auto iter = g_Config.vCustomAdhocServerList.begin(); iter != g_Config.vCustomAdhocServerList.end();) { // If the host is already in the public list, skip it. We don't want duplicates. if (hostInEntries(*iter) || iter->empty()) { // Remove things that duplicate the public list, or that are empty (probably added by mistake). iter = g_Config.vCustomAdhocServerList.erase(iter); recreateParent_ = true; continue; } AdhocServerListEntry entry; entry.name = *iter; entry.host = *iter; entry.mode = AdhocDataMode::P2P; customEntries.push_back(entry); if (*iter == editValue_) { currentServerFound = true; } iter++; } ScrollView *scrollView = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)); LinearLayout *innerView = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); innerView->SetSpacing(5.0f); auto AddButtonFromEntry = [this](UI::ViewGroup *parent, const AdhocServerListEntry &entry, bool showDeleteButton) { AdhocServerRow *row = new AdhocServerRow(&editValue_, entry, showDeleteButton, screenManager()); parent->Add(row); row->OnSelected.Add([this](UI::EventParams &e) { std::string value = e.v->Tag(); if (!value.empty()) { editValue_ = value; } }); row->SetTag(entry.host); }; if (!customEntries.empty()) { CollapsibleSection *customSection = innerView->Add(new CollapsibleSection(n->T("Custom server list"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); if (!currentServerFound) { // Add the entry to one of the lists. AdhocServerListEntry entry; entry.name = editValue_; entry.host = editValue_; // Let's do a heuristic, we don't have a good value here.. entry.mode = (AdhocServerRelayMode)g_Config.iAdhocServerRelayMode == AdhocServerRelayMode::AlwaysOn ? AdhocDataMode::AemuPostoffice : AdhocDataMode::P2P; if (entry.mode == AdhocDataMode::AemuPostoffice) { g_Config.vCustomAdhocServerListWithRelay.insert(g_Config.vCustomAdhocServerListWithRelay.begin(), editValue_); } else { g_Config.vCustomAdhocServerList.insert(g_Config.vCustomAdhocServerList.begin(), editValue_); } recreateParent_ = true; AddButtonFromEntry(customSection, entry, true); } for (const auto &entry : customEntries) { AddButtonFromEntry(customSection, entry, true); } } CollapsibleSection *publicSection = innerView->Add(new CollapsibleSection(n->T("Public server list"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); for (const auto &entry : entries) { // Show even hidden entries, as long as they are chosen currently. if (entry.hidden && entry.host != g_Config.sProAdhocServer) continue; AddButtonFromEntry(publicSection, entry, false); } CollapsibleSection *localSection = innerView->Add(new CollapsibleSection(n->T("Local network addresses"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); for (const auto &entry : localEntries) { AddButtonFromEntry(localSection, entry, false); } scrollView->Add(innerView); parent->Add(scrollView); progressView_ = parent->Add(new NoticeView(NoticeLevel::INFO, n->T("Validating address..."), "", new LinearLayoutParams(Margins(0, 5, 0, 0)))); progressView_->SetVisibility(UI::V_GONE); } void AdhocServerScreen::ResolverThread() { std::unique_lock guard(resolverLock_); while (resolverState_ != ResolverState::QUIT) { resolverCond_.wait(guard); if (resolverState_ == ResolverState::QUEUED) { resolverState_ = ResolverState::PROGRESS; addrinfo *resolved = nullptr; std::string err; toResolveResult_ = net::DNSResolve(toResolve_, "80", &resolved, err); if (resolved) net::DNSResolveFree(resolved); resolverState_ = ResolverState::READY; } } } bool AdhocServerScreen::CanComplete(DialogResult result) { auto n = GetI18NCategory(I18NCat::NETWORKING); if (result != DR_OK) return true; std::string value = editValue_; if (lastResolved_ == value) { return true; } // Currently running. if (resolverState_ == ResolverState::PROGRESS) return false; std::lock_guard guard(resolverLock_); switch (resolverState_) { case ResolverState::PROGRESS: case ResolverState::QUIT: return false; case ResolverState::QUEUED: case ResolverState::WAITING: break; case ResolverState::READY: if (toResolve_ == value) { // Reset the state, nothing there now. resolverState_ = ResolverState::WAITING; toResolve_.clear(); lastResolved_ = value; lastResolvedResult_ = toResolveResult_; if (lastResolvedResult_) { progressView_->SetVisibility(UI::V_GONE); } else { progressView_->SetText(n->T("Invalid IP or hostname")); progressView_->SetLevel(NoticeLevel::ERROR); progressView_->SetVisibility(UI::V_VISIBLE); } return true; } // Throw away that last result, it was for a different value. break; } resolverState_ = ResolverState::QUEUED; toResolve_ = value; resolverCond_.notify_one(); progressView_->SetText(n->T("Validating address...")); progressView_->SetLevel(NoticeLevel::INFO); progressView_->SetVisibility(UI::V_VISIBLE); return false; } void AdhocServerScreen::OnCompleted(DialogResult result) { if (result == DR_OK) { *value_ = StripSpaces(editValue_); } } bool AdhocServerNameIsCustom() { for (auto iter : g_Config.vCustomAdhocServerList) { if (iter == g_Config.sProAdhocServer) { return true; } } for (auto iter : g_Config.vCustomAdhocServerListWithRelay) { if (iter == g_Config.sProAdhocServer) { return true; } } return false; } static void EditServerName(std::string_view newServerName) { newServerName = StripSpaces(newServerName); if (newServerName != g_Config.sProAdhocServer) { for (auto &name : g_Config.vCustomAdhocServerList) { if (name == g_Config.sProAdhocServer) { name = newServerName; } } for (auto &name : g_Config.vCustomAdhocServerListWithRelay) { if (name == g_Config.sProAdhocServer) { name = newServerName; } } g_Config.sProAdhocServer = newServerName; } } void AskToEditCurrentServer(int requestToken, ScreenManager *screenManager) { using namespace UI; auto n = GetI18NCategory(I18NCat::NETWORKING); // Choose method depending on platform capabilities. if (System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) { System_InputBoxGetString(requestToken, n->T("Ad hoc server address"), g_Config.sProAdhocServer, false, [](std::string_view enteredValue, int) { EditServerName(enteredValue); }); return; } static std::string editText = g_Config.sProAdhocServer; TextEditPopupScreen *popupScreen = new TextEditPopupScreen(&editText, editText, n->T("Ad hoc server address"), 256); if (System_GetPropertyBool(SYSPROP_KEYBOARD_IS_SOFT)) { popupScreen->SetAlignTop(true); } popupScreen->OnChange.Add([](EventParams &e) { EditServerName(editText); }); screenManager->push(popupScreen); }