Examples
Create Systems

Create Extension Systems

The commented code can be found in packages/contracts/src/systems/WriteDemoSystem.sol (opens in a new tab).

We want our expansion system to call a system on the Primodium world. If this is successful, we return the buildingEntity.

This system is quite a bit more involved. We'll break it down by function.

Imports

examples/WriteDemo/packages/contracts/src/systems/WriteDemoSystem.sol
import {
  Asteroid,
  Home,
  Level,
  Dimensions,
  DimensionsData,
  PositionData,
  Spawned,
  UsedTiles,
  P_AsteroidData,
  P_Asteroid,
  P_Blueprint,
  P_EnumToPrototype,
  P_MaxLevel,
  P_RequiredTile,
  P_Terrain,
} from "primodium/index.sol";
 
import { EBuilding } from "primodium/common.sol";
 
import { Bounds, EResource } from "src/Types.sol";
import { BuildingKey, ExpansionKey } from "src/Keys.sol";
 
import { FunctionSelectors } from "@latticexyz/world/src/codegen/tables/FunctionSelectors.sol";
Explanation
WriteDemoSystem.sol:14
import {
  Asteroid,
  Home,
  Level,
  Dimensions,
  DimensionsData,
  PositionData,
  Spawned,
  UsedTiles,
  P_AsteroidData,
  P_Asteroid,
  P_Blueprint,
  P_EnumToPrototype,
  P_MaxLevel,
  P_RequiredTile,
  P_Terrain,
} from "primodium/index.sol";

Previously we imported Tables directly, one at a time. However, MUD collects and exposes them in index.sol automatically, so we're going to use that now.

WriteDemoSystem.sol:19
import { EBuilding } from "primodium/common.sol";

Primodium stores lists of entity lookup keys in common.sol as enumerations. Here we can find buildings, resources, units, etc.

WriteDemoSystem.sol:16
import { Bounds, EResource } from "src/Types.sol";
import { BuildingKey, ExpansionKey } from "src/Keys.sol";

We use structs to pass around collections of related data; these can be found in Types.sol. Similarly, we have some internal keys we used look up data, and those can be found in Keys.sol.

WriteDemoSystem.sol:9
import { FunctionSelectors } from "@latticexyz/world/src/codegen/tables/FunctionSelectors.sol";

Normally we won't need this. However, there is a bug (opens in a new tab) in how MUD v2.0.1 handles function selectors when using callFrom. We need to use callFrom to interact with systems that take action for the user, so we're going to use this to workaround the bug until it is resolved in future versions.

Top Level Function

examples/WriteDemo/packages/contracts/src/systems/WriteDemoSystem.sol
  function buildIronMine() public returns (bytes32 buildingEntity) {
 
    StoreSwitch.setStoreAddress(_world());
 
    bytes32 playerEntity = addressToEntity(_msgSender());
 
    bool playerIsSpawned = Spawned.get(playerEntity);
    require(playerIsSpawned, "[WriteDemoSystem] Player is not spawned");
 
    bytes32 asteroidEntity = Home.get(playerEntity);
 
    EBuilding building = EBuilding.IronMine;
 
    PositionData memory position = getTilePosition(asteroidEntity, building);
 
    bytes4 worldFunctionSelector = IPrimodiumWorld(_world()).Primodium__build.selector;
 
    (ResourceId buildSystemId, bytes4 buildSystemFunctionSelector) = FunctionSelectors.get(worldFunctionSelector);
 
    bytes memory result = IPrimodiumWorld(_world()).callFrom(
      _msgSender(),
      buildSystemId,
      abi.encodeWithSelector(
        bytes4(buildSystemFunctionSelector),
        building,
        position.x,
        position.y,
        position.parentEntity
      )
    );
 
    buildingEntity = abi.decode(result, (bytes32));
  }
Explanation
WriteDemoSystem.sol:39
bool playerIsSpawned = Spawned.get(playerEntity);

Spawned is a Primodium Table we can call to check if a player has started playing. You can examine the imported contract, and see what types to expect in return, and any other functionality it provides. Most of the time we will only be using get() and set().

WriteDemoSystem.sol:47
EBuilding building = EBuilding.IronMine;

Enumerations let us use clear names instead of magic numbers to represent data. It makes the code much more readable. Enumerations decode to uint8 in the abi.

WriteDemoSystem.sol:58
PositionData memory position = getTilePosition(asteroidEntity, building);

This function gets pretty complex. We will walk through it in detail later, but at the high level, the map on each asteroid is broken into a tile grid, and this function walks the map and looks for an appropriate tile for the building passed in.

WriteDemoSystem.sol:65
ResourceId buildSystemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, PRIMODIUM_NAMESPACE, "BuildSystem");
bytes memory buildingEntity = IPrimodiumWorld(_world()).callFrom(
    _msgSender(),
    buildSystemId,
    abi.encodeWithSignature("Primodium__build(uint8,(int32,int32,bytes32))", building, (position))
);

This is normally how we would call a System that is going to take action for the user. Find the ResourceId for the system, and execute a callFrom using _msgSender() to identify the user, with the signature of the of the function being called. The target System needs to have received delegation permission from the user in advance. We'll discuss delegation later.

The parameters of the function signature deconstruct the struct into its internal native types, to match the abi. Note the extra () around position to indicate that the struct is a tuple.

WriteDemoSystem.sol:73
bytes4 worldFunctionSelector = IPrimodiumWorld(_world()).Primodium__build.selector;
 
(ResourceId buildSystemId, bytes4 buildSystemFunctionSelector) = FunctionSelectors.get(worldFunctionSelector);
 
bytes memory result = IPrimodiumWorld(_world()).callFrom(
  _msgSender(),
  buildSystemId,
  abi.encodeWithSelector(
    bytes4(buildSystemFunctionSelector),
    building,
    position.x,
    position.y,
    position.parentEntity
  )
);

This is our workaround for the MUD function selector bug. The function selector used by the World is different from the original function selector derived from the function signature.

We start in the same place, and find the original signature. Then we check the FunctionSelectors table to retrieve the actual function selector we need to use in callFrom.

Finally, we execute the callFrom using manually built calldata.

WriteDemoSystem.sol:92
buildingEntity = abi.decode(result, bytes32);

callFrom returns bytes memory, so we convert it to the bytes32 we expect and return.

Enumerations

We need to select the building we want to build. Primodium stores lists of entity lookup keys in common.sol. Here we can find buildings, resources, units, etc. The enums are used to lookup prototypes and blueprints later, in their specific Systems. We're going to build an EBuilding.IronMine for now.

Map Interactions

Each asteroid has a bounded set of tiles where buildings can be placed. We need to find the bounds for this specific map (our Home Base in this case). These bounds change as you increase your expansion level, so we get the level, and save the DimensionData range. From there, we use an P_AsteroidData struct to manage the data, calculate the bounds, and return them.

We step through each tile and check if it is the correct type. To check if it is empty, we use a P_Blueprint and verify the building fits within the bounds. If all of this is successful, we return the tile position.

Calling a Game System

We want our expansion system to call a system on the Primodium world. If all of this is successful, we return the buildingEntity.

Unlike the ReadDemo, the WriteDemo acts on behalf of the user. Our system needs to have permission to do so. This is allowed through a Delegation that needs to occur before the system it called. You can find documentation and example code for delegations at:

As mentioned in the detail, we normally get the system ResourceId, and the use abi.encodeWithSignature to call the function. However, there is currently a bug (opens in a new tab) in MUD, and we need to retrieve the internal function selector from the FunctionSelectors table, and then use that with abi.encodeWithSelector. Example code is provided; we expect the normal usage to return in future MUD versions.