Merge branch 'develop' into ci/address-sanitiser_on_debug_builds

This commit is contained in:
Rachel Powers 2023-07-08 01:27:30 -07:00
commit 7f138e3538
No known key found for this signature in database
GPG Key ID: E10E321EB160949B
34 changed files with 408 additions and 244 deletions

26
.github/workflows/update-flake.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Update Flake Lockfile
on:
schedule:
# run weekly on sunday
- cron: "0 0 * * 0"
workflow_dispatch:
permissions:
pull-requests: write
jobs:
update-flake:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v22
- uses: DeterminateSystems/update-flake-lock@v19
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"
pr-labels: |
Linux
simple change

48
flake.lock generated
View File

@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"lastModified": 1688254665,
"narHash": "sha256-8FHEgBrr7gYNiS/NzCxIO3m4hvtLRW9YY1nYo1ivm3o=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"rev": "267149c58a14d15f7f81b4d737308421de9d7152",
"type": "github"
},
"original": {
@ -35,12 +35,15 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
@ -88,11 +91,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1685012353,
"narHash": "sha256-U3oOge4cHnav8OLGdRVhL45xoRj4Ppd+It6nPC9nNIU=",
"lastModified": 1688221086,
"narHash": "sha256-cdW6qUL71cNWhHCpMPOJjlw0wzSRP0pVlRn2vqX/VVg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "aeb75dba965e790de427b73315d5addf91a54955",
"rev": "cd99c2b3c9f160cd004318e0697f90bbd5960825",
"type": "github"
},
"original": {
@ -105,11 +108,11 @@
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1682879489,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"lastModified": 1688049487,
"narHash": "sha256-100g4iaKC9MalDjUW9iN6Jl/OocTDtXdeAj7pEGIRh4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"rev": "4bc72cae107788bf3f24f30db2e2f685c9298dc9",
"type": "github"
},
"original": {
@ -135,11 +138,11 @@
]
},
"locked": {
"lastModified": 1684842236,
"narHash": "sha256-rYWsIXHvNhVQ15RQlBUv67W3YnM+Pd+DuXGMvCBq2IE=",
"lastModified": 1688386108,
"narHash": "sha256-Vffto9QaVonzYAcPlAzd0soqWYpPpKk60dfNLSIXcFA=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "61e567d6497bc9556f391faebe5e410e6623217f",
"rev": "42587d3414d1747999a5f71e92a83cf6547b62da",
"type": "github"
},
"original": {
@ -156,6 +159,21 @@
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@ -687,8 +687,16 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->reset("PastebinCustomAPIBase");
}
}
// meta URL
m_settings->registerSetting("MetaURLOverride", "");
{
// Meta URL
m_settings->registerSetting("MetaURLOverride", "");
QUrl metaUrl(m_settings->get("MetaURLOverride").toString());
// get rid of invalid meta urls
if (!metaUrl.isValid() || metaUrl.scheme() != "http" || metaUrl.scheme() != "https")
m_settings->reset("MetaURLOverride");
}
m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false);

View File

