From fdd4fde699979624e1e5642d5e033a270af3496b Mon Sep 17 00:00:00 2001
From: CodedSakura
Date: Tue, 11 Aug 2026 14:04:59 +0300
Subject: [PATCH] implement heat sink validation
---
index.html | 2 +-
scripts/dev-mode.js | 2 +-
scripts/files.js | 4 +-
scripts/overlays.js | 13 ++++
scripts/planner.js | 24 ++++++-
scripts/solver.js | 166 +++++++++++++++++++++++++++++++++++++++++---
style.css | 8 +++
7 files changed, 207 insertions(+), 12 deletions(-)
diff --git a/index.html b/index.html
index 34ff839..7e47541 100644
--- a/index.html
+++ b/index.html
@@ -146,7 +146,7 @@
Roadmap:
- - Reactor verification and stats
+ - Reactor stats
- Casing improvements
- Keyboard shortcuts
- MSR support
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);