From 6005854d7e8707bc4a674e9318a74db0da585cb6 Mon Sep 17 00:00:00 2001 From: CodedSakura Date: Tue, 11 Aug 2026 14:04:59 +0300 Subject: [PATCH] implement heat sink validation --- scripts/dev-mode.js | 2 +- scripts/files.js | 4 +- scripts/overlays.js | 13 ++++ scripts/planner.js | 24 ++++++- scripts/solver.js | 166 +++++++++++++++++++++++++++++++++++++++++--- style.css | 8 +++ 6 files changed, 206 insertions(+), 11 deletions(-) diff --git a/scripts/dev-mode.js b/scripts/dev-mode.js index 0c7a91d..21948f3 100644 --- a/scripts/dev-mode.js +++ b/scripts/dev-mode.js @@ -39,7 +39,7 @@ registerLoadCallback(() => { }); awaitEvent("config:end").then(() => { - void loadExampleFile("neutron test"); + void loadExampleFile("Pandora's Fission Reactor"); }); queueMicrotask(() => emitEvent("dev-mode:end")); diff --git a/scripts/files.js b/scripts/files.js index 3cb2ca9..8fd080c 100644 --- a/scripts/files.js +++ b/scripts/files.js @@ -132,7 +132,7 @@ export async function saveFile(name = undefined) { if (!fullBlock || !fullBlock.modules || !("ncpf:block_recipes" in fullBlock.modules)) return; const designIndex = designBlocksAtlasMap[Number(designAsCellData[x][y][z].tile)].designIndex; - const solverMetadata = getMetadata(designAsCellData[x][y][z].solverMetadata); + const solverMetadata = getMetadata(designAsCellData[x][y][z].metadata); const minimizedBlock = minimizedBlocks[designIndex]; let recipeIndex = -1; @@ -156,6 +156,8 @@ export async function saveFile(name = undefined) { } } + console.log(recipeIndex) + return recipeIndex; }) .filter(b => b != null) diff --git a/scripts/overlays.js b/scripts/overlays.js index b7cb80e..786bb9c 100644 --- a/scripts/overlays.js +++ b/scripts/overlays.js @@ -11,6 +11,19 @@ export function processCellOverlays(node, key = null, added = null) { overlayNode.classList.add("planner--overlay"); node.appendChild(overlayNode); } + + if (key === "active" && metadata) { + const overlayNode = makeAtlasImage(-1); + overlayNode.dataset.overlay = key; + overlayNode.classList.add("planner--overlay"); + node.appendChild(overlayNode); + } + if (key === "active" && !metadata) { + const overlayNode = makeAtlasImage(-1); + overlayNode.dataset.overlay = "inactive"; + overlayNode.classList.add("planner--overlay"); + node.appendChild(overlayNode); + } // FIXME: handle remove } // TODO: handle generic update diff --git a/scripts/planner.js b/scripts/planner.js index b81bdb8..ed36ae0 100644 --- a/scripts/planner.js +++ b/scripts/planner.js @@ -384,6 +384,22 @@ export function* getCellTooltip(cell) { const moderator = data.modules["nuclearcraft:overhaul_sfr:moderator"]; yield `Flux factor: ${moderator.flux}`; yield `Efficiency: ${moderator.efficiency}`; + yield { + type: "compound", + items: [ + "Moderator ", + { + type: "bold", + text: solverMetadata?.active ? "active" : "inactive", + }, + " for heat sinks", + ], + }; + if (solverMetadata?.flux) { + for (const [axis, flux] of Object.entries(solverMetadata.flux)) { + yield `Flux on ${axis}: ${flux}`; + } + } } if ("nuclearcraft:overhaul_sfr:reflector" in data.modules) { @@ -394,13 +410,19 @@ export function* getCellTooltip(cell) { if ("nuclearcraft:overhaul_sfr:heat_sink" in data.modules) { const heatSink = data.modules["nuclearcraft:overhaul_sfr:heat_sink"]; - if (heatSink.rules) { + if (heatSink.rules && !solverMetadata.active) { for (const rule of heatSink.rules) { yield "Rule not met: " + parseRuleAsString(rule, { configurationHint: metadata.configuration }); } } } + if ("nuclearcraft:overhaul_sfr:irradiator" in data.modules) { + if (solverMetadata?.flux) { + yield `Flux: ${solverMetadata.flux}`; + } + } + yield { type: "italics", text: metadata.configurationAddon.name, diff --git a/scripts/solver.js b/scripts/solver.js index 03cdb2b..415cc2d 100644 --- a/scripts/solver.js +++ b/scripts/solver.js @@ -72,6 +72,18 @@ function *travelUntil(grid, { x, y, z }, {x: dirX = 0, y: dirY = 0, z: dirZ = 0} yield grid[x][y][z]; } } +function neighbor(grid, { x, y, z }, {x: dirX = 0, y: dirY = 0, z: dirZ = 0}) { + x = Number(x); + y = Number(y); + z = Number(z); + if (x > 0 && y > 0 && z > 0 + && x < grid.length - 1 && y < grid[0].length - 1 && z < grid[0][0].length - 1) { + x += dirX; + y += dirY; + z += dirZ; + return grid[x][y][z]; + } +} async function debugBlockSolver(data) { const planner = getPlannerDom(); @@ -107,13 +119,15 @@ export async function runSolver() { // const reflectors = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:reflector"]); // const shields = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:neutron_shield"]); // const moderators = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:moderator"]); - // const coolers = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:heat_sink"]); + const heatSinks = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:heat_sink"]); // const irradiators = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:irradiator"]); // const conductors = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:conductor"]); fuelCells.forEach(cell => { const recipeList = cell.tile.modules["ncpf:block_recipes"].recipes; - cell.recipe = recipeList.find(r => checkEquivalence(r, cell.metadata.block_recipe)); + cell.recipe = recipeList.find(r => checkEquivalence(r, cell.metadata?.block_recipe)); + // TODO: handle blank recipe + if (!cell.recipe) return; // FIXME cell.axes = Object.values(sixAxes).map(dir => [...travelUntil( plannerBlocksGrid, cell, @@ -136,36 +150,172 @@ export async function runSolver() { )]); if (cell.recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"].self_priming) { cell.primer = cell; + cell.active = true; } else if (cell.axes.some(a => (a[a.length - 1]?.tile?.modules ?? {})["nuclearcraft:overhaul_sfr:neutron_source"])) { const primerAxis = cell.axes.find(a => (a[a.length - 1]?.tile?.modules ?? {})["nuclearcraft:overhaul_sfr:neutron_source"]); cell.primer = primerAxis[primerAxis.length - 1]; + cell.active = true; } setCellMetadata(cell.dom, "primer", cell.primer); }); await debugBlockSolver("primed"); - fuelCells.forEach(cell => { + const seenModerators = new Set(); + const activeCells = fuelCells.filter(c => c.active); + let activeCellIndex = 0; + while (activeCellIndex < activeCells.length) { + const cell = activeCells[activeCellIndex]; + if (!cell.axes) return; // ignore bad cells Object.keys(sixAxes).map((name, i) => { - console.log(name, cell.axes[i]); let moderatorSum = 0; + let moderatorPath = []; for (const block of cell.axes[i]) { + if (!block) break; + if (block.tile.modules["nuclearcraft:overhaul_sfr:moderator"]) { moderatorSum += block.tile.modules["nuclearcraft:overhaul_sfr:moderator"].flux; + moderatorPath.push(block); + seenModerators.add(block); + if (!block.flux) { + block.flux = {}; + } + block.flux[name] = moderatorSum; continue; } - console.log(block); if (block.tile.modules["nuclearcraft:overhaul_sfr:fuel_cell"] || block.tile.modules["nuclearcraft:overhaul_sfr:irradiator"]) { block.flux = (block.flux ?? 0) + moderatorSum; - setCellMetadata(block.dom, "flux", block.flux); } if (block.tile.modules["nuclearcraft:overhaul_sfr:reflector"]) { - cell.flux = (cell.flux ?? 0) + moderatorSum * 2; - setCellMetadata(cell.dom, "flux", cell.flux); + const backAxis = name.replace(/^[+-]/, s => s === "+" ? "-" : "+"); + for (const blockBack of moderatorPath.toReversed()) { + moderatorSum += blockBack.tile.modules["nuclearcraft:overhaul_sfr:moderator"].flux; + moderatorPath.push(blockBack); + blockBack.flux[backAxis] = moderatorSum; + } + cell.flux = (cell.flux ?? 0) + moderatorSum; + } + if (block.tile.modules["nuclearcraft:overhaul_sfr:fuel_cell"] && !block.active) { + if (!block.recipe) break; // FIXME + const fuelStats = block.recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"]; + if (fuelStats.criticality <= block.flux) { + block.active = true; + activeCells.push(block); + } } break; } + if (moderatorPath.length > 0) { + moderatorPath[0].active = true; + } }); + + activeCellIndex++; + } + seenModerators.forEach(moderator => { + setCellMetadata(moderator.dom, "flux", moderator.flux); + if (moderator.active) { + setCellMetadata(moderator.dom, "active", moderator.active); + } }); + fuelCells.forEach(cell => { + if (cell.flux) { + setCellMetadata(cell.dom, "flux", cell.flux); + } + setCellMetadata(cell.dom, "active", cell.active ?? false); + }); + + await debugBlockSolver("moderators"); + + heatSinks.forEach(hs => { + hs.neighbors = {}; + Object.entries(sixAxes).forEach(([name, dir]) => { + hs.neighbors[name] = neighbor(plannerBlocksGrid, hs, dir); + }); + hs.rules = hs.tile.modules["nuclearcraft:overhaul_sfr:heat_sink"].rules; + }); + let heatSinkProcessed = false; + const unprocessedHeatSinks = new Set(heatSinks); + do { + heatSinkProcessed = false; + unprocessedHeatSinks.forEach(hs => { + if (hs.active) { + console.log("active hs in queue", hs); + return; + } + const activeNeighbors = Object.entries(hs.neighbors).filter(([_, n]) => { + if (!n) return false; + if (n.active) return true; + if (n.tile.modules) { + return n.tile.modules["nuclearcraft:overhaul_sfr:reflector"] + || n.tile.modules["nuclearcraft:overhaul_sfr:irradiator"] + || n.tile.modules["nuclearcraft:overhaul_sfr:casing"]; + } + return false; + }); + if (activeNeighbors.length === 0) return; + + let valid = false; + for (const rule of hs.rules) { + if (checkHeatSinkRule(hs, rule, activeNeighbors)) { + valid = true; + break; + } + } + if (valid) { + heatSinkProcessed = true; + unprocessedHeatSinks.delete(hs); + hs.active = true; + setCellMetadata(hs.dom, "active", true); + } + }); + } while (heatSinkProcessed); + unprocessedHeatSinks.forEach(hs => { + setCellMetadata(hs.dom, "active", false); + }); + console.log(unprocessedHeatSinks); + + await debugBlockSolver("heatsinks"); + } +} + +function checkHeatSinkRule(hs, rule, activeNeighbors) { + switch (rule.type) { + case "and": + return rule.rules.every(r => checkHeatSinkRule(hs, r, activeNeighbors)); + + case "or": + return rule.rules.some(r => checkHeatSinkRule(hs, r, activeNeighbors)); + + case "between": { + const blocks = activeNeighbors + .map(([,b]) => b.tile) + .filter(b => { + if (rule.block.type === "module") { + return b.modules && rule.block.name in b.modules; + } + return checkEquivalence(rule.block, b); + }); + return rule.min <= blocks.length && blocks.length <= rule.max; + } + case "axial": { + const axialPairs = activeNeighbors + .filter(([,b]) => { + if (rule.block.type === "module") { + return b.tile.modules && rule.block.name in b.tile.modules; + } + return checkEquivalence(rule.block, b.tile); + }) + .reduce((acc, [dir, block]) => { + acc[dir[1]] = acc[dir[1]] ?? []; + acc[dir[1]].push(block); + return acc; + }, {}); + const count = Object.values(axialPairs).filter(p => p.length === 2).length; + return rule.min <= count && count <= rule.max; + } + default: + console.warn("unknown rule: %o", rule); + return false; } } diff --git a/style.css b/style.css index 999a47e..d31436d 100644 --- a/style.css +++ b/style.css @@ -271,6 +271,14 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) { .planner--cell > .planner--overlay[data-overlay="block_recipe"] { transform: scale(var(--recipe-scale-factor, 0.8)); } +#planner .planner--cell > .planner--overlay[data-overlay="active"] { + outline-offset: calc(var(--scale-factor) * -1px); + outline: calc(var(--scale-factor) * 1px) solid var(--cell-active-outline, #0f08); +} +#planner .planner--cell > .planner--overlay[data-overlay="inactive"] { + outline-offset: calc(var(--scale-factor) * -1px); + outline: calc(var(--scale-factor) * 1px) solid var(--cell-inactive-outline, #f008); +} .planner--resize { --resize-width: var(--width);