@ -36,6 +36,7 @@
*/
#include "FileSystem.h"
#include <QPair>
#include "BuildConfig.h"
@ -102,7 +103,7 @@ namespace fs = ghc::filesystem;
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <unistd.h>
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS)
#include <sys/attr.h>
#include <sys/clonefile.h>
#elif defined(Q_OS_WIN)
@ -246,6 +247,7 @@ bool copy::operator()(const QString& offset, bool dryRun)
{
using copy_opts = fs::copy_options;
m_copied = 0; // reset counter
m_failedPaths.clear();
// NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
@ -277,6 +279,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
m_failedPaths.append(dst_path);
emit copyFailed(relative_dst_path);
return;
}
m_copied++;
emit fileCopied(relative_dst_path);
@ -1077,6 +1082,7 @@ bool clone::operator()(const QString& offset, bool dryRun)
}
m_cloned = 0; // reset counter
m_failedClones.clear();
auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset);
@ -1097,6 +1103,9 @@ bool clone::operator()(const QString& offset, bool dryRun)
qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
m_failedClones.append(qMakePair(src_path, dst_path));
emit cloneFailed(src_path, dst_path);
return;
}
m_cloned++;
emit fileCloned(src_path, dst_path);
@ -1156,7 +1165,7 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
return false;
}
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS)
if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
qDebug() << "failed macos_bsd_clonefile:";
@ -1385,7 +1394,7 @@ bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std
return true;
}
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{

View File

@ -43,6 +43,7 @@
#include <system_error>
#include <QDir>
#include <QPair>
#include <QFlags>
#include <QLocalServer>
#include <QObject>
@ -112,9 +113,12 @@ class copy : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCopied() { return m_copied; }
int totalFailed() { return m_failedPaths.length(); }
QStringList failed() { return m_failedPaths; }
signals:
void fileCopied(const QString& relativeName);
void copyFailed(const QString& relativeName);
// TODO: maybe add a "shouldCopy" signal in the future?
private:
@ -127,6 +131,7 @@ class copy : public QObject {
QDir m_src;
QDir m_dst;
int m_copied;
QStringList m_failedPaths;
};
struct LinkPair {
@ -471,6 +476,9 @@ class clone : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCloned() { return m_cloned; }
int totalFailed() { return m_failedClones.length(); }
QList<QPair<QString, QString>> failed() { return m_failedClones; }
signals:
void fileCloned(const QString& src, const QString& dst);
@ -485,6 +493,7 @@ class clone : public QObject {
QDir m_src;
QDir m_dst;
int m_cloned;
QList<QPair<QString, QString>> m_failedClones;
};
/**

View File

@ -187,8 +187,8 @@ void LaunchController::login() {
switch(m_accountToUse->accountState()) {
case AccountState::Offline: {
m_session->wants_online = false;
// NOTE: fallthrough is intentional
}
/* fallthrough */
case AccountState::Online: {
if(!m_session->wants_online) {
// we ask the user for a player name
@ -267,8 +267,8 @@ void LaunchController::login() {
// This means some sort of soft error that we can fix with a refresh ... so let's refresh.
case AccountState::Unchecked: {
m_accountToUse->refresh();
// NOTE: fallthrough intentional
}
/* fallthrough */
case AccountState::Working: {
// refresh is in progress, we need to wait for it to finish to proceed.
ProgressDialog progDialog(m_parentWidget);

View File

@ -193,31 +193,21 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
}
case Qt::ToolTipRole:
{
switch(column)
if(column == Name && hasRecommended)
{
case Name:
auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if(value.toBool())
{
if(hasRecommended)
return tr("Recommended");
} else if(hasLatest) {
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if(value.toBool())
{
auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if(value.toBool())
{
return tr("Recommended");
}
else if(hasLatest)
{
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if(value.toBool())
{
return tr("Latest");
}
}
return tr("Latest");
}
}
default:
{
return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
}
} else {
return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
}
}
case Qt::DecorationRole:

View File

@ -45,10 +45,10 @@ QVariant Index::data(const QModelIndex &index, int role) const
switch (role)
{
case Qt::DisplayRole:
switch (index.column())
{
case 0: return list->humanReadable();
default: break;
if (index.column() == 0) {
return list->humanReadable();
} else {
break;
}
case UidRole: return list->uid();
case NameRole: return list->name();

View File

@ -328,6 +328,9 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case AccountState::Gone: {
return tr("Gone", "Account status");
}
default: {
return tr("Unknown", "Account status");
}
}
}
@ -354,11 +357,12 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
return QVariant::fromValue(account);
case Qt::CheckStateRole:
switch (index.column())
{
case ProfileNameColumn:
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
if (index.column() == ProfileNameColumn) {
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
} else {
return QVariant();
}
default:
return QVariant();

View File

@ -273,6 +273,7 @@ void Yggdrasil::processReply() {
AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
);
return;
}
default:
changeState(

View File

@ -74,6 +74,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
break;
}
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
@ -83,6 +84,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT };
break;
}
}
return { 0, false };

View File

@ -91,6 +91,7 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
break;
}
case SortType::VERSION: {
auto this_ver = Version(version());
@ -99,11 +100,13 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
return { 1, type == SortType::VERSION };
if (this_ver < other_ver)
return { -1, type == SortType::VERSION };
break;
}
case SortType::PROVIDER: {
auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
if (compare_result != 0)
return { compare_result, type == SortType::PROVIDER };
break;
}
}
return { 0, false };
@ -166,6 +169,13 @@ auto Mod::homeurl() const -> QString
return details().homeurl;
}
auto Mod::metaurl() const -> QString
{
if (metadata() == nullptr)
return homeurl();
return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id);
}
auto Mod::description() const -> QString
{
return details().description;

View File

@ -70,6 +70,7 @@ public:
auto provider() const -> std::optional<QString>;
auto licenses() const -> const QList<ModLicense>&;
auto issueTracker() const -> QString;
auto metaurl() const -> QString;
/** Get the intneral path to the mod's icon file*/
QString iconPath() const { return m_local_details.icon_file; };

View File

@ -71,6 +71,7 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
return { 1, type == SortType::ENABLED };
if (!enabled() && other.enabled())
return { -1, type == SortType::ENABLED };
break;
case SortType::NAME: {
QString this_name{ name() };
QString other_name{ other.name() };
@ -81,12 +82,14 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
if (compare_result != 0)
return { compare_result, type == SortType::NAME };
break;
}
case SortType::DATE:
if (dateTimeChanged() > other.dateTimeChanged())
return { 1, type == SortType::DATE };
if (dateTimeChanged() < other.dateTimeChanged())
return { -1, type == SortType::DATE };
break;
}
return { 0, false };

