This commit is contained in:
BruebachL 2026-04-25 10:01:48 -03:00 committed by GitHub
commit e796dfc683
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 877 additions and 3 deletions

View file

@ -34,5 +34,31 @@
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>cod</string>
</array>
<key>CFBundleTypeName</key>
<string>Cockatrice</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Cockatrice</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cockatrice</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -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

View file

@ -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 ./)

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-cockatrice">
<comment>Cockatrice Deck File</comment>
<glob pattern="*.cod"/>
</mime-type>
</mime-info>

View file

@ -6,3 +6,5 @@ Name=Cockatrice
Exec=cockatrice
Icon=cockatrice
Categories=Game;CardGame;
MimeType=application/x-cockatrice;
X-Scheme-Handler/cockatrice=true

View file

@ -0,0 +1,14 @@
#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
#include <QString>
struct ContextConnectToServer
{
QString hostname;
QString port;
QString username;
QString password;
};
#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H

View file

@ -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

View file

@ -0,0 +1,14 @@
#ifndef COCKATRICE_CONTEXT_JOIN_ROOM_H
#define COCKATRICE_CONTEXT_JOIN_ROOM_H
#include "context_connect_to_server.h"
#include <QString>
struct ContextJoinRoom
{
ContextConnectToServer serverContext;
int roomId;
};
#endif // COCKATRICE_CONTEXT_JOIN_ROOM_H

View file

@ -0,0 +1 @@
#include "intent.h"

View file

@ -0,0 +1,49 @@
#ifndef COCKATRICE_INTENT_H
#define COCKATRICE_INTENT_H
#include <QObject>
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

View file

@ -0,0 +1 @@
#include "intent_connect_to_server.h"

View file

@ -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 <QTimer>
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

View file

@ -0,0 +1 @@
#include "intent_disconnect_from_server.h"

View file

@ -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

View file

@ -0,0 +1 @@
#include "intent_join_server_game.h"

View file

@ -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

View file

@ -0,0 +1 @@
#include "intent_join_server_room.h"

View file

@ -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

View file

@ -0,0 +1 @@
#include "intent_login.h"

View file

@ -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

View file

@ -0,0 +1 @@
#include "intent_open_local_deck.h"

View file

@ -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<LoadedDeck> 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

View file

@ -0,0 +1 @@
#include "intent_wait_for_database_load.h"

View file

@ -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

View file

@ -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 <QDebug>
#include <QUrl>
#include <QUrlQuery>
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();
}

View file

@ -0,0 +1,22 @@
#ifndef COCKATRICE_URL_PARSER_H
#define COCKATRICE_URL_PARSER_H
#include <QObject>
#include <QUrlQuery>
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

View file

@ -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<int, QString> &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)

View file

@ -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

View file

@ -125,6 +125,10 @@ public:
{
return ownUser;
}
[[nodiscard]] GameSelector *getGameSelector() const
{
return gameSelector;
}
PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd);
void sendRoomCommand(PendingCommand *pend);

View file

@ -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
{

View file

@ -149,6 +149,10 @@ public:
{
return userListManager;
}
[[nodiscard]] TabServer *getTabServer() const
{
return tabServer;
}
[[nodiscard]] const QMap<int, TabRoom *> &getRoomTabs() const
{
return roomTabs;

View file

@ -172,6 +172,11 @@ public:
}
~MainWindow() override;
RemoteClient *getRemoteClient() const
{
return client;
};
TabSupervisor *getTabSupervisor() const
{
return tabSupervisor;

View file

@ -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 <QApplication>
@ -172,6 +176,7 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter);
#endif
// Logging setup
#ifdef Q_OS_APPLE
// <build>/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;
}

View file

@ -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<int>(sizeof(quint32)))
return;
stream >> *expectedSize;
}
// Step 2: wait for full payload
if (buffer->size() < static_cast<int>(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);
}

View file

@ -0,0 +1,28 @@
#ifndef COCKATRICE_SINGLE_INSTANCE_MANAGER_H
#define COCKATRICE_SINGLE_INSTANCE_MANAGER_H
#include <QDataStream>
#include <QDebug>
#include <QLocalServer>
#include <QLocalSocket>
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

View file

@ -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,

View file

@ -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);
}

View file

@ -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(),