Templates
TypeScript
Contracts

Contracts (Vanilla and React-ECS)

This section explains the contracts for the vanilla and react-ecs templates. The Three.JS and React templates use different contracts.

The onchain components can be divided into two types of functionality:

  • Tables, storing the data of the application.
  • Systems, business logic that can be called to read or modify data in the tables.

Tables

The table schema

mud.config.ts

The table schema is declared in packages/contracts/mud.config.ts. Read more details about the schema definition here.

The table schema provided in the example is extremely simple (one singleton).

mud.config.ts
import { mudConfig } from "@latticexyz/world/register";
 
export default mudConfig({
  tables: {
    Counter: {
      keySchema: {},
      valueSchema: "uint32",
    },
  },
});

There are two automatically generated files related to the tables:

  • packages/contracts/src/codegen/index.sol
  • packages/contracts/src/codegen/tables/Counter.sol
The automatically generated table files

index.sol

This file just imports all of the automatically generated tables and their identifiers.

index.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
 
/* Autogenerated file. Do not edit manually. */
 
import { Counter } from "./tables/Counter.sol";

In this case there is only one table, Counter.

Counter.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
 
/* Autogenerated file. Do not edit manually. */
 
// Import store internals
import { IStore } from "@latticexyz/store/src/IStore.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { StoreCore } from "@latticexyz/store/src/StoreCore.sol";
import { Bytes } from "@latticexyz/store/src/Bytes.sol";
import { Memory } from "@latticexyz/store/src/Memory.sol";
import { SliceLib } from "@latticexyz/store/src/Slice.sol";
import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol";
import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol";
import { Schema } from "@latticexyz/store/src/Schema.sol";
import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";

These are various definitions required for a MUD table. You don't typically need to worry about them.

library Counter {

This library contains all the definitions necessary to use the table.

// Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "Counter", typeId: RESOURCE_TABLE });`
ResourceId constant _tableId = ResourceId.wrap(0x74620000000000000000000000000000436f756e746572000000000000000000);

The <table name>._tableId value is the ResourceId, the identifier for the table in the World. It is composed of three fields:

BytesFieldValue here
0-1Resource type identifier (opens in a new tab)tb
2-15Resource's namespaceRoot namespace, which is empty
16-31Actual resource nameCounter
FieldLayout constant _fieldLayout = FieldLayout.wrap(
  0x0004010004000000000000000000000000000000000000000000000000000000
);

The field layout (opens in a new tab) encodes the lengths of the various fields.

BytesFieldValue here
0-1Total length of static1 fields4 bytes
2Number of static data fields1 static field
3Number of dynamic2 fieldsNo dynamic fields
4Length of first static field4 bytes (uint32)
5Length of second static field (if there is one)0x00, no such field
...
31Length of 28th3 static field0x00, no such field

(1) In this context "static" means fixed length. For example, uint8, int16, and bool are all static fields.

(2) In this context "dynamic" means variable length. For example, bytes, string, and uint8[] are all dynamic fields.

(3) A MUD table can have up to 28 static fields.

// Hex-encoded key schema of ()
Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000);
 
// Hex-encoded value schema of (uint32)
Schema constant _valueSchema = Schema.wrap(0x0004010003000000000000000000000000000000000000000000000000000000);

The key schema and the value schema for the table. In this case, the key schema has no fields because it is a singleton, with just one record. The value schema includes a single static field. The exact schema encoding is explained under the store docs.

/**
 * @notice Get the table's key field names.
 * @return keyNames An array of strings with the names of key fields.
 */
function getKeyNames() internal pure returns (string[] memory keyNames) {
  keyNames = new string[](0);
}

Get the field names for the key, an empty array in the case of a singleton.

/**
 * @notice Get the table's value field names.
 * @return fieldNames An array of strings with the names of value fields.
 */
function getFieldNames() internal pure returns (string[] memory fieldNames) {
  fieldNames = new string[](1);
  fieldNames[0] = "value";
}

Get the field names for the value. In this case there is only one, value, the current value of the counter.

/**
 * @notice Register the table with its config.
 */
function register() internal {
  StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames());
}
 
/**
 * @notice Register the table with its config.
 */
function _register() internal {
  StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames());
}

These functions register the schema. The _register() function is used when running inside the context of the World, for example in a root namespace System. The register() function is used when running in any other context, for example from a Solidity script or a normal System. Note that you can use register() when running in the context of the World, it is just slightly less efficient than _register()

The same distinction between <function>(), which is usable everything, and _<function>() which can only be used in the World context, exists in most other table functions.

/**
 * @notice Get value.
 */
function getValue() internal view returns (uint32 value) {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
  return (uint32(bytes4(_blob)));
}
 
/**
 * @notice Get value.
 */
function _getValue() internal view returns (uint32 value) {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
  return (uint32(bytes4(_blob)));
}

These functions return the value field. The _keyTuple is empty, because the table is a singleton.

If there are more fields in the value schema, they each have get<field name>(<key>) functions.

/**
 * @notice Get value.
 */
function get() internal view returns (uint32 value) {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
  return (uint32(bytes4(_blob)));
}
 
/**
 * @notice Get value.
 */
function _get() internal view returns (uint32 value) {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
  return (uint32(bytes4(_blob)));
}

These functions return the entire value, which just happens to have a single field called value. In this case there is only one value and there are no keys, so they just get the first entry, the one with index zero.

/**
 * @notice Set value.
 */
function setValue(uint32 value) internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}
 
/**
 * @notice Set value.
 */
function _setValue(uint32 value) internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}

Set the value field. Again, there is one function pair for each value field.

/**
 * @notice Set value.
 */
function set(uint32 value) internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}
 
/**
 * @notice Set value.
 */
function _set(uint32 value) internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}

Set all the value fields.

/**
 * @notice Delete all data for given keys.
 */
function deleteRecord() internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  StoreSwitch.deleteRecord(_tableId, _keyTuple);
}
 
/**
 * @notice Delete all data for given keys.
 */
function _deleteRecord() internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout);
}

These functions delete the value. Normally it would be the value associated with the a key provided as a parameter, but this is a singleton.

  /**
   * @notice Encode all of a record's fields.
   * @return The static (fixed length) data, encoded into a sequence of bytes.
   * @return The lengths of the dynamic fields (packed into a single bytes32 value).
   * @return The dynamic (variable length) data, encoded into a sequence of bytes.
   */
  function encode(uint32 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) {
    bytes memory _staticData = encodeStatic(value);
 
    EncodedLengths _encodedLengths;
    bytes memory _dynamicData;
 
    return (_staticData, _encodedLengths, _dynamicData);
  }
 
  /**
   * @notice Encode keys as a bytes32 array using this table's field layout.
   */
  function encodeKeyTuple() internal pure returns (bytes32[] memory) {
    bytes32[] memory _keyTuple = new bytes32[](0);
 
    return _keyTuple;
  }
}

Utility functions to encode a value.

Systems

The way MUD works, onchain logic is implemented by one or more System contracts. Those systems are always called by a central World contract.

IncrementSystem.sol

This is the system that is provided by the demo (packages/contracts/src/systems/IncrementSystem.sol). As the name suggests, it includes a single function that increments Counter.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
 
import { System } from "@latticexyz/world/src/System.sol";
import { Counter } from "../codegen/Tables.sol";

The system needs to know how to be a System, as well as have access to the table (or tables) it needs.

contract IncrementSystem is System {
  function increment() public returns (uint32) {

There could be multiple functions in the same system, but in this case there is only one, increment.

    uint32 counter = Counter.get();

Read the value. Because Counter is a singleton, there are no keys to look up.

    uint32 newValue = counter + 1;
    Counter.set(newValue);

Update the value.

    return newValue;
  }
}

Return the new value.