Allow to see rooms of incompatible versions in the lobby
authorS.D.
Tue, 27 Sep 2022 14:59:03 +0300
changeset 15878 fc3cb23fd26f
parent 15877 6cb7330113d8
child 15879 4c58b320056c
Allow to see rooms of incompatible versions in the lobby For the new clients the room version is shown in a separate column. There is also a hack for previous versions clients: the room vesion specifier is prepended to the room names for rooms of incompatible versions, and the server shows 'incompatible version' error if the client tries to join them.
QTfrontend/model/roomslistmodel.cpp
QTfrontend/model/roomslistmodel.h
QTfrontend/net/newnetclient.cpp
QTfrontend/ui/page/pageroomslist.cpp
QTfrontend/ui/page/pageroomslist.h
gameServer/Actions.hs
gameServer/CoreTypes.hs
gameServer/HWProtoInRoomState.hs
gameServer/HWProtoLobbyState.hs
gameServer/HandlerUtils.hs
gameServer/Utils.hs
--- a/QTfrontend/model/roomslistmodel.cpp	Fri Sep 23 12:47:47 2022 -0400
+++ b/QTfrontend/model/roomslistmodel.cpp	Tue Sep 27 14:59:03 2022 +0300
@@ -27,10 +27,11 @@
 
 #include "roomslistmodel.h"
 #include "MapModel.h"
+#include "hwconsts.h"
 
 RoomsListModel::RoomsListModel(QObject *parent) :
     QAbstractTableModel(parent),
-    c_nColumns(9)
+    c_nColumns(10)
 {
     m_headerData = QStringList();
     m_headerData << tr("In progress");
@@ -44,6 +45,7 @@
     m_headerData << tr("Script");
     m_headerData << tr("Rules");
     m_headerData << tr("Weapons");
+    m_headerData << tr("Version");
 
     m_staticMapModel = DataManager::instance().staticMapModel();
     m_missionMapModel = DataManager::instance().missionMapModel();
@@ -77,6 +79,59 @@
 }
 
 
+QString RoomsListModel::protoToVersion(const QString & proto)
+{
+    bool ok;
+    uint protoNum = proto.toUInt(&ok);
+    if (!ok)
+        return "Unknown";
+    switch (protoNum) {
+    case 17: return "0.9.7-dev";
+    case 19: return "0.9.7";
+    case 20: return "0.9.8-dev";
+    case 21: return "0.9.8";
+    case 22: return "0.9.9-dev";
+    case 23: return "0.9.9";
+    case 24: return "0.9.10-dev";
+    case 25: return "0.9.10";
+    case 26: return "0.9.11-dev";
+    case 27: return "0.9.11";
+    case 28: return "0.9.12-dev";
+    case 29: return "0.9.12";
+    case 30: return "0.9.13-dev";
+    case 31: return "0.9.13";
+    case 32: return "0.9.14-dev";
+    case 33: return "0.9.14";
+    case 34: return "0.9.15-dev";
+    case 35: return "0.9.14.1";
+    case 37: return "0.9.15";
+    case 38: return "0.9.16-dev";
+    case 39: return "0.9.16";
+    case 40: return "0.9.17-dev";
+    case 41: return "0.9.17";
+    case 42: return "0.9.18-dev";
+    case 43: return "0.9.18";
+    case 44: return "0.9.19-dev";
+    case 45: return "0.9.19";
+    case 46: return "0.9.20-dev";
+    case 47: return "0.9.20";
+    case 48: return "0.9.21-dev";
+    case 49: return "0.9.21";
+    case 50: return "0.9.22-dev";
+    case 51: return "0.9.22";
+    case 52: return "0.9.23-dev";
+    case 53: return "0.9.23";
+    case 54: return "0.9.24-dev";
+    case 55: return "0.9.24";
+    case 56: return "0.9.25-dev";
+    case 57: return "0.9.25";
+    case 58: return "1.0.0-dev";
+    case 59: return "1.0.0";
+    case 60: return "1.0.1-dev";
+    default: return "Unknown";
+    }
+}
+
 QVariant RoomsListModel::data(const QModelIndex &index, int role) const
 {
     int column = index.column();
@@ -101,9 +156,10 @@
             || ((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();
+                    if ((role != Qt::ForegroundRole))
+                        // UserRole is used for version column filtering
+                        if ((role != Qt::UserRole))
+                            return QVariant();
 
     // decorate room name based on room state
     if (role == Qt::DecorationRole)
@@ -159,6 +215,10 @@
                 !m_missionMapModel->mapExists(content))
                 return QString ("? %1").arg(content);
         }
+        else if (column == VersionColumn)
+        {
+            return protoToVersion(content);
+        }
 
         return content;
     }
