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).
// 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.
// 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
);