mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Compare commits
2 commits
734f91c8e9
...
164a67404d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
164a67404d | ||
|
|
77f60e0526 |
7 changed files with 976 additions and 15 deletions
|
|
@ -248,6 +248,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/playmat/playmat_settings_dialog.cpp
|
||||
src/interface/widgets/quick_settings/settings_button_widget.cpp
|
||||
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
||||
src/interface/widgets/replay/replay_event_index.cpp
|
||||
src/interface/widgets/replay/replay_manager.cpp
|
||||
src/interface/widgets/replay/replay_quick_settings_widget.cpp
|
||||
src/interface/widgets/replay/replay_timeline_widget.cpp
|
||||
|
|
|
|||
876
cockatrice/src/interface/widgets/replay/replay_event_index.cpp
Normal file
876
cockatrice/src/interface/widgets/replay/replay_event_index.cpp
Normal file
|
|
@ -0,0 +1,876 @@
|
|||
#include "replay_event_index.h"
|
||||
|
||||
#include "../../../game/phase.h"
|
||||
#include "libcockatrice/protocol/get_pb_extension.h"
|
||||
#include "libcockatrice/protocol/pb/card_attributes.pb.h"
|
||||
#include "libcockatrice/protocol/pb/context_concede.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/context_mulligan.pb.h"
|
||||
#include "libcockatrice/protocol/pb/context_ready_start.pb.h"
|
||||
#include "libcockatrice/protocol/pb/context_set_sideboard_lock.pb.h"
|
||||
#include "libcockatrice/protocol/pb/context_undo_draw.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_attach_card.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_change_zone_properties.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_create_counter.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_create_token.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_del_counter.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_destroy_card.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_draw_cards.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_dump_zone.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_flip_card.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_log_notice.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_move_card.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_player_properties_changed.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_reveal_cards.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_reverse_turn.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_roll_die.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/event_set_card_attr.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_set_card_counter.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_set_counter.pb.h"
|
||||
#include "libcockatrice/protocol/pb/event_shuffle.pb.h"
|
||||
#include "libcockatrice/protocol/pb/game_event.pb.h"
|
||||
#include "libcockatrice/protocol/pb/game_event_container.pb.h"
|
||||
#include "libcockatrice/protocol/pb/game_event_context.pb.h"
|
||||
#include "libcockatrice/protocol/pb/game_replay.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_card.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_counter.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_player.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_playerproperties.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_zone.pb.h"
|
||||
#include "libcockatrice/utility/zone_names.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QHash>
|
||||
#include <QStringList>
|
||||
#include <QtGlobal>
|
||||
#include <algorithm>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct PendingDetail
|
||||
{
|
||||
QString text;
|
||||
QString badge;
|
||||
ReplayEventKind kind = ReplayEventKind::Action;
|
||||
QString actor;
|
||||
};
|
||||
|
||||
QString formatTime(int timeMs)
|
||||
{
|
||||
const int totalSeconds = qMax(0, timeMs) / 1000;
|
||||
const int hours = totalSeconds / 3600;
|
||||
const int minutes = (totalSeconds / 60) % 60;
|
||||
const int seconds = totalSeconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return QStringLiteral("%1:%2:%3")
|
||||
.arg(hours)
|
||||
.arg(minutes, 2, 10, QLatin1Char('0'))
|
||||
.arg(seconds, 2, 10, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
return QStringLiteral("%1:%2").arg(minutes, 2, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
class ReplayEventIndexBuilder
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(ReplayEventIndex)
|
||||
|
||||
public:
|
||||
ReplayEventIndexBuilder(const GameReplay *replay, const QList<int> &timeline) : replay(replay), timeline(timeline)
|
||||
{
|
||||
}
|
||||
|
||||
QList<ReplayEventEntry> build()
|
||||
{
|
||||
QList<ReplayEventEntry> entries;
|
||||
|
||||
for (int i = 0; i < replay->event_list_size(); ++i) {
|
||||
const GameEventContainer &container = replay->event_list(i);
|
||||
const QList<PendingDetail> details = describeContainer(container);
|
||||
if (details.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ReplayEventEntry entry;
|
||||
entry.eventIndex = i;
|
||||
entry.timeMs = timeline.value(i, static_cast<int>(container.seconds_elapsed()) * 1000);
|
||||
entry.timeLabel = formatTime(entry.timeMs);
|
||||
entry.kind = details.first().kind;
|
||||
entry.badge = details.first().badge;
|
||||
if (details.size() > 1) {
|
||||
entry.badge += QStringLiteral(" +%1").arg(details.size() - 1);
|
||||
}
|
||||
|
||||
const QString sharedActor = details.first().actor;
|
||||
const bool mixedActors = std::any_of(details.cbegin(), details.cend(), [&sharedActor](const auto &detail) {
|
||||
return detail.actor != sharedActor;
|
||||
});
|
||||
|
||||
QStringList detailTexts;
|
||||
for (const PendingDetail &detail : details) {
|
||||
if (mixedActors && !detail.actor.isEmpty()) {
|
||||
detailTexts.append(tr("%1: %2").arg(detail.actor, detail.text));
|
||||
} else {
|
||||
detailTexts.append(detail.text);
|
||||
}
|
||||
}
|
||||
entry.actor = mixedActors ? QString() : sharedActor;
|
||||
entry.detail = detailTexts.join(QStringLiteral(" • "));
|
||||
|
||||
entries.append(entry);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private:
|
||||
const GameReplay *replay;
|
||||
const QList<int> &timeline;
|
||||
|
||||
QHash<int, QString> playerNames;
|
||||
QHash<int, bool> spectators;
|
||||
QHash<QString, QString> cardNames;
|
||||
QHash<QString, QString> counterNames;
|
||||
|
||||
int activePlayerId = -1;
|
||||
bool gameStarted = false;
|
||||
bool sawGameState = false;
|
||||
|
||||
static QString cardKey(int playerId, const QString &zone, int cardId)
|
||||
{
|
||||
return QStringLiteral("%1/%2/%3").arg(playerId).arg(zone).arg(cardId);
|
||||
}
|
||||
|
||||
static QString counterKey(int playerId, int counterId)
|
||||
{
|
||||
return QStringLiteral("%1/%2").arg(playerId).arg(counterId);
|
||||
}
|
||||
|
||||
QString playerName(int playerId) const
|
||||
{
|
||||
if (playerId < 0) {
|
||||
return tr("Game");
|
||||
}
|
||||
return playerNames.value(playerId, tr("Player %1").arg(playerId));
|
||||
}
|
||||
|
||||
QString actorName(int playerId) const
|
||||
{
|
||||
if (playerId < 0) {
|
||||
return QString();
|
||||
}
|
||||
return playerNames.value(playerId, tr("Player %1").arg(playerId));
|
||||
}
|
||||
|
||||
bool isSpectator(int playerId) const
|
||||
{
|
||||
return spectators.value(playerId, false);
|
||||
}
|
||||
|
||||
void rememberPlayer(const ServerInfo_PlayerProperties &properties)
|
||||
{
|
||||
const int playerId = properties.player_id();
|
||||
if (playerId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString name = QString::fromStdString(properties.user_info().name());
|
||||
if (!name.isEmpty()) {
|
||||
playerNames.insert(playerId, name);
|
||||
}
|
||||
spectators.insert(playerId, properties.spectator());
|
||||
}
|
||||
|
||||
void rememberCard(int playerId, const QString &zone, int cardId, const QString &name)
|
||||
{
|
||||
if (cardId >= 0 && !name.isEmpty()) {
|
||||
cardNames.insert(cardKey(playerId, zone, cardId), name);
|
||||
}
|
||||
}
|
||||
|
||||
void forgetCard(int playerId, const QString &zone, int cardId)
|
||||
{
|
||||
if (cardId >= 0) {
|
||||
cardNames.remove(cardKey(playerId, zone, cardId));
|
||||
}
|
||||
}
|
||||
|
||||
QString cardName(int playerId, const QString &zone, int cardId) const
|
||||
{
|
||||
if (cardId < 0) {
|
||||
return QString();
|
||||
}
|
||||
return cardNames.value(cardKey(playerId, zone, cardId));
|
||||
}
|
||||
|
||||
void rememberCounter(int playerId, const ServerInfo_Counter &counter)
|
||||
{
|
||||
const QString name = QString::fromStdString(counter.name());
|
||||
if (!name.isEmpty()) {
|
||||
counterNames.insert(counterKey(playerId, counter.id()), name);
|
||||
}
|
||||
}
|
||||
|
||||
QString counterName(int playerId, int counterId) const
|
||||
{
|
||||
return counterNames.value(counterKey(playerId, counterId));
|
||||
}
|
||||
|
||||
QString zoneName(const QString &zone) const
|
||||
{
|
||||
if (zone == ZoneNames::TABLE) {
|
||||
return tr("battlefield");
|
||||
}
|
||||
if (zone == ZoneNames::GRAVE) {
|
||||
return tr("graveyard");
|
||||
}
|
||||
if (zone == ZoneNames::EXILE) {
|
||||
return tr("exile");
|
||||
}
|
||||
if (zone == ZoneNames::HAND) {
|
||||
return tr("hand");
|
||||
}
|
||||
if (zone == ZoneNames::DECK) {
|
||||
return tr("library");
|
||||
}
|
||||
if (zone == ZoneNames::SIDEBOARD) {
|
||||
return tr("sideboard");
|
||||
}
|
||||
if (zone == ZoneNames::STACK) {
|
||||
return tr("stack");
|
||||
}
|
||||
return zone;
|
||||
}
|
||||
|
||||
QString zonePhrase(int playerId, const QString &zone, int actorId) const
|
||||
{
|
||||
if (zone == ZoneNames::TABLE) {
|
||||
return tr("the battlefield");
|
||||
}
|
||||
if (zone == ZoneNames::EXILE) {
|
||||
return tr("exile");
|
||||
}
|
||||
if (zone == ZoneNames::STACK) {
|
||||
return tr("the stack");
|
||||
}
|
||||
|
||||
const QString name = zoneName(zone);
|
||||
if (playerId < 0 || playerId == actorId) {
|
||||
return tr("their %1").arg(name);
|
||||
}
|
||||
return tr("%1's %2").arg(playerName(playerId), name);
|
||||
}
|
||||
|
||||
QString formatCardList(const QStringList &names, int total) const
|
||||
{
|
||||
// Keep repeats: each entry is a drawn/revealed card, so the list length must agree with the count.
|
||||
QStringList listedNames;
|
||||
for (const QString &name : names) {
|
||||
if (!name.isEmpty()) {
|
||||
listedNames.append(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (listedNames.isEmpty()) {
|
||||
if (total <= 0) {
|
||||
return tr("no cards");
|
||||
}
|
||||
return total == 1 ? tr("1 card") : tr("%1 cards").arg(total);
|
||||
}
|
||||
|
||||
if (total < listedNames.size()) {
|
||||
total = listedNames.size();
|
||||
}
|
||||
|
||||
if (listedNames.size() > 3) {
|
||||
return tr("%1 and %2 more").arg(listedNames.mid(0, 3).join(QStringLiteral(", "))).arg(total - 3);
|
||||
}
|
||||
if (total > listedNames.size()) {
|
||||
return tr("%1 and %2 more").arg(listedNames.join(QStringLiteral(", "))).arg(total - listedNames.size());
|
||||
}
|
||||
|
||||
return listedNames.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
void updateGameState(const Event_GameStateChanged &event)
|
||||
{
|
||||
for (int i = 0; i < event.player_list_size(); ++i) {
|
||||
const ServerInfo_Player &player = event.player_list(i);
|
||||
const int playerId = player.properties().player_id();
|
||||
rememberPlayer(player.properties());
|
||||
|
||||
for (int j = 0; j < player.zone_list_size(); ++j) {
|
||||
const ServerInfo_Zone &zone = player.zone_list(j);
|
||||
for (int k = 0; k < zone.card_list_size(); ++k) {
|
||||
const ServerInfo_Card &card = zone.card_list(k);
|
||||
rememberCard(playerId, QString::fromStdString(zone.name()), card.id(),
|
||||
QString::fromStdString(card.name()));
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < player.counter_list_size(); ++j) {
|
||||
rememberCounter(playerId, player.counter_list(j));
|
||||
}
|
||||
}
|
||||
|
||||
if (event.has_active_player_id()) {
|
||||
activePlayerId = event.active_player_id();
|
||||
}
|
||||
if (event.has_game_started()) {
|
||||
gameStarted = event.game_started();
|
||||
}
|
||||
}
|
||||
|
||||
QString describeMoveCard(const Event_MoveCard &event, int actorId, const GameEventContext &context)
|
||||
{
|
||||
const int startPlayerId = event.start_player_id() >= 0 ? event.start_player_id() : actorId;
|
||||
const int targetPlayerId = event.target_player_id() >= 0 ? event.target_player_id() : startPlayerId;
|
||||
const QString startZone = QString::fromStdString(event.start_zone());
|
||||
const QString targetZone = event.has_target_zone() ? QString::fromStdString(event.target_zone()) : startZone;
|
||||
|
||||
QString name;
|
||||
if (event.has_card_name()) {
|
||||
name = QString::fromStdString(event.card_name());
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
name = cardName(startPlayerId, startZone, event.card_id());
|
||||
}
|
||||
|
||||
const bool knownName = !name.isEmpty();
|
||||
if (name.isEmpty()) {
|
||||
name = event.face_down() ? tr("a face-down card") : tr("a card");
|
||||
}
|
||||
|
||||
const int newCardId = event.has_new_card_id() ? event.new_card_id() : event.card_id();
|
||||
forgetCard(startPlayerId, startZone, event.card_id());
|
||||
if (knownName) {
|
||||
rememberCard(targetPlayerId, targetZone, newCardId, name);
|
||||
}
|
||||
|
||||
if (startZone == targetZone) {
|
||||
return tr("moved %1 within %2.").arg(name, zonePhrase(targetPlayerId, targetZone, actorId));
|
||||
}
|
||||
if (context.HasExtension(Context_UndoDraw::ext)) {
|
||||
return tr("returned %1 to %2.").arg(name, zonePhrase(targetPlayerId, targetZone, actorId));
|
||||
}
|
||||
|
||||
return tr("moved %1 from %2 to %3.")
|
||||
.arg(name, zonePhrase(startPlayerId, startZone, actorId), zonePhrase(targetPlayerId, targetZone, actorId));
|
||||
}
|
||||
|
||||
QString describeSetCardAttribute(const Event_SetCardAttr &event, int actorId)
|
||||
{
|
||||
const QString zone = QString::fromStdString(event.zone_name());
|
||||
QString name;
|
||||
if (event.has_card_id()) {
|
||||
name = cardName(actorId, zone, event.card_id());
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
name = event.has_card_id() ? tr("a card") : tr("all cards");
|
||||
}
|
||||
|
||||
const QString value = QString::fromStdString(event.attr_value());
|
||||
switch (event.attribute()) {
|
||||
case AttrTapped:
|
||||
return value == QLatin1String("1") ? tr("tapped %1.").arg(name) : tr("untapped %1.").arg(name);
|
||||
case AttrAttacking:
|
||||
return value == QLatin1String("1") ? tr("%1 is now attacking.").arg(name)
|
||||
: tr("%1 is no longer attacking.").arg(name);
|
||||
case AttrFaceDown:
|
||||
return value == QLatin1String("1") ? tr("turned %1 face down.").arg(name)
|
||||
: tr("turned %1 face up.").arg(name);
|
||||
case AttrColor:
|
||||
return tr("changed %1's color to %2.").arg(name, value);
|
||||
case AttrPT:
|
||||
return tr("set %1's power/toughness to %2.").arg(name, value);
|
||||
case AttrAnnotation:
|
||||
return value.isEmpty() ? tr("cleared %1's annotation.").arg(name)
|
||||
: tr("annotated %1: %2").arg(name, value);
|
||||
case AttrDoesntUntap:
|
||||
return value == QLatin1String("1") ? tr("%1 will no longer untap.").arg(name)
|
||||
: tr("%1 will untap normally again.").arg(name);
|
||||
}
|
||||
|
||||
return tr("changed %1.").arg(name);
|
||||
}
|
||||
|
||||
QString badgeForCardAttribute(CardAttribute attribute) const
|
||||
{
|
||||
switch (attribute) {
|
||||
case AttrTapped:
|
||||
return tr("TAP");
|
||||
case AttrAttacking:
|
||||
return tr("ATTACK");
|
||||
case AttrFaceDown:
|
||||
return tr("FLIP");
|
||||
case AttrColor:
|
||||
return tr("COLOR");
|
||||
case AttrPT:
|
||||
return tr("PT");
|
||||
case AttrAnnotation:
|
||||
return tr("NOTE");
|
||||
case AttrDoesntUntap:
|
||||
return tr("TAP");
|
||||
}
|
||||
return tr("CARD");
|
||||
}
|
||||
|
||||
QString describeLeaveReason(Event_Leave::LeaveReason reason) const
|
||||
{
|
||||
switch (reason) {
|
||||
case Event_Leave::USER_KICKED:
|
||||
return tr("kicked by game host or moderator");
|
||||
case Event_Leave::USER_LEFT:
|
||||
return tr("left the game");
|
||||
case Event_Leave::USER_DISCONNECTED:
|
||||
return tr("disconnected");
|
||||
case Event_Leave::OTHER:
|
||||
default:
|
||||
return tr("reason unknown");
|
||||
}
|
||||
}
|
||||
|
||||
void addDetail(QList<PendingDetail> &details,
|
||||
const QString &text,
|
||||
const QString &badge,
|
||||
ReplayEventKind kind,
|
||||
int actorId,
|
||||
bool forcedByJudge) const
|
||||
{
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString finalText = forcedByJudge ? tr("%1 (judge)").arg(text) : text;
|
||||
details.append(PendingDetail{.text = finalText, .badge = badge, .kind = kind, .actor = actorName(actorId)});
|
||||
}
|
||||
|
||||
QList<PendingDetail> describeContainer(const GameEventContainer &container)
|
||||
{
|
||||
QList<PendingDetail> details;
|
||||
const GameEventContext &context = container.context();
|
||||
const bool forcedByJudge = container.has_forced_by_judge();
|
||||
const auto contextType = static_cast<GameEventContext::ContextType>(getPbExtension(context));
|
||||
|
||||
for (int i = 0; i < container.event_list_size(); ++i) {
|
||||
const GameEvent &event = container.event_list(i);
|
||||
const int eventPlayerId = event.player_id();
|
||||
const auto eventType = static_cast<GameEvent::GameEventType>(getPbExtension(event));
|
||||
|
||||
switch (eventType) {
|
||||
case GameEvent::GAME_STATE_CHANGED: {
|
||||
const Event_GameStateChanged &state = event.GetExtension(Event_GameStateChanged::ext);
|
||||
const bool wasStarted = gameStarted;
|
||||
updateGameState(state);
|
||||
if (state.game_started() && !wasStarted) {
|
||||
addDetail(details, tr("Game started."), tr("GAME"), ReplayEventKind::System, -1, forcedByJudge);
|
||||
} else if (!sawGameState) {
|
||||
addDetail(details, tr("Game state loaded."), tr("GAME"), ReplayEventKind::System, -1,
|
||||
forcedByJudge);
|
||||
}
|
||||
sawGameState = true;
|
||||
break;
|
||||
}
|
||||
case GameEvent::PLAYER_PROPERTIES_CHANGED: {
|
||||
const Event_PlayerPropertiesChanged &propertiesEvent =
|
||||
event.GetExtension(Event_PlayerPropertiesChanged::ext);
|
||||
const bool hasProperties = propertiesEvent.has_player_properties();
|
||||
const ServerInfo_PlayerProperties &properties = propertiesEvent.player_properties();
|
||||
// The server only fills in the field that changed, so player_id is absent from these events
|
||||
// unless they are about a specific player; fall back to the event's own player id.
|
||||
const bool describesPlayer = hasProperties && properties.has_player_id();
|
||||
const int playerId = describesPlayer ? properties.player_id() : eventPlayerId;
|
||||
if (describesPlayer) {
|
||||
rememberPlayer(properties);
|
||||
}
|
||||
|
||||
switch (contextType) {
|
||||
case GameEventContext::READY_START:
|
||||
addDetail(details,
|
||||
properties.ready_start() ? tr("is ready to start.") : tr("is no longer ready."),
|
||||
tr("READY"), ReplayEventKind::System, playerId, forcedByJudge);
|
||||
break;
|
||||
case GameEventContext::CONCEDE:
|
||||
addDetail(details, tr("conceded the game."), tr("CONCEDE"), ReplayEventKind::System,
|
||||
playerId, forcedByJudge);
|
||||
break;
|
||||
case GameEventContext::UNCONCEDE:
|
||||
addDetail(details, tr("unconceded the game."), tr("CONCEDE"), ReplayEventKind::System,
|
||||
playerId, forcedByJudge);
|
||||
break;
|
||||
case GameEventContext::DECK_SELECT: {
|
||||
const Context_DeckSelect &deckSelect = context.GetExtension(Context_DeckSelect::ext);
|
||||
const int sideboardSize = deckSelect.sideboard_size();
|
||||
addDetail(details,
|
||||
sideboardSize >= 0
|
||||
? tr("selected a deck with %1 sideboard cards.").arg(sideboardSize)
|
||||
: tr("selected a deck."),
|
||||
tr("DECK"), ReplayEventKind::System, playerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEventContext::MULLIGAN: {
|
||||
const Context_Mulligan &mulligan = context.GetExtension(Context_Mulligan::ext);
|
||||
addDetail(details,
|
||||
mulligan.number() > 0 ? tr("took a mulligan (%1).").arg(mulligan.number())
|
||||
: tr("took a mulligan."),
|
||||
tr("MULLIGAN"), ReplayEventKind::System, playerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEventContext::SET_SIDEBOARD_LOCK:
|
||||
addDetail(details,
|
||||
properties.sideboard_locked() ? tr("locked their sideboard.")
|
||||
: tr("unlocked their sideboard."),
|
||||
tr("SIDEBOARD"), ReplayEventKind::System, playerId, forcedByJudge);
|
||||
break;
|
||||
case GameEventContext::CONNECTION_STATE_CHANGED:
|
||||
addDetail(details,
|
||||
properties.ping_seconds() != -1 ? tr("reconnected to the game.")
|
||||
: tr("lost connection to the game."),
|
||||
tr("CONNECTION"), ReplayEventKind::System, playerId, forcedByJudge);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameEvent::JOIN: {
|
||||
const Event_Join &join = event.GetExtension(Event_Join::ext);
|
||||
if (join.has_player_properties()) {
|
||||
rememberPlayer(join.player_properties());
|
||||
}
|
||||
const int playerId = join.player_properties().player_id() >= 0
|
||||
? join.player_properties().player_id()
|
||||
: eventPlayerId;
|
||||
addDetail(details, isSpectator(playerId) ? tr("joined as a spectator.") : tr("joined the game."),
|
||||
tr("JOIN"), ReplayEventKind::System, playerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::LEAVE: {
|
||||
const Event_Leave &leave = event.GetExtension(Event_Leave::ext);
|
||||
const QString reason = describeLeaveReason(leave.reason());
|
||||
if (eventPlayerId < 0) {
|
||||
addDetail(details, tr("A player left the game (%1).").arg(reason), tr("LEAVE"),
|
||||
ReplayEventKind::System, -1, forcedByJudge);
|
||||
} else {
|
||||
addDetail(details,
|
||||
isSpectator(eventPlayerId) ? tr("stopped spectating (%1).").arg(reason)
|
||||
: tr("left the game (%1).").arg(reason),
|
||||
tr("LEAVE"), ReplayEventKind::System, eventPlayerId, forcedByJudge);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameEvent::KICKED:
|
||||
addDetail(details,
|
||||
eventPlayerId >= 0 ? tr("was kicked from the game.")
|
||||
: tr("A player was kicked from the game."),
|
||||
tr("KICK"), ReplayEventKind::System, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
case GameEvent::GAME_CLOSED:
|
||||
addDetail(details, tr("Game closed."), tr("GAME"), ReplayEventKind::System, -1, forcedByJudge);
|
||||
break;
|
||||
case GameEvent::GAME_HOST_CHANGED:
|
||||
addDetail(details, eventPlayerId >= 0 ? tr("is now the game host.") : tr("The game host changed."),
|
||||
tr("HOST"), ReplayEventKind::System, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
case GameEvent::GAME_SAY: {
|
||||
const Event_GameSay &say = event.GetExtension(Event_GameSay::ext);
|
||||
addDetail(details, QString::fromStdString(say.message()), tr("CHAT"), ReplayEventKind::Chat,
|
||||
eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::SET_ACTIVE_PLAYER: {
|
||||
const Event_SetActivePlayer &setActivePlayer = event.GetExtension(Event_SetActivePlayer::ext);
|
||||
activePlayerId = setActivePlayer.active_player_id();
|
||||
if (activePlayerId >= 0) {
|
||||
addDetail(details, tr("begins their turn."), tr("TURN"), ReplayEventKind::Turn, activePlayerId,
|
||||
forcedByJudge);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameEvent::SET_ACTIVE_PHASE: {
|
||||
const Event_SetActivePhase &setActivePhase = event.GetExtension(Event_SetActivePhase::ext);
|
||||
if (setActivePhase.phase() >= 0) {
|
||||
const QString phaseName = Phases::getPhase(setActivePhase.phase()).getName();
|
||||
addDetail(details, tr("entered the %1 phase.").arg(phaseName), tr("PHASE"),
|
||||
ReplayEventKind::Phase, activePlayerId >= 0 ? activePlayerId : eventPlayerId,
|
||||
forcedByJudge);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameEvent::REVERSE_TURN: {
|
||||
const Event_ReverseTurn &reverseTurn = event.GetExtension(Event_ReverseTurn::ext);
|
||||
addDetail(details,
|
||||
reverseTurn.reversed() ? tr("reversed the turn order.") : tr("restored the turn order."),
|
||||
tr("TURN"), ReplayEventKind::Turn, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::MOVE_CARD: {
|
||||
const Event_MoveCard &moveCard = event.GetExtension(Event_MoveCard::ext);
|
||||
const QString text = describeMoveCard(moveCard, eventPlayerId, context);
|
||||
const QString startZone = QString::fromStdString(moveCard.start_zone());
|
||||
const QString targetZone = QString::fromStdString(moveCard.target_zone());
|
||||
QString badge = tr("MOVE");
|
||||
if (context.HasExtension(Context_UndoDraw::ext)) {
|
||||
badge = tr("UNDO");
|
||||
} else if (startZone == ZoneNames::STACK) {
|
||||
badge = tr("RESOLVE");
|
||||
} else if (startZone == ZoneNames::HAND && targetZone == ZoneNames::TABLE) {
|
||||
badge = tr("PLAY");
|
||||
}
|
||||
addDetail(details, text, badge, ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::DRAW_CARDS: {
|
||||
const Event_DrawCards &drawCards = event.GetExtension(Event_DrawCards::ext);
|
||||
QStringList names;
|
||||
for (int j = 0; j < drawCards.cards_size(); ++j) {
|
||||
const ServerInfo_Card &card = drawCards.cards(j);
|
||||
const QString name = QString::fromStdString(card.name());
|
||||
names.append(name);
|
||||
rememberCard(eventPlayerId, ZoneNames::HAND, card.id(), name);
|
||||
}
|
||||
if (drawCards.number() > 0) {
|
||||
addDetail(details, tr("drew %1.").arg(formatCardList(names, drawCards.number())), tr("DRAW"),
|
||||
ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameEvent::REVEAL_CARDS: {
|
||||
const Event_RevealCards &revealCards = event.GetExtension(Event_RevealCards::ext);
|
||||
const QString zone = QString::fromStdString(revealCards.zone_name());
|
||||
QStringList names;
|
||||
for (int j = 0; j < revealCards.cards_size(); ++j) {
|
||||
const ServerInfo_Card &card = revealCards.cards(j);
|
||||
const QString name = QString::fromStdString(card.name());
|
||||
names.append(name);
|
||||
rememberCard(eventPlayerId, zone, card.id(), name);
|
||||
}
|
||||
const int total =
|
||||
revealCards.number_of_cards() > 0 ? revealCards.number_of_cards() : revealCards.cards_size();
|
||||
QString text =
|
||||
tr("revealed %1 from %2.")
|
||||
.arg(formatCardList(names, total), zonePhrase(eventPlayerId, zone, eventPlayerId));
|
||||
if (revealCards.other_player_id() >= 0 && revealCards.other_player_id() != eventPlayerId) {
|
||||
text = tr("revealed %1 from %2 to %3.")
|
||||
.arg(formatCardList(names, total), zonePhrase(eventPlayerId, zone, eventPlayerId),
|
||||
playerName(revealCards.other_player_id()));
|
||||
}
|
||||
addDetail(details, text, tr("REVEAL"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::SHUFFLE: {
|
||||
const Event_Shuffle &shuffle = event.GetExtension(Event_Shuffle::ext);
|
||||
addDetail(
|
||||
details,
|
||||
tr("shuffled %1.")
|
||||
.arg(zonePhrase(eventPlayerId, QString::fromStdString(shuffle.zone_name()), eventPlayerId)),
|
||||
tr("SHUFFLE"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::ROLL_DIE: {
|
||||
const Event_RollDie &rollDie = event.GetExtension(Event_RollDie::ext);
|
||||
QStringList rolls;
|
||||
for (int j = 0; j < rollDie.values_size(); ++j) {
|
||||
rolls.append(QString::number(rollDie.values(j)));
|
||||
}
|
||||
if (rolls.isEmpty() && rollDie.value() > 0) {
|
||||
rolls.append(QString::number(rollDie.value()));
|
||||
}
|
||||
const QString sides = rollDie.sides() > 0 ? QString::number(rollDie.sides()) : QStringLiteral("?");
|
||||
addDetail(details, tr("rolled a d%1: %2.").arg(sides, rolls.join(QStringLiteral(", "))), tr("DICE"),
|
||||
ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::FLIP_CARD: {
|
||||
const Event_FlipCard &flipCard = event.GetExtension(Event_FlipCard::ext);
|
||||
const QString zone = QString::fromStdString(flipCard.zone_name());
|
||||
if (flipCard.has_card_name()) {
|
||||
rememberCard(eventPlayerId, zone, flipCard.card_id(),
|
||||
QString::fromStdString(flipCard.card_name()));
|
||||
}
|
||||
QString name = flipCard.has_card_name() ? QString::fromStdString(flipCard.card_name())
|
||||
: cardName(eventPlayerId, zone, flipCard.card_id());
|
||||
if (name.isEmpty()) {
|
||||
name = tr("a card");
|
||||
}
|
||||
addDetail(details,
|
||||
flipCard.face_down() ? tr("flipped %1 face down.").arg(name)
|
||||
: tr("flipped %1 face up.").arg(name),
|
||||
tr("FLIP"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::DESTROY_CARD: {
|
||||
const Event_DestroyCard &destroyCard = event.GetExtension(Event_DestroyCard::ext);
|
||||
const QString zone = QString::fromStdString(destroyCard.zone_name());
|
||||
const QString name = cardName(eventPlayerId, zone, destroyCard.card_id());
|
||||
forgetCard(eventPlayerId, zone, destroyCard.card_id());
|
||||
addDetail(details, tr("destroyed %1.").arg(name.isEmpty() ? tr("a card") : name), tr("DESTROY"),
|
||||
ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::ATTACH_CARD: {
|
||||
const Event_AttachCard &attachCard = event.GetExtension(Event_AttachCard::ext);
|
||||
const QString startZone = QString::fromStdString(attachCard.start_zone());
|
||||
const QString source = cardName(eventPlayerId, startZone, attachCard.card_id());
|
||||
const QString sourceText = source.isEmpty() ? tr("a card") : source;
|
||||
// An unattach event carries no target at all, so the target fields default to player 0 / no zone
|
||||
// / card 0; only a present target_player_id means the card was attached to something.
|
||||
if (!attachCard.has_target_player_id()) {
|
||||
addDetail(details, tr("unattached %1.").arg(sourceText), tr("ATTACH"), ReplayEventKind::Action,
|
||||
eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
const QString targetZone = QString::fromStdString(attachCard.target_zone());
|
||||
const int targetPlayerId =
|
||||
attachCard.target_player_id() >= 0 ? attachCard.target_player_id() : eventPlayerId;
|
||||
const QString target = cardName(targetPlayerId, targetZone, attachCard.target_card_id());
|
||||
addDetail(details,
|
||||
tr("attached %1 to %2.").arg(sourceText, target.isEmpty() ? tr("a card") : target),
|
||||
tr("ATTACH"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::CREATE_TOKEN: {
|
||||
const Event_CreateToken &createToken = event.GetExtension(Event_CreateToken::ext);
|
||||
const QString name = QString::fromStdString(createToken.card_name());
|
||||
rememberCard(eventPlayerId, QString::fromStdString(createToken.zone_name()), createToken.card_id(),
|
||||
name);
|
||||
const QString pt = QString::fromStdString(createToken.pt());
|
||||
addDetail(
|
||||
details,
|
||||
tr("created a token: %1%2.")
|
||||
.arg(name.isEmpty() ? tr("token") : name, pt.isEmpty() ? QString() : tr(" (%1)").arg(pt)),
|
||||
tr("TOKEN"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::SET_CARD_ATTR: {
|
||||
const Event_SetCardAttr &setCardAttr = event.GetExtension(Event_SetCardAttr::ext);
|
||||
addDetail(details, describeSetCardAttribute(setCardAttr, eventPlayerId),
|
||||
badgeForCardAttribute(setCardAttr.attribute()), ReplayEventKind::Action, eventPlayerId,
|
||||
forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::SET_CARD_COUNTER: {
|
||||
const Event_SetCardCounter &setCardCounter = event.GetExtension(Event_SetCardCounter::ext);
|
||||
const QString name = cardName(eventPlayerId, QString::fromStdString(setCardCounter.zone_name()),
|
||||
setCardCounter.card_id());
|
||||
addDetail(
|
||||
details,
|
||||
tr("set a counter on %1 to %2.")
|
||||
.arg(name.isEmpty() ? tr("a card") : name, QString::number(setCardCounter.counter_value())),
|
||||
tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::CREATE_COUNTER: {
|
||||
const Event_CreateCounter &createCounter = event.GetExtension(Event_CreateCounter::ext);
|
||||
const ServerInfo_Counter &counter = createCounter.counter_info();
|
||||
rememberCounter(eventPlayerId, counter);
|
||||
const QString name = QString::fromStdString(counter.name());
|
||||
addDetail(details,
|
||||
tr("created the %1 counter (%2).")
|
||||
.arg(name.isEmpty() ? tr("new") : name, QString::number(counter.count())),
|
||||
tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::SET_COUNTER: {
|
||||
const Event_SetCounter &setCounter = event.GetExtension(Event_SetCounter::ext);
|
||||
const QString name = counterName(eventPlayerId, setCounter.counter_id());
|
||||
addDetail(details,
|
||||
tr("set %1 to %2.")
|
||||
.arg(name.isEmpty() ? tr("a counter") : tr("the %1 counter").arg(name),
|
||||
QString::number(setCounter.value())),
|
||||
tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::DEL_COUNTER: {
|
||||
const Event_DelCounter &delCounter = event.GetExtension(Event_DelCounter::ext);
|
||||
const QString name = counterName(eventPlayerId, delCounter.counter_id());
|
||||
counterNames.remove(counterKey(eventPlayerId, delCounter.counter_id()));
|
||||
addDetail(details,
|
||||
tr("removed %1.").arg(name.isEmpty() ? tr("a counter") : tr("the %1 counter").arg(name)),
|
||||
tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::DUMP_ZONE: {
|
||||
const Event_DumpZone &dumpZone = event.GetExtension(Event_DumpZone::ext);
|
||||
const QString zoneOwnerPhrase = zonePhrase(
|
||||
dumpZone.zone_owner_id(), QString::fromStdString(dumpZone.zone_name()), eventPlayerId);
|
||||
if (dumpZone.number_cards() < 0) {
|
||||
addDetail(details, tr("looked at %1.").arg(zoneOwnerPhrase), tr("LOOK"),
|
||||
ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
} else {
|
||||
addDetail(details,
|
||||
tr("looked at %1 from %2.")
|
||||
.arg(formatCardList(QStringList(), dumpZone.number_cards()), zoneOwnerPhrase),
|
||||
tr("LOOK"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameEvent::CHANGE_ZONE_PROPERTIES: {
|
||||
const Event_ChangeZoneProperties &changeZone = event.GetExtension(Event_ChangeZoneProperties::ext);
|
||||
const QString zone = QString::fromStdString(changeZone.zone_name());
|
||||
QStringList changes;
|
||||
if (changeZone.always_reveal_top_card()) {
|
||||
changes.append(tr("top card revealed to all"));
|
||||
}
|
||||
if (changeZone.always_look_at_top_card()) {
|
||||
changes.append(tr("top card visible to its owner"));
|
||||
}
|
||||
addDetail(
|
||||
details,
|
||||
changes.isEmpty()
|
||||
? tr("changed the properties of %1.").arg(zonePhrase(eventPlayerId, zone, eventPlayerId))
|
||||
: tr("changed %1: %2.")
|
||||
.arg(zonePhrase(eventPlayerId, zone, eventPlayerId),
|
||||
changes.join(QStringLiteral(", "))),
|
||||
tr("ZONE"), ReplayEventKind::Action, eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
case GameEvent::GAME_LOG_NOTICE: {
|
||||
const Event_GameLogNotice ¬ice = event.GetExtension(Event_GameLogNotice::ext);
|
||||
switch (notice.notice_type()) {
|
||||
case Event_GameLogNotice::UNDO_DRAW_FAILED:
|
||||
addDetail(details, tr("failed to undo a draw."), tr("NOTICE"), ReplayEventKind::System,
|
||||
eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
default:
|
||||
addDetail(details, tr("A game notice was logged."), tr("NOTICE"), ReplayEventKind::System,
|
||||
-1, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameEvent::CREATE_ARROW:
|
||||
case GameEvent::DELETE_ARROW:
|
||||
// Arrows are transient visual annotations and would make the history very noisy.
|
||||
break;
|
||||
default:
|
||||
addDetail(details, tr("Unrecognized game event."), tr("EVENT"), ReplayEventKind::Action,
|
||||
eventPlayerId, forcedByJudge);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
QList<ReplayEventEntry> buildReplayEventEntries(const GameReplay *replay, const QList<int> &timeline)
|
||||
{
|
||||
if (!replay) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return ReplayEventIndexBuilder(replay, timeline).build();
|
||||
}
|
||||
43
cockatrice/src/interface/widgets/replay/replay_event_index.h
Normal file
43
cockatrice/src/interface/widgets/replay/replay_event_index.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#ifndef COCKATRICE_REPLAY_EVENT_INDEX_H
|
||||
#define COCKATRICE_REPLAY_EVENT_INDEX_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
class GameReplay;
|
||||
|
||||
enum class ReplayEventKind
|
||||
{
|
||||
Action,
|
||||
Turn,
|
||||
Phase,
|
||||
Chat,
|
||||
System
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief One user-visible entry in the replay history sidebar.
|
||||
*
|
||||
* The event index refers to the corresponding GameEventContainer in the replay. It is used to seek the replay to that
|
||||
* container.
|
||||
*/
|
||||
struct ReplayEventEntry
|
||||
{
|
||||
int eventIndex = -1;
|
||||
int timeMs = 0;
|
||||
QString timeLabel;
|
||||
QString actor;
|
||||
QString badge;
|
||||
QString detail;
|
||||
ReplayEventKind kind = ReplayEventKind::Action;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builds a compact, human-readable index of the events in a replay.
|
||||
*
|
||||
* The returned entries copy all the data they need, so the caller does not need to keep the replay object alive after
|
||||
* this function returns.
|
||||
*/
|
||||
QList<ReplayEventEntry> buildReplayEventEntries(const GameReplay *replay, const QList<int> &timeline);
|
||||
|
||||
#endif // COCKATRICE_REPLAY_EVENT_INDEX_H
|
||||
|
|
@ -57,7 +57,7 @@ ReplayManager::ReplayManager(QObject *parent, GameReplay *replay)
|
|||
|
||||
rewindBufferingTimer = new QTimer(this);
|
||||
rewindBufferingTimer->setSingleShot(true);
|
||||
connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayManager::processRewind);
|
||||
connect(rewindBufferingTimer, &QTimer::timeout, this, [this] { processRewind(); });
|
||||
}
|
||||
|
||||
ReplayManager::~ReplayManager()
|
||||
|
|
@ -65,7 +65,7 @@ ReplayManager::~ReplayManager()
|
|||
delete replay;
|
||||
}
|
||||
|
||||
void ReplayManager::skipToTime(int newTime, bool doRewindBuffering)
|
||||
void ReplayManager::skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime)
|
||||
{
|
||||
// check boundary conditions
|
||||
if (newTime < 0) {
|
||||
|
|
@ -75,15 +75,17 @@ void ReplayManager::skipToTime(int newTime, bool doRewindBuffering)
|
|||
newTime = maxTime;
|
||||
}
|
||||
|
||||
newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval
|
||||
if (!includeEventsAtTime) {
|
||||
newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval
|
||||
}
|
||||
|
||||
const bool isBackwardsSkip = newTime < currentProcessedTime;
|
||||
currentVisualTime = newTime;
|
||||
|
||||
if (isBackwardsSkip) {
|
||||
handleBackwardsSkip(doRewindBuffering);
|
||||
handleBackwardsSkip(doRewindBuffering, includeEventsAtTime);
|
||||
} else {
|
||||
processNewEvents(FORWARD_SKIP);
|
||||
processNewEvents(FORWARD_SKIP, includeEventsAtTime);
|
||||
}
|
||||
|
||||
timeChanged(currentVisualTime);
|
||||
|
|
@ -95,7 +97,7 @@ void ReplayManager::skipToTime(int newTime, bool doRewindBuffering)
|
|||
* @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind
|
||||
* is processed at the end. When false, the backwards skip will always cause an immediate rewind.
|
||||
*/
|
||||
void ReplayManager::handleBackwardsSkip(bool doRewindBuffering)
|
||||
void ReplayManager::handleBackwardsSkip(bool doRewindBuffering, bool includeEventsAtTime)
|
||||
{
|
||||
if (doRewindBuffering) {
|
||||
// We use a one-shot timer to implement the rewind buffering.
|
||||
|
|
@ -105,11 +107,11 @@ void ReplayManager::handleBackwardsSkip(bool doRewindBuffering)
|
|||
rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs());
|
||||
} else {
|
||||
// otherwise, process the rewind immediately
|
||||
processRewind();
|
||||
processRewind(includeEventsAtTime);
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayManager::processRewind()
|
||||
void ReplayManager::processRewind(bool includeEventsAtTime)
|
||||
{
|
||||
// stop any queued-up rewinds
|
||||
rewindBufferingTimer->stop();
|
||||
|
|
@ -117,7 +119,7 @@ void ReplayManager::processRewind()
|
|||
// process the rewind
|
||||
currentEvent = 0;
|
||||
emit rewound();
|
||||
processNewEvents(BACKWARD_SKIP);
|
||||
processNewEvents(BACKWARD_SKIP, includeEventsAtTime);
|
||||
}
|
||||
|
||||
void ReplayManager::replayTimerTimeout()
|
||||
|
|
@ -134,11 +136,13 @@ void ReplayManager::replayTimerTimeout()
|
|||
}
|
||||
|
||||
/** @brief Processes all unprocessed events up to the current time. */
|
||||
void ReplayManager::processNewEvents(PlaybackMode playbackMode)
|
||||
void ReplayManager::processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime)
|
||||
{
|
||||
currentProcessedTime = currentVisualTime;
|
||||
|
||||
while (currentEvent < replayTimeline.size() && replayTimeline[currentEvent] < currentProcessedTime) {
|
||||
while (currentEvent < replayTimeline.size() &&
|
||||
(replayTimeline[currentEvent] < currentProcessedTime ||
|
||||
(includeEventsAtTime && replayTimeline[currentEvent] == currentProcessedTime))) {
|
||||
EventProcessingOptions options;
|
||||
|
||||
// backwards skip => always skip reveal windows
|
||||
|
|
@ -236,6 +240,24 @@ void ReplayManager::setTime(int time)
|
|||
skipToTime(time, false);
|
||||
}
|
||||
|
||||
void ReplayManager::setTimeToEvent(int eventIndex)
|
||||
{
|
||||
if (eventIndex < 0 || eventIndex >= replayTimeline.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Flush a pending buffered rewind before seeking, otherwise its timer fires after this seek and rebuilds the
|
||||
// replay state from an outdated currentVisualTime, dropping the event we seek to. Stopping the timer instead
|
||||
// of processing it is not enough: playback ticks advance currentProcessedTime without rewinding currentEvent,
|
||||
// so the board could still be ahead of the target.
|
||||
if (rewindBufferingTimer->isActive()) {
|
||||
processRewind();
|
||||
}
|
||||
|
||||
// Unlike a timeline seek, jumping to an event should apply that event immediately.
|
||||
skipToTime(replayTimeline.at(eventIndex), false, true);
|
||||
}
|
||||
|
||||
void ReplayManager::skipByAmount(int amount)
|
||||
{
|
||||
skipToTime(currentVisualTime + amount, amount < 0);
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ class ReplayManager : public QObject
|
|||
///< to rewind buffering
|
||||
int currentEvent = 0; ///< current event's index
|
||||
|
||||
void skipToTime(int newTime, bool doRewindBuffering);
|
||||
void handleBackwardsSkip(bool doRewindBuffering);
|
||||
void processRewind();
|
||||
void processNewEvents(PlaybackMode playbackMode);
|
||||
void skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime = false);
|
||||
void handleBackwardsSkip(bool doRewindBuffering, bool includeEventsAtTime = false);
|
||||
void processRewind(bool includeEventsAtTime = false);
|
||||
void processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime = false);
|
||||
void handleSkipEmptySection();
|
||||
|
||||
private slots:
|
||||
|
|
@ -73,6 +73,7 @@ public slots:
|
|||
void startReplay();
|
||||
void stopReplay();
|
||||
void setTime(int time);
|
||||
void setTimeToEvent(int eventIndex);
|
||||
void skipByAmount(int amount); // use a negative amount to skip backwards
|
||||
|
||||
signals:
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
|
|||
connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed);
|
||||
connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished);
|
||||
connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound);
|
||||
connect(replayManager, &ReplayManager::timeChanged, this, &ReplayWidget::timeChanged);
|
||||
|
||||
// timeline widget
|
||||
timelineWidget = new ReplayTimelineWidget;
|
||||
|
|
@ -96,6 +97,16 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
|
|||
refreshShortcuts();
|
||||
}
|
||||
|
||||
const QList<int> &ReplayWidget::getReplayTimeline() const
|
||||
{
|
||||
return replayManager->getReplayTimeline();
|
||||
}
|
||||
|
||||
void ReplayWidget::seekToEvent(int eventIndex)
|
||||
{
|
||||
replayManager->setTimeToEvent(eventIndex);
|
||||
}
|
||||
|
||||
void ReplayWidget::replayFinished()
|
||||
{
|
||||
replayPlayButton->setChecked(false);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#ifndef REPLAY_WIDGET_H
|
||||
#define REPLAY_WIDGET_H
|
||||
|
||||
#include <QList>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/game/player/event_processing_options.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
|
@ -35,8 +36,14 @@ public:
|
|||
*/
|
||||
ReplayWidget(QWidget *parent, GameReplay *replay);
|
||||
|
||||
[[nodiscard]] const QList<int> &getReplayTimeline() const;
|
||||
|
||||
public slots:
|
||||
void seekToEvent(int eventIndex);
|
||||
|
||||
signals:
|
||||
void rewound();
|
||||
void timeChanged(int time);
|
||||
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
|
||||
|
||||
private:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue