Show loaded deck messages to players joining a game (#552)

The "Player X has loaded a deck" message is only broadcast when a deck is
selected, so players and spectators who join a game afterwards never see it.

On game join, replay a deck-select event context privately to the joining
participant for every player that already has a deck loaded, reusing the
existing Event_PlayerPropertiesChanged + Context_DeckSelect mechanism so no
protocol version bump is needed.

- Server_AbstractPlayer: expose the sideboard-locked state
- Server_Game: synthesize deck-select contexts in createGameJoinedEvent
This commit is contained in:
Lukas Brübach 2026-09-07 00:50:26 +02:00
parent fb482f037e
commit 82ae0ecf01
2 changed files with 43 additions and 0 deletions

View file

@ -47,6 +47,10 @@ public:
{
return deck;
}
bool getSideboardLocked() const
{
return sideboardLocked;
}
bool getReadyStart() const
{
return readyStart;

View file

@ -37,6 +37,7 @@
#include <google/protobuf/descriptor.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/protocol/pb/context_connection_state_changed.pb.h>
#include <libcockatrice/protocol/pb/context_deck_select.pb.h>
#include <libcockatrice/protocol/pb/context_ping_changed.pb.h>
#include <libcockatrice/protocol/pb/event_delete_arrow.pb.h>
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
@ -783,6 +784,44 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
}
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1));
// A newly joined player never saw the "Player X has loaded a deck" messages
// (they are only broadcast when a deck is selected), so replay a deck-select
// context for every player that already has a deck loaded. It is delivered
// privately to the joining participant, whose client renders it as a log
// entry in the same way as a fresh deck selection.
for (auto *participant : participants.values()) {
if (participant == joiningParticipant) {
continue;
}
auto *deckOwner = qobject_cast<Server_Player *>(participant);
if (deckOwner == nullptr) {
continue;
}
const DeckList *deckList = deckOwner->getDeckList();
if (deckList == nullptr) {
continue;
}
Event_PlayerPropertiesChanged event;
event.mutable_player_properties()->set_sideboard_locked(deckOwner->getSideboardLocked());
event.mutable_player_properties()->set_deck_hash(deckList->getDeckHash().toStdString());
Context_DeckSelect deckSelect;
deckSelect.set_deck_hash(deckList->getDeckHash().toStdString());
deckSelect.set_sideboard_size(deckList->getSideboardSize());
if (getShareDecklistsOnLoad()) {
deckSelect.set_deck_list(deckList->writeToString_Native().toStdString());
}
GameEventContext context;
context.GetReflection()
->MutableMessage(&context, deckSelect.GetDescriptor()->FindExtensionByName("ext"))
->CopyFrom(deckSelect);
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER,
prepareGameEvent(event, deckOwner->getPlayerId(), &context));
}
}
void Server_Game::sendGameEventContainer(GameEventContainer *cont,