Skip to content

Latest commit

 

History

History

README.md

Nethereum.Mud

Nethereum.Mud provides core infrastructure for interacting with MUD (Onchain Engine) applications. MUD is a framework for building ambitious Ethereum applications with on-chain state management using an Entity-Component-System (ECS) architecture.

What is MUD?

MUD (Onchain Engine) is a framework for building complex, composable applications on Ethereum. It provides:

  • Standardized On-Chain Data Storage - All application state lives on-chain in structured tables
  • Entity-Component-System Architecture - Organize game/app logic using ECS patterns
  • Composability - Applications can read and extend each other's data permissionlessly
  • Automatic Synchronization - Client-side state stays in sync with blockchain state
  • Developer Experience - TypeScript + Solidity tooling for rapid development

Nethereum.Mud brings this power to .NET, enabling you to build MUD clients, indexers, and tools in C#.

Features

  • Table Records - Strongly-typed representations of MUD tables (keys + values)
  • Schema Encoding/Decoding - Automatic encoding/decoding of MUD schemas
  • Resource Management - Resource identifiers for namespaces, tables, and systems
  • TableRepository - Query interface with LINQ-like predicates
  • In-Memory Storage - Local table record caching and change tracking
  • REST API Client - Query remote table repositories via HTTP

Installation

dotnet add package Nethereum.Mud

Dependencies

  • Nethereum.Web3
  • Nethereum.Util.Rest

MUD Architecture

MUD organizes on-chain applications using several core concepts:

World Contract

The World is the central registry contract that contains:

  • All namespaces, tables, and systems
  • Access control and permissions
  • Store logic for reading/writing data

Every MUD application has one World contract address.

Namespaces

Namespaces organize related tables and systems:

  • Provide access control boundaries
  • Group related functionality
  • Example: "Land", "Inventory", "Combat"

Tables

Tables store on-chain data in structured key-value format:

  • Schema defines field types (keys + values)
  • Keys identify records (e.g., playerId, itemId)
  • Values contain the actual data
  • Stored in the World's Store contract

Example table structure:

Table: "Player"
Keys: [playerId: uint256]
Values: [name: string, level: uint8, health: uint16]

Systems

Systems are smart contracts containing application logic:

  • Functions that read/write table data
  • Registered in the World
  • Can be called via World contract's delegation
  • Example: "MoveSystem", "CraftingSystem", "TradeSystem"

Resources

Resources are identified by namespace:name:

  • Tables: tb type (e.g., "Game:Player"0x7462...)
  • Systems: sy type (e.g., "Game:MoveSystem"0x7379...)
  • Encoded as bytes32 resource IDs

Composability

MUD's composability model allows:

  1. Reading other apps' data - Any application can query any MUD World's tables
  2. Extending applications - Deploy new systems that interact with existing tables
  3. Building on top - Create meta-applications using multiple Worlds as data sources

Code Generation

MUD tables are typically code-generated from mud.config.ts. Use .nethereum-gen.multisettings for C# generation:

Configuration File

Create .nethereum-gen.multisettings in your contracts directory:

{
  "paths": ["mud.config.ts"],
  "generatorConfigs": [
    {
      "baseNamespace": "MyGame.Tables",
      "basePath": "../MyGame/Tables",
      "generatorType": "MudTables"
    },
    {
      "baseNamespace": "MyGame.Systems",
      "basePath": "../MyGame/Systems",
      "generatorType": "MudExtendedService"
    }
  ]
}

Generator Types

1. MudTables - Generates table record classes:

  • TableRecord classes with typed Keys and Values
  • Automatic encoding/decoding methods
  • Schema information

2. MudExtendedService - Generates system service classes:

  • Service classes for calling system functions
  • Typed parameters and return values
  • Integration with World contract

Generated Table Record Example

From a MUD config defining a Player table:

// Auto-generated from mud.config.ts
public partial class PlayerTableRecord : TableRecord<PlayerKey, PlayerValue>
{
    public PlayerTableRecord() : base("MyWorld", "Player") { }

    public class PlayerKey
    {
        [Parameter("uint256", "playerId", 1)]
        public BigInteger PlayerId { get; set; }
    }

    public class PlayerValue
    {
        [Parameter("string", "name", 1)]
        public string Name { get; set; }

        [Parameter("uint8", "level", 2)]
        public byte Level { get; set; }

