#include "BalancingWindow.h" #include #include #include #include #include #include #include "ConfigLoader.h" #include "InspectWindow.h" #include "VisualsLoader.h" namespace { // Escapes characters that would break a markdown table cell. QString escapeCell(const QString& text) { QString escaped = text; escaped.replace("|", "\\|"); return escaped; } QString stateText(ArenaWidget::State state) { switch (state) { case ArenaWidget::State::NotStarted: return QStringLiteral("not started"); case ArenaWidget::State::Running: return QStringLiteral("running"); case ArenaWidget::State::Ended: return QStringLiteral("ended"); } return QString(); } // Team column header: "[WON] Name — threat N", matching the arena widget. QString teamHeaderCell(const ArenaStatus& status, int teamIndex) { const ArenaStatus::TeamStatus& team = status.teams[teamIndex]; QString header = QString::fromStdString(team.name); if (status.finished && status.winnerTeam == teamIndex) { header = QStringLiteral("[WON] ") + header; } header += QStringLiteral(" - threat %1").arg(QString::number(team.threatLevel, 'f', 0)); header += QStringLiteral(" - EHP %1").arg(QString::fromStdString(team.ehpPercentText())); return escapeCell(header); } // Single entry line: "surviving/total DisplayName [L]", matching the arena widget. QString entryCell(const ArenaStatus::Entry& entry) { QString cell = QString("%1/%2 %3") .arg(entry.surviving) .arg(entry.total) .arg(QString::fromStdString(entry.displayName)); if (entry.level.has_value()) { cell += QStringLiteral(" L%1").arg(entry.level.value()); } return escapeCell(cell); } } BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig, GameConfig gameConfig, const std::string& configDir, const std::string& balancingConfigPath, QWidget* parent) : QWidget(parent) , m_gameConfig(std::move(gameConfig)) , m_configDir(configDir) , m_balancingConfigPath(balancingConfigPath) , m_nextSeed(0) , m_inspectWindow(nullptr) , m_inspectedArenaIndex(-1) { m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml"); setWindowTitle(tr("DotaFactory — Balancing Tool")); resize(800, 600); QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); QHBoxLayout* buttonRow = new QHBoxLayout(); m_reloadButton = new QPushButton(tr("Reload Config"), this); m_startAllButton = new QPushButton(tr("Start All"), this); m_logButton = new QPushButton(tr("Log"), this); buttonRow->addWidget(m_reloadButton); buttonRow->addWidget(m_startAllButton); buttonRow->addWidget(m_logButton); buttonRow->addStretch(); mainLayout->addLayout(buttonRow); connect(m_reloadButton, &QPushButton::clicked, this, &BalancingWindow::reloadConfig); connect(m_startAllButton, &QPushButton::clicked, this, &BalancingWindow::startAll); connect(m_logButton, &QPushButton::clicked, this, &BalancingWindow::writeLog); m_scrollArea = new QScrollArea(this); m_scrollArea->setWidgetResizable(true); mainLayout->addWidget(m_scrollArea); populateArenas(balancingConfig); m_pollTimer = new QTimer(this); connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses); m_pollTimer->start(100); registerForEvents(); } BalancingWindow::~BalancingWindow() { unregisterForEvents(); m_pollTimer->stop(); if (m_inspectWindow) { delete m_inspectWindow; m_inspectWindow = nullptr; } m_inspectedSim.reset(); stopAllArenas(); } void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig) { stopAllArenas(); m_arenas.clear(); QWidget* scrollContent = new QWidget(m_scrollArea); QVBoxLayout* contentLayout = new QVBoxLayout(scrollContent); contentLayout->setSpacing(8); contentLayout->setContentsMargins(8, 8, 8, 8); for (const ArenaConfig& arenaConfig : balancingConfig.arenas) { int index = static_cast(m_arenas.size()); ArenaEntry entry; entry.config = arenaConfig; entry.simulation = std::make_unique( m_gameConfig, arenaConfig, m_nextSeed++); entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent); contentLayout->addWidget(entry.widget); entry.widget->updateStatus(entry.simulation->status()); m_arenas.push_back(std::move(entry)); } contentLayout->addStretch(); m_scrollArea->setWidget(scrollContent); updateButtons(); } void BalancingWindow::stopAllArenas() { for (ArenaEntry& entry : m_arenas) { entry.simulation->requestStop(); } for (ArenaEntry& entry : m_arenas) { if (entry.worker.joinable()) { entry.worker.join(); } } } void BalancingWindow::pollStatuses() { for (ArenaEntry& entry : m_arenas) { if (entry.worker.joinable()) { const ArenaStatus status = entry.simulation->status(); entry.widget->updateStatus(status); } } if (m_inspectedSim && m_inspectedArenaIndex >= 0) { const ArenaStatus status = m_inspectedSim->status(); m_arenas[static_cast(m_inspectedArenaIndex)].widget->updateStatus(status); } updateButtons(); } void BalancingWindow::reloadConfig() { try { GameConfig newGameConfig = ConfigLoader::loadFromDirectory(m_configDir); BalancingConfig newBalancingConfig = loadBalancingConfig(m_balancingConfigPath); m_gameConfig = std::move(newGameConfig); populateArenas(newBalancingConfig); } catch (const std::exception& e) { QMessageBox::critical(this, tr("Reload Failed"), QString::fromStdString(e.what())); } } void BalancingWindow::startAll() { for (int i = 0; i < static_cast(m_arenas.size()); ++i) { startArena(i); } } void BalancingWindow::handleEvent(std::shared_ptr event) { startArena(event->arenaIndex); } void BalancingWindow::handleEvent(std::shared_ptr event) { inspectArena(event->arenaIndex); } void BalancingWindow::handleEvent(std::shared_ptr /*event*/) { closeInspectWindow(); } void BalancingWindow::startArena(int index) { ArenaEntry& entry = m_arenas[index]; if (entry.worker.joinable()) { entry.simulation->requestStop(); entry.worker.join(); } entry.simulation = std::make_unique( m_gameConfig, entry.config, m_nextSeed++); entry.widget->startSimulation(); entry.widget->updateStatus(entry.simulation->status()); ArenaSimulation* sim = entry.simulation.get(); entry.worker = std::thread([sim]() { sim->run(); }); updateButtons(); } void BalancingWindow::inspectArena(int index) { if (m_inspectWindow) { delete m_inspectWindow; m_inspectWindow = nullptr; if (m_inspectedSim && m_inspectedArenaIndex >= 0 && !m_inspectedSim->isFinished()) { m_arenas[static_cast(m_inspectedArenaIndex)].widget->resetToGrey(); } m_inspectedSim.reset(); m_inspectedArenaIndex = -1; } ArenaEntry& entry = m_arenas[static_cast(index)]; if (entry.worker.joinable()) { entry.simulation->requestStop(); entry.worker.join(); } m_inspectedSim = std::make_unique( m_gameConfig, entry.config, m_nextSeed++); m_inspectedArenaIndex = index; entry.widget->resetToGrey(); entry.widget->startSimulation(); entry.widget->updateStatus(m_inspectedSim->status()); m_inspectWindow = new InspectWindow( m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr); setMainControlsEnabled(false); m_inspectWindow->show(); } void BalancingWindow::closeInspectWindow() { if (!m_inspectWindow) { return; } m_inspectWindow->deleteLater(); m_inspectWindow = nullptr; if (m_inspectedArenaIndex >= 0 && m_inspectedSim) { if (!m_inspectedSim->isFinished()) { m_arenas[static_cast(m_inspectedArenaIndex)].widget->resetToGrey(); } } m_inspectedSim.reset(); m_inspectedArenaIndex = -1; setMainControlsEnabled(true); updateButtons(); } void BalancingWindow::setMainControlsEnabled(bool enabled) { m_reloadButton->setEnabled(enabled); m_startAllButton->setEnabled(enabled); m_logButton->setEnabled(enabled); for (ArenaEntry& entry : m_arenas) { for (QPushButton* btn : entry.widget->findChildren()) { btn->setEnabled(enabled); } } } void BalancingWindow::updateButtons() { if (m_inspectWindow) { return; } bool anyRunning = false; bool allRunning = true; for (ArenaEntry& entry : m_arenas) { if (entry.worker.joinable() && !entry.simulation->status().finished) { anyRunning = true; } else { allRunning = false; } } if (m_arenas.empty()) { allRunning = false; } m_reloadButton->setEnabled(!anyRunning); m_startAllButton->setEnabled(!allRunning); m_logButton->setEnabled(true); } void BalancingWindow::writeLog() { QFile file(QStringLiteral("balancing_log.md")); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { QMessageBox::warning(this, tr("Log Failed"), tr("Could not open balancing_log.md for writing.")); return; } QTextStream out(&file); out.setCodec("UTF-8"); out << "# Balancing Log\n"; for (const ArenaEntry& entry : m_arenas) { const ArenaStatus& status = entry.widget->getLastStatus(); const ArenaWidget::State state = entry.widget->getState(); QString stateSuffix = stateText(state); if (state == ArenaWidget::State::Ended) { stateSuffix += QStringLiteral(", %1 s") .arg(QString::number(status.durationSeconds, 'f', 1)); } out << "\n## Arena: " << QString::fromStdString(entry.config.name) << " (" << stateSuffix << ")\n\n"; out << "| " << teamHeaderCell(status, 0) << " | " << teamHeaderCell(status, 1) << " |\n"; out << "|---|---|\n"; const std::size_t rowCount = std::max(status.teams[0].entries.size(), status.teams[1].entries.size()); for (std::size_t row = 0; row < rowCount; ++row) { QString leftCell; if (row < status.teams[0].entries.size()) { leftCell = entryCell(status.teams[0].entries[row]); } QString rightCell; if (row < status.teams[1].entries.size()) { rightCell = entryCell(status.teams[1].entries[row]); } out << "| " << leftCell << " | " << rightCell << " |\n"; } } }