View File

@ -102,6 +102,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
break;
}
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
@ -111,6 +112,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT };
break;
}
}
return { 0, false };

View File

@ -70,11 +70,17 @@ auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString t
}
QCryptographicHash hash(algo);
if(!hash.addData(device))
if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!";
Q_ASSERT(hash.result().length() == hash.hashLength(algo));
return { hash.result().toHex() };
}
QString getMetaURL(ResourceProvider provider, QVariant projectID)
{
return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") +
projectID.toString();
}
} // namespace ModPlatform

View File

@ -144,6 +144,7 @@ inline auto getOverrideDeps() -> QList<OverrideDep>
{ "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH },
{ "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } };
};
QString getMetaURL(ResourceProvider provider, QVariant projectID);
} // namespace ModPlatform

View File

@ -470,8 +470,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
// fallthrough intentional, we treat these as plain old mods and dump them wherever.
}
/* fallthrough */
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {

View File

@ -454,6 +454,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const
return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1);
}
}
qWarning("TranslationModel::data not implemented when role is DisplayRole");
}
case Qt::ToolTipRole:
{

View File

@ -927,21 +927,8 @@ void MainWindow::onCatToggled(bool state)
void MainWindow::setCatBackground(bool enabled)
{
if (enabled) {
view->setStyleSheet(QString(R"(
InstanceView
{
background-image: url(:/backgrounds/%1);
background-attachment: fixed;
background-clip: padding;
background-position: bottom right;
background-repeat: none;
background-color:palette(base);
})")
.arg(ThemeManager::getCatImage()));
} else {
view->setStyleSheet(QString());
}
view->setPaintCat(enabled);
view->viewport()->repaint();
}
void MainWindow::runModalTask(Task *task)

View File

@ -34,6 +34,7 @@
*/
#include "ProgressDialog.h"
#include <QPoint>
#include "ui_ProgressDialog.h"
#include <limits>
@ -68,6 +69,7 @@ ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::Pr
setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true);
setSkipButton(false);
changeProgress(0, 100);
updateSize();
}
void ProgressDialog::setSkipButton(bool present, QString label)
@ -96,22 +98,29 @@ ProgressDialog::~ProgressDialog()
void ProgressDialog::updateSize()
{
QSize lastSize = this->size();
QSize qSize = QSize(480, minimumSizeHint().height());
QPoint lastPos = this->pos();
int minHeight = ui->globalStatusDetailsLabel->minimumSize().height() + (ui->verticalLayout->spacing() * 2);
minHeight += ui->globalProgressBar->minimumSize().height() + ui->verticalLayout->spacing();
if (!ui->taskProgressScrollArea->isHidden())
minHeight += ui->taskProgressScrollArea->minimumSizeHint().height() + ui->verticalLayout->spacing();
if (ui->skipButton->isVisible())
minHeight += ui->skipButton->height() + ui->verticalLayout->spacing();
minHeight = std::max(minHeight, 60);
QSize minSize = QSize(480, minHeight);
setMinimumSize(minSize);
adjustSize();
QSize newSize = this->size();
// if the current window is too small
if ((lastSize != qSize) && (lastSize.height() < qSize.height()))
if ((lastSize != newSize) && (lastSize.height() < newSize.height()))
{
resize(qSize);
// keep the dialog in the center after a resize
this->move(
this->parentWidget()->x() + (this->parentWidget()->width() - this->width()) / 2,
this->parentWidget()->y() + (this->parentWidget()->height() - this->height()) / 2
);
QSize sizeDiff = lastSize - newSize; // last size was smaller, the results should be negative
// center on old position after resize
QPoint newPos(lastPos.x() + (sizeDiff.width() / 2), lastPos.y() + (sizeDiff.height() / 2));
this->move(newPos);
}
setMinimumSize(qSize);
}
int ProgressDialog::execWithTask(Task* task)
@ -201,7 +210,9 @@ void ProgressDialog::onTaskSucceeded()
void ProgressDialog::changeStatus(const QString& status)
{
ui->globalStatusLabel->setText(task->getStatus());
ui->globalStatusLabel->adjustSize();
ui->globalStatusDetailsLabel->setText(task->getDetails());
ui->globalStatusDetailsLabel->adjustSize();
updateSize();
}

