+#include
+#include
+#include
+#include
+#include
+
+#include "cubiomes/biomes.h"
+#include "cubiomes/finders.h"
+#include "cubiomes/generator.h"
+
+// 计算以centerX, centerZ为中心的挂机点,24格之外128格之内的史莱姆区块覆盖面积
+static int calculateSlimeArea(uint64_t seed, int centerX, int centerZ, bool includeBiome, Generator *g, int mc)
+{
+ const int MIN_DIST = 24; // 24格之外
+ const int MAX_DIST = 128; // 128格之内
+ const int MIN_DIST2 = MIN_DIST * MIN_DIST;
+ const int MAX_DIST2 = MAX_DIST * MAX_DIST;
+
+ int totalArea = 0;
+
+ // 计算需要检查的区块范围
+ int minChunkX = (centerX - MAX_DIST) >> 4;
+ int maxChunkX = (centerX + MAX_DIST) >> 4;
+ int minChunkZ = (centerZ - MAX_DIST) >> 4;
+ int maxChunkZ = (centerZ + MAX_DIST) >> 4;
+
+ for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++)
+ {
+ for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++)
+ {
+ // 检查是否是史莱姆区块
+ if (!isSlimeChunk(seed, chunkX, chunkZ))
+ continue;
+
+ // 计算区块的边界坐标
+ int chunkMinX = chunkX << 4;
+ int chunkMaxX = chunkMinX + 15;
+ int chunkMinZ = chunkZ << 4;
+ int chunkMaxZ = chunkMinZ + 15;
+
+ // 计算区块四个角点到挂机点的距离平方
+ int dx1 = chunkMinX - centerX;
+ int dz1 = chunkMinZ - centerZ;
+ int dist2_1 = dx1 * dx1 + dz1 * dz1;
+
+ int dx2 = chunkMaxX - centerX;
+ int dz2 = chunkMinZ - centerZ;
+ int dist2_2 = dx2 * dx2 + dz2 * dz2;
+
+ int dx3 = chunkMinX - centerX;
+ int dz3 = chunkMaxZ - centerZ;
+ int dist2_3 = dx3 * dx3 + dz3 * dz3;
+
+ int dx4 = chunkMaxX - centerX;
+ int dz4 = chunkMaxZ - centerZ;
+ int dist2_4 = dx4 * dx4 + dz4 * dz4;
+
+ // 找出区块四个角点的最小和最大距离平方
+ int minDist2 = dist2_1;
+ if (dist2_2 < minDist2) minDist2 = dist2_2;
+ if (dist2_3 < minDist2) minDist2 = dist2_3;
+ if (dist2_4 < minDist2) minDist2 = dist2_4;
+
+ int maxDist2 = dist2_1;
+ if (dist2_2 > maxDist2) maxDist2 = dist2_2;
+ if (dist2_3 > maxDist2) maxDist2 = dist2_3;
+ if (dist2_4 > maxDist2) maxDist2 = dist2_4;
+
+ // 如果区块完全在24圆内或完全在128圆外,跳过
+ if (maxDist2 < MIN_DIST2 || minDist2 > MAX_DIST2)
+ continue;
+
+ // 计算区块内实际在24-128范围内的面积
+ // 遍历区块内的每个方块,计算在范围内的面积
+ int chunkArea = 0;
+ for (int z = chunkMinZ; z <= chunkMaxZ; z++)
+ {
+ for (int x = chunkMinX; x <= chunkMaxX; x++)
+ {
+ int dx = x - centerX;
+ int dz = z - centerZ;
+ int dist2 = dx * dx + dz * dz;
+
+ // 只计算在24格之外128格之内的方块
+ if (dist2 >= MIN_DIST2 && dist2 <= MAX_DIST2)
+ {
+ chunkArea++;
+ }
+ }
+ }
+
+ // 如果没有面积在范围内,跳过
+ if (chunkArea == 0)
+ continue;
+
+ // 应用群系修正
+ if (includeBiome && g)
+ {
+ // 使用区块中心取样群系
+ int chunkCenterX = (chunkX << 4) + 8;
+ int chunkCenterZ = (chunkZ << 4) + 8;
+ int biomeId = none;
+
+ if (mc >= MC_1_18)
+ {
+ // MC 1.18+:在y=-64取样群系,使用scale=4
+ int y = -64 >> 2; // -16 for scale=4
+ biomeId = getBiomeAt(g, 4, chunkCenterX >> 2, y, chunkCenterZ >> 2);
+ }
+ else
+ {
+ // MC 1.17及以前:在y=0取样群系
+ biomeId = getBiomeAt(g, 4, chunkCenterX >> 2, 0, chunkCenterZ >> 2);
+ }
+
+ // 根据版本决定哪些群系需要处理
+ // 1.17及以前:仅计算河流和蘑菇岛的减成
+ // 1.18:增加溶洞的减成
+ // 1.19:增加深暗的减成
+
+ // 蘑菇岛不算(所有版本)
+ if (biomeId == mushroom_fields)
+ {
+ chunkArea = 0;
+ }
+ // 深暗之域不算(1.19+)
+ else if (mc >= MC_1_19 && biomeId == deep_dark)
+ {
+ chunkArea = 0;
+ }
+ else
+ {
+ // 河流的面积乘以5/6(所有版本)
+ if (biomeId == river)
+ {
+ chunkArea = (chunkArea * 5) / 6;
+ }
+ // 溶洞的面积乘以5/6(1.18+)
+ else if (mc >= MC_1_18 && biomeId == dripstone_caves)
+ {
+ chunkArea = (chunkArea * 5) / 6;
+ }
+ // Old Growth Pine Taiga的面积乘以0.95(所有版本)
+ else if (biomeId == old_growth_pine_taiga)
+ {
+ chunkArea = (chunkArea * 95) / 100;
+ }
+ // 其他群系面积不变
+ }
+ }
+
+ totalArea += chunkArea;
+ }
+ }
+
+ return totalArea;
+}
+
+void AnalysisSlime::run()
+{
+ stop = false;
+
+ Generator g;
+ setupGenerator(&g, wi.mc, wi.large);
+
+ if (includeBiome)
+ {
+ applySeed(&g, DIM_OVERWORLD, wi.seed);
+ }
+
+ for (idx = 0; idx < (long)seeds.size(); idx++)
+ {
+ if (stop) break;
+
+ uint64_t seed = seeds[idx];
+ wi.seed = seed;
+
+ if (includeBiome)
+ {
+ applySeed(&g, DIM_OVERWORLD, seed);
+ }
+
+ QList results;
+
+ // 遍历所有可能的挂机点(xz都是16的倍数)
+ // 确保坐标是16的倍数
+ // startX: >= x1 的最小的16的倍数
+ // endX: <= x2 的最大的16的倍数
+ int startX = (x1 % 16 == 0) ? x1 : ((x1 < 0) ? ((x1 / 16) - 1) * 16 : ((x1 / 16) + 1) * 16);
+ int startZ = (z1 % 16 == 0) ? z1 : ((z1 < 0) ? ((z1 / 16) - 1) * 16 : ((z1 / 16) + 1) * 16);
+ int endX = (x2 / 16) * 16;
+ int endZ = (z2 / 16) * 16;
+
+ for (int centerZ = startZ; centerZ <= endZ && !stop; centerZ += 16)
+ {
+ for (int centerX = startX; centerX <= endX && !stop; centerX += 16)
+ {
+ int area = calculateSlimeArea(seed, centerX, centerZ, includeBiome, includeBiome ? &g : nullptr, wi.mc);
+
+ if (area >= minArea)
+ {
+ SlimeResult result;
+ result.centerX = centerX;
+ result.centerZ = centerZ;
+ result.area = area;
+ results.append(result);
+ }
+ }
+ }
+
+ if (!results.empty() && !stop)
+ {
+ emit seedDone(seed, results);
+ }
+ }
+}
+
+TabSlime::TabSlime(MainWindow *parent)
+ : QWidget(parent)
+ , ui(new Ui::TabSlime)
+ , parent(parent)
+ , thread()
+{
+ ui->setupUi(this);
+
+ // 设置翻译文本
+ ui->labelDescription->setText(tr("Calculate optimal AFK positions for slime chunk coverage (24-128 blocks radius)."));
+ ui->label->setText(tr("Seed(s):"));
+ ui->comboSeedSource->setItemText(0, tr("Current seed"));
+ ui->comboSeedSource->setItemText(1, tr("From matching seeds list"));
+ ui->labelX1->setText(QString("X1:
"));
+ ui->labelZ1->setText(QString("Z1:
"));
+ ui->labelX2->setText(QString("X2:
"));
+ ui->labelZ2->setText(QString("Z2:
"));
+ ui->buttonFromVisible->setText(tr("From visible"));
+ ui->labelMinArea->setText(tr("Minimum area:"));
+ ui->checkIncludeBiome->setText(tr("Include biome parameters"));
+ ui->checkIncludeBiome->setToolTip(tr("Sample biomes at y=-64 (for MC 1.18+). Exclude mushroom fields and deep dark. Apply multipliers for dripstone caves/rivers (5/6) and old growth pine taiga (0.95).
"));
+ if (ui->treeResults->headerItem())
+ {
+ ui->treeResults->headerItem()->setText(0, tr("seed"));
+ ui->treeResults->headerItem()->setText(1, tr("area"));
+ ui->treeResults->headerItem()->setText(2, tr("x"));
+ ui->treeResults->headerItem()->setText(3, tr("z"));
+ }
+ ui->pushExport->setText(tr("Export..."));
+ ui->pushStart->setText(tr("Analyze"));
+
+ updt = 20;
+ nextupdate = 0;
+
+ QIntValidator *intval = new QIntValidator(-60e6, 60e6, this);
+ ui->lineX1->setValidator(intval);
+ ui->lineZ1->setValidator(intval);
+ ui->lineX2->setValidator(intval);
+ ui->lineZ2->setValidator(intval);
+
+ ui->lineMinArea->setValidator(new QIntValidator(1, INT_MAX, this));
+ ui->lineMinArea->setText("10000");
+
+ ui->treeResults->setSortingEnabled(true);
+ ui->treeResults->sortByColumn(1, Qt::DescendingOrder); // 按面积降序排列
+
+ connect(&thread, &AnalysisSlime::seedDone, this, &TabSlime::onAnalysisSeedDone, Qt::BlockingQueuedConnection);
+ connect(&thread, &AnalysisSlime::finished, this, &TabSlime::onAnalysisFinished);
+
+ // 连接信号槽,实现双向同步
+ connect(ui->lineX1, &QLineEdit::editingFinished, this, &TabSlime::on_lineX1_editingFinished);
+ connect(ui->lineZ1, &QLineEdit::editingFinished, this, &TabSlime::on_lineZ1_editingFinished);
+ connect(ui->lineX2, &QLineEdit::editingFinished, this, &TabSlime::on_lineX2_editingFinished);
+ connect(ui->lineZ2, &QLineEdit::editingFinished, this, &TabSlime::on_lineZ2_editingFinished);
+ connect(ui->comboSeedSource, QOverload::of(&QComboBox::currentIndexChanged), this, &TabSlime::on_comboSeedSource_currentIndexChanged);
+}
+
+TabSlime::~TabSlime()
+{
+ thread.stop = true;
+ thread.wait(500);
+ delete ui;
+}
+
+bool TabSlime::event(QEvent *e)
+{
+ if (e->type() == QEvent::LayoutRequest)
+ {
+ QFontMetrics fm = QFontMetrics(ui->treeResults->font());
+ ui->treeResults->setColumnWidth(0, txtWidth(fm) * 24);
+ ui->treeResults->setColumnWidth(1, txtWidth(fm) * 16);
+ ui->treeResults->setColumnWidth(2, txtWidth(fm) * 9);
+ ui->treeResults->setColumnWidth(3, txtWidth(fm) * 9);
+ }
+ return QWidget::event(e);
+}
+
+void TabSlime::save(QSettings& settings)
+{
+ int x1 = ui->lineX1->text().toInt();
+ int z1 = ui->lineZ1->text().toInt();
+ int x2 = ui->lineX2->text().toInt();
+ int z2 = ui->lineZ2->text().toInt();
+ int seedsrc = ui->comboSeedSource->currentIndex();
+
+ // 保存到共享的键,与其他tab同步
+ settings.setValue("analysis/x1", x1);
+ settings.setValue("analysis/z1", z1);
+ settings.setValue("analysis/x2", x2);
+ settings.setValue("analysis/z2", z2);
+ settings.setValue("analysis/seedsrc", seedsrc);
+
+ // 也保存到自己的键
+ settings.setValue("slime/x1", x1);
+ settings.setValue("slime/z1", z1);
+ settings.setValue("slime/x2", x2);
+ settings.setValue("slime/z2", z2);
+ settings.setValue("slime/seedsrc", seedsrc);
+ settings.setValue("slime/minarea", ui->lineMinArea->text().toInt());
+ settings.setValue("slime/includebiome", ui->checkIncludeBiome->isChecked());
+}
+
+static void loadCheck(QSettings *s, QCheckBox *cb, const char *key)
+{
+ cb->setChecked(s->value(key, cb->isChecked()).toBool());
+}
+static void loadCombo(QSettings *s, QComboBox *combo, const char *key)
+{
+ combo->setCurrentIndex(s->value(key, combo->currentIndex()).toInt());
+}
+static void loadLine(QSettings *s, QLineEdit *line, const char *key)
+{
+ qlonglong x = line->text().toLongLong();
+ line->setText(QString::number(s->value(key, x).toLongLong()));
+}
+
+void TabSlime::load(QSettings& settings)
+{
+ // 优先从其他tab(如tabbiomes)读取坐标和seeds选项
+ QSettings s(APP_STRING, APP_STRING);
+ int x1 = s.value("analysis/x1", ui->lineX1->text().toInt()).toInt();
+ int z1 = s.value("analysis/z1", ui->lineZ1->text().toInt()).toInt();
+ int x2 = s.value("analysis/x2", ui->lineX2->text().toInt()).toInt();
+ int z2 = s.value("analysis/z2", ui->lineZ2->text().toInt()).toInt();
+ int seedsrc = s.value("analysis/seedsrc", ui->comboSeedSource->currentIndex()).toInt();
+
+ // 如果其他tab有值,使用其他tab的值,否则使用自己的保存值
+ if (s.contains("analysis/x1"))
+ {
+ ui->lineX1->setText(QString::number(x1));
+ ui->lineZ1->setText(QString::number(z1));
+ ui->lineX2->setText(QString::number(x2));
+ ui->lineZ2->setText(QString::number(z2));
+ ui->comboSeedSource->setCurrentIndex(seedsrc);
+ }
+ else
+ {
+ loadLine(&settings, ui->lineX1, "slime/x1");
+ loadLine(&settings, ui->lineZ1, "slime/z1");
+ loadLine(&settings, ui->lineX2, "slime/x2");
+ loadLine(&settings, ui->lineZ2, "slime/z2");
+ loadCombo(&settings, ui->comboSeedSource, "slime/seedsrc");
+ }
+
+ loadLine(&settings, ui->lineMinArea, "slime/minarea");
+ loadCheck(&settings, ui->checkIncludeBiome, "slime/includebiome");
+}
+
+void TabSlime::refresh()
+{
+ // 从其他tab同步坐标和seeds选项
+ QSettings settings(APP_STRING, APP_STRING);
+ if (settings.contains("analysis/x1"))
+ {
+ int x1 = settings.value("analysis/x1").toInt();
+ int z1 = settings.value("analysis/z1").toInt();
+ int x2 = settings.value("analysis/x2").toInt();
+ int z2 = settings.value("analysis/z2").toInt();
+ int seedsrc = settings.value("analysis/seedsrc", 0).toInt();
+
+ // 临时断开信号,避免触发保存
+ ui->lineX1->blockSignals(true);
+ ui->lineZ1->blockSignals(true);
+ ui->lineX2->blockSignals(true);
+ ui->lineZ2->blockSignals(true);
+ ui->comboSeedSource->blockSignals(true);
+
+ ui->lineX1->setText(QString::number(x1));
+ ui->lineZ1->setText(QString::number(z1));
+ ui->lineX2->setText(QString::number(x2));
+ ui->lineZ2->setText(QString::number(z2));
+ ui->comboSeedSource->setCurrentIndex(seedsrc);
+
+ ui->lineX1->blockSignals(false);
+ ui->lineZ1->blockSignals(false);
+ ui->lineX2->blockSignals(false);
+ ui->lineZ2->blockSignals(false);
+ ui->comboSeedSource->blockSignals(false);
+ }
+}
+
+void TabSlime::on_lineX1_editingFinished()
+{
+ QSettings settings(APP_STRING, APP_STRING);
+ int x1 = ui->lineX1->text().toInt();
+ settings.setValue("analysis/x1", x1);
+ settings.setValue("slime/x1", x1);
+}
+
+void TabSlime::on_lineZ1_editingFinished()
+{
+ QSettings settings(APP_STRING, APP_STRING);
+ int z1 = ui->lineZ1->text().toInt();
+ settings.setValue("analysis/z1", z1);
+ settings.setValue("slime/z1", z1);
+}
+
+void TabSlime::on_lineX2_editingFinished()
+{
+ QSettings settings(APP_STRING, APP_STRING);
+ int x2 = ui->lineX2->text().toInt();
+ settings.setValue("analysis/x2", x2);
+ settings.setValue("slime/x2", x2);
+}
+
+void TabSlime::on_lineZ2_editingFinished()
+{
+ QSettings settings(APP_STRING, APP_STRING);
+ int z2 = ui->lineZ2->text().toInt();
+ settings.setValue("analysis/z2", z2);
+ settings.setValue("slime/z2", z2);
+}
+
+void TabSlime::on_comboSeedSource_currentIndexChanged(int index)
+{
+ QSettings settings(APP_STRING, APP_STRING);
+ settings.setValue("analysis/seedsrc", index);
+ settings.setValue("slime/seedsrc", index);
+}
+
+void TabSlime::onAnalysisSeedDone(uint64_t seed, QList results)
+{
+ if (results.empty())
+ return;
+
+ // 创建种子父节点
+ QTreeWidgetItem *seeditem = new QTreeWidgetItem();
+ seeditem->setData(0, Qt::DisplayRole, QVariant::fromValue((qlonglong)seed));
+ seeditem->setData(0, Qt::UserRole+0, QVariant::fromValue(seed));
+ seeditem->setData(0, Qt::UserRole+1, QVariant::fromValue((int)DIM_OVERWORLD));
+
+ // 为每个结果创建子节点
+ for (const SlimeResult& result : results)
+ {
+ QTreeWidgetItem *item = new QTreeWidgetItem(seeditem);
+ item->setText(0, "-");
+ item->setData(1, Qt::DisplayRole, QVariant::fromValue(result.area));
+ item->setData(2, Qt::DisplayRole, QVariant::fromValue(result.centerX));
+ item->setData(3, Qt::DisplayRole, QVariant::fromValue(result.centerZ));
+ item->setData(0, Qt::UserRole+0, QVariant::fromValue(seed));
+ item->setData(0, Qt::UserRole+1, QVariant::fromValue((int)DIM_OVERWORLD));
+ item->setData(0, Qt::UserRole+2, QVariant::fromValue(Pos{result.centerX, result.centerZ}));
+ }
+
+ qbufl.push_back(seeditem);
+ quint64 ns = elapsed.nsecsElapsed();
+ if (ns > nextupdate)
+ {
+ nextupdate = ns + updt * 1e6;
+ QTimer::singleShot(updt, this, &TabSlime::onBufferTimeout);
+ }
+}
+
+void TabSlime::onAnalysisFinished()
+{
+ onBufferTimeout();
+ ui->pushStart->setChecked(false);
+ ui->pushStart->setText(tr("Analyze"));
+}
+
+void TabSlime::onBufferTimeout()
+{
+ if (qbufl.empty())
+ return;
+
+ uint64_t t = -elapsed.elapsed();
+
+ ui->treeResults->setSortingEnabled(false);
+ ui->treeResults->setUpdatesEnabled(false);
+ ui->treeResults->addTopLevelItems(qbufl);
+ ui->treeResults->setUpdatesEnabled(true);
+ ui->treeResults->setSortingEnabled(true);
+ qbufl.clear();
+
+ QString progress = QString::asprintf(" (%ld/%zu)", thread.idx.load(), thread.seeds.size());
+ ui->pushStart->setText(tr("Stop") + progress);
+
+ QApplication::processEvents(); // force processing of events so we can time correctly
+
+ t += elapsed.elapsed();
+ if (8*t > updt)
+ updt = 4*t;
+ nextupdate = elapsed.nsecsElapsed() + 1e6 * updt;
+}
+
+void TabSlime::on_pushStart_clicked()
+{
+ if (thread.isRunning())
+ {
+ thread.stop = true;
+ return;
+ }
+
+ // 保存当前设置到共享键
+ QSettings settings(APP_STRING, APP_STRING);
+ save(settings);
+
+ parent->getSeed(&thread.wi);
+ thread.seeds.clear();
+ if (ui->comboSeedSource->currentIndex() == 0)
+ thread.seeds.push_back(thread.wi.seed);
+ else
+ thread.seeds = parent->formControl->getResults();
+
+ if (thread.seeds.empty())
+ {
+ warn(parent, tr("No seeds to process."));
+ return;
+ }
+
+ int x1 = ui->lineX1->text().toInt();
+ int z1 = ui->lineZ1->text().toInt();
+ int x2 = ui->lineX2->text().toInt();
+ int z2 = ui->lineZ2->text().toInt();
+ if (x2 < x1) std::swap(x1, x2);
+ if (z2 < z1) std::swap(z1, z2);
+
+ thread.x1 = x1;
+ thread.z1 = z1;
+ thread.x2 = x2;
+ thread.z2 = z2;
+ thread.minArea = ui->lineMinArea->text().toInt();
+ thread.includeBiome = ui->checkIncludeBiome->isChecked();
+
+ if (thread.minArea <= 0)
+ thread.minArea = 10000;
+
+ ui->treeResults->setSortingEnabled(false);
+ while (ui->treeResults->topLevelItemCount() > 0)
+ delete ui->treeResults->takeTopLevelItem(0);
+ ui->treeResults->setSortingEnabled(true);
+
+ updt = 20;
+ nextupdate = 0;
+ elapsed.start();
+
+ ui->pushExport->setEnabled(false);
+ ui->pushStart->setChecked(true);
+ QString progress = QString::asprintf(" (0/%zu)", thread.seeds.size());
+ ui->pushStart->setText(tr("Stop") + progress);
+ thread.start();
+}
+
+static void csvline(QTextStream& stream, const QString& qte, const QString& sep, QStringList& cols)
+{
+ if (qte.isEmpty())
+ {
+ for (QString& s : cols)
+ if (s.contains(sep))
+ s = "\"" + s + "\"";
+ }
+ stream << qte << cols.join(sep) << qte << "\n";
+}
+
+void TabSlime::exportResults(QTextStream& stream)
+{
+ QString qte = parent->config.quote;
+ QString sep = parent->config.separator;
+
+ stream << "Sep=" + sep + "\n";
+ sep = qte + sep + qte;
+
+ QStringList header = { tr("seed"), tr("area"), tr("x"), tr("z") };
+ csvline(stream, qte, sep, header);
+
+ QTreeWidgetItemIterator it(ui->treeResults);
+ QString seed;
+ for (; *it; ++it)
+ {
+ QTreeWidgetItem *item = *it;
+ if (item->text(0) != "-")
+ {
+ seed = item->text(0);
+ continue;
+ }
+ QStringList cols;
+ cols.append(seed);
+ cols.append(item->text(1));
+ cols.append(item->text(2));
+ cols.append(item->text(3));
+ csvline(stream, qte, sep, cols);
+ }
+ stream.flush();
+}
+
+void TabSlime::on_pushExport_clicked()
+{
+#if WASM
+ QByteArray content;
+ QTextStream stream(&content);
+ exportResults(stream);
+ QFileDialog::saveFileContent(content, "slime.csv");
+#else
+ QString fnam = QFileDialog::getSaveFileName(
+ this, tr("Export slime results"), parent->prevdir, tr("Text files (*.txt *.csv);;Any files (*)"));
+ if (fnam.isEmpty())
+ return;
+
+ QFileInfo finfo(fnam);
+ QFile file(fnam);
+ parent->prevdir = finfo.absolutePath();
+
+ if (!file.open(QIODevice::WriteOnly))
+ {
+ warn(parent, tr("Failed to open file for export:\n\"%1\"").arg(fnam));
+ return;
+ }
+
+ QTextStream stream(&file);
+ exportResults(stream);
+#endif
+}
+
+void TabSlime::on_buttonFromVisible_clicked()
+{
+ MapView *mapview = parent->getMapView();
+
+ int x1, z1, x2, z2;
+ mapview->getVisible(&x1, &z1, &x2, &z2);
+
+ ui->lineX1->setText(QString::number(x1));
+ ui->lineZ1->setText(QString::number(z1));
+ ui->lineX2->setText(QString::number(x2));
+ ui->lineZ2->setText(QString::number(z2));
+}
+
+void TabSlime::on_treeResults_itemClicked(QTreeWidgetItem *item, int column)
+{
+ (void) column;
+ QVariant dat;
+ dat = item->data(0, Qt::UserRole);
+ if (dat.isValid())
+ {
+ uint64_t seed = qvariant_cast(dat);
+ int dim = item->data(0, Qt::UserRole+1).toInt();
+ WorldInfo wi;
+ parent->getSeed(&wi);
+ if (wi.seed != seed || (dim != DIM_UNDEF && dim != parent->getDim()))
+ {
+ wi.seed = seed;
+ parent->getMapView()->deleteWorld();
+ }
+ parent->setSeed(wi, dim);
+ }
+
+ dat = item->data(0, Qt::UserRole+2);
+ if (dat.isValid())
+ {
+ Pos p = qvariant_cast(dat);
+
+ // 创建两个圆形:半径为128和24
+ currentShapes.clear();
+
+ Shape circle128;
+ circle128.type = Shape::CIRCLE;
+ circle128.dim = DIM_OVERWORLD;
+ circle128.p1 = p;
+ circle128.p2 = Pos{0, 0};
+ circle128.r = 128;
+ currentShapes.push_back(circle128);
+
+ Shape circle24;
+ circle24.type = Shape::CIRCLE;
+ circle24.dim = DIM_OVERWORLD;
+ circle24.p1 = p;
+ circle24.p2 = Pos{0, 0};
+ circle24.r = 24;
+ currentShapes.push_back(circle24);
+
+ // 跳转到坐标并设置形状
+ parent->getMapView()->setView(p.x+0.5, p.z+0.5);
+ parent->getMapView()->setShapes(currentShapes);
+ }
+}
+
+void TabSlime::clearSlimeShapes()
+{
+ currentShapes.clear();
+ parent->getMapView()->setShapes(currentShapes);
+}
+
diff --git a/src/tabslime.h b/src/tabslime.h
new file mode 100644
index 0000000..893b852
--- /dev/null
+++ b/src/tabslime.h
@@ -0,0 +1,92 @@
+#ifndef TABSLIME_H
+#define TABSLIME_H
+
+#include
+#include
+#include
+
+#include "mainwindow.h"
+#include "util.h"
+
+namespace Ui {
+class TabSlime;
+}
+
+struct SlimeResult
+{
+ int centerX, centerZ; // 挂机点坐标(16的倍数)
+ int area; // 有效史莱姆区块刷怪面积
+};
+
+class AnalysisSlime : public QThread
+{
+ Q_OBJECT
+public:
+ explicit AnalysisSlime(QObject *parent = nullptr)
+ : QThread(parent), idx() {}
+
+ virtual void run() override;
+
+signals:
+ void seedDone(uint64_t seed, QList results);
+
+public:
+ std::vector seeds;
+ WorldInfo wi;
+ std::atomic_bool stop;
+ std::atomic_long idx;
+ int x1, z1, x2, z2; // 挂机位置坐标范围
+ int minArea; // 最小面积阈值
+ bool includeBiome; // 是否包含群系计算
+};
+
+class TabSlime : public QWidget, public ISaveTab
+{
+ Q_OBJECT
+
+public:
+ explicit TabSlime(MainWindow *parent = nullptr);
+ ~TabSlime();
+
+ virtual bool event(QEvent *e) override;
+
+ virtual void save(QSettings& settings) override;
+ virtual void load(QSettings& settings) override;
+ virtual void refresh() override;
+
+ void clearSlimeShapes(); // 清除史莱姆坐标的红圈
+
+private slots:
+ void onAnalysisSeedDone(uint64_t seed, QList results);
+ void onAnalysisFinished();
+ void onBufferTimeout();
+
+ void on_pushStart_clicked();
+ void on_pushExport_clicked();
+ void on_buttonFromVisible_clicked();
+ void on_treeResults_itemClicked(QTreeWidgetItem *item, int column);
+
+ void on_lineX1_editingFinished();
+ void on_lineZ1_editingFinished();
+ void on_lineX2_editingFinished();
+ void on_lineZ2_editingFinished();
+ void on_comboSeedSource_currentIndexChanged(int index);
+
+private:
+ void exportResults(QTextStream& stream);
+
+private:
+ Ui::TabSlime *ui;
+ MainWindow *parent;
+ AnalysisSlime thread;
+
+ QElapsedTimer elapsed;
+ uint64_t updt;
+ uint64_t nextupdate;
+ QList qbufl;
+
+ std::vector currentShapes; // 当前绘制的形状
+};
+
+#endif // TABSLIME_H
+
diff --git a/src/tabslime.ui b/src/tabslime.ui
new file mode 100644
index 0000000..1732a40
--- /dev/null
+++ b/src/tabslime.ui
@@ -0,0 +1,278 @@
+
+
+ TabSlime
+
+
+
+ 0
+ 0
+ 549
+ 353
+
+
+
+ Form
+
+
+ -
+
+
-
+
+
+ false
+
+
+ Export...
+
+
+
+ -
+
+
+ Analyze
+
+
+
+
+
+ -
+
+
+ Minimum area:
+
+
+
+ -
+
+
+ Calculate optimal AFK positions for slime chunk coverage (24-128 blocks radius).
+
+
+
+ -
+
+
+
+ Monospace
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+ true
+
+
+ true
+
+
+
+ seed
+
+
+
+ Monospace
+
+
+
+
+
+ area
+
+
+
+ Monospace
+
+
+
+
+
+ x
+
+
+
+ Monospace
+
+
+
+
+
+ z
+
+
+
+ Monospace
+
+
+
+
+
+ -
+
+
+ -
+
+
+ <html><head/></head><body><p>Sample biomes at y=-64 (for MC 1.18+). Exclude mushroom fields and deep dark. Apply multipliers for dripstone caves/rivers (5/6) and old growth pine taiga (0.95).</p></body></html>
+
+
+ Include biome parameters
+
+
+
+ -
+
+
+ 10000
+
+
+
+ -
+
+
+ Seed(s):
+
+
+
+ -
+
+
-
+
+
-
+
+
+ Lower bound (inclusive)
+
+
+ <html><head/><body><p>X<span style=" vertical-align:sub;">1</span>:</p></body></html>
+
+
+
+ -
+
+
+ Lower bound (inclusive)
+
+
+ 0
+
+
+
+ -
+
+
+ Lower bound (inclusive)
+
+
+ <html><head/><body><p>Z<span style=" vertical-align:sub;">1</span>:</p></body></html>
+
+
+
+ -
+
+
+ Lower bound (inclusive)
+
+
+ 0
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Upper bound (inclusive)
+
+
+ <html><head/><body><p>X<span style=" vertical-align:sub;">2</span>:</p></body></html>
+
+
+
+ -
+
+
+ Upper bound (inclusive)
+
+
+ 0
+
+
+
+ -
+
+
+ Upper bound (inclusive)
+
+
+ <html><head/><body><p>Z<span style=" vertical-align:sub;">2</span>:</p></body></html>
+
+
+
+ -
+
+
+ Upper bound (inclusive)
+
+
+ 0
+
+
+
+
+
+ -
+
+
+ From visible
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
-
+
+ Current seed
+
+
+ -
+
+ From matching seeds list
+
+
+
+
+
+
+
+
+ CoordEdit
+ QLineEdit
+
+
+
+ StyledComboBox
+ QComboBox
+
+
+
+
+
+
diff --git a/src/tabstructures.cpp b/src/tabstructures.cpp
index 9fb48ac..d350f23 100644
--- a/src/tabstructures.cpp
+++ b/src/tabstructures.cpp
@@ -2,6 +2,7 @@
#include "ui_tabstructures.h"
#include "message.h"
+#include "search.h"
#include "util.h"
#include
@@ -41,6 +42,8 @@ void AnalysisStructures::run()
wi.seed = seeds[idx];
if (quad)
runQuads(&g);
+ else if (this->isDouble)
+ runDoubles(&g);
else
runStructs(&g);
}
@@ -215,6 +218,42 @@ void AnalysisStructures::runQuads(Generator *g)
emit quadDone(seeditem);
}
+void AnalysisStructures::runDoubles(Generator *g)
+{
+ applySeed(g, 0, wi.seed);
+
+ QVector dsinfo;
+ findDoubleMonuments(g, area.x1, area.z1, area.x2, area.z2, &dsinfo);
+ if (dsinfo.empty())
+ return;
+
+ QTreeWidgetItem *seeditem = new TreeIntItem();
+ seeditem->setText(0, QString::asprintf("%" PRId64, wi.seed));
+ seeditem->setData(0, Qt::UserRole+0, QVariant::fromValue(wi.seed));
+ seeditem->setData(0, Qt::UserRole+1, QVariant::fromValue((int)DIM_OVERWORLD));
+
+ for (DoubleInfo& di : dsinfo)
+ {
+ QTreeWidgetItem *item = new QTreeWidgetItem(seeditem);
+
+ qreal dist = di.center.x*(qreal)di.center.x + di.center.z*(qreal)di.center.z;
+ dist = sqrt(dist);
+
+ item->setText(0, "-");
+ item->setData(1, Qt::DisplayRole, QVariant::fromValue(QString("double-monument")));
+ item->setData(2, Qt::DisplayRole, QVariant::fromValue((qlonglong)dist));
+ item->setData(3, Qt::DisplayRole, QVariant::fromValue(di.center.x));
+ item->setData(4, Qt::DisplayRole, QVariant::fromValue(di.center.z));
+ item->setData(5, Qt::DisplayRole, QVariant::fromValue(di.dx));
+ item->setData(6, Qt::DisplayRole, QVariant::fromValue(di.dz));
+ item->setData(0, Qt::UserRole+0, QVariant::fromValue(wi.seed));
+ item->setData(0, Qt::UserRole+1, QVariant::fromValue((int)DIM_OVERWORLD));
+ item->setData(0, Qt::UserRole+2, QVariant::fromValue(di.center));
+ }
+
+ emit doubleDone(seeditem);
+}
+
TabStructures::TabStructures(MainWindow *parent)
: QWidget(parent)
@@ -223,6 +262,7 @@ TabStructures::TabStructures(MainWindow *parent)
, thread(this)
, sortcols(-1)
, sortcolq(-1)
+ , sortcold(-1)
, nextupdate()
, updt(100)
{
@@ -235,12 +275,18 @@ TabStructures::TabStructures(MainWindow *parent)
ui->treeQuads->sortByColumn(-1, Qt::AscendingOrder);
connect(ui->treeQuads->header(), &QHeaderView::sectionClicked, this, [=](){ onHeaderClick(ui->treeQuads); } );
+ ui->treeDoubles->setColumnWidth(0, 160);
+ ui->treeDoubles->sortByColumn(-1, Qt::AscendingOrder);
+ connect(ui->treeDoubles->header(), &QHeaderView::sectionClicked, this, [=](){ onHeaderClick(ui->treeDoubles); } );
+
connect(&thread, &AnalysisStructures::itemDone, this, &TabStructures::onAnalysisItemDone, Qt::BlockingQueuedConnection);
connect(&thread, &AnalysisStructures::quadDone, this, &TabStructures::onAnalysisQuadDone, Qt::BlockingQueuedConnection);
+ connect(&thread, &AnalysisStructures::doubleDone, this, &TabStructures::onAnalysisDoubleDone, Qt::BlockingQueuedConnection);
connect(&thread, &AnalysisStructures::finished, this, &TabStructures::onAnalysisFinished);
connect(ui->treeStructs, &QTreeWidget::itemClicked, this, &TabStructures::onTreeItemClicked);
connect(ui->treeQuads, &QTreeWidget::itemClicked, this, &TabStructures::onTreeItemClicked);
+ connect(ui->treeDoubles, &QTreeWidget::itemClicked, this, &TabStructures::onTreeItemClicked);
}
TabStructures::~TabStructures()
@@ -268,6 +314,14 @@ bool TabStructures::event(QEvent *e)
ui->treeQuads->setColumnWidth(4, txtWidth(fm) * 10);
ui->treeQuads->setColumnWidth(5, txtWidth(fm, "_123.123"));
ui->treeQuads->setColumnWidth(6, txtWidth(fm) * 14);
+
+ ui->treeDoubles->setColumnWidth(0, txtWidth(fm) * 23);
+ ui->treeDoubles->setColumnWidth(1, txtWidth(fm, "_double-monument"));
+ ui->treeDoubles->setColumnWidth(2, txtWidth(fm) * 10);
+ ui->treeDoubles->setColumnWidth(3, txtWidth(fm) * 10);
+ ui->treeDoubles->setColumnWidth(4, txtWidth(fm) * 10);
+ ui->treeDoubles->setColumnWidth(5, txtWidth(fm) * 10);
+ ui->treeDoubles->setColumnWidth(6, txtWidth(fm) * 10);
}
return QWidget::event(e);
}
@@ -312,7 +366,7 @@ void TabStructures::load(QSettings& settings)
void TabStructures::onHeaderClick(QTreeView *tree)
{
- int& col = (tree == ui->treeStructs) ? sortcols : sortcolq;
+ int& col = (tree == ui->treeStructs) ? sortcols : ((tree == ui->treeQuads) ? sortcolq : sortcold);
int section = tree->header()->sortIndicatorSection();
if (tree->header()->sortIndicatorOrder() == Qt::AscendingOrder && col == section)
{
@@ -344,19 +398,31 @@ void TabStructures::onAnalysisQuadDone(QTreeWidgetItem *item)
}
}
+void TabStructures::onAnalysisDoubleDone(QTreeWidgetItem *item)
+{
+ qbufd.push_back(item);
+ quint64 ns = elapsed.nsecsElapsed();
+ if (ns > nextupdate)
+ {
+ nextupdate = ns + updt * 1e6;
+ QTimer::singleShot(updt, this, &TabStructures::onBufferTimeout);
+ }
+}
+
void TabStructures::onAnalysisFinished()
{
onBufferTimeout();
on_tabWidget_currentChanged(-1);
ui->treeStructs->setSortingEnabled(true);
ui->treeQuads->setSortingEnabled(true);
+ ui->treeDoubles->setSortingEnabled(true);
ui->pushStart->setChecked(false);
ui->pushStart->setText(tr("Analyze"));
}
void TabStructures::onBufferTimeout()
{
- if (qbufs.empty() && qbufq.empty())
+ if (qbufs.empty() && qbufq.empty() && qbufd.empty())
return;
uint64_t t = -elapsed.elapsed();
if (!qbufs.empty())
@@ -380,6 +446,17 @@ void TabStructures::onBufferTimeout()
ui->treeQuads->setSortingEnabled(true);
qbufq.clear();
}
+ if (!qbufd.empty())
+ {
+ ui->treeDoubles->setSortingEnabled(false);
+ ui->treeDoubles->setUpdatesEnabled(false);
+ ui->treeDoubles->addTopLevelItems(qbufd);
+ for (QTreeWidgetItem *item: qAsConst(qbufd))
+ item->setExpanded(true);
+ ui->treeDoubles->setUpdatesEnabled(true);
+ ui->treeDoubles->setSortingEnabled(true);
+ qbufd.clear();
+ }
QString progress = QString::asprintf(" (%d/%zu)", thread.idx.load(), thread.seeds.size());
ui->pushStart->setText(tr("Stop") + progress);
@@ -458,21 +535,33 @@ void TabStructures::on_pushStart_clicked()
if (ui->tabWidget->currentWidget() == ui->tabStructures)
{
thread.quad = false;
+ thread.isDouble = false;
dats = thread.area;
ui->treeStructs->setSortingEnabled(false);
while (ui->treeStructs->topLevelItemCount() > 0)
delete ui->treeStructs->takeTopLevelItem(0);
ui->treeStructs->setSortingEnabled(true);
}
- else
+ else if (ui->tabWidget->currentWidget() == ui->tabQuads)
{
thread.quad = true;
+ thread.isDouble = false;
datq = thread.area;
ui->treeQuads->setSortingEnabled(false);
while (ui->treeQuads->topLevelItemCount() > 0)
delete ui->treeQuads->takeTopLevelItem(0);
ui->treeQuads->setSortingEnabled(true);
}
+ else if (ui->tabWidget->currentWidget() == ui->tabDoubles)
+ {
+ thread.quad = false;
+ thread.isDouble = true;
+ datd = thread.area;
+ ui->treeDoubles->setSortingEnabled(false);
+ while (ui->treeDoubles->topLevelItemCount() > 0)
+ delete ui->treeDoubles->takeTopLevelItem(0);
+ ui->treeDoubles->setSortingEnabled(true);
+ }
ui->pushExport->setEnabled(false);
ui->pushStart->setChecked(true);
@@ -593,6 +682,30 @@ void TabStructures::exportResults(QTextStream& stream)
csvline(stream, qte, sep, cols);
}
}
+ else if(ui->tabWidget->currentWidget() == ui->tabDoubles)
+ {
+ stream << qte << "#X1" << sep << datd.x1 << qte << "\n";
+ stream << qte << "#Z1" << sep << datd.z1 << qte << "\n";
+ stream << qte << "#X2" << sep << datd.x2 << qte << "\n";
+ stream << qte << "#Z2" << sep << datd.z2 << qte << "\n";
+
+ QStringList header = { tr("seed"), tr("type"), tr("distance"), tr("x"), tr("z"), tr("dx"), tr("dz") };
+ csvline(stream, qte, sep, header);
+ QString seed;
+ for (QTreeWidgetItemIterator it(ui->treeDoubles); *it; ++it)
+ {
+ QTreeWidgetItem *item = *it;
+ if (item->text(0) != "-")
+ {
+ seed = item->text(0);
+ continue;
+ }
+ QStringList cols = { seed };
+ for (int i = 1, n = item->columnCount(); i < n; i++)
+ cols.append(item->text(i));
+ csvline(stream, qte, sep, cols);
+ }
+ }
stream.flush();
}
@@ -644,8 +757,10 @@ void TabStructures::on_tabWidget_currentChanged(int)
{
if (ui->tabWidget->currentWidget() == ui->tabStructures)
ok = ui->treeStructs->topLevelItemCount() > 0;
- if (ui->tabWidget->currentWidget() == ui->tabQuads)
+ else if (ui->tabWidget->currentWidget() == ui->tabQuads)
ok = ui->treeQuads->topLevelItemCount() > 0;
+ else if (ui->tabWidget->currentWidget() == ui->tabDoubles)
+ ok = ui->treeDoubles->topLevelItemCount() > 0;
}
ui->pushExport->setEnabled(ok);
}
diff --git a/src/tabstructures.h b/src/tabstructures.h
index fb4645c..c3dadb0 100644
--- a/src/tabstructures.h
+++ b/src/tabstructures.h
@@ -17,10 +17,12 @@ class AnalysisStructures : public QThread
virtual void run() override;
void runStructs(Generator *g);
void runQuads(Generator *g);
+ void runDoubles(Generator *g);
signals:
void itemDone(QTreeWidgetItem *item);
void quadDone(QTreeWidgetItem *item);
+ void doubleDone(QTreeWidgetItem *item);
public:
std::vector seeds;
@@ -32,6 +34,7 @@ class AnalysisStructures : public QThread
bool mapshow[D_STRUCT_NUM];
bool collect;
bool quad;
+ bool isDouble;
};
class TabStructures : public QWidget, public ISaveTab
@@ -52,6 +55,7 @@ private slots:
void onAnalysisItemDone(QTreeWidgetItem *item);
void onAnalysisQuadDone(QTreeWidgetItem *item);
+ void onAnalysisDoubleDone(QTreeWidgetItem *item);
void onAnalysisFinished();
void onBufferTimeout();
@@ -69,14 +73,15 @@ private slots:
Ui::TabStructures *ui;
MainWindow *parent;
AnalysisStructures thread;
- AnalysisStructures::Dat dats, datq;
- int sortcols, sortcolq;
+ AnalysisStructures::Dat dats, datq, datd;
+ int sortcols, sortcolq, sortcold;
QElapsedTimer elapsed;
uint64_t nextupdate;
uint64_t updt;
QList qbufs;
QList qbufq;
+ QList qbufd;
};
#endif // TABSTRUCTURES_H
diff --git a/src/tabstructures.ui b/src/tabstructures.ui
index 69f9f2e..e81fcb6 100644
--- a/src/tabstructures.ui
+++ b/src/tabstructures.ui
@@ -227,6 +227,81 @@