#include "BuildingConfig.h" #include "FactoryQueries.h" #include #include #include #include #include "BeltSystem.h" #include "Building.h" #include "BuildingsConfig.h" #include "BuildingSystem.h" #include "DisplayName.h" #include "Simulation.h" namespace { // The blueprint-relevant geometry shared by operational buildings and construction // sites. Resolved from whichever of the two a selected id refers to. struct SelectedBuilding { BuildingId id; BuildingType type; Rotation rotation; QPoint anchor; const std::vector* bodyCells; }; // Resolves a selected id to a player-placeable building or construction site, if it // is one. Returns std::nullopt for an unknown id or a non-player-placeable building // (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE). std::optional resolvePlaceable(const Simulation& sim, BuildingId id) { const Building* building = findBuilding(sim.getFactoryState(), id); const ConstructionSite* site = building ? nullptr : findSite(sim.getFactoryState(), id); if (!building && !site) { return std::nullopt; } const BuildingType type = building ? building->type : site->type; const BuildingDef* def = sim.getConfig().buildings.findBuildingDef(type); if (!def || !def->playerPlaceable) { return std::nullopt; } SelectedBuilding resolved; resolved.id = id; resolved.type = type; resolved.rotation = building ? building->rotation : site->rotation; resolved.anchor = building ? building->anchor : site->anchor; resolved.bodyCells = building ? &building->bodyCells : &site->bodyCells; return resolved; } // Position of a building type in buildings.toml, which is the order the build button // bar lays out its buttons (REQ-UI-BUILD-BAR) and so the tie-break order for a // blueprint's contents line. A type with no config entry sorts after every known one. std::size_t configOrderIndex(BuildingType type, const BuildingsConfig& buildings) { for (std::size_t i = 0; i < buildings.buildings.size(); ++i) { if (buildings.buildings[i].type == type) { return i; } } return buildings.buildings.size(); } } // namespace std::optional readBuildingConfig(const Simulation& sim, BuildingId id) { const Building* building = findBuilding(sim.getFactoryState(), id); const ConstructionSite* site = building ? nullptr : findSite(sim.getFactoryState(), id); if (!building && !site) { return std::nullopt; } BuildingConfig config; config.type = building ? building->type : site->type; const std::string& recipeId = building ? building->recipeId : site->recipeId; if (!recipeId.empty()) { config.recipeId = recipeId; } config.shipLayout = building ? building->shipLayout : site->shipLayout; if (config.type == BuildingType::Splitter) { config.isSplitter = true; if (building) { // Operational splitter filters live in the BeltSystem, keyed by tile. const std::optional info = sim.getBelts().getSplitterInfo(building->anchor); if (info.has_value()) { config.splitterFilterA = info->filterA; config.splitterFilterB = info->filterB; } } else { // A site keeps its pre-completion filters on the ConstructionSite. config.splitterFilterA = site->splitterFilterA; config.splitterFilterB = site->splitterFilterB; } } return config; } Blueprint captureBlueprintFromSelection(const Simulation& sim, const std::vector& selectedIds) { std::vector entries; entries.reserve(selectedIds.size()); for (const BuildingId id : selectedIds) { const std::optional resolved = resolvePlaceable(sim, id); if (resolved.has_value()) { entries.push_back(*resolved); } } if (entries.empty()) { return Blueprint{}; } int minX = INT_MAX, maxX = INT_MIN; int minY = INT_MAX, maxY = INT_MIN; for (const SelectedBuilding& e : entries) { for (const QPoint& cell : *e.bodyCells) { minX = std::min(minX, cell.x()); maxX = std::max(maxX, cell.x()); minY = std::min(minY, cell.y()); maxY = std::max(maxY, cell.y()); } } const QPoint center((minX + maxX) / 2, (minY + maxY) / 2); Blueprint blueprint; blueprint.buildings.reserve(entries.size()); for (const SelectedBuilding& e : entries) { BlueprintBuilding building; building.type = e.type; building.rotation = e.rotation; building.offset = e.anchor - center; // Recipe / schematic / layout / splitter-filter capture goes through // readBuildingConfig, which handles operational buildings and construction // sites alike. const std::optional config = readBuildingConfig(sim, e.id); if (config.has_value()) { building.recipeId = config->recipeId.value_or(std::string()); building.shipLayout = config->shipLayout; building.splitterFilterA = config->splitterFilterA; building.splitterFilterB = config->splitterFilterB; } blueprint.buildings.push_back(building); } return blueprint; } bool selectionHasPlaceableBuilding(const Simulation& sim, const std::vector& selectedIds) { for (const BuildingId id : selectedIds) { if (resolvePlaceable(sim, id).has_value()) { return true; } } return false; } std::vector summarizeBlueprintContents( const Blueprint& blueprint, const BuildingsConfig& buildings) { std::map counts; for (const BlueprintBuilding& building : blueprint.buildings) { ++counts[building.type]; } // The config index is carried through the sort so the comparator stays a strict // total order; the enum value is the final tie-break, which only two config-less // types could ever reach. struct RankedType { BuildingType type; int count; std::size_t order; }; std::vector ranked; ranked.reserve(counts.size()); for (const std::pair& entry : counts) { ranked.push_back({entry.first, entry.second, configOrderIndex(entry.first, buildings)}); } std::sort(ranked.begin(), ranked.end(), [](const RankedType& left, const RankedType& right) { if (left.count != right.count) { return left.count > right.count; } if (left.order != right.order) { return left.order < right.order; } return static_cast(left.type) < static_cast(right.type); }); std::vector summary; summary.reserve(ranked.size()); for (const RankedType& entry : ranked) { summary.push_back({toDisplayName(buildingTypeId(entry.type)), entry.count}); } return summary; } int computeBlueprintCost(const Blueprint& blueprint, const BuildingsConfig& buildings) { int total = 0; for (const BlueprintBuilding& building : blueprint.buildings) { const BuildingDef* def = buildings.findBuildingDef(building.type); if (def) { total += def->cost; } } return total; }