diff --git a/cmake/Info.plist b/cmake/Info.plist index 82c1e2007..7f01befcb 100644 --- a/cmake/Info.plist +++ b/cmake/Info.plist @@ -115,4 +115,4 @@ - \ No newline at end of file + diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 8f81e2565..84b2c38af 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -416,9 +416,18 @@ Section "un.Application" UnSecApplication RMDir "$SMPROGRAMS\Cockatrice" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" - DeleteRegKey HKCR ".cod" - DeleteRegKey HKCR "Cockatrice" - DeleteRegKey HKCR "cockatrice" + + ; Only remove the file/protocol associations if we registered them (i.e. the + ; install was not portable) and .cod is still owned by Cockatrice, so we don't + ; clobber a .cod association installed by another application. + ${If} Not ${FileExists} "$INSTDIR\portable.dat" + ReadRegStr $0 HKCR ".cod" "" + ${If} $0 == "Cockatrice" + DeleteRegKey HKCR ".cod" + DeleteRegKey HKCR "Cockatrice" + DeleteRegKey HKCR "cockatrice" + ${EndIf} + ${EndIf} SectionEnd ; unselected because it is /o diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 6f0495ec6..574c9bc34 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -530,6 +530,22 @@ if(UNIX) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR}) + + # Refresh the freedesktop databases so the file associations and scheme + # handler register without requiring the user to run them manually. The + # tools may be missing on minimal systems; that is fine, packaging systems + # usually refresh these databases through their own triggers. + find_program(UPDATE_MIME_DATABASE update-mime-database) + if(UPDATE_MIME_DATABASE) + install(CODE "execute_process(COMMAND \"${UPDATE_MIME_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/mime\")") + endif() + + find_program(UPDATE_DESKTOP_DATABASE update-desktop-database) + if(UPDATE_DESKTOP_DATABASE) + install( + CODE "execute_process(COMMAND \"${UPDATE_DESKTOP_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/applications\")" + ) + endif() endif() elseif(WIN32) install(TARGETS cockatrice RUNTIME DESTINATION ./) diff --git a/cockatrice/cockatrice-cod.xml b/cockatrice/cockatrice-cod.xml index 2144aec8b..1a0199433 100644 --- a/cockatrice/cockatrice-cod.xml +++ b/cockatrice/cockatrice-cod.xml @@ -4,4 +4,4 @@ Cockatrice Deck File - \ No newline at end of file + diff --git a/cockatrice/cockatrice.desktop b/cockatrice/cockatrice.desktop index ef952e4dd..4b15fa9c1 100644 --- a/cockatrice/cockatrice.desktop +++ b/cockatrice/cockatrice.desktop @@ -7,4 +7,4 @@ Exec=cockatrice %U Icon=cockatrice Categories=Game;CardGame; MimeType=application/x-cockatrice; -X-Scheme-Handler/cockatrice=true \ No newline at end of file +X-Scheme-Handler/cockatrice=true diff --git a/cockatrice/src/client/url_scheme_event_filter.h b/cockatrice/src/client/url_scheme_event_filter.h index 22e7f25ca..9e96502ca 100644 --- a/cockatrice/src/client/url_scheme_event_filter.h +++ b/cockatrice/src/client/url_scheme_event_filter.h @@ -5,29 +5,33 @@ #include #include #include +#include /** - * @brief Event filter that catches QFileOpenEvent URLs matching a scheme - * prefix and re-emits them as urlReceived(). + * @brief Event filter that catches QFileOpenEvent URLs matching a scheme and + * re-emits them as urlReceived(). * * On macOS, when the application is registered as a URL scheme handler, the * OS delivers incoming URLs via QFileOpenEvent on the QApplication object. * Install this filter on QApplication to intercept them: * * @code - * UrlSchemeEventFilter filter(QStringLiteral("cockatrice://")); + * UrlSchemeEventFilter filter(QStringList{QStringLiteral("cockatrice")}); * QObject::connect(&filter, &UrlSchemeEventFilter::urlReceived, * &mainWindow, &MainWindow::handleUrl); * app.installEventFilter(&filter); * @endcode + * + * Note: the strings are compared against QUrl::scheme(), so they must be + * written without the "://" suffix (e.g. "cockatrice", not "cockatrice://"). */ class UrlSchemeEventFilter : public QObject { Q_OBJECT public: - explicit UrlSchemeEventFilter(const QStringList &schemePrefix, QObject *parent = nullptr) - : QObject(parent), m_prefixes(schemePrefix) + explicit UrlSchemeEventFilter(const QStringList &schemes, QObject *parent = nullptr) + : QObject(parent), prefixes(schemes) { } @@ -42,7 +46,7 @@ public: const QUrl url = fileEvent->url(); - for (auto prefix : m_prefixes) { + for (const auto &prefix : prefixes) { if (url.scheme() == prefix) { emit urlReceived(url.toString()); return true; @@ -59,7 +63,7 @@ public: } private: - QStringList m_prefixes; + QStringList prefixes; }; #endif // COCKATRICE_URL_SCHEME_EVENT_FILTER_H diff --git a/cockatrice/src/interface/intents/intent.cpp b/cockatrice/src/interface/intents/intent.cpp index 199812a43..c02a89f35 100644 --- a/cockatrice/src/interface/intents/intent.cpp +++ b/cockatrice/src/interface/intents/intent.cpp @@ -30,3 +30,19 @@ void Intent::runDependency(Intent *dependency) dependency->execute(); } + +void Intent::emitFinished() +{ + if (!completed) { + completed = true; + emit finished(); + } +} + +void Intent::emitFailed(const QString &reason) +{ + if (!completed) { + completed = true; + emit failed(reason); + } +} diff --git a/cockatrice/src/interface/intents/intent.h b/cockatrice/src/interface/intents/intent.h index 731e6de75..125900ecd 100644 --- a/cockatrice/src/interface/intents/intent.h +++ b/cockatrice/src/interface/intents/intent.h @@ -25,6 +25,13 @@ protected: // Helper to chain another intent void runDependency(Intent *dependency); + + // Emit the outcome exactly once; ignore late signals after the intent is done. + void emitFinished(); + void emitFailed(const QString &reason); + +private: + bool completed = false; }; #endif // COCKATRICE_INTENT_H diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.cpp b/cockatrice/src/interface/intents/intent_connect_to_server.cpp index 927079d5d..1cccc5a23 100644 --- a/cockatrice/src/interface/intents/intent_connect_to_server.cpp +++ b/cockatrice/src/interface/intents/intent_connect_to_server.cpp @@ -2,6 +2,8 @@ #include "intent_disconnect_from_server.h" +#include + IntentConnectToServer::IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context) : Intent(), remoteClient(_remoteClient), context(_context) { @@ -16,10 +18,14 @@ void IntentConnectToServer::onPreconditionSatisfied() { remoteClient->connectToServer(context->hostname, context->port.toUInt(), context->username, context->password); connect(remoteClient, &RemoteClient::statusChanged, this, &IntentConnectToServer::onStatusChanged); - connect(remoteClient, &RemoteClient::loginError, this, - [this](Response::ResponseCode, const QString &reason, quint32, const QList &) { - emit failed(reason); - }); + connect(remoteClient, &RemoteClient::socketError, this, &IntentConnectToServer::onSocketError); + connect( + remoteClient, &RemoteClient::loginError, this, + [this](Response::ResponseCode, const QString &reason, quint32, const QList &) { emitFailed(reason); }); + + QTimer::singleShot(15000, this, [this]() { + emitFailed(tr("Timed out while connecting to %1:%2").arg(context->hostname, context->port)); + }); } void IntentConnectToServer::onPreconditionNotSatisfied() @@ -30,6 +36,11 @@ void IntentConnectToServer::onPreconditionNotSatisfied() void IntentConnectToServer::onStatusChanged(ClientStatus status) { if (status == ClientStatus::StatusLoggedIn) { - emit finished(); + emitFinished(); } } + +void IntentConnectToServer::onSocketError(const QString &errorString) +{ + emitFailed(tr("Failed to connect to %1:%2: %3").arg(context->hostname, context->port, errorString)); +} diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.h b/cockatrice/src/interface/intents/intent_connect_to_server.h index 86490a765..eab4d1a21 100644 --- a/cockatrice/src/interface/intents/intent_connect_to_server.h +++ b/cockatrice/src/interface/intents/intent_connect_to_server.h @@ -23,6 +23,7 @@ private: private slots: void onStatusChanged(ClientStatus status); + void onSocketError(const QString &errorString); }; #endif // COCKATRICE_INTENT_CONNECT_TO_SERVER_H diff --git a/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp index fd53dccc5..cb39d7bab 100644 --- a/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp +++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp @@ -12,7 +12,7 @@ bool IntentDisconnectFromServer::checkPrecondition() const void IntentDisconnectFromServer::onPreconditionSatisfied() { - emit finished(); + emitFinished(); } void IntentDisconnectFromServer::onPreconditionNotSatisfied() @@ -24,6 +24,6 @@ void IntentDisconnectFromServer::onPreconditionNotSatisfied() void IntentDisconnectFromServer::onStatusChanged(ClientStatus status) { if (status == ClientStatus::StatusDisconnected) { - emit finished(); + emitFinished(); } } diff --git a/cockatrice/src/interface/intents/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp index 775abe3fe..fb9c4d5ce 100644 --- a/cockatrice/src/interface/intents/intent_join_server_game.cpp +++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp @@ -9,8 +9,8 @@ IntentJoinServerGame::IntentJoinServerGame(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, - ContextJoinGame *_context) - : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context) + std::unique_ptr _context) + : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context.release()) { } @@ -25,6 +25,9 @@ bool IntentJoinServerGame::checkPrecondition() const if (remoteClient->peerName() != context->roomContext.serverContext.hostname) { return false; } + if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) { + return false; + } if (!tabSupervisor->getRoomTabs().contains(context->roomContext.roomId)) { return false; @@ -53,8 +56,7 @@ bool IntentJoinServerGame::tryJoinGame(TabRoom *room) } if (room->getGameSelector()->joinGameById(context->gameId)) { - joined = true; - emit finished(); + emitFinished(); return true; } @@ -70,9 +72,5 @@ void IntentJoinServerGame::waitForGame(TabRoom *room) } }); - QTimer::singleShot(15000, this, [this]() { - if (!joined) { - emit failed(tr("Game %1 not found in the room").arg(context->gameId)); - } - }); + QTimer::singleShot(15000, this, [this]() { emitFailed(tr("Game %1 not found in the room").arg(context->gameId)); }); } diff --git a/cockatrice/src/interface/intents/intent_join_server_game.h b/cockatrice/src/interface/intents/intent_join_server_game.h index cd55f8323..5e196df38 100644 --- a/cockatrice/src/interface/intents/intent_join_server_game.h +++ b/cockatrice/src/interface/intents/intent_join_server_game.h @@ -6,6 +6,7 @@ #include "remote_client.h" #include +#include class TabRoom; class TabSupervisor; @@ -15,7 +16,9 @@ class IntentJoinServerGame : public Intent Q_OBJECT public: - IntentJoinServerGame(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinGame *_context); + IntentJoinServerGame(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + std::unique_ptr _context); protected: bool checkPrecondition() const override; @@ -29,7 +32,6 @@ private: TabSupervisor *tabSupervisor; RemoteClient *remoteClient; QScopedPointer context; - bool joined = false; }; #endif // COCKATRICE_INTENT_JOIN_SERVER_GAME_H diff --git a/cockatrice/src/interface/intents/intent_join_server_room.cpp b/cockatrice/src/interface/intents/intent_join_server_room.cpp index c88dd5871..d25bc8d17 100644 --- a/cockatrice/src/interface/intents/intent_join_server_room.cpp +++ b/cockatrice/src/interface/intents/intent_join_server_room.cpp @@ -5,6 +5,9 @@ #include "../widgets/tabs/tab_supervisor.h" #include "intent_connect_to_server.h" +#include +#include + IntentJoinServerRoom::IntentJoinServerRoom(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinRoom *_context) @@ -23,6 +26,9 @@ bool IntentJoinServerRoom::checkPrecondition() const if (remoteClient->peerName() != context->serverContext.hostname) { return false; } + if (QString::number(remoteClient->peerPort()) != context->serverContext.port) { + return false; + } return true; } @@ -31,7 +37,7 @@ void IntentJoinServerRoom::onPreconditionSatisfied() { if (tabSupervisor->getRoomTabs().contains(context->roomId)) { tabSupervisor->setCurrentWidget(tabSupervisor->getRoomTabs().value(context->roomId)); - emit finished(); + emitFinished(); return; } @@ -41,12 +47,25 @@ void IntentJoinServerRoom::onPreconditionSatisfied() tabServer = tabSupervisor->getTabServer(); } if (!tabServer) { - emit failed(tr("No server tab available")); + emitFailed(tr("No server tab available")); return; } - tabServer->joinRoom(context->roomId, true); - connect(tabServer, &TabServer::roomJoined, this, &IntentJoinServerRoom::finished); + const int roomId = context->roomId; + tabServer->joinRoom(roomId, true); + connect(tabServer, &TabServer::roomJoined, this, [this, roomId](const ServerInfo_Room &info, bool) { + if (info.room_id() == roomId) { + emitFinished(); + } + }); + connect(tabServer, &TabServer::roomJoinFailed, this, [this, roomId](int failedRoomId) { + if (failedRoomId == roomId) { + emitFailed(tr("Failed to join the server room %1").arg(roomId)); + } + }); + + QTimer::singleShot(15000, this, + [this, roomId]() { emitFailed(tr("Timed out while joining the server room %1").arg(roomId)); }); } void IntentJoinServerRoom::onPreconditionNotSatisfied() diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp index 010f3eaf2..ff871fd03 100644 --- a/cockatrice/src/interface/intents/intent_login.cpp +++ b/cockatrice/src/interface/intents/intent_login.cpp @@ -21,13 +21,13 @@ void IntentGetLoginCredentials::onPreconditionSatisfied() if (index >= 0) { context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); - emit finished(); + emitFinished(); } else { - emit failed(tr("No saved credentials for this server")); + emitFailed(tr("No saved credentials for this server")); } } void IntentGetLoginCredentials::onPreconditionNotSatisfied() { - emit failed(tr("No saved credentials for this server")); + emitFailed(tr("No saved credentials for this server")); } diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.cpp b/cockatrice/src/interface/intents/intent_open_local_deck.cpp index ccafd434c..2457bec72 100644 --- a/cockatrice/src/interface/intents/intent_open_local_deck.cpp +++ b/cockatrice/src/interface/intents/intent_open_local_deck.cpp @@ -22,8 +22,10 @@ void IntentOpenLocalDeck::onPreconditionSatisfied() std::optional deckOpt = DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true); if (deckOpt) { tabSupervisor->openDeckInNewTab(deckOpt.value()); + emitFinished(); + } else { + emitFailed(tr("Unable to load deck file %1").arg(file)); } - emit finished(); } void IntentOpenLocalDeck::onPreconditionNotSatisfied() diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp index 62c7f346b..c36378818 100644 --- a/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp +++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp @@ -9,11 +9,11 @@ bool IntentWaitForDatabaseLoad::checkPrecondition() const void IntentWaitForDatabaseLoad::onPreconditionSatisfied() { - emit finished(); + emitFinished(); } void IntentWaitForDatabaseLoad::onPreconditionNotSatisfied() { connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, - [this]() { emit finished(); }); + [this]() { emitFinished(); }); } diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index 5732df49d..8b5309603 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -6,8 +6,10 @@ #include "intent_login.h" #include +#include #include #include +#include IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow) { @@ -35,17 +37,29 @@ void IntentUrlParser::handle(const QString &urlStr) void IntentUrlParser::handleJoinGame(const QUrlQuery &query) { - auto ctx = new ContextJoinGame(); + auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); }; + + auto ctx = std::make_unique(); ctx->roomContext.serverContext.hostname = query.queryItemValue("hostname"); ctx->roomContext.serverContext.port = query.queryItemValue("port"); + if (ctx->roomContext.serverContext.hostname.isEmpty()) { + showError(tr("Missing or empty hostname in the game link")); + return; + } + bool ok = false; + ctx->roomContext.serverContext.port.toUShort(&ok); + if (!ok) { + showError(tr("Invalid or missing port in the game link")); + return; + } + ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok); if (!ok) { - qWarning() << "Invalid or missing roomId"; - delete ctx; + showError(tr("Invalid or missing room id in the game link")); return; } @@ -53,21 +67,23 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query) ctx->gameId = query.queryItemValue("gameid").toInt(&ok); if (!ok) { - qWarning() << "Invalid or missing gameId"; - delete ctx; + showError(tr("Invalid or missing game id in the game link")); return; } // The join game intent owns the context and the credential lookup; once the // chain finishes (or fails) it deletes the whole tree. - auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), ctx); + ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; + auto joinGameIntent = + new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx)); joinGameIntent->setParent(this); - auto getLoginCredentialsIntent = new IntentGetLoginCredentials(&ctx->roomContext.serverContext); + auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext); getLoginCredentialsIntent->setParent(joinGameIntent); connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed); + connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); getLoginCredentialsIntent->execute(); } diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h index 7b17079ea..bac0e3d25 100644 --- a/cockatrice/src/interface/intents/url_parser.h +++ b/cockatrice/src/interface/intents/url_parser.h @@ -13,8 +13,6 @@ public: void handle(const QString &urlStr); void handleJoinGame(const QUrlQuery &query); - void parse(QString url); - private: MainWindow *mainWindow; }; diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 30690dc82..11b36ca92 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -10,12 +10,16 @@ #include "games_model.h" #include "user/user_list_manager.h" +#include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -315,6 +319,21 @@ void GameSelector::customContextMenu(const QPoint &point) dlg.exec(); }); + QAction copyLink(tr("Copy Game Link")); + connect(©Link, &QAction::triggered, this, [=, this]() { + const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt()); + QUrl url; + url.setScheme("cockatrice"); + url.setHost("joingame"); + QUrlQuery query; + query.addQueryItem("hostname", client->serverName()); + query.addQueryItem("port", QString::number(client->serverPort())); + query.addQueryItem("roomid", QString::number(gameInfo.room_id())); + query.addQueryItem("gameid", QString::number(gameInfo.game_id())); + url.setQuery(query); + QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded)); + }); + QMenu menu; menu.addAction(&joinGame); @@ -332,6 +351,11 @@ void GameSelector::customContextMenu(const QPoint &point) menu.addAction(&spectateGame); menu.addAction(&getGameInfo); + + if (!client->serverName().isEmpty()) { + menu.addAction(©Link); + } + menu.exec(gameListView->mapToGlobal(point)); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.cpp b/cockatrice/src/interface/widgets/tabs/tab_server.cpp index 2fce5c1fa..13a77e957 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_server.cpp @@ -191,7 +191,10 @@ void TabServer::joinRoom(int id, bool setCurrent) PendingCommand *pend = client->prepareSessionCommand(cmd); pend->setExtraData(setCurrent); - connect(pend, &PendingCommand::finished, this, &TabServer::joinRoomFinished); + connect(pend, &PendingCommand::finished, this, + [this, id](const Response &r, const CommandContainer &c, const QVariant &v) { + joinRoomFinished(r, c, v, id); + }); client->sendCommand(pend); @@ -205,7 +208,8 @@ void TabServer::joinRoom(int id, bool setCurrent) void TabServer::joinRoomFinished(const Response &r, const CommandContainer & /*commandContainer*/, - const QVariant &extraData) + const QVariant &extraData, + int roomId) { switch (r.response_code()) { case Response::RespOk: @@ -213,21 +217,25 @@ void TabServer::joinRoomFinished(const Response &r, case Response::RespNameNotFound: QMessageBox::critical(this, tr("Error"), tr("Failed to join the server room: it doesn't exist on the server.")); + emit roomJoinFailed(roomId); return; case Response::RespContextError: QMessageBox::critical( this, tr("Error"), tr("The server thinks you are in the server room but your client is unable to display it. " "Try restarting your client.")); + emit roomJoinFailed(roomId); return; case Response::RespUserLevelTooLow: QMessageBox::critical(this, tr("Error"), tr("You do not have the required permission to join this server room.")); + emit roomJoinFailed(roomId); return; default: QMessageBox::critical( this, tr("Error"), tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); + emit roomJoinFailed(roomId); return; } diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.h b/cockatrice/src/interface/widgets/tabs/tab_server.h index 634dd4cde..c10b7945b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.h +++ b/cockatrice/src/interface/widgets/tabs/tab_server.h @@ -49,9 +49,13 @@ class TabServer : public Tab Q_OBJECT signals: void roomJoined(const ServerInfo_Room &info, bool setCurrent); + void roomJoinFailed(int roomId); private slots: void processServerMessageEvent(const Event_ServerMessage &event); - void joinRoomFinished(const Response &resp, const CommandContainer &commandContainer, const QVariant &extraData); + void joinRoomFinished(const Response &resp, + const CommandContainer &commandContainer, + const QVariant &extraData, + int roomId); private: AbstractClient *client; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 65f1ae96f..0524112e4 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -263,6 +264,35 @@ int main(int argc, char *argv[]) Logger::getInstance().logToFile(true); } + // --- Handle files or URLs passed at startup --- + // Only positional arguments are treated as files/URLs, so options like + // --connect are never handed off to another instance. + const QStringList startupFiles = parser.positionalArguments(); + const bool hasActivationFiles = !startupFiles.isEmpty(); + + SingleInstanceManager instance; + + if (hasActivationFiles) { + // Activation launch: hand off to the primary instance if one is + // running, otherwise become the primary ourselves. Do this before + // constructing the main window so a hand-off exits cheaply. + if (!instance.tryRun(startupFiles)) { + // Sent successfully → exit + return 0; + } + // No primary instance → become server + qInfo() << "No existing instance found, becoming primary instance"; + } else { + // Plain launch: if another instance is running, run independently + // instead of handing off and exiting. + if (!instance.tryRun(QStringList())) { + // Another instance is already running → just run independently + qInfo() << "Another instance exists, running independently"; + } else { + qInfo() << "No existing instance found, starting server"; + } + } + rng = new RNG_SFMT; themeManager = new ThemeManager; soundEngine = new SoundEngine; @@ -301,6 +331,9 @@ int main(int argc, char *argv[]) urlParser->handle(file); } else if (QFileInfo(file).exists()) { auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); + QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) { + QMessageBox::warning(&ui, QObject::tr("Open deck"), reason); + }); openDeckIntent->execute(); } }; @@ -326,35 +359,6 @@ int main(int argc, char *argv[]) // then reload the DB. otherwise just reload the DB SpoilerBackgroundUpdater spoilerBackgroundUpdater; - // --- Handle files or URLs passed at startup --- - QStringList startupFiles; - for (int i = 1; i < argc; ++i) { - startupFiles.append(QString::fromLocal8Bit(argv[i])); - } - - bool hasActivationFiles = !startupFiles.isEmpty(); - - SingleInstanceManager instance; - - if (hasActivationFiles) { - // Activation launch: try to forward - if (!instance.tryRun(startupFiles)) { - // Sent successfully → exit - return 0; - } - // No primary instance → become server - qInfo() << "No existing instance found, becoming primary instance"; - } else { - // Plain launch: if another instance is running, run independently - // instead of handing off and exiting. - if (!instance.tryRun(QStringList())) { - // Another instance is already running → just run independently - qInfo() << "Another instance exists, running independently"; - } else { - qInfo() << "No existing instance found, starting server"; - } - } - ui.show(); qCInfo(MainLog) << "ui.show() finished"; diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp index 3aa0f1a12..aca23160c 100644 --- a/cockatrice/src/single_instance_manager.cpp +++ b/cockatrice/src/single_instance_manager.cpp @@ -1,12 +1,24 @@ #include "single_instance_manager.h" +#include + SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent) { } bool SingleInstanceManager::tryRun(const QStringList &filesToSend) { - serverName = "CockatriceSingleInstance"; + // Scope the socket name to the current user. On Linux the default abstract + // namespace is system-wide, so a plain name would let one user's instance + // hijack another user's session. + QString userName = qEnvironmentVariable("USER"); + if (userName.isEmpty()) { + userName = qEnvironmentVariable("USERNAME"); + } + if (userName.isEmpty()) { + userName = QDir::home().dirName(); + } + serverName = QStringLiteral("CockatriceSingleInstance-%1").arg(userName); // Hand off to an already-running primary instance if one exists. if (forwardToPrimary(filesToSend)) { diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index 2eb7e3356..982aa6bf3 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h @@ -122,6 +122,20 @@ public: return userName; } + /** + * @brief Returns the server address configured for the current connection. + * + * May be empty for clients that have no server counterpart (e.g. local test clients). + */ + virtual QString serverName() const + { + return {}; + } + virtual quint16 serverPort() const + { + return 0; + } + static PendingCommand *prepareSessionCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId); static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd); diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h index e699ec30a..862dac06e 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h @@ -139,6 +139,14 @@ public: return socket->peerPort(); } } + QString serverName() const override + { + return lastHostname; + } + quint16 serverPort() const override + { + return static_cast(lastPort); + } void connectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password); void registerToServer(const QString &hostname, diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index 4de2695f4..5c271328b 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -337,4 +337,4 @@ bool ServersSettings::hasCredentials(const QString &host, const QString &port) c bool ServersSettings::hasLoginData(const QString &host, const QString &port) const { return hasUsername(host, port) && hasCredentials(host, port); -} \ No newline at end of file +}