-
Notifications
You must be signed in to change notification settings - Fork 0
Block Networks
A generic connected-graph framework: self-orienting node blocks, live network instances with
merge/fracture handling, and a single manager ModSystem. It also ships
the two concrete networks the mods use - the PipeNetwork (gas + water, registered as "pipe" by
iiex) and the MoltenNetwork (metal canals, registered as "molten" by iiex) - so all three
mods share one implementation. Each mod just registers the type and supplies its own
content-specific pieces through the seams below. You register your own network type the same way.
⚠ A block does not have to spend its base class on being a node. Form, process and membership are three independent axes: the base class belongs to form (what the block is - a container, a multiblock part), while network membership and the production tick are behaviours you attach.
BlockNetworkNodebelow is the convenient answer when a block has no other form to be, and it is what the shipped pipes use - but a block that must also be, say, a container hostsBEBehaviorNetworkMemberinstead of inheriting from here.BlockEntityNetworkNodeis itself only a host for that behaviour, so the two routes join immediately. See Production Machines for the same split on the process axis.
The model (BlockNetwork and every I*Node/I*Connector interface - no Vintage Story block types
involved) and the engine-facing shell (BlockNetworkNode, BlockEntityNetworkNode,
BlockNetworkModSystem) now share one namespace, ExpandedLib.Networks; a modder building a
network imports it once. ExpandedLib.Industry.Pipes / ExpandedLib.Industry.Molten hold
the shipped PipeNetwork/PipeNetworkState and MoltenNetwork themselves, alongside their own node
blocks and block entities.
Network tunables (litres per pipe, leak/evaporation rates, over-pressure grace, molten flow rate
and minimum) live in exlib's own config, ExlibValues (the exlib section of ModConfig/ex_values.json,
/exmod config exlib …), since the network code that reads them lives here now. Content-specific
numbers (pipe burst pressures, chimney draw rate, molten cooldown) stay in each mod's own config.
| Piece | Type | Role |
|---|---|---|
BlockNetworkNode |
abstract Block
|
A node block that auto-orients to its neighbours and reports its connector faces. |
BlockEntityNetworkNode |
abstract BlockEntity
|
The node's block entity: registers/unregisters with the graph, persists state, forwards network updates. |
BlockNetwork |
abstract class | One live network instance: owns typed State, merges/splits/ticks. |
BlockNetworkModSystem |
ModSystem |
The graph manager: add/remove nodes, BFS fracture detection, per-tick dispatch. |
INetworkNode / INetworkConnector
|
interfaces | Contracts a block entity / block implement to participate. |
The mental model: blocks expose connector faces, block entities are graph nodes, the
manager maintains the graph and hands each connected component a BlockNetwork instance
that carries your gameplay state (pressure, fluid, temperature...).
Once, during ModSystem.Start, give the manager a factory for your network type:
public override void Start(ICoreAPI api)
{
var networks = api.ModLoader.GetModSystem<BlockNetworkModSystem>();
// iiex's pipe: the vent strategy is optional content (the vanilla-chimney gas draw).
networks.RegisterNetworkType("pipe", () => new PipeNetwork(networks, new IiexChimneyVent()));
// iiex's molten canals need no extra pieces beyond the IMoltenCell block entities.
networks.RegisterNetworkType("molten", () => new MoltenNetwork(networks));
}The manager creates one network per connected component and calls into it as the topology and clock advance. The factory runs once per network instance, so anything the network needs per-run (e.g. the chimney-vent's sound-throttle state) can be created fresh in the factory.
Because PipeNetwork / MoltenNetwork live in exlib, they reach content-specific behaviour
through small interfaces the mods implement, never by naming a mod's block type:
| Seam | Implemented by | Role |
|---|---|---|
IMoltenCell |
the canal block entities | Per-cell metal state + capability flags (IsFlowSource, AcceptsSubMinimumFlow) the molten flow driver reads. |
IBurstablePipe |
BlockPipe |
{ CanBurst, BurstPressure } - the pipe network walks nodes for the weakest burst rating. |
IPipeVentStrategy |
IiexChimneyVent |
Optional gas-vent (chimney) classification + draw, injected at RegisterNetworkType. |
INetworkNode.OnLeak |
BlockEntityPipe (override) |
Leak feedback (particles/sound) for a node on a leaking run; default no-op. |
The orientation table is not hand-written: BlockNetworkNode derives it from this class's own
code-first definitions, so the shape's type x orientation map lives in the variant groups and in
one place only.
[BlockRegister]
public partial class MyPipe : BlockNetworkNode, IExBlockDefProvider
{
public override string NetworkType => "pipe";
public static IEnumerable<ExBlockDef> Definitions(string domain) =>
[
ExBlockDef.Create(domain, "mypipe")
.Class<MyPipe>()
.VariantGroup("type", "straight")
.VariantGroup("orientation", "ns", "we", "ud"),
];
}Only NetworkType is required. AllowedOrientations is virtual with a definitions-derived
default (ExDefinitions.OrientationMap), so overriding it duplicates the variant groups and is worth
doing only for a block whose orientation is not a type x orientation pair. Its companion
GetFallbackOrientation is protected virtual - override it protected, not public, or the
compiler rejects the widening (CS0507).
BlockNetworkNode does the heavy lifting: at placement it computes the best orientation from
surrounding network blocks (TryPlaceBlock), recomputes on neighbour change
(OnNeighbourBlockChange), supplies rotated collision/selection boxes, drops a canonical
(fallback-orientation) item, and supports wrench rotation (IWrenchOrientable).
Orientation is the concatenation of single-character face codes the node connects on -
"ns" (north+south), "we", "ud" (up+down), "nswe" (all four horizontals). The variant
group that drives placement is orientation (singular); a mis-named variant group silently
breaks placement and wrenching, so keep it exactly that.
public abstract string NetworkType { get; } // the only member you must write
public virtual Dictionary<string, string[]> AllowedOrientations { get; } // derived from your defs
protected virtual string GetFallbackOrientation(string? type); // first state, else "ns"
public virtual bool IsNetworkEndPoint { get; } // fixed endpoint, excluded from neighbour discovery
protected virtual bool IsFullCube { get; } // full-cell blocks cycle the full topology on wrench
protected virtual bool CanWrenchRotate(IWorldAccessor world, BlockPos pos);
public virtual bool HasConnectorAt(BlockFacing face); // true if Orientation contains the face code
public virtual BlockFacing[]? GetConnectorFaces(); // all connector faces, or null if unorientated
public virtual bool IsValidNonNetworkConnection(Block neighborBlock, BlockFacing face); // suppress false leaks
protected virtual void GetRotations(string orientation, out float rotX, out float rotY, out float rotZ);
public virtual void RecalculateAndSyncOrientations(IWorldAccessor world, BlockPos pos);Override GetRotations only for custom orientation alphabets; override
IsValidNonNetworkConnection to tell GetOpenConnectorFaces that an adjacent non-network block (a
machine housing, say) seals the face rather than leaving it open. Note that nothing in the suite
overrides it: an open face only becomes a leak when the neighbour is air, so a face against a solid
block is already quiet. Overriding it changes which faces are open for every consumer of that set,
not just the leak count.
[BlockEntityRegister]
public class BlockEntityPipe : BlockEntityNetworkNode
{
public override string NetworkType { get; set; } = "pipe";
}BlockEntityNetworkNode registers the node in Initialize, unregisters in OnBlockRemoved,
and persists networkType / orientation / possibleOrientations / network state. You usually
override only the state-serialization hooks and the dynamic-connectivity hook:
public override string NetworkType { get; set; }
public virtual bool HasConnectorAt(BlockFacing face);
public virtual void OnNetworkUpdate(object? state); // network pushes its latest state here (client display)
public virtual bool IsConnectionBroken(); // return true to dynamically sever the graph here
public virtual void OnOpenConnectorsChanged(BlockFacing[] openFaces); // open faces (leaks) changed
// State persistence hooks - override these to round-trip your typed network state:
protected virtual bool IsNetworkStateMeaningful(object? state);
protected virtual object? DeserializeNetworkState(ITreeAttribute tree);
protected virtual void SerializeNetworkState(ITreeAttribute tree, object? state);Dynamic severing. A node that can cut the network (a closed valve) overrides
IsConnectionBroken()to returntruewhile closed. The graph re-walks on every state change, so toggling it merges or fractures the network live. Restore the broken/closed flag inFromTreeAttributesbeforeInitializeruns soAddNodesees the right state.
Your network subclass owns the gameplay simulation. The manager calls these:
public abstract string NetworkType { get; }
public HashSet<BlockPos> Nodes { get; } // every position in this network
public BlockPos? RootPos { get; set; } // for root-anchored networks; else null
public object? State { get; protected set; } // your typed state object
public virtual void RestoreState(object? state); // injected on world load - cast to your type
public void BroadcastUpdate(IBlockAccessor world); // push State to every node's OnNetworkUpdate
public virtual bool CanMerge(BlockNetwork other, IBlockAccessor world); // veto a merge (default: allow)
public abstract void OnMerge(BlockNetwork other, IBlockAccessor world); // combine state on merge
public abstract void OnSplitFragment(BlockNetwork original, IBlockAccessor world); // split state after fracture
public abstract void OnTick(IBlockAccessor world, float dt, BlockNetworkModSystem manager); // per-tick sim
public virtual void InheritStateFrom(BlockNetwork source); // preserve state across rebuilds
public virtual void OnTopologyChanged(); // Nodes changed - drop caches here
protected virtual void OnBeforeBroadcast(IBlockAccessor world); // update derived state before broadcast
protected virtual object? GetStatePayload(); // the object actually sent in a broadcastThe contract you must satisfy when state is conserved (fluid, gas, charge):
-
OnMerge- foldother's state into this network (sum volumes, average temperature...). -
OnSplitFragment- when a fracture produces a new fragment, distribute the original's state into it (usually proportional to node count). Clamp to physical ceilings: over-pressure is legitimate up to a burst limit, so cap at the burst ceiling, not at nominal capacity, or every re-walk dumps the run. -
OnTopologyChanged- invalidate any cached aggregates after the node set changes.
public IServerWorldAccessor? ServerWorld { get; } // non-null on server during tick
public IEnumerable<BlockNetwork> AllNetworks { get; } // every live network (server-side)
public void RegisterNetworkType(string networkType, Func<BlockNetwork> factory);
public BlockNetwork? GetNetworkAt(BlockPos pos);
public BlockNetwork? GetConnectedNetworkAcross(IBlockAccessor world, BlockPos connectorPos, BlockFacing connectorFace);
public virtual void AddNode(IBlockAccessor world, BlockPos pos, string networkType, bool broadcast = true);
public virtual void RemoveNode(IBlockAccessor world, BlockPos pos, bool broadcast = true);
public BlockNetwork? RebuildFromRoot(IBlockAccessor world, BlockPos rootPos, string networkType, bool broadcast = true);
public BlockFacing[] GetOpenConnectorFaces(IBlockAccessor world, BlockPos pos, INetworkMember member);
public IEnumerable<BlockPos> GetConnectedNeighbors(IBlockAccessor world, BlockPos pos, string networkType);
public static BlockFacing? SideToFace(string? side);
public static bool IsCompatibleNetworkBlock(Block neighbour, string id);
public static bool IsCompatibleNetworkBlockAt(IBlockAccessor world, BlockPos pos, Block neighbour, string id);BlockEntityNetworkNode calls AddNode/RemoveNode for you. A block entity that is not a
BlockEntityNetworkNode (e.g. a multiblock structure that also acts as a node) must call
AddNode/RemoveNode itself - only the dedicated base does it automatically.
Two interfaces, two roles:
public interface INetworkNode // implemented by the block ENTITY (a graph node)
{
string? Orientation { get; }
string[] PossibleOrientations { get; }
string NetworkType { get; }
bool HasConnectorAt(BlockFacing face);
void OnOpenConnectorsChanged(BlockFacing[] openFaces);
void OnLeak(BlockFacing[] leakingFaces, bool isLiquid, float intensity); // leak feedback; default no-op
void OnNetworkUpdate(object? state);
}
public interface INetworkConnector // implemented by the BLOCK (anything a pipe can touch)
{
string NetworkType { get; }
bool HasConnectorAt(BlockFacing face);
string NetworkTypeAt(IBlockAccessor world, BlockPos pos);
bool HasConnectorAt(IBlockAccessor world, BlockPos pos, BlockFacing face);
}A node is a full graph participant. A connector is anything a pipe will connect to,
including fixed machine ports that are not nodes - a boiler's steam outlet, an engine's intake.
Such ports read/write the network in the cell on the far side of their connector face; see
Production Machines for the MachinePorts helpers that do exactly that.
NetworkHighlightModSystem renders every live network in its own transparent colour, toggled
per player with .exmod network hi / .exmod network unhi. The graph is server-only, so this is
a client->server request plus server-side HighlightBlocks, polled every 250 ms for live updates.
See Commands.
- Production Machines - fixed machines that tap a network through a port.
- Multiblock Structures - big machines that are also nodes.
-
Testing Harness -
StubNetwork/TestNetworkBlockexercise the graph headlessly.
Expanded Library · framework mod for Vintage Story
exlib - Blocks
- Block Entities
- Block Networks
- Multiblock Structures
- Production Machines
- Construction (RCC)
- Migrations & Healing
exlib - Extending our mods
exlib - Registration
- Registries
- Modules
- Code-First Definitions
- Config System
- Commands
- Recipe Costs
- Source Generators
- Checks
exlib - Utilities
exlib.testing