MUD client

Interacting with MUD from a client

MUD is client agnostic: any client -- a browser, a game engine, or an iOS app -- can implement the synchronization protocol and a client-side cache to replicate Store tables, along with the necessary infrastructure to send transactions to the World.

However, currently we only officially support the browser and Node. We provide tooling for representing the onchain state and replicate it from an RPC or MODE via a network stack.

There also exists tooling for Unity (opens in a new tab) from the folks at emergence.land (opens in a new tab).

Representing state in the browser

When using MUD in the browser, we provide two different ways to query the Store and react to changes:

  1. recs: recs is a client-side store optimize for ECS modelling. If you model your MUD data using ECS, this is a great choice.
  2. store-cache: store-cache is reactive tuple-database. It has a more powerful query system than recs, and support multiple keys. If don't use the ECS data model (eg: you have multi-key tables, like one-to-many relationship), we recommend using store-cache.

Note: when using the default MUD templates (created with pnpm create mud@canary), the Store data will be available in both client-side stores. You are free to query with the one that makes the most sense.

Over time, we will migrate to store-cache as the default and surface an opinionated layer for ECS data on top.

recs: the reactive ECS database

recs has been designed to represent and query ECS data. If your MUD tables are set up with a single bytes32 key (see the ECS modelling guide), and you plan to represent an entity and its associated components as the same bytes32 key on multiple tables, then recs will let you query your data in a natural way.

To read data and react to changes with recs, there exists two APIs:

  1. Reading component value directly
  2. Subscribing to a query that returns a set of matching entities

For the examples below, let's assume this MUD config. Notice that each of these tables have an (implicit) bytes32 key; which is recommended when modelling your MUD data using ECS.

const config = mudConfig({
  tables: {
    NameComponent: "string",
    PlayerComponent: "bool",
    PositionComponent: { schema: { x: "int32", y: "int32" } },
  },
});

Reading component value directly

If you have a specific bytes32 key and a reference to a component, you can query its value directly, or use a React hook that will re-render when the corresponding component value updates.

Reading component value in vanilla javascript

import { getComponentValueStrict } from "@latticexyz/recs";
// note: NameComponent is an recs component; components could come from the `setup` function of MUD.
const { NameComponent } = components
// get a reference to the recs world; as an example from the `setup` function of MUD.
const world = [...]
// you need a reference to an `Entity` from recs. For the sake of the example, let's say we know that the bytes32 key is "0xDEAD"
// you wouldn't do normally: entities would be found via queries.
const entityID = "0xDEAD" as EntityID;
const entity = world.registerEntity({ id: "0xDEAD" as EntityI })
// now we can fetch the value of the NameComponent on our entity
const name = getComponentValueStrict(NameComponent, entity)
// -> "John Doe"

Reading component value in react with @latticexyz/react

import { useComponentValue } from "@latticexyz/react";
function ExampleComponent() {
  const { components, world } = useMUD;
  const { NameComponent } = components;
  // you need a reference to an `Entity` from recs. For the sake of the example, let's say we know that the bytes32 key is "0xDEAD"
  // you wouldn't do this normally: entities would be found via queries.
  const entityID = "0xDEAD" as EntityID;
  const entity = world.registerEntity({ id: "0xDEAD" });
  // now we can fetch the value of the NameComponent on our entity
  const name = useComponentValue(NameComponent, entity);
  // -> "John Doe"
  // this will re-render when the component value changes!
  return <p>{name ?? "<no name>"}</p>;
}

Running queries

A common way of working with ECS data is by using queries. A query is a function that returns a list of entities that match a set of predicates called query fragments.

