Add handling of some messages, reuse models from the old frontend
authorunc0rr
Fri, 24 May 2019 23:39:51 +0200
changeset 15047 773beead236f
parent 15046 0730c68fdf97
child 15048 d131ec7be850
Add handling of some messages, reuse models from the old frontend
qmlfrontend/CMakeLists.txt
qmlfrontend/net_session.cpp
qmlfrontend/net_session.h
qmlfrontend/players_model.cpp
qmlfrontend/players_model.h
qmlfrontend/rooms_model.cpp
qmlfrontend/rooms_model.h
--- a/qmlfrontend/CMakeLists.txt	Fri May 24 22:27:19 2019 +0200
+++ b/qmlfrontend/CMakeLists.txt	Fri May 24 23:39:51 2019 +0200
@@ -18,6 +18,8 @@
     "engine_interface.h"
     "preview_acceptor.cpp" "preview_acceptor.h"
     "net_session.cpp" "net_session.h"
+    "players_model.cpp" "players_model.h"
+    "rooms_model.cpp" "rooms_model.h"
     )
 
 target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Quick)
--- a/qmlfrontend/net_session.cpp	Fri May 24 22:27:19 2019 +0200
+++ b/qmlfrontend/net_session.cpp	Fri May 24 23:39:51 2019 +0200
@@ -1,6 +1,14 @@
 #include "net_session.h"
 
-NetSession::NetSession(QObject *parent) : QObject(parent) {}
+#include "players_model.h"
+#include "rooms_model.h"
+
+NetSession::NetSession(QObject *parent)
+    : QObject(parent),
+      m_playersModel(new PlayersListModel()),
+      m_roomsModel(new RoomsListModel()) {}
+
+NetSession::~NetSession() { close(); }
 
 QUrl NetSession::url() const { return m_url; }
 
@@ -31,6 +39,8 @@
   return m_sessionState;
 }
 
+QString NetSession::room() const { return m_room; }
+
 void NetSession::setUrl(const QUrl &url) {
   if (m_url == url) return;
 
@@ -52,12 +62,20 @@
   emit passwordChanged(m_password);
 }
 
+void NetSession::setRoom(const QString &room) {
+  if (m_room == room) return;
+
+  m_room = room;
+  emit roomChanged(m_room);
+}
+
 void NetSession::close() {
   if (!m_socket.isNull()) {
     m_socket->disconnectFromHost();
     m_socket.clear();
 
     setSessionState(NotConnected);
+    setRoom({});
   }
 }
 
@@ -182,9 +200,33 @@
 
 void NetSession::handleLeft(const QStringList &parameters) {}
 
