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
+}