Implements REQ-MAT-OUTPUT-GROUP. A recipe had two shapes -- outputs produced together, or outputs of which exactly one happened -- and every rule over them was written twice, selected by `building == ReprocessingPlant`: sizing a buffer, deciding whether a cycle fits, resolving what a cycle makes, costing an item. RecipeDef now holds output groups, each a weight and a list of items, and a cycle yields exactly one group. One group is the ordinary recipe, so the old two cases are the same shape with one and with several, and all four rules collapse to one expression apiece with no building-type test left. rollReprocessingOutput becomes rollOutputGroup, where a single group returns without drawing or testing eligibility. That early-out is load-bearing twice over. Drawing there would consume entropy for every ordinary recipe and shift every later random outcome; and eligibility must not apply either, since implicit unlocking is demand-derived, so an ordinary recipe's output can be producible while nothing yet calls for it -- testing it would stop the building producing rather than gate a drop. Past the early-out a group is eligible only when all of its items are unlocked, being produced whole. Threat follows the recipe's shape rather than the building, and the per-unit value now divides by the group's amount as well as its odds. That moves no number today: every item resolved through this path has amount 1, which is why the threat expectations are untouched. Config keeps `outputs = [...]` as the single-group form, so only the two reprocessing recipes change shape. The recipe summary gains "/" between groups and keeps "+" within one, which also fixes the plant reading as though a cycle produced all of its items at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
212 lines
6.9 KiB
C++
212 lines
6.9 KiB
C++
#include "SplitterContent.h"
|
|
|
|
#include <algorithm>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <QLabel>
|
|
#include <QListWidget>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "BeltSystem.h"
|
|
#include "BuildingTarget.h"
|
|
#include "ClearBeltControl.h"
|
|
#include "Command.h"
|
|
#include "CommandRequestedEvent.h"
|
|
#include "EventManager.h"
|
|
#include "FactoryQueries.h"
|
|
#include "GameConfig.h"
|
|
#include "ItemType.h"
|
|
#include "Rotation.h"
|
|
#include "SelectionNames.h"
|
|
#include "Simulation.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
// Height cap on a filter list, so two of them plus the rest of the card still fit.
|
|
const int kFilterListHeightPx = 100;
|
|
|
|
QString getRotationLabel(Rotation rotation)
|
|
{
|
|
// Written as code points because the sources are read as ASCII by the compiler.
|
|
const QChar upArrow(0x2191); // U+2191 UPWARDS ARROW
|
|
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
|
const QChar downArrow(0x2193); // U+2193 DOWNWARDS ARROW
|
|
const QChar leftArrow(0x2190); // U+2190 LEFTWARDS ARROW
|
|
|
|
switch (rotation)
|
|
{
|
|
case Rotation::North: return QObject::tr("North (%1)").arg(upArrow);
|
|
case Rotation::East: return QObject::tr("East (%1)").arg(rightArrow);
|
|
case Rotation::South: return QObject::tr("South (%1)").arg(downArrow);
|
|
case Rotation::West: return QObject::tr("West (%1)").arg(leftArrow);
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
// Every item type the economy knows, from both sides of every recipe.
|
|
std::vector<std::string> getAllItemIds(const RecipesConfig& recipes)
|
|
{
|
|
std::set<std::string> seen;
|
|
for (const RecipeDef& recipe : recipes.recipes)
|
|
{
|
|
for (const RecipeIngredient& ingredient : recipe.inputs)
|
|
{
|
|
seen.insert(ingredient.item);
|
|
}
|
|
for (const std::string& item : getProducibleItems(recipe))
|
|
{
|
|
seen.insert(item);
|
|
}
|
|
}
|
|
return std::vector<std::string>(seen.begin(), seen.end());
|
|
}
|
|
|
|
std::vector<ItemType> collectCheckedItems(const QListWidget* list)
|
|
{
|
|
std::vector<ItemType> filter;
|
|
for (int row = 0; row < list->count(); ++row)
|
|
{
|
|
const QListWidgetItem* item = list->item(row);
|
|
if (item->checkState() == Qt::Checked)
|
|
{
|
|
filter.push_back(ItemType{ item->text().toStdString() });
|
|
}
|
|
}
|
|
return filter;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
SplitterContent::SplitterContent(const SelectionContext& context,
|
|
const SelectionRequest& request, QWidget* parent)
|
|
: SelectionContent(context, asConstructionSite(context, request.buildings.front()),
|
|
parent)
|
|
, m_id(request.buildings.front())
|
|
, m_isSite(asConstructionSite(context, m_id).has_value())
|
|
, m_tile(0, 0)
|
|
{
|
|
m_filterALabel = new QLabel(this);
|
|
m_filterAList = new QListWidget(this);
|
|
m_filterBLabel = new QLabel(this);
|
|
m_filterBList = new QListWidget(this);
|
|
m_filterAList->setMaximumHeight(kFilterListHeightPx);
|
|
m_filterBList->setMaximumHeight(kFilterListHeightPx);
|
|
|
|
getConfigurationLayout()->addWidget(m_filterALabel);
|
|
getConfigurationLayout()->addWidget(m_filterAList);
|
|
getConfigurationLayout()->addWidget(m_filterBLabel);
|
|
getConfigurationLayout()->addWidget(m_filterBList);
|
|
|
|
getRuntimeLayout()->addWidget(
|
|
new ClearBeltControl(context, request.buildings, this));
|
|
|
|
// Populated once, here, rather than on every refresh: re-checking the boxes at 30 Hz
|
|
// would fight the player's clicks.
|
|
populateFilters();
|
|
|
|
connect(m_filterAList, &QListWidget::itemChanged,
|
|
this, [this]() { applyFilters(); });
|
|
connect(m_filterBList, &QListWidget::itemChanged,
|
|
this, [this]() { applyFilters(); });
|
|
}
|
|
|
|
void SplitterContent::refreshConfiguration()
|
|
{
|
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
|
if (!target.isValid())
|
|
{
|
|
return;
|
|
}
|
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
|
}
|
|
|
|
void SplitterContent::populateFilters()
|
|
{
|
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
|
if (!target.isValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// An operational splitter's outputs and filters live in the belt subsystem, keyed by
|
|
// tile; a site's are stored on the site itself (REQ-BLD-SITE-CONFIG).
|
|
std::optional<BeltSystem::SplitterInfo> info;
|
|
if (m_isSite)
|
|
{
|
|
info = getSiteSplitterInfo(getContext().sim->getFactoryState(),
|
|
*getContext().config, m_id);
|
|
}
|
|
else
|
|
{
|
|
m_tile = target.anchor;
|
|
info = getContext().sim->getBelts().getSplitterInfo(m_tile);
|
|
}
|
|
if (!info.has_value())
|
|
{
|
|
m_filterALabel->hide();
|
|
m_filterAList->hide();
|
|
m_filterBLabel->hide();
|
|
m_filterBList->hide();
|
|
return;
|
|
}
|
|
|
|
const std::vector<std::string> itemIds = getAllItemIds(getContext().config->recipes);
|
|
|
|
auto fillList = [&](QListWidget* list, QLabel* label, const QString& directionLabel,
|
|
const std::vector<ItemType>& filter)
|
|
{
|
|
label->setText(tr("%1 filter (empty = all):").arg(directionLabel));
|
|
list->blockSignals(true);
|
|
list->clear();
|
|
for (const std::string& itemId : itemIds)
|
|
{
|
|
// Only implicitly unlocked item types are offered (REQ-LOCK-UI-SPLITTER).
|
|
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
|
|
|
|
QListWidgetItem* row =
|
|
new QListWidgetItem(QString::fromStdString(itemId), list);
|
|
const bool checked = !filter.empty()
|
|
&& std::find(filter.begin(), filter.end(), ItemType{ itemId })
|
|
!= filter.end();
|
|
row->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
|
|
row->setFlags(row->flags() | Qt::ItemIsUserCheckable);
|
|
}
|
|
list->blockSignals(false);
|
|
label->show();
|
|
list->show();
|
|
};
|
|
|
|
fillList(m_filterAList, m_filterALabel, getRotationLabel(info->outputA),
|
|
info->filterA);
|
|
fillList(m_filterBList, m_filterBLabel, getRotationLabel(info->outputB),
|
|
info->filterB);
|
|
}
|
|
|
|
void SplitterContent::applyFilters()
|
|
{
|
|
if (m_isSite)
|
|
{
|
|
std::shared_ptr<SetSiteSplitterFiltersCommand> command =
|
|
std::make_shared<SetSiteSplitterFiltersCommand>();
|
|
command->id = m_id;
|
|
command->filterA = collectCheckedItems(m_filterAList);
|
|
command->filterB = collectCheckedItems(m_filterBList);
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
return;
|
|
}
|
|
|
|
std::shared_ptr<SetSplitterFiltersCommand> command =
|
|
std::make_shared<SetSplitterFiltersCommand>();
|
|
command->tile = m_tile;
|
|
command->filterA = collectCheckedItems(m_filterAList);
|
|
command->filterB = collectCheckedItems(m_filterBList);
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
}
|