Modules
Modules are on-chain scripts that can be executed on the World. They are used to install tables, systems, hooks, and new entry point to a World in order to extend its capabilities.
Currently the code of each module you want to use need to exist in your project, but we are paving the way to having an on-chain module registry à la NPM.
Default modules
Currently, CoreModule
(opens in a new tab) is installed on each World.
It adds critical tables and systems to the World, which are externalized to keep the World contract size within the limit.
See World internals for table and system details.
RegistrationModule
The RegistrationModule
(opens in a new tab) adds the RegistrationSystem
(opens in a new tab) along with its dependencies to the World. This registration system surfaces the APIs necessary to register Systems, Tables, and Namespaces on-chain without being the World root user. If you do not want to make your World permissionlessly extensible, you can simply not install the RegistrationModule (this is not supported by the CLI right now).
UniqueEntityModule
The UniqueEntityModule
(opens in a new tab) installs a system that always returns a unique bytes32
key. It is designed for tables that have one key (eg: ECS components).
It exposes a single method:
function getUniqueEntity() returns (bytes32 uniqueEntity)
increments and returns an entity nonce
Using getUniqueEntity
to create a new unit:
import { getUniqueEntity } from "@latticexyz/world/src/modules/uniqueentity/getUniqueEntity.sol";
import { Soldier } from "../codegen/tables/Soldier.sol";
...
function spawnSoldier() public {
bytes32 key = getUniqueEntity();
Soldier.setHealth(key, 100);
}
Querying modules
The KeysInTableModule
and KeysWithValueModule
modules index information about tables on-chain. Their functionality is leveraged in SnapSyncModule
and query
to allow on-chain querying.
KeysInTableModule
The KeysInTableModule
(opens in a new tab) allows for querying for all keys that are in a given table. It can only be used on tables that have one key (eg: ECS components).
For example, it can be used to fetch all keys in the "Position" table (if the table models key → Position
).
It offers two methods:
getKeysInTable(bytes32 tableId)
returns all keys in a tablehasKey(bytes32 tableId, bytes32[] memory key)
returns whether a given key is in a table (with O(1) gas)
Using getKeysInTable
to retrieve all keys in the Player table:
// assumes a boolean flag indicates a player:
// { Player: "bool" }
//
import { getKeysInTable } from "@latticexyz/world/src/modules/keysintable/getKeysInTable.sol";
import { Player, PlayerTableId } from "../codegen/tables/Player.sol";
// get all players
bytes32[][] memory players = getKeysInTable(world, PlayerTableId);
Using hasKey
to check if a key is in the Player table:
// assumes a boolean flag indicates a player:
// { Player: "bool" }
//
import { hasKey } from "@latticexyz/world/src/modules/keysintable/hasKey.sol";
import { Player, PlayerTableId } from "../codegen/tables/Player.sol";
bytes32[] memory keyTuple = new bytes32[](1);
keyTuple[0] = "bob";
// check if key "bob" is a player
bool memory isPlayer = hasKey(world, PlayerTableId, keyTuple);
Internally, it works by installing a hook (opens in a new tab) that maintains:
- an array of all keys in the table
- an index or "reverse mapping" of whether a given key is in the table
KeysWithValueModule
The KeysWithValueModule
(opens in a new tab) allows for querying for all keys that have a given value in a table. It can only be used on tables that have one key (eg: ECS components).
As an example, this can be used to ask for all NFTs owned by a specific user (if the table models NFT ID → Owner
), or all units on a specific position (if the ECS component is modeled as entity → Position
)
It offers a single method:
getKeysWithValue(uint256 tableId, bytes memory value)
returns all keys with the given value in a table
Using getKeysWithValue
to retrieve all NFTs owned by a specific address:
// assumes this ownership table:
// Owners: {
// schema: "address",
// keySchema: { nftId: "uint256" }
// }
import { getKeysWithValue } from "@latticexyz/world/src/modules/keyswithvalue/getKeysWithValue.sol";
import { Owners, OwnersId } from "../codegen/tables/Owners.sol";
// get all nfts (as bytes, need to convert to uint256) owned by address 0x42
bytes32[] memory keysWithValue = getKeysWithValue(world, OwnersId, Owners.encode(address(42)));
Internally, it works by installing a hook that maintains an array of all keys in the table.
SnapSyncModule
The SnapSyncModule
(opens in a new tab) installs a System that returns all records in a given table, with offset-based pagination. It requires the KeysInTable
module to be installed on each table, but the SnapSync
plugin does this automatically.
SnapSyncSystem
exposes two public view
functions:
getRecords(bytes32 tableId, uint256 limit, uint256 offset)
returns all keys in a tablegetNumKeysInTable(bytes32 tableId)
returns the number of keys in a table
Clients can use snap-sync to get all records on the World, then begin syncing regularly from the current block:
import { getSnapSyncRecords } from "@latticexyz/network";
import { getTableIds } from "@latticexyz/utils";
...
if (networkConfig.snapSync) {
const currentBlockNumber = await provider.getBlockNumber();
const tableRecords = await getSnapSyncRecords(
networkConfig.worldAddress,
getTableIds(storeConfig),
currentBlockNumber,
signerOrProvider
);
result.startSync(tableRecords, currentBlockNumber);
} else {
result.startSync();
}
query
query
provides a simple API to get a list of keys matching certain specified criteria. It is not a standalone module, but requires KeysInTable
and KeysWithValue
to be installed.
A query consists of a list of query fragments. A query fragment is a struct with three fields:
struct QueryFragment {
QueryType queryType;
bytes32 tableId;
bytes value;
}
Available query fragments are:
Has
: filter for keys that are in the specified table. The value field in the query fragment is ignored.HasValue
: filter for keys that are in the specified table with the specified value.Not
: filter for keys that are not in the specified table. The value field in th query fragment is ignored.NotValue
: filter for keys that are not in the specified table with the specified value.
The query fragments are executed from left to right and are concatenated with a logical AND. For performance reasons, the most restrictive query fragment should be first in the list of query fragments, in order to reduce the number of keys the next query fragment needs to be checked for.
Example: Query for all keys in the movable table at position (0,0):
import { query, QueryFragment, QueryType } from "@latticexyz/world/src/modules/keysintable/query.sol";
QueryFragment[] memory fragments = new QueryFragment[](2);
// Specify the more restrictive filter first for performance reasons
fragments[0] = QueryFragment(QueryType.HasValue, positionTableId, abi.encode(Coord(0,0)));
// The value argument is ignored in Has query fragments
fragments[1] = QueryFragment(QueryType.Has, movableTableId, new bytes(0));
bytes32[][] memory keyTuples = query(fragments);
Installing a module
To install a module, create an entry in the modules
field of your config, and specify whether it should be a root module with the root
key. The arguments sent to the install
functions are listed in the args
key, and you can use dynamic functions that resolve to resources in the World (currently only resolveTableId
is available).
import { mudConfig } from "@latticexyz/world/register";
import { resolveTableId } from "@latticexyz/config";
export default mudConfig({
tables: {
MyTable: {
schema: "uint32",
},
},
modules: [
{
name: "KeysWithValueModule",
root: true,
args: [resolveTableId("MyTable")],
},
],
});