From 72ff342d6325cab42cc3d8401b4fac5b2f7eff3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Thu, 12 Apr 2018 01:44:51 +0200 Subject: [PATCH] GH-2053 basics of the servers.dat management --- application/CMakeLists.txt | 3 + application/InstancePageProvider.h | 2 + application/InstanceWindow.cpp | 2 +- application/pages/instance/ServersPage.cpp | 743 ++++++++++++++++++ application/pages/instance/ServersPage.h | 87 ++ application/pages/instance/ServersPage.ui | 207 +++++ .../multimc/128x128/unknown_server.png | Bin 0 -> 11085 bytes application/resources/multimc/index.theme | 5 + application/resources/multimc/multimc.qrc | 3 + application/widgets/PageContainer.cpp | 15 +- application/widgets/PageContainer.h | 1 + 11 files changed, 1064 insertions(+), 4 deletions(-) create mode 100644 application/pages/instance/ServersPage.cpp create mode 100644 application/pages/instance/ServersPage.h create mode 100644 application/pages/instance/ServersPage.ui create mode 100644 application/resources/multimc/128x128/unknown_server.png diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index d67540b90..c0268da53 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -100,6 +100,8 @@ SET(MULTIMC_SOURCES pages/instance/ScreenshotsPage.h pages/instance/OtherLogsPage.cpp pages/instance/OtherLogsPage.h + pages/instance/ServersPage.cpp + pages/instance/ServersPage.h pages/instance/LegacyUpgradePage.cpp pages/instance/LegacyUpgradePage.h pages/instance/WorldListPage.cpp @@ -231,6 +233,7 @@ SET(MULTIMC_UIS pages/instance/ScreenshotsPage.ui pages/instance/OtherLogsPage.ui pages/instance/LegacyUpgradePage.ui + pages/instance/ServersPage.ui pages/instance/WorldListPage.ui # Global settings pages diff --git a/application/InstancePageProvider.h b/application/InstancePageProvider.h index 9dda78597..b7a9513cb 100644 --- a/application/InstancePageProvider.h +++ b/application/InstancePageProvider.h @@ -15,6 +15,7 @@ #include "pages/instance/OtherLogsPage.h" #include "pages/instance/LegacyUpgradePage.h" #include "pages/instance/WorldListPage.h" +#include "pages/instance/ServersPage.h" class InstancePageProvider : public QObject, public BasePageProvider @@ -43,6 +44,7 @@ public: values.append(new TexturePackPage(onesix.get())); values.append(new NotesPage(onesix.get())); values.append(new WorldListPage(onesix.get(), onesix->worldList(), "worlds", "worlds", tr("Worlds"), "Worlds")); + values.append(new ServersPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->minecraftRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix.get())); } diff --git a/application/InstanceWindow.cpp b/application/InstanceWindow.cpp index 5895ca3a4..b36781a77 100644 --- a/application/InstanceWindow.cpp +++ b/application/InstanceWindow.cpp @@ -181,7 +181,7 @@ void InstanceWindow::closeEvent(QCloseEvent *event) bool InstanceWindow::saveAll() { - return m_container->prepareToClose(); + return m_container->saveAll(); } void InstanceWindow::on_btnKillMinecraft_clicked() diff --git a/application/pages/instance/ServersPage.cpp b/application/pages/instance/ServersPage.cpp new file mode 100644 index 000000000..f7ddc7daa --- /dev/null +++ b/application/pages/instance/ServersPage.cpp @@ -0,0 +1,743 @@ +#include "ServersPage.h" +#include "ui_ServersPage.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. + +struct Server +{ + // Types + enum class AcceptsTextures : int + { + ASK = 0, + ALWAYS = 1, + NEVER = 2 + }; + + // Methods + Server() + { + m_name = QObject::tr("Minecraft Server"); + } + Server(const QString & name, const QString & address) + { + m_name = name; + m_address = address; + } + Server(nbt::tag_compound& server) + { + std::string addressStr(server["ip"]); + m_address = QString::fromUtf8(addressStr.c_str()); + + std::string nameStr(server["name"]); + m_name = QString::fromUtf8(nameStr.c_str()); + + if(server["icon"]) + { + std::string base64str(server["icon"]); + m_icon = QByteArray::fromBase64(base64str.c_str()); + } + + if(server.has_key("acceptTextures", nbt::tag_type::Byte)) + { + bool value = server["acceptTextures"].as().get(); + if(value) + { + m_acceptsTextures = AcceptsTextures::ALWAYS; + } + else + { + m_acceptsTextures = AcceptsTextures::NEVER; + } + } + } + + void serialize(nbt::tag_compound& server) + { + server.insert("name", m_name.toUtf8().toStdString()); + server.insert("ip", m_address.toUtf8().toStdString()); + if(m_icon.size()) + { + server.insert("icon", m_icon.toBase64().toStdString()); + } + if(m_acceptsTextures != AcceptsTextures::ASK) + { + server.insert("acceptTextures", nbt::tag_byte(m_acceptsTextures == AcceptsTextures::ALWAYS)); + } + } + + // Data - persistent and user changeable + QString m_name; + QString m_address; + AcceptsTextures m_acceptsTextures = AcceptsTextures::ASK; + + // Data - persistent and automatically updated + QByteArray m_icon; + + // Data - temporary + bool m_checked = false; + bool m_up = false; + QString m_motd; // https://mctools.org/motd-creator + int m_ping = 0; + int m_currentPlayers = 0; + int m_maxPlayers = 0; +}; + +static std::unique_ptr parseServersDat(const QString& filename) +{ + try + { + QByteArray input = FS::read(filename); + std::istringstream foo(std::string(input.constData(), input.size())); + auto pair = nbt::io::read_compound(foo); + + if(pair.first != "") + return nullptr; + + if(pair.second == nullptr) + return nullptr; + + return std::move(pair.second); + } + catch(...) + { + return nullptr; + } +} + +static bool serializeServerDat(const QString& filename, nbt::tag_compound * levelInfo) +{ + try + { + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val(s.str().data(), (int) s.str().size() ); + FS::write(filename, val); + return true; + } + catch(...) + { + return false; + } +} + +class ServersModel: public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + ServerPtrRole = Qt::UserRole, + }; + explicit ServersModel(const QString &path, QObject *parent = 0) + : QAbstractListModel(parent) + { + m_path = path; + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ServersModel::fileChanged); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &ServersModel::dirChanged); + m_saveTimer.setSingleShot(true); + m_saveTimer.setInterval(5000); + connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal); + } + virtual ~ServersModel() {}; + + void observe() + { + if(m_observed) + { + return; + } + m_observed = true; + + if(!m_loaded) + { + load(); + } + + updateFSObserver(); + } + + void unobserve() + { + if(!m_observed) + { + return; + } + m_observed = false; + + updateFSObserver(); + } + + void lock() + { + if(m_locked) + { + return; + } + saveNow(); + + m_locked = true; + updateFSObserver(); + } + + void unlock() + { + if(!m_locked) + { + return; + } + m_locked = false; + + updateFSObserver(); + } + + int addEmptyRow(int position) + { + if(m_locked) + { + return -1; + } + if(position < 0 || position >= rowCount()) + { + position = rowCount(); + } + beginInsertRows(QModelIndex(), position, position); + m_servers.insert(position, Server()); + endInsertRows(); + scheduleSave(); + return position; + } + + bool removeRow(int row) + { + if(m_locked) + { + return false; + } + if(row < 0 || row >= rowCount()) + { + return false; + } + beginRemoveRows(QModelIndex(), row, row); + m_servers.removeAt(row); + endRemoveRows(); // does absolutely nothing, the selected server stays as the next line... + scheduleSave(); + return true; + } + + bool moveUp(int row) + { + if(m_locked) + { + return false; + } + if(row <= 0) + { + return false; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); + m_servers.swap(row-1, row); + endMoveRows(); + scheduleSave(); + return true; + } + + bool moveDown(int row) + { + if(m_locked) + { + return false; + } + int count = rowCount(); + if(row + 1 >= count) + { + return false; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); + m_servers.swap(row+1, row); + endMoveRows(); + scheduleSave(); + return true; + } + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override + { + if (section < 0 || section >= COLUMN_COUNT) + return QVariant(); + + if(role == Qt::DisplayRole) + { + switch(section) + { + case 0: + return tr("Name"); + case 1: + return tr("Address"); + case 2: + return tr("Latency"); + } + } + + return QAbstractListModel::headerData(section, orientation, role); + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + if(column < 0 || column >= COLUMN_COUNT) + return QVariant(); + + if (row < 0 || row >= m_servers.size()) + return QVariant(); + + switch(column) + { + case 0: + switch (role) + { + case Qt::DecorationRole: + { + auto & bytes = m_servers[row].m_icon; + if(bytes.size()) + { + QPixmap px; + if(px.loadFromData(bytes)) + return QIcon(px); + } + return MMC->getThemedIcon("unknown_server"); + } + case Qt::DisplayRole: + return m_servers[row].m_name; + case ServerPtrRole: + return QVariant::fromValue((void *)&m_servers[row]); + default: + return QVariant(); + } + case 1: + switch (role) + { + case Qt::DisplayRole: + return m_servers[row].m_address; + default: + return QVariant(); + } + case 2: + switch (role) + { + case Qt::DisplayRole: + return m_servers[row].m_ping; + default: + return QVariant(); + } + default: + return QVariant(); + } + } + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + return m_servers.size(); + } + int columnCount(const QModelIndex & parent) const override + { + return COLUMN_COUNT; + } + + Server * at(int index) + { + if(index < 0 || index >= rowCount()) + { + return nullptr; + } + return &m_servers[index]; + } + + void setName(int row, const QString & name) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_name == name) + { + return; + } + server->m_name = name; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void setAddress(int row, const QString & address) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_address == address) + { + return; + } + server->m_address = address; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void setAcceptsTextures(int row, Server::AcceptsTextures textures) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_acceptsTextures == textures) + { + return; + } + server->m_acceptsTextures = textures; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void load() + { + cancelSave(); + beginResetModel(); + QList servers; + auto serversDat = parseServersDat(serversPath()); + if(serversDat) + { + auto &serversList = serversDat->at("servers").as(); + for(auto iter = serversList.begin(); iter != serversList.end(); iter++) + { + auto & serverTag = (*iter).as(); + Server s(serverTag); + servers.append(s); + } + } + m_servers.swap(servers); + m_loaded = true; + endResetModel(); + } + + void saveNow() + { + if(saveIsScheduled()) + { + save_internal(); + } + } + + +public slots: + void dirChanged(const QString& path) + { + qDebug() << "Changed:" << path; + load(); + } + void fileChanged(const QString& path) + { + qDebug() << "Changed:" << path; + } + +private slots: + void save_internal() + { + cancelSave(); + qDebug() << "Server list save is performed for" << m_path; + + nbt::tag_compound out; + nbt::tag_list list; + for(auto & server: m_servers) + { + nbt::tag_compound serverNbt; + server.serialize(serverNbt); + list.push_back(std::move(serverNbt)); + } + out.insert("servers", nbt::value(std::move(list))); + + if(!serializeServerDat(serversPath(), &out)) + { + qDebug() << "Failed to save server list:" << m_path << "Will try again."; + scheduleSave(); + } + } + +private: + void scheduleSave() + { + if(!m_loaded) + { + qDebug() << "Server list should never save if it didn't successfully load, path:" << m_path; + return; + } + if(!m_dirty) + { + m_dirty = true; + qDebug() << "Server list save is scheduled for" << m_path; + } + m_saveTimer.start(); + } + + void cancelSave() + { + m_dirty = false; + m_saveTimer.stop(); + } + + bool saveIsScheduled() const + { + return m_dirty; + } + + void updateFSObserver() + { + bool observingFS = m_watcher->directories().contains(m_path); + if(m_observed && m_locked) + { + if(!observingFS) + { + qWarning() << "Will watch" << m_path; + if(!m_watcher->addPath(m_path)) + { + qWarning() << "Failed to start watching" << m_path; + } + } + } + else + { + if(observingFS) + { + qWarning() << "Will stop watching" << m_path; + if(!m_watcher->removePath(m_path)) + { + qWarning() << "Failed to stop watching" << m_path; + } + } + } + } + + QString serversPath() + { + QFileInfo foo(FS::PathCombine(m_path, "servers.dat")); + return foo.canonicalFilePath(); + } + +private: + bool m_loaded = false; + bool m_locked = false; + bool m_observed = false; + bool m_dirty = false; + QString m_path; + QList m_servers; + QFileSystemWatcher *m_watcher = nullptr; + QTimer m_saveTimer; +}; + +ServersPage::ServersPage(MinecraftInstance * inst, QWidget* parent) + : QWidget(parent), ui(new Ui::ServersPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + m_inst = inst; + m_model = new ServersModel(inst->minecraftRoot(), this); + ui->serversView->setIconSize(QSize(64,64)); + ui->serversView->setModel(m_model); + auto head = ui->serversView->header(); + if(head->count()) + { + head->setSectionResizeMode(0, QHeaderView::Stretch); + for(int i = 1; i < head->count(); i++) + { + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + } + + auto selectionModel = ui->serversView->selectionModel(); + connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged); + connect(m_inst, &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed); + connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); + connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); + connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); + + m_locked = m_inst->isRunning(); + if(m_locked) + { + m_model->lock(); + } + + updateState(); +} + +ServersPage::~ServersPage() +{ + m_model->saveNow(); +} + +void ServersPage::on_RunningState_changed(bool running) +{ + if(m_locked == running) + { + return; + } + m_locked = running; + if(m_locked) + { + m_model->lock(); + } + else + { + m_model->unlock(); + } + updateState(); +} + +void ServersPage::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + int nextServer = -1; + if (!current.isValid()) + { + nextServer = -1; + } + else + { + nextServer = current.row(); + } + currentServer = nextServer; + updateState(); +} + +// WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal. +void ServersPage::rowsRemoved(const QModelIndex& parent, int first, int last) +{ + if(currentServer < first) + { + // current was before the removal + return; + } + else if(currentServer >= first && currentServer <= last) + { + // current got removed... + return; + } + else + { + // current was past the removal + int count = last - first + 1; + currentServer -= count; + } +} + +void ServersPage::nameEdited(const QString& name) +{ + m_model->setName(currentServer, name); +} + +void ServersPage::addressEdited(const QString& address) +{ + m_model->setAddress(currentServer, address); +} + +void ServersPage::resourceIndexChanged(int index) +{ + auto acceptsTextures = Server::AcceptsTextures(index); + m_model->setAcceptsTextures(currentServer, acceptsTextures); +} + +void ServersPage::updateState() +{ + auto server = m_model->at(currentServer); + + bool serverEditEnabled = server && !m_locked; + ui->addressLine->setEnabled(serverEditEnabled); + ui->nameLine->setEnabled(serverEditEnabled); + ui->resourceComboBox->setEnabled(serverEditEnabled); + ui->moveDownBtn->setEnabled(serverEditEnabled); + ui->moveUpBtn->setEnabled(serverEditEnabled); + ui->removeBtn->setEnabled(serverEditEnabled); + + if(server) + { + ui->addressLine->setText(server->m_address); + ui->nameLine->setText(server->m_name); + ui->resourceComboBox->setCurrentIndex(int(server->m_acceptsTextures)); + } + else + { + ui->addressLine->setText(QString()); + ui->nameLine->setText(QString()); + ui->resourceComboBox->setCurrentIndex(0); + } + + ui->addBtn->setDisabled(m_locked); +} + +void ServersPage::openedImpl() +{ + m_model->observe(); +} + +void ServersPage::closedImpl() +{ + m_model->unobserve(); +} + +void ServersPage::on_addBtn_clicked() +{ + int position = m_model->addEmptyRow(currentServer + 1); + if(position < 0) + { + return; + } + // select the new row + ui->serversView->selectionModel()->setCurrentIndex( + m_model->index(position), + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear | QItemSelectionModel::Rows + ); + currentServer = position; +} + +void ServersPage::on_removeBtn_clicked() +{ + m_model->removeRow(currentServer); +} + +void ServersPage::on_moveUpBtn_clicked() +{ + if(m_model->moveUp(currentServer)) + { + currentServer --; + } +} + +void ServersPage::on_moveDownBtn_clicked() +{ + if(m_model->moveDown(currentServer)) + { + currentServer ++; + } +} + +void ServersPage::on_refreshBtn_clicked() +{ + m_model->load(); +} + +#include "ServersPage.moc" diff --git a/application/pages/instance/ServersPage.h b/application/pages/instance/ServersPage.h new file mode 100644 index 000000000..6c812bd90 --- /dev/null +++ b/application/pages/instance/ServersPage.h @@ -0,0 +1,87 @@ +/* Copyright 2013-2018 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "pages/BasePage.h" +#include + +namespace Ui +{ +class ServersPage; +} + +struct Server; +class ServersModel; +class MinecraftInstance; + +class ServersPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ServersPage(MinecraftInstance *inst, QWidget *parent = 0); + virtual ~ServersPage(); + + void openedImpl() override; + void closedImpl() override; + + virtual QString displayName() const override + { + return tr("Servers"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("unknown_server"); + } + virtual QString id() const override + { + return "servers"; + } + virtual QString helpPage() const override + { + return "Servers-management"; + } +private: + void updateState(); + void scheduleSave(); + bool saveIsScheduled() const; + +private slots: + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + void rowsRemoved(const QModelIndex &parent, int first, int last); + + void on_addBtn_clicked(); + void on_removeBtn_clicked(); + void on_moveUpBtn_clicked(); + void on_moveDownBtn_clicked(); + void on_refreshBtn_clicked(); + void on_RunningState_changed(bool running); + + void nameEdited(const QString & name); + void addressEdited(const QString & address); + void resourceIndexChanged(int index); + +private: // data + int currentServer = -1; + bool m_locked = true; + Ui::ServersPage *ui = nullptr; + ServersModel * m_model = nullptr; + MinecraftInstance * m_inst = nullptr; +}; + diff --git a/application/pages/instance/ServersPage.ui b/application/pages/instance/ServersPage.ui new file mode 100644 index 000000000..6d1a9bc01 --- /dev/null +++ b/application/pages/instance/ServersPage.ui @@ -0,0 +1,207 @@ + + + ServersPage + + + + 0 + 0 + 706 + 575 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + 0 + 0 + + + + Tab 1 + + + + + + + + + Ask to download + + + + + Always download + + + + + Never download + + + + + + + + Reso&urces + + + resourceComboBox + + + + + + + + + + &Name + + + nameLine + + + + + + + + + + Address + + + addressLine + + + + + + + + 0 + 0 + + + + true + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + 64 + 64 + + + + false + + + false + + + + + + + + + + + &Add + + + + + + + &Remove + + + + + + + Move Up + + + + + + + Move Down + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Refresh + + + + + + + + + + + + + tabWidget + serversView + nameLine + addressLine + resourceComboBox + addBtn + removeBtn + moveUpBtn + moveDownBtn + refreshBtn + + + + diff --git a/application/resources/multimc/128x128/unknown_server.png b/application/resources/multimc/128x128/unknown_server.png new file mode 100644 index 0000000000000000000000000000000000000000..ec98382d47272f9cebceb0d2aa8e4f187fc0b54f GIT binary patch literal 11085 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/log.png 64x64/log.png + + 128x128/unknown_server.png + scalable/screenshot-placeholder.svg diff --git a/application/widgets/PageContainer.cpp b/application/widgets/PageContainer.cpp index 98de57e8f..c8c2b57a5 100644 --- a/application/widgets/PageContainer.cpp +++ b/application/widgets/PageContainer.cpp @@ -218,10 +218,9 @@ void PageContainer::currentChanged(const QModelIndex ¤t) bool PageContainer::prepareToClose() { - for (auto page : m_model->pages()) + if(!saveAll()) { - if (!page->apply()) - return false; + return false; } if (m_currentPage) { @@ -229,3 +228,13 @@ bool PageContainer::prepareToClose() } return true; } + +bool PageContainer::saveAll() +{ + for (auto page : m_model->pages()) + { + if (!page->apply()) + return false; + } + return true; +} diff --git a/application/widgets/PageContainer.h b/application/widgets/PageContainer.h index ea9f8ce16..a05e74c41 100644 --- a/application/widgets/PageContainer.h +++ b/application/widgets/PageContainer.h @@ -46,6 +46,7 @@ public: * @return true if everything can be saved, false if there is something that requires attention */ bool prepareToClose(); + bool saveAll(); /* request close - used by individual pages */ bool requestClose() override