Programs in DUST are smart contracts that react to world events through hooks. They use the MUD framework.
- Systems: Contract logic that modifies the world state
- Tables: On-chain data storage defined through schemas
- Hooks: Functions that execute when specific world events occur (e.g., block placement, hitting a force field)
- Registration: Programs are registered under a namespace within the world
- Event Listening: Programs implement specific hooks
- Execution: When actions happen in the world, registered programs execute their logic
- State Modification: Programs can modify world state through system calls
// In mud.config.ts
export default defineWorld({
tables: {
Owner: {
schema: {
entity: "EntityId",
owner: "address",
},
key: ["entity"],
},
// Add more tables as needed
},
});EntityId- Represents all unique entities in the game (players, blocks, tools)- Import from
@dust/world/src/types/EntityId.sol
- Import from
ObjectType- The type of an entity- Import from
@dust/world/src/types/ObjectType.sol
- Import from
After modifying mud.config.ts, run:
pnpm buildThis command:
- Reads your schema definitions from
mud.config.ts - Generates Solidity table and system libraries
- Compiles the Solidity code using Foundry (
forge build)
Generated files appear in:
src/codegen/- Solidity table librariessrc/codegen/tables/- Solidity table librariessrc/codegen/world/- World interface updates
- Start local chain (from repo root):
pnpm dev- Deploy contracts (from contracts package):
cd packages/contracts
pnpm deploy:local- Set environment variables:
export PRIVATE_KEY="your-private-key"- Deploy (from contracts package):
cd packages/contracts
pnpm deploy:redstoneEvery program should inherit from the appropriate base classes:
- BaseProgram - Provides common functionality for all programs
- System - Required for MUD system integration
- Interface(s) - The specific hooks your program responds to (e.g., ITransfer for chests)
Important distinction:
- Programs - React to world events through hooks, should only contain reactive logic
- Systems - Provide callable functions for configuration and management
Configuration in mud.config.ts:
// Programs - always have openAccess: false and registerWorldFunctions: false
ChestProgram: {
openAccess: false,
deploy: { registerWorldFunctions: false },
},
// Systems - don't need openAccess (defaults to true), always registerWorldFunctions: false
TradingChainSystem: {
deploy: { registerWorldFunctions: false },
},When to separate logic into a System:
- Admin/owner configuration functions (e.g., setting trade links, game parameters)
- Functions that need to be called through the explorer or externally
- Any non-reactive logic that doesn't belong in hooks
Example Pattern:
// Program - handles reactive logic
contract TradingChainProgram is ITransfer, BaseProgram {
function onTransfer(HookContext calldata ctx, TransferData calldata transfer) external onlyWorld {
// Only reactive logic here
// Check trading rules, validate transfers
}
}
// System - handles configuration
contract TradingChainSystem is System {
function setTradeLink(EntityId chest, ObjectType fromItem, ObjectType toItem) external {
// Configuration logic here
// Can be called through explorer
}
}MUD System Detection Requirements:
- Programs MUST explicitly inherit from
Systemeven thoughBaseProgramalready inherits from it - MUD's build system only detects contracts that directly inherit from
System - Always include the override functions for
_msgSender()and_msgValue()when inheriting from both System and BaseProgram
Import Patterns:
TransferDatais automatically available when importingITransfer- don't import it separately- Use generated system libraries (e.g.,
counterSystemfromCounterSystemLib.sol) to interact with deployed systems - Never manually deploy systems in tests - MUD handles deployment and provides access through generated libraries
Example of correct Program structure:
import { System, WorldContextConsumer } from "@latticexyz/world/src/System.sol";
import { HookContext, ITransfer } from "@dust/world/src/ProgramHooks.sol"; // TransferData is included
contract MyProgram is ITransfer, System, BaseProgram { // Explicit System inheritance required!
// Implementation...
// Required overrides
function _msgSender() public view override(WorldContextConsumer, BaseProgram) returns (address) {
return BaseProgram._msgSender();
}
function _msgValue() public view override(WorldContextConsumer, BaseProgram) returns (uint256) {
return BaseProgram._msgValue();
}
}Testing Systems:
// Correct: Use generated library
import { counterSystem } from "../src/codegen/systems/CounterSystemLib.sol";
// Then use directly: counterSystem.increment(world, ...);
// Wrong: Manual deployment
CounterSystem counterSystem = new CounterSystem();// Modify the existing BaseProgram onAttach and onDetach hooks
import { Owner } from "./codegen/tables/Owner.sol";
abstract contract BaseProgram is IAttachProgram, IDetachProgram, System, WorldConsumer(Constants.DUST_WORLD) {
function onAttachProgram(HookContext calldata ctx) public virtual override onlyWorld {
Owner.set(ctx.target, ctx.caller.getPlayerAddress());
}
function onDetachProgram(HookContext calldata ctx) public virtual override onlyWorld {
// Only revert if revertOnFailure is set, otherwise we wouldn't reach the cleanup
if (ctx.revertOnFailure) {
// ctx.target is the chest
// ctx.caller is the player
// The revert reason will be displayed in the client
require(Owner.get(ctx.target) == ctx.caller.getPlayerAddress(), "Only the owner can use this chest");
}
// Always perform cleanup!
Owner.deleteRecord(ctx.target);
}// Import program interfaces and HookContext struct from @dust/world package
import { HookContext, ITransfer } from "@dust/world/src/ProgramHooks.sol";
import { Owner } from "./codegen/tables/Owner.sol";
contract MyChestProgram is ITransfer, System, BaseProgram {
// React to transferring stuff from/to chest's inventory
function onTransfer(
HookContext calldata ctx,
TransferData calldata transfer
) public {
// Only revert if revertOnFailure is set
if (ctx.revertOnFailure) {
// ctx.target is the chest
// ctx.caller is the player
// The revert reason will be displayed in the client
require(Owner.get(ctx.target) == ctx.caller.getPlayerAddress(), "Only the owner can use this chest");
}
// Perform state changes / cleanup if needed
// Assuming a SomeTable table with schema EntityId -> uint256
SomeTable.set(ctx.target, 123);
}
// ...
}// In mud.config.ts
export default defineWorld({
tables: {
Owner: {
schema: {
entity: "EntityId",
owner: "address",
},
key: ["entity"],
},
SomeOtherTable: {
schema: {
chest: "EntityId",
value: "uint256",
someOtherValue: "bytes32",
},
key: ["chest"],
},
},
});import { SomeOtherTable, SomeOtherTableData } from "./codegen/tables/SomeOtherTable.sol";
// Read specific field
uint256 value = SomeOtherTable.getValue(ctx.target);
// Read both value and someOtherValue into a struct
SomeOtherTableData memory data = SomeOtherTable.get(ctx.target);
// Then access fields from the struct
data.value;
// Write specific field
SomeOtherTable.setValue(ctx.target, newValue);
// Write all values at once by using the SomeOtherTableData struct
SomeOtherTable.set(ctx.target, data);
// Delete entire record for the target entity id
SomeOtherTable.deleteRecord(ctx.target);// Get the player's address from ctx.caller
address playerAddress = ctx.caller.getPlayerAddress();import { ObjectType, ObjectTypes } from "@dust/world/src/types/ObjectType.sol";
// Common object types
ObjectTypes.WheatSeed // For wheat seeds
ObjectTypes.Dirt // For dirt blocks
mine.objectType.isLeaf() // Check if a block is a leaf type
// Compare object types
if (transfer.deposits[0].objectType == ObjectTypes.WheatSeed) {
// Handle wheat seed deposits
}// Death table - tracks player deaths
import { Death } from "@dust/world/src/codegen/tables/Death.sol";
uint256 deathCount = Death.getDeaths(ctx.caller);
// EntityTypeLib - convert between EntityId and player addresses
import { EntityTypeLib } from "@dust/world/src/types/EntityId.sol";
EntityId playerId = EntityTypeLib.encodePlayer(playerAddress);// TransferData structure contains deposits and withdrawals
function onTransfer(HookContext calldata ctx, TransferData calldata transfer) external {
// Check deposits
if (transfer.deposits.length > 0) {
for (uint i = 0; i < transfer.deposits.length; i++) {
ObjectType itemType = transfer.deposits[i].objectType;
uint256 amount = transfer.deposits[i].amount;
// Process deposit
}
}
// Check withdrawals
if (transfer.withdrawals.length > 0) {
// Handle withdrawals
}
}- Owner Tracking:
import { Owner } from "./codegen/tables/Owner.sol";
// Set owner on attach
function onAttachProgram(HookContext calldata ctx) public override onlyWorld {
Owner.set(ctx.target, ctx.caller.getPlayerAddress());
}
// Check ownership
require(Owner.get(ctx.target) == ctx.caller.getPlayerAddress(), "Not owner");
// Clear owner on detach
Owner.deleteRecord(ctx.target);- State Management:
// Track boolean states
GameActive.set(ctx.target, true);
// Track counters
uint256 currentCount = Counter.get(ctx.target);
Counter.set(ctx.target, currentCount + 1);
// Track lists (using multiple records with composite keys)- Single Entity Storage:
// For data tied to a single entity
tables: {
EntityData: {
schema: {
entity: "EntityId",
value: "uint256",
},
key: ["entity"],
},
}- Composite Key Storage:
// For relationships between entities
tables: {
Relationship: {
schema: {
entity1: "EntityId",
entity2: "EntityId",
data: "bytes32",
},
key: ["entity1", "entity2"],
},
}- Player-Entity Mapping:
// For player-specific data per entity
tables: {
PlayerData: {
schema: {
entity: "EntityId",
player: "address",
value: "uint256",
},
key: ["entity", "player"],
},
}For storing lists of players or entities:
// Define an array table
tables: {
PlayerList: {
schema: {
players: "address[]",
},
key: [], // No key means single record
},
}
// In Solidity
import { PlayerList } from "./codegen/tables/PlayerList.sol";
// Add to array
PlayerList.push(newPlayer);
// Get entire array
address[] memory players = PlayerList.get();
// Iterate through array
for (uint256 i = 0; i < players.length; i++) {
// Process each player
}- Always Handle ctx.revertOnFailure:
function onTransfer(HookContext calldata ctx, TransferData calldata) external {
// Only enforce rules when revertOnFailure is true
if (ctx.revertOnFailure) {
require(someCondition, "Error message");
}
// Perform state updates regardless
updateState();
}- Clean Up in onDetachProgram:
function onDetachProgram(HookContext calldata ctx) public override {
// Always clean up data, even without reverting
MyTable.deleteRecord(ctx.target);
// Clear any related tables
RelatedTable.deleteRecord(ctx.target);
}- Initialize State in onAttachProgram:
function onAttachProgram(HookContext calldata ctx) public override {
// Set initial state
GameState.set(ctx.target, false);
Owner.set(ctx.target, ctx.caller.getPlayerAddress());
}- Use Memory for Complex Reads:
// Read once into memory for multiple accesses
GameStateData memory gameData = GameState.get(ctx.target);
if (gameData.active && gameData.playerCount > 0) {
// Use gameData multiple times without re-reading
}- Namespace Collision Prevention:
- Always use a unique namespace in mud.config.ts (this is the first thing the user should change!)
- Check existing namespaces before deploying (worst case it will just fail)
- Define Namespace and Schema: Edit
mud.config.tsto use a custom unique namespace and add/modify tables - Generate Code: Run
pnpm buildto generate TypeScript/Solidity code - Write Logic: Implement systems/programs in
src/ - Test Locally: Use
pnpm testto run unit tests - Deploy: Deploy to target network
- Initialize: Run post-deploy scripts
# Install dependencies
pnpm install
# Generate code from schema (in contracts package)
pnpm build
# Start local development (at the root of the repo)
pnpm dev
# Run tests (in contracts package)
pnpm test
# Deploy contracts (within the contracts package)
pnpm deploy:local
pnpm deploy:redstone
# Clean build artifacts (if you have issues)
forge clean && pnpm buildPrograms must be attached to entities to become active:
- Get the Entity ID: Click the MUD icon when interacting with an entity in-game
- Get the Program ID: You can find them in
./packages/contracts/.mud/local/systems.json - Attach via the Explorer: Use the updateProgram function in the interact tab
- Or via Script: Use
pnpm attach <entity id> <program id>
The player can get the entity id for a smart entity (chests, forcefields, etc) by clicking the MUD icon at the top when the native UI opens for the object, and selecting the "EntityId" field.
Every hook receives a HookContext struct containing:
struct HookContext {
EntityId caller; // The entity that triggered the hook (usually the player)
EntityId target; // The entity the program is attached to, for which the hook is being called
bool revertOnFailure; // Whether to revert the action if hook fails
bytes extraData; // Additional data passed to the hook (usually empty)
}- Conditional Reverting: Only revert if
ctx.revertOnFailureis true - Cleanup: Always perform cleanup in detach hooks, even without reverting
- Access Control: Use hook context to determine if an action should be allowed
Programs can react to various world events. Each type of entity can react to different events:
onAttachProgram(HookContext ctx)- Called when the program is attached to an entityonDetachProgram(HookContext ctx)- Called when the program is detached from an entity
-
onTransfer(HookContext ctx, TransferData transfer)- Called when items are transferred to/from the cheststruct TransferData { SlotData[] deposits; // Items being deposited SlotData[] withdrawals; // Items being withdrawn } struct SlotData { ObjectType objectType; // Type of item uint256 amount; // Quantity }
-
onOpen(HookContext ctx)- Called when the chest is opened -
onClose(HookContext ctx)- Called when the chest is closed
onMine(HookContext ctx, MineData mine)- Called when a block is mined within the force fieldstruct MineData { EntityId entity; // The entity that was mined EntityId tool; // Tool used for mining Vec3 coord; // Coordinates where mining occurred ObjectType objectType; // Type of object mined }
onBuild(HookContext ctx, BuildData build)- Called when a block is built within the force fieldstruct BuildData { EntityId entity; // The entity being built Vec3 coord; // Build coordinates ObjectType slotType; // Slot type used ObjectType objectType; // Object type being built Orientation orientation; // Direction/rotation }
onHit(HookContext ctx, HitData hit)- Called when the force field is hitstruct HitData { EntityId tool; // Tool/weapon used uint128 damage; // Amount of damage }
onEnergize(HookContext ctx, EnergizeData energize)- Called when the force field is energizedstruct EnergizeData { uint128 amount; // Energy amount }
onAddFragment(HookContext ctx, AddFragmentData fragment)- Called when a fragment is addedstruct AddFragmentData { EntityId added; // The fragment entity that was added }
onRemoveFragment(HookContext ctx, RemoveFragmentData fragment)- Called when a fragment is removedstruct RemoveFragmentData { EntityId removed; // The fragment entity that was removed }
validateProgram(HookContext ctx, ProgramData program)- Validates if aprogramcan be attached toprogrammed. In this case,ctx.targetis the force fieldstruct ProgramData { EntityId programmed; // Entity being programmed ProgramId program; // Program being attached }
onSpawn(HookContext ctx, SpawnData spawn)- Called when a player spawns on the tilestruct SpawnData { uint128 energy; // Energy amount Vec3 coord; // Spawn coordinates }
onSleep(HookContext ctx)- Called when a player sleeps in the bedonWakeup(HookContext ctx)- Called when a player wakes up from the bed
This program restricts chest access to the player who attached it:
// In mud.config.ts, add:
tables: {
ChestOwner: {
schema: {
chest: "EntityId",
owner: "address",
},
key: ["chest"],
},
}
// In OwnerChest.sol:
contract OwnerChest is ITransfer, System, BaseProgram {
import { ChestOwner } from "./codegen/tables/ChestOwner.sol";
function onAttachProgram(HookContext calldata ctx) public override onlyWorld {
ChestOwner.set(ctx.target, ctx.caller.getPlayerAddress());
}
function onDetachProgram(HookContext calldata ctx) public override onlyWorld {
ChestOwner.deleteRecord(ctx.target);
}
function onTransfer(HookContext calldata ctx, TransferData calldata) external onlyWorld {
if (ctx.revertOnFailure) {
require(
ChestOwner.getOwner(ctx.target) == ctx.caller.getPlayerAddress(),
"Only owner can access"
);
}
}
}This program allows only specific block types to be mined:
// In mud.config.ts:
tables: {
AllowedBlocks: {
schema: {
forcefield: "EntityId",
blockType: "ObjectType",
allowed: "bool",
},
key: ["forcefield", "blockType"],
},
}
// In MiningRestriction.sol:
import { AllowedBlocks } from "./codegen/tables/AllowedBlocks.sol";
contract MiningRestriction is IMine, System, BaseProgram {
function onMine(HookContext calldata ctx, MineData calldata mine) external view onlyWorld {
if (ctx.revertOnFailure) {
require(
AllowedBlocks.getAllowed(ctx.target, mine.objectType),
"Cannot mine this block type"
);
}
}
}A complete working example of a Spleef-style game using death count tracking:
// mud.config.ts:
tables: {
Participant: {
schema: {
player: "address",
deathCount: "uint256", // Death count when they joined
isSet: "bool",
},
key: ["player"],
},
Depositors: {
schema: {
depositors: "address[]", // List of all participants
},
key: [],
},
}// SpleefChest.sol - Entry fee chest and winner withdrawal:
import { Death } from "@dust/world/src/codegen/tables/Death.sol";
import { EntityTypeLib } from "@dust/world/src/types/EntityId.sol";
import { ObjectTypes } from "@dust/world/src/types/ObjectType.sol";
import { Depositors } from "./codegen/tables/Depositors.sol";
import { Participant, ParticipantData } from "./codegen/tables/Participant.sol";
contract SpleefChest is ITransfer, System, BaseProgram {
function onTransfer(HookContext calldata ctx, TransferData calldata transfer) external onlyWorld {
if (!ctx.revertOnFailure) return;
address player = ctx.caller.getPlayerAddress();
ParticipantData memory data = Participant.get(player);
// Handle withdrawals (winner takes all)
if (transfer.withdrawals.length > 0) {
require(data.isSet, "You are not part of the game!");
address[] memory depositors = Depositors.get();
// Check if all other players are dead
for (uint256 i = 0; i < depositors.length; i++) {
address otherPlayer = depositors[i];
uint256 currentDeaths = Death.getDeaths(EntityTypeLib.encodePlayer(otherPlayer));
require(
otherPlayer == player || currentDeaths > Participant.getDeathCount(otherPlayer),
"Game is still ongoing! Kill them all!"
);
}
return;
}
// Handle deposits (entry fee)
require(!data.isSet, "Already deposited!");
require(transfer.deposits.length == 1, "Only one wheat seeds deposit is allowed");
require(transfer.deposits[0].objectType == ObjectTypes.WheatSeed, "Only wheat seeds");
require(transfer.deposits[0].amount >= 5, "At least 5 wheat seeds required");
// Register participant with their current death count
Participant.set(player, Death.getDeaths(ctx.caller), true);
Depositors.push(player);
}
}// SpleefArena.sol - Force field controlling the arena:
contract SpleefArena is IMine, IBuild, System, BaseProgram {
function onMine(HookContext calldata ctx, MineData calldata mine) external view onlyWorld {
if (!ctx.revertOnFailure) return;
address player = ctx.caller.getPlayerAddress();
// Only participants can mine
require(Participant.getIsSet(player), "You are not part of the game!");
// Can only mine leaves (spleef blocks)
require(mine.objectType.isLeaf(), "Can only mine leaves!");
}
function onBuild(HookContext calldata ctx, BuildData calldata) external view onlyWorld {
if (!ctx.revertOnFailure) return;
// No building allowed in the arena
revert("Building is not allowed in the arena!");
}
}How it works:
- Players deposit 5 wheat seeds to enter the game
- Their death count is recorded when they join
- Players can only mine leaves within the force field
- Last player alive (others have increased death count) can withdraw all wheat seeds
Build fails after schema change
- Rebuild (including MUD codegen):
pnpm build - Clean and rebuild if issues persist:
forge clean && pnpm build
Deployment fails
- Check account balance
- Verify RPC connection
- Ensure PRIVATE_KEY is set correctly
- Check if namespace is already taken
Attaching program fails
- Check error message and transaction traces (
cast run --rpc-url https://rpc.redstonechain.com <tx hash>) - Check any permissions implemented by the program
- Ensure you're close enough to the entity in-game
- Verify the program was deployed successfully
Program not working as intended
- Ensure program is attached by checking the EntityProgram table in the explorer (https://explorer.mud.dev/redstone/worlds/0x253eb85B3C953bFE3827CC14a151262482E7189C/explore)
- Check the data of the tables registered under your namespace
- Verify hook implementations match expected signatures
- Check if
ctx.revertOnFailureis being handled correctly
"Hook not supported" error
- Entity type doesn't support that specific hook
- Check you're implementing the correct interface for your entity type
- Ensure fallback function is properly configured
Table access errors
- Ensure tables are imported from the correct codegen path
- Run
pnpm buildafter any schema changes - Check table keys match what you defined in mud.config.ts
Player address issues
- Use
ctx.caller.getPlayerAddress()to get the player's address - EntityId and address are different - always convert when needed
For advanced math operations, you can use:
- Solady's FixedPointMathLib: Import from
solady/src/utils/FixedPointMathLib.sol- Provides WAD math operations (1e18 precision)
- Useful for percentages, ratios, and complex calculations
For simple operations, use native Solidity:
// Simple percentage calculation
uint256 result = (value * percentage) / 100;
// Decay calculation
uint256 decayed = (value * 950) / 1000; // 95% of valueWhen programs need to invalidate old data between attachments (e.g., resetting scores), use a session counter:
// In mud.config.ts
tables: {
Config: {
schema: {
entity: "EntityId",
sessionId: "uint256",
// other fields...
},
key: ["entity"],
},
PlayerData: {
schema: {
entity: "EntityId",
sessionId: "uint256", // Include session ID in composite key
player: "address",
data: "uint256",
},
key: ["entity", "sessionId", "player"], // Old sessions automatically ignored
},
}
// In Program.sol
function onAttachProgram(HookContext calldata ctx) public override {
// Use existing session ID (0 on first attachment)
uint256 sessionId = Config.getSessionId(ctx.target);
Config.set(ctx.target, sessionId, otherData...);
}
function onDetachProgram(HookContext calldata ctx) public override {
// Increment session ID for next attachment
uint256 nextSessionId = Config.getSessionId(ctx.target) + 1;
Config.setSessionId(ctx.target, nextSessionId);
// No need to delete old player data - different sessionId makes it inaccessible
}This pattern avoids iteration to clean up player data while ensuring each program attachment starts fresh.
- MUD Documentation: https://mud.dev
- Solidity Documentation: https://docs.soliditylang.org
- Dust docs: https://dev.dustproject.org/
- Project Scripts: See
package.jsonfor all available commands
The Dust client supports embedded apps - web apps that integrate directly into the game client UI and interact with in-game objects and physics. Apps let developers build on top of the world and extend the game client with custom functionality like shops and marketplaces.
A Dust app is:
- A web app hosted at a URL
- Described by a JSON manifest (schema)
- Registered onchain (once per manifest URL)
- Launched manually (e.g. installing into client's "desktop" view) or contextually (e.g. opening a chest)
- Registration: Developer interacts with the App Registry to register the app's manifest URL.
- Discovery: The Dust client automatically detects app registrations.
- Launch:
- Manual: User opens via their desktop
- Contextual: Interacts with an in-game entity (e.g. chest)
- Communication:
- App loads in iframe
- Dustkit sets up postMessage channel
- App sends
readymessage - Client sends contextual info (e.g.
entityIdof chest that opened the app)
Before registering an app to make it available to everyone, you can preview your app in the production client by using the debug-app URL parameter.
Example: https://alpha.dustproject.org/?debug-app=http://localhost:3000/dust-app.json
To make an app available in everyone's client, you have to register it in the global app registry.
-
Register a new MUD namespace.
import { ResourceId, WorldResourceIdInstance, WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol"; IWorld world = IWorld(0x253eb85B3C953bFE3827CC14a151262482E7189C); ResourceId appNamespaceId = WorldResourceIdLib.encodeNamespace(bytes14(bytes("your-dust-app"))); if (!ResourceIds.getExists(appNamespaceId)) { world.registerNamespace(appNamespaceId); }
-
Register by setting a resource tag that points to your (app's manifest)
import { metadataSystem } from "@latticexyz/world-module-metadata/src/codegen/experimental/systems/MetadataSystemLib.sol"; metadataSystem.setResourceTag(appNamespaceId, "dust.appConfigUrl", bytes("https://your-dust-app.com/dust-app.json"));
In this repository, you can run the RegisterScript.s.sol forge script.
The easiest way to run it is via the pnpm manager:registerApp:redstone command in the contracts package.
To show a contextual app when interacting with an entity that has your program installed, your program needs to implement the appConfigURI function.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
contract CustomProgram {
function appConfigURI(EntityId viaEntity) external returns (string memory uri) {
return "https://your-dust-app.com/dust-app.json";
}
}Spawn apps are displayed on the spawn screen and should implement spawning functionality for custom spawn tiles.
Registering them is very similar to registering global apps, just using the dust.spawnAppConfigUrl resource tag instead.
-
Register a new MUD namespace.
import { ResourceId, WorldResourceIdInstance, WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol"; IWorld world = IWorld(0x253eb85B3C953bFE3827CC14a151262482E7189C); ResourceId appNamespaceId = WorldResourceIdLib.encodeNamespace(bytes14(bytes("your-dust-app"))); if (!ResourceIds.getExists(appNamespaceId)) { world.registerNamespace(appNamespaceId); }
-
Register by setting a resource tag that points to your (spawn app's manifest)
import { metadataSystem } from "@latticexyz/world-module-metadata/src/codegen/experimental/systems/MetadataSystemLib.sol"; metadataSystem.setResourceTag(appNamespaceId, "dust.spawnAppConfigUrl", bytes("https://your-dust-spawn-app.com/dust-app.json"));
Dustkit is the bridge between apps and the native Dust browser client.
This is already done in this repository, but here are the setup instructions for context.
- Add
dustkitas a dependency topackage.json. The tag at the end corresponds to the github commit on main.
"dustkit": "https://pkg.pr.new/dustproject/dust/dustkit@27f724c"- Connect the dustkit client:
import { connectDustClient } from "dustkit/internal";
const { appContext, provider } = await connectDustClient();- You can now access the methods on the
providerobject. For example, to get the player's position:
const position = await provider.request({
method: "getPlayerPosition",
params: {
entity: "0x",
},
});Sets a waypoint for a specific entity with a label.
Parameters:
entity(EntityId): The entity to set the waypoint forlabel(string): The label for the waypoint
Returns: void
Retrieves slot information for inventory operations.
Parameters:
entity(EntityId): The entity to get slots forobjectType(number): The type of objectamount(number): The amount of objectsoperationType("withdraw" | "deposit"): "withdraw" means you want the slots where this object & amount exists and "deposit" means you want the slots where this object & amount will fit
Returns:
{
slots: {
slot: number;
amount: number;
}
[];
}Executes a system call in the world
Parameters:
params(SystemCalls): The system call parameters
Returns: Either a user operation receipt or transaction receipt:
{
userOperationHash: Hex;
receipt: UserOperationReceipt;
} | {
transactionHash: Hex;
receipt: TransactionReceipt;
}Gets the 3D position of a player entity.
Parameters:
entity(EntityId): The player entity
Returns:
{
x: number;
y: number;
z: number;
}Sets a blueprint with block positions and options.
Parameters:
blocks: Array of block definitions:{ objectTypeId: number; x: number; y: number; z: number; orientation: number; } [];
options(optional, defaults to true for both): Blueprint display options:{ showBlocksToMine: boolean; showBlocksToBuild: boolean; }
Returns: void
Gets the currently selected object type in the players hotbar.
Parameters: None
Returns: number - The selected object type ID
Gets force field information at a specific coordinate.
Parameters:
x(number): X coordinatey(number): Y coordinatez(number): Z coordinate
Returns: Force field data or undefined:
{
forceFieldId: Hex;
fragmentId: Hex;
fragmentPos: {
x: number;
y: number;
z: number;
};
forceFieldCreatedAt: bigint;
extraDrainRate: bigint;
} | undefinedGets the current cursor position in the world, if available.
Parameters: None
Returns: Cursor position or undefined:
{
x: number;
y: number;
z: number;
} | undefined