        [Parameter("uint16", "health", 3)]
        public ushort Health { get; set; }
    }
}

Running Code Generation

# Using Nethereum.Generators.JavaScript
npm install -g nethereum-codegen

# Generate from mud.config.ts
nethereum-codegen generate

Or use the VS Code Solidity extension with multisettings support: https://github.com/juanfranblanco/vscode-solidity

Encoding & Decoding

MUD uses custom encoding for efficient on-chain storage:

Schema Encoding

Table schemas are encoded into bytes32:

  • First 2 bytes: Static field types
  • Next 2 bytes: Dynamic field types
  • Remaining bytes: Field count metadata
using Nethereum.Mud.EncodingDecoding;

var schema = SchemaEncoder.GetSchemaEncoded<PlayerKey, PlayerValue>(resourceId);

Console.WriteLine($"Key schema: {schema.KeySchema.ToHex()}");
Console.WriteLine($"Value schema: {schema.ValueSchema.ToHex()}");
Console.WriteLine($"Static fields: {schema.NumStaticFields}");
Console.WriteLine($"Dynamic fields: {schema.NumDynamicFields}");

Key Encoding

Keys are encoded as fixed 32-byte chunks:

  • Each key component padded to 32 bytes
  • Concatenated together
  • Used as the record identifier
var playerRecord = new PlayerTableRecord
{
    Keys = new PlayerTableRecord.PlayerKey { PlayerId = 42 }
};

// Encoded as: 0x000000000000000000000000000000000000000000000000000000000000002a
var encodedKey = playerRecord.GetEncodedKey();

Value Encoding

Values are encoded in two parts:

1. Static Data - Fixed-size fields (uint256, address, bool, etc.):

// Static fields packed tightly
// e.g., uint8 + uint16 = 3 bytes total

2. Dynamic Data - Variable-size fields (string, bytes, arrays):

// Prefixed with EncodedLengths (packed field lengths)
// Then concatenated dynamic data

Example:

var playerRecord = new PlayerTableRecord
{
    Keys = new PlayerTableRecord.PlayerKey { PlayerId = 1 },
    Values = new PlayerTableRecord.PlayerValue
    {
        Name = "Alice",
        Level = 5,
        Health = 100
    }
};

var encodedValues = playerRecord.GetEncodeValues();
Console.WriteLine($"Static data: {encodedValues.StaticData.ToHex()}");
Console.WriteLine($"Dynamic data: {encodedValues.DynamicData.ToHex()}");
Console.WriteLine($"Encoded lengths: {encodedValues.EncodedLengths.ToHex()}");

Decoding

Decode on-chain data back to typed records:

// Decode from on-chain bytes
playerRecord.DecodeKey(encodedKeyBytes);
playerRecord.DecodeValues(encodedValueBytes);

Console.WriteLine($"Player {playerRecord.Keys.PlayerId}: {playerRecord.Values.Name}");
Console.WriteLine($"Level {playerRecord.Values.Level}, Health {playerRecord.Values.Health}");

Usage Examples

Example 1: Working with Table Records

using Nethereum.Mud;
using Nethereum.Web3;

// Table record with key and values
var playerRecord = new PlayerTableRecord();

// Set key
playerRecord.Keys = new PlayerTableRecord.PlayerKey
{
    PlayerId = 42
};

// Set values
playerRecord.Values = new PlayerTableRecord.PlayerValue
{
    Name = "Alice",
    Level = 10,
    Health = 100
};

// Encode for on-chain storage
var encodedKey = playerRecord.GetEncodedKey();
var encodedValues = playerRecord.GetEncodeValues();

// Decode from on-chain data
playerRecord.DecodeKey(encodedKeyBytes);
playerRecord.DecodeValues(encodedValuesBytes);

Console.WriteLine($"Player {playerRecord.Keys.PlayerId}: {playerRecord.Values.Name}");

Example 2: In-Memory Table Repository

using Nethereum.Mud.TableRepository;

// Create in-memory repository
var repository = new InMemoryTableRepository<PlayerTableRecord>();

// Add records
var player1 = new PlayerTableRecord
{
    Keys = new() { PlayerId = 1 },
    Values = new() { Name = "Alice", Level = 10, Health = 100 }
};

var player2 = new PlayerTableRecord
{
    Keys = new() { PlayerId = 2 },
    Values = new() { Name = "Bob", Level = 5, Health = 50 }
};

