Modules

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.

  1. 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 table
  • hasKey(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 table
  • getNumKeysInTable(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")],
    },
  ],
});