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