await repository.UpsertAsync(player1);
await repository.UpsertAsync(player2);

// Query by key
var record = await repository.GetByKeyAsync(player1.GetEncodedKey());
Console.WriteLine($"Found player: {record.Values.Name}");

// Query all records
var allPlayers = await repository.GetAsync();
Console.WriteLine($"Total players: {allPlayers.Count}");

Example 3: Query with Predicates

using Nethereum.Mud.TableRepository;

var repository = new InMemoryTableRepository<PlayerTableRecord>();

// Add multiple records
// ...

// Query with predicate builder
var predicate = new TablePredicateBuilder<PlayerTableRecord>()
    .Where(player => player.Values.Level > 5)
    .And(player => player.Values.Health >= 50)
    .Build();

var results = await repository.GetAsync(predicate);

foreach (var player in results)
{
    Console.WriteLine($"Player {player.Keys.PlayerId}: Level {player.Values.Level}, Health {player.Values.Health}");
}

Example 4: Change Tracking Repository

using Nethereum.Mud.TableRepository;

// Repository with change tracking
var repository = new InMemoryChangeTrackerTableRepository<PlayerTableRecord>();

// Modify records
var player = await repository.GetByKeyAsync(encodedKey);
player.Values.Level += 1;
player.Values.Health -= 10;
await repository.UpsertAsync(player);

// Get all changes since last checkpoint
var changeSet = repository.GetChangeSet();

Console.WriteLine($"Added: {changeSet.AddedRecords.Count}");
Console.WriteLine($"Updated: {changeSet.UpdatedRecords.Count}");
Console.WriteLine($"Deleted: {changeSet.DeletedRecords.Count}");

// Clear change tracking
repository.ClearChangeSet();

Example 5: Resource Identifiers

using Nethereum.Mud.EncodingDecoding;

// Create resource ID for a table (returns bytes32)
var playerTableResourceId = ResourceEncoder.EncodeTable("Game", "Player");
Console.WriteLine($"Table Resource ID: {playerTableResourceId.ToHex()}");

// Create resource ID for a system
var moveSystemResourceId = ResourceEncoder.EncodeSystem("Game", "MoveSystem");
Console.WriteLine($"System Resource ID: {moveSystemResourceId.ToHex()}");

// Create resource ID for a namespace
var gameNamespaceId = ResourceEncoder.EncodeNamespace("Game");
Console.WriteLine($"Namespace Resource ID: {gameNamespaceId.ToHex()}");

// Decode a resource ID back to its components
var decoded = ResourceEncoder.Decode(playerTableResourceId);
Console.WriteLine($"Namespace: {decoded.Namespace}, Name: {decoded.Name}");

Example 6: Schema Encoding

using Nethereum.Mud.EncodingDecoding;

// Get schema for a table record type
var schema = SchemaEncoder.GetSchemaEncoded<PlayerKey, PlayerValue>(resourceId);

Console.WriteLine($"Key schema: {schema.KeySchema.ToHex()}");
Console.WriteLine($"Value schema: {schema.ValueSchema.ToHex()}");
Console.WriteLine($"Total static fields: {schema.NumStaticFields}");
Console.WriteLine($"Total dynamic fields: {schema.NumDynamicFields}");

Example 7: REST API Client

using Nethereum.Mud.TableRepository;

// Connect to remote table repository API
var httpHelper = new RestHttpHelper(new HttpClient());
var apiClient = new StoredRecordRestApiClient(httpHelper, "https://api.example.com/mud");

// Query specific table
var records = await apiClient.GetRecordsAsync<PlayerTableRecord>(
    worldAddress: "0xWorldAddress",
    tableId: playerTableResource.ResourceIdEncoded.ToHex()
);

foreach (var record in records)
{
    Console.WriteLine($"Player {record.Keys.PlayerId}: {record.Values.Name}");
}

// Query with filters
var filteredRecords = await apiClient.GetRecordsAsync<PlayerTableRecord>(
    worldAddress: "0xWorldAddress",
    tableId: playerTableResource.ResourceIdEncoded.ToHex(),
    filter: $"Level gt 5"
);

Example 8: Working with Stored Records

using Nethereum.Mud.TableRepository;

// StoredRecord is the persisted form of a table record
var storedRecord = new StoredRecord
{
    WorldAddress = "0xWorldAddress",
    TableId = playerTableResource.ResourceIdEncoded.ToHex(),
    Key0 = encodedKey[0].ToHex(),  // First key component
    StaticData = encodedStaticData.ToHex(),
    DynamicData = encodedDynamicData.ToHex(),
    EncodedLengths = encodedLengths.ToHex(),
    IsDeleted = false
};

// Convert to table record
var mapper = new StoredRecordDTOMapper<PlayerTableRecord>();
var tableRecord = mapper.MapFromStoredRecord(storedRecord);

Console.WriteLine($"Restored player {tableRecord.Keys.PlayerId}");

Example 9: Singleton Tables (No Keys)

Some MUD tables have no keys (configuration singletons):

using Nethereum.Mud;

// Singleton table record
public class ConfigTableRecord : TableRecordSingleton<ConfigValue>
{
    public ConfigTableRecord() : base("MyWorld", "Config") { }

    public class ConfigValue
    {
        [Parameter("uint256", "maxPlayers", 1)]
        public BigInteger MaxPlayers { get; set; }

        [Parameter("bool", "isPaused", 2)]
        public bool IsPaused { get; set; }
    }
}

// Usage
var config = new ConfigTableRecord();
config.Values = new ConfigTableRecord.ConfigValue
{
    MaxPlayers = 100,
    IsPaused = false
};

// Only has values, no keys
var encodedValues = config.GetEncodeValues();

Example 10: Production MUD Application Pattern

using Nethereum.Mud;
using Nethereum.Mud.TableRepository;
using Nethereum.Web3;

// Initialize repository with change tracking
var playerRepository = new InMemoryChangeTrackerTableRepository<PlayerTableRecord>();
var inventoryRepository = new InMemoryChangeTrackerTableRepository<InventoryTableRecord>();

// Load initial state from chain or database
// ...

// Application logic modifies records
var player = await playerRepository.GetByKeyAsync(encodedPlayerId);
player.Values.Level += 1;
player.Values.Health = 100;
await playerRepository.UpsertAsync(player);

var inventory = await inventoryRepository.GetByKeyAsync(encodedInventoryKey);
inventory.Values.Quantity -= 1;
await inventoryRepository.UpsertAsync(inventory);

// Get changes to sync with chain
var playerChanges = playerRepository.GetChangeSet();
var inventoryChanges = inventoryRepository.GetChangeSet();

// Batch changes for on-chain transaction
var allChanges = new List<TableRecordChangeSet>
{
    playerChanges,
    inventoryChanges
};

// Send to chain via MUD World contract
// (See Nethereum.Mud.Contracts for World interaction)

// Clear change tracking after sync
playerRepository.ClearChangeSet();
inventoryRepository.ClearChangeSet();

Core Classes

TableRecord<TKey, TValue>

Base class for MUD table records with keys.

public abstract class TableRecord<TKey, TValue> : ITableRecord
    where TKey : class, new()
    where TValue : class, new()
{
    public TKey Keys { get; set; }
    public TValue Values { get; set; }

    public List<byte[]> GetEncodedKey();
    public EncodedValues GetEncodeValues();
    public void DecodeKey(List<byte[]> encodedKey);
    public void DecodeValues(EncodedValues encodedValues);
}

TableRecordSingleton

Base class for tables without keys (singletons).

public abstract class TableRecordSingleton<TValue> : ITableRecordSingleton
    where TValue : class, new()
{
    public TValue Values { get; set; }

    public EncodedValues GetEncodeValues();
    public void DecodeValues(EncodedValues encodedValues);
}

ITableRepository

Interface for table record storage and querying.

public interface ITableRepository<TTableRecord> where TTableRecord : ITableRecord, new()
{
    Task<TTableRecord> GetByKeyAsync(List<byte[]> key);
    Task<List<TTableRecord>> GetAsync();
    Task<List<TTableRecord>> GetAsync(TablePredicate<TTableRecord> predicate);
    Task UpsertAsync(TTableRecord record);
    Task DeleteAsync(List<byte[]> key);
}

ResourceEncoder

Static utility for creating and decoding MUD resource identifiers (bytes32).

public static class ResourceEncoder
{
    public static byte[] EncodeTable(string @namespace, string name);
    public static byte[] EncodeSystem(string @namespace, string name);
    public static byte[] EncodeNamespace(string @namespace);
    public static byte[] EncodeOffchainTable(string @namespace, string name);
    public static byte[] EncodeRootTable(string name);
    public static byte[] EncodeRootSystem(string name);
    public static Resource Decode(byte[] resourceBytes);
}

Advanced Topics

Custom Encoding

using Nethereum.Mud.EncodingDecoding;

// Custom key encoding
var customKeys = KeyEncoderDecoder.EncodeKey(new MyKey
{
    PlayerId = 1,
    ItemId = 42
});

// Custom value encoding
var customValues = ValueEncoderDecoder.EncodeValues(new MyValue
{
    Quantity = 10,
    IsActive = true
});

Field Layout

using Nethereum.Mud.EncodingDecoding;

// Get field layout for a schema
var fieldLayout = FieldLayoutEncoder.Encode(
    staticFieldLengths: new List<byte> { 32, 32, 1 }, // uint256, uint256, bool
    numDynamicFields: 2 // Two dynamic fields (bytes or arrays)
);

Resource Registry

using Nethereum.Mud;

// Register custom resource types
ResourceTypeRegistry.Register("CustomType", 0x1234);

// Get resource type
var tableType = ResourceTypeRegistry.GetResourceTypeId("Table"); // 0x7462...
var systemType = ResourceTypeRegistry.GetResourceTypeId("System"); // 0x7379...

Production Patterns

1. Local-First Architecture

Keep MUD table data in memory for fast reads, sync changes to chain:

// In-memory repositories for all tables
var repositories = new Dictionary<string, object>
{
    ["Player"] = new InMemoryChangeTrackerTableRepository<PlayerTableRecord>(),
    ["Inventory"] = new InMemoryChangeTrackerTableRepository<InventoryTableRecord>(),
    // ...
};

// User interacts locally
// Changes tracked automatically

// Periodic sync to chain
await SyncAllChangesToChainAsync(repositories);

2. Offline Mode with REST API

// Load initial state from REST API
var httpHelper = new RestHttpHelper(new HttpClient());
var apiClient = new StoredRecordRestApiClient(httpHelper, "https://api.mud.game");
var records = await apiClient.GetRecordsAsync<PlayerTableRecord>(worldAddress, tableId);

var localRepo = new InMemoryTableRepository<PlayerTableRecord>();
foreach (var record in records)
{
    await localRepo.UpsertAsync(record);
}

// Work offline
// ...

// Sync back when online
var changes = GetLocalChanges();
await SyncToChainAsync(changes);

3. Real-Time Updates

// Subscribe to on-chain table updates
// (See Nethereum.Mud.Contracts for event subscriptions)

void OnStoreSetRecordEvent(StoreSetRecordEventDTO evt)
{
    var storedRecord = evt.ToStoredRecord();
    var tableRecord = mapper.MapFromStoredRecord<PlayerTableRecord>(storedRecord);

    // Update local repository
    await repository.UpsertAsync(tableRecord);

    // Notify UI
    NotifyUIOfUpdate(tableRecord);
}

Why Use MUD?

Composability

MUD applications are inherently composable:

  • Read any World's data - Query tables from other applications
  • Extend existing apps - Deploy new systems that interact with existing tables
  • Build meta-applications - Aggregate data from multiple Worlds

On-Chain Data Indexing

All state lives on-chain in structured tables:

  • Queryable - Use Store events to index data
  • Verifiable - All data is on-chain and cryptographically secure
  • Persistent - Data survives as long as Ethereum does

Client Synchronization

MUD provides automatic state sync:

  • Store Events - Store_SetRecord, Store_DeleteRecord
  • Real-time updates - Clients stay in sync via event subscriptions
  • Optimistic updates - Apply changes locally, sync to chain asynchronously

Complex Application Building

MUD enables complex on-chain applications:

  • Games - Fully on-chain games with rich state
  • Autonomous Worlds - Persistent, extensible virtual worlds
  • DeFi Protocols - Complex multi-table financial logic
  • Social Networks - On-chain social graphs and interactions

Related Packages

Dependencies

  • Nethereum.Web3 - Ethereum interaction
  • Nethereum.Util.Rest - REST API utilities

Used By

  • Nethereum.Mud.Contracts - On-chain MUD World and Store contracts
  • Nethereum.Mud.Repositories.EntityFramework - EF Core persistence
  • Nethereum.Mud.Repositories.Postgres - PostgreSQL persistence

Additional Resources

Support