@@ -166,16 +226,23 @@
     // 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 (m_data[row][VersionColumn] != *cProtoVer)
+            return QBrush(QColor("darkgrey"));
+
+        if (column == MapColumn)
+        {
+            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"));
+        }
+        return QVariant();
     }
 
     if (role == Qt::TextAlignmentRole)
@@ -183,6 +250,9 @@
         return (int)(Qt::AlignHCenter | Qt::AlignVCenter);
     }
 
+    if (role == Qt::UserRole && column == VersionColumn)
+        return content;
+
     Q_ASSERT(false);
     return QVariant();
 }
--- a/QTfrontend/model/roomslistmodel.h	Fri Sep 23 12:47:47 2022 -0400
+++ b/QTfrontend/model/roomslistmodel.h	Tue Sep 27 14:59:03 2022 +0300
@@ -42,8 +42,10 @@
         TeamCountColumn,
         OwnerColumn,
         MapColumn,
+        ScriptColumn,
         SchemeColumn,
-        WeaponsColumn
+        WeaponsColumn,
+        VersionColumn,
     };
 
     explicit RoomsListModel(QObject *parent = 0);
@@ -51,6 +53,7 @@
     QVariant headerData(int section, Qt::Orientation orientation, int role) const;
     int rowCount(const QModelIndex & parent) const;
     int columnCount(const QModelIndex & parent) const;
+    int columnCountSupported() const { return c_nColumns; };
     QVariant data(const QModelIndex &index, int role) const;
 
 public slots:
@@ -66,6 +69,7 @@
     QStringList m_headerData;
     MapModel * m_staticMapModel;
     MapModel * m_missionMapModel;
+    static QString protoToVersion(const QString & proto);
 };
 
 #endif // HEDGEWARS_ROOMSLISTMODEL_H
