mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 10:23:02 -07:00
Merge branch 'master' into tooomm-qt5
This commit is contained in:
commit
3bc08ef94c
357 changed files with 23069 additions and 2916 deletions
63
cockatrice/src/client/lag_monitor.cpp
Normal file
63
cockatrice/src/client/lag_monitor.cpp
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#include "lag_monitor.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QEvent>
|
||||
#include <QTimer>
|
||||
|
||||
LagMonitor::LagMonitor(QObject *parent) : QObject(parent)
|
||||
{
|
||||
qApp->installEventFilter(this);
|
||||
|
||||
timer = new QTimer(this);
|
||||
timer->setInterval(TICK_INTERVAL_MS);
|
||||
connect(timer, &QTimer::timeout, this, &LagMonitor::checkTick);
|
||||
tickClock.start();
|
||||
timer->start();
|
||||
}
|
||||
|
||||
QList<LagMonitor::StallRecord> LagMonitor::recentStalls() const
|
||||
{
|
||||
return stalls;
|
||||
}
|
||||
|
||||
void LagMonitor::clearStalls()
|
||||
{
|
||||
stalls.clear();
|
||||
}
|
||||
|
||||
bool LagMonitor::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::ApplicationStateChange) {
|
||||
// The transition may span a suspend or an arbitrary unfocused period;
|
||||
// discard the gap so it cannot be mistaken for a stall.
|
||||
tickClock.restart();
|
||||
}
|
||||
return QObject::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void LagMonitor::checkTick()
|
||||
{
|
||||
recordGap(tickClock.restart());
|
||||
}
|
||||
|
||||
void LagMonitor::recordGap(qint64 gapMs)
|
||||
{
|
||||
if (gapMs <= STALL_THRESHOLD_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gapMs > MAX_PLAUSIBLE_STALL_MS) {
|
||||
qCDebug(LagMonitorLog, "Ignoring implausible %lld ms gap (likely suspend)", static_cast<long long>(gapMs));
|
||||
return;
|
||||
}
|
||||
|
||||
const StallRecord record{.timestampMsSinceEpoch = QDateTime::currentMSecsSinceEpoch(), .durationMs = gapMs};
|
||||
|
||||
stalls.append(record);
|
||||
while (stalls.size() > MAX_RECORDED_STALLS) {
|
||||
stalls.removeFirst();
|
||||
}
|
||||
|
||||
qCWarning(LagMonitorLog, "Event loop stalled for %lld ms (threshold: %d ms)", static_cast<long long>(gapMs),
|
||||
STALL_THRESHOLD_MS);
|
||||
}
|
||||
87
cockatrice/src/client/lag_monitor.h
Normal file
87
cockatrice/src/client/lag_monitor.h
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* @file lag_monitor.h
|
||||
* @ingroup Client
|
||||
*/
|
||||
|
||||
#ifndef LAG_MONITOR_H
|
||||
#define LAG_MONITOR_H
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QElapsedTimer>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(LagMonitorLog, "lag_monitor");
|
||||
|
||||
class QEvent;
|
||||
class QTimer;
|
||||
|
||||
/**
|
||||
* @brief Detects main-thread event loop stalls ("UI freezes") from the inside.
|
||||
*
|
||||
* A timer is expected to fire every TICK_INTERVAL_MS of wall time. When the
|
||||
* observed gap greatly exceeds that interval, some other task blocked the
|
||||
* event loop for roughly the overshooting duration. This is what separates
|
||||
* "my client froze" from "the network is lagging" in user reports.
|
||||
*
|
||||
* Gaps that span an application state change (suspend, minimize, focus
|
||||
* loss) are discarded, and implausibly huge gaps are dropped, so operating
|
||||
* system power events do not fabricate stalls. This handling is load-bearing
|
||||
* on Windows, where the monotonic clock used by Qt counts sleep time.
|
||||
*
|
||||
* Healthy operation costs one timer wakeup per tick and two integer
|
||||
* comparisons. Allocations happen only when a stall is actually recorded.
|
||||
*/
|
||||
class LagMonitor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct StallRecord
|
||||
{
|
||||
qint64 timestampMsSinceEpoch = 0; ///< when the stalled period ended
|
||||
qint64 durationMs = 0; ///< approximate length of the freeze; measured tick to tick, so it can exceed the true
|
||||
///< stall by up to TICK_INTERVAL_MS
|
||||
};
|
||||
|
||||
static constexpr int TICK_INTERVAL_MS = 500;
|
||||
static constexpr int STALL_THRESHOLD_MS = 2000;
|
||||
static constexpr int MAX_RECORDED_STALLS = 32;
|
||||
|
||||
/// Gaps beyond this are treated as suspend artifacts rather than stalls.
|
||||
static constexpr qint64 MAX_PLAUSIBLE_STALL_MS = 600000;
|
||||
|
||||
explicit LagMonitor(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Stalls recorded during this session, oldest first.
|
||||
*
|
||||
* Intended consumers are log output and the diagnostics export. The list
|
||||
* holds at most MAX_RECORDED_STALLS entries.
|
||||
*/
|
||||
QList<StallRecord> recentStalls() const;
|
||||
|
||||
void clearStalls();
|
||||
|
||||
/**
|
||||
* @brief Feeds a measured tick-to-tick gap through the detection logic.
|
||||
*
|
||||
* Split out of checkTick so threshold, plausibility, and trim behavior
|
||||
* stay unit-testable without real timing.
|
||||
*/
|
||||
void recordGap(qint64 gapMs);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void checkTick();
|
||||
|
||||
private:
|
||||
QTimer *timer;
|
||||
QElapsedTimer tickClock; ///< monotonic clock, so wall clock steps do not fabricate stalls
|
||||
QList<StallRecord> stalls;
|
||||
};
|
||||
|
||||
#endif
|
||||
51
cockatrice/src/client/latency_graph_widget.cpp
Normal file
51
cockatrice/src/client/latency_graph_widget.cpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @file latency_graph_widget.cpp
|
||||
* @ingroup Client
|
||||
*/
|
||||
|
||||
#include "latency_graph_widget.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
LatencyGraphWidget::LatencyGraphWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void LatencyGraphWidget::setSamples(const QList<int> &samplesMs)
|
||||
{
|
||||
samples = samplesMs;
|
||||
update();
|
||||
}
|
||||
|
||||
void LatencyGraphWidget::paintEvent(QPaintEvent * /* event */)
|
||||
{
|
||||
if (samples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter painter(this);
|
||||
|
||||
// Heights are relative to the window's own worst sample (floored at
|
||||
// MinScaleMs) so the shape of the variance stays readable even when every
|
||||
// value is small.
|
||||
qint64 heightScaleMs = MinScaleMs;
|
||||
for (int sample : samples) {
|
||||
heightScaleMs = qMax(heightScaleMs, static_cast<qint64>(sample));
|
||||
}
|
||||
|
||||
const qreal widthPerBar = static_cast<qreal>(width()) / samples.size();
|
||||
for (int i = 0; i < samples.size(); ++i) {
|
||||
const qreal heightRatio = qBound(0.0, static_cast<double>(samples.at(i)) / heightScaleMs, 1.0);
|
||||
const qreal barHeight = heightRatio * height();
|
||||
|
||||
// Colors follow an absolute quality ramp: a steady good ping stays
|
||||
// green no matter how uniform the window is.
|
||||
const qreal colorRatio = qBound(0.0, static_cast<double>(samples.at(i)) / ColorScaleMs, 1.0);
|
||||
QColor color;
|
||||
color.setHsv(qRound(120.0 * (1.0 - colorRatio)), 255, 255);
|
||||
|
||||
const QRectF bar(static_cast<qreal>(i) * widthPerBar + 1.0, static_cast<qreal>(height()) - barHeight,
|
||||
qMax(1.0, widthPerBar - 2.0), barHeight);
|
||||
painter.fillRect(bar, color);
|
||||
}
|
||||
}
|
||||
43
cockatrice/src/client/latency_graph_widget.h
Normal file
43
cockatrice/src/client/latency_graph_widget.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* @file latency_graph_widget.h
|
||||
* @ingroup Client
|
||||
*/
|
||||
|
||||
#ifndef LATENCY_GRAPH_WIDGET_H
|
||||
#define LATENCY_GRAPH_WIDGET_H
|
||||
|
||||
#include <QList>
|
||||
#include <QWidget>
|
||||
|
||||
/**
|
||||
* @brief Bar graph of recent network round-trip samples.
|
||||
*
|
||||
* Draws one bar per sample, oldest on the left. Bar height is relative to the
|
||||
* window's own scale so the shape of the variance stays readable, while bar
|
||||
* color maps each sample onto an absolute quality ramp (green at rest through
|
||||
* red at ColorScaleMs) so a steady good ping never looks alarming. Size
|
||||
* agnostic: the status bar embeds a small instance while the latency detail
|
||||
* popup shows a large one.
|
||||
*/
|
||||
class LatencyGraphWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LatencyGraphWidget(QWidget *parent = nullptr);
|
||||
|
||||
/// Sample in milliseconds that maps to a fully red bar.
|
||||
static constexpr qint64 ColorScaleMs = 500;
|
||||
|
||||
void setSamples(const QList<int> &samplesMs);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
/// Floor of the vertical scale in milliseconds. Keeps small windows readable.
|
||||
static constexpr qint64 MinScaleMs = 100;
|
||||
|
||||
QList<int> samples;
|
||||
};
|
||||
|
||||
#endif
|
||||
111
cockatrice/src/client/latency_status_widget.cpp
Normal file
111
cockatrice/src/client/latency_status_widget.cpp
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* @file latency_status_widget.cpp
|
||||
* @ingroup Client
|
||||
*/
|
||||
|
||||
#include "latency_status_widget.h"
|
||||
|
||||
#include "latency_graph_widget.h"
|
||||
|
||||
#include <QEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
pingLabel = new QLabel(this);
|
||||
pingLabel->setAccessibleName(tr("Ping"));
|
||||
|
||||
latencyGraph = new LatencyGraphWidget(this);
|
||||
latencyGraph->setFixedSize(90, 14);
|
||||
|
||||
auto *layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(4);
|
||||
layout->addWidget(latencyGraph);
|
||||
layout->addWidget(pingLabel);
|
||||
|
||||
// Clicking anywhere in the area opens the detail view.
|
||||
for (QObject *child : QList<QObject *>{pingLabel, latencyGraph}) {
|
||||
child->installEventFilter(this);
|
||||
}
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
void LatencyStatusWidget::updateData(const LatencyTracker::Stats &stats, const QList<int> &samplesMs)
|
||||
{
|
||||
latestSamples = samplesMs;
|
||||
latencyGraph->setSamples(samplesMs);
|
||||
if (popup && popup->isVisible() && detailGraph) {
|
||||
detailGraph->setSamples(samplesMs);
|
||||
}
|
||||
|
||||
if (stats.sampleCount == 0) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString statsStr = statsText(stats);
|
||||
|
||||
pingLabel->setText(tr("Ping: %1 ms").arg(stats.lastMs));
|
||||
pingLabel->setToolTip(statsStr);
|
||||
pingLabel->setAccessibleDescription(statsStr);
|
||||
if (popup && popup->isVisible() && detailLabel) {
|
||||
detailLabel->setText(statsStr);
|
||||
}
|
||||
show();
|
||||
}
|
||||
|
||||
bool LatencyStatusWidget::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if ((watched == pingLabel || watched == latencyGraph) && event->type() == QEvent::MouseButtonPress) {
|
||||
togglePopup();
|
||||
return true;
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void LatencyStatusWidget::togglePopup()
|
||||
{
|
||||
if (!popup) {
|
||||
popup = new QWidget(this, Qt::Popup | Qt::FramelessWindowHint);
|
||||
auto *layout = new QVBoxLayout(popup);
|
||||
layout->setContentsMargins(8, 8, 8, 8);
|
||||
|
||||
detailLabel = new QLabel(popup);
|
||||
detailLabel->setAccessibleName(tr("Connection latency details"));
|
||||
detailLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
detailGraph = new LatencyGraphWidget(popup);
|
||||
detailGraph->setFixedSize(280, 80);
|
||||
|
||||
layout->addWidget(detailLabel, 0, Qt::AlignLeft);
|
||||
layout->addWidget(detailGraph, 0, Qt::AlignHCenter);
|
||||
}
|
||||
|
||||
if (popup->isVisible()) {
|
||||
popup->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Qt::Popup closes itself on any outside click, so just position and show.
|
||||
if (latestSamples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
detailGraph->setSamples(latestSamples);
|
||||
detailLabel->setText(pingLabel->toolTip());
|
||||
popup->adjustSize();
|
||||
const QPoint anchor = mapToGlobal(QPoint(width() / 2, 0));
|
||||
popup->move(anchor.x() - popup->width() / 2, anchor.y() - popup->height() - 6);
|
||||
popup->show();
|
||||
}
|
||||
|
||||
QString LatencyStatusWidget::statsText(const LatencyTracker::Stats &stats) const
|
||||
{
|
||||
return tr("Connection quality over the last %n sample(s):", "", stats.sampleCount) + "\n" +
|
||||
tr("Last: %1 ms").arg(stats.lastMs) + "\n" + tr("Median: %1 ms").arg(stats.medianMs) + "\n" +
|
||||
tr("95th percentile: %1 ms").arg(stats.p95Ms) + "\n" + tr("Maximum: %1 ms").arg(stats.maxMs);
|
||||
}
|
||||
50
cockatrice/src/client/latency_status_widget.h
Normal file
50
cockatrice/src/client/latency_status_widget.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @file latency_status_widget.h
|
||||
* @ingroup Client
|
||||
*/
|
||||
|
||||
#ifndef LATENCY_STATUS_WIDGET_H
|
||||
#define LATENCY_STATUS_WIDGET_H
|
||||
|
||||
#include <QList>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/network/client/abstract/latency_tracker.h>
|
||||
|
||||
class QLabel;
|
||||
class LatencyGraphWidget;
|
||||
|
||||
/**
|
||||
* @brief Status bar presentation of server round-trip health.
|
||||
*
|
||||
* Combines the textual "Ping" readout with a small LatencyGraphWidget
|
||||
* sparkline of the rolling sample window. Clicking anywhere in the area opens
|
||||
* a popup with a larger graph and the numeric statistics. It closes on any
|
||||
* outside click. Stays hidden while disconnected or before any samples exist.
|
||||
* Owns all latency display state so MainWindow only needs to forward one
|
||||
* signal here.
|
||||
*/
|
||||
class LatencyStatusWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LatencyStatusWidget(QWidget *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void updateData(const LatencyTracker::Stats &stats, const QList<int> &samplesMs);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
void togglePopup();
|
||||
QString statsText(const LatencyTracker::Stats &stats) const;
|
||||
|
||||
QLabel *pingLabel = nullptr;
|
||||
LatencyGraphWidget *latencyGraph = nullptr;
|
||||
QWidget *popup = nullptr;
|
||||
LatencyGraphWidget *detailGraph = nullptr;
|
||||
QLabel *detailLabel = nullptr;
|
||||
QList<int> latestSamples;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -44,6 +44,8 @@ void ConnectionController::wireClientSignals()
|
|||
|
||||
connect(remoteClient, &RemoteClient::statusChanged, this, &ConnectionController::onStatusChanged);
|
||||
|
||||
connect(remoteClient, &AbstractClient::pingStatsUpdated, this, &ConnectionController::pingStatsUpdated);
|
||||
|
||||
connect(remoteClient, &RemoteClient::userInfoChanged, this, &ConnectionController::onUserInfoReceived,
|
||||
Qt::BlockingQueuedConnection);
|
||||
|
||||
|
|
@ -296,6 +298,15 @@ void ConnectionController::onLoginError(int r,
|
|||
return;
|
||||
}
|
||||
|
||||
case Response::RespPasswordChangeRequired: {
|
||||
QMessageBox::information(
|
||||
dialogParent, tr("Password Change Required"),
|
||||
tr("An administrator has reset your password. Please contact your server administrator to obtain "
|
||||
"your temporary password, then log in and change it via Account -> Change Password."));
|
||||
remoteClient->disconnectFromServer();
|
||||
return;
|
||||
}
|
||||
|
||||
case Response::RespServerFull: {
|
||||
QMessageBox::critical(dialogParent, tr("Server Full"),
|
||||
tr("The server has reached its maximum user capacity, please check back later."));
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ signals:
|
|||
// action enable/disable logic
|
||||
void statusChanged(ClientStatus status);
|
||||
|
||||
// Forwarded from AbstractClient::pingStatsUpdated. See that signal for the
|
||||
// meaning of the parameters.
|
||||
void pingStatsUpdated(const LatencyTracker::Stats &stats, const QList<int> &samplesMs);
|
||||
|
||||
private slots:
|
||||
// Slots wired directly to RemoteClient signals
|
||||
void onStatusChanged(ClientStatus status);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList
|
|||
{
|
||||
auto copyIfNotAToken = [&destination](const auto node, const auto card) {
|
||||
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||
if (dbCard && !dbCard->getIsToken()) {
|
||||
if (dbCard && !dbCard->getIsToken() && node->getName() != DECK_ZONE_MAYBEBOARD) {
|
||||
DecklistCardNode *addedCard = destination.addCard(card->getName(), node->getName(), -1);
|
||||
addedCard->setNumber(card->getNumber());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi
|
|||
{
|
||||
auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) {
|
||||
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||
if (!dbCard || dbCard->getIsToken()) {
|
||||
if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -786,6 +786,10 @@ private:
|
|||
ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabLogs",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabReport",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabModeration",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Moderation"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue