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.21;
 
/* Autogenerated file. Do not edit manually. */
 
import { Counter, CounterTableId } from "./tables/Counter.sol";

In this case there is only one table, Counter.

Counter.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
/* Autogenerated file. Do not edit manually. */
 
// Import schema type
import { SchemaType } from "@latticexyz/schema-type/src/solidity/SchemaType.sol";
 
// 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, FieldLayoutLib } from "@latticexyz/store/src/FieldLayout.sol";
import { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol";
import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { RESOURCE_TABLE, RESOURCE_OFFCHAIN_TABLE } from "@latticexyz/store/src/storeResourceTypes.sol";

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

ResourceId constant _tableId = ResourceId.wrap(
  bytes32(abi.encodePacked(RESOURCE_TABLE, bytes14(""), bytes16("Counter")))
);
ResourceId constant CounterTableId = _tableId;

One advantage of MUD is that the metadata, the information that describes the data we are managing, is available onchain. One of those pieces of metadata is the ResourceId. 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 table's value schema.

BytesFieldValue here
31-30Total length of static1 fields4 bytes
29Number of static data fields1 static field
28Number of dynamic2 fieldsNo dynamic fields
27Length of first static field4 bytes (uint32)
26Length of second static field (if there is one)0x00, no such field
...
0Length 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.

library Counter {
  /**
   * @notice Get the table values' field layout.
   * @return _fieldLayout The field layout for the table.
   */
  function getFieldLayout() internal pure returns (FieldLayout) {
    return _fieldLayout;
  }

Get the field layout (opens in a new tab).

/**
 * @notice Get the table's key schema.
 * @return _keySchema The key schema for the table.
 */
function getKeySchema() internal pure returns (Schema) {
  SchemaType[] memory _keySchema = new SchemaType[](0);
 
  return SchemaLib.encode(_keySchema);
}
 
/**
 * @notice Get the table's value schema.
 * @return _valueSchema The value schema for the table.
 */
function getValueSchema() internal pure returns (Schema) {
  SchemaType[] memory _valueSchema = new SchemaType[](1);
  _valueSchema[0] = SchemaType.UINT32;
 
  return SchemaLib.encode(_valueSchema);
}

These two functions return the schema, the different types of fields, for the value and the key. In this case, there is nothing in the key because the table only has a single row.

The list of field types is available here (opens in a new tab).

/**
 * @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";
}

This function returns the name of the table and the names of the fields in it. In this case there is only one, and as we didn't specify the name in mud.config.ts it is called by the default, value.

  /**
   * @notice Register the table with its config.
   */
  function register() internal {
    StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), g$
  }
 
  /**
   * @notice Register the table with its config.
   */
  function _register() internal {
    StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames());
  }
 
  /**
   * @notice Register the table with its config (using the specified store).
   */
  function register(IStore _store) internal {
    _store.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames());
  }

These functions register the schema, either to the default Store (StoreSwitch) or to one given as a parameter.

/**
 * @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)));
}
 
/**
 * @notice Get value (using the specified store).
 */
function getValue(IStore _store) internal view returns (uint32 value) {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  bytes32 _blob = _store.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 = 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)));
}
 
/**
 * @notice Get value (using the specified store).
 */
function get(IStore _store) internal view returns (uint32 value) {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  bytes32 _blob = _store.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
  return (uint32(bytes4(_blob)));
}

These functions read the 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);
}
 
/**
 * @notice Set value (using the specified store).
 */
function setValue(IStore _store, uint32 value) internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  _store.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}
 
/**
 * @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);
}
 
/**
 * @notice Set value (using the specified store).
 */
function set(IStore _store, uint32 value) internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  _store.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}

These functions overwrite the value.

/**
 * @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);
}
 
/**
 * @notice Delete all data for given keys (using the specified store).
 */
function deleteRecord(IStore _store) internal {
  bytes32[] memory _keyTuple = new bytes32[](0);
 
  _store.deleteRecord(_tableId, _keyTuple);
}

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

/**
 * @notice Tightly pack static (fixed length) data using this table's schema.
 * @return The static data, encoded into a sequence of bytes.
 */
function encodeStatic(uint32 value) internal pure returns (bytes memory) {
  return abi.encodePacked(value);
}
 
/**
 * @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 dyanmic (variable length) data, encoded into a sequence of bytes.
 */
function encode(uint32 value) internal pure returns (bytes memory, PackedCounter, bytes memory) {
  bytes memory _staticData = encodeStatic(value);
 
  PackedCounter _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.0;
 
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.