Table of Contents

Ports and Notches

How a platform connects to the rest of the factory, and how to work out what crosses a boundary. This page is about the real simulation graph; the prediction graph has its own parallel set of types, covered in Read what shapes will flow where.

A platform's ports are buildings

The first thing to get straight, because it shapes everything else: an ordinary buildable platform declares no island connectors at all. The BiggerPlatforms sample builds platforms of every size and passes an empty array:

return new IslandConnectorData(
    Array.Empty<EntityIO<LocalChunkPivot, IIslandConnector>>(),
    chunkLayout.ChunkPositions);

A normal platform docks to its neighbours by notches, and its ports are belt-port and fluid-port buildings that the player places on it. IIslandConnector is for special islands that swallow a space belt or pipe directly — the SandboxIslands sample's FluidTrash is one.

Two consequences worth internalising:

  • Whether a port is an input or an output is not a property of the platform. It is which port building sits at that tile — sender or receiver.
  • If you are adding a platform that needs ports, you do not need a family of definitions for every port arrangement. One definition plus the game's own port buildings covers it.

Notches

A notch is one side of one chunk. NotchDefinition fixes its geometry:

private static int NotchPosX = 17;
private static readonly ChunkTileCoordinate[] EastNotchTilesL = new ChunkTileCoordinate[4] { ... };
public static readonly int NotchTileCount = EastNotchTilesL.Length;   // 4

Four tiles across, and GetNotchLocationOnChunk_L(direction, index, short layer) repeats them on each building layer — so one notch carries up to twelve ports (4 positions × 3 layers).

A platform of w × h chunks has 2(w + h) outward-facing chunk sides, so notch capacity grows twice as fast as area. A 1×5 strip offers twelve notches for five chunks; a 3×3 offers the same twelve for nine.

To place a port's tile within a notch, the game gives you two helpers:

// Local: which of the four positions, given a direction. Ignores height.
NotchDefinition.TryGetIndexOfNotchLocation_L(tile_L, direction, out int index);

// Global: resolves the chunk and direction itself from an island definition.
NotchDefinition.TryGetIndexOfNotchLocation(pivot, islandTransform, islandDefinition, out int index);

The local form is the useful one if you also want to know which side: convert the tile to chunk-local coordinates and try all four directions — the one that matches is the side the port faces.

GlobalChunkCoordinate chunk = tile.ToChunkCoordinate();
GlobalChunkTransform unrotated = new GlobalChunkTransform(chunk, GridRotation.NoRotate);
ChunkTileCoordinate local = tile.ToChunkCoordinate(in unrotated);
Tip

If you are grouping a boundary for any purpose — describing it, reproducing it, comparing two of them — the notch is a far better unit than the individual port. A notch is what one connection to a neighbour occupies, so grouping by notch matches how the player thinks about their factory, and it avoids inventing which of four slots a belt "should" land in.

GridRotation has no None; the identity is GridRotation.NoRotate.

What kind of simulation a port is

There is no shared interface over the port simulations, so identifying one means naming every shape the game can leave a port in. All of these are boundary ports:

Simulation Situation Direction
SpaceBeltPortReceiverSimulation space belt in input
SpaceBeltPortSenderSimulation space belt out output
SpaceFluidPortReceiverSimulation / SpaceFluidPortSenderSimulation space pipe in / out
BeltPortReceiverDisabledSimulation receiver with no sender opposite input, unconnected
BeltPortSenderBlockedSimulation sender with no receiver opposite output, unconnected
FluidPortReceiverDisabledSimulation / FluidPortBlockedSimulation same, for fluid in / out, unconnected
BeltPortTransferSimulation two docked platforms, belt spans both
FluidPortTransferSimulation two docked platforms, fluid spans both

The blocked and disabled variants are the ones people miss. BeltPortSystem creates them through CreateBlockedSenderSimulation and CreateOnlyReceiverSimulation when a port building has no counterpart — the game still simulates what such a port would carry, and they are boundary ports by definition.

Their lanes are public and unterminated, which makes them the natural handles for a detached simulation:

public readonly BeltLane InputLane;    // BeltPortSenderBlockedSimulation - nothing consumes it
public readonly BeltLane OutputLane;   // BeltPortReceiverDisabledSimulation - nothing fills it

Do not use the connection graph to decide "external"

This is the trap, and it is a convincing one.

The obvious way to ask whether a port leaves a region is: does anything it connects to lie outside? ISimulator.FindAllConnectedSimulations will happily answer.

For a space port the answer is always no. Its graph neighbours are all on its own platform, because the hop across space is carried by a separate path simulation two tiles out — so the most external thing on the platform reports as internal, and every space connection silently disappears from your results.

Classify structurally instead, from the table above. A space port reaches across space by construction; a blocked port has nothing opposite; only a transfer simulation needs a question asked of it.

Transfer simulations occupy two chunks

ConnectablePort wraps the transfer simulations, and it spans both platforms:

public int NumOccupiedChunks => Input.Pivot...ToChunkCoordinate()
    == Output.Pivot...ToChunkCoordinate() ? 1 : 2;

public GlobalChunkCoordinate GetOccupiedChunk(int index)
    => index != 0 ? Output.Pivot.Position.ToChunkCoordinate()
                  : Input.Pivot.Position.ToChunkCoordinate();

So index 0 is the sending side and index 1 the receiving side. Two things follow:

  • Checking only GetOccupiedChunk(0) makes every inbound docked port invisible, because an inbound port is anchored on the platform outside your region.
  • Which end you own tells you the direction: own the sender, and items leave you; own the receiver, and they arrive. Own both, and it is an internal connection you can hide.

Establish ownership before classifying

map.Simulator.Simulations holds every port on the map. A classifier that names port types without first checking that the port belongs to the islands you care about will cheerfully report the whole map's boundary — in one real case, 384 ports across 36 notches for a platform one chunk in size.

Check every occupied chunk, not just the first, for the reason above:

private static bool Touches(ILocalizedSimulation localized, IMapModel map, HashSet<IslandId> selected)
{
    for (int i = 0; i < localized.NumOccupiedChunks; i++)
    {
        if (map.TryGetIsland(localized.GetOccupiedChunk(i), out IslandModel owner)
            && selected.Contains(owner.Id))
        {
            return true;
        }
    }

    return false;
}

Tile height is absolute

A port's tile z is measured from the bottom of the map, not the bottom of its platform. A platform on island layer 1 reports tiles at z 20, 21, 22 for what the player sees as building layers 0, 1 and 2.

CoordinateConstants.TilesPerIslandLayer is 20, and the game itself reduces with FastMath.SafeMod(pivot.Position.z, 20) inside NotchDefinition. Do the same before comparing a layer to anything.

See Coordinate Systems for the wider set of conversions.

Note

Derived from the game's own port systems and from a mod that classifies boundaries, not from an official sample. The type names are stable enough to grep for, but verify against your game version.