View File

@ -248,8 +248,8 @@ bool AccessibleInstanceView::selectColumn(int column)
if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) {
return false;
}
// fallthrough intentional
}
/* fallthrough */
case QAbstractItemView::ContiguousSelection: {
if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
view()->clearSelection();

View File

@ -48,6 +48,7 @@
#include <QAccessible>
#include "VisualGroup.h"
#include "ui/themes/ThemeManager.h"
#include <QDebug>
#include <Application.h>
@ -73,6 +74,7 @@ InstanceView::InstanceView(QWidget *parent)
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
setAcceptDrops(true);
setAutoScroll(true);
setPaintCat(APPLICATION->settings()->get("TheCat").toBool());
}
InstanceView::~InstanceView()
@ -498,12 +500,34 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent *event)
}
}
void InstanceView::paintEvent(QPaintEvent *event)
void InstanceView::setPaintCat(bool visible)
{
m_catVisible = visible;
if (visible)
m_catPixmap.load(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage()));
else
m_catPixmap = QPixmap();
}
void InstanceView::paintEvent(QPaintEvent* event)
{
executeDelayedItemsLayout();
QPainter painter(this->viewport());
if (m_catVisible) {
int widWidth = this->viewport()->width();
int widHeight = this->viewport()->height();
if (m_catPixmap.width() < widWidth)
widWidth = m_catPixmap.width();
if (m_catPixmap.height() < widHeight)
widHeight = m_catPixmap.height();
auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio);
QRect rectOfPixmap = pixmap.rect();
rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight());
painter.drawPixmap(rectOfPixmap.topLeft(), pixmap);
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QStyleOptionViewItem option;
initViewItemOption(&option);

View File

@ -85,10 +85,8 @@ public:
virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override;
int spacing() const
{
return m_spacing;
};
int spacing() const { return m_spacing; };
void setPaintCat(bool visible);
public slots:
virtual void updateGeometries() override;
@ -139,6 +137,8 @@ private:
int m_currentItemsPerRow = -1;
int m_currentCursorColumn= -1;
mutable QCache<int, QRect> geometryCache;
bool m_catVisible = false;
QPixmap m_catPixmap;
// point where the currently active mouse action started in geometry coordinates
QPoint m_pressedPosition;

