Implement building status light (REQ-UI-STATUS-LIGHT)
Add a per-building status light: a small circle drawn in the operational building's upper-right corner (rotating with the building) so production state is readable without selecting it. - Sim: BuildingSystem::getProductionStatus classifies each building into Unconfigured/Producing/Starved/Blocked (nullopt for non-production types); Salvage Bay is a two-state special case (Producing with scrap, Starved when empty). Extracted shared predicates (gatherCandidateRecipes, recipeInputsAvailable, computeShipyardRequiredMaterials, hasInputsToStart) so tickProduction / tickShipyardProduction and the classifier stay in sync. - UI: GameWorldView draws the circle, mapping status -> color from a new visuals.toml [status_light] section (grey/green/red/yellow + outline). - Tests: getProductionStatus cases for every state, including red-over-yellow precedence and the auto-recipe/salvage-bay specials. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -360,3 +360,17 @@ modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs
|
||||
bg = "#000000cc"
|
||||
fg = "#ffffff"
|
||||
font_size = 14
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Building status light (REQ-UI-STATUS-LIGHT)
|
||||
#
|
||||
# Fill color per production state, drawn as a small circle in the building's
|
||||
# upper-right corner, plus the constant outline color.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[status_light]
|
||||
grey = "#808080" # no recipe/schematic selected
|
||||
green = "#33cc33" # producing (Salvage Bay: holding scrap)
|
||||
red = "#cc3333" # idle, input missing (Salvage Bay: empty)
|
||||
yellow = "#e6c619" # idle, output buffer full
|
||||
outline = "#000000"
|
||||
|
||||
@@ -951,43 +951,13 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
||||
// recipe of their type in config order, running the first whose inputs
|
||||
// are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
|
||||
// try only their selected recipe.
|
||||
std::vector<const RecipeDef*> candidates;
|
||||
if (autoRecipe)
|
||||
{
|
||||
for (const RecipeDef& r : m_config.recipes.recipes)
|
||||
{
|
||||
if (r.building == building.type && !r.inputs.empty())
|
||||
{
|
||||
candidates.push_back(&r);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (recipe)
|
||||
{
|
||||
candidates.push_back(recipe);
|
||||
}
|
||||
}
|
||||
const std::vector<const RecipeDef*> candidates =
|
||||
gatherCandidateRecipes(building);
|
||||
|
||||
for (const RecipeDef* recipe : candidates)
|
||||
{
|
||||
// 1. All required inputs present?
|
||||
bool inputsOk = true;
|
||||
for (const RecipeIngredient& ing : recipe->inputs)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
const std::map<ItemType, int>::const_iterator it =
|
||||
building.inputBuffer.counts.find(type);
|
||||
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
||||
if (have < ing.amount)
|
||||
{
|
||||
inputsOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inputsOk)
|
||||
if (!recipeInputsAvailable(building, *recipe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1082,26 +1052,8 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
}
|
||||
|
||||
// Build combined materials list (base + modules).
|
||||
std::map<std::string, int> requiredMaterials;
|
||||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||||
{
|
||||
requiredMaterials[ing.item] += ing.amount;
|
||||
}
|
||||
if (building.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : building.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const RecipeIngredient& ing : modDef->materials)
|
||||
{
|
||||
requiredMaterials[ing.item] += ing.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::map<std::string, int> requiredMaterials =
|
||||
computeShipyardRequiredMaterials(building);
|
||||
|
||||
// Idle: check if all combined materials are available.
|
||||
bool inputsOk = true;
|
||||
@@ -1328,6 +1280,147 @@ int BuildingSystem::getActiveProductionBuildingCount() const
|
||||
return count;
|
||||
}
|
||||
|
||||
std::vector<const RecipeDef*>
|
||||
BuildingSystem::gatherCandidateRecipes(const Building& b) const
|
||||
{
|
||||
std::vector<const RecipeDef*> candidates;
|
||||
if (isAutoRecipeBuildingType(b.type))
|
||||
{
|
||||
for (const RecipeDef& r : m_config.recipes.recipes)
|
||||
{
|
||||
if (r.building == b.type && !r.inputs.empty())
|
||||
{
|
||||
candidates.push_back(&r);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(b.recipeId, b.type);
|
||||
if (recipe)
|
||||
{
|
||||
candidates.push_back(recipe);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
bool BuildingSystem::recipeInputsAvailable(const Building& b,
|
||||
const RecipeDef& recipe) const
|
||||
{
|
||||
for (const RecipeIngredient& ing : recipe.inputs)
|
||||
{
|
||||
const std::map<ItemType, int>::const_iterator it =
|
||||
b.inputBuffer.counts.find(ItemType{ing.item});
|
||||
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
|
||||
if (have < ing.amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::map<std::string, int>
|
||||
BuildingSystem::computeShipyardRequiredMaterials(const Building& b) const
|
||||
{
|
||||
std::map<std::string, int> requiredMaterials;
|
||||
const ShipDef* shipDef = findShipDef(b.recipeId);
|
||||
if (!shipDef)
|
||||
{
|
||||
return requiredMaterials;
|
||||
}
|
||||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||||
{
|
||||
requiredMaterials[ing.item] += ing.amount;
|
||||
}
|
||||
if (b.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const RecipeIngredient& ing : modDef->materials)
|
||||
{
|
||||
requiredMaterials[ing.item] += ing.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return requiredMaterials;
|
||||
}
|
||||
|
||||
bool BuildingSystem::hasInputsToStart(const Building& b) const
|
||||
{
|
||||
if (b.type == BuildingType::Shipyard)
|
||||
{
|
||||
const std::map<std::string, int> required =
|
||||
computeShipyardRequiredMaterials(b);
|
||||
for (const std::pair<const std::string, int>& req : required)
|
||||
{
|
||||
const std::map<ItemType, int>::const_iterator it =
|
||||
b.inputBuffer.counts.find(ItemType{req.first});
|
||||
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
|
||||
if (have < req.second)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recipe buildings: startable if any candidate recipe's inputs are satisfied.
|
||||
// A Miner recipe has no inputs, so an idle Miner is always startable and its
|
||||
// only idle reason is a full output buffer.
|
||||
for (const RecipeDef* recipe : gatherCandidateRecipes(b))
|
||||
{
|
||||
if (recipeInputsAvailable(b, *recipe))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<ProductionStatus>
|
||||
BuildingSystem::getProductionStatus(const Building& building) const
|
||||
{
|
||||
// Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is
|
||||
// "producing" while it holds scrap to push out, and starved when empty.
|
||||
if (building.type == BuildingType::SalvageBay)
|
||||
{
|
||||
return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing
|
||||
: ProductionStatus::Starved;
|
||||
}
|
||||
|
||||
// Only the five recipe/cycle production types show a status light besides the
|
||||
// Salvage Bay; belts, splitters, tunnels, HQ, and stations show none.
|
||||
if (!isProductionBuildingType(building.type))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Grey only applies to player-configured types; auto-recipe buildings
|
||||
// (Smelter, Reprocessing Plant) always run an implicit recipe.
|
||||
if (!isAutoRecipeBuildingType(building.type) && building.recipeId.empty())
|
||||
{
|
||||
return ProductionStatus::Unconfigured;
|
||||
}
|
||||
|
||||
if (building.production.has_value())
|
||||
{
|
||||
return ProductionStatus::Producing;
|
||||
}
|
||||
|
||||
// Idle: missing inputs (red) take precedence over a full output buffer
|
||||
// (yellow). If inputs are present yet the building is idle, the only remaining
|
||||
// reason it could not start a cycle is a full output buffer (REQ-MAT-CYCLE).
|
||||
return hasInputsToStart(building) ? ProductionStatus::Blocked
|
||||
: ProductionStatus::Starved;
|
||||
}
|
||||
|
||||
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::getAllBeltTiles() const
|
||||
{
|
||||
std::vector<BeltTileInfo> result;
|
||||
|
||||
@@ -26,6 +26,18 @@
|
||||
|
||||
class Hasher;
|
||||
|
||||
// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
|
||||
// The simulation owns the classification so it stays in sync with the
|
||||
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
|
||||
// color.
|
||||
enum class ProductionStatus
|
||||
{
|
||||
Unconfigured, // no recipe/schematic selected (grey)
|
||||
Producing, // a production cycle is active (green)
|
||||
Starved, // idle: a required input is missing / Salvage Bay empty (red)
|
||||
Blocked, // idle: output buffer full, inputs otherwise present (yellow)
|
||||
};
|
||||
|
||||
// Manages building placement, construction queuing, and the per-tick
|
||||
// production loop (belt→building pull, production, building→belt push).
|
||||
// All types including Belt and Splitter are stored as Building instances;
|
||||
@@ -122,6 +134,12 @@ public:
|
||||
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
|
||||
// that currently has an active production cycle.
|
||||
int getActiveProductionBuildingCount() const;
|
||||
|
||||
// Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns
|
||||
// nullopt for building types that show no light (belts, splitters, tunnels,
|
||||
// HQ, defence stations). The Salvage Bay is a two-state special case:
|
||||
// Producing while its output buffer holds scrap, Starved when empty.
|
||||
std::optional<ProductionStatus> getProductionStatus(const Building& building) const;
|
||||
std::vector<BeltTileInfo> getAllBeltTiles() const;
|
||||
bool isTileOccupied(QPoint tile) const;
|
||||
|
||||
@@ -200,6 +218,22 @@ private:
|
||||
const Port& outputPort,
|
||||
const Item& item);
|
||||
|
||||
// Candidate recipes an idle building would try this tick: an auto-recipe
|
||||
// building (Smelter, Reprocessing Plant) offers every recipe of its type with
|
||||
// inputs; other buildings offer only their selected recipe. Shared by
|
||||
// tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT).
|
||||
std::vector<const RecipeDef*> gatherCandidateRecipes(const Building& b) const;
|
||||
// True if every input of `recipe` is present in `b`'s input buffers in the
|
||||
// required per-cycle amount (REQ-MAT-CYCLE input check).
|
||||
bool recipeInputsAvailable(const Building& b,
|
||||
const RecipeDef& recipe) const;
|
||||
// Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD).
|
||||
std::map<std::string, int> computeShipyardRequiredMaterials(const Building& b) const;
|
||||
// True if the building currently has all inputs/materials to start a cycle
|
||||
// (ignoring output-buffer space); drives the Starved/Blocked distinction of
|
||||
// the status light (REQ-UI-STATUS-LIGHT).
|
||||
bool hasInputsToStart(const Building& b) const;
|
||||
|
||||
const BuildingDef* findBuildingDef(BuildingType type) const;
|
||||
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
||||
const ShipDef* findShipDef(const std::string& id) const;
|
||||
|
||||
@@ -1308,3 +1308,116 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
|
||||
REQUIRE(builtInfo->filterA == filterA);
|
||||
REQUIRE(builtInfo->filterB.empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Production status classifier (REQ-UI-STATUS-LIGHT)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[building]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
|
||||
// Pick representative config ids so the test survives content edits.
|
||||
std::string minerRecipeId;
|
||||
for (const RecipeDef& r : f.cfg.recipes.recipes)
|
||||
{
|
||||
if (r.building == BuildingType::Miner) { minerRecipeId = r.id; break; }
|
||||
}
|
||||
REQUIRE_FALSE(minerRecipeId.empty());
|
||||
|
||||
const RecipeDef* assemblerRecipe = nullptr;
|
||||
for (const RecipeDef& r : f.cfg.recipes.recipes)
|
||||
{
|
||||
if (r.building == BuildingType::Assembler && !r.inputs.empty())
|
||||
{
|
||||
assemblerRecipe = &r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(assemblerRecipe != nullptr);
|
||||
|
||||
std::string shipId;
|
||||
for (const ShipDef& s : f.cfg.ships.ships)
|
||||
{
|
||||
if (!s.schematic.materials.empty()) { shipId = s.id; break; }
|
||||
}
|
||||
REQUIRE_FALSE(shipId.empty());
|
||||
|
||||
const auto statusOf = [&f](const Building& b) { return f.bs.getProductionStatus(b); };
|
||||
|
||||
SECTION("non-production buildings show no status light")
|
||||
{
|
||||
Building belt; belt.type = BuildingType::Belt;
|
||||
Building hq; hq.type = BuildingType::Hq;
|
||||
REQUIRE_FALSE(statusOf(belt).has_value());
|
||||
REQUIRE_FALSE(statusOf(hq).has_value());
|
||||
}
|
||||
|
||||
SECTION("Miner: unconfigured, producing, output-blocked")
|
||||
{
|
||||
Building miner; miner.type = BuildingType::Miner;
|
||||
REQUIRE(statusOf(miner) == ProductionStatus::Unconfigured); // no recipe -> grey
|
||||
|
||||
miner.recipeId = minerRecipeId;
|
||||
miner.production = Production{};
|
||||
REQUIRE(statusOf(miner) == ProductionStatus::Producing); // active cycle -> green
|
||||
|
||||
// A miner has no inputs, so its only idle reason is a full output buffer.
|
||||
miner.production = std::nullopt;
|
||||
miner.outputBuffer.capacity = 2;
|
||||
miner.outputBuffer.items = { makeItem("iron_ore"), makeItem("iron_ore") };
|
||||
REQUIRE(statusOf(miner) == ProductionStatus::Blocked); // -> yellow
|
||||
}
|
||||
|
||||
SECTION("Assembler: starved vs blocked, input-missing takes precedence")
|
||||
{
|
||||
Building assembler; assembler.type = BuildingType::Assembler;
|
||||
assembler.recipeId = assemblerRecipe->id;
|
||||
|
||||
// Idle with inputs missing -> red.
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Starved);
|
||||
|
||||
// Inputs present but idle -> the only remaining reason is a full output
|
||||
// buffer -> yellow.
|
||||
for (const RecipeIngredient& ing : assemblerRecipe->inputs)
|
||||
{
|
||||
assembler.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
|
||||
}
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Blocked);
|
||||
|
||||
// Inputs missing AND output full -> red wins over yellow.
|
||||
assembler.inputBuffer.counts.clear();
|
||||
assembler.outputBuffer.capacity = 2;
|
||||
assembler.outputBuffer.items = { makeItem("x"), makeItem("x") };
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Starved);
|
||||
}
|
||||
|
||||
SECTION("Smelter (auto-recipe) is never grey")
|
||||
{
|
||||
Building smelter; smelter.type = BuildingType::Smelter;
|
||||
// No player-selectable recipe and empty inputs -> red, not grey.
|
||||
REQUIRE(statusOf(smelter) == ProductionStatus::Starved);
|
||||
}
|
||||
|
||||
SECTION("Shipyard: unconfigured, then starved without materials, then producing")
|
||||
{
|
||||
Building yard; yard.type = BuildingType::Shipyard;
|
||||
REQUIRE(statusOf(yard) == ProductionStatus::Unconfigured); // no schematic -> grey
|
||||
|
||||
yard.recipeId = shipId;
|
||||
REQUIRE(statusOf(yard) == ProductionStatus::Starved); // no materials -> red
|
||||
|
||||
yard.production = Production{};
|
||||
REQUIRE(statusOf(yard) == ProductionStatus::Producing); // active cycle -> green
|
||||
}
|
||||
|
||||
SECTION("Salvage Bay: red when empty, green when holding scrap")
|
||||
{
|
||||
Building bay; bay.type = BuildingType::SalvageBay;
|
||||
bay.outputBuffer.capacity = 20;
|
||||
REQUIRE(statusOf(bay) == ProductionStatus::Starved); // empty -> red
|
||||
|
||||
bay.outputBuffer.items = { makeItem("scrap") };
|
||||
REQUIRE(statusOf(bay) == ProductionStatus::Producing); // holding scrap -> green
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,20 @@ QPoint portBodyTile(QPoint portTile, Rotation direction)
|
||||
return portTile;
|
||||
}
|
||||
|
||||
// Fill color for a building's status light per its production state
|
||||
// (REQ-UI-STATUS-LIGHT).
|
||||
QColor statusLightFill(ProductionStatus status, const StatusLightVisuals& sl)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case ProductionStatus::Unconfigured: return sl.grey;
|
||||
case ProductionStatus::Producing: return sl.green;
|
||||
case ProductionStatus::Starved: return sl.red;
|
||||
case ProductionStatus::Blocked: return sl.yellow;
|
||||
}
|
||||
return sl.grey;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -1039,6 +1053,35 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
||||
port.direction, bv.outline, /*centered*/ false);
|
||||
}
|
||||
|
||||
// Status light: a small circle in the building's upper-right corner
|
||||
// (in default/East orientation), anchored to that footprint corner and
|
||||
// rotating with the building (REQ-UI-STATUS-LIGHT). All status-light
|
||||
// building footprints are rectangular, so a corner of the axis-aligned
|
||||
// bounding box is the true corner.
|
||||
if (const std::optional<ProductionStatus> status =
|
||||
m_sim->getBuildings().getProductionStatus(b))
|
||||
{
|
||||
const float px = getTilePx();
|
||||
const float r = px * 0.18f;
|
||||
const float inset = r + px * 0.12f;
|
||||
// Default orientation (East) puts the light at the top-right corner;
|
||||
// clockwise rotation carries it around the footprint.
|
||||
QPointF center(bboxRect.right() - inset, bboxRect.top() + inset);
|
||||
switch (b.rotation)
|
||||
{
|
||||
case Rotation::East: break;
|
||||
case Rotation::South: center = QPointF(bboxRect.right() - inset,
|
||||
bboxRect.bottom() - inset); break;
|
||||
case Rotation::West: center = QPointF(bboxRect.left() + inset,
|
||||
bboxRect.bottom() - inset); break;
|
||||
case Rotation::North: center = QPointF(bboxRect.left() + inset,
|
||||
bboxRect.top() + inset); break;
|
||||
}
|
||||
painter.setBrush(statusLightFill(*status, m_visuals->statusLight));
|
||||
painter.setPen(QPen(m_visuals->statusLight.outline, 1));
|
||||
painter.drawEllipse(center, r, r);
|
||||
}
|
||||
|
||||
// HP bar below the HQ footprint; the HQ's HP lives on its proxy entity.
|
||||
if (b.type == BuildingType::Hq)
|
||||
{
|
||||
|
||||
@@ -60,6 +60,17 @@ struct ToastVisuals
|
||||
int fontSize;
|
||||
};
|
||||
|
||||
// Fill colors for the building status light (REQ-UI-STATUS-LIGHT), plus its
|
||||
// constant outline color.
|
||||
struct StatusLightVisuals
|
||||
{
|
||||
QColor grey; // no recipe/schematic selected
|
||||
QColor green; // producing
|
||||
QColor red; // idle, input missing (or Salvage Bay empty)
|
||||
QColor yellow; // idle, output buffer full
|
||||
QColor outline;
|
||||
};
|
||||
|
||||
struct VisualsConfig
|
||||
{
|
||||
TileVisuals asteroid;
|
||||
@@ -69,7 +80,8 @@ struct VisualsConfig
|
||||
std::map<std::string, ItemVisuals> items;
|
||||
std::map<std::string, ShipVisuals> ships;
|
||||
|
||||
BeamVisuals beams;
|
||||
OverlayVisuals overlays;
|
||||
ToastVisuals toast;
|
||||
BeamVisuals beams;
|
||||
OverlayVisuals overlays;
|
||||
ToastVisuals toast;
|
||||
StatusLightVisuals statusLight;
|
||||
};
|
||||
|
||||
@@ -237,5 +237,15 @@ VisualsConfig VisualsLoader::load(const std::string& path)
|
||||
cfg.toast.fontSize = requireInt(t, "font_size", "toast");
|
||||
}
|
||||
|
||||
// Status light (REQ-UI-STATUS-LIGHT)
|
||||
{
|
||||
toml::table& sl = requireSubtable(tbl, "status_light", "root");
|
||||
cfg.statusLight.grey = parseColor(requireString(sl, "grey", "status_light"), "status_light.grey");
|
||||
cfg.statusLight.green = parseColor(requireString(sl, "green", "status_light"), "status_light.green");
|
||||
cfg.statusLight.red = parseColor(requireString(sl, "red", "status_light"), "status_light.red");
|
||||
cfg.statusLight.yellow = parseColor(requireString(sl, "yellow", "status_light"), "status_light.yellow");
|
||||
cfg.statusLight.outline = parseColor(requireString(sl, "outline", "status_light"), "status_light.outline");
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user