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).
import { defineWorld } from "@latticexyz/world";
export default defineWorld({
namespace: "app",
tables: {
Counter: {
schema: {
value: "uint32",
},
key: [],
},
},
});
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.
// 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:
Bytes | Field | Value here |
---|---|---|
0-1 | Resource type identifier (opens in a new tab) | tb |
2-15 | Resource's namespace | Root namespace, which is empty |
16-31 | Actual resource name | Counter |
FieldLayout constant _fieldLayout = FieldLayout.wrap(
0x0004010004000000000000000000000000000000000000000000000000000000
);
The field layout (opens in a new tab) encodes the lengths of the various fields.
Bytes | Field | Value here |
---|---|---|
0-1 | Total length of static1 fields | 4 bytes |
2 | Number of static data fields | 1 static field |
3 | Number of dynamic2 fields | No dynamic fields |
4 | Length of first static field | 4 bytes (uint32 ) |
5 | Length of second static field (if there is one) | 0x00, no such field |
... | ||
31 | Length of 28th3 static field | 0x00, 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.