There are 4 types of query fragments:

  1. Has: matches entities that have a specific component.
  2. Not: matches entities that do not have a specific component.
  3. HasValue: matches entities that have a specific component with a set value.
  4. NotValue: matches entities that do not have a component with a set value (will also match if it doesn't have the component at all)

Running query with vanilla javascript

import { runQuery, Has, HasValue, getComponentValueStrict } from "@latticexyz/recs";
const { PlayerComponent, PositionComponent, NameComponent } = components
// query for all named players at the center of the universe
const matchingEntities = runQuery([
  Has(PlayerComponent),
  Has(Name),
  HasValue(PositionCompoennt, {x: 0, y: 0})
])
// now you can map these to their name as an example
const names = matchingEntities.map(
  playerEntity => getComponentValueStrict(NameComponent, playerEntity
)
// -> ["Bob", "Alice", "Eve"]

Reacting to queries with vanilla javascript

import { defineSystem, Has, HasValue, getComponentValueStrict, UpdateType } from "@latticexyz/recs";
const { PlayerComponent, PositionComponent, NameComponent } = components
// get a reference to the recs world; as an example from the `setup` function of MUD.
const world = [...]
defineSystem(world, [Has(PlayerComponent), Has(Name), HasValue(PositionComponent, {x: 0, y: 0}], ({entity, component, value, type}) => {
  // every time an entity enter, exit, or get updated within a query; this callback with fire
  // the "type" will match these: UpdateType.Enter, UpdateType.Exit, UpdateType.Update
  // as an example, if a new entity is named, has a player component, and is at the center of the universe; the callback fires
  // if the name of a player at the center of the universe changes, the callback will also fire for that entity
  // let's only log when a new entity matches this query
  // every time a named player reaches {0, 0}; we want to log their name
  if(type !== UpdateType.Enter) return
  console.log(getComponentValueStrict(NameComponent, entity) + " reached the center!")
})

Reacting to queries with React and @latticexyz/react

import { useEntityQuery } from "@latticexyz/react";
import { Has, HasValue, getComponentValueStrict } from "@latticexyz/recs";
function ExampleComponent() {
  const { components, world } = useMUD;
  const { NameComponent, PlayerComponent, PositionCompoennt } = components
  // get a list of all entities that are named, players, and at the center of the universe
  // it is reactive and will trigger a re-render when the set of matching queries update
  const entities = useEntityQuery([Has(PlayerComponent), Has(Name), HasValue(PositionComponent, {x: 0, y: 0}])
  // [entity1, entity2, ...]
  return(
    <div>
      <span>Players at the center:</span>
      <ul>
      {entities.map(entity => (
        <li>{getComponentValueStrict(NameComponent, entity)}</li>
      ))}
      </ul>
    </div>
  )
}

store-cache: a general tuple database

store-cache is a typed reactive database used to replicate the onchain Store of a MUD project into a Javascript client (Node or Browser based).

Unlike recs, store-cache supports multiple keys. It also comes with a more powerful query system. Currently, ECS-like queries where the same primary key is assumed to be same entity (like what recs does) are quite verbose. We are working on making this better. Meanwhile, please make these queries with recs.

store-cache surfaces up three APIs:

  1. Fetching a specific record from a table when the keys are known
  2. Scanning a table with a query
  3. Subscribing to a query

The store-cache package also exposes React hooks for each of these use cases.

Let's assume this MUD config for the examples below. Notice that some tables have multiple keys in their keySchema. We recommend using store-cache when querying these tables.

const config = mudConfig({
  tables: {
    MultiKey: { keySchema: { first: "bytes32", second: "uint32" }, schema: { value: "uint256" } },
    AddressCounter: { keySchema: { owner: "address", counterId: "uint256" }, schema: { value: "int256" } },
    Position: { schema: { x: "int32", y: "int32" } },
  },
});

Fetching a specific record

Fetching a record in vanilla javascript

// This is typed!
const position = client.tables.Position.get({ key: "0x00" });
// -> { x: 1, y: 2}
const multiKey = client.tables.MultiKey.get({ first: "0xDEAD", second: 42 });
// -> { value: 38843n }

Fetching a record with React and @latticexyz/react

import { useRow } from "@latticexyz/react";
function ExampleComponent() {
  const { storeCache } = useMUD;
  const position = useRow(client, { table: "Position", key: { key: "0x01" } });
  // -> { namespace: "", table: "Position", key: { key: "0x01" }, value: { x: 1, y: 2 } }
  const { value } = position;
  return (
    <div>
      <span>Player position</span>
      <span>
        x: {value.x}, y: {value.y}
      </span>
    </div>
  );
}

Scanning a table with a query

Scanning a table in vanilla javascript

const rows = client.scan(); // get all rows in the store
// -> all rows in the store
const positions = client.tables.Position.scan(); // get all rows in the position table
// -> [{key: "0x00", value: {x: 10, y: 32}, namespace: config["namespace"], table: Position}, ...]
const positionsFiltered = client.tables.Position.scan({
  key: { gt: { key: "0x02" } },
}); // get all entries whose key is bigger than 0x02
// -> [{key: "0x03", value: {x: 69, y: 4}, namespace: config["namespace"], table: Position}, ...]

Scanning a table with React and @latticexyz/react

import { useRows } from "@latticexyz/react";
function ExampleComponent() {
  const { storeCache } = useMUD;
  const positions = useRows(client, { table: "Position" });
  // -> [{key: "0x00", value: {x: 10, y: 32}, namespace: config["namespace"], table: Position}, ...]
  const positionsFiltered = useRows(client, { table: "Position", key: { gt: { key: "0x02" } } });
  //-> [{key: "0x03", value: {x: 69, y: 4}, namespace: config["namespace"], table: Position}, ...]
  return (
    <div>
      <span>Positions:</span>
      <ul>
        {positions.map(({ key, value }) => (
          <li>
            {key}: {value.x}, {value.y}
          </li>
        ))}
      </ul>
      <span>Filtered Positions:</span>
      <ul>
        {positionsFiltered.map(({ key, value }) => (
          <li>
            {key}: {value.x}, {value.y}
          </li>
        ))}
      </ul>
    </div>
  );
}

Subscribe to queries

Subscribing to queries in vanilla javascript

// subscribe to all changes of the Position table
client.tables.Position.subscribe(({ set, remove }) => {
  for (const entrySet of set) {
    // { key: "0x00" }, { x: 1, y: 2 }
    console.log(entrySet.key, entrySet.value);
  }
});
// subscribe to all changes of a single record with key 0x01
client.tables.Position.subscribe(
  ({ set, remove }) => {
    for (const entrySet of set) {
      // { key: "0x01" }, { x: 1, y: 2 }
      console.log(entrySet.key, entrySet.value);
    }
  },
  { key: { eq: { key: "0x01" } } }
);