-void NetSession::handleLobbyJoined(const QStringList &parameters) {}
+void NetSession::handleLobbyJoined(const QStringList &parameters) {
+  for (auto player : parameters) {
+    if (player == m_nickname) {
+      // check if server is authenticated or no authentication was performed at
+      // all
+      if (!m_serverAuthHash.isEmpty()) {
+        emit error(tr("Server authentication error"));
+
+        close();
+      }
+
+      setSessionState(Lobby);
+    }
 
-void NetSession::handleLobbyLeft(const QStringList &parameters) {}
+    m_playersModel->addPlayer(player, false);
+  }
+}
+
+void NetSession::handleLobbyLeft(const QStringList &parameters) {
+  if (parameters.length() == 1) {
+    m_playersModel->removePlayer(parameters[0]);
+  } else if (parameters.length() == 2) {
+    m_playersModel->removePlayer(parameters[0], parameters[1]);
+  } else {
+    qWarning("Malformed LOBBY:LEFT message");
+  }
+}
 
 void NetSession::handleNick(const QStringList &parameters) {
   if (parameters.length()) setNickname(parameters[0]);
@@ -206,9 +248,29 @@
 
 void NetSession::handleRoomAbandoned(const QStringList &parameters) {}
 
-void NetSession::handleRoom(const QStringList &parameters) {}
+void NetSession::handleRoom(const QStringList &parameters) {
+  if (parameters.size() == 10 && parameters[0] == "ADD") {
+    m_roomsModel->addRoom(parameters.mid(1));
+  } else if (parameters.length() == 11 && parameters[0] == "UPD") {
+    m_roomsModel->updateRoom(parameters[1], parameters.mid(2));
 
-void NetSession::handleRooms(const QStringList &parameters) {}
+    // keep track of room name so correct name is displayed
+    if (m_room == parameters[1]) {
+      setRoom(parameters[2]);
+    }
+  } else if (parameters.size() == 2 && parameters[0] == "DEL") {
+    m_roomsModel->removeRoom(parameters[1]);
+  }
+}
+
+void NetSession::handleRooms(const QStringList &parameters) {
+  if (parameters.size() % 9) {
+    qWarning("Net: Malformed ROOMS message");
+    return;
+  }
+
+  m_roomsModel->setRoomsList(parameters);
+}
 
 void NetSession::handleRoundFinished(const QStringList &parameters) {}
 
--- a/qmlfrontend/net_session.h	Fri May 24 22:27:19 2019 +0200
+++ b/qmlfrontend/net_session.h	Fri May 24 23:39:51 2019 +0200
@@ -6,6 +6,8 @@
 #include <QStringList>
 #include <QUrl>
 
+class PlayersListModel;
+class RoomsListModel;
 class NetSession : public QObject {
   Q_OBJECT
 
@@ -18,6 +20,7 @@
   Q_PROPERTY(QString nickname READ nickname WRITE setNickname NOTIFY nicknameChanged)
   Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
   Q_PROPERTY(SessionState sessionState READ sessionState NOTIFY sessionStateChanged)
+  Q_PROPERTY(QString room READ room NOTIFY roomChanged)
   // clang-format on
 
  public:
@@ -25,21 +28,23 @@
   Q_ENUMS(SessionState)
 
   explicit NetSession(QObject *parent = nullptr);
+  ~NetSession() override;
 
   QUrl url() const;
   QAbstractSocket::SocketState state() const;
 
-  Q_INVOKABLE void open();
-
   QString nickname() const;
   QString password() const;
   SessionState sessionState() const;
+  QString room() const;
 
  public slots:
+  void open();
+  void close();
+
   void setUrl(const QUrl &url);
   void setNickname(const QString &nickname);
   void setPassword(const QString &password);
-  void close();
 
  signals:
   void urlChanged(const QUrl url);
@@ -49,6 +54,7 @@
   void sessionStateChanged(SessionState sessionState);
   void warning(const QString &message);
   void error(const QString &message);
+  void roomChanged(const QString &room);
 
  private slots:
   void onReadyRead();
@@ -97,14 +103,21 @@
   void send(const QString &message, const QStringList &parameters);
   void send(const QStringList &message);
   void setSessionState(SessionState sessionState);
+  void setRoom(const QString &room);
 
  private:
   QUrl m_url;
   QSharedPointer<QTcpSocket> m_socket;
+  QSharedPointer<PlayersListModel> m_playersModel;
+  QSharedPointer<RoomsListModel> m_roomsModel;
   QString m_nickname;
   QString m_password;
   QStringList m_buffer;
   SessionState m_sessionState;
+  QString m_serverAuthHash;
+  QString m_room;
+
+  Q_DISABLE_COPY(NetSession)
 };
 
 #endif  // NET_SESSION_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmlfrontend/players_model.cpp	Fri May 24 23:39:51 2019 +0200
@@ -0,0 +1,364 @@
+#include <QDebug>
+#include <QFile>
+#include <QModelIndex>
+#include <QModelIndexList>
+#include <QPainter>
+#include <QTextStream>
+
+#include "players_model.h"
+
+PlayersListModel::PlayersListModel(QObject *parent)
+    : QAbstractListModel(parent) {
+  m_fontInRoom = QFont();
+  m_fontInRoom.setItalic(true);
+}
+
+int PlayersListModel::rowCount(const QModelIndex &parent) const {
+  if (parent.isValid())
+    return 0;
+  else
+    return m_data.size();
+}
+
+QVariant PlayersListModel::data(const QModelIndex &index, int role) const {
+  if (!index.isValid() || index.row() < 0 || index.row() >= rowCount() ||
+      index.column() != 0)
+    return QVariant(QVariant::Invalid);
+
+  return m_data.at(index.row()).value(role);
+}
+
+bool PlayersListModel::setData(const QModelIndex &index, const QVariant &value,
+                               int role) {
+  if (!index.isValid() || index.row() < 0 || index.row() >= rowCount() ||
+      index.column() != 0)
+    return false;
+
+  m_data[index.row()].insert(role, value);
+
+  emit dataChanged(index, index);
+
+  return true;
+}
+
+bool PlayersListModel::insertRows(int row, int count,
+                                  const QModelIndex &parent) {
+  if (parent.isValid() || row > rowCount() || row < 0 || count < 1)
+    return false;
+
+  beginInsertRows(parent, row, row + count - 1);
+
+  for (int i = 0; i < count; ++i) m_data.insert(row, DataEntry());
+
+  endInsertRows();
+
+  return true;
+}
+
+bool PlayersListModel::removeRows(int row, int count,
+                                  const QModelIndex &parent) {
+  if (parent.isValid() || row + count > rowCount() || row < 0 || count < 1)
+    return false;
+
+  beginRemoveRows(parent, row, row + count - 1);
+
+  for (int i = 0; i < count; ++i) m_data.removeAt(row);
+
+  endRemoveRows();
+
+  return true;
+}
+
+QModelIndex PlayersListModel::nicknameIndex(const QString &nickname) {
+  QModelIndexList mil =
+      match(index(0), Qt::DisplayRole, nickname, 1, Qt::MatchExactly);
+
+  if (mil.size() > 0)
+    return mil[0];
+  else
+    return QModelIndex();
+}
+
+void PlayersListModel::addPlayer(const QString &nickname, bool notify) {
+  insertRow(rowCount());
+
+  QModelIndex mi = index(rowCount() - 1);
+  setData(mi, nickname);
+
+  checkFriendIgnore(mi);
+
+  emit nickAddedLobby(nickname, notify);
+}
+
+void PlayersListModel::removePlayer(const QString &nickname,
+                                    const QString &msg) {
+  if (msg.isEmpty())
+    emit nickRemovedLobby(nickname, QString());
+  else
+    emit nickRemovedLobby(nickname, msg);
+
+  QModelIndex mi = nicknameIndex(nickname);
+
+  if (mi.isValid()) removeRow(mi.row());
+}
+
+void PlayersListModel::playerJoinedRoom(const QString &nickname, bool notify) {
+  QModelIndex mi = nicknameIndex(nickname);
+
+  if (mi.isValid()) {
+    setData(mi, true, RoomFilterRole);
+    updateIcon(mi);
+    updateSortData(mi);
+  }
+
+  emit nickAdded(nickname, notify);
+}
+
+void PlayersListModel::playerLeftRoom(const QString &nickname) {
+  emit nickRemoved(nickname);
+
+  QModelIndex mi = nicknameIndex(nickname);
+
+  if (mi.isValid()) {
+    setData(mi, false, RoomFilterRole);
+    setData(mi, false, RoomAdmin);
+    setData(mi, false, Ready);
+    setData(mi, false, InGame);
+    updateIcon(mi);
+  }
+}
+
+void PlayersListModel::setFlag(const QString &nickname, StateFlag flagType,
+                               bool isSet) {
+  if (flagType == Friend) {
+    if (isSet)
+      m_friendsSet.insert(nickname.toLower());
+    else
+      m_friendsSet.remove(nickname.toLower());
+
+    // FIXME: set proper file name
+    // saveSet(m_friendsSet, "friends");
+  } else if (flagType == Ignore) {
+    if (isSet)
+      m_ignoredSet.insert(nickname.toLower());
+    else
+      m_ignoredSet.remove(nickname.toLower());
+
+    // FIXME: set proper file name
+    // saveSet(m_ignoredSet, "ignore");
+  }
+
+  QModelIndex mi = nicknameIndex(nickname);
+
+  if (mi.isValid()) {
+    setData(mi, isSet, flagType);
+
+    if (flagType == Friend || flagType == ServerAdmin || flagType == Ignore ||
+        flagType == RoomAdmin)
+      updateSortData(mi);
+
+    updateIcon(mi);
+  }
+}
+
+bool PlayersListModel::isFlagSet(const QString &nickname, StateFlag flagType) {
+  QModelIndex mi = nicknameIndex(nickname);
+
+  if (mi.isValid())
+    return mi.data(flagType).toBool();
+  else if (flagType == Friend)
+    return isFriend(nickname);
+  else if (flagType == Ignore)
+    return isIgnored(nickname);
+  else
+    return false;
+}
+
+void PlayersListModel::resetRoomFlags() {
+  for (int i = rowCount() - 1; i >= 0; --i) {
+    QModelIndex mi = index(i);
+
+    if (mi.data(RoomFilterRole).toBool()) {
+      setData(mi, false, RoomFilterRole);
+      setData(mi, false, RoomAdmin);
+      setData(mi, false, Ready);
+      setData(mi, false, InGame);
+
+      updateSortData(mi);
+      updateIcon(mi);
+    }
+  }
+}
+
+void PlayersListModel::updateIcon(const QModelIndex &index) {
+  quint32 iconNum = 0;
+
+  QList<bool> flags;
+  flags << index.data(Ready).toBool() << index.data(ServerAdmin).toBool()
+        << index.data(RoomAdmin).toBool() << index.data(Registered).toBool()
+        << index.data(Friend).toBool() << index.data(Ignore).toBool()
+        << index.data(InGame).toBool() << index.data(RoomFilterRole).toBool()
+        << index.data(InRoom).toBool() << index.data(Contributor).toBool();
+
+  for (int i = flags.size() - 1; i >= 0; --i)
+    if (flags[i]) iconNum |= 1 << i;
+
+  if (m_icons().contains(iconNum)) {
+    setData(index, m_icons().value(iconNum), Qt::DecorationRole);
+  } else {
+    QPixmap result(24, 16);
+    result.fill(Qt::transparent);
+
+    QPainter painter(&result);
+
+    if (index.data(RoomFilterRole).toBool()) {
+      if (index.data(InGame).toBool()) {
+        painter.drawPixmap(0, 0, 16, 16, QPixmap(":/res/chat/ingame.png"));
+      } else {
+        if (index.data(Ready).toBool())
+          painter.drawPixmap(0, 0, 16, 16, QPixmap(":/res/chat/lamp.png"));
+        else
+          painter.drawPixmap(0, 0, 16, 16, QPixmap(":/res/chat/lamp_off.png"));
+      }
+    } else {  // we're in lobby
+      if (!index.data(InRoom).toBool())
+        painter.drawPixmap(0, 0, 16, 16, QPixmap(":/res/Flake.png"));
+    }
+
+    QString mainIconName(":/res/chat/");
+
+    if (index.data(ServerAdmin).toBool())
+      mainIconName += "serveradmin";
+    else {
+      if (index.data(RoomAdmin).toBool())
+        mainIconName += "roomadmin";
+      else
+        mainIconName += "hedgehog";
+
+      if (index.data(Contributor).toBool()) mainIconName += "contributor";
+    }
+
+    if (!index.data(Registered).toBool()) mainIconName += "_gray";
+
+    painter.drawPixmap(8, 0, 16, 16, QPixmap(mainIconName + ".png"));
+
+    if (index.data(Ignore).toBool())
+      painter.drawPixmap(8, 0, 16, 16, QPixmap(":/res/chat/ignore.png"));
+    else if (index.data(Friend).toBool())
+      painter.drawPixmap(8, 0, 16, 16, QPixmap(":/res/chat/friend.png"));
+
+    painter.end();
+
+    QIcon icon(result);
+
+    setData(index, icon, Qt::DecorationRole);
+    m_icons().insert(iconNum, icon);
+  }
+
+  if (index.data(Ignore).toBool())
+    setData(index, QColor(Qt::gray), Qt::ForegroundRole);
+  else if (index.data(Friend).toBool())
+    setData(index, QColor(Qt::green), Qt::ForegroundRole);
+  else
+    setData(index, QBrush(QColor(0xff, 0xcc, 0x00)), Qt::ForegroundRole);
+}
+
+QHash<quint32, QIcon> &PlayersListModel::m_icons() {
+  static QHash<quint32, QIcon> iconsCache;
+
+  return iconsCache;
+}
+
+void PlayersListModel::updateSortData(const QModelIndex &index) {
+  QString result =
+      QString("%1%2%3%4%5%6")
+          // room admins go first, then server admins, then friends
+          .arg(1 - index.data(RoomAdmin).toInt())
+          .arg(1 - index.data(ServerAdmin).toInt())
+          .arg(1 - index.data(Friend).toInt())
+          // ignored at bottom
+          .arg(index.data(Ignore).toInt())
+          // keep nicknames starting from non-letter character at bottom within
+          // group assume there are no empty nicks in list
+          .arg(index.data(Qt::DisplayRole).toString().at(0).isLetter() ? 0 : 1)
+          // sort ignoring case
+          .arg(index.data(Qt::DisplayRole).toString().toLower());
+
+  setData(index, result, SortRole);
+}
+
+void PlayersListModel::setNickname(const QString &nickname) {
+  m_nickname = nickname;
+
+  // FIXME: set proper file names
+  // loadSet(m_friendsSet, "friends");
+  // loadSet(m_ignoredSet, "ignore");
+
+  for (int i = rowCount() - 1; i >= 0; --i) checkFriendIgnore(index(i));
+}
+
+bool PlayersListModel::isFriend(const QString &nickname) {
+  return m_friendsSet.contains(nickname.toLower());
+}
+
+bool PlayersListModel::isIgnored(const QString &nickname) {
+  return m_ignoredSet.contains(nickname.toLower());
+}
+
+void PlayersListModel::checkFriendIgnore(const QModelIndex &mi) {
+  setData(mi, isFriend(mi.data().toString()), Friend);
+  setData(mi, isIgnored(mi.data().toString()), Ignore);
+
+  updateIcon(mi);
+  updateSortData(mi);
+}
+
+void PlayersListModel::loadSet(QSet<QString> &set, const QString &fileName) {
+  set.clear();
+
+  QFile txt(fileName);
+  if (!txt.open(QIODevice::ReadOnly)) return;
+
+  QTextStream stream(&txt);
+  stream.setCodec("UTF-8");
+
+  while (!stream.atEnd()) {
+    QString str = stream.readLine();
+    if (str.startsWith(";") || str.isEmpty()) continue;
+
+    set.insert(str.trimmed());
+  }
+
+  txt.close();
+}
+
+void PlayersListModel::saveSet(const QSet<QString> &set,
+                               const QString &fileName) {
+  qDebug("saving set");
+
+  QFile txt(fileName);
+
+  // list empty? => rather have no file for the list than an empty one
+  if (set.isEmpty()) {
+    if (txt.exists()) {
+      // try to remove file, if successful we're done here.
+      if (txt.remove()) return;
+    } else
+      // there is no file
+      return;
+  }
+
+  if (!txt.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
+
+  QTextStream stream(&txt);
+  stream.setCodec("UTF-8");
+
+  stream << "; this list is used by Hedgewars - do not edit it unless you know "
+            "what you're doing!"
+         << endl;
+
+  foreach (const QString &nick, set.values())
+    stream << nick << endl;
+
+  txt.close();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmlfrontend/players_model.h	Fri May 24 23:39:51 2019 +0200
@@ -0,0 +1,83 @@
+#ifndef PLAYERSLISTMODEL_H
+#define PLAYERSLISTMODEL_H
+
+#include <QAbstractListModel>
+#include <QFont>
+#include <QHash>
+#include <QIcon>
+#include <QModelIndex>
+#include <QSet>
+
+class PlayersListModel : public QAbstractListModel {
+  Q_OBJECT
+
+ public:
+  enum StateFlag {
+    Ready = Qt::UserRole,
+    ServerAdmin = Qt::UserRole + 1,
+    RoomAdmin = Qt::UserRole + 2,
+    Registered = Qt::UserRole + 3,
+    Friend = Qt::UserRole + 4,
+    Ignore = Qt::UserRole + 5,
+    InGame = Qt::UserRole + 6,
+    InRoom = Qt::UserRole + 7,
+    Contributor = Qt::UserRole + 8
+    // if you add a role that will affect the player icon,
+    // then also add it to the flags Qlist in updateIcon()!
+  };
+
+  enum SpecialRoles {
+    SortRole = Qt::UserRole + 100,
+    RoomFilterRole = Qt::UserRole + 101
+  };
+
+  explicit PlayersListModel(QObject *parent = 0);
+
+  int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+  QVariant data(const QModelIndex &index, int role) const override;
+  bool setData(const QModelIndex &index, const QVariant &value,
+               int role = Qt::DisplayRole) override;
+
+  void setFlag(const QString &nickname, StateFlag flagType, bool isSet);
+  bool isFlagSet(const QString &nickname, StateFlag flagType);
+
+  bool insertRows(int row, int count,
+                  const QModelIndex &parent = QModelIndex()) override;
+  bool removeRows(int row, int count,
+                  const QModelIndex &parent = QModelIndex()) override;
+
+  QModelIndex nicknameIndex(const QString &nickname);
+
+ public slots:
+  void addPlayer(const QString &nickname, bool notify);
+  void removePlayer(const QString &nickname, const QString &msg = QString());
+  void playerJoinedRoom(const QString &nickname, bool notify);
+  void playerLeftRoom(const QString &nickname);
+  void resetRoomFlags();
+  void setNickname(const QString &nickname);
+
+ signals:
+  void nickAdded(const QString &nick, bool notifyNick);
+  void nickRemoved(const QString &nick);
+  void nickAddedLobby(const QString &nick, bool notifyNick);
+  void nickRemovedLobby(const QString &nick, const QString &message);
+
+ private:
+  QHash<quint32, QIcon> &m_icons();
+  using DataEntry = QHash<int, QVariant>;
+  QList<DataEntry> m_data;
+  QSet<QString> m_friendsSet, m_ignoredSet;
+  QString m_nickname;
+  QFont m_fontInRoom;
+
+  void updateIcon(const QModelIndex &index);
+  void updateSortData(const QModelIndex &index);
+  void loadSet(QSet<QString> &set, const QString &fileName);
+  void saveSet(const QSet<QString> &set, const QString &fileName);
+  void checkFriendIgnore(const QModelIndex &mi);
+  bool isFriend(const QString &nickname);
+  bool isIgnored(const QString &nickname);
+};
+
+#endif  // PLAYERSLISTMODEL_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmlfrontend/rooms_model.cpp	Fri May 24 23:39:51 2019 +0200
@@ -0,0 +1,231 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * This program 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; version 2 of the License
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+/**
+ * @file
+ * @brief RoomsListModel class implementation
+ */
+
+#include <QBrush>
+#include <QColor>
+#include <QIcon>
+
+#include "rooms_model.h"
+
+RoomsListModel::RoomsListModel(QObject *parent)
+    : QAbstractTableModel(parent), c_nColumns(9) {
+  m_headerData = QStringList();
+  m_headerData << tr("In progress");
+  m_headerData << tr("Room Name");
+  //: Caption of the column for the number of connected clients in the list of
+  // rooms
+  m_headerData << tr("C");
+  //: Caption of the column for the number of teams in the list of rooms
+  m_headerData << tr("T");
+  m_headerData << tr("Owner");
+  m_headerData << tr("Map");
+  m_headerData << tr("Script");
+  m_headerData << tr("Rules");
+  m_headerData << tr("Weapons");
+
+  // m_staticMapModel = DataManager::instance().staticMapModel();
+  // m_missionMapModel = DataManager::instance().missionMapModel();
+}
+
+QVariant RoomsListModel::headerData(int section, Qt::Orientation orientation,
+                                    int role) const {
+  if (orientation == Qt::Vertical || role != Qt::DisplayRole)
+    return QVariant();
+  else
+    return QVariant(m_headerData.at(section));
+}
+
+int RoomsListModel::rowCount(const QModelIndex &parent) const {
+  if (parent.isValid())
+    return 0;
+  else
+    return m_data.size();
+}
+
+int RoomsListModel::columnCount(const QModelIndex &parent) const {
+  if (parent.isValid())
+    return 0;
+  else
+    return c_nColumns;
+}
+
+QVariant RoomsListModel::data(const QModelIndex &index, int role) const {
+  int column = index.column();
+  int row = index.row();
+
+  // invalid index
+  if (!index.isValid()) return QVariant();
+
+  // invalid row
+  if ((row < 0) || (row >= m_data.size())) return QVariant();
+
+  // invalid column
+  if ((column < 0) || (column >= c_nColumns)) return QVariant();
+
+  // not a role we have data for
+  if (role != Qt::DisplayRole)
+    // only custom-align counters
+    if ((role != Qt::TextAlignmentRole) ||
+        ((column != PlayerCountColumn) && (column != TeamCountColumn)))
+      // only decorate name column
+      if ((role != Qt::DecorationRole) || (column != NameColumn))
+        // only dye map column
+        if ((role != Qt::ForegroundRole) || (column != MapColumn))
+          return QVariant();
+
+  // decorate room name based on room state
+  if (role == Qt::DecorationRole) {
+    const QIcon roomBusyIcon(":/res/iconDamage.png");
+    const QIcon roomBusyIconGreen(":/res/iconDamageLockG.png");
+    const QIcon roomBusyIconRed(":/res/iconDamageLockR.png");
+    const QIcon roomWaitingIcon(":/res/iconTime.png");
+    const QIcon roomWaitingIconGreen(":/res/iconTimeLockG.png");
+    const QIcon roomWaitingIconRed(":/res/iconTimeLockR.png");
+
+    QString flags = m_data.at(row).at(StateColumn);
+
+    if (flags.contains("g")) {
+      if (flags.contains("j"))
+        return QVariant(roomBusyIconRed);
+      else if (flags.contains("p"))
+        return QVariant(roomBusyIconGreen);
+      else
+        return QVariant(roomBusyIcon);
+    } else {
+      if (flags.contains("j"))
+        return QVariant(roomWaitingIconRed);
+      else if (flags.contains("p"))
+        return QVariant(roomWaitingIconGreen);
+      else
+        return QVariant(roomWaitingIcon);
+    }
+  }
+
+  QString content = m_data.at(row).at(column);
+
+  if (role == Qt::DisplayRole) {
+    // display room names
+    if (column == 5) {
+      // special names
+      if (content[0] == '+') {
+        if (content == "+rnd+") return tr("Random Map");
+        if (content == "+maze+") return tr("Random Maze");
+        if (content == "+perlin+") return tr("Random Perlin");
+        if (content == "+drawn+") return tr("Hand-drawn");
+        if (content == "+forts+") return tr("Forts");
+      }
+
+      // prefix ? if map not available
+      /*if (!m_staticMapModel->mapExists(content) &&
+          !m_missionMapModel->mapExists(content))
+        return QString("? %1").arg(content);*/
+    }
+
+    return content;
+  }
+
+  // dye map names red if map not available
+  /*if (role == Qt::ForegroundRole) {
+    if (content == "+rnd+" || content == "+maze+" || content == "+perlin+" ||
+        content == "+drawn+" || content == "+forts+" ||
+        m_staticMapModel->mapExists(content) ||
+        m_missionMapModel->mapExists(content))
+      return QVariant();
+    else
+      return QBrush(QColor("darkred"));
+  }*/
+
+  if (role == Qt::TextAlignmentRole) {
+    return (int)(Qt::AlignHCenter | Qt::AlignVCenter);
+  }
+
+  Q_ASSERT(false);
+  return QVariant();
+}
+
+void RoomsListModel::setRoomsList(const QStringList &rooms) {
+  beginResetModel();
+
+  m_data.clear();
+
+  int nRooms = rooms.size();
+
+  for (int i = 0; i < nRooms; i += c_nColumns) {
+    QStringList l;
+    l.reserve(c_nColumns);
+
+    for (int t = 0; t < c_nColumns; t++) {
+      l.append(rooms[i + t]);
+    }
+
+    m_data.append(l);
+  }
+
+  endResetModel();
+}
+
+void RoomsListModel::addRoom(const QStringList &info) {
+  beginInsertRows(QModelIndex(), 0, 0);
+
+  m_data.prepend(info);
+
+  endInsertRows();
+}
+
+int RoomsListModel::rowOfRoom(const QString &name) {
+  int size = m_data.size();
+
+  if (size < 1) return -1;
+
+  int i = 0;
+
+  // search for record with matching room name
+  while (m_data[i].at(NameColumn) != name) {
+    i++;
+    if (i >= size) return -1;
+  }
+
+  return i;
+}
+
+void RoomsListModel::removeRoom(const QString &name) {
+  int i = rowOfRoom(name);
+
+  if (i < 0) return;
+
+  beginRemoveRows(QModelIndex(), i, i);
+
+  m_data.removeAt(i);
+
+  endRemoveRows();
+}
+
+void RoomsListModel::updateRoom(const QString &name, const QStringList &info) {
+  int i = rowOfRoom(name);
+
+  if (i < 0) return;
+
+  m_data[i] = info;
+
+  emit dataChanged(index(i, 0), index(i, columnCount(QModelIndex()) - 1));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmlfrontend/rooms_model.h	Fri May 24 23:39:51 2019 +0200
@@ -0,0 +1,69 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * This program 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; version 2 of the License
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+/**
+ * @file
+ * @brief RoomsListModel class definition
+ */
+
+#ifndef HEDGEWARS_ROOMSLISTMODEL_H
+#define HEDGEWARS_ROOMSLISTMODEL_H
+
+#include <QAbstractTableModel>
+#include <QStringList>
+
+class RoomsListModel : public QAbstractTableModel {
+  Q_OBJECT
+ public:
+  // if you add a column here, also incr. c_nColumns in constructor
+  // also adjust header in constructor to changes
+  enum Column {
+    StateColumn,
+    NameColumn,
+    PlayerCountColumn,
+    TeamCountColumn,
+    OwnerColumn,
+    MapColumn,
+    SchemeColumn,
+    WeaponsColumn
+  };
+
+  explicit RoomsListModel(QObject *parent = 0);
+
+  QVariant headerData(int section, Qt::Orientation orientation,
+                      int role) const override;
+  int rowCount(const QModelIndex &parent) const override;
+  int columnCount(const QModelIndex &parent) const override;
+  QVariant data(const QModelIndex &index, int role) const override;
+
+ public slots:
+  void setRoomsList(const QStringList &rooms);
+  void addRoom(const QStringList &info);
+  void removeRoom(const QString &name);
+  void updateRoom(const QString &name, const QStringList &info);
+  int rowOfRoom(const QString &name);
+
+ private:
+  const int c_nColumns;
+  QList<QStringList> m_data;
+  QStringList m_headerData;
+  // MapModel * m_staticMapModel;
+  // MapModel * m_missionMapModel;
+};
+
+#endif  // HEDGEWARS_ROOMSLISTMODEL_H