#include "BlueprintLibrary.h" #include #include #include #include #include #include #include #include "BlueprintPlacementRequestedEvent.h" #include "BlueprintSerializer.h" #include "EventManager.h" #include "ExitBlueprintModeRequestedEvent.h" #include "BuildingConfig.h" #include "Simulation.h" BlueprintLibrary::BlueprintLibrary(Simulation* sim, const GameConfig* config, QWidget* dialogParent) : m_sim(sim) , m_config(config) , m_dialogParent(dialogParent) { loadFromDisk(); registerForEvents(); } BlueprintLibrary::~BlueprintLibrary() { saveToDisk(); unregisterForEvents(); } bool BlueprintLibrary::getCanCaptureSelection() const { // A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE). return selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds); } void BlueprintLibrary::saveSelectionAs(const QString& name) { Blueprint blueprint = createBlueprintFromSelection(); if (blueprint.buildings.empty()) { return; } blueprint.name = name; m_blueprints.push_back(std::move(blueprint)); } const std::vector& BlueprintLibrary::getBlueprints() const { return m_blueprints; } QString BlueprintLibrary::getContentsSummary(int index) const { if (index < 0 || index >= static_cast(m_blueprints.size())) { return QString(); } QStringList entries; for (const BlueprintContentEntry& entry : summarizeBlueprintContents( m_blueprints[static_cast(index)], m_config->buildings)) { // The same " x " notation the building multi-selection summary // uses (REQ-UI-MULTI-SELECTION), which also sidesteps plural forms. entries << QObject::tr("%1 x %2") .arg(QString::fromStdString(entry.buildingName)) .arg(entry.count); } return entries.join(QStringLiteral(", ")); } int BlueprintLibrary::getCost(int index) const { if (index < 0 || index >= static_cast(m_blueprints.size())) { return 0; } return computeBlueprintCost(m_blueprints[static_cast(index)], m_config->buildings); } bool BlueprintLibrary::getCanAfford(int index) const { return m_sim->getBuildingBlocksStock() >= getCost(index); } void BlueprintLibrary::remove(int index) { if (index < 0 || index >= static_cast(m_blueprints.size())) { return; } if (m_activeIndex == index) { m_activeIndex = std::nullopt; EventManager::getInstance()->sendEventImmediately( std::make_shared()); } else if (m_activeIndex.has_value() && *m_activeIndex > index) { --*m_activeIndex; } m_blueprints.erase(m_blueprints.begin() + index); } void BlueprintLibrary::beginPlacement(int index) { if (index < 0 || index >= static_cast(m_blueprints.size())) { return; } m_activeIndex = index; EventManager::getInstance()->sendEventImmediately( std::make_shared( m_blueprints[static_cast(index)])); } void BlueprintLibrary::handleEvent(std::shared_ptr event) { m_selectedBuildingIds = event->ids; } void BlueprintLibrary::handleEvent(std::shared_ptr /*event*/) { // Only the saved-blueprint index is cleared. A temporary blueprint deliberately // survives its placement mode so V can re-enter it (REQ-UI-BLUEPRINT-TEMP). m_activeIndex = std::nullopt; } void BlueprintLibrary::handleEvent( std::shared_ptr /*event*/) { // C (REQ-UI-BLUEPRINT-TEMP): capture the current selection and enter placement mode // for it, without naming it, listing it, or persisting it. If nothing player-placeable // is selected, do nothing at all -- in particular, keep the previous temporary // blueprint, which V can still place. Blueprint blueprint = createBlueprintFromSelection(); if (blueprint.buildings.empty()) { return; } m_temporaryBlueprint = std::move(blueprint); // No saved blueprint is active while a temporary one is being placed. m_activeIndex = std::nullopt; EventManager::getInstance()->sendEventImmediately( std::make_shared(*m_temporaryBlueprint)); } void BlueprintLibrary::handleEvent( std::shared_ptr /*event*/) { // V (REQ-UI-BLUEPRINT-TEMP): re-enter placement mode for the temporary blueprint, // capturing nothing. With none captured, nothing happens -- no mode is entered and // whichever mode is active is left alone. Affordability is not checked here, matching // C; cost is enforced at placement (REQ-UI-BLUEPRINT-PLACE). The blueprint is copied, // not moved: it stays available for the next V. if (!m_temporaryBlueprint.has_value()) { return; } m_activeIndex = std::nullopt; EventManager::getInstance()->sendEventImmediately( std::make_shared(*m_temporaryBlueprint)); } void BlueprintLibrary::handleEvent(std::shared_ptr /*event*/) { // A restart begins a new run, and the temporary blueprint belongs to the old one // (REQ-UI-BLUEPRINT-TEMP). The saved blueprints are not run state and stay. m_temporaryBlueprint = std::nullopt; } Blueprint BlueprintLibrary::createBlueprintFromSelection() const { // Capture is shared, testable logic in lib/sim: it resolves each selected id as an // operational building or a construction site alike (REQ-UI-BLUEPRINT-CREATE, // REQ-UI-BLUEPRINT-STORAGE). return captureBlueprintFromSelection(*m_sim, m_selectedBuildingIds); } void BlueprintLibrary::loadFromDisk() { // Load at startup (REQ-UI-BLUEPRINT-LOAD). Missing file: start empty, no error. const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml"; QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; } try { m_blueprints = BlueprintSerializer::deserialize(file.readAll().toStdString()); } catch (const std::exception& e) { QMessageBox::critical(m_dialogParent, QObject::tr("Load Failed"), QObject::tr("Failed to load blueprints:\n%1").arg(e.what())); m_blueprints.clear(); } } void BlueprintLibrary::saveToDisk() const { // Persist on shutdown; write errors are silently ignored (REQ-UI-BLUEPRINT-SAVE). const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml"; QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; } try { const std::string content = BlueprintSerializer::serialize(m_blueprints); file.write(QByteArray::fromStdString(content)); } catch (...) {} }