diff --git a/cmake/Info.plist b/cmake/Info.plist
index 614d82509..813a9d4dc 100644
--- a/cmake/Info.plist
+++ b/cmake/Info.plist
@@ -34,5 +34,31 @@
${MACOSX_BUNDLE_COPYRIGHT}
NSHighResolutionCapable
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeExtensions
+
+ cod
+
+ CFBundleTypeName
+ Cockatrice
+ CFBundleTypeRole
+ Editor
+ LSHandlerRank
+ Default
+
+
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ Cockatrice
+ CFBundleURLSchemes
+
+ cockatrice
+
+
+
diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in
index 7b52b7bcc..6cc28a87c 100644
--- a/cmake/NSIS.template.in
+++ b/cmake/NSIS.template.in
@@ -293,6 +293,20 @@ Section "Application" SecApplication
SetShellVarContext all
SetOutPath "$INSTDIR"
+${If} $PortableMode = 0
+
+ ; --- Register .cod file type ---
+ WriteRegStr HKCR ".cod" "" "Cockatrice"
+ WriteRegStr HKCR "Cockatrice" "" "Cockatrice Deck File"
+ WriteRegStr HKCR "Cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"'
+
+ ; --- Register custom URI protocol ---
+ WriteRegStr HKCR "cockatrice" "" "URL: Cockatrice Protocol"
+ WriteRegStr HKCR "cockatrice" "URL Protocol" ""
+ WriteRegStr HKCR "cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"'
+
+${EndIf}
+
${If} $PortableMode = 1
${AndIf} ${FileExists} "$INSTDIR\portable.dat"
; upgrade portable mode
@@ -401,6 +415,9 @@ Section "un.Application" UnSecApplication
RMDir "$SMPROGRAMS\Cockatrice"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
+ DeleteRegKey HKCR ".cod"
+ DeleteRegKey HKCR "Cockatrice"
+ DeleteRegKey HKCR "cockatrice"
SectionEnd
; unselected because it is /o
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index 12733afe6..fbe1dc2dd 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -125,6 +125,9 @@ set(cockatrice_SOURCES
src/interface/card_picture_loader/card_picture_loader_worker.cpp
src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
src/interface/card_picture_loader/card_picture_to_load.cpp
+ src/interface/intents/intent.cpp
+ src/interface/intents/intent_open_local_deck.cpp
+ src/interface/intents/intent_wait_for_database_load.cpp
src/interface/layouts/flow_layout.cpp
src/interface/layouts/overlap_layout.cpp
src/interface/widgets/utility/line_edit_completer.cpp
@@ -325,6 +328,19 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
+ src/single_instance_manager.cpp
+ src/single_instance_manager.h
+ src/interface/intents/intent_connect_to_server.cpp
+ src/interface/intents/intent_connect_to_server.h
+ src/interface/intents/intent_disconnect_from_server.cpp
+ src/interface/intents/intent_disconnect_from_server.h
+ src/interface/intents/intent_join_server_game.cpp
+ src/interface/intents/intent_join_server_room.cpp
+ src/interface/intents/intent_join_server_room.h
+ src/interface/intents/url_parser.cpp
+ src/interface/intents/url_parser.h
+ src/interface/intents/intent_login.cpp
+ src/interface/intents/intent_login.h
)
add_subdirectory(sounds)
@@ -382,6 +398,11 @@ set(DESKTOPDIR
CACHE STRING "desktop file destination"
)
+set(MIMEDIR
+ share/mime/packages
+ CACHE STRING "mime file destination"
+)
+
set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
@@ -466,6 +487,7 @@ if(UNIX)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps)
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})
endif()
elseif(WIN32)
install(TARGETS cockatrice RUNTIME DESTINATION ./)
diff --git a/cockatrice/cockatrice-cod.xml b/cockatrice/cockatrice-cod.xml
new file mode 100644
index 000000000..2144aec8b
--- /dev/null
+++ b/cockatrice/cockatrice-cod.xml
@@ -0,0 +1,7 @@
+
+
+
+ Cockatrice Deck File
+
+
+
\ No newline at end of file
diff --git a/cockatrice/cockatrice.desktop b/cockatrice/cockatrice.desktop
index 092d84ef5..65e77e9f9 100644
--- a/cockatrice/cockatrice.desktop
+++ b/cockatrice/cockatrice.desktop
@@ -6,3 +6,5 @@ Name=Cockatrice
Exec=cockatrice
Icon=cockatrice
Categories=Game;CardGame;
+MimeType=application/x-cockatrice;
+X-Scheme-Handler/cockatrice=true
\ No newline at end of file
diff --git a/cockatrice/src/interface/intents/contexts/context_connect_to_server.h b/cockatrice/src/interface/intents/contexts/context_connect_to_server.h
new file mode 100644
index 000000000..c7c40b261
--- /dev/null
+++ b/cockatrice/src/interface/intents/contexts/context_connect_to_server.h
@@ -0,0 +1,14 @@
+#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
+#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
+
+#include
+
+struct ContextConnectToServer
+{
+ QString hostname;
+ QString port;
+ QString username;
+ QString password;
+};
+
+#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
diff --git a/cockatrice/src/interface/intents/contexts/context_join_game.h b/cockatrice/src/interface/intents/contexts/context_join_game.h
new file mode 100644
index 000000000..102e2a520
--- /dev/null
+++ b/cockatrice/src/interface/intents/contexts/context_join_game.h
@@ -0,0 +1,11 @@
+#ifndef COCKATRICE_CONTEXT_JOIN_GAME_H
+#define COCKATRICE_CONTEXT_JOIN_GAME_H
+#include "context_join_room.h"
+
+struct ContextJoinGame
+{
+ ContextJoinRoom roomContext;
+ int gameId;
+};
+
+#endif // COCKATRICE_CONTEXT_JOIN_GAME_H
diff --git a/cockatrice/src/interface/intents/contexts/context_join_room.h b/cockatrice/src/interface/intents/contexts/context_join_room.h
new file mode 100644
index 000000000..23ae05e81
--- /dev/null
+++ b/cockatrice/src/interface/intents/contexts/context_join_room.h
@@ -0,0 +1,14 @@
+#ifndef COCKATRICE_CONTEXT_JOIN_ROOM_H
+#define COCKATRICE_CONTEXT_JOIN_ROOM_H
+
+#include "context_connect_to_server.h"
+
+#include
+
+struct ContextJoinRoom
+{
+ ContextConnectToServer serverContext;
+ int roomId;
+};
+
+#endif // COCKATRICE_CONTEXT_JOIN_ROOM_H
diff --git a/cockatrice/src/interface/intents/intent.cpp b/cockatrice/src/interface/intents/intent.cpp
new file mode 100644
index 000000000..916af1649
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent.cpp
@@ -0,0 +1 @@
+#include "intent.h"
diff --git a/cockatrice/src/interface/intents/intent.h b/cockatrice/src/interface/intents/intent.h
new file mode 100644
index 000000000..651946a14
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent.h
@@ -0,0 +1,49 @@
+#ifndef COCKATRICE_INTENT_H
+#define COCKATRICE_INTENT_H
+
+#include
+
+class Intent : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit Intent(QObject *parent = nullptr) : QObject(parent)
+ {
+ }
+ virtual ~Intent() = default;
+
+ void execute()
+ {
+ if (checkPrecondition()) {
+ onPreconditionSatisfied();
+ } else {
+ onPreconditionNotSatisfied();
+ }
+ }
+
+signals:
+ void finished();
+ void failed(QString reason);
+
+protected:
+ // --- Subclasses must implement these ---
+ virtual bool checkPrecondition() const = 0;
+ virtual void onPreconditionSatisfied() = 0;
+ virtual void onPreconditionNotSatisfied() = 0;
+
+ // Helper to chain another intent
+ void runDependency(Intent *dependency)
+ {
+ connect(dependency, &Intent::finished, this, [this]() {
+ // Re-check after dependency finishes
+ this->execute();
+ });
+
+ connect(dependency, &Intent::failed, this, &Intent::failed);
+
+ dependency->execute();
+ }
+};
+
+#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
new file mode 100644
index 000000000..912afa09c
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_connect_to_server.cpp
@@ -0,0 +1 @@
+#include "intent_connect_to_server.h"
diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.h b/cockatrice/src/interface/intents/intent_connect_to_server.h
new file mode 100644
index 000000000..291f68849
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_connect_to_server.h
@@ -0,0 +1,54 @@
+#ifndef COCKATRICE_INTENT_CONNECT_TO_SERVER_H
+#define COCKATRICE_INTENT_CONNECT_TO_SERVER_H
+
+#include "contexts/context_connect_to_server.h"
+#include "intent.h"
+#include "intent_disconnect_from_server.h"
+#include "remote_client.h"
+
+#include
+
+class IntentConnectToServer : public Intent
+{
+ Q_OBJECT
+
+public:
+ IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context)
+ : Intent(), remoteClient(_remoteClient), context(_context)
+ {
+ }
+
+protected:
+ bool checkPrecondition() const override
+ {
+ return remoteClient->getStatus() == ClientStatus::StatusDisconnected;
+ }
+
+ void onPreconditionSatisfied() override
+ {
+ remoteClient->connectToServer(context->hostname, context->port.toUInt(), context->username, context->password);
+ connect(remoteClient, &RemoteClient::statusChanged, this, &IntentConnectToServer::onStatusChanged);
+ }
+
+ void onPreconditionNotSatisfied() override
+ {
+ runDependency(new IntentDisconnectFromServer(remoteClient));
+ }
+
+private:
+ RemoteClient *remoteClient;
+ ContextConnectToServer *context;
+private slots:
+ void onStatusChanged(ClientStatus status)
+ {
+ if (status == ClientStatus::StatusLoggedIn) {
+ auto timer = new QTimer(this);
+ timer->setSingleShot(true);
+ timer->setInterval(2000);
+ connect(timer, &QTimer::timeout, this, &IntentConnectToServer::finished);
+ timer->start();
+ }
+ }
+};
+
+#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
new file mode 100644
index 000000000..36f78cede
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp
@@ -0,0 +1 @@
+#include "intent_disconnect_from_server.h"
diff --git a/cockatrice/src/interface/intents/intent_disconnect_from_server.h b/cockatrice/src/interface/intents/intent_disconnect_from_server.h
new file mode 100644
index 000000000..b11452639
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.h
@@ -0,0 +1,47 @@
+#ifndef COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H
+#define COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H
+#include "intent.h"
+#include "remote_client.h"
+
+class IntentDisconnectFromServer : public Intent
+{
+ Q_OBJECT
+
+public:
+ IntentDisconnectFromServer(RemoteClient *_remoteClient) : Intent(), remoteClient(_remoteClient)
+ {
+ }
+
+protected:
+ bool checkPrecondition() const override
+ {
+ return remoteClient->getStatus() == ClientStatus::StatusDisconnected;
+ }
+
+ void onPreconditionSatisfied() override
+ {
+ qWarning() << "Client disconnected, disconnect is finished";
+ emit finished();
+ }
+
+ void onPreconditionNotSatisfied() override
+ {
+ qWarning() << "Client not disconnected, hooking up signal and disconnecting." << remoteClient->getStatus();
+ connect(remoteClient, &RemoteClient::statusChanged, this, &IntentDisconnectFromServer::onStatusChanged);
+ remoteClient->disconnectFromServer();
+ }
+
+private:
+ RemoteClient *remoteClient;
+private slots:
+ void onStatusChanged(ClientStatus status)
+ {
+ qWarning() << "Client Status changed: " << status;
+ if (status == ClientStatus::StatusDisconnected) {
+ qWarning() << "Client disconnected, finished";
+ emit finished();
+ }
+ }
+};
+
+#endif // COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H
diff --git a/cockatrice/src/interface/intents/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp
new file mode 100644
index 000000000..853307eb5
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp
@@ -0,0 +1 @@
+#include "intent_join_server_game.h"
diff --git a/cockatrice/src/interface/intents/intent_join_server_game.h b/cockatrice/src/interface/intents/intent_join_server_game.h
new file mode 100644
index 000000000..79bbfe7f9
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_join_server_game.h
@@ -0,0 +1,63 @@
+#ifndef COCKATRICE_INTENT_JOIN_SERVER_GAME_H
+#define COCKATRICE_INTENT_JOIN_SERVER_GAME_H
+
+#include "../widgets/server/game_selector.h"
+#include "../widgets/tabs/tab_room.h"
+#include "../widgets/tabs/tab_server.h"
+#include "../widgets/tabs/tab_supervisor.h"
+#include "contexts/context_join_game.h"
+#include "contexts/context_join_room.h"
+#include "intent.h"
+#include "intent_join_server_room.h"
+#include "remote_client.h"
+
+class IntentJoinServerGame : public Intent
+{
+ Q_OBJECT
+
+public:
+ IntentJoinServerGame(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinGame *_context)
+ : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context)
+ {
+ }
+
+protected:
+ bool checkPrecondition() const override
+ {
+ if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
+ return false;
+ }
+ if (remoteClient->peerName() != context->roomContext.serverContext.hostname) {
+ return false;
+ }
+ if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) {
+ return false;
+ }
+
+ if (!tabSupervisor->getRoomTabs()[context->roomContext.roomId]) {
+ qWarning() << "No room tab";
+ return false;
+ };
+
+ return true;
+ }
+
+ void onPreconditionSatisfied() override
+ {
+ qWarning() << "All lights green, joining game";
+ TabRoom *room = tabSupervisor->getRoomTabs()[context->roomContext.roomId];
+ room->getGameSelector()->joinGameById(context->gameId);
+ }
+
+ void onPreconditionNotSatisfied() override
+ {
+ runDependency(new IntentJoinServerRoom(tabSupervisor, remoteClient, &context->roomContext));
+ }
+
+private:
+ TabSupervisor *tabSupervisor;
+ RemoteClient *remoteClient;
+ ContextJoinGame *context;
+};
+
+#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
new file mode 100644
index 000000000..89dffc0b3
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_join_server_room.cpp
@@ -0,0 +1 @@
+#include "intent_join_server_room.h"
diff --git a/cockatrice/src/interface/intents/intent_join_server_room.h b/cockatrice/src/interface/intents/intent_join_server_room.h
new file mode 100644
index 000000000..cd48c1df9
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_join_server_room.h
@@ -0,0 +1,58 @@
+#ifndef COCKATRICE_INTENT_JOIN_SERVER_ROOM_H
+#define COCKATRICE_INTENT_JOIN_SERVER_ROOM_H
+
+#include "../widgets/tabs/tab_server.h"
+#include "../widgets/tabs/tab_supervisor.h"
+#include "contexts/context_connect_to_server.h"
+#include "contexts/context_join_room.h"
+#include "intent.h"
+#include "intent_connect_to_server.h"
+#include "intent_disconnect_from_server.h"
+#include "remote_client.h"
+
+class IntentJoinServerRoom : public Intent
+{
+ Q_OBJECT
+
+public:
+ IntentJoinServerRoom(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinRoom *_context)
+ : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context)
+ {
+ }
+
+protected:
+ bool checkPrecondition() const override
+ {
+ if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
+ return false;
+ }
+ if (remoteClient->peerName() != context->serverContext.hostname) {
+ return false;
+ }
+ if (QString::number(remoteClient->peerPort()) != context->serverContext.port) {
+ return false;
+ }
+
+ return true;
+ }
+
+ void onPreconditionSatisfied() override
+ {
+ auto tabServer = tabSupervisor->getTabServer();
+ tabServer->joinRoom(context->roomId, true);
+
+ connect(tabServer, &TabServer::roomJoined, this, &IntentJoinServerRoom::finished);
+ }
+
+ void onPreconditionNotSatisfied() override
+ {
+ runDependency(new IntentConnectToServer(remoteClient, &context->serverContext));
+ }
+
+private:
+ TabSupervisor *tabSupervisor;
+ RemoteClient *remoteClient;
+ ContextJoinRoom *context;
+};
+
+#endif // COCKATRICE_INTENT_JOIN_SERVER_ROOM_H
diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp
new file mode 100644
index 000000000..b788872c3
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_login.cpp
@@ -0,0 +1 @@
+#include "intent_login.h"
diff --git a/cockatrice/src/interface/intents/intent_login.h b/cockatrice/src/interface/intents/intent_login.h
new file mode 100644
index 000000000..d7df3df7a
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_login.h
@@ -0,0 +1,52 @@
+#ifndef COCKATRICE_INTENT_LOGIN_H
+#define COCKATRICE_INTENT_LOGIN_H
+
+#include "../../client/settings/cache_settings.h"
+#include "contexts/context_connect_to_server.h"
+#include "intent.h"
+#include "remote_client.h"
+
+class IntentGetLoginCredentials : public Intent
+{
+ Q_OBJECT
+
+public:
+ IntentGetLoginCredentials(RemoteClient *_remoteClient, ContextConnectToServer *_context)
+ : Intent(), remoteClient(_remoteClient), context(_context)
+ {
+ }
+
+protected:
+ bool checkPrecondition() const override
+ {
+ ServersSettings &servers = SettingsCache::instance().servers();
+ return servers.hasLoginData(context->hostname, context->port);
+ }
+
+ void onPreconditionSatisfied() override
+ {
+ ServersSettings &servers = SettingsCache::instance().servers();
+ auto index = servers.findServerIndex(context->hostname, context->port);
+
+ 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();
+ qWarning() << "Using saved credentials";
+ } else {
+ qWarning() << "No saved server entry";
+ }
+ }
+
+ void onPreconditionNotSatisfied() override
+ {
+ }
+
+private:
+ RemoteClient *remoteClient;
+ ContextConnectToServer *context;
+};
+
+#endif // COCKATRICE_INTENT_LOGIN_H
diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.cpp b/cockatrice/src/interface/intents/intent_open_local_deck.cpp
new file mode 100644
index 000000000..c4a0c81b8
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_open_local_deck.cpp
@@ -0,0 +1 @@
+#include "intent_open_local_deck.h"
diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.h b/cockatrice/src/interface/intents/intent_open_local_deck.h
new file mode 100644
index 000000000..0c06f7078
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_open_local_deck.h
@@ -0,0 +1,44 @@
+#ifndef COCKATRICE_INTENT_OPEN_LOCAL_DECK_H
+#define COCKATRICE_INTENT_OPEN_LOCAL_DECK_H
+#include "../widgets/tabs/tab_supervisor.h"
+#include "intent.h"
+#include "intent_wait_for_database_load.h"
+#include "libcockatrice/card/database/card_database_manager.h"
+
+class IntentOpenLocalDeck : public Intent
+{
+ Q_OBJECT
+
+public:
+ IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file)
+ : Intent(), tabSupervisor(_tabSupervisor), file(_file)
+ {
+ }
+
+protected:
+ bool checkPrecondition() const override
+ {
+ return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok;
+ }
+
+ void onPreconditionSatisfied() override
+ {
+ std::optional deckOpt =
+ DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true);
+ if (deckOpt) {
+ tabSupervisor->openDeckInNewTab(deckOpt.value());
+ }
+ emit finished();
+ }
+
+ void onPreconditionNotSatisfied() override
+ {
+ runDependency(new IntentWaitForDatabaseLoad);
+ }
+
+private:
+ TabSupervisor *tabSupervisor;
+ const QString &file;
+};
+
+#endif // COCKATRICE_INTENT_OPEN_LOCAL_DECK_H
diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp
new file mode 100644
index 000000000..dfdeab8b1
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp
@@ -0,0 +1 @@
+#include "intent_wait_for_database_load.h"
diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.h b/cockatrice/src/interface/intents/intent_wait_for_database_load.h
new file mode 100644
index 000000000..51f4712a8
--- /dev/null
+++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.h
@@ -0,0 +1,29 @@
+#ifndef COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H
+#define COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H
+
+#include "intent.h"
+#include "libcockatrice/card/database/card_database_manager.h"
+
+class IntentWaitForDatabaseLoad : public Intent
+{
+ Q_OBJECT
+
+protected:
+ bool checkPrecondition() const override
+ {
+ return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok;
+ }
+
+ void onPreconditionSatisfied() override
+ {
+ emit finished();
+ }
+
+ void onPreconditionNotSatisfied() override
+ {
+ connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
+ [this]() { emit finished(); });
+ }
+};
+
+#endif // COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H
diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp
new file mode 100644
index 000000000..0987ea842
--- /dev/null
+++ b/cockatrice/src/interface/intents/url_parser.cpp
@@ -0,0 +1,67 @@
+#include "url_parser.h"
+
+#include "../window_main.h"
+#include "contexts/context_join_room.h"
+#include "intent_join_server_game.h"
+#include "intent_join_server_room.h"
+#include "intent_login.h"
+
+#include
+#include
+#include
+
+IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow)
+{
+}
+
+void IntentUrlParser::handle(const QString &urlStr)
+{
+ QUrl url(urlStr);
+
+ if (url.scheme() != "cockatrice")
+ return;
+
+ const QString action = url.host();
+ QUrlQuery query(url);
+
+ if (action == "joingame") {
+ handleJoinGame(query);
+ } else if (action == "opendeck") {
+ // handleOpenDeck(query);
+ } else {
+ qWarning() << "Unknown intent:" << action;
+ }
+}
+
+void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
+{
+ auto ctx = new ContextJoinGame();
+
+ ctx->roomContext.serverContext.hostname = query.queryItemValue("hostname");
+ ctx->roomContext.serverContext.port = query.queryItemValue("port");
+
+ bool ok = false;
+ ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok);
+
+ if (!ok) {
+ qWarning() << "Invalid or missing roomId";
+ return;
+ }
+
+ ok = false;
+ ctx->gameId = query.queryItemValue("gameid").toInt(&ok);
+
+ if (!ok) {
+ qWarning() << "Invalid or missing gameId";
+ return;
+ }
+
+ auto getLoginCredentialsIntent =
+ new IntentGetLoginCredentials(mainWindow->getRemoteClient(), &ctx->roomContext.serverContext);
+
+ auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), ctx);
+
+ connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
+
+ getLoginCredentialsIntent->execute();
+}
diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h
new file mode 100644
index 000000000..7b17079ea
--- /dev/null
+++ b/cockatrice/src/interface/intents/url_parser.h
@@ -0,0 +1,22 @@
+#ifndef COCKATRICE_URL_PARSER_H
+#define COCKATRICE_URL_PARSER_H
+#include
+#include
+
+class MainWindow;
+class IntentUrlParser : public QObject
+{
+ Q_OBJECT
+
+public:
+ IntentUrlParser(QObject *parent, MainWindow *mainWindow);
+ void handle(const QString &urlStr);
+ void handleJoinGame(const QUrlQuery &query);
+
+ void parse(QString url);
+
+private:
+ MainWindow *mainWindow;
+};
+
+#endif // COCKATRICE_URL_PARSER_H
diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp
index 0ff2a5542..aaee45dfe 100644
--- a/cockatrice/src/interface/widgets/server/game_selector.cpp
+++ b/cockatrice/src/interface/widgets/server/game_selector.cpp
@@ -307,6 +307,7 @@ void GameSelector::customContextMenu(const QPoint &point)
connect(&getGameInfo, &QAction::triggered, this, [=, this]() {
const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt());
const QMap &gameTypes = gameListModel->getGameTypes().value(gameInfo.room_id());
+ qWarning() << "Game Id: " << gameInfo.game_id();
DlgCreateGame dlg(gameInfo, gameTypes, this);
dlg.exec();
@@ -376,6 +377,24 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge)
disableButtons();
}
+bool GameSelector::joinGameById(int gameId)
+{
+ auto *model = gameListView->model();
+
+ for (int row = 0; row < model->rowCount(); ++row) {
+ QModelIndex idx = model->index(row, 0);
+ const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt());
+ if (game.game_id() == gameId) {
+ gameListView->setCurrentIndex(idx);
+ joinGame();
+ return true;
+ }
+ }
+
+ qWarning() << "Game" << gameId << "not found";
+ return false;
+}
+
void GameSelector::disableButtons()
{
if (createButton)
diff --git a/cockatrice/src/interface/widgets/server/game_selector.h b/cockatrice/src/interface/widgets/server/game_selector.h
index fa91e5f96..da34d5322 100644
--- a/cockatrice/src/interface/widgets/server/game_selector.h
+++ b/cockatrice/src/interface/widgets/server/game_selector.h
@@ -202,6 +202,7 @@ public:
* @param info The ServerInfo_Game object containing information about the game to update.
*/
void processGameInfo(const ServerInfo_Game &info);
+ bool joinGameById(int gameId);
};
#endif
diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h
index 67d9afc86..d5a45533a 100644
--- a/cockatrice/src/interface/widgets/tabs/tab_room.h
+++ b/cockatrice/src/interface/widgets/tabs/tab_room.h
@@ -125,6 +125,10 @@ public:
{
return ownUser;
}
+ [[nodiscard]] GameSelector *getGameSelector() const
+ {
+ return gameSelector;
+ }
PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd);
void sendRoomCommand(PendingCommand *pend);
diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.h b/cockatrice/src/interface/widgets/tabs/tab_server.h
index f2dd8f0a2..218ddb230 100644
--- a/cockatrice/src/interface/widgets/tabs/tab_server.h
+++ b/cockatrice/src/interface/widgets/tabs/tab_server.h
@@ -51,7 +51,6 @@ signals:
void roomJoined(const ServerInfo_Room &info, bool setCurrent);
private slots:
void processServerMessageEvent(const Event_ServerMessage &event);
- void joinRoom(int id, bool setCurrent);
void joinRoomFinished(const Response &resp, const CommandContainer &commandContainer, const QVariant &extraData);
private:
@@ -62,6 +61,7 @@ private:
public:
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client);
+ void joinRoom(int id, bool setCurrent);
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h
index 0c4428f83..8f5436519 100644
--- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h
+++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h
@@ -149,6 +149,10 @@ public:
{
return userListManager;
}
+ [[nodiscard]] TabServer *getTabServer() const
+ {
+ return tabServer;
+ }
[[nodiscard]] const QMap &getRoomTabs() const
{
return roomTabs;
diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h
index ed6de5b0d..515527ef2 100644
--- a/cockatrice/src/interface/window_main.h
+++ b/cockatrice/src/interface/window_main.h
@@ -172,6 +172,11 @@ public:
}
~MainWindow() override;
+ RemoteClient *getRemoteClient() const
+ {
+ return client;
+ };
+
TabSupervisor *getTabSupervisor() const
{
return tabSupervisor;
diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp
index 7092a3fd7..9200b2f29 100644
--- a/cockatrice/src/main.cpp
+++ b/cockatrice/src/main.cpp
@@ -24,11 +24,15 @@
#include "client/settings/cache_settings.h"
#include "client/sound_engine.h"
#include "database/interface/settings_card_preference_provider.h"
+#include "interface/intents/intent_open_local_deck.h"
+#include "interface/intents/url_parser.h"
#include "interface/logger.h"
#include "interface/pixel_map_generator.h"
#include "interface/theme_manager.h"
#include "interface/widgets/dialogs/dlg_settings.h"
+#include "interface/widgets/tabs/tab_supervisor.h"
#include "interface/window_main.h"
+#include "single_instance_manager.h"
#include "version_string.h"
#include
@@ -172,6 +176,7 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter);
#endif
+ // Logging setup
#ifdef Q_OS_APPLE
// /cockatrice/cockatrice.app/Contents/MacOS/cockatrice
const QByteArray configPath = "../../../qtlogging.ini";
@@ -189,6 +194,7 @@ int main(int argc, char *argv[])
// Set the QT_LOGGING_CONF environment variable
qputenv("QT_LOGGING_CONF", configPath);
}
+
qSetMessagePattern(
"\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} "
"%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{"
@@ -198,6 +204,7 @@ int main(int argc, char *argv[])
QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit);
qInstallMessageHandler(CockatriceLogger);
+
#ifdef Q_OS_WIN
app.addLibraryPath(app.applicationDirPath() + "/plugins");
#endif
@@ -213,6 +220,7 @@ int main(int argc, char *argv[])
qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true);
#endif
+ // Translations
#ifdef Q_OS_MAC
translationPath = qApp->applicationDirPath() + "/../Resources/translations";
#elif defined(Q_OS_WIN)
@@ -221,6 +229,7 @@ int main(int argc, char *argv[])
translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations";
#endif
+ // Command-line parser
QCommandLineParser parser;
parser.setApplicationDescription("Cockatrice");
parser.addHelpOption();
@@ -269,6 +278,34 @@ 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: try to start server, but do not connect to any existing
+ if (!instance.tryRun(QStringList())) {
+ // Server already exists → just run independently
+ qInfo() << "Another instance exists, running independently";
+ } else {
+ qInfo() << "No existing instance found, starting server";
+ }
+ }
+
ui.show();
qCInfo(MainLog) << "ui.show() finished";
@@ -278,7 +315,31 @@ int main(int argc, char *argv[])
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
app.setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
- app.exec();
+
+ for (const QString &file : startupFiles) {
+ if (file.startsWith("cockatrice://")) {
+ auto urlParser = new IntentUrlParser(&ui, &ui);
+ urlParser->handle(file);
+ } else if (QFileInfo(file).exists()) {
+ auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file);
+ openDeckIntent->execute();
+ }
+ }
+
+ // Connect to future file/URL events from other instances
+ QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&ui](const QStringList &files) {
+ for (const QString &file : files) {
+ if (file.startsWith("cockatrice://")) {
+ auto urlParser = new IntentUrlParser(&ui, &ui);
+ urlParser->handle(file);
+ } else if (QFileInfo(file).exists()) {
+ auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file);
+ openDeckIntent->execute();
+ }
+ }
+ });
+
+ int ret = app.exec();
qCInfo(MainLog) << "Event loop finished, terminating...";
delete rng;
@@ -286,5 +347,5 @@ int main(int argc, char *argv[])
CountryPixmapGenerator::clear();
UserLevelPixmapGenerator::clear();
- return 0;
+ return ret;
}
diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp
new file mode 100644
index 000000000..371922d55
--- /dev/null
+++ b/cockatrice/src/single_instance_manager.cpp
@@ -0,0 +1,91 @@
+#include "single_instance_manager.h"
+
+SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent)
+{
+}
+
+bool SingleInstanceManager::tryRun(const QStringList &filesToSend)
+{
+ serverName = "CockatriceSingleInstance";
+
+ // Attempt to connect only if we have files to send
+ if (!filesToSend.isEmpty()) {
+ QLocalSocket socket;
+ socket.connectToServer(serverName);
+ if (socket.waitForConnected(200)) {
+ // Serialize payload with length prefix
+ QByteArray payload;
+ QDataStream out(&payload, QIODevice::WriteOnly);
+ out << filesToSend;
+
+ QByteArray message;
+ QDataStream msgStream(&message, QIODevice::WriteOnly);
+ msgStream << quint32(payload.size());
+ message.append(payload);
+
+ socket.write(message);
+ socket.flush();
+ socket.waitForBytesWritten(1000);
+
+ return false; // Sent successfully → exit
+ }
+ }
+
+ // Otherwise, start server
+ server = new QLocalServer(this);
+ connect(server, &QLocalServer::newConnection, this, &SingleInstanceManager::handleNewConnection);
+
+ if (!server->listen(serverName)) {
+ QLocalServer::removeServer(serverName);
+ server->listen(serverName);
+ }
+
+ return true; // This process is now primary server
+}
+
+void SingleInstanceManager::handleNewConnection()
+{
+ QLocalSocket *socket = server->nextPendingConnection();
+
+ // Per-connection state
+ auto buffer = new QByteArray();
+ auto expectedSize = new quint32(0);
+
+ connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer, expectedSize]() {
+ buffer->append(socket->readAll());
+
+ QDataStream stream(buffer, QIODevice::ReadOnly);
+
+ while (true) {
+ // Step 1: read size
+ if (*expectedSize == 0) {
+ if (buffer->size() < static_cast(sizeof(quint32)))
+ return;
+
+ stream >> *expectedSize;
+ }
+
+ // Step 2: wait for full payload
+ if (buffer->size() < static_cast(sizeof(quint32) + *expectedSize))
+ return;
+
+ // Step 3: extract payload
+ QByteArray payload = buffer->mid(sizeof(quint32), *expectedSize);
+
+ QDataStream payloadStream(&payload, QIODevice::ReadOnly);
+ QStringList files;
+ payloadStream >> files;
+
+ emit filesReceived(files);
+
+ // Reset buffer (single message use-case)
+ buffer->clear();
+ *expectedSize = 0;
+
+ socket->disconnectFromServer();
+ return;
+ }
+ });
+
+ connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater);
+}
\ No newline at end of file
diff --git a/cockatrice/src/single_instance_manager.h b/cockatrice/src/single_instance_manager.h
new file mode 100644
index 000000000..9f0e54d32
--- /dev/null
+++ b/cockatrice/src/single_instance_manager.h
@@ -0,0 +1,28 @@
+#ifndef COCKATRICE_SINGLE_INSTANCE_MANAGER_H
+#define COCKATRICE_SINGLE_INSTANCE_MANAGER_H
+
+#include
+#include
+#include
+#include
+
+class SingleInstanceManager : public QObject
+{
+ Q_OBJECT
+public:
+ explicit SingleInstanceManager(QObject *parent = nullptr);
+
+ bool tryRun(const QStringList &initialFiles);
+
+signals:
+ void filesReceived(const QStringList &files);
+
+private slots:
+ void handleNewConnection();
+
+private:
+ QString serverName;
+ QLocalServer *server = nullptr;
+};
+
+#endif // COCKATRICE_SINGLE_INSTANCE_MANAGER_H
\ No newline at end of file
diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h
index 15e3e8ef5..b5e80eee1 100644
--- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h
+++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h
@@ -131,6 +131,14 @@ public:
return socket->peerName();
}
}
+ quint16 peerPort() const
+ {
+ if (usingWebSocket) {
+ return websocket->peerPort();
+ } else {
+ return socket->peerPort();
+ }
+ }
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 0140182be..835b97aca 100644
--- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp
+++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp
@@ -289,3 +289,46 @@ bool ServersSettings::updateExistingServer(QString saveName,
}
return false;
}
+
+int ServersSettings::findServerIndex(const QString &host, const QString &port) const
+{
+ int size = getValue("totalServers", "server", "server_details").toInt();
+
+ for (int i = 0; i <= size; ++i) {
+ QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString();
+ QString storedPort = getValue(QString("port%1").arg(i), "server", "server_details").toString();
+
+ if (storedHost == host && storedPort == port) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+bool ServersSettings::hasUsername(const QString &host, const QString &port) const
+{
+ int index = findServerIndex(host, port);
+ if (index < 0)
+ return false;
+
+ QString user = getValue(QString("username%1").arg(index), "server", "server_details").toString();
+ return !user.isEmpty();
+}
+
+bool ServersSettings::hasCredentials(const QString &host, const QString &port) const
+{
+ int index = findServerIndex(host, port);
+ if (index < 0)
+ return false;
+
+ bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool();
+ QString password = getValue(QString("password%1").arg(index), "server", "server_details").toString();
+
+ return save && !password.isEmpty();
+}
+
+bool ServersSettings::hasLoginData(const QString &host, const QString &port) const
+{
+ return hasUsername(host, port) && hasCredentials(host, port);
+}
\ No newline at end of file
diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h
index 22603a356..e18857931 100644
--- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h
+++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h
@@ -61,6 +61,10 @@ public:
QString password,
bool savePassword,
QString site = QString());
+ int findServerIndex(const QString &host, const QString &port) const;
+ bool hasUsername(const QString &host, const QString &port) const;
+ bool hasCredentials(const QString &host, const QString &port) const;
+ bool hasLoginData(const QString &host, const QString &port) const;
bool updateExistingServerWithoutLoss(QString saveName,
QString serv = QString(),