mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2025-04-30 06:34:27 +02:00
Show online players on servers screen (#3112)
This commit is contained in:
commit
d0b9073f60
@ -918,6 +918,12 @@ SET(LAUNCHER_SOURCES
|
|||||||
ui/pages/instance/ServersPage.h
|
ui/pages/instance/ServersPage.h
|
||||||
ui/pages/instance/WorldListPage.cpp
|
ui/pages/instance/WorldListPage.cpp
|
||||||
ui/pages/instance/WorldListPage.h
|
ui/pages/instance/WorldListPage.h
|
||||||
|
ui/pages/instance/McClient.cpp
|
||||||
|
ui/pages/instance/McClient.h
|
||||||
|
ui/pages/instance/McResolver.cpp
|
||||||
|
ui/pages/instance/McResolver.h
|
||||||
|
ui/pages/instance/ServerPingTask.cpp
|
||||||
|
ui/pages/instance/ServerPingTask.h
|
||||||
|
|
||||||
# GUI - global settings pages
|
# GUI - global settings pages
|
||||||
ui/pages/global/AccountListPage.cpp
|
ui/pages/global/AccountListPage.cpp
|
||||||
|
@ -43,6 +43,10 @@
|
|||||||
|
|
||||||
#include "tasks/Task.h"
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Runs a list of tasks concurrently (according to `max_concurrent` parameter).
|
||||||
|
* Behaviour is the same as regular Task (e.g. starts using start())
|
||||||
|
*/
|
||||||
class ConcurrentTask : public Task {
|
class ConcurrentTask : public Task {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
@ -59,6 +63,7 @@ class ConcurrentTask : public Task {
|
|||||||
inline auto isMultiStep() const -> bool override { return totalSize() > 1; }
|
inline auto isMultiStep() const -> bool override { return totalSize() > 1; }
|
||||||
auto getStepProgress() const -> TaskStepProgressList override;
|
auto getStepProgress() const -> TaskStepProgressList override;
|
||||||
|
|
||||||
|
//! Adds a task to execute in this ConcurrentTask
|
||||||
void addTask(Task::Ptr task);
|
void addTask(Task::Ptr task);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
@ -79,6 +79,13 @@ Q_DECLARE_METATYPE(TaskStepProgress)
|
|||||||
|
|
||||||
using TaskStepProgressList = QList<std::shared_ptr<TaskStepProgress>>;
|
using TaskStepProgressList = QList<std::shared_ptr<TaskStepProgress>>;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Represents a task that has to be done.
|
||||||
|
* To create a task, you need to subclass this class, implement the executeTask() method and call
|
||||||
|
* emitSucceeded() or emitFailed() when the task is done.
|
||||||
|
* the caller needs to call start() to start the task.
|
||||||
|
*/
|
||||||
class Task : public QObject, public QRunnable {
|
class Task : public QObject, public QRunnable {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
@ -130,23 +137,27 @@ class Task : public QObject, public QRunnable {
|
|||||||
signals:
|
signals:
|
||||||
void started();
|
void started();
|
||||||
void progress(qint64 current, qint64 total);
|
void progress(qint64 current, qint64 total);
|
||||||
|
//! called when a task has either succeeded, aborted or failed.
|
||||||
void finished();
|
void finished();
|
||||||
|
//! called when a task has succeeded
|
||||||
void succeeded();
|
void succeeded();
|
||||||
|
//! called when a task has been aborted by calling abort()
|
||||||
void aborted();
|
void aborted();
|
||||||
void failed(QString reason);
|
void failed(QString reason);
|
||||||
void status(QString status);
|
void status(QString status);
|
||||||
void details(QString details);
|
void details(QString details);
|
||||||
void stepProgress(TaskStepProgress const& task_progress);
|
void stepProgress(TaskStepProgress const& task_progress);
|
||||||
|
|
||||||
/** Emitted when the canAbort() status has changed.
|
//! Emitted when the canAbort() status has changed. */
|
||||||
*/
|
|
||||||
void abortStatusChanged(bool can_abort);
|
void abortStatusChanged(bool can_abort);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
// QRunnable's interface
|
// QRunnable's interface
|
||||||
void run() override { start(); }
|
void run() override { start(); }
|
||||||
|
|
||||||
|
//! used by the task caller to start the task
|
||||||
virtual void start();
|
virtual void start();
|
||||||
|
//! used by external code to ask the task to abort
|
||||||
virtual bool abort()
|
virtual bool abort()
|
||||||
{
|
{
|
||||||
if (canAbort())
|
if (canAbort())
|
||||||
@ -161,11 +172,16 @@ class Task : public QObject, public QRunnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
//! The task subclass must implement this method. This method is called to start to run the task.
|
||||||
|
//! The task is not finished when this method returns. the subclass must manually call emitSucceeded() or emitFailed() instead.
|
||||||
virtual void executeTask() = 0;
|
virtual void executeTask() = 0;
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
|
//! The Task subclass must call this method when the task has succeeded
|
||||||
virtual void emitSucceeded();
|
virtual void emitSucceeded();
|
||||||
|
//! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead.
|
||||||
virtual void emitAborted();
|
virtual void emitAborted();
|
||||||
|
//! The Task subclass must call this method when the task has failed
|
||||||
virtual void emitFailed(QString reason = "");
|
virtual void emitFailed(QString reason = "");
|
||||||
|
|
||||||
virtual void propagateStepProgress(TaskStepProgress const& task_progress);
|
virtual void propagateStepProgress(TaskStepProgress const& task_progress);
|
||||||
|
164
launcher/ui/pages/instance/McClient.cpp
Normal file
164
launcher/ui/pages/instance/McClient.cpp
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
#include <QObject>
|
||||||
|
#include <QTcpSocket>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <qtconcurrentrun.h>
|
||||||
|
|
||||||
|
#include <Exception.h>
|
||||||
|
#include "McClient.h"
|
||||||
|
#include "Json.h"
|
||||||
|
|
||||||
|
// 7 first bits
|
||||||
|
#define SEGMENT_BITS 0x7F
|
||||||
|
// last bit
|
||||||
|
#define CONTINUE_BIT 0x80
|
||||||
|
|
||||||
|
McClient::McClient(QObject *parent, QString domain, QString ip, short port): QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {}
|
||||||
|
|
||||||
|
void McClient::getStatusData() {
|
||||||
|
qDebug() << "Connecting to socket..";
|
||||||
|
|
||||||
|
connect(&m_socket, &QTcpSocket::connected, this, [this]() {
|
||||||
|
qDebug() << "Connected to socket successfully";
|
||||||
|
sendRequest();
|
||||||
|
|
||||||
|
connect(&m_socket, &QTcpSocket::readyRead, this, &McClient::readRawResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() {
|
||||||
|
emitFail("Socket disconnected: " + m_socket.errorString());
|
||||||
|
});
|
||||||
|
|
||||||
|
m_socket.connectToHost(m_ip, m_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
void McClient::sendRequest() {
|
||||||
|
QByteArray data;
|
||||||
|
writeVarInt(data, 0x00); // packet ID
|
||||||
|
writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1)
|
||||||
|
writeVarInt(data, m_domain.size()); // server address length
|
||||||
|
writeString(data, m_domain.toStdString()); // server address
|
||||||
|
writeFixedInt(data, m_port, 2); // server port
|
||||||
|
writeVarInt(data, 0x01); // next state
|
||||||
|
writePacketToSocket(data); // send handshake packet
|
||||||
|
|
||||||
|
writeVarInt(data, 0x00); // packet ID
|
||||||
|
writePacketToSocket(data); // send status packet
|
||||||
|
}
|
||||||
|
|
||||||
|
void McClient::readRawResponse() {
|
||||||
|
if (m_responseReadState == 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_resp.append(m_socket.readAll());
|
||||||
|
if (m_responseReadState == 0 && m_resp.size() >= 5) {
|
||||||
|
m_wantedRespLength = readVarInt(m_resp);
|
||||||
|
m_responseReadState = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) {
|
||||||
|
if (m_resp.size() > m_wantedRespLength) {
|
||||||
|
qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " << m_resp.size() << " received)";
|
||||||
|
}
|
||||||
|
parseResponse();
|
||||||
|
m_responseReadState = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void McClient::parseResponse() {
|
||||||
|
qDebug() << "Received response successfully";
|
||||||
|
|
||||||
|
int packetID = readVarInt(m_resp);
|
||||||
|
if (packetID != 0x00) {
|
||||||
|
throw Exception(
|
||||||
|
QString("Packet ID doesn't match expected value (0x00 vs 0x%1)")
|
||||||
|
.arg(packetID, 0, 16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_UNUSED(readVarInt(m_resp)); // json length
|
||||||
|
|
||||||
|
// 'resp' should now be the JSON string
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(m_resp);
|
||||||
|
emitSucceed(doc.object());
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://wiki.vg/Protocol#VarInt_and_VarLong
|
||||||
|
void McClient::writeVarInt(QByteArray &data, int value) {
|
||||||
|
while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits
|
||||||
|
// Write 7 bits
|
||||||
|
data.append((value & SEGMENT_BITS) | CONTINUE_BIT);
|
||||||
|
|
||||||
|
// Erase theses 7 bits from the value to write
|
||||||
|
// Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
|
||||||
|
value >>= 7;
|
||||||
|
}
|
||||||
|
data.append(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://wiki.vg/Protocol#VarInt_and_VarLong
|
||||||
|
int McClient::readVarInt(QByteArray &data) {
|
||||||
|
int value = 0;
|
||||||
|
int position = 0;
|
||||||
|
char currentByte;
|
||||||
|
|
||||||
|
while (position < 32) {
|
||||||
|
currentByte = readByte(data);
|
||||||
|
value |= (currentByte & SEGMENT_BITS) << position;
|
||||||
|
|
||||||
|
if ((currentByte & CONTINUE_BIT) == 0) break;
|
||||||
|
|
||||||
|
position += 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position >= 32) throw Exception("VarInt is too big");
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
char McClient::readByte(QByteArray &data) {
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
throw Exception("No more bytes to read");
|
||||||
|
}
|
||||||
|
|
||||||
|
char byte = data.at(0);
|
||||||
|
data.remove(0, 1);
|
||||||
|
return byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write number with specified size in big endian format
|
||||||
|
void McClient::writeFixedInt(QByteArray &data, int value, int size) {
|
||||||
|
for (int i = size - 1; i >= 0; i--) {
|
||||||
|
data.append((value >> (i * 8)) & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void McClient::writeString(QByteArray &data, const std::string &value) {
|
||||||
|
data.append(value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void McClient::writePacketToSocket(QByteArray &data) {
|
||||||
|
// we prefix the packet with its length
|
||||||
|
QByteArray dataWithSize;
|
||||||
|
writeVarInt(dataWithSize, data.size());
|
||||||
|
dataWithSize.append(data);
|
||||||
|
|
||||||
|
// write it to the socket
|
||||||
|
m_socket.write(dataWithSize);
|
||||||
|
m_socket.flush();
|
||||||
|
|
||||||
|
data.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void McClient::emitFail(QString error) {
|
||||||
|
qDebug() << "Minecraft server ping for status error:" << error;
|
||||||
|
emit failed(error);
|
||||||
|
emit finished();
|
||||||
|
}
|
||||||
|
|
||||||
|
void McClient::emitSucceed(QJsonObject data) {
|
||||||
|
emit succeeded(data);
|
||||||
|
emit finished();
|
||||||
|
}
|
51
launcher/ui/pages/instance/McClient.h
Normal file
51
launcher/ui/pages/instance/McClient.h
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#include <QObject>
|
||||||
|
#include <QTcpSocket>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QFuture>
|
||||||
|
|
||||||
|
#include <Exception.h>
|
||||||
|
|
||||||
|
// Client for the Minecraft protocol
|
||||||
|
class McClient : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QString m_domain;
|
||||||
|
QString m_ip;
|
||||||
|
short m_port;
|
||||||
|
QTcpSocket m_socket;
|
||||||
|
|
||||||
|
// 0: did not start reading the response yet
|
||||||
|
// 1: read the response length, still reading the response
|
||||||
|
// 2: finished reading the response
|
||||||
|
unsigned m_responseReadState = 0;
|
||||||
|
unsigned m_wantedRespLength = 0;
|
||||||
|
QByteArray m_resp;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit McClient(QObject *parent, QString domain, QString ip, short port);
|
||||||
|
//! Read status data of the server, and calls the succeeded() signal with the parsed JSON data
|
||||||
|
void getStatusData();
|
||||||
|
private:
|
||||||
|
void sendRequest();
|
||||||
|
//! Accumulate data until we have a full response, then call parseResponse() once
|
||||||
|
void readRawResponse();
|
||||||
|
void parseResponse();
|
||||||
|
|
||||||
|
void writeVarInt(QByteArray &data, int value);
|
||||||
|
int readVarInt(QByteArray &data);
|
||||||
|
char readByte(QByteArray &data);
|
||||||
|
//! write number with specified size in big endian format
|
||||||
|
void writeFixedInt(QByteArray &data, int value, int size);
|
||||||
|
void writeString(QByteArray &data, const std::string &value);
|
||||||
|
|
||||||
|
void writePacketToSocket(QByteArray &data);
|
||||||
|
|
||||||
|
void emitFail(QString error);
|
||||||
|
void emitSucceed(QJsonObject data);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void succeeded(QJsonObject data);
|
||||||
|
void failed(QString error);
|
||||||
|
void finished();
|
||||||
|
};
|
73
launcher/ui/pages/instance/McResolver.cpp
Normal file
73
launcher/ui/pages/instance/McResolver.cpp
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#include <QObject>
|
||||||
|
#include <QDnsLookup>
|
||||||
|
#include <QtNetwork/qtcpsocket.h>
|
||||||
|
#include <QHostInfo>
|
||||||
|
|
||||||
|
#include "McResolver.h"
|
||||||
|
|
||||||
|
McResolver::McResolver(QObject *parent, QString domain, int port): QObject(parent), m_constrDomain(domain), m_constrPort(port) {}
|
||||||
|
|
||||||
|
void McResolver::ping() {
|
||||||
|
pingWithDomainSRV(m_constrDomain, m_constrPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
void McResolver::pingWithDomainSRV(QString domain, int port) {
|
||||||
|
QDnsLookup *lookup = new QDnsLookup(this);
|
||||||
|
lookup->setName(QString("_minecraft._tcp.%1").arg(domain));
|
||||||
|
lookup->setType(QDnsLookup::SRV);
|
||||||
|
|
||||||
|
connect(lookup, &QDnsLookup::finished, this, [this, domain, port]() {
|
||||||
|
QDnsLookup *lookup = qobject_cast<QDnsLookup *>(sender());
|
||||||
|
|
||||||
|
lookup->deleteLater();
|
||||||
|
|
||||||
|
if (lookup->error() != QDnsLookup::NoError) {
|
||||||
|
qDebug() << QString("Warning: SRV record lookup failed (%1), trying A record lookup").arg(lookup->errorString());
|
||||||
|
pingWithDomainA(domain, port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto records = lookup->serviceRecords();
|
||||||
|
if (records.isEmpty()) {
|
||||||
|
qDebug() << "Warning: no SRV entries found for domain, trying A record lookup";
|
||||||
|
pingWithDomainA(domain, port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& firstRecord = records.at(0);
|
||||||
|
QString domain = firstRecord.target();
|
||||||
|
int port = firstRecord.port();
|
||||||
|
pingWithDomainA(domain, port);
|
||||||
|
});
|
||||||
|
|
||||||
|
lookup->lookup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void McResolver::pingWithDomainA(QString domain, int port) {
|
||||||
|
QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo &hostInfo){
|
||||||
|
if (hostInfo.error() != QHostInfo::NoError) {
|
||||||
|
emitFail("A record lookup failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto records = hostInfo.addresses();
|
||||||
|
if (records.isEmpty()) {
|
||||||
|
emitFail("No A entries found for domain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& firstRecord = records.at(0);
|
||||||
|
emitSucceed(firstRecord.toString(), port);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void McResolver::emitFail(QString error) {
|
||||||
|
qDebug() << "DNS resolver error:" << error;
|
||||||
|
emit failed(error);
|
||||||
|
emit finished();
|
||||||
|
}
|
||||||
|
|
||||||
|
void McResolver::emitSucceed(QString ip, int port) {
|
||||||
|
emit succeeded(ip, port);
|
||||||
|
emit finished();
|
||||||
|
}
|
28
launcher/ui/pages/instance/McResolver.h
Normal file
28
launcher/ui/pages/instance/McResolver.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QDnsLookup>
|
||||||
|
#include <QtNetwork/qtcpsocket.h>
|
||||||
|
#include <QHostInfo>
|
||||||
|
|
||||||
|
// resolve the IP and port of a Minecraft server
|
||||||
|
class McResolver : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QString m_constrDomain;
|
||||||
|
int m_constrPort;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit McResolver(QObject *parent, QString domain, int port);
|
||||||
|
void ping();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void pingWithDomainSRV(QString domain, int port);
|
||||||
|
void pingWithDomainA(QString domain, int port);
|
||||||
|
void emitFail(QString error);
|
||||||
|
void emitSucceed(QString ip, int port);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void succeeded(QString ip, int port);
|
||||||
|
void failed(QString error);
|
||||||
|
void finished();
|
||||||
|
};
|
47
launcher/ui/pages/instance/ServerPingTask.cpp
Normal file
47
launcher/ui/pages/instance/ServerPingTask.cpp
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#include <QFutureWatcher>
|
||||||
|
|
||||||
|
#include "ServerPingTask.h"
|
||||||
|
#include "McResolver.h"
|
||||||
|
#include "McClient.h"
|
||||||
|
#include <Json.h>
|
||||||
|
|
||||||
|
unsigned getOnlinePlayers(QJsonObject data) {
|
||||||
|
return Json::requireInteger(Json::requireObject(data, "players"), "online");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServerPingTask::executeTask() {
|
||||||
|
qDebug() << "Querying status of " << QString("%1:%2").arg(m_domain).arg(m_port);
|
||||||
|
|
||||||
|
// Resolve the actual IP and port for the server
|
||||||
|
McResolver *resolver = new McResolver(nullptr, m_domain, m_port);
|
||||||
|
QObject::connect(resolver, &McResolver::succeeded, this, [this, resolver](QString ip, int port) {
|
||||||
|
qDebug() << "Resolved Address for" << m_domain << ": " << ip << ":" << port;
|
||||||
|
|
||||||
|
// Now that we have the IP and port, query the server
|
||||||
|
McClient *client = new McClient(nullptr, m_domain, ip, port);
|
||||||
|
|
||||||
|
QObject::connect(client, &McClient::succeeded, this, [this](QJsonObject data) {
|
||||||
|
m_outputOnlinePlayers = getOnlinePlayers(data);
|
||||||
|
qDebug() << "Online players: " << m_outputOnlinePlayers;
|
||||||
|
emitSucceeded();
|
||||||
|
});
|
||||||
|
QObject::connect(client, &McClient::failed, this, [this](QString error) {
|
||||||
|
emitFailed(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete McClient object when done
|
||||||
|
QObject::connect(client, &McClient::finished, this, [this, client]() {
|
||||||
|
client->deleteLater();
|
||||||
|
});
|
||||||
|
client->getStatusData();
|
||||||
|
});
|
||||||
|
QObject::connect(resolver, &McResolver::failed, this, [this](QString error) {
|
||||||
|
emitFailed(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete McResolver object when done
|
||||||
|
QObject::connect(resolver, &McResolver::finished, [resolver]() {
|
||||||
|
resolver->deleteLater();
|
||||||
|
});
|
||||||
|
resolver->ping();
|
||||||
|
}
|
22
launcher/ui/pages/instance/ServerPingTask.h
Normal file
22
launcher/ui/pages/instance/ServerPingTask.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <tasks/Task.h>
|
||||||
|
|
||||||
|
|
||||||
|
class ServerPingTask : public Task {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ServerPingTask(QString domain, int port) : Task(), m_domain(domain), m_port(port) {}
|
||||||
|
~ServerPingTask() override = default;
|
||||||
|
int m_outputOnlinePlayers = -1;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_domain;
|
||||||
|
int m_port;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void executeTask() override;
|
||||||
|
};
|
@ -38,6 +38,7 @@
|
|||||||
#include "ServersPage.h"
|
#include "ServersPage.h"
|
||||||
#include "ui/dialogs/CustomMessageBox.h"
|
#include "ui/dialogs/CustomMessageBox.h"
|
||||||
#include "ui_ServersPage.h"
|
#include "ui_ServersPage.h"
|
||||||
|
#include "ServerPingTask.h"
|
||||||
|
|
||||||
#include <FileSystem.h>
|
#include <FileSystem.h>
|
||||||
#include <io/stream_reader.h>
|
#include <io/stream_reader.h>
|
||||||
@ -51,8 +52,9 @@
|
|||||||
#include <QFileSystemWatcher>
|
#include <QFileSystemWatcher>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <tasks/ConcurrentTask.h>
|
||||||
|
|
||||||
static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things.
|
static const int COLUMN_COUNT = 3; // 3 , TBD: latency and other nice things.
|
||||||
|
|
||||||
struct Server {
|
struct Server {
|
||||||
// Types
|
// Types
|
||||||
@ -112,8 +114,7 @@ struct Server {
|
|||||||
bool m_checked = false;
|
bool m_checked = false;
|
||||||
bool m_up = false;
|
bool m_up = false;
|
||||||
QString m_motd; // https://mctools.org/motd-creator
|
QString m_motd; // https://mctools.org/motd-creator
|
||||||
int m_ping = 0;
|
std::optional<int> m_currentPlayers; // nullopt if not calculated/calculating
|
||||||
int m_currentPlayers = 0;
|
|
||||||
int m_maxPlayers = 0;
|
int m_maxPlayers = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -296,7 +297,7 @@ class ServersModel : public QAbstractListModel {
|
|||||||
case 1:
|
case 1:
|
||||||
return tr("Address");
|
return tr("Address");
|
||||||
case 2:
|
case 2:
|
||||||
return tr("Latency");
|
return tr("Online");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,7 +346,11 @@ class ServersModel : public QAbstractListModel {
|
|||||||
case 2:
|
case 2:
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
return m_servers[row].m_ping;
|
if (m_servers[row].m_currentPlayers) {
|
||||||
|
return *m_servers[row].m_currentPlayers;
|
||||||
|
} else {
|
||||||
|
return "...";
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@ -433,6 +438,40 @@ class ServersModel : public QAbstractListModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void queryServersStatus()
|
||||||
|
{
|
||||||
|
// Abort the currently running task if present
|
||||||
|
if (m_currentQueryTask != nullptr) {
|
||||||
|
m_currentQueryTask->abort();
|
||||||
|
qDebug() << "Aborted previous server query task";
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentQueryTask = ConcurrentTask::Ptr(
|
||||||
|
new ConcurrentTask("Query servers status", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())
|
||||||
|
);
|
||||||
|
int row = 0;
|
||||||
|
for (Server &server : m_servers) {
|
||||||
|
// reset current players
|
||||||
|
server.m_currentPlayers = {};
|
||||||
|
emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
|
||||||
|
|
||||||
|
// Start task to query server status
|
||||||
|
auto target = MinecraftTarget::parse(server.m_address, false);
|
||||||
|
auto *task = new ServerPingTask(target.address, target.port);
|
||||||
|
m_currentQueryTask->addTask(Task::Ptr(task));
|
||||||
|
|
||||||
|
// Update the model when the task is done
|
||||||
|
connect(task, &Task::finished, this, [this, task, row]() {
|
||||||
|
if (m_servers.size() < row) return;
|
||||||
|
m_servers[row].m_currentPlayers = task->m_outputOnlinePlayers;
|
||||||
|
emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
|
||||||
|
});
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentQueryTask->start();
|
||||||
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void dirChanged(const QString& path)
|
void dirChanged(const QString& path)
|
||||||
{
|
{
|
||||||
@ -520,6 +559,7 @@ class ServersModel : public QAbstractListModel {
|
|||||||
QList<Server> m_servers;
|
QList<Server> m_servers;
|
||||||
QFileSystemWatcher* m_watcher = nullptr;
|
QFileSystemWatcher* m_watcher = nullptr;
|
||||||
QTimer m_saveTimer;
|
QTimer m_saveTimer;
|
||||||
|
ConcurrentTask::Ptr m_currentQueryTask = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage)
|
ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage)
|
||||||
@ -676,6 +716,9 @@ void ServersPage::openedImpl()
|
|||||||
m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name);
|
m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name);
|
||||||
|
|
||||||
ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray());
|
ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray());
|
||||||
|
|
||||||
|
// ping servers
|
||||||
|
m_model->queryServersStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServersPage::closedImpl()
|
void ServersPage::closedImpl()
|
||||||
@ -734,4 +777,9 @@ void ServersPage::on_actionJoin_triggered()
|
|||||||
APPLICATION->launch(m_inst, true, false, std::make_shared<MinecraftTarget>(MinecraftTarget::parse(address, false)));
|
APPLICATION->launch(m_inst, true, false, std::make_shared<MinecraftTarget>(MinecraftTarget::parse(address, false)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ServersPage::on_actionRefresh_triggered()
|
||||||
|
{
|
||||||
|
m_model->queryServersStatus();
|
||||||
|
}
|
||||||
|
|
||||||
#include "ServersPage.moc"
|
#include "ServersPage.moc"
|
||||||
|
@ -85,6 +85,7 @@ class ServersPage : public QMainWindow, public BasePage {
|
|||||||
void on_actionMove_Up_triggered();
|
void on_actionMove_Up_triggered();
|
||||||
void on_actionMove_Down_triggered();
|
void on_actionMove_Down_triggered();
|
||||||
void on_actionJoin_triggered();
|
void on_actionJoin_triggered();
|
||||||
|
void on_actionRefresh_triggered();
|
||||||
|
|
||||||
void runningStateChanged(bool running);
|
void runningStateChanged(bool running);
|
||||||
|
|
||||||
|
@ -149,6 +149,8 @@
|
|||||||
<addaction name="actionMove_Up"/>
|
<addaction name="actionMove_Up"/>
|
||||||
<addaction name="actionMove_Down"/>
|
<addaction name="actionMove_Down"/>
|
||||||
<addaction name="actionJoin"/>
|
<addaction name="actionJoin"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionRefresh"/>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="actionAdd">
|
<action name="actionAdd">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -175,6 +177,11 @@
|
|||||||
<string>Join</string>
|
<string>Join</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionRefresh">
|
||||||
|
<property name="text">
|
||||||
|
<string>Refresh</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user