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:
Bryce! 2026-11-04 15:54:51 -05:00
commit e32bd364bd
443 changed files with 15635 additions and 11767 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,3 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
add_subdirectory(local)
add_subdirectory(remote)

View file

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

View file

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

View file

@ -21,6 +21,7 @@ struct GameConfig
bool spectatorsSeeEverything = true;
int startingLifeTotal = 20;
bool shareDecklistsOnLoad = true;
bool shufflePlayers = false;
};
#endif

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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