--- a/QTfrontend/net/newnetclient.cpp	Fri Sep 23 12:47:47 2022 -0400
+++ b/QTfrontend/net/newnetclient.cpp	Tue Sep 27 14:59:03 2022 +0300
@@ -394,7 +394,7 @@
 
     if (lst[0] == "ROOMS")
     {
-        if(lst.size() % 9 != 1)
+        if(lst.size() % m_roomsListModel->columnCountSupported() != 1)
         {
             qWarning("Net: Malformed ROOMS message");
             return;
@@ -644,7 +644,7 @@
         return;
     }
 
-    if(lst[0] == "ROOM" && lst.size() == 11 && lst[1] == "ADD")
+    if(lst[0] == "ROOM" && lst.size() == m_roomsListModel->columnCountSupported() + 2 && lst[1] == "ADD")
     {
         QStringList tmp = lst;
         tmp.removeFirst();
@@ -654,7 +654,7 @@
         return;
     }
 
-    if(lst[0] == "ROOM" && lst.size() == 12 && lst[1] == "UPD")
+    if(lst[0] == "ROOM" && lst.size() == m_roomsListModel->columnCountSupported() + 3 && lst[1] == "UPD")
     {
         QStringList tmp = lst;
         tmp.removeFirst();
--- a/QTfrontend/ui/page/pageroomslist.cpp	Fri Sep 23 12:47:47 2022 -0400
+++ b/QTfrontend/ui/page/pageroomslist.cpp	Tue Sep 27 14:59:03 2022 +0300
@@ -84,10 +84,14 @@
     showJoinRestricted = new QAction(QAction::tr("Show join restricted"), stateMenu);
     showJoinRestricted->setCheckable(true);
     showJoinRestricted->setChecked(true);
+    showIncompatible = new QAction(QAction::tr("Show incompatible"), stateMenu);
+    showIncompatible->setCheckable(true);
+    showIncompatible->setChecked(true);
     stateMenu->addAction(showGamesInLobby);
     stateMenu->addAction(showGamesInProgress);
     stateMenu->addAction(showPassword);
     stateMenu->addAction(showJoinRestricted);
+    stateMenu->addAction(showIncompatible);
     btnState->setMenu(stateMenu);
 
     // Help/prompt message at top
@@ -199,6 +203,7 @@
     connect(showGamesInProgress, SIGNAL(triggered()), this, SLOT(onFilterChanged()));
     connect(showPassword, SIGNAL(triggered()), this, SLOT(onFilterChanged()));
     connect(showJoinRestricted, SIGNAL(triggered()), this, SLOT(onFilterChanged()));
+    connect(showIncompatible, SIGNAL(triggered()), this, SLOT(onFilterChanged()));
     connect(searchText, SIGNAL(textChanged (const QString &)), this, SLOT(onFilterChanged()));
     connect(this, SIGNAL(askJoinConfirmation (const QString &)), this, SLOT(onJoinConfirmation(const QString &)), Qt::QueuedConnection);
 
@@ -232,6 +237,7 @@
 {
     roomsModel = NULL;
     stateFilteredModel = NULL;
+    versionFilteredModel = NULL;
 
     initPage();
 }
@@ -554,7 +560,7 @@
 void PageRoomsList::setModel(RoomsListModel * model)
 {
     // filter chain:
-    // model -> stateFilteredModel -> schemeFilteredModel ->
+    // model -> versionFilteredModel -> stateFilteredModel -> schemeFilteredModel ->
     // -> weaponsFilteredModel -> roomsModel (search filter+sorting)
 
     if (roomsModel == NULL)
@@ -564,12 +570,17 @@
         roomsModel->setSortCaseSensitivity(Qt::CaseInsensitive);
         roomsModel->sort(RoomsListModel::StateColumn, Qt::AscendingOrder);
 
-        stateFilteredModel = new QSortFilterProxyModel(this);
+        versionFilteredModel = new QSortFilterProxyModel(this);
+        versionFilteredModel->setDynamicSortFilter(true);
+        versionFilteredModel->setFilterKeyColumn(RoomsListModel::VersionColumn);
+        versionFilteredModel->setFilterRole(Qt::UserRole);
 
+        stateFilteredModel = new QSortFilterProxyModel(this);
         stateFilteredModel->setDynamicSortFilter(true);
+        stateFilteredModel->setFilterKeyColumn(RoomsListModel::StateColumn);
+        stateFilteredModel->setSourceModel(versionFilteredModel);
 
         roomsModel->setFilterKeyColumn(-1); // search in all columns
-        stateFilteredModel->setFilterKeyColumn(RoomsListModel::StateColumn);
 
         roomsModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
 
@@ -585,7 +596,7 @@
         connect(roomsList->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(roomSelectionChanged(const QModelIndex &, const QModelIndex &)));
     }
 
-    stateFilteredModel->setSourceModel(model);
+    versionFilteredModel->setSourceModel(model);
 
     QHeaderView * h = roomsList->horizontalHeader();
 
@@ -638,6 +649,12 @@
     bool stateProgress = showGamesInProgress->isChecked();
     bool statePassword = showPassword->isChecked();
     bool stateJoinRestricted = showJoinRestricted->isChecked();
+    bool stateIncompatible = showIncompatible->isChecked();
+
+    if (!stateIncompatible)
+        versionFilteredModel->setFilterFixedString(*cProtoVer);
+    else
+        versionFilteredModel->setFilterFixedString("");
 
     QString filter;
     if (!stateLobby && !stateProgress)
--- a/QTfrontend/ui/page/pageroomslist.h	Fri Sep 23 12:47:47 2022 -0400
+++ b/QTfrontend/ui/page/pageroomslist.h	Tue Sep 27 14:59:03 2022 +0300
@@ -93,10 +93,12 @@
         QSettings * m_gameSettings;
         QSortFilterProxyModel * roomsModel;
         QSortFilterProxyModel * stateFilteredModel;
+        QSortFilterProxyModel * versionFilteredModel;
         QAction * showGamesInLobby;
         QAction * showGamesInProgress;
         QAction * showPassword;
         QAction * showJoinRestricted;
+        QAction * showIncompatible;
         QSplitter * m_splitter;
 
         GameSchemeModel * gameSchemeModel;
--- a/gameServer/Actions.hs	Fri Sep 23 12:47:47 2022 -0400
+++ b/gameServer/Actions.hs	Tue Sep 27 14:59:03 2022 +0300
@@ -24,6 +24,7 @@
 import qualified Data.Set as Set
 import qualified Data.Map as Map
 import qualified Data.List as L
+import Data.Word
 import qualified Control.Exception as Exception
 import System.Log.Logger
 import Control.Monad
@@ -65,6 +66,12 @@
     ri <- clientRoomA
     liftM (map sendChan . filter (/= cl)) $ roomClientsS ri
 
+othersChansProto :: StateT ServerState IO [(ClientChan, Word16)]
+othersChansProto = do
+    cl <- client's id
+    ri <- clientRoomA
+    map (\ci -> (sendChan ci, clientProto ci)) . filter (/= cl) <$> roomClientsS ri
+
 processAction :: Action -> StateT ServerState IO ()
 
 
@@ -72,6 +79,10 @@
     io $ mapM_ (`writeChan` (msg `deepseq` msg)) (chans `deepseq` chans)
 
 
+processAction (AnswerClientsByProto chansProto msgFunc) =
+    io $ mapM_ (\(chan, proto) -> writeChan chan (msgFunc proto)) chansProto
+
+
 processAction SendServerMessage = do
     chan <- client's sendChan
     protonum <- client's clientProto
@@ -279,8 +290,9 @@
                 )
 
     newRoom' <- io $ room'sM rnc id ri
-    chans <- liftM (map sendChan) $! sameProtoClientsS proto
-    processAction $ AnswerClients chans ("ROOM" : "UPD" : oldRoomName : roomInfo proto (maybeNick newMaster) newRoom')
+    chansProto <- fmap (map (\c -> (sendChan c, clientProto c))) $! allClientsS
+    let oldRoomNameByProto = roomNameByProto oldRoomName (roomProto newRoom')
+    processAction $ AnswerClientsByProto chansProto (\p -> "ROOM" : "UPD" : oldRoomNameByProto p : roomInfo p (maybeNick newMaster) newRoom')
 
 
 processAction (AddRoom roomName roomPassword) = do
@@ -300,10 +312,10 @@
 
     processAction $ MoveToRoom rId
 
-    chans <- liftM (map sendChan) $! sameProtoClientsS proto
+    chansProto <- fmap (map (\c -> (sendChan c, clientProto c))) $! allClientsS
 
     mapM_ processAction [
-      AnswerClients chans ("ROOM" : "ADD" : roomInfo proto n rm{playersIn = 1})
+      AnswerClientsByProto chansProto (\p -> "ROOM" : "ADD" : roomInfo p n rm{playersIn = 1})
         ]
 
 
@@ -312,13 +324,13 @@
     rnc <- gets roomsClients
     ri <- io $ clientRoomM rnc clId
     roomName <- io $ room'sM rnc name ri
-    others <- othersChans
-    proto <- client's clientProto
-    chans <- liftM (map sendChan) $! sameProtoClientsS proto
+    roomProto <- io $ room'sM rnc roomProto ri
+    others <- othersChansProto
+    chansProto <- fmap (map (\c -> (sendChan c, clientProto c))) $! allClientsS
 
     mapM_ processAction [
-            AnswerClients chans ["ROOM", "DEL", roomName],
-            AnswerClients others ["ROOMABANDONED", roomName]
+            AnswerClientsByProto chansProto (\p -> ["ROOM", "DEL", roomNameByProto roomName roomProto p]),
+            AnswerClientsByProto others (\p -> ["ROOMABANDONED", roomNameByProto roomName roomProto p])
         ]
 
     io $ removeRoom rnc ri
@@ -331,8 +343,9 @@
     ri <- io $ clientRoomM rnc clId
     rm <- io $ room'sM rnc id ri
     masterCl <- io $ client'sM rnc id `DT.mapM` (masterID rm)
-    chans <- liftM (map sendChan) $! sameProtoClientsS proto
-    processAction $ AnswerClients chans ("ROOM" : "UPD" : name rm : roomInfo proto (maybeNick masterCl) rm)
+    chansProto <- fmap (map (\c -> (sendChan c, clientProto c))) $! allClientsS
+    let thisRoomNameByProto = roomNameByProto (name rm) (roomProto rm)
+    processAction $ AnswerClientsByProto chansProto (\p -> "ROOM" : "UPD" : thisRoomNameByProto p : roomInfo p (maybeNick masterCl) rm)
 
 
 processAction UnreadyRoomClients = do
@@ -536,7 +549,7 @@
         rooms <- roomsM rnc
         mapM (\r -> (mapM (client'sM rnc id) $ masterID r)
             >>= \cn -> return $ roomInfo clProto (maybeNick cn) r)
-            $ filter (\r -> (roomProto r == clProto)) rooms
+            $ filter ((/=) 0 . roomProto) rooms
 
     mapM_ processAction . concat $ [
         [AnswerClients clientsChans ["LOBBY:JOINED", clientNick]]
--- a/gameServer/CoreTypes.hs	Fri Sep 23 12:47:47 2022 -0400
+++ b/gameServer/CoreTypes.hs	Tue Sep 27 14:59:03 2022 +0300
@@ -46,6 +46,7 @@
 
 data Action =
     AnswerClients ![ClientChan] ![B.ByteString]
+    | AnswerClientsByProto ![(ClientChan, Word16)] !(Word16 -> [B.ByteString])
     | SendServerMessage
     | SendServerVars
     | MoveToRoom RoomIndex
--- a/gameServer/HWProtoInRoomState.hs	Fri Sep 23 12:47:47 2022 -0400
+++ b/gameServer/HWProtoInRoomState.hs	Tue Sep 27 14:59:03 2022 +0300
@@ -313,7 +313,8 @@
     cl <- thisClient
     rs <- allRoomInfos
     rm <- thisRoom
-    chans <- sameProtoChans
+    chansProto <- allChansProto
+    let thisRoomNameByProto = roomNameByProto (name rm) (roomProto rm)
 
     return $
         if illegalName newName then
@@ -326,7 +327,7 @@
             [Warning $ loc "A room with the same name already exists."]
         else
             [ModifyRoom roomUpdate,
-            AnswerClients chans ("ROOM" : "UPD" : name rm : roomInfo (clientProto cl) (nick cl) (roomUpdate rm)),
+            AnswerClientsByProto chansProto (\p -> "ROOM" : "UPD" : thisRoomNameByProto p : roomInfo p (nick cl) (roomUpdate rm)),
             RegisterEvent RoomNameUpdate]
     where
         roomUpdate r = r{name = newName}
--- a/gameServer/HWProtoLobbyState.hs	Fri Sep 23 12:47:47 2022 -0400
+++ b/gameServer/HWProtoLobbyState.hs	Tue Sep 27 14:59:03 2022 +0300
@@ -40,7 +40,7 @@
     (ci, irnc) <- ask
     let cl = irnc `client` ci
     rooms <- allRoomInfos
-    let roomsInfoList = concatMap (\r -> roomInfo (clientProto cl) (maybeNick . liftM (client irnc) $ masterID r) r) . filter (\r -> (roomProto r == clientProto cl))
+    let roomsInfoList = concatMap (\r -> roomInfo (clientProto cl) (maybeNick . liftM (client irnc) $ masterID r) r) . filter ((/=) 0 . roomProto)
     return $ if hasAskedList cl then [] else
         [ ModifyClient (\c -> c{hasAskedList = True})
         , AnswerClients [sendChan cl] ("ROOMS" : roomsInfoList rooms)]
@@ -91,7 +91,9 @@
                 []
     let clTeamsNames = map teamname clTeams
     return $
-        if isNothing maybeRI then
+        if isNothing maybeRI && clientProto cl < 60 && B.isPrefixOf "[v" roomName then
+            [Warning $ loc "Room version incompatible to your Hedgewars version!"]
+            else if isNothing maybeRI then
             [Warning $ loc "No such room."]
             else if (not sameProto) && (not $ isAdministrator cl) then
             [Warning $ loc "Room version incompatible to your Hedgewars version!"]
--- a/gameServer/HandlerUtils.hs	Fri Sep 23 12:47:47 2022 -0400
+++ b/gameServer/HandlerUtils.hs	Tue Sep 27 14:59:03 2022 +0300
@@ -21,6 +21,7 @@
 import Control.Monad.Reader
 import qualified Data.ByteString.Char8 as B
 import Data.List
+import Data.Word
 
 import RoomsAndClients
 import CoreTypes
@@ -74,6 +75,11 @@
     let p = clientProto (rnc `client` ci)
     return . map sendChan . filter (\c -> clientProto c == p) . map (client rnc) $ allClients rnc
 
+allChansProto :: Reader (ClientIndex, IRnC) [(ClientChan, Word16)]
+allChansProto = do
+    (ci, rnc) <- ask
+    return . map ((\c -> (sendChan c, clientProto c)) . client rnc) $ allClients rnc
+
 answerClient :: [B.ByteString] -> Reader (ClientIndex, IRnC) [Action]
 answerClient msg = liftM ((: []) . flip AnswerClients msg) thisClientChans
 
--- a/gameServer/Utils.hs	Fri Sep 23 12:47:47 2022 -0400
+++ b/gameServer/Utils.hs	Tue Sep 27 14:59:03 2022 +0300
@@ -158,11 +158,16 @@
 upperCase :: B.ByteString -> B.ByteString
 upperCase = UTF8.fromString . map Char.toUpper . UTF8.toString
 
+roomNameByProto :: B.ByteString -> Word16 -> Word16 -> B.ByteString
+roomNameByProto roomName roomProto clientProto
+    | clientProto < 60 && roomProto /= clientProto = B.concat [B.pack "[v", protoNumber2ver roomProto, B.pack "] ", roomName]
+    | otherwise = roomName
+
 roomInfo :: Word16 -> B.ByteString -> RoomInfo -> [B.ByteString]
 roomInfo p n r
     | p < 46 = [
         showB $ isJust $ gameInfo r,
-        name r,
+        roomNameByProto (name r) (roomProto r) p,
         showB $ playersIn r,
         showB $ length $ teams r,
         n,
@@ -172,7 +177,18 @@
         ]
     | p < 48 = [
         showB $ isJust $ gameInfo r,
-        name r,
+        roomNameByProto (name r) (roomProto r) p,
+        showB $ playersIn r,
+        showB $ length $ teams r,
+        n,
+        Map.findWithDefault "+rnd+" "MAP" (mapParams r),
+        head (Map.findWithDefault ["Normal"] "SCRIPT" (params r)),
+        head (Map.findWithDefault ["Default"] "SCHEME" (params r)),
+        head (Map.findWithDefault ["Default"] "AMMO" (params r))
+        ]
+    | p < 60 = [
+        B.pack roomFlags,
+        roomNameByProto (name r) (roomProto r) p,
         showB $ playersIn r,
         showB $ length $ teams r,
         n,
@@ -190,7 +206,8 @@
         Map.findWithDefault "+rnd+" "MAP" (mapParams r),
         head (Map.findWithDefault ["Normal"] "SCRIPT" (params r)),
         head (Map.findWithDefault ["Default"] "SCHEME" (params r)),
-        head (Map.findWithDefault ["Default"] "AMMO" (params r))
+        head (Map.findWithDefault ["Default"] "AMMO" (params r)),
+        showB $ roomProto r
         ]
     where
         roomFlags = concat [