Systems

Systems: On-Chain Logic

Primodium systems are smart contracts that execute on-chain logic to modify state stored in Primodium tables. Primodium systems are based on systems (opens in a new tab) as defined in the open MUD standard, which are contracts that changes on-chain state called by the central World contract.

All systems are inherited from the PrimodiumSystem contract which provides the addressToEntity() and entityToAddress() functions to convert between Ethereum addresses and their corresponding entity IDs stored in table contracts (opens in a new tab).

/packages/contracts/src/systems/internal/PrimodiumSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
import { System } from "@latticexyz/world/src/System.sol";
 
contract PrimodiumSystem is System {
  function addressToEntity(address a) internal pure returns (bytes32) {
    return bytes32(uint256(uint160((a))));
  }
 
  function entityToAddress(bytes32 a) internal pure returns (address) {
    return address(uint160(uint256((a))));
  }
}
 

Systems

A new system is defined for a group of related on-chain logic. For example, the BuildSystem is responsible for building new buildings in the game, as shown in the following code sample.

/packages/contracts/src/systems/BuildSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
 
// external
import { PrimodiumSystem } from "systems/internal/PrimodiumSystem.sol";
 
// tables
import { P_EnumToPrototype, PositionData } from "codegen/index.sol";
 
// libraries
import { LibBuilding } from "libraries/LibBuilding.sol";
import { IWorld } from "codegen/world/IWorld.sol";
 
// types
import { BuildingKey } from "src/Keys.sol";
import { EBuilding } from "src/Types.sol";
 
/*
  EBuilding is an uint256 that encodes different building types.
 
  PositionData captures the x and y coordinates of a building as well as the asteroid ID, shown below.
 
  struct PositionData {
    int32 x;
    int32 y;
    bytes32 parent;
  }
*/
 
contract BuildSystem is PrimodiumSystem {
  function build(
    EBuilding buildingType,
    PositionData memory coord
  ) public _claimResources(coord.parentEntity) returns (bytes32 buildingEntity) {
 
    bytes32 buildingPrototype = P_EnumToPrototype.get(BuildingKey, uint8(buildingType));
    buildingEntity = LibBuilding.build(_player(), buildingPrototype, coord, false);
  }
}
 

In TypeScript, a system is called using the write$ observable. First, a worldContract object is initialized as follows, where getContract() is defined here (opens in a new tab) in the @latticexyz/mud package.

import { ContractWrite, getContract } from "@latticexyz/common";
import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
 
const publicClient = createPublicClient(clientOptions);
 
const write$ = new Subject<ContractWrite>();
const worldContract = getContract({
  address: worldAddress as Hex,
  abi: IWorldAbi,
  publicClient,
  onWrite: (write) => write$.next(write),
});

Then, a system can be called as follows as a writeContract (opens in a new tab) call in viem (opens in a new tab), with types defined here (opens in a new tab).

worldContract.write.build([building, position], { gas: 7000000n });

Note that most Primodium systems can only be called by the owner of an asteroid, i.e. calling build requires the sender to be the owner of the asteroid at the given position. The exception is if the system is called by a system that has been delegated control of the asteroid by the owner. See Delegation for more information.

Primodium systems can also be called from the CLI with the publicly-available ABIs, which are accessible from the latest version information.

Subsystems

Subsystems in Primodium are created for reusable logic that can be called by multiple systems, prefixed by S_. Primodium subsystems are written to be called by other systems and are not intended to be called directly by the World contract. This is to ensure that the subsystems are only called in the context of a system that has been delegated control of an asteroid by the owner.

Subsystems also allow us to split code into subsystems without exposing functionality to malicious actors.

The following is an example of a system calling the S_BattleSystem subsystem via the SystemCall MUD SystemCallcallWithHooksOrRevert API defined here (opens in a new tab).

bytes memory rawBr = SystemCall.callWithHooksOrRevert(
  DUMMY_ADDRESS,
  getSystemResourceId("S_BattleSystem"),
  abi.encodeCall(S_BattleSystem.battle, (invader, defender, rockEntity, ESendType.Invade)),
  0
);