1 /** @file multiplayerservermenuwidget.cpp 2 * 3 * @authors Copyright (c) 2016-2017 Jaakko Keränen <jaakko.keranen@iki.fi> 4 * 5 * @par License 6 * GPL: http://www.gnu.org/licenses/gpl.html 7 * 8 * <small>This program is free software; you can redistribute it and/or modify 9 * it under the terms of the GNU General Public License as published by the 10 * Free Software Foundation; either version 2 of the License, or (at your 11 * option) any later version. This program is distributed in the hope that it 12 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 14 * Public License for more details. You should have received a copy of the GNU 15 * General Public License along with this program; if not, see: 16 * http://www.gnu.org/licenses</small> 17 */ 18 19 #include "ui/widgets/multiplayerservermenuwidget.h" 20 #include "ui/home/multiplayerpanelbuttonwidget.h" 21 #include "ui/widgets/homemenuwidget.h" 22 #include "network/serverlink.h" 23 #include "clientapp.h" 24 25 #include <doomsday/Games> 26 #include <de/MenuWidget> 27 #include <de/Address> 28 29 using namespace de; 30 31 DENG2_PIMPL(MultiplayerServerMenuWidget) 32 , DENG2_OBSERVES(DoomsdayApp, GameChange) 33 , DENG2_OBSERVES(Games, Readiness) 34 , DENG2_OBSERVES(ServerLink, DiscoveryUpdate) 35 , DENG2_OBSERVES(MultiplayerPanelButtonWidget, AboutToJoin) 36 , public ChildWidgetOrganizer::IWidgetFactory 37 { 38 static ServerLink &link() { return ClientApp::serverLink(); } 39 40 static String hostId(shell::ServerInfo const &sv) 41 { 42 if (sv.serverId()) 43 { 44 return String::format("%x", sv.serverId()); 45 } 46 return sv.address().asText(); 47 } 48 49 /** 50 * Data item with information about a found server. 51 */ 52 class ServerListItem : public ui::Item 53 { 54 public: 55 ServerListItem(shell::ServerInfo const &serverInfo, bool isLocal) 56 : _lan(isLocal) 57 { 58 setData(hostId(serverInfo)); 59 _info = serverInfo; 60 } 61 62 bool isLocal() const 63 { 64 return _lan; 65 } 66 67 void setLocal(bool isLocal) 68 { 69 _lan = isLocal; 70 } 71 72 shell::ServerInfo const &info() const 73 { 74 return _info; 75 } 76 77 void setInfo(shell::ServerInfo const &serverInfo) 78 { 79 _info = serverInfo; 80 notifyChange(); 81 } 82 83 String title() const 84 { 85 return _info.name(); 86 } 87 88 String gameId() const 89 { 90 return _info.gameId(); 91 } 92 93 private: 94 shell::ServerInfo _info; 95 bool _lan; 96 }; 97 98 DiscoveryMode mode = NoDiscovery; 99 ServerLink::FoundMask mask = ServerLink::Any; 100 101 Impl(Public *i) : Base(i) 102 { 103 DoomsdayApp::app().audienceForGameChange() += this; 104 DoomsdayApp::games().audienceForReadiness() += this; 105 link().audienceForDiscoveryUpdate() += this; 106 107 self().organizer().setWidgetFactory(*this); 108 } 109 110 void linkDiscoveryUpdate(ServerLink const &link) override 111 { 112 ui::Data &items = self().items(); 113 114 QSet<String> foundHosts; 115 foreach (Address const &host, link.foundServers(mask)) 116 { 117 shell::ServerInfo info; 118 if (link.foundServerInfo(host, info, mask)) 119 { 120 foundHosts.insert(hostId(info)); 121 } 122 } 123 124 // Remove obsolete entries. 125 for (ui::Data::Pos idx = 0; idx < items.size(); ++idx) 126 { 127 String const id = items.at(idx).data().toString(); 128 if (!foundHosts.contains(id)) 129 { 130 items.remove(idx--); 131 } 132 } 133 134 // Add new entries and update existing ones. 135 foreach (Address const &host, link.foundServers(mask)) 136 { 137 shell::ServerInfo info; 138 if (!link.foundServerInfo(host, info, mask)) continue; 139 140 ui::Data::Pos found = items.findData(hostId(info)); 141 const bool isLocal = link.isServerOnLocalNetwork(info.address()); 142 143 if (found == ui::Data::InvalidPos) 144 { 145 // Needs to be added. 146 items.append(new ServerListItem(info, isLocal)); 147 } 148 else 149 { 150 // Update the info of an existing item. 151 auto &it = items.at(found).as<ServerListItem>(); 152 153 // Prefer the info received via LAN, if the server is the same instance. 154 if (!it.isLocal() || isLocal) 155 { 156 it.setInfo(info); 157 it.setLocal(isLocal); 158 } 159 } 160 } 161 162 items.stableSort([] (ui::Item const &a, ui::Item const &b) 163 { 164 auto const &first = a.as<ServerListItem>(); 165 auto const &second = b.as<ServerListItem>(); 166 167 // LAN games shown first. 168 if (first.isLocal() == second.isLocal()) 169 { 170 // Sort by number of players. 171 if (first.info().playerCount() == second.info().playerCount()) 172 { 173 // Finally, by game ID. 174 int cmp = first.info().gameId().compareWithCase(second.info().gameId()); 175 if (!cmp) 176 { 177 // Lastly by server name. 178 return first.info().name().compareWithoutCase(second.info().name()) < 0; 179 } 180 return cmp < 0; 181 } 182 return first.info().playerCount() - second.info().playerCount() > 0; 183 } 184 return first.isLocal(); 185 }); 186 } 187 188 void currentGameChanged(Game const &newGame) override 189 { 190 if (newGame.isNull() && mode == DiscoverUsingMaster) 191 { 192 // If the session menu exists across game changes, it's good to 193 // keep it up to date. 194 link().discoverUsingMaster(); 195 } 196 } 197 198 void gameReadinessUpdated() override 199 { 200 foreach (GuiWidget *w, self().childWidgets()) 201 { 202 updateAvailability(*w); 203 } 204 } 205 206 void updateAvailability(GuiWidget &menuItemWidget) 207 { 208 auto const &item = self().organizer().findItemForWidget(menuItemWidget)->as<ServerListItem>(); 209 210 bool playable = false; 211 String gameId = item.gameId(); 212 if (DoomsdayApp::games().contains(gameId)) 213 { 214 playable = DoomsdayApp::games()[gameId].isPlayable(); 215 } 216 menuItemWidget.enable(playable); 217 } 218 219 void aboutToJoinMultiplayerGame(shell::ServerInfo const &sv) override 220 { 221 DENG2_FOR_PUBLIC_AUDIENCE2(AboutToJoin, i) i->aboutToJoinMultiplayerGame(sv); 222 } 223 224 //- ChildWidgetOrganizer::IWidgetFactory -------------------------------------- 225 226 GuiWidget *makeItemWidget(ui::Item const &, GuiWidget const *) override 227 { 228 auto *b = new MultiplayerPanelButtonWidget; 229 b->audienceForAboutToJoin() += this; 230 return b; 231 } 232 233 void updateItemWidget(GuiWidget &widget, ui::Item const &item) override 234 { 235 auto const &serverItem = item.as<ServerListItem>(); 236 237 widget.as<MultiplayerPanelButtonWidget>() 238 .updateContent(serverItem.info()); 239 240 // Is it playable? 241 updateAvailability(widget); 242 } 243 244 DENG2_PIMPL_AUDIENCE(AboutToJoin) 245 }; 246 247 DENG2_AUDIENCE_METHOD(MultiplayerServerMenuWidget, AboutToJoin); 248 249 MultiplayerServerMenuWidget::MultiplayerServerMenuWidget(DiscoveryMode discovery, 250 String const &name) 251 : HomeMenuWidget(name) 252 , d(new Impl(this)) 253 { 254 d->mode = discovery; 255 256 switch (d->mode) 257 { 258 case DiscoverUsingMaster: 259 d->mask = ServerLink::LocalNetwork | ServerLink::MasterServer; 260 d->link().discoverUsingMaster(); 261 break; 262 263 case DirectDiscoveryOnly: 264 d->mask = ServerLink::Direct; 265 break; 266 267 case NoDiscovery: 268 break; 269 } 270 } 271