mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-09 03:23:24 -08:00
Merge current upstream into random-reveal review continuation
Preserve SET_PLAYER_ORDER alongside REVEAL_RANDOM_CARDS in the shared command enum; retain current upstream source and the reviewed PlayerActions routing.
This commit is contained in:
commit
e32bd364bd
443 changed files with 15635 additions and 11767 deletions
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
add_subdirectory(libcockatrice/network/client)
|
||||
add_subdirectory(libcockatrice/network/server)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
add_subdirectory(abstract)
|
||||
add_subdirectory(local)
|
||||
add_subdirectory(remote)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS abstract_client.h latency_tracker.h)
|
||||
|
||||
set(SOURCES abstract_client.cpp latency_tracker.cpp)
|
||||
|
|
@ -15,7 +11,7 @@ target_include_directories(libcockatrice_network_client_abstract PUBLIC .)
|
|||
target_link_libraries(
|
||||
libcockatrice_network_client_abstract
|
||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
||||
libcockatrice_network_server_remote
|
||||
libcockatrice_network_server_remote libcockatrice_utility
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_network_client_abstract PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
||||
#include "libcockatrice/protocol/pb/session_commands.pb.h"
|
||||
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
||||
#include "libcockatrice/utility/passwordhasher.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QList>
|
||||
|
|
@ -80,6 +81,26 @@ AbstractClient::~AbstractClient()
|
|||
{
|
||||
}
|
||||
|
||||
Command_AccountPassword AbstractClient::makeAccountPasswordCommand(const QString &oldPassword,
|
||||
const QString &newPassword) const
|
||||
{
|
||||
Command_AccountPassword cmd;
|
||||
cmd.set_old_password(oldPassword.toStdString());
|
||||
if (getServerSupportsChallengeResponse()) {
|
||||
// A verifier lets the server upgrade the stored credential without ever seeing the
|
||||
// password, and it is what a challenge-response server expects for a new password.
|
||||
cmd.set_hashed_new_password(PasswordHasher::generatePasswordVerifier(newPassword).toStdString());
|
||||
} else if (getServerSupportsPasswordHash()) {
|
||||
// The server hashes the password itself with its own salt; a salted hash is the
|
||||
// strongest form it accepts, so the plaintext never goes on the wire.
|
||||
const QString passwordSalt = PasswordHasher::generateRandomSalt();
|
||||
cmd.set_hashed_new_password(PasswordHasher::computeHash(newPassword, passwordSalt).toStdString());
|
||||
} else {
|
||||
cmd.set_new_password(newPassword.toStdString());
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
void AbstractClient::processProtocolItem(const ServerMessage &item)
|
||||
{
|
||||
switch (item.message_type()) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class Event_UserJoined;
|
|||
class Event_UserLeft;
|
||||
class Event_ServerMessage;
|
||||
class Event_ListRooms;
|
||||
class Command_AccountPassword;
|
||||
class Event_GameJoined;
|
||||
class Event_UserMessage;
|
||||
class Event_NotifyUser;
|
||||
|
|
@ -67,6 +68,9 @@ enum ClientStatus
|
|||
StatusSubmitForgotPasswordReset,
|
||||
StatusSubmitForgotPasswordChallenge,
|
||||
StatusGettingPasswordSalt,
|
||||
//! The server accepted the credentials but keeps the session open until the account
|
||||
//! password is replaced; the connection must survive for `Command_AccountPassword`.
|
||||
StatusPasswordChangeRequired,
|
||||
};
|
||||
|
||||
class AbstractClient : public QObject
|
||||
|
|
@ -171,6 +175,16 @@ public:
|
|||
{
|
||||
return serverSupportsChallengeResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Builds the account-password command in the form this server understands.
|
||||
*
|
||||
* Challenge-response servers take a scrypt verifier; servers that only advertise
|
||||
* SupportsPasswordHash take a salted SHA-512 hash; anything older gets the plaintext,
|
||||
* which is the only form it understands. Both password-change paths go through here so
|
||||
* the two cannot drift apart and hand a plaintext password to a server that can hash it.
|
||||
*/
|
||||
Command_AccountPassword makeAccountPasswordCommand(const QString &oldPassword, const QString &newPassword) const;
|
||||
const QString &getUserName() const
|
||||
{
|
||||
return userName;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS local_client.h)
|
||||
|
||||
set(SOURCES local_client.cpp)
|
||||
|
|
|
|||
|
|
@ -33,16 +33,18 @@ LocalClient::LocalClient(LocalServerInterface *_lsi,
|
|||
sendCommand(prepareSessionCommand(joinCmd));
|
||||
}
|
||||
|
||||
LocalClient::~LocalClient()
|
||||
{
|
||||
}
|
||||
|
||||
void LocalClient::sendCommandContainer(const CommandContainer &cont)
|
||||
{
|
||||
if (LocalClientLog().isDebugEnabled()) {
|
||||
qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont);
|
||||
}
|
||||
|
||||
if (lsi.isNull()) {
|
||||
qCWarning(LocalClientLog).noquote()
|
||||
<< userName << "dropping command, local server interface no longer exists:" << getSafeDebugString(cont);
|
||||
return;
|
||||
}
|
||||
|
||||
lsi->itemFromClient(cont);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "../abstract/abstract_client.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
|
|
@ -24,14 +25,15 @@ class LocalClient : public AbstractClient
|
|||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
LocalServerInterface *lsi;
|
||||
// Guarded pointer: the LocalServer (and its interfaces) may be destroyed while this client
|
||||
// still has pending commands queued on the event loop.
|
||||
QPointer<LocalServerInterface> lsi;
|
||||
|
||||
public:
|
||||
LocalClient(LocalServerInterface *_lsi,
|
||||
const QString &_playerName,
|
||||
const QString &_clientId,
|
||||
QObject *parent = nullptr);
|
||||
~LocalClient() override;
|
||||
|
||||
void sendCommandContainer(const CommandContainer &cont) override;
|
||||
private slots:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS remote_client.h)
|
||||
|
||||
set(SOURCES remote_client.cpp)
|
||||
|
|
|
|||
|
|
@ -50,11 +50,12 @@ namespace
|
|||
// The server warns that this account still uses the legacy password hash while this client
|
||||
// holds a scrypt verifier. Sending the verifier would downgrade the proof, so the client
|
||||
// stops; the login only succeeds after the saved verifier is gone and the user types the
|
||||
// password, which also upgrades the account.
|
||||
// password, which also upgrades the account. Unticking "Save password" drops the verifier
|
||||
// for the next connection.
|
||||
constexpr const char *const LOGIN_ABORT_AUTH_DOWNGRADE = QT_TRANSLATE_NOOP(
|
||||
"RemoteClient",
|
||||
"The server still uses an old password format for this account, so the saved login on this computer cannot be "
|
||||
"used. Untick 'Save password' in the Connect window and connect; when it opens again, type your password.");
|
||||
"used. Untick 'Save password' in the Connect window and log in with your password.");
|
||||
// The account is still on the legacy hash and only the plaintext password can produce it.
|
||||
constexpr const char *const LOGIN_ABORT_PASSWORD_REQUIRED =
|
||||
QT_TRANSLATE_NOOP("RemoteClient",
|
||||
|
|
@ -125,6 +126,7 @@ RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSe
|
|||
&RemoteClient::doSubmitForgotPasswordResetToServer);
|
||||
connect(this, &RemoteClient::sigSubmitForgotPasswordChallengeToServer, this,
|
||||
&RemoteClient::doSubmitForgotPasswordChallengeToServer);
|
||||
connect(this, &RemoteClient::sigChangePasswordToServer, this, &RemoteClient::doChangePasswordToServer);
|
||||
}
|
||||
|
||||
RemoteClient::~RemoteClient()
|
||||
|
|
@ -136,6 +138,7 @@ RemoteClient::~RemoteClient()
|
|||
void RemoteClient::slotSocketError(QAbstractSocket::SocketError /*error*/)
|
||||
{
|
||||
QString errorString = socket->errorString();
|
||||
qCWarning(RemoteClientLog) << "Server connection error:" << errorString;
|
||||
doDisconnectFromServer();
|
||||
emit socketError(errorString);
|
||||
}
|
||||
|
|
@ -145,6 +148,7 @@ void RemoteClient::slotWebSocketError(QAbstractSocket::SocketError /*error*/)
|
|||
|
||||
QString errorString = websocket->errorString();
|
||||
if (getStatus() != ClientStatus::StatusDisconnected) {
|
||||
qCWarning(RemoteClientLog) << "Server connection error:" << errorString;
|
||||
doDisconnectFromServer();
|
||||
emit socketError(errorString);
|
||||
}
|
||||
|
|
@ -152,6 +156,7 @@ void RemoteClient::slotWebSocketError(QAbstractSocket::SocketError /*error*/)
|
|||
|
||||
void RemoteClient::slotConnected()
|
||||
{
|
||||
qCInfo(RemoteClientLog) << "Server transport connected";
|
||||
timeRunning = lastDataReceived = 0;
|
||||
timer->start();
|
||||
|
||||
|
|
@ -165,6 +170,12 @@ void RemoteClient::slotConnected()
|
|||
|
||||
void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentification &event)
|
||||
{
|
||||
// Identification arrives before login, including when protocol versions differ.
|
||||
// Quote the server-supplied strings in the log and limit their size.
|
||||
const QString serverName = QString::fromStdString(event.server_name()).left(256);
|
||||
const QString serverVersion = QString::fromStdString(event.server_version()).left(256);
|
||||
qCInfo(RemoteClientLog) << "Server identification:" << serverName << "version" << serverVersion
|
||||
<< "protocol version" << event.protocol_version();
|
||||
if (event.protocol_version() != protocolVersion) {
|
||||
emit protocolVersionMismatch(protocolVersion, event.protocol_version());
|
||||
setStatus(StatusDisconnecting);
|
||||
|
|
@ -354,6 +365,29 @@ void RemoteClient::submitPasswordVerifierResponse(const Response &response)
|
|||
}
|
||||
}
|
||||
|
||||
void RemoteClient::changePasswordToServer(const QString &oldPassword, const QString &newPassword)
|
||||
{
|
||||
emit sigChangePasswordToServer(oldPassword, newPassword);
|
||||
}
|
||||
|
||||
void RemoteClient::doChangePasswordToServer(const QString &oldPassword, const QString &newPassword)
|
||||
{
|
||||
const Command_AccountPassword cmd = makeAccountPasswordCommand(oldPassword, newPassword);
|
||||
|
||||
PendingCommand *pend = prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &RemoteClient::accountPasswordResponse);
|
||||
sendCommand(pend);
|
||||
}
|
||||
|
||||
void RemoteClient::accountPasswordResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
emit sigPasswordChangeSuccess();
|
||||
} else {
|
||||
emit sigPasswordChangeError(response.response_code());
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /*event*/)
|
||||
{
|
||||
doDisconnectFromServer();
|
||||
|
|
@ -475,6 +509,23 @@ void RemoteClient::loginResponse(const Response &response)
|
|||
emit notifyUserAboutUpdate();
|
||||
}
|
||||
|
||||
} else if (response.response_code() == Response::RespPasswordChangeRequired) {
|
||||
// The credentials were accepted: the server keeps this session open and serves
|
||||
// nothing but ping and Command_AccountPassword until the password is replaced.
|
||||
// Treating it as a failed login would disconnect before the dialog that collects
|
||||
// the new password (queued to the UI thread behind this slot) can send anything:
|
||||
// the command would sit in pendingCommands forever, because the ping timer that
|
||||
// times it out stops with the connection. Keep the session and let the controller
|
||||
// report the outcome through accountPasswordResponse().
|
||||
QList<QString> missingFeatures;
|
||||
if (resp.missing_features_size() > 0) {
|
||||
for (int i = 0; i < resp.missing_features_size(); ++i) {
|
||||
missingFeatures << QString::fromStdString(resp.missing_features(i));
|
||||
}
|
||||
}
|
||||
setStatus(StatusPasswordChangeRequired);
|
||||
emit loginError(response.response_code(), QString::fromStdString(resp.denied_reason_str()),
|
||||
static_cast<quint32>(resp.denied_end_time()), missingFeatures);
|
||||
} else if (response.response_code() != Response::RespNotConnected) {
|
||||
QList<QString> missingFeatures;
|
||||
if (resp.missing_features_size() > 0) {
|
||||
|
|
@ -657,6 +708,8 @@ void RemoteClient::sendCommandContainer(const CommandContainer &cont)
|
|||
void RemoteClient::connectToHost(const QString &hostname, unsigned int port)
|
||||
{
|
||||
usingWebSocket = port == 443 || port == 80 || port == 4748 || port == 8080;
|
||||
qCInfo(RemoteClientLog) << "Connecting to server" << hostname << "port" << port
|
||||
<< (usingWebSocket ? "using WebSocket" : "using TCP");
|
||||
if (usingWebSocket) {
|
||||
QUrl url(QString("%1://%2:%3/servatrice").arg(port == 443 ? "wss" : "ws").arg(hostname).arg(port));
|
||||
websocket->open(url);
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ signals:
|
|||
//! should only store it for the connection it is currently negotiating, so
|
||||
//! the hostname and user name are intentionally not part of the signal.
|
||||
void sigPasswordVerifierReady(const QString &verifier);
|
||||
//! \brief Outcome of the password change the server demanded at login.
|
||||
void sigPasswordChangeSuccess();
|
||||
void sigPasswordChangeError(int responseCode);
|
||||
//! \brief Request a password change; queued to the client thread like the other requests.
|
||||
void sigChangePasswordToServer(const QString &oldPassword, const QString &newPassword);
|
||||
private slots:
|
||||
void slotConnected();
|
||||
void readData();
|
||||
|
|
@ -99,6 +104,8 @@ private slots:
|
|||
Command_Login generateCommandLogin();
|
||||
void doSubmitPasswordVerifier();
|
||||
void submitPasswordVerifierResponse(const Response &response);
|
||||
void accountPasswordResponse(const Response &response);
|
||||
void doChangePasswordToServer(const QString &oldPassword, const QString &newPassword);
|
||||
void doDisconnectFromServer();
|
||||
void doActivateToServer(const QString &_token);
|
||||
void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);
|
||||
|
|
@ -201,6 +208,10 @@ public:
|
|||
unsigned int port,
|
||||
const QString &_userName,
|
||||
const QString &_email);
|
||||
//! \brief Change the password of the account that just logged in, following the
|
||||
//! server's forced-password-change reply. Emits sigPasswordChangeSuccess or
|
||||
//! sigPasswordChangeError when the server answers.
|
||||
void changePasswordToServer(const QString &oldPassword, const QString &newPassword);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
add_subdirectory(local)
|
||||
add_subdirectory(remote)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS local_server.h local_server_interface.h)
|
||||
|
||||
set(SOURCES local_server.cpp local_server_interface.cpp)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS
|
||||
game/dump_zone_range.h
|
||||
game/server_abstract_participant.h
|
||||
|
|
@ -12,6 +8,7 @@ set(HEADERS
|
|||
game/server_cardzone.h
|
||||
game/server_counter.h
|
||||
game/game_config.h
|
||||
game/role_info.h
|
||||
game/server_deck_validation_strategy.h
|
||||
game/server_game.h
|
||||
game/server_game_lifecycle_strategy.h
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ struct GameConfig
|
|||
bool spectatorsSeeEverything = true;
|
||||
int startingLifeTotal = 20;
|
||||
bool shareDecklistsOnLoad = true;
|
||||
bool shufflePlayers = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef COCKATRICE_ROLE_INFO_H
|
||||
#define COCKATRICE_ROLE_INFO_H
|
||||
|
||||
/**
|
||||
* @brief Represents information about a participant's roles.
|
||||
*/
|
||||
struct RoleInfo
|
||||
{
|
||||
bool isJudge = false; ///< Whether the participant joined as a judge
|
||||
bool isSpectator = false; ///< Whether the participant joined as a spectator
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_ROLE_INFO_H
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_player_order.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
|
|
@ -61,10 +62,10 @@
|
|||
Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game,
|
||||
int _playerId,
|
||||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
const RoleInfo &_roleInfo,
|
||||
Server_AbstractUserInterface *_userInterface)
|
||||
: ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), pingTime(0),
|
||||
playerId(_playerId), judge(_judge)
|
||||
playerId(_playerId), roleInfo(_roleInfo)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -98,8 +99,8 @@ void Server_AbstractParticipant::getProperties(ServerInfo_PlayerProperties &resu
|
|||
if (withUserInfo) {
|
||||
copyUserInfo(*(result.mutable_user_info()), true);
|
||||
}
|
||||
result.set_spectator(spectator);
|
||||
result.set_judge(judge);
|
||||
result.set_spectator(isSpectator());
|
||||
result.set_judge(isJudge());
|
||||
result.set_ping_seconds(pingTime);
|
||||
getPlayerProperties(result);
|
||||
}
|
||||
|
|
@ -176,7 +177,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdUnconcede(const Command_Un
|
|||
Response::ResponseCode
|
||||
Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
||||
{
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +205,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdReadyStart(const Command_R
|
|||
Response::ResponseCode
|
||||
Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
|
||||
{
|
||||
if (spectator) {
|
||||
if (isSpectator()) {
|
||||
/* Spectators can only talk if:
|
||||
* (a) the game creator allows it
|
||||
* (b) the spectator is a moderator/administrator
|
||||
|
|
@ -365,7 +366,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_Nex
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +382,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetActivePhase(const Comma
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -416,8 +417,8 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_
|
|||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage &ges)
|
||||
{
|
||||
if (!judge) {
|
||||
if (spectator) {
|
||||
if (!isJudge()) {
|
||||
if (isSpectator()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -435,6 +436,33 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_
|
|||
return Response::RespOk;
|
||||
}
|
||||
|
||||
Response::ResponseCode Server_AbstractParticipant::cmdSetPlayerOrder(const Command_SetPlayerOrder &cmd,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
{
|
||||
if ((game->getHostId() != playerId) && !(userInfo->user_level() & ServerInfo_User::IsModerator)) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
if (game->getGameStarted()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
if (cmd.randomize()) {
|
||||
game->shufflePlayerSeats();
|
||||
} else {
|
||||
QList<QString> playerNames;
|
||||
for (int i = 0; i < cmd.player_names_size(); ++i) {
|
||||
playerNames.append(QString::fromStdString(cmd.player_names(i)));
|
||||
}
|
||||
game->reorderPlayerSeats(playerNames);
|
||||
}
|
||||
|
||||
// Clients learn the new seating from the Event_PlayerOrderChanged that
|
||||
// reseatPlayers broadcasts; no full game state resend is needed for it.
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
||||
Response::ResponseCode
|
||||
Server_AbstractParticipant::processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges)
|
||||
{
|
||||
|
|
@ -558,6 +586,9 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo
|
|||
case GameCommand::SET_PLAYMAT:
|
||||
return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges);
|
||||
break;
|
||||
case GameCommand::SET_PLAYER_ORDER:
|
||||
return cmdSetPlayerOrder(command.GetExtension(Command_SetPlayerOrder::ext), rc, ges);
|
||||
break;
|
||||
default:
|
||||
return Response::RespInvalidCommand;
|
||||
}
|
||||
|
|
@ -592,7 +623,7 @@ void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface *
|
|||
void Server_AbstractParticipant::disconnectClient()
|
||||
{
|
||||
bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered;
|
||||
if (!isRegistered || spectator) {
|
||||
if (!isRegistered || isSpectator()) {
|
||||
game->removeParticipant(this, Event_Leave::USER_DISCONNECTED);
|
||||
} else {
|
||||
setUserInterface(nullptr);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define ABSTRACT_PARTICIPANT_H
|
||||
|
||||
#include "../serverinfo_user_container.h"
|
||||
#include "role_info.h"
|
||||
#include "server_arrowtarget.h"
|
||||
|
||||
#include <QMutex>
|
||||
|
|
@ -52,6 +53,7 @@ class Command_DeckSelect;
|
|||
class Command_SetSideboardLock;
|
||||
class Command_ChangeZoneProperties;
|
||||
class Command_SetPlaymat;
|
||||
class Command_SetPlayerOrder;
|
||||
|
||||
class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container
|
||||
{
|
||||
|
|
@ -61,8 +63,7 @@ protected:
|
|||
Server_AbstractUserInterface *userInterface;
|
||||
int pingTime;
|
||||
int playerId;
|
||||
bool spectator;
|
||||
bool judge;
|
||||
RoleInfo roleInfo;
|
||||
virtual void getPlayerProperties(ServerInfo_PlayerProperties &result);
|
||||
mutable QMutex playerMutex;
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ public:
|
|||
Server_AbstractParticipant(Server_Game *_game,
|
||||
int _playerId,
|
||||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
const RoleInfo &_roleInfo,
|
||||
Server_AbstractUserInterface *_handler);
|
||||
~Server_AbstractParticipant() override;
|
||||
virtual void prepareDestroy()
|
||||
|
|
@ -89,13 +90,17 @@ public:
|
|||
{
|
||||
return playerId;
|
||||
}
|
||||
void setPlayerId(int _playerId)
|
||||
{
|
||||
playerId = _playerId;
|
||||
}
|
||||
bool isSpectator() const
|
||||
{
|
||||
return spectator;
|
||||
return roleInfo.isSpectator;
|
||||
}
|
||||
bool isJudge() const
|
||||
{
|
||||
return judge;
|
||||
return roleInfo.isJudge;
|
||||
}
|
||||
Server_Game *getGame() const
|
||||
{
|
||||
|
|
@ -173,6 +178,8 @@ public:
|
|||
virtual Response::ResponseCode
|
||||
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdSetPlayerOrder(const Command_SetPlayerOrder &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
|
||||
Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges);
|
||||
|
|
|
|||
|
|
@ -81,10 +81,9 @@ Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game,
|
|||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
Server_AbstractUserInterface *_userInterface)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface), conceded(false), deck(nullptr),
|
||||
sideboardLocked(true), readyStart(false), nextCardId(0)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge}, _userInterface), conceded(false),
|
||||
deck(nullptr), sideboardLocked(true), readyStart(false), nextCardId(0)
|
||||
{
|
||||
spectator = false;
|
||||
}
|
||||
|
||||
Server_AbstractPlayer::~Server_AbstractPlayer() = default;
|
||||
|
|
@ -295,7 +294,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
|
|||
{
|
||||
// Disallow controller change to other zones than the table.
|
||||
if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) &&
|
||||
(startzone->getPlayer() != targetzone->getPlayer()) && !judge) {
|
||||
(startzone->getPlayer() != targetzone->getPlayer()) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -808,7 +807,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) {
|
||||
if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -821,7 +820,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
if ((startPlayer != this) && (targetPlayer != this) && !judge) {
|
||||
if ((startPlayer != this) && (targetPlayer != this) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -1607,7 +1606,7 @@ Response::ResponseCode Server_AbstractPlayer::cmdRevealCards(const Command_Revea
|
|||
zone->addWritePermission(cmd.player_id());
|
||||
}
|
||||
|
||||
if (judge) {
|
||||
if (isJudge()) {
|
||||
ges.setOverwriteOwnership(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
#include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
||||
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
||||
#include "role_info.h"
|
||||
#include "server_abstract_player.h"
|
||||
#include "server_abstractuserinterface.h"
|
||||
#include "server_arrow.h"
|
||||
|
|
@ -72,11 +73,13 @@
|
|||
#include <libcockatrice/protocol/pb/event_join.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_kicked.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_leave.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_player_order_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_player_properties_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_replay_added.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_phase.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_player.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <qlogging.h>
|
||||
#include <qnamespace.h>
|
||||
|
|
@ -90,8 +93,8 @@ Server_Game::Server_Game(const GameConfig &config, Server_Room *_room)
|
|||
spectatorsAllowed(config.spectatorsAllowed), spectatorsNeedPassword(config.spectatorsNeedPassword),
|
||||
spectatorsCanTalk(config.spectatorsCanTalk), spectatorsSeeEverything(config.spectatorsSeeEverything),
|
||||
startingLifeTotal(config.startingLifeTotal), shareDecklistsOnLoad(config.shareDecklistsOnLoad),
|
||||
inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false),
|
||||
turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr),
|
||||
shufflePlayers(config.shufflePlayers), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0),
|
||||
firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr),
|
||||
deckValidationStrategy(new Server_DefaultDeckValidationStrategy),
|
||||
lifecycleStrategy(new Server_DefaultLifecycleStrategy), matchResultStrategy(new Server_NullMatchResultStrategy),
|
||||
gameMutex()
|
||||
|
|
@ -291,6 +294,32 @@ int Server_Game::getSpectatorCount() const
|
|||
return result;
|
||||
}
|
||||
|
||||
QList<Server_AbstractParticipant *> Server_Game::getParticipantsInSeatOrder() const
|
||||
{
|
||||
// Players in seat order first (fall back to id order for seats predating
|
||||
// seatOrder), then spectators. Player ids are stable; only the ordering
|
||||
// changes on shuffle/reorder.
|
||||
QList<Server_AbstractParticipant *> ordered;
|
||||
ordered.reserve(participants.size());
|
||||
for (int id : seatOrder) {
|
||||
auto *participant = participants.value(id, nullptr);
|
||||
if (participant && !participant->isSpectator() && !ordered.contains(participant)) {
|
||||
ordered.append(participant);
|
||||
}
|
||||
}
|
||||
for (auto *participant : participants.values()) {
|
||||
if (!participant->isSpectator() && !ordered.contains(participant)) {
|
||||
ordered.append(participant);
|
||||
}
|
||||
}
|
||||
for (auto *participant : participants.values()) {
|
||||
if (participant->isSpectator()) {
|
||||
ordered.append(participant);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event,
|
||||
Server_AbstractParticipant *recipient,
|
||||
bool omniscient,
|
||||
|
|
@ -305,16 +334,24 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event,
|
|||
event->set_game_started(false);
|
||||
}
|
||||
|
||||
for (Server_AbstractParticipant *participant : participants.values()) {
|
||||
// Emit the player list in seat order so every client (including old ones
|
||||
// that ignore Event_PlayerOrderChanged) sees the current seating. Player
|
||||
// ids are stable; only this ordering changes on shuffle/reorder.
|
||||
QList<Server_AbstractParticipant *> ordered = getParticipantsInSeatOrder();
|
||||
for (Server_AbstractParticipant *participant : ordered) {
|
||||
participant->getInfo(event->add_player_list(), recipient, omniscient, withUserInfo);
|
||||
}
|
||||
|
||||
if (recipient != nullptr) {
|
||||
event->set_local_player_id(recipient->getPlayerId());
|
||||
}
|
||||
}
|
||||
|
||||
void Server_Game::sendGameStateToPlayers()
|
||||
{
|
||||
// game state information for replay and omniscient spectators
|
||||
Event_GameStateChanged omniscientEvent;
|
||||
createGameStateChangedEvent(&omniscientEvent, nullptr, true, false);
|
||||
createGameStateChangedEvent(&omniscientEvent, nullptr, true, true);
|
||||
|
||||
GameEventContainer *replayCont = prepareGameEvent(omniscientEvent, -1);
|
||||
replayCont->set_seconds_elapsed(secondsElapsed - startTimeOfThisGame);
|
||||
|
|
@ -326,7 +363,7 @@ void Server_Game::sendGameStateToPlayers()
|
|||
// the data we used for the replay. All spectators are equal, so we don't need to make a createGameStateChangedEvent
|
||||
// call for each one.
|
||||
Event_GameStateChanged spectatorNormalEvent;
|
||||
createGameStateChangedEvent(&spectatorNormalEvent, nullptr, false, false);
|
||||
createGameStateChangedEvent(&spectatorNormalEvent, nullptr, false, true);
|
||||
|
||||
// send game state info to clients according to their role in the game
|
||||
for (auto *participant : participants.values()) {
|
||||
|
|
@ -339,7 +376,7 @@ void Server_Game::sendGameStateToPlayers()
|
|||
}
|
||||
} else {
|
||||
Event_GameStateChanged event;
|
||||
createGameStateChangedEvent(&event, participant, participant->isJudge(), false);
|
||||
createGameStateChangedEvent(&event, participant, participant->isJudge(), true);
|
||||
|
||||
gec = prepareGameEvent(event, -1);
|
||||
}
|
||||
|
|
@ -379,6 +416,11 @@ void Server_Game::doStartGameIfReady(bool forceStartGame)
|
|||
return;
|
||||
}
|
||||
|
||||
if (shufflePlayers && players.size() > 1) {
|
||||
shufflePlayerSeats();
|
||||
players = getPlayers();
|
||||
}
|
||||
|
||||
for (Server_AbstractPlayer *player : players.values()) {
|
||||
player->setupZones();
|
||||
}
|
||||
|
|
@ -432,6 +474,116 @@ void Server_Game::startGameIfReady(bool forceStartGame)
|
|||
emit sigStartGameIfReady(forceStartGame);
|
||||
}
|
||||
|
||||
void Server_Game::shufflePlayerSeats()
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
// Collect players in current seat order (fall back to id order for seats
|
||||
// predating seatOrder). Spectators keep their seats.
|
||||
QList<Server_AbstractParticipant *> players;
|
||||
for (int id : seatOrder) {
|
||||
auto *participant = participants.value(id, nullptr);
|
||||
if (participant && !participant->isSpectator()) {
|
||||
players.append(participant);
|
||||
}
|
||||
}
|
||||
for (auto *participant : participants.values()) {
|
||||
if (!participant->isSpectator() && !players.contains(participant)) {
|
||||
players.append(participant);
|
||||
}
|
||||
}
|
||||
QList<Server_AbstractParticipant *> spectators;
|
||||
for (auto *participant : participants.values()) {
|
||||
if (participant->isSpectator()) {
|
||||
spectators.append(participant);
|
||||
}
|
||||
}
|
||||
|
||||
// Fisher-Yates shuffle of the players.
|
||||
QList<Server_AbstractParticipant *> shuffledPlayers;
|
||||
while (!players.isEmpty()) {
|
||||
const int index = rng->rand(0, players.size() - 1);
|
||||
shuffledPlayers.append(players.takeAt(index));
|
||||
}
|
||||
|
||||
reseatPlayers(shuffledPlayers + spectators, true);
|
||||
}
|
||||
|
||||
void Server_Game::reorderPlayerSeats(const QList<QString> &orderedNames)
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
// Collect players in current seat order (fall back to id order for seats
|
||||
// predating seatOrder). Spectators keep their seats.
|
||||
QList<Server_AbstractParticipant *> players;
|
||||
for (int id : seatOrder) {
|
||||
auto *participant = participants.value(id, nullptr);
|
||||
if (participant && !participant->isSpectator()) {
|
||||
players.append(participant);
|
||||
}
|
||||
}
|
||||
for (auto *participant : participants.values()) {
|
||||
if (!participant->isSpectator() && !players.contains(participant)) {
|
||||
players.append(participant);
|
||||
}
|
||||
}
|
||||
QList<Server_AbstractParticipant *> spectators;
|
||||
for (auto *participant : participants.values()) {
|
||||
if (participant->isSpectator()) {
|
||||
spectators.append(participant);
|
||||
}
|
||||
}
|
||||
|
||||
// Order the players according to the given names, appending any player that
|
||||
// was not listed (should not happen) in their current seat order.
|
||||
QList<Server_AbstractParticipant *> orderedPlayers;
|
||||
QSet<Server_AbstractParticipant *> placed;
|
||||
for (const QString &name : orderedNames) {
|
||||
for (auto *participant : players) {
|
||||
if (!placed.contains(participant) && (QString::fromStdString(participant->getUserInfo()->name()) == name)) {
|
||||
orderedPlayers.append(participant);
|
||||
placed.insert(participant);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto *participant : players) {
|
||||
if (!placed.contains(participant)) {
|
||||
orderedPlayers.append(participant);
|
||||
}
|
||||
}
|
||||
|
||||
reseatPlayers(orderedPlayers + spectators, false);
|
||||
}
|
||||
|
||||
void Server_Game::reseatPlayers(const QList<Server_AbstractParticipant *> &orderedParticipants, bool randomized)
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
// Keep player ids stable and only reorder the seating/turn order. Changing
|
||||
// ids would break old clients (stale localPlayerId from Event_GameJoined
|
||||
// duplicates their own seat as an opponent), invalidate the home-server
|
||||
// per-user game/seat mapping used for ISL command routing, and orphan the
|
||||
// host id. All of those maps stay valid because ids never change here.
|
||||
// The current host therefore keeps its id and needs no re-key tracking.
|
||||
seatOrder.clear();
|
||||
for (auto *participant : orderedParticipants) {
|
||||
if (!participant->isSpectator()) {
|
||||
seatOrder.append(participant->getPlayerId());
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast the new seating order so that every client can display it.
|
||||
Event_PlayerOrderChanged event;
|
||||
for (auto *participant : orderedParticipants) {
|
||||
if (!participant->isSpectator()) {
|
||||
event.add_player_names(QString::fromStdString(participant->getUserInfo()->name()).toStdString());
|
||||
}
|
||||
}
|
||||
event.set_randomized(randomized);
|
||||
sendGameEventContainer(prepareGameEvent(event, -1));
|
||||
}
|
||||
|
||||
void Server_Game::stopGameIfFinished()
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
|
@ -479,9 +631,8 @@ void Server_Game::stopGameIfFinished()
|
|||
|
||||
Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
||||
const QString &_password,
|
||||
bool spectator,
|
||||
bool overrideRestrictions,
|
||||
bool asJudge)
|
||||
const RoleInfo &roleInfo,
|
||||
bool overrideRestrictions)
|
||||
{
|
||||
Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface();
|
||||
for (auto *participant : participants.values()) {
|
||||
|
|
@ -490,11 +641,11 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
|||
}
|
||||
}
|
||||
|
||||
if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) {
|
||||
if (roleInfo.isJudge && !(user->user_level() & ServerInfo_User::IsJudge)) {
|
||||
return Response::RespUserLevelTooLow;
|
||||
}
|
||||
if (!(overrideRestrictions && (user->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)))) {
|
||||
if ((_password != password) && !(spectator && !spectatorsNeedPassword)) {
|
||||
if ((_password != password) && !(roleInfo.isSpectator && !spectatorsNeedPassword)) {
|
||||
return Response::RespWrongPassword;
|
||||
}
|
||||
if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) {
|
||||
|
|
@ -510,13 +661,13 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
|||
QString::fromStdString(user->name()))) {
|
||||
return Response::RespInIgnoreList;
|
||||
}
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
if (!spectatorsAllowed) {
|
||||
return Response::RespSpectatorsNotAllowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!spectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) {
|
||||
if (!roleInfo.isSpectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) {
|
||||
return Response::RespGameFull;
|
||||
}
|
||||
|
||||
|
|
@ -537,19 +688,18 @@ bool Server_Game::containsUser(const QString &userName) const
|
|||
|
||||
void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
||||
ResponseContainer &rc,
|
||||
bool spectator,
|
||||
bool judge,
|
||||
const RoleInfo &roleInfo,
|
||||
bool broadcastUpdate)
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
Server_AbstractParticipant *newParticipant;
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
||||
judge, userInterface);
|
||||
roleInfo.isJudge, userInterface);
|
||||
} else {
|
||||
newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), judge,
|
||||
userInterface);
|
||||
newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
||||
roleInfo.isJudge, userInterface);
|
||||
}
|
||||
|
||||
newParticipant->moveToThread(thread());
|
||||
|
|
@ -560,10 +710,11 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
|||
|
||||
const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name());
|
||||
participants.insert(newParticipant->getPlayerId(), newParticipant);
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
allSpectatorsEver.insert(playerName);
|
||||
} else {
|
||||
allPlayersEver.insert(playerName);
|
||||
seatOrder.append(newParticipant->getPlayerId());
|
||||
|
||||
// if the original creator of the game joins, give them host status back
|
||||
//! \todo Transferring host to spectators has side effects.
|
||||
|
|
@ -579,7 +730,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
|||
emit gameInfoChanged(gameInfo);
|
||||
}
|
||||
|
||||
if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !spectator) {
|
||||
if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !roleInfo.isSpectator) {
|
||||
room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newParticipant->getPlayerId());
|
||||
}
|
||||
|
||||
|
|
@ -593,6 +744,7 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve
|
|||
room->getServer()->removePersistentPlayer(QString::fromStdString(participant->getUserInfo()->name()), room->getId(),
|
||||
gameId, participant->getPlayerId());
|
||||
participants.remove(participant->getPlayerId());
|
||||
seatOrder.removeAll(participant->getPlayerId());
|
||||
|
||||
bool spectator = participant->isSpectator();
|
||||
GameEventStorage ges;
|
||||
|
|
@ -612,10 +764,20 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve
|
|||
participant->prepareDestroy();
|
||||
|
||||
if (playerHost) {
|
||||
// Pass host to the next player in seat order so the game always keeps
|
||||
// a host while players remain.
|
||||
int newHostId = -1;
|
||||
for (auto *otherPlayer : getPlayers().values()) {
|
||||
newHostId = otherPlayer->getPlayerId();
|
||||
break;
|
||||
for (int id : seatOrder) {
|
||||
if (participants.contains(id)) {
|
||||
newHostId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newHostId == -1) {
|
||||
for (auto *otherPlayer : getPlayers().values()) {
|
||||
newHostId = otherPlayer->getPlayerId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newHostId != -1) {
|
||||
hostId = newHostId;
|
||||
|
|
@ -772,7 +934,20 @@ void Server_Game::nextTurn()
|
|||
}
|
||||
|
||||
auto players = getPlayers();
|
||||
const QList<int> keys = players.keys();
|
||||
// Advance in seat order, not id order, so shuffles/reorders change the
|
||||
// turn sequence while ids stay stable.
|
||||
QList<int> keys;
|
||||
keys.reserve(players.size());
|
||||
for (int id : seatOrder) {
|
||||
if (players.contains(id)) {
|
||||
keys.append(id);
|
||||
}
|
||||
}
|
||||
for (int id : players.keys()) {
|
||||
if (!keys.contains(id)) {
|
||||
keys.append(id);
|
||||
}
|
||||
}
|
||||
int listPos = -1;
|
||||
if (activePlayer != -1) {
|
||||
listPos = keys.indexOf(activePlayer);
|
||||
|
|
@ -822,7 +997,7 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
|||
event2.set_active_phase(activePhase);
|
||||
|
||||
bool omniscient = (joiningParticipant->isSpectator() && spectatorsSeeEverything) || joiningParticipant->isJudge();
|
||||
for (auto *participant : participants.values()) {
|
||||
for (auto *participant : getParticipantsInSeatOrder()) {
|
||||
participant->getInfo(event2.add_player_list(), joiningParticipant, omniscient, true);
|
||||
}
|
||||
|
||||
|
|
@ -871,6 +1046,13 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
|||
}
|
||||
}
|
||||
|
||||
bool Server_Game::canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const
|
||||
{
|
||||
return participant->getPlayerId() == privatePlayerId // same player
|
||||
|| participant->isJudge() // judges see everything
|
||||
|| (participant->isSpectator() && spectatorsSeeEverything); // spectators see everything
|
||||
}
|
||||
|
||||
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||
GameEventStorageItem::EventRecipients recipients,
|
||||
int privatePlayerId)
|
||||
|
|
@ -879,8 +1061,7 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
|||
|
||||
cont->set_game_id(gameId);
|
||||
for (auto *participant : participants.values()) {
|
||||
const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || participant->isJudge() ||
|
||||
(participant->isSpectator() && spectatorsSeeEverything);
|
||||
const bool playerPrivate = canSeePrivateEventsFor(participant, privatePlayerId);
|
||||
if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) ||
|
||||
(recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) {
|
||||
participant->sendGameEvent(*cont);
|
||||
|
|
@ -945,6 +1126,7 @@ void Server_Game::getInfo(ServerInfo_Game &result) const
|
|||
result.set_spectators_can_chat(spectatorsCanTalk);
|
||||
result.set_spectators_omniscient(spectatorsSeeEverything);
|
||||
result.set_share_decklists_on_load(shareDecklistsOnLoad);
|
||||
result.set_shuffle_players(shufflePlayers);
|
||||
result.set_spectators_count(getSpectatorCount());
|
||||
result.set_start_time(startTime.toSecsSinceEpoch());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class Message;
|
|||
} // namespace protobuf
|
||||
} // namespace google
|
||||
struct GameConfig;
|
||||
struct RoleInfo;
|
||||
|
||||
class Server_Game : public QObject
|
||||
{
|
||||
|
|
@ -73,6 +74,11 @@ private:
|
|||
int hostId;
|
||||
ServerInfo_User *creatorInfo;
|
||||
QMap<int, Server_AbstractParticipant *> participants;
|
||||
// Seating/turn order as an ordered list of player ids. Player ids themselves
|
||||
// are stable for the lifetime of the participant; shuffling or reordering
|
||||
// only permutes this list so old clients, ISL routes and persistent
|
||||
// references (all keyed by player id) keep working.
|
||||
QList<int> seatOrder;
|
||||
QSet<QString> allPlayersEver, allSpectatorsEver;
|
||||
bool gameStarted;
|
||||
bool gameClosed;
|
||||
|
|
@ -89,6 +95,7 @@ private:
|
|||
bool spectatorsSeeEverything;
|
||||
int startingLifeTotal;
|
||||
bool shareDecklistsOnLoad;
|
||||
bool shufflePlayers;
|
||||
int inactivityCounter;
|
||||
int startTimeOfThisGame, secondsElapsed;
|
||||
bool firstGameStarted;
|
||||
|
|
@ -108,7 +115,10 @@ private:
|
|||
Server_AbstractParticipant *recipient,
|
||||
bool omniscient,
|
||||
bool withUserInfo);
|
||||
QList<Server_AbstractParticipant *> getParticipantsInSeatOrder() const;
|
||||
void reseatPlayers(const QList<Server_AbstractParticipant *> &orderedParticipants, bool randomized);
|
||||
void storeGameInformation();
|
||||
bool canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const;
|
||||
signals:
|
||||
void sigStartGameIfReady(bool override);
|
||||
void gameInfoChanged(ServerInfo_Game gameInfo);
|
||||
|
|
@ -147,6 +157,10 @@ public:
|
|||
{
|
||||
return participants;
|
||||
}
|
||||
QList<int> getSeatOrder() const
|
||||
{
|
||||
return seatOrder;
|
||||
}
|
||||
int getGameId() const
|
||||
{
|
||||
return gameId;
|
||||
|
|
@ -187,13 +201,16 @@ public:
|
|||
{
|
||||
return shareDecklistsOnLoad;
|
||||
}
|
||||
bool getShufflePlayers() const
|
||||
{
|
||||
return shufflePlayers;
|
||||
}
|
||||
Response::ResponseCode
|
||||
checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge);
|
||||
checkJoin(ServerInfo_User *user, const QString &_password, const RoleInfo &roleInfo, bool overrideRestrictions);
|
||||
bool containsUser(const QString &userName) const;
|
||||
void addPlayer(Server_AbstractUserInterface *userInterface,
|
||||
ResponseContainer &rc,
|
||||
bool spectator,
|
||||
bool judge,
|
||||
const RoleInfo &roleInfo,
|
||||
bool broadcastUpdate = true);
|
||||
void removeParticipant(Server_AbstractParticipant *participant, Event_Leave::LeaveReason reason);
|
||||
void removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_AbstractPlayer *player);
|
||||
|
|
@ -201,6 +218,8 @@ public:
|
|||
bool kickParticipant(int playerId);
|
||||
void startGameIfReady(bool forceStartGame);
|
||||
void stopGameIfFinished();
|
||||
void shufflePlayerSeats();
|
||||
void reorderPlayerSeats(const QList<QString> &orderedNames);
|
||||
int getActivePlayer() const
|
||||
{
|
||||
return activePlayer;
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer &
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (conceded && !judge) {
|
||||
if (conceded && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -589,7 +589,7 @@ Response::ResponseCode Server_Player::cmdSetActivePhase(const Command_SetActiveP
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
if (conceded) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
|
@ -623,7 +623,7 @@ Response::ResponseCode
|
|||
Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
||||
{
|
||||
|
||||
if (!judge && conceded) {
|
||||
if (!isJudge() && conceded) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ Server_Spectator::Server_Spectator(Server_Game *_game,
|
|||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
Server_AbstractUserInterface *_userInterface)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge, .isSpectator = true}, _userInterface)
|
||||
{
|
||||
spectator = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,16 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session,
|
|||
return authState;
|
||||
}
|
||||
|
||||
if (authState == PasswordChangeRequired) {
|
||||
// The credentials were right, but the account must choose a new password before it may
|
||||
// use the server. Give the session an identity so it can complete that change, without
|
||||
// registering a user session or announcing the user as logged in.
|
||||
ServerInfo_User pendingData = databaseInterface->getUserData(name, true);
|
||||
pendingData.set_address(session->getAddress().toStdString());
|
||||
session->setUserInfo(pendingData);
|
||||
return authState;
|
||||
}
|
||||
|
||||
ServerInfo_User data = databaseInterface->getUserData(name, true);
|
||||
data.set_address(session->getAddress().toStdString());
|
||||
name = QString::fromStdString(data.name()); // Compensate for case indifference
|
||||
|
|
@ -296,7 +306,9 @@ void Server::removeClient(Server_ProtocolHandler *client)
|
|||
QWriteLocker locker(&clientsLock);
|
||||
clients.removeAt(clientIndex);
|
||||
ServerInfo_User *data = client->getUserInfo();
|
||||
if (data) {
|
||||
// A session that never completed login (for example one that still owes a password change)
|
||||
// has no session id: it was never announced as joined, so it must not be announced as leaving.
|
||||
if (data && data->has_session_id()) {
|
||||
Event_UserLeft event;
|
||||
event.set_name(data->name());
|
||||
SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event);
|
||||
|
|
@ -311,12 +323,10 @@ void Server::removeClient(Server_ProtocolHandler *client)
|
|||
users.remove(QString::fromStdString(data->name()));
|
||||
qDebug() << "Server::removeClient: name=" << QString::fromStdString(data->name());
|
||||
|
||||
if (data->has_session_id()) {
|
||||
const qint64 sessionId = data->session_id();
|
||||
usersBySessionId.remove(sessionId);
|
||||
emit endSession(sessionId);
|
||||
qDebug() << "closed session id:" << sessionId;
|
||||
}
|
||||
const qint64 sessionId = data->session_id();
|
||||
usersBySessionId.remove(sessionId);
|
||||
emit endSession(sessionId);
|
||||
qDebug() << "closed session id:" << sessionId;
|
||||
}
|
||||
qDebug() << "Server::removeClient: removed" << (void *)client << ";" << clients.size() << "clients; "
|
||||
<< users.size() << "users left";
|
||||
|
|
@ -330,7 +340,9 @@ QList<QString> Server::getOnlineModeratorList() const
|
|||
ServerInfo_User *data = client->getUserInfo();
|
||||
|
||||
//! \todo This line should be updated in the event there is any type of new user level created.
|
||||
if (data &&
|
||||
// A session that still owes a forced password change has user info but no session id:
|
||||
// it never joined, so it must not be reported as an online moderator either.
|
||||
if (data && data->has_session_id() &&
|
||||
(data->user_level() & ServerInfo_User::IsModerator || data->user_level() & ServerInfo_User::IsAdmin)) {
|
||||
results << QString::fromStdString(data->name()).simplified();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,9 @@ Server_ProtocolHandler::Server_ProtocolHandler(Server *_server,
|
|||
Server_DatabaseInterface *_databaseInterface,
|
||||
QObject *parent)
|
||||
: QObject(parent), Server_AbstractUserInterface(_server), deleted(false), databaseInterface(_databaseInterface),
|
||||
authState(NotLoggedIn), usingRealPassword(false), acceptsUserListChanges(false), acceptsRoomListChanges(false),
|
||||
idleClientWarningSent(false), timeRunning(0), lastDataReceived(0), lastActionReceived(0)
|
||||
authState(NotLoggedIn), usingRealPassword(false), passwordChangePending(false), acceptsUserListChanges(false),
|
||||
acceptsRoomListChanges(false), idleClientWarningSent(false), timeRunning(0), lastDataReceived(0),
|
||||
lastActionReceived(0)
|
||||
|
||||
{
|
||||
connect(server, &Server::pingClockTimeout, this, &Server_ProtocolHandler::pingClockTimeout);
|
||||
|
|
@ -212,7 +213,10 @@ Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(co
|
|||
logDebugMessage(getSafeDebugString(sc));
|
||||
}
|
||||
const auto commandType = static_cast<SessionCommand::SessionCommandType>(num);
|
||||
if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) {
|
||||
if (passwordChangePending && commandType != SessionCommand::PING &&
|
||||
commandType != SessionCommand::ACCOUNT_PASSWORD) {
|
||||
resp = Response::RespFunctionNotAllowed;
|
||||
} else if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) {
|
||||
resp = Response::RespLoginNeeded;
|
||||
} else {
|
||||
switch (commandType) {
|
||||
|
|
@ -489,6 +493,19 @@ void Server_ProtocolHandler::processCommandContainer(const CommandContainer &con
|
|||
|
||||
lastDataReceived = timeRunning;
|
||||
|
||||
// A session that still owes a forced password change may only reach the session family,
|
||||
// where processSessionCommandContainer narrows it further to ping and the change itself.
|
||||
// Every other family is rejected here, including a container that also carries a session
|
||||
// command: the dispatch below prefers game/room over session, so testing only for the
|
||||
// absence of a session command would let such a container through.
|
||||
if (passwordChangePending &&
|
||||
(cont.game_command_size() || cont.room_command_size() || cont.moderator_command_size() ||
|
||||
cont.admin_command_size() || cont.developer_command_size())) {
|
||||
sendResponseContainer(ResponseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1),
|
||||
Response::RespFunctionNotAllowed);
|
||||
return;
|
||||
}
|
||||
|
||||
ResponseContainer responseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1);
|
||||
Response::ResponseCode finalResponseCode;
|
||||
|
||||
|
|
@ -696,6 +713,11 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
|
|||
server->recordFailedLogin(getAddress(), userName);
|
||||
return Response::RespAccountNotActivated;
|
||||
case PasswordChangeRequired:
|
||||
// The credentials were right, but the account must choose a new password first.
|
||||
// Keep the session authenticated just far enough for the client to do that:
|
||||
// processCommandContainer refuses everything except the change itself.
|
||||
authState = PasswordRight;
|
||||
passwordChangePending = true;
|
||||
return Response::RespPasswordChangeRequired;
|
||||
default:
|
||||
server->clearFailedLogins(getAddress(), userName);
|
||||
|
|
@ -1094,6 +1116,7 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
|
|||
int startingLifeTotal = cmd.has_starting_life_total() ? cmd.starting_life_total() : 20;
|
||||
|
||||
bool shareDecklistsOnLoad = cmd.has_share_decklists_on_load() ? cmd.share_decklists_on_load() : false;
|
||||
bool shufflePlayers = cmd.has_shuffle_players() ? cmd.shuffle_players() : false;
|
||||
|
||||
const int gameId = databaseInterface->getNextGameId();
|
||||
if (gameId == -1) {
|
||||
|
|
@ -1115,11 +1138,13 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
|
|||
.spectatorsCanTalk = cmd.spectators_can_talk(),
|
||||
.spectatorsSeeEverything = cmd.spectators_see_everything(),
|
||||
.startingLifeTotal = startingLifeTotal,
|
||||
.shareDecklistsOnLoad = shareDecklistsOnLoad};
|
||||
.shareDecklistsOnLoad = shareDecklistsOnLoad,
|
||||
.shufflePlayers = shufflePlayers};
|
||||
|
||||
auto *game = new Server_Game(config, room);
|
||||
|
||||
game->addPlayer(this, rc, asSpectator, asJudge, false);
|
||||
RoleInfo roleInfo{.isJudge = asJudge, .isSpectator = asSpectator};
|
||||
game->addPlayer(this, rc, roleInfo, false);
|
||||
room->addGame(game);
|
||||
|
||||
return Response::RespOk;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ protected:
|
|||
Server_DatabaseInterface *databaseInterface;
|
||||
AuthenticationResult authState;
|
||||
bool usingRealPassword;
|
||||
/**
|
||||
* @brief True when the credentials were right but the account must choose a new password
|
||||
* before it may use the server. Only ping, the password change and disconnecting
|
||||
* are served while this is set.
|
||||
*/
|
||||
bool passwordChangePending;
|
||||
bool acceptsUserListChanges;
|
||||
bool acceptsRoomListChanges;
|
||||
bool idleClientWarningSent;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "server_room.h"
|
||||
|
||||
#include "game/role_info.h"
|
||||
#include "game/server_game.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "libcockatrice/protocol/pb/room_event.pb.h"
|
||||
|
|
@ -372,11 +373,11 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam
|
|||
|
||||
QMutexLocker gameLocker(&game->gameMutex);
|
||||
|
||||
Response::ResponseCode result =
|
||||
game->checkJoin(userInterface->getUserInfo(), QString::fromStdString(cmd.password()), cmd.spectator(),
|
||||
cmd.override_restrictions(), cmd.join_as_judge());
|
||||
RoleInfo roleInfo{.isJudge = cmd.join_as_judge(), .isSpectator = cmd.spectator()};
|
||||
Response::ResponseCode result = game->checkJoin(
|
||||
userInterface->getUserInfo(), QString::fromStdString(cmd.password()), roleInfo, cmd.override_restrictions());
|
||||
if (result == Response::RespOk) {
|
||||
game->addPlayer(userInterface, rc, cmd.spectator(), cmd.join_as_judge());
|
||||
game->addPlayer(userInterface, rc, roleInfo);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue