Extending World
On this page you learn how to modify a World
that is already deployed to the blockchain.
If you want to learn how to modify a World
before it is deployed, see the hello world page.
The sample program
To learn how to extend a World
, we will extend the Counter
example to allow users to leave a message while incrementing the counter.
The steps to create the example World
that we'll extend are here.
You can either do it now, or wait until after you've created the extension resources.
To extend the World
with the message functionality requires adding several resources:
-
Namespace. A namescape can contain tables and systems. In most cases, the only way you would be able to extend a
World
that somebody else owns is to create your own namespace within that world. -
Table. A table to store the messages that have been sent.
-
System. A system that updates the messages table and then calls
increment
to update the counter.
Create the Solidity code
The easiest way to create the Solidity code is to use the MUD template:
-
Create a new MUD application. Use either the
vanilla
template or thereact-ecs
one.pnpm create mud@next extension cd extension/packages/contracts
-
Edit
mud.config.ts
to include the definitions we need.mud.config.tsimport { mudConfig } from "@latticexyz/world/register"; export default mudConfig({ namespace: "messaging", tables: { Messages: { keySchema: { counterValue: "uint32", }, valueSchema: { message: "string", }, }, }, systems: { MessageSystem: { name: "MessageSystem", openAccess: true, }, }, });
-
Create
src/systems/MessageSystem.sol
.MessageSystem.sol// SPDX-License-Identifier: MIT pragma solidity >=0.8.21; import { System } from "@latticexyz/world/src/System.sol"; import { Messages } from "../codegen/index.sol"; interface WorldWithIncrement { function increment() external returns (uint32); } contract MessageSystem is System { function incrementMessage(string memory message) public returns (uint32) { uint32 newVal = WorldWithIncrement(_world()).increment(); Messages.set(newVal, message); return newVal; } }
Explanation
import { System } from "@latticexyz/world/src/System.sol"; import { Messages } from "../codegen/index.sol";
These are the two main things the
System
needs to know: how to be aSystem
and how to access theMessages
table.interface WorldWithIncrement { function increment() external returns (uint32); }
This
System
needs to callincrement
on theWorld
where it is implemented. However, as an extension author you might not have access to the source code of anySystem
that isn't part of your extension.If you define your own interface for
World
you can add whatever function signatures are supported. Note that in Ethereum a function signature (opens in a new tab) is the function name and its parameter types, it does not include the return type. So if you are unsure of the return type that is not a huge problem.contract MessageSystem is System { function incrementMessage(string memory message) public returns (uint32) { uint32 newVal = WorldWithIncrement(_world()).increment();
This is how we use the
WorldWithIncrement
interface we created. The_world()
(opens in a new tab) call gives us the address of theWorld
that called us. When we specifyWorldWithIncrement(<address>)
, we are telling Solidity that there is already aWorldWithIncrement
at that address, and therefore we can use functions that are supported byWorldWithIncrement
, such asincrement()
.Messages.set(newVal, message);
This is one way to create a record with the key
newVal
and the valuemessage
.return newVal; } }
When we are called by a user, an externally owned account, the return value is meaningless. However, just as we call
increment()
from a contract and use the return value (instead of having to readCounter
ourselves), some future onchain code might callincrementMessage
and use the returned value. -
Remove files that are part of the template but don't make sense when our extension doesn't have its own counter.
rm src/systems/IncrementSystem.sol test/*.t.sol script/PostDeploy.s.sol
-
Build and compile the Solidity code.
pnpm build
Deploy to the blockchain
-
To have a
Counter
exampleWorld
to modify, go to a separate command line window and create a blockchain with aWorld
using the TypeScript template and start the execution. Choose either the react user interface or the vanilla one.pnpm create mud@next extendMe cd extendMe pnpm dev
-
Back in the extension's
packages/contracts
directory, create a.env
file with:PRIVATE_KEY
- the private key of an account that has ETH on the blockchain.WORLD_ADDRESS
- the address of theWorld
to which you add the namespace.
If you are using the template with a fresh
pnpm dev
, then you can use this.env
:.env# Anvil default private key for the second account # (NOT the account that deployed the World) PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d # Address for the world we are extending WORLD_ADDRESS=0x6e9474e9c83676b9a71133ff96db43e7aa0a4342
-
Create this script in
script/MessagingExtension.s.sol
.MessagingExtension.s.sol// SPDX-License-Identifier: MIT pragma solidity >=0.8.21; import { Script } from "forge-std/Script.sol"; import { console } from "forge-std/console.sol"; import { IBaseWorld } from "@latticexyz/world-modules/src/interfaces/IBaseWorld.sol"; import { WorldRegistrationSystem } from "@latticexyz/world/src/modules/core/implementations/WorldRegistrationSystem.sol"; // Create resource identifiers (for the namespace and system) import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; // For registering the table import { Messages, MessagesTableId } from "../src/codegen/index.sol"; import { IStore } from "@latticexyz/store/src/IStore.sol"; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; // For deploying MessageSystem import { MessageSystem } from "../src/systems/MessageSystem.sol"; contract MessagingExtension is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address worldAddress = vm.envAddress("WORLD_ADDRESS"); WorldRegistrationSystem world = WorldRegistrationSystem(worldAddress); ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("messaging")); ResourceId systemResource = WorldResourceIdLib.encode(RESOURCE_SYSTEM, "messaging", "message"); vm.startBroadcast(deployerPrivateKey); world.registerNamespace(namespaceResource); StoreSwitch.setStoreAddress(worldAddress); Messages.register(); MessageSystem messageSystem = new MessageSystem(); console.log("MessageSystem address: ", address(messageSystem)); world.registerSystem(systemResource, messageSystem, true); world.registerFunctionSelector(systemResource, "incrementMessage(string)"); vm.stopBroadcast(); } }
Explanation
// SPDX-License-Identifier: MIT pragma solidity >=0.8.21;
Standard Solidity boilerplate.
import { Script } from "forge-std/Script.sol"; import { console } from "forge-std/console.sol";
The definitions for forge scripts (opens in a new tab) and the console (opens in a new tab).
import { IBaseWorld } from "@latticexyz/world-modules/src/interfaces/IBaseWorld.sol";
Use IBaseWorld.sol (opens in a new tab) to get definitions that are common to all
World
contracts.import { WorldRegistrationSystem } from "@latticexyz/world/src/modules/core/implementations/WorldRegistrationSystem.sol";
WorldRegistartionSystem.sol (opens in a new tab) contains the function definitions necessary to register new namespaces and systems with an existing
World
.// Create resource identifiers (for the namespace and system) import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
These definitions make it easy to manage resource identifiers. We need them for the resource IDs we need to create: the namespace and the system.
// For registering the table import { Messages, MessagesTableId } from "../src/codegen/index.sol"; import { IStore } from "@latticexyz/store/src/IStore.sol"; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
These are the definitions we need to register the
Messages
table.// For deploying MessageSystem import { MessageSystem } from "../src/systems/MessageSystem.sol";
These are the definitions we need to deploy the
MessageSystem
contract so we'll then be able to register it as aSystem
in theWorld
.contract MessagingExtension is Script { function run() external {
This is the function that implements the script.
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address worldAddress = vm.envAddress("WORLD_ADDRESS");
Read the private key and the address of the
World
from the environment (which includes the content of the.env
file).WorldRegistrationSystem world = WorldRegistrationSystem(worldAddress); ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("messaging")); ResourceId systemResource = WorldResourceIdLib.encode(RESOURCE_SYSTEM, "messaging", "message");
Among other things, a MUD
World
is aWorldRegistrationSystem
, so it has the appropriate functions. AResourceId
is a 32 byte value that uniquely identifies a resource in a MUDWorld
. It is two bytes of resource type followed by 14 bytes of namespace and then 16 bytes of the name of the actual resource.Here we create two
ResourceId
values:Name Type Namespace Resource name namespaceResource ns
(namespace)messaging Empty systemResource sy
(system)messaging message If you want to see these values, add these two lines to the script:
console.log("Namespace ID: %x", uint256(ResourceId.unwrap(namespaceResource))); console.log("System ID: %x", uint256(ResourceId.unwrap(systemResource)));
Note that
console.log
requires auint256
value, and we can't get that directly from aResourceId
. Instead, we have to unwrap (opens in a new tab) ourResourceId
to get the original type (bytes32
) and then cast (opens in a new tab) it touint256
.The expected values are:
Name Expected value Namespace ID 0x6e736d6573736167696e67000000000000000000000000000000000000000000 System ID 0x73796d6573736167696e6700000000006d657373616765000000000000000000 You can use an online calculator (opens in a new tab) to verify the values are correct.
Hex value ASCII 6e736d6573736167696e67 nsmessaging 73796d6573736167696e67 symessaging 6d657373616765 message vm.startBroadcast(deployerPrivateKey);
Use the private key to submit transactions.
world.registerNamespace(namespaceResource);
Register the namespace (opens in a new tab).
StoreSwitch.setStoreAddress(worldAddress); Messages.register();
Register the
Messages
table toworldAddress
.MessageSystem messageSystem = new MessageSystem(); world.registerSystem(systemResource, messageSystem, true);
Deploy the new system and then register it with the
World
we are extending. The last parameter is whether or not we allow everybody to access thisSystem
.world.registerFunctionSelector(systemResource, "incrementMessage(string)");
Register
MessageSystem.incrementMessage(string)
. This step is necessary to make the function accessible through theWorld
. The function's name when accessed through the world is<namespace>_<system>_<function>
, so this function will be available asmessaging_message_incrementMessage(string)
.Note: This is the case for all namespaces except for the root namespace, where we just use the name of the function (such as
increment()
).vm.stopBroadcast(); } }
Stop using the private key. Here this call is not necessary because we immediately leave the script, but it is a good idea to include it in case
MessagingExtension.run()
ever becomes part of a larger script. -
Run the script. Note that you need to provide the URL in the command line, you can't rely on the
ETH_RPC_URL
environment variable.forge script script/MessagingExtension.s.sol --rpc-url http://localhost:8545 --broadcast
-
Increment and write a message.
source .env cast send $WORLD_ADDRESS --private-key $PRIVATE_KEY "messaging_message_incrementMessage(string)" "hello"
When a function is not in the root namespace, it is accessible as
<namespace>_<system>_<function name>
(as long as it is registered (opens in a new tab)).Here we call our
incrementMessage(string)
with the parameterhello
. -
You can see in the user interface of
extendMe
that the counter has been incremented. To see the message we sent, use these commands:TABLE_ID=0x74626d6573736167696e6700000000004d657373616765730000000000000000 KEY=[`cast to-int256 2`] RAW_RESULT=`cast call $WORLD_ADDRESS "getRecord(bytes32,bytes32[])" $TABLE_ID $KEY` cast --to-ascii ${RAW_RESULT:322:-2}
Explanation
TABLE_ID=0x74626d6573736167696e6700000000004d657373616765730000000000000000
TABLE_ID
is theResourceId
for the table, taken fromsrc/codegen/tables/Messages.sol
You can verify the interpretation with the online calculator (opens in a new tab).Type Namespace Resource name tb
(table)messaging Messages KEY=[`cast to-int256 2`]
The key to a MUD table is always an array of
byte32
values. To create that value, we convert our key (2
, because that's the first user call toincrement()
, the 0->1 increment is done by the post deploy script) to a 256 bit value usingcast
(opens in a new tab) and then envelop it in an array.RAW_RESULT=`cast call $WORLD_ADDRESS "getRecord(bytes32,bytes32[],bytes32)" $TABLE_ID $KEY $FIELD_LAYOUT`
To call
getRecord
(opens in a new tab) we need the tableID, the key array, and the field layout. The call result is the entire value, which may have multiple static (fixed length) and dynamic (variable length) fields.Here is the raw result divided into 32 byte words.
Word Value 0 0000000000000000000000000000000000000000000000000000000000000060
1 0000000000000000000000000000000000000000000000000500000000000005
2 0000000000000000000000000000000000000000000000000000000000000080
3 0000000000000000000000000000000000000000000000000000000000000000
4 0000000000000000000000000000000000000000000000000000000000000005
5 68656c6c6f000000000000000000000000000000000000000000000000000000
When there is a variable-length field (a field whose length is not known at compile-time) in a Solidity function's return value, it is represented by a word that tells us at what offset into the return data that field starts. This function is used for different tables with different lengths of static fields, so the static fields are a variable length field as far as Solidity is concerned.
Word 0 shows us that this field's value starts at
0x60
, which is word 3. Because there are no static fields, word 3 is all zeros.Word 1 is a
PackedCounter
(opens in a new tab) with the lengths of the dynamic fields. Here is the interpretation.Bytes Value Meaning 6-0 00000000000005
The total length of all dynamic fields is five bytes. 11-7 0000000005
The length of the first dynamic field is five bytes. 16-12 0000000000
The length of the (non existent in Messages
) second dynamic field is zero21-17 0000000000
The length of the (non existent in Messages
) third dynamic field is zero26-22 0000000000
The length of the (non existent in Messages
) fourth dynamic field is zero31-27 0000000000
The length of the (non existent in Messages
) fifth dynamic field is zeroMUD tables can only have up to five dynamic fields because
PackedCounter
needs to fit in a 32 byte word.Word 2 shows us that the field with the dynamic lengths starts at byte 0x80, which is word 4. Word 4 gives us the length of the string.
Finally, word 5 gives us the actual message, "hello".
cast --to-ascii ${RAW_RESULT:322:-2}
Based on the above we don't need the first 322 characters of
$RAW_RESULT
. The first two characters are the0x
that tells us this is a hexadecimal number. The next 320 characters are words 0-4, which are not the actual message (each word is 32 bytes, which is 64 hexadecimal digits). We also don't need the trailing newline.${RAW_RESULT:322:-2}
removes those characters so we can usecast
(opens in a new tab) to get the ASCII.