241 lines
7.7 KiB
C++
241 lines
7.7 KiB
C++
#include "BuildingConfig.h"
|
|
#include "FactoryQueries.h"
|
|
|
|
#include <algorithm>
|
|
#include <climits>
|
|
#include <cstddef>
|
|
#include <map>
|
|
|
|
#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<QPoint>* 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<SelectedBuilding> 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<BuildingConfig> 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<BeltSystem::SplitterInfo> 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<BuildingId>& selectedIds)
|
|
{
|
|
std::vector<SelectedBuilding> entries;
|
|
entries.reserve(selectedIds.size());
|
|
for (const BuildingId id : selectedIds)
|
|
{
|
|
const std::optional<SelectedBuilding> 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<BuildingConfig> 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<BuildingId>& selectedIds)
|
|
{
|
|
for (const BuildingId id : selectedIds)
|
|
{
|
|
if (resolvePlaceable(sim, id).has_value())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::vector<BlueprintContentEntry> summarizeBlueprintContents(
|
|
const Blueprint& blueprint, const BuildingsConfig& buildings)
|
|
{
|
|
std::map<BuildingType, int> 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<RankedType> ranked;
|
|
ranked.reserve(counts.size());
|
|
for (const std::pair<const BuildingType, int>& 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<int>(left.type) < static_cast<int>(right.type);
|
|
});
|
|
|
|
std::vector<BlueprintContentEntry> 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;
|
|
}
|