Download Game! Currently 87 players and visitors. Last logged in:OdradekGlauriCilwandIberiamssp

Blitzer's Blog >> 72174

Back to blogs index
Posted: 07 Sep 2026 13:24 [ permalink ]
To make a distributed MUD work over a Virtual Filesystem, we have to shift our
mindset away from traditional relational databases.
The biggest trap in this architecture is the **LWW (Last-Write-Wins) Race
Condition**. If Player A and Player B both drop an item in the town square at
the exact same millisecond, and they both overwrite `/mud/rooms/town_square.jso
n`, one of their items will be lost forever.
As an architect, the solution to this is **Path-Based Entity Ownership**. We
apply the "One Entity, One File" rule. Instead of storing the *entire* room
state in one file, we treat VFS paths like a directory structure where each
entity owns its own file.
Here is how we map MUD mechanics to the dVFS.
### 1. The VFS Directory Schema
We break the state down into fine-grained paths. The room is no longer a
single JSON object; it is a *collection of files* under a shared prefix.
```text
# Static Room Data (Only written by builders/admins)
# Dynamic Entities (Players, NPCs, Items currently in the room)
```
Because Player 1 only ever writes to `player_1.json`, they will never
accidentally overwrite Player 2's data, eliminating race conditions entirely!
### 2. Upgrading the Adapter: Watching "Directories"
To make this work, the client needs to know when *any* file in a room changes.
We need to add a small `watchDir` method to our `MeshVFS` class from the
previous step.
```javascript
/ Add this to your MeshVFS class:
watchDir(prefix, callback) {
    if (!this.prefixListeners) this.prefixListeners = new Map();
    if (!this.prefixListeners.has(prefix)) {
        this.prefixListeners.set(prefix, []);
    }
    this.prefixListeners.get(prefix).push(callback);
}
/ And update `_emit` in MeshVFS to trigger it:
_emit(path, type, data) {
    // Trigger exact path listeners...
    if (this.listeners.has(path)) {
        this.listeners.get(path).forEach(cb => cb(type, data));
    }
    // Trigger directory/prefix listeners...
    if (this.prefixListeners) {
        for (const [prefix, callbacks] of this.prefixListeners.entries()) {
            if (path.startsWith(prefix)) {
                callbacks.forEach(cb => cb(path, type, data));
            }
        }
    }
}
```
### 3. Implementing MUD Mechanics
Now, let's look at how your Node.js or Browser game logic actually handles
movement and item drops using this schema.
#### Mechanic A: Player Movement
Moving is simply deleting your presence file from the old room and writing it
to the new room.
```javascript
class PlayerController {
    constructor(vfs, playerId) {
        this.vfs = vfs;
        this.id = playerId;
        this.currentRoom = null;
    }
    moveTo(newRoomId) {
        const playerData = JSON.stringify({ 
            name: "Hero", 
            hp: 100, 
            status: "idle" 
        });
        // 1. Write presence to the new room
        const newPath = `/mud/rooms/${newRoomId}/entities/${this.id}.json`;
        this.vfs.writeFile(newPath, playerData);
        // 2. Erase presence from the old room
        if (this.currentRoom) {
            const oldPath = `/mud/rooms/${this.currentRoom}/entities/${this.id}
.json`;
            this.vfs.deleteFile(oldPath);
        }
        this.currentRoom = newRoomId;
        console.log(`Moved to ${newRoomId}`);
    }
}
```
#### Mechanic B: Dropping an Item
When an item is dropped, the player removes it from their inventory (which
might be another VFS path) and writes it into the room's entity space.
```javascript
function dropItem(vfs, roomId, itemId, itemData) {
    // Generate a unique ID if it's a stackable/generic item to avoid
collisions
    const uniqueInstanceId = `${itemId}_${Date.now()}`;
    const itemPath = `/mud/rooms/${roomId}/entities/item_${uniqueInstanceId}.js
on`;
    
    // Write the item to the room's floor
    vfs.writeFile(itemPath, JSON.stringify(itemData));
    
    console.log(`Dropped ${itemData.name} in ${roomId}`);
}
```
#### Mechanic C: Rendering the Room (Browser UI)
Because we are using `watchDir`, the UI client is completely decoupled and
reactive. It just listens to the room prefix and rebuilds the screen whenever
anyone moves or drops something.
```javascript
const currentRoomEntities = new Map();
/ The UI client simply says: "Tell me about everything in the town square"
vfs.watchDir('/mud/rooms/town_square/entities/', (path, type, dataString) => {
    
    if (type === 'VFS_WRITE') {
        currentRoomEntities.set(path, JSON.parse(dataString));
    } else if (type === 'VFS_DELETE') {
        currentRoomEntities.delete(path);
    }
    // Re-render your UI based on the Map!
    renderRoomScreen(currentRoomEntities);
});
function renderRoomScreen(entities) {
    const players = [];
    const items = [];
    entities.forEach((entityData, path) => {
        if (path.includes('item_')) items.push(entityData.name);
        else players.push(entityData.name);
    });
    console.log(`You see players: ${players.join(', ')}`);
    console.log(`Items on floor: ${items.join(', ')}`);
}
```
### The Beauty of this Architecture
By treating the dVFS as an Entity Component System (ECS), **you don't need a
central game loop**.
* If a player attacks a goblin, they just write an updated HP value to the
goblin's file.
* The UI instantly sees the file change and plays a damage animation.
* The Node.js AI process sees the file change, realizes the goblin is angry,
and writes an attack file back aimed at the player.