added missing files

This commit is contained in:
WatermelonModders
2022-06-01 10:27:42 -04:00
parent 49ec03caaa
commit 105e8101c1
10 changed files with 1625 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
#-------------------------------------------------
#
# Project created by QtCreator 2016-06-13T19:13:31
#
#-------------------------------------------------
QT += core gui network widgets
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = hearthmod
TEMPLATE = app
SOURCES += main.cpp\
proto.cpp \
mainwindow.cpp
HEADERS += \
proto.h \
mainwindow.h
FORMS += \
mainwindow.ui
DISTFILES += \
wait.cur
+43
View File
@@ -0,0 +1,43 @@
/*
hm_client - hearthmod client
Copyright (C) 2016 Filip Pancik
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QApplication>
#include <QMessageBox>
#include <QStyle>
#include <QDesktopWidget>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
if (!QSslSocket::supportsSsl()) {
QMessageBox::information(0, "Secure Socket Client",
"This system does not support OpenSSL.");
return -1;
}
MainWindow w;
w.setGeometry(
QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, w.size(), qApp->desktop()->availableGeometry())
);
w.show();
return a.exec();
}
+377
View File
@@ -0,0 +1,377 @@
/*
hm_client - hearthmod client
Copyright (C) 2016 Filip Pancik
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QtWidgets/QScrollBar>
#include <QtWidgets/QStyle>
#include <QtWidgets/QToolButton>
#include <QtWidgets/QMessageBox>
#include <QtNetwork/QSslCipher>
#include <QCryptographicHash>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <QFile>
#include <QProcess>
#include <QDir>
#include <QNetworkRequest>
#include <QNetworkReply>
#include "proto.h"
#include "mainwindow.h"
#include "ui_mainwindow.h"
static int version = 1;
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
socket = NULL;
ui = new Ui::MainWindow;
ui->setupUi(this);
m_isReady = true;
secureConnect();
ncards = 0;
connect(ui->loginButton, SIGNAL(clicked()),
this, SLOT(login()));
connect(ui->pushPlay, SIGNAL(clicked()),
this, SLOT(play()));
downloadFileFromURL("http://hearthmod.com/static/checksum", "../../hearthstone/Data/Win", E_CHECKSUM);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::socketError(QAbstractSocket::SocketError)
{
addLog("[Lobby]: Connection error: " + socket->errorString());
QMessageBox::critical(this, QString(socket->errorString()), QString("Connection error"));
if(socket->error() == 13) {
return;
}
exit(0);
}
void MainWindow::play()
{
#ifdef __linux__
int r = system("cd ../../hearthstone/ && sh linux");
#else
QString run = "RunAsDate.exe \"01\\04\\2016\" \"" + QDir::currentPath().replace("/", "\\") + "\\..\\..\\hearthstone\\Hearthstone.exe\" -launch";
QByteArray ba = run.toLatin1();
int r = system(ba.data());
if(r != 0) {
addLog("Executable not found ");
}
#endif
}
void MainWindow::login()
{
if(ui->editUsername->text().length() < 2 || ui->editUsername->text().length() > 32 ||
ui->editPassword->text().length() < 2 || ui->editPassword->text().length() > 32
) {
addLog("[Lobby]: Username and Password must be between 2 and 32 chars");
return;
}
QJsonDocument doc;
QJsonObject obj;
obj["user"] = ui->editUsername->text();
obj["pass"] = ui->editPassword->text();
doc.setObject(obj);
QByteArray js = doc.toJson();
char output[128];
int plen = proto.packet(output, P_LOGIN, js.data(), js.length());
socket->write(output, plen);
}
void MainWindow::socketStateChanged(QAbstractSocket::SocketState state)
{
qDebug() << "state changed " << state;
}
void MainWindow::addLog(QString msg)
{
if(ui->listLogs->count() > 10) {
ui->listLogs->clear();
}
ui->listLogs->addItem(msg);
}
void MainWindow::socketReadyRead()
{
QByteArray qb = socket->readAll();
QJsonDocument doc(QJsonDocument::fromJson(qb));
QJsonObject json = doc.object();
qint32 type = json["type"].toInt();
qint32 error = json["error"].toInt();
QString secret = json["secret"].toString();
if(error != 0) {
switch(error) {
case 1:
case 2:
addLog(QString("[Lobby]: Incorrect login details, to create an account go to hearthmod.com"));
break;
case 3:
addLog(QString("[Lobby]: No deck found, please go to hearthmod.com and create your deck"));
break;
}
return;
}
if(type == P_LOGIN) {
addLog(QString("[Lobby]: Login successful"));
addLog(QString("[Lobby]: Deck found"));
ui->editPassword->setDisabled(true);
ui->editUsername->setDisabled(true);
ui->loginButton->setDisabled(true);
ui->player1->setDisabled(true);
ui->player2->setDisabled(true);
ui->checkDeck->setChecked(1);
ui->pushPlay->setEnabled(1);
QString cmd = "Windows Registry Editor Version 5.00\n\
\n\
[HKEY_CURRENT_USER\\Software\\Blizzard Entertainment\\Battle.net\\Launch Options\\WTCG]\n\
\"REGION\"=\"EU\"\n\
\"HBS_TOKENX\"=\"<>\"\n\
";
cmd.replace("<>", secret);
if(ui->player1->isChecked()) {
cmd.replace("HBS_TOKENX", "HBS_TOKEN0");
#ifdef __linux__
int r = system("cp ../../hearthstone/Hearthstone_Data/Managed/player1/Assembly-CSharp.dll ../../hearthstone/Hearthstone_Data/Managed/");
if(r != 0) {
qDebug() << "couldn't copy";
}
#else
int r = system("copy ..\\..\\hearthstone\\Hearthstone_Data\\Managed\\player1\\Assembly-CSharp.dll ..\\..\\hearthstone\\Hearthstone_Data\\Managed\\ ");
if(r != 0) {
qDebug() << "couldn't copy";
}
#endif
} else {
cmd.replace("HBS_TOKENX", "HBS_TOKEN1");
#ifdef __linux__
int r = system("cp ../../hearthstone/Hearthstone_Data/Managed/player2/Assembly-CSharp.dll ../../hearthstone/Hearthstone_Data/Managed/");
if(r != 0) {
qDebug() << "couldn't copy";
}
#else
int r = system("copy ..\\..\\hearthstone\\Hearthstone_Data\\Managed\\player2\\Assembly-CSharp.dll ..\\..\\hearthstone\\Hearthstone_Data\\Managed\\ ");
if(r != 0) {
qDebug() << "couldn't copy";
}
#endif
}
QString filename = "tmp.reg";
QFile file(filename);
if(file.open(QIODevice::ReadWrite)) {
QTextStream stream(&file);
stream << cmd << endl;
file.close();
//qDebug() << QDir::currentPath();
#ifdef __linux__
int r = system("regedit tmp.reg");
#else
int r = system("tmp.reg");
#endif
if(r != 0) {
addLog(QString("[Lobby]: Cannot register client"));
ui->pushPlay->setEnabled(0);
}
#ifdef __linux__
r = system("rm tmp.reg");
#else
r = system("del tmp.reg");
#endif
if(r != 0) {
addLog(QString("[Lobby]: Cannot clean client"));
}
}
}
}
void MainWindow::sslErrors(const QList<QSslError> &errors)
{
socket->ignoreSslErrors();
if (socket->state() != QAbstractSocket::ConnectedState) {
socketStateChanged(socket->state());
}
}
void MainWindow::secureConnect()
{
if (!socket) {
socket = new QSslSocket(this);
connect(socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this, SLOT(socketStateChanged(QAbstractSocket::SocketState)));
connect(socket, SIGNAL(encrypted()),
this, SLOT(socketEncrypted()));
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)),
this, SLOT(socketError(QAbstractSocket::SocketError)));
connect(socket, SIGNAL(sslErrors(QList<QSslError>)),
this, SLOT(sslErrors(QList<QSslError>)));
connect(socket, SIGNAL(readyRead()),
this, SLOT(socketReadyRead()));
}
addLog(QString("[Lobby]: Connecting to hearthmod.com"));
socket->connectToHostEncrypted("hearthmod.com", 1119);
updateEnabledState();
}
void MainWindow::socketEncrypted()
{
qDebug() << "socket encrypted";
}
void MainWindow::updateEnabledState()
{
qDebug() << "state: "<< socket->state();
}
void MainWindow::downloadFileFromURL(const QString &url, const QString &filePath, enum download_step_e step) {
if (!m_isReady)
return;
m_isReady = false;
assets_step = step;
const QString fileName = filePath + url.right(url.size() - url.lastIndexOf("/")); // your filePath should end with a forward slash "/"
m_file = new QFile();
m_file->setFileName(fileName);
m_file->open(QIODevice::WriteOnly);
if (!m_file->isOpen()) {
m_isReady = true;
addLog(QString("Couldn't open file " + fileName));
return;
}
if(assets_step == E_CHECKSUM) {
addLog(QString("[Update]: Version checking.. "));
} else {
addLog(QString("[Update]: Updating assets.. "));
}
QNetworkAccessManager *manager = new QNetworkAccessManager;
QNetworkRequest request;
request.setUrl(QUrl(url));
connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(onDownloadFileComplete(QNetworkReply *)));
manager->get(request);
}
void MainWindow::onDownloadFileComplete(QNetworkReply *reply) {
int update = 0;
if (!m_file->isWritable()) {
m_isReady = true;
addLog(QString("Update couldn't complete"));
return; // TODO: error check
}
if(assets_step == E_CHECKSUM) {
// write to checksum
QString cs = reply->readAll();
if(cs.length() < 1) {
QMessageBox::critical(this, tr("Version check"), "Version check failed. Server is most likely offline.");
//QMessageBox::critical(this, QString(socket->errorString()), QString("Connection error"));
exit(0);
}
m_file->write(cs.toStdString().c_str());
m_file->close();
QFile f("../../hearthstone/Data/Win/local_checksum");
f.open(QFile::ReadOnly | QFile::Text);
QTextStream in(&f);
QString checksum_local = in.readAll();
f.close();
assets_step = E_ASSETS;
if(cs == checksum_local) {
addLog(QString("[Update]: Assets up to date."));
} else {
addLog(QString("[Update]: Update required."));
update = 1;
}
// write to local checksum
QFile f1("../../hearthstone/Data/Win/local_checksum");
f1.open(QFile::ReadWrite);
f1.write(cs.toStdString().c_str());
f1.close();
m_isReady = true;
if(update == 1) {
ui->editPassword->setDisabled(true);
ui->editUsername->setDisabled(true);
ui->loginButton->setDisabled(true);
ui->player1->setDisabled(true);
ui->player2->setDisabled(true);
downloadFileFromURL("http://hearthmod.com/static/cardxml0.unity3d", "../../hearthstone/Data/Win", E_ASSETS);
} else {
ui->checkAssets->setChecked(1);
}
} else {
addLog(QString("[Update]: Assets downloaded."));
m_file->write(reply->readAll());
m_file->close(); // TODO: delete the file from the system later on
m_isReady = true;
ui->editPassword->setDisabled(false);
ui->editUsername->setDisabled(false);
ui->loginButton->setDisabled(false);
ui->player1->setDisabled(false);
ui->player2->setDisabled(false);
ui->checkAssets->setChecked(1);
}
}
+96
View File
@@ -0,0 +1,96 @@
/*
hm_client - hearthmod client
Copyright (C) 2016 Filip Pancik
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef MainWindow_H
#define MainWindow_H
#include <QtWidgets/QWidget>
#include <QtNetwork/QAbstractSocket>
#include <QtNetwork/QSslSocket>
#include <QMainWindow>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QFile>
#include "proto.h"
namespace Ui {
class MainWindow;
}
enum arch_e {
ARCH_LINUX = 0,
ARCH_WIN,
};
enum download_step_e {
E_CHECKSUM = 1,
E_ASSETS
};
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void secureConnect();
void socketStateChanged(QAbstractSocket::SocketState state);
void socketEncrypted();
void sslErrors(const QList<QSslError> &errors);
void socketError(QAbstractSocket::SocketError);
void socketReadyRead();
void updateEnabledState();
void onDownloadFileComplete(QNetworkReply *reply);
void login();
void play();
private:
void addLog(QString msg);
void setNcards(const int type);
void downloadFileFromURL(const QString &url, const QString &filePath, enum download_step_e step);
QFile *m_file;
bool m_isReady;
Proto proto;
int ncards;
Ui::MainWindow *ui;
QSslSocket *socket;
enum download_step_e assets_step;
};
/*
Q_OBJECT
QFile *m_file;
bool m_isReady = true;
public:
explicit Downloader(QObject *parent = 0) : QObject(parent) {}
virtual ~Downloader() { delete m_file; }
void downloadFileFromURL(const QString &url, const QString &filePath);
private slots:
void onDownloadFileComplete(QNetworkReply *reply);
*/
#endif // MainWindow_H
+275
View File
@@ -0,0 +1,275 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>751</width>
<height>431</height>
</rect>
</property>
<property name="windowTitle">
<string>HearthMod Beta</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>../../../.wine/drive_c/games/HS3/Hearthstone_Data/wait.cur</normaloff>../../../.wine/drive_c/games/HS3/Hearthstone_Data/wait.cur</iconset>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<widget class="QWidget" name="centralWidget">
<widget class="QGroupBox" name="groupBox_2">
<property name="geometry">
<rect>
<x>10</x>
<y>190</y>
<width>731</width>
<height>191</height>
</rect>
</property>
<property name="title">
<string>Logs</string>
</property>
<widget class="QListWidget" name="listLogs">
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>711</width>
<height>151</height>
</rect>
</property>
</widget>
</widget>
<widget class="QGroupBox" name="lobby">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>731</width>
<height>181</height>
</rect>
</property>
<property name="title">
<string>Lobby</string>
</property>
<widget class="QGroupBox" name="groupBox">
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>341</width>
<height>141</height>
</rect>
</property>
<property name="title">
<string>Login</string>
</property>
<widget class="QLineEdit" name="editUsername">
<property name="geometry">
<rect>
<x>90</x>
<y>40</y>
<width>231</width>
<height>25</height>
</rect>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>40</y>
<width>71</width>
<height>17</height>
</rect>
</property>
<property name="text">
<string>Username</string>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>10</x>
<y>70</y>
<width>67</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>Password</string>
</property>
</widget>
<widget class="QLineEdit" name="editPassword">
<property name="geometry">
<rect>
<x>90</x>
<y>70</y>
<width>231</width>
<height>25</height>
</rect>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
<widget class="QPushButton" name="loginButton">
<property name="geometry">
<rect>
<x>210</x>
<y>100</y>
<width>111</width>
<height>25</height>
</rect>
</property>
<property name="text">
<string>Login</string>
</property>
</widget>
<widget class="QLabel" name="label_3">
<property name="geometry">
<rect>
<x>10</x>
<y>100</y>
<width>71</width>
<height>17</height>
</rect>
</property>
<property name="text">
<string>Player</string>
</property>
</widget>
<widget class="QRadioButton" name="player1">
<property name="geometry">
<rect>
<x>90</x>
<y>100</y>
<width>41</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>1</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
<widget class="QRadioButton" name="player2">
<property name="geometry">
<rect>
<x>140</x>
<y>100</y>
<width>41</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>2</string>
</property>
</widget>
</widget>
<widget class="QGroupBox" name="groupBox_3">
<property name="geometry">
<rect>
<x>370</x>
<y>30</y>
<width>351</width>
<height>141</height>
</rect>
</property>
<property name="title">
<string>Game</string>
</property>
<widget class="QCheckBox" name="checkDeck">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>92</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>Deck</string>
</property>
</widget>
<widget class="QPushButton" name="pushPlay">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>20</x>
<y>100</y>
<width>321</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string>Play!</string>
</property>
</widget>
<widget class="QCheckBox" name="checkAssets">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>60</y>
<width>92</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>Assets</string>
</property>
</widget>
</widget>
</widget>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>751</width>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuMenu">
<property name="title">
<string>Menu</string>
</property>
<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuAbout">
<property name="title">
<string>Help</string>
</property>
</widget>
<addaction name="menuMenu"/>
<addaction name="menuAbout"/>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<action name="actionQuit">
<property name="text">
<string>Quit</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>
+105
View File
@@ -0,0 +1,105 @@
/*
hm_client - hearthmod client
Copyright (C) 2016 Filip Pancik
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <qdebug.h>
#include "proto.h"
//#include "ui_mainwindow.h"
Proto::Proto()
{
}
Proto::~Proto()
{
}
int Proto::add(char **dst, const char *maxdst, const void *src, const int nsrc)
{
char *start;
if(maxdst < *dst + nsrc + sizeof(nsrc)) {
qDebug() << "pair too long";
exit(0);
return -1;
}
start = *dst;
qDebug() << "written0 " << (*dst - start);
memcpy(*dst, &nsrc, sizeof(nsrc));
*dst += sizeof(nsrc);
qDebug() << "written1 " << (*dst - start);
memcpy(*dst, src, nsrc);
*dst += nsrc;
qDebug() << "whearthmodritten2 " << (*dst - start);
return (*dst - start);
}
int Proto::packet(char *dst, enum packet_e packet, char *src, const int nsrc)
{
int len;
int magic = 0xffffffff;
len = 3 * sizeof(packet) + nsrc;
memcpy(dst, &magic, sizeof(magic));
dst += sizeof(magic);
memcpy(dst, &packet, sizeof(packet));
dst += sizeof(packet);
memcpy(dst, &len, sizeof(len));
dst += sizeof(len);
memcpy(dst, src, nsrc);
dst += nsrc;
return len;
}
/*
QMap Proto::read(char *src, const int nsrc)
{
QMap<QString, QString> map;
enum packet_e t;
char *tmp;
if(nsrc < 16) {
return P_NONE;
}
t = (enum packet_e)(*(int *)(src + sizeof(int)));
start = src + 2*(sizeof(int));
end = start + nsrc;
while(start < end) {
if(t == P_LOGIN) {
map['error'] = src;
}
}
}
*/
+68
View File
@@ -0,0 +1,68 @@
/*
hm_client - hearthmod client
Copyright (C) 2016 Filip Pancik
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PROTO_H
#define PROTO_H
enum packet_e {
P_NONE = 0,
P_CREATEACCOUNT = 1,
P_LOGIN = 2,
P_DECK = 3,
};
namespace Ui {
class Proto;
}
class Proto
{
public:
explicit Proto();
~Proto();
int add(char **dst, const char *maxdst, const void *src, const int nsrc);
int packet(char *dst, enum packet_e packet, char *src, const int nsrc);
enum packet_e read(char *src, const int nsrc);
/*
private slots:
void secureConnect();
void socketStateChanged(QAbstractSocket::SocketState state);
void socketEncrypted();
void sslErrors(const QList<QSslError> &errors);
void socketError(QAbstractSocket::SocketError);
void socketReadyRead();
void updateEnabledState();
void addCard();
void clearDeck();
void removeCard();
void saveDeck();
private:
void setNcards(const int type);
int ncards;
Ui::MainWindow *ui;
QSslSocket *socket;
*/
};
#endif // PROTO_H
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB