implement heat sink validation

This commit is contained in:
CodedSakura 2026-08-11 14:04:59 +03:00
parent 2b542e5f34
commit fdd4fde699
7 changed files with 207 additions and 12 deletions

View File

@ -146,7 +146,7 @@
Roadmap: Roadmap:
</p> </p>
<ul> <ul>
<li>Reactor verification and stats</li> <li>Reactor stats</li>
<li>Casing improvements</li> <li>Casing improvements</li>
<li>Keyboard shortcuts</li> <li>Keyboard shortcuts</li>
<li>MSR support</li> <li>MSR support</li>

View File

@ -39,7 +39,7 @@ registerLoadCallback(() => {
}); });
awaitEvent("config:end").then(() => { awaitEvent("config:end").then(() => {
void loadExampleFile("neutron test"); void loadExampleFile("Pandora's Fission Reactor");
}); });
queueMicrotask(() => emitEvent("dev-mode:end")); queueMicrotask(() => emitEvent("dev-mode:end"));

View File

@ -132,7 +132,7 @@ export async function saveFile(name = undefined) {
if (!fullBlock || !fullBlock.modules || !("ncpf:block_recipes" in fullBlock.modules)) return; if (!fullBlock || !fullBlock.modules || !("ncpf:block_recipes" in fullBlock.modules)) return;
const designIndex = designBlocksAtlasMap[Number(designAsCellData[x][y][z].tile)].designIndex; 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]; const minimizedBlock = minimizedBlocks[designIndex];
let recipeIndex = -1; let recipeIndex = -1;
@ -156,6 +156,8 @@ export async function saveFile(name = undefined) {
} }
} }
console.log(recipeIndex)
return recipeIndex; return recipeIndex;
}) })
.filter(b => b != null) .filter(b => b != null)

View File

@ -11,6 +11,19 @@ export function processCellOverlays(node, key = null, added = null) {
overlayNode.classList.add("planner--overlay"); overlayNode.classList.add("planner--overlay");
node.appendChild(overlayNode); 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 // FIXME: handle remove
} }
// TODO: handle generic update // TODO: handle generic update

View File

@ -384,6 +384,22 @@ export function* getCellTooltip(cell) {
const moderator = data.modules["nuclearcraft:overhaul_sfr:moderator"]; const moderator = data.modules["nuclearcraft:overhaul_sfr:moderator"];
yield `Flux factor: ${moderator.flux}`; yield `Flux factor: ${moderator.flux}`;
yield `Efficiency: ${moderator.efficiency}`; 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) { 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) { if ("nuclearcraft:overhaul_sfr:heat_sink" in data.modules) {
const heatSink = data.modules["nuclearcraft:overhaul_sfr:heat_sink"]; const heatSink = data.modules["nuclearcraft:overhaul_sfr:heat_sink"];
if (heatSink.rules) { if (heatSink.rules && !solverMetadata.active) {
for (const rule of heatSink.rules) { for (const rule of heatSink.rules) {
yield "Rule not met: " + parseRuleAsString(rule, { configurationHint: metadata.configuration }); 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 { yield {
type: "italics", type: "italics",
text: metadata.configurationAddon.name, text: metadata.configurationAddon.name,

View File

@ -72,6 +72,18 @@ function *travelUntil(grid, { x, y, z }, {x: dirX = 0, y: dirY = 0, z: dirZ = 0}
yield grid[x][y][z]; 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) { async function debugBlockSolver(data) {
const planner = getPlannerDom(); 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 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 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 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 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"]); // const conductors = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:conductor"]);
fuelCells.forEach(cell => { fuelCells.forEach(cell => {
const recipeList = cell.tile.modules["ncpf:block_recipes"].recipes; 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( cell.axes = Object.values(sixAxes).map(dir => [...travelUntil(
plannerBlocksGrid, plannerBlocksGrid,
cell, cell,
@ -136,36 +150,172 @@ export async function runSolver() {
)]); )]);
if (cell.recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"].self_priming) { if (cell.recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"].self_priming) {
cell.primer = cell; cell.primer = cell;
cell.active = true;
} else if (cell.axes.some(a => (a[a.length - 1]?.tile?.modules ?? {})["nuclearcraft:overhaul_sfr:neutron_source"])) { } 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"]); const primerAxis = cell.axes.find(a => (a[a.length - 1]?.tile?.modules ?? {})["nuclearcraft:overhaul_sfr:neutron_source"]);
cell.primer = primerAxis[primerAxis.length - 1]; cell.primer = primerAxis[primerAxis.length - 1];
cell.active = true;
} }
setCellMetadata(cell.dom, "primer", cell.primer); setCellMetadata(cell.dom, "primer", cell.primer);
}); });
await debugBlockSolver("primed"); 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) => { Object.keys(sixAxes).map((name, i) => {
console.log(name, cell.axes[i]);
let moderatorSum = 0; let moderatorSum = 0;
let moderatorPath = [];
for (const block of cell.axes[i]) { for (const block of cell.axes[i]) {
if (!block) break;
if (block.tile.modules["nuclearcraft:overhaul_sfr:moderator"]) { if (block.tile.modules["nuclearcraft:overhaul_sfr:moderator"]) {
moderatorSum += block.tile.modules["nuclearcraft:overhaul_sfr:moderator"].flux; 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; continue;
} }
console.log(block);
if (block.tile.modules["nuclearcraft:overhaul_sfr:fuel_cell"] || block.tile.modules["nuclearcraft:overhaul_sfr:irradiator"]) { if (block.tile.modules["nuclearcraft:overhaul_sfr:fuel_cell"] || block.tile.modules["nuclearcraft:overhaul_sfr:irradiator"]) {
block.flux = (block.flux ?? 0) + moderatorSum; block.flux = (block.flux ?? 0) + moderatorSum;
setCellMetadata(block.dom, "flux", block.flux);
} }
if (block.tile.modules["nuclearcraft:overhaul_sfr:reflector"]) { if (block.tile.modules["nuclearcraft:overhaul_sfr:reflector"]) {
cell.flux = (cell.flux ?? 0) + moderatorSum * 2; const backAxis = name.replace(/^[+-]/, s => s === "+" ? "-" : "+");
setCellMetadata(cell.dom, "flux", cell.flux); 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; 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;
} }
} }

View File

@ -271,6 +271,14 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
.planner--cell > .planner--overlay[data-overlay="block_recipe"] { .planner--cell > .planner--overlay[data-overlay="block_recipe"] {
transform: scale(var(--recipe-scale-factor, 0.8)); 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 { .planner--resize {
--resize-width: var(--width); --resize-width: var(--width);