Cockatrice/libcockatrice_game/libcockatrice/game/game_event_handler.cpp
BruebachL 5ea16c805f
[Server/Client/Protocol] Shuffle or reorder player seats before a game starts (#7398)
* [Protocol] Add shuffle players option and player order command/event

Adds a 'Shuffle players at start' option to game creation plus a local
player id on the game state so clients can reconcile seat changes, and a
SET_PLAYER_ORDER command (1206) with a PLAYER_ORDER_CHANGED event (2023)
so the game host can set or randomize the seating order.

Took 6 minutes

* [Server] Shuffle and reorder player seats before a game starts

Auto-shuffles the seats at game start when enabled (now for games with
two or more players instead of three or more) and handles the new
SET_PLAYER_ORDER command so the game host or a moderator can reorder or
randomize the seats before the game starts. Re-seating re-keys the
player ids while keeping the per-user game/seat routing and persistent
player records in sync, then broadcasts the new order to everyone.

* [Client] Add shuffle players option and turn order dialog

Adds the 'Shuffle players at start' checkbox to game creation and a
host-only 'Set turn order...' action that opens a dialog to reorder or
randomize the seats before the game starts. Seating changes are shown in
the game's message log.

* [Client] Move the turn order dialog strings into retranslateUi

Follows the DlgInviteToGame convention so the dialog can be retranslated
instead of freezing its strings at construction time.

* [Server] Keep player ids stable across seat shuffles

* [Client] Lay out table and turn order dialog in seat order

* [Server] Send game-joined player list in seat order

* [Client] Reword the shuffle option to match its behaviour

* [Client] Address the seat-order review comments

- Give the game setup checkboxes a row of their own so they no longer
  collide in the create-game dialog
- Rename DlgTurnOrder::randomize() to isRandomizeRequested()
- Document what the ids in GameState::seatOrder mean
- Keep "Set turn order..." in sync with the game: hidden for non-hosts
  and once the game has started, re-shown when the game ends or hosting
  moves to the local player (new GameState::hostChanged signal)

* [Client] Do not preview a randomized turn order

Randomizing is a request to the server, not a local shuffle. Shuffling the
list locally meant the dialog showed an order the server then discarded, and
let the host re-open it until a favourable order appeared. The dialog now
switches to a mode that says the server will randomize and toggles back to the
manual list on request.

* [Client] Let moderators set the turn order

The server accepts SET_PLAYER_ORDER from the host or a moderator, but the
client hid the action from everyone except the host, making the moderator
branch unreachable. Visibility and the handler now share canSetTurnOrder().

* [Server] Rely on the player order event instead of a full state resend

cmdSetPlayerOrder re-sent the whole game state after reseating, so every
client reloaded its deck view mid-lobby for no reason. The new seating reaches
clients through the Event_PlayerOrderChanged that reseatPlayers broadcasts.

* [Client] Show random turn order in the game selector

The restrictions column listed "open decklists" but not the new seating
option, so a joining player had no way to see that seats will be randomized
each game. Game info already shows the disabled checkbox.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 15:44:50 +01:00

746 lines
28 KiB
C++

#include "game_event_handler.h"
#include "abstract_game.h"
#include "game_meta_info.h"
#include "game_state.h"
#include "libcockatrice/protocol/pb/commands.pb.h"
#include "libcockatrice/protocol/pb/game_commands.pb.h"
#include "libcockatrice/protocol/pb/game_event.pb.h"
#include "libcockatrice/protocol/pb/game_event_context.pb.h"
#include "libcockatrice/protocol/pb/response.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_game.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_player.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_playerproperties.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
#include "player/player_event_handler.h"
#include "player/player_info.h"
#include "player/player_logic.h"
#include "player/player_manager.h"
#include <QList>
#include <QMap>
#include <QMapIterator>
#include <QMessageLogger>
#include <QSet>
#include <QSharedPointer>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
#include <google/protobuf/stubs/port.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/get_pb_extension.h>
#include <libcockatrice/protocol/pb/command_concede.pb.h>
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/command_game_say.pb.h>
#include <libcockatrice/protocol/pb/command_leave_game.pb.h>
#include <libcockatrice/protocol/pb/command_next_turn.pb.h>
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
#include <libcockatrice/protocol/pb/command_reverse_turn.pb.h>
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.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>
#include <libcockatrice/protocol/pb/context_connection_state_changed.pb.h>
#include <libcockatrice/protocol/pb/context_deck_select.pb.h>
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
#include <libcockatrice/protocol/pb/event_game_host_changed.pb.h>
#include <libcockatrice/protocol/pb/event_game_say.pb.h>
#include <libcockatrice/protocol/pb/event_game_state_changed.pb.h>
#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_reverse_turn.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_event_container.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/zone_names.h>
#include <string>
#include <utility>
GameEventHandler::GameEventHandler(AbstractGame *_game) : QObject(_game), game(_game)
{
}
void GameEventHandler::sendGameCommand(PendingCommand *pend, int playerId)
{
AbstractClient *client = game->getClientForPlayer(playerId);
if (!client) {
return;
}
connect(pend, &PendingCommand::finished, this, &GameEventHandler::commandFinished);
client->sendCommand(pend);
}
void GameEventHandler::sendGameCommand(const google::protobuf::Message &command, int playerId)
{
AbstractClient *client = game->getClientForPlayer(playerId);
if (!client) {
return;
}
PendingCommand *pend = prepareGameCommand(command);
connect(pend, &PendingCommand::finished, this, &GameEventHandler::commandFinished);
client->sendCommand(pend);
}
void GameEventHandler::commandFinished(const Response &response)
{
if (response.response_code() == Response::RespChatFlood) {
emit gameFlooded();
}
}
PendingCommand *GameEventHandler::prepareGameCommand(const ::google::protobuf::Message &cmd)
{
CommandContainer cont;
cont.set_game_id(static_cast<google::protobuf::uint32>(game->getGameMetaInfo()->gameId()));
GameCommand *c = cont.add_game_command();
c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd);
return new PendingCommand(cont);
}
PendingCommand *GameEventHandler::prepareGameCommand(const QList<const ::google::protobuf::Message *> &cmdList)
{
CommandContainer cont;
cont.set_game_id(static_cast<google::protobuf::uint32>(game->getGameMetaInfo()->gameId()));
for (auto i : cmdList) {
GameCommand *c = cont.add_game_command();
c->GetReflection()->MutableMessage(c, i->GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(*i);
delete i;
}
return new PendingCommand(cont);
}
void GameEventHandler::processGameEventContainer(const GameEventContainer &cont,
AbstractClient *client,
EventProcessingOptions options)
{
const GameEventContext &context = cont.context();
emit containerProcessingStarted(context);
const int eventListSize = cont.event_list_size();
for (int i = 0; i < eventListSize; ++i) {
const GameEvent &event = cont.event_list(i);
const int playerId = event.player_id();
const auto eventType = static_cast<GameEvent::GameEventType>(getPbExtension(event));
if (cont.has_forced_by_judge()) {
auto id = cont.forced_by_judge();
PlayerLogic *judgep = game->getPlayerManager()->getPlayers().value(id, nullptr);
if (judgep) {
emit setContextJudgeName(judgep->getPlayerInfo()->getName());
} else if (game->getPlayerManager()->getSpectators().contains(id)) {
emit setContextJudgeName(
QString::fromStdString(game->getPlayerManager()->getSpectators().value(id).name()));
}
}
if (game->getPlayerManager()->getSpectators().contains(playerId)) {
switch (eventType) {
case GameEvent::GAME_SAY:
eventSpectatorSay(event.GetExtension(Event_GameSay::ext), playerId, context);
break;
case GameEvent::LEAVE:
eventSpectatorLeave(event.GetExtension(Event_Leave::ext), playerId, context);
break;
default:
break;
}
} else {
if ((game->getGameState()->getClients().size() > 1) && (playerId != -1)) {
if (game->getGameState()->getClients().at(playerId) != client) {
continue;
}
}
switch (eventType) {
case GameEvent::GAME_STATE_CHANGED:
eventGameStateChanged(event.GetExtension(Event_GameStateChanged::ext), playerId, context);
break;
case GameEvent::PLAYER_PROPERTIES_CHANGED:
eventPlayerPropertiesChanged(event.GetExtension(Event_PlayerPropertiesChanged::ext), playerId,
context);
break;
case GameEvent::JOIN:
eventJoin(event.GetExtension(Event_Join::ext), playerId, context);
break;
case GameEvent::LEAVE:
eventLeave(event.GetExtension(Event_Leave::ext), playerId, context);
break;
case GameEvent::KICKED:
eventKicked(event.GetExtension(Event_Kicked::ext), playerId, context);
break;
case GameEvent::GAME_HOST_CHANGED:
eventGameHostChanged(event.GetExtension(Event_GameHostChanged::ext), playerId, context);
break;
case GameEvent::GAME_CLOSED:
eventGameClosed(event.GetExtension(Event_GameClosed::ext), playerId, context);
break;
case GameEvent::SET_ACTIVE_PLAYER:
eventSetActivePlayer(event.GetExtension(Event_SetActivePlayer::ext), playerId, context);
break;
case GameEvent::SET_ACTIVE_PHASE:
eventSetActivePhase(event.GetExtension(Event_SetActivePhase::ext), playerId, context);
break;
case GameEvent::REVERSE_TURN:
eventReverseTurn(event.GetExtension(Event_ReverseTurn::ext), playerId, context);
break;
case GameEvent::PLAYER_ORDER_CHANGED:
eventPlayerOrderChanged(event.GetExtension(Event_PlayerOrderChanged::ext), playerId, context);
break;
default: {
PlayerLogic *player = game->getPlayerManager()->getPlayers().value(playerId, 0);
if (!player) {
qCWarning(GameEventHandlerLog) << "unhandled game event: invalid player id";
break;
}
player->getPlayerEventHandler()->processGameEvent(eventType, event, context, options);
emitUserEvent();
}
}
}
}
emit containerProcessingDone();
}
void GameEventHandler::handleNextTurn()
{
sendGameCommand(Command_NextTurn());
}
void GameEventHandler::handleReverseTurn()
{
sendGameCommand(Command_ReverseTurn());
}
void GameEventHandler::handleSetPlayerOrder(const QList<QString> &playerNames, bool randomize)
{
Command_SetPlayerOrder command;
command.set_randomize(randomize);
for (const QString &name : playerNames) {
command.add_player_names(name.toStdString());
}
sendGameCommand(command);
}
void GameEventHandler::handleActiveLocalPlayerConceded()
{
sendGameCommand(Command_Concede());
}
void GameEventHandler::handleActiveLocalPlayerUnconceded()
{
sendGameCommand(Command_Unconcede());
}
void GameEventHandler::handleActivePhaseChanged(int phase)
{
Command_SetActivePhase cmd;
cmd.set_phase(static_cast<google::protobuf::uint32>(phase));
sendGameCommand(cmd);
}
void GameEventHandler::handleUntapAll()
{
Command_SetCardAttr cmd;
cmd.set_zone(ZoneNames::TABLE);
cmd.set_attribute(AttrTapped);
cmd.set_attr_value("0");
sendGameCommand(cmd);
}
void GameEventHandler::handleDrawCard()
{
Command_DrawCards cmd;
cmd.set_number(1);
sendGameCommand(cmd);
}
PendingCommand *GameEventHandler::prepareDeckSelect(const QString &deckList)
{
Command_DeckSelect cmd;
cmd.set_deck(deckList.toStdString());
return prepareGameCommand(cmd);
}
PendingCommand *GameEventHandler::prepareDeckSelectId(int deckId)
{
Command_DeckSelect cmd;
cmd.set_deck_id(deckId);
return prepareGameCommand(cmd);
}
PendingCommand *GameEventHandler::prepareSetPlaymat(const PlaymatInfo &playmat)
{
Command_SetPlaymat cmd;
auto *pp = cmd.mutable_playmat_params();
pp->set_card_name(playmat.card.name.toStdString());
pp->set_card_provider_id(playmat.card.providerId.toStdString());
pp->set_margin_pct_l(playmat.params.marginPctL);
pp->set_margin_pct_r(playmat.params.marginPctR);
pp->set_vertical_offset(playmat.params.verticalOffset);
pp->set_zoom(playmat.params.zoom);
return prepareGameCommand(cmd);
}
void GameEventHandler::sendReadyStart(bool ready, int playerId)
{
Command_ReadyStart cmd;
cmd.set_ready(ready);
sendGameCommand(cmd, playerId);
}
void GameEventHandler::sendForceStart(int playerId)
{
Command_ReadyStart cmd;
cmd.set_force_start(true);
cmd.set_ready(true);
sendGameCommand(cmd, playerId);
}
void GameEventHandler::sendSetSideboardLock(bool locked, int playerId)
{
Command_SetSideboardLock cmd;
cmd.set_locked(locked);
sendGameCommand(cmd, playerId);
}
void GameEventHandler::sendSetSideboardPlan(const QList<MoveCard_ToZone> &plan, int playerId)
{
Command_SetSideboardPlan cmd;
for (const auto &zone : plan) {
cmd.add_move_list()->CopyFrom(zone);
}
sendGameCommand(cmd, playerId);
}
void GameEventHandler::handleGameLeft()
{
sendGameCommand(Command_LeaveGame());
}
void GameEventHandler::handleChatMessageSent(const QString &chatMessage)
{
Command_GameSay cmd;
cmd.set_message(chatMessage.toStdString());
sendGameCommand(cmd);
}
void GameEventHandler::handleArrowDeletion(int creatorId, int arrowId)
{
Command_DeleteArrow cmd;
cmd.set_arrow_id(arrowId);
auto preparedCommand = prepareGameCommand(cmd);
connect(preparedCommand, &PendingCommand::finished, this, [creatorId, arrowId, this](const Response &response) {
handleArrowDeletionFinished(response, creatorId, arrowId);
});
sendGameCommand(preparedCommand);
}
void GameEventHandler::handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId)
{
// The server confirms the arrow no longer exists whether it deleted it itself
// (RespOk, followed by an Event_DeleteArrow broadcast) or never had it
// (RespNameNotFound). In both cases the local copy has to go. deleteArrow is
// a no-op if the arrow was already removed by the event broadcast.
if (response.response_code() == Response::RespOk || response.response_code() == Response::RespNameNotFound) {
emit arrowDeleted(creatorId, arrowId);
}
}
void GameEventHandler::eventSpectatorSay(const Event_GameSay &event,
int eventPlayerId,
const GameEventContext & /*context*/)
{
const ServerInfo_User &userInfo = game->getPlayerManager()->getSpectators().value(eventPlayerId);
emit logSpectatorSay(userInfo, QString::fromStdString(event.message()));
}
void GameEventHandler::eventSpectatorLeave(const Event_Leave &event,
int eventPlayerId,
const GameEventContext & /*context*/)
{
emit logSpectatorLeave(game->getPlayerManager()->getSpectatorName(eventPlayerId), getLeaveReason(event.reason()));
emit spectatorLeft(eventPlayerId);
game->getPlayerManager()->removeSpectator(eventPlayerId);
}
void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event,
int /*eventPlayerId*/,
const GameEventContext & /*context*/)
{
const int playerListSize = event.player_list_size();
if (event.has_local_player_id()) {
game->getPlayerManager()->setLocalPlayerId(event.local_player_id());
}
QSet<int> authoritativePlayerIds;
for (int i = 0; i < playerListSize; ++i) {
const ServerInfo_PlayerProperties &prop = event.player_list(i).properties();
if (!prop.spectator()) {
authoritativePlayerIds.insert(prop.player_id());
}
}
// Players whose seats were shuffled away from the previous game are no longer
// part of the authoritative list and must be removed.
for (int stalePlayerId : game->getPlayerManager()->getPlayers().keys()) {
if (!authoritativePlayerIds.contains(stalePlayerId)) {
game->getPlayerManager()->removePlayer(stalePlayerId);
emit playerLeft(stalePlayerId);
}
}
QVector<QPair<int, QPair<QString, QString>>> opponentDecksToDisplay;
for (int i = 0; i < playerListSize; ++i) {
const ServerInfo_Player &playerInfo = event.player_list(i);
const ServerInfo_PlayerProperties &prop = playerInfo.properties();
const int playerId = prop.player_id();
QString playerName = QString::fromStdString(prop.user_info().name());
emit addPlayerToAutoCompleteList("@" + playerName);
if (prop.spectator()) {
if (!game->getPlayerManager()->getSpectators().contains(playerId)) {
game->getPlayerManager()->addSpectator(playerId, prop);
emit spectatorJoined(prop);
}
} else {
PlayerLogic *player = game->getPlayerManager()->getPlayers().value(playerId, 0);
if (!player) {
player = game->getPlayerManager()->addPlayer(playerId, prop.user_info());
emit playerJoined(prop);
}
player->processPlayerInfo(playerInfo);
// Extract playmat from player properties for opponent display
if (prop.has_playmat_params()) {
player->setPlaymatFromProperties(prop);
}
if (player->getPlayerInfo()->getLocal()) {
emit localPlayerDeckSelected(player, playerId, playerInfo);
} else {
if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) {
continue;
}
if (!playerInfo.has_deck_list()) {
continue;
}
opponentDecksToDisplay.append(
qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list()))));
}
}
}
processCardAttachmentsForPlayers(event);
emit remotePlayersDecksSelected(opponentDecksToDisplay);
// The server emits player_list in seat order, so record it to lay out the
// table by seating rather than by player id.
QList<int> seatOrder;
for (int i = 0; i < playerListSize; ++i) {
const ServerInfo_PlayerProperties &prop = event.player_list(i).properties();
if (!prop.spectator()) {
seatOrder.append(prop.player_id());
}
}
if (!seatOrder.isEmpty()) {
game->getGameState()->setSeatOrder(seatOrder);
}
game->getGameState()->setGameTime(event.seconds_elapsed());
if (event.game_started() && !game->getGameMetaInfo()->started()) {
game->getGameState()->setResuming(!game->getGameState()->isGameStateKnown());
game->getGameMetaInfo()->setStarted(event.game_started());
if (game->getGameState()->isGameStateKnown()) {
emit logGameStart();
}
game->getGameState()->setActivePlayer(event.active_player_id());
game->getGameState()->setCurrentPhase(event.active_phase());
} else if (!event.game_started() && game->getGameMetaInfo()->started()) {
game->getGameState()->setCurrentPhase(-1);
game->getGameState()->setActivePlayer(-1);
game->getGameMetaInfo()->setStarted(false);
emit gameStopped();
}
game->getGameState()->setGameStateKnown(true);
emitUserEvent();
}
void GameEventHandler::processCardAttachmentsForPlayers(const Event_GameStateChanged &event)
{
for (int i = 0; i < event.player_list_size(); ++i) {
const ServerInfo_Player &playerInfo = event.player_list(i);
const ServerInfo_PlayerProperties &prop = playerInfo.properties();
if (!prop.spectator()) {
PlayerLogic *player = game->getPlayerManager()->getPlayers().value(prop.player_id(), 0);
if (!player) {
continue;
}
player->processCardAttachment(playerInfo);
}
}
}
void GameEventHandler::eventPlayerPropertiesChanged(const Event_PlayerPropertiesChanged &event,
int eventPlayerId,
const GameEventContext &context)
{
PlayerLogic *player = game->getPlayerManager()->getPlayers().value(eventPlayerId, 0);
if (!player) {
return;
}
const ServerInfo_PlayerProperties &prop = event.player_properties();
emit playerPropertiesChanged(prop, eventPlayerId);
// Update playmat from player properties
if (prop.has_playmat_params()) {
player->setPlaymatFromProperties(prop);
}
const auto contextType = static_cast<GameEventContext::ContextType>(getPbExtension(context));
switch (contextType) {
case GameEventContext::READY_START: {
bool ready = prop.ready_start();
if (player->getPlayerInfo()->getLocal()) {
emit localPlayerReadyStateChanged(player->getPlayerInfo()->getId(), ready);
}
if (ready) {
emit logReadyStart(player);
} else {
emit logNotReadyStart(player);
}
break;
}
case GameEventContext::CONCEDE: {
player->setConceded(true);
QMapIterator<int, PlayerLogic *> playerIterator(game->getPlayerManager()->getPlayers());
while (playerIterator.hasNext()) {
playerIterator.next().value()->updateZones();
}
emit logConcede(eventPlayerId);
break;
}
case GameEventContext::UNCONCEDE: {
player->setConceded(false);
QMapIterator<int, PlayerLogic *> playerIterator(game->getPlayerManager()->getPlayers());
while (playerIterator.hasNext()) {
playerIterator.next().value()->updateZones();
}
emit logUnconcede(eventPlayerId);
break;
}
case GameEventContext::DECK_SELECT: {
Context_DeckSelect deckSelect = context.GetExtension(Context_DeckSelect::ext);
emit logDeckSelect(player, QString::fromStdString(deckSelect.deck_hash()), deckSelect.sideboard_size());
if (game->getGameMetaInfo()->proto().share_decklists_on_load() && deckSelect.has_deck_list() &&
eventPlayerId != game->getPlayerManager()->getLocalPlayerId()) {
emit remotePlayerDeckSelected(QString::fromStdString(deckSelect.deck_list()), eventPlayerId,
player->getPlayerInfo()->getName());
}
break;
}
case GameEventContext::SET_SIDEBOARD_LOCK: {
if (player->getPlayerInfo()->getLocal()) {
emit localPlayerSideboardLocked(player->getPlayerInfo()->getId(), prop.sideboard_locked());
}
emit logSideboardLockSet(player, prop.sideboard_locked());
break;
}
case GameEventContext::CONNECTION_STATE_CHANGED: {
emit logConnectionStateChanged(player, prop.ping_seconds() != -1);
break;
}
default:;
}
}
void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/, const GameEventContext & /*context*/)
{
const ServerInfo_PlayerProperties &playerInfo = event.player_properties();
const int playerId = playerInfo.player_id();
QString playerName = QString::fromStdString(playerInfo.user_info().name());
emit addPlayerToAutoCompleteList(playerName);
PlayerManager *playerManager = game->getPlayerManager();
if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
return;
}
if (playerInfo.spectator()) {
playerManager->addSpectator(playerId, playerInfo);
emit logJoinSpectator(playerName);
emit spectatorJoined(playerInfo);
} else {
PlayerLogic *newPlayer = game->getPlayerManager()->addPlayer(playerId, playerInfo.user_info());
emit logJoinPlayer(newPlayer);
emit playerJoined(playerInfo);
emitUserEvent();
}
}
QString GameEventHandler::getLeaveReason(Event_Leave::LeaveReason reason)
{
switch (reason) {
case Event_Leave::USER_KICKED:
return tr("kicked by game host or moderator");
break;
case Event_Leave::USER_LEFT:
return tr("player left the game");
break;
case Event_Leave::USER_DISCONNECTED:
return tr("player disconnected from server");
break;
case Event_Leave::OTHER:
default:
return tr("reason unknown");
break;
}
}
void GameEventHandler::eventLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext & /*context*/)
{
PlayerLogic *player = game->getPlayerManager()->getPlayers().value(eventPlayerId, 0);
if (!player) {
return;
}
player->clear();
emit playerLeft(eventPlayerId);
emit logLeave(player, getLeaveReason(event.reason()));
game->getPlayerManager()->removePlayer(eventPlayerId);
player->deleteLater();
// Rearrange all remaining zones so that attachment relationship updates take place
QMapIterator<int, PlayerLogic *> playerIterator(game->getPlayerManager()->getPlayers());
while (playerIterator.hasNext()) {
playerIterator.next().value()->updateZones();
}
emitUserEvent();
}
void GameEventHandler::eventKicked(const Event_Kicked & /*event*/,
int /*eventPlayerId*/,
const GameEventContext & /*context*/)
{
emit gameClosed();
emit logKicked();
emit playerKicked();
emitUserEvent();
}
void GameEventHandler::eventReverseTurn(const Event_ReverseTurn &event,
int eventPlayerId,
const GameEventContext & /*context*/)
{
PlayerLogic *player = game->getPlayerManager()->getPlayers().value(eventPlayerId, 0);
if (!player) {
return;
}
emit logTurnReversed(player, event.reversed());
}
void GameEventHandler::eventPlayerOrderChanged(const Event_PlayerOrderChanged &event,
int /*eventPlayerId*/,
const GameEventContext & /*context*/)
{
QStringList playerNames;
for (const auto &name : event.player_names()) {
playerNames.append(QString::fromStdString(name));
}
// Track the seating by stable player id so the table layout and the turn
// order dialog follow the shuffled order instead of id order.
QList<int> seatOrder;
const auto &players = game->getPlayerManager()->getPlayers();
for (const QString &name : playerNames) {
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
if (it.value() && it.value()->getPlayerInfo()->getName() == name) {
seatOrder.append(it.key());
break;
}
}
}
if (!seatOrder.isEmpty()) {
game->getGameState()->setSeatOrder(seatOrder);
}
emit logTurnOrderChanged(playerNames, event.randomized());
}
void GameEventHandler::eventGameHostChanged(const Event_GameHostChanged & /*event*/,
int eventPlayerId,
const GameEventContext & /*context*/)
{
game->getGameState()->setHostId(eventPlayerId);
}
void GameEventHandler::eventGameClosed(const Event_GameClosed & /*event*/,
int /*eventPlayerId*/,
const GameEventContext & /*context*/)
{
game->getGameMetaInfo()->setStarted(false);
game->getGameState()->setGameClosed(true);
emit gameClosed();
emit logGameClosed();
emitUserEvent();
}
void GameEventHandler::eventSetActivePlayer(const Event_SetActivePlayer &event,
int /*eventPlayerId*/,
const GameEventContext & /*context*/)
{
game->getGameState()->setActivePlayer(event.active_player_id());
PlayerLogic *player = game->getPlayerManager()->getPlayer(event.active_player_id());
if (!player) {
return;
}
emit logActivePlayer(player);
emitUserEvent();
}
void GameEventHandler::eventSetActivePhase(const Event_SetActivePhase &event,
int /*eventPlayerId*/,
const GameEventContext & /*context*/)
{
const int phase = event.phase();
if (game->getGameState()->getCurrentPhase() != phase) {
emit logActivePhaseChanged(phase);
}
game->getGameState()->setCurrentPhase(phase);
emitUserEvent();
}