View File

@ -81,6 +81,8 @@ APIPage::APIPage(QWidget *parent) :
connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder);
// This function needs to be called even when the ComboBox's index is still in its default state.
updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex());
// NOTE: this allows http://, but we replace that with https later anyway
ui->metaURL->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->metaURL));
ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry));
ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID));
ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey));
@ -163,7 +165,7 @@ void APIPage::applySettings()
QString msaClientID = ui->msaClientID->text();
s->set("MSAClientIDOverride", msaClientID);
QUrl metaURL = ui->metaURL->text();
QUrl metaURL(ui->metaURL->text());
// Add required trailing slash
if (!metaURL.isEmpty() && !metaURL.path().endsWith('/'))
{

View File

@ -157,6 +157,17 @@
<string>Try to check or update all selected resources (all resources if none are selected)</string>
</property>
</action>
<action name="actionVisitItemPage">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Visit mod's page</string>
</property>
<property name="toolTip">
<string>Go to mods home page</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -45,6 +45,7 @@
#include <QMenu>
#include <QMessageBox>
#include <QSortFilterProxyModel>
#include <algorithm>
#include "Application.h"
@ -60,6 +61,7 @@
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "Version.h"
@ -86,12 +88,30 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
ui->actionsToolbar->insertActionAfter(ui->actionViewFolder, ui->actionVisitItemPage);
connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages);
auto check_allow_update = [this] {
return (!m_instance || !m_instance->isRunning()) && (ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
};
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
[this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
ui->actionUpdateItem->setEnabled(check_allow_update());
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
auto mods_list = m_model->selectedMods(selection);
auto selected = std::count_if(mods_list.cbegin(), mods_list.cend(),
[](Mod* v) { return v->metadata() != nullptr || v->homeurl().size() != 0; });
if (selected <= 1) {
ui->actionVisitItemPage->setText(tr("Visit mod's page"));
ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
} else {
ui->actionVisitItemPage->setText(tr("Visit mods' pages"));
ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods"));
}
ui->actionVisitItemPage->setEnabled(selected != 0);
});
connect(mods.get(), &ModFolderModel::rowsInserted, this,
[this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
@ -133,7 +153,7 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI
return true;
}
void ModFolderPage::removeItems(const QItemSelection &selection)
void ModFolderPage::removeItems(const QItemSelection& selection)
{
m_model->deleteMods(selection.indexes());
}
@ -207,8 +227,7 @@ void ModFolderPage::updateMods()
message = tr("All selected mods are up-to-date! :)");
}
}
CustomMessageBox::selectable(this, tr("Update checker"), message)
->exec();
CustomMessageBox::selectable(this, tr("Update checker"), message)->exec();
return;
}
@ -275,3 +294,13 @@ bool NilModFolderPage::shouldDisplay() const
{
return m_model->dir().exists();
}
void ModFolderPage::visitModPages()
{
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
for (auto mod : m_model->selectedMods(selection)) {
auto url = mod->metaurl();
if (!url.isEmpty())
DesktopServices::openUrl(url);
}
}

View File

