Last active
June 24, 2024 14:26
-
-
Save Lucus16/5c9452277f1ff3d7a6d8b359db5b0582 to your computer and use it in GitHub Desktop.
A better fluid model for Factorio.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Optimal flow speed is when pipes are at half pressure. | |
const Node = struct { | |
// All of these must be positive, I'm just using signed types to avoid casting. | |
height: i32, | |
width: i32, | |
contents: i32, | |
potential_inflow: i32, | |
potential_outflow: i32, | |
pub inline fn capacity(node: Node) i32 { | |
return node.width * node.height; | |
} | |
pub inline fn space(node: Node) i32 { | |
return node.capacity() - node.contents; | |
} | |
pub inline fn level(node: Node) i32 { | |
return @divTrunc(node.contents, node.width); | |
} | |
}; | |
const Edge = struct { | |
node_a: *Node, | |
node_b: *Node, | |
// Positive means flow from A to B, negative means flow from B to A. | |
flow_ab: i32, | |
node_a_min_level: i32, | |
node_b_min_level: i32, | |
// TODO: Allow top-up valves by introducing a max_level or min_space. | |
}; | |
inline fn muldiv(x: i32, multiplier: i32, divisor: i32) i32 { | |
return @intCast(@divTrunc(@as(i64, x) * @as(i64, multiplier), @as(i64, divisor))); | |
} | |
// Invariant: The flow through a pipe is never larger than the total amount of | |
// fluid in its input and never larger than the total amount of space in its | |
// output. | |
const Model = struct { | |
nodes: []Node, | |
edges: []Edge, | |
fn tick(model: Model) void { | |
// Clear flow potentials. | |
for (model.nodes) |*node| { | |
node.potential_inflow = 0; | |
node.potential_outflow = 0; | |
} | |
// Compute ideal next flow level, based on level difference. | |
for (model.edges) |*edge| { | |
const effective_node_a_level = @max(0, edge.node_a.level() - edge.node_a_min_level); | |
const effective_node_b_level = @max(0, edge.node_b.level() - edge.node_b_min_level); | |
const force_ab_from_level = effective_node_a_level - effective_node_b_level; | |
// Increase the flow by a portion of the level difference to model inertia. | |
// Don't go higher than 1/2. | |
edge.flow_ab += muldiv(force_ab_from_level, 1, 10); | |
// If desired, at this point you can add resistance to edges, which | |
// can reduce the maximum amount of flow based on the number edges | |
// being traversed. | |
// For example, to lower the speed exponentially with distance: | |
// edge.flow_ab = muldiv(edge.flow_ab, 99, 100); | |
// Or to lower it linearly with distance: | |
// edge.flow_ab = @max(0, edge.flow_ab - 10); | |
edge.node_a.potential_inflow += @max(0, -edge.flow_ab); | |
edge.node_a.potential_outflow += @max(0, edge.flow_ab); | |
edge.node_b.potential_inflow += @max(0, edge.flow_ab); | |
edge.node_b.potential_outflow += @max(0, -edge.flow_ab); | |
} | |
// Scale down flow based on available input fluid and output space. | |
for (model.edges) |*edge| { | |
if (edge.flow_ab > 0) { | |
// Flow from A to B. | |
const input_limited_flow = muldiv(edge.flow_ab, edge.node_a.contents, edge.node_a.potential_outflow); | |
const output_limited_flow = muldiv(edge.flow_ab, edge.node_b.space(), edge.node_b.potential_inflow); | |
edge.flow_ab = @min(edge.flow_ab, @min(input_limited_flow, output_limited_flow)); | |
} else if (edge.flow_ab < 0) { | |
// Flow from B to A. | |
const flow_ba = -edge.flow_ab; | |
const input_limited_flow = muldiv(flow_ba, edge.node_b.contents, edge.node_b.potential_outflow); | |
const output_limited_flow = muldiv(flow_ba, edge.node_a.space(), edge.node_a.potential_inflow); | |
edge.flow_ab = -@min(flow_ba, @min(input_limited_flow, output_limited_flow)); | |
} | |
} | |
// Apply flow. This must be a separate step from computing the flow | |
// since the flow computation is based on the node contents which are | |
// changed here. | |
for (model.edges) |edge| { | |
edge.node_a.contents -= edge.flow_ab; | |
edge.node_b.contents += edge.flow_ab; | |
} | |
} | |
}; | |
pub fn main() !void { | |
var model = Model{ | |
.nodes = &.{}, | |
.edges = &.{}, | |
}; | |
model.tick(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The only actual if statement can also easily be converted into branchless code due to its symmetry by the way, I just wrote it like this because it's more readable.