@ -4,6 +4,7 @@
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -60,10 +61,11 @@ class ModFolderPage : public ExternalResourcesPage {
private slots:
void runningStateChanged(bool running);
void removeItems(const QItemSelection &selection) override;
void removeItems(const QItemSelection& selection) override;
void installMods();
void updateMods();
void visitModPages();
protected:
std::shared_ptr<ModFolderModel> m_model;

View File

@ -97,37 +97,30 @@ public:
return;
if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0))
return;
int tries = 5;
while (tries)
{
if (!m_cache->stale(m_path))
return;
QImage image(m_path);
if (image.isNull())
{
QThread::msleep(500);
tries--;
continue;
}
QImage small;
if (image.width() > image.height())
small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
else
small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
QImage square(QSize(256, 256), QImage::Format_ARGB32);
square.fill(Qt::transparent);
QPainter painter(&square);
painter.drawImage(offset, small);
painter.end();
QIcon icon(QPixmap::fromImage(square));
m_cache->add(m_path, icon);
m_resultEmitter.emitResultsReady(m_path);
if (!m_cache->stale(m_path))
return;
QImage image(m_path);
if (image.isNull()) {
m_resultEmitter.emitResultsFailed(m_path);
qDebug() << "Error loading screenshot: " + m_path + ". Perhaps too large?";
return;
}
m_resultEmitter.emitResultsFailed(m_path);
QImage small;
if (image.width() > image.height())
small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
else
small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
QImage square(QSize(256, 256), QImage::Format_ARGB32);
square.fill(Qt::transparent);
QPainter painter(&square);
painter.drawImage(offset, small);
painter.end();
QIcon icon(QPixmap::fromImage(square));
m_cache->add(m_path, icon);
m_resultEmitter.emitResultsReady(m_path);
}
QString m_path;
SharedIconCachePtr m_cache;
@ -146,9 +139,12 @@ public:
m_thumbnailCache = std::make_shared<SharedIconCache>();
m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder"));
connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
// FIXME: the watched file set is not updated when files are removed
}
virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); }
virtual ~FilterModel() {
m_thumbnailingPool.clear();
if (!m_thumbnailingPool.waitForDone(500))
qDebug() << "Thumbnail pool took longer than 500ms to finish";
}
virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const
{
auto model = sourceModel();
@ -215,10 +211,12 @@ private slots:
void fileChanged(QString filepath)
{
m_thumbnailCache->setStale(filepath);
thumbnailImage(filepath);
// reinsert the path...
watcher.removePath(filepath);
watcher.addPath(filepath);
if (QFile::exists(filepath)) {
watcher.addPath(filepath);
thumbnailImage(filepath);
}
}
private:

View File

@ -339,6 +339,7 @@ void WorldListPage::mceditState(LoggedProcess::State state)
{
failed = true;
}
/* fallthrough */
case LoggedProcess::Running:
case LoggedProcess::Finished:
{

View File

@ -69,6 +69,7 @@ bool JavaWizardPage::validatePage()
case JavaSettingsWidget::ValidationStatus::AllOK:
{
settings->set("JavaPath", m_java_widget->javaPath());
return true;
}
case JavaSettingsWidget::ValidationStatus::JavaBad:
{

View File

@ -1,54 +1,70 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* 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, version 3.
*
* 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 <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 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.
*/
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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, version 3.
*
* 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 <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 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.
*/
#include <QLabel>
#include <QMessageBox>
#include <QToolTip>
#include "InfoFrame.h"
#include "ui_InfoFrame.h"
#include "ui/dialogs/CustomMessageBox.h"
InfoFrame::InfoFrame(QWidget *parent) :
QFrame(parent),
ui(new Ui::InfoFrame)
void setupLinkToolTip(QLabel* label)
{
QObject::connect(label, &QLabel::linkHovered, [label](const QString& link) {
if (auto url = QUrl(link); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https"))
return;
label->setToolTip(link);
});
}
InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame)
{
ui->setupUi(this);
ui->descriptionLabel->setHidden(true);
ui->nameLabel->setHidden(true);
ui->licenseLabel->setHidden(true);
ui->issueTrackerLabel->setHidden(true);
setupLinkToolTip(ui->iconLabel);
setupLinkToolTip(ui->descriptionLabel);
setupLinkToolTip(ui->nameLabel);
setupLinkToolTip(ui->licenseLabel);
setupLinkToolTip(ui->issueTrackerLabel);
updateHiddenState();
}
@ -59,45 +75,43 @@ InfoFrame::~InfoFrame()
void InfoFrame::updateWithMod(Mod const& m)
{
if (m.type() == ResourceType::FOLDER)
{
if (m.type() == ResourceType::FOLDER) {
clear();
return;
}
QString text = "";
QString name = "";
QString link = m.metaurl();
if (m.name().isEmpty())
name = m.internal_id();
else
name = m.name();
if (m.homeurl().isEmpty())
if (link.isEmpty())
text = name;
else
text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>";
else {
text = "<a href=\"" + link + "\">" + name + "</a>";
}
if (!m.authors().isEmpty())
text += " by " + m.authors().join(", ");
setName(text);
if (m.description().isEmpty())
{
if (m.description().isEmpty()) {
setDescription(QString());
}
else
{
} else {
setDescription(m.description());
}
setImage(m.icon({64,64}));
setImage(m.icon({ 64, 64 }));
auto licenses = m.licenses();
QString licenseText = "";
if (!licenses.empty()) {
for (auto l : licenses) {
if (!licenseText.isEmpty()) {
licenseText += "\n"; // add newline between licenses
licenseText += "\n"; // add newline between licenses
}
if (!l.name.isEmpty()) {
if (l.url.isEmpty()) {
@ -109,9 +123,9 @@ void InfoFrame::updateWithMod(Mod const& m)
licenseText += "<a href=\"" + l.url + "\">" + l.url + "</a>";
}
if (!l.description.isEmpty() && l.description != l.name) {
licenseText += " " + l.description;
licenseText += " " + l.description;
}
}
}
}
if (!licenseText.isEmpty()) {
setLicense(tr("License: %1").arg(licenseText));
@ -123,7 +137,7 @@ void InfoFrame::updateWithMod(Mod const& m)
if (!m.issueTracker().isEmpty()) {
issueTracker += tr("Report issues to: ");
issueTracker += "<a href=\"" + m.issueTracker() + "\">" + m.issueTracker() + "</a>";
}
}
setIssueTracker(issueTracker);
}
@ -133,7 +147,8 @@ void InfoFrame::updateWithResource(const Resource& resource)
setImage();
}
QString InfoFrame::renderColorCodes(QString input) {
QString InfoFrame::renderColorCodes(QString input)
{
// We have to manually set the colors for use.
//
// A color is set using §x, with x = a hex number from 0 to f.
@ -144,16 +159,12 @@ QString InfoFrame::renderColorCodes(QString input) {
// TODO: Wrap links inside <a> tags
// https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes
const QMap<QChar, QString> color_codes_map = {
{'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"},
{'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"},
{'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"},
{'f', "#FFFFFF"}
};
const QMap<QChar, QString> color_codes_map = { { '0', "#000000" }, { '1', "#0000AA" }, { '2', "#00AA00" }, { '3', "#00AAAA" },
{ '4', "#AA0000" }, { '5', "#AA00AA" }, { '6', "#FFAA00" }, { '7', "#AAAAAA" },
{ '8', "#555555" }, { '9', "#5555FF" }, { 'a', "#55FF55" }, { 'b', "#55FFFF" },
{ 'c', "#FF5555" }, { 'd', "#FF55FF" }, { 'e', "#FFFF55" }, { 'f', "#FFFFFF" } };
// https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes
const QMap<QChar, QString> formatting_codes_map = {
{'l', "b"}, {'m', "s"}, {'n', "u"}, {'o', "i"}
};
const QMap<QChar, QString> formatting_codes_map = { { 'l', "b" }, { 'm', "s" }, { 'n', "u" }, { 'o', "i" } };
QString html("<html>");
QList<QString> tags{};
@ -198,14 +209,14 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
{
setName(renderColorCodes(resource_pack.name()));
setDescription(renderColorCodes(resource_pack.description()));
setImage(resource_pack.image({64, 64}));
setImage(resource_pack.image({ 64, 64 }));
}
void InfoFrame::updateWithTexturePack(TexturePack& texture_pack)
{
setName(renderColorCodes(texture_pack.name()));
setDescription(renderColorCodes(texture_pack.description()));
setImage(texture_pack.image({64, 64}));
setImage(texture_pack.image({ 64, 64 }));
}
void InfoFrame::clear()
@ -229,12 +240,9 @@ void InfoFrame::updateHiddenState()
void InfoFrame::setName(QString text)
{
if(text.isEmpty())
{
if (text.isEmpty()) {
ui->nameLabel->setHidden(true);
}
else
{
} else {
ui->nameLabel->setText(text);
ui->nameLabel->setHidden(false);
}
@ -243,14 +251,11 @@ void InfoFrame::setName(QString text)
void InfoFrame::setDescription(QString text)
{
if(text.isEmpty())
{
if (text.isEmpty()) {
ui->descriptionLabel->setHidden(true);
updateHiddenState();
return;
}
else
{
} else {
ui->descriptionLabel->setHidden(false);
updateHiddenState();
}
@ -260,9 +265,8 @@ void InfoFrame::setDescription(QString text)
QChar rem('\n');
QString finaltext;
finaltext.reserve(intermediatetext.size());
foreach(const QChar& c, intermediatetext)
{
if(c == rem && prev){
foreach (const QChar& c, intermediatetext) {
if (c == rem && prev) {
continue;
}
prev = c == rem;
@ -270,17 +274,14 @@ void InfoFrame::setDescription(QString text)
}
QString labeltext;
labeltext.reserve(300);
if(finaltext.length() > 290)
{
if (finaltext.length() > 290) {
ui->descriptionLabel->setOpenExternalLinks(false);
ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText);
m_description = text;
// This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
}
else
{
} else {
ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
labeltext.append(finaltext);
}
@ -289,14 +290,11 @@ void InfoFrame::setDescription(QString text)
void InfoFrame::setLicense(QString text)
{
if(text.isEmpty())
{
if (text.isEmpty()) {
ui->licenseLabel->setHidden(true);
updateHiddenState();
return;
}
else
{
} else {
ui->licenseLabel->setHidden(false);
updateHiddenState();
}
@ -306,9 +304,8 @@ void InfoFrame::setLicense(QString text)
QChar rem('\n');
QString finaltext;
finaltext.reserve(intermediatetext.size());
foreach(const QChar& c, intermediatetext)
{
if(c == rem && prev){
foreach (const QChar& c, intermediatetext) {
if (c == rem && prev) {
continue;
}
prev = c == rem;
@ -316,17 +313,14 @@ void InfoFrame::setLicense(QString text)
}
QString labeltext;
labeltext.reserve(300);
if(finaltext.length() > 290)
{
if (finaltext.length() > 290) {
ui->licenseLabel->setOpenExternalLinks(false);
ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText);
m_description = text;
// This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler);
}
else
{
} else {
ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText);
labeltext.append(finaltext);
}
@ -335,12 +329,9 @@ void InfoFrame::setLicense(QString text)
void InfoFrame::setIssueTracker(QString text)
{
if(text.isEmpty())
{
if (text.isEmpty()) {
ui->issueTrackerLabel->setHidden(true);
}
else
{
} else {
ui->issueTrackerLabel->setText(text);
ui->issueTrackerLabel->setHidden(false);
}
@ -359,28 +350,22 @@ void InfoFrame::setImage(QPixmap img)
void InfoFrame::descriptionEllipsisHandler(QString link)
{
if(!m_current_box)
{
if (!m_current_box) {
m_current_box = CustomMessageBox::selectable(this, "", m_description);
connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
m_current_box->show();
}
else
{
} else {
m_current_box->setText(m_description);
}
}
void InfoFrame::licenseEllipsisHandler(QString link)
{
if(!m_current_box)
{
if (!m_current_box) {
m_current_box = CustomMessageBox::selectable(this, "", m_license);
connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
m_current_box->show();
}
else
{
} else {
m_current_box->setText(m_license);
}
}

View File

@ -1,16 +1,36 @@
/* Copyright 2013-2021 MultiMC Contributors
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
* 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, version 3.
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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.
*
* 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.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 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
@ -21,8 +41,7 @@
#include "minecraft/mod/ResourcePack.h"
#include "minecraft/mod/TexturePack.h"
namespace Ui
{
namespace Ui {
class InfoFrame;
}

View File

@ -89,8 +89,10 @@ void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev)
switch (prev.len) {
case 3:
prev.h ^= data[2] << 16;
/* fall through */
case 2:
prev.h ^= data[1] << 8;
/* fall through */
case 1:
prev.h ^= data[0];
prev.h *= m;