Chat system — MVP pattern, message bus decorators, commands, auto-translation, encrypted history, state machine. Use when adding chat commands, modifying message flow, or working with chat services.
Scanned 5/27/2026
Install via CLI
openskills install decentraland/unity-explorer---
name: chat-system
description: "Chat system — MVP pattern, message bus decorators, commands, auto-translation, encrypted history, state machine. Use when adding chat commands, modifying message flow, or working with chat services."
user-invocable: false
---
# Chat System
## Sources
- `docs/chat.md` — Chat architecture (MVP, state machine, commands, services, event bus, auto-translation)
- `docs/chat-emojis.md` — Emoji atlas creation with TextMesh Pro, noto-emoji font
- `docs/chat-history-local-storage.md` — Local encrypted chat history persistence, feature flag control
---
## Architecture Overview
The chat system uses **MVP + State Machine + EventBus + Commands**:
- **View** (MonoBehaviour): draw-only; forwards UI events
- **Presenter** (POCO): listens to View + EventBus, delegates to Commands/Services, updates View
- **Model**: embodied by Services and data stores (`IChatHistory`)
- **Commands**: single-purpose business logic (e.g., `SendMessageCommand`)
- **Services**: long-lived shared state or I/O boundaries (history, member lists, input blocking)
- **EventBus**: decoupled comms via `ChatEvents` (no tight coupling between Presenters)
- **State Machine**: `ChatStateMachine` controls top-level UI mode; replaces boolean soup
- **Composition root**: `ChatPlugin` wires Views, Presenters, Commands, Services, and State Machine
### Component Map
| Layer | Key Classes |
|-------|------------|
| Composition | `ChatPlugin`, `ChatMainSharedAreaController` |
| Presenters | `ChatPanelPresenter`, `ChatTitlebarPresenter`, `ChatInputPresenter`, `ChatMessageFeedPresenter`, `ChatChannelsPresenter`, `ChatMemberListPresenter` |
| Commands | `SendMessageCommand`, `SelectChannelCommand`, `InitializeChatSystemCommand`, `GetMessageHistoryCommand`, `ResolveInputStateCommand`, ~20 more |
| Services | `CurrentChannelService`, `ChatHistoryService`, `ChatMemberListService`, `ChatInputBlockingService`, `ChatContextMenuService`, `ChatWorldBubbleService` |
| State Machine | `ChatStateMachine` with `Init`, `Default`, `Focused`, `Members`, `Minimized`, `Hidden` states |
### Message Send Flow
1. `ChatInputView` raises `onSubmit("Hello")`
2. `ChatInputPresenter` calls `SendMessageCommand`
3. Command pulls active channel from `CurrentChannelService`, calls `IChatMessagesBus`
4. Bus echoes `MessageAdded` locally (optimistic UI)
5. `ChatHistoryService` persists to `IChatHistory`
6. `ChatMessageFeedPresenter` maps via `CreateMessageViewModelCommand` and updates View
---
## Message Bus Decorator Chain
`IChatMessagesBus` is wrapped in decorators, each adding a concern. The chain is composed in `DynamicWorldContainer`.
### Decorator Responsibilities
| Decorator | Role |
|-----------|------|
| `MultiplayerChatMessagesBus` | Core transport: sends via LiveKit pipes (Island/Scene/Chat), receives from subscribed pipes, deduplicates, rate-limits, buffers nearby messages |
| `SelfResendChatMessageBus` | On `Send()`, also fires `MessageAdded` for the sender's own message (optimistic local echo) |
| `IgnoreWithSymbolsChatMessageBus` | Filters messages containing forbidden control characters (`\u2410`, `\u2406`, `\u2411`) |
| `CommandsHandleChatMessageBus` | Intercepts `/`-prefixed messages, parses command + args, dispatches to `IChatCommand`, sends system response |
### Interface
```csharp
public interface IChatMessagesBus : IDisposable
{
event Action<ChatChannel.ChannelId, ChatChannel.ChatChannelType, ChatMessage> MessageAdded;
void Send(ChatChannel channel, string message, ChatMessageOrigin origin, double timestamp);
}
```
Each decorator wraps `origin.Send()` and forwards `origin.MessageAdded` events, adding its own logic before or after.
---
## Chat Command Pattern
### Interface
```csharp
public interface IChatCommand
{
string Command { get; }
string Description { get; }
bool DebugOnly => false;
bool ValidateParameters(string[] parameters) => parameters.Length == 0;
UniTask<string> ExecuteCommandAsync(string[] parameters, CancellationToken ct);
}
```
### Existing Commands
| Command | Class | Debug? |
|---------|-------|--------|
| `/goto <x,y\|random\|crowd\|world>` | `GoToChatCommand` | No |
| `/help` | `HelpChatCommand` | No |
| `/version` | `VersionChatCommand` | No |
| `/gotolocal <x,y>` | `GoToLocalChatCommand` | Yes |
| `/reloadscene` | `ReloadSceneChatCommand` | Yes |
| `/debugpanel` | `DebugPanelChatCommand` | Yes |
| `/showentity <id>` | `ShowEntityChatCommand` | Yes |
| `/logs <category> <level>` | `LogsChatCommand` | Yes |
| `/logmatrix` | `LogMatrixChatCommand` | Yes |
| `/rooms` | `RoomsChatCommand` | Yes |
| `/loadpx <urn>` | `LoadPortableExperienceChatCommand` | Yes |
| `/killpx <urn>` | `KillPortableExperienceChatCommand` | Yes |
| `/appargs` | `AppArgsChatCommand` | Yes |
Commands are registered in `DynamicWorldContainer` as `IReadOnlyList<IChatCommand>` and looked up by name in a dictionary.
---
## Input State Machine
`ChatStateMachine` manages 6 UI states via `MVCStateMachine<ChatState>`. Each state class contains only logic relevant to that state.
### State Transitions
```
Init --> Default (on view show)
Default --> Focused (click inside / focus requested)
Default --> Minimized (close requested)
Default --> Members (toggle members)
Focused --> Default (click outside)
Focused --> Minimized (close requested)
Focused --> Members (toggle members)
Members --> Default (click outside)
Members --> Focused (close/back/toggle)
Minimized --> Focused (focus requested / minimize toggle)
Hidden <-- (SharedSpaceManager hides chat when other panels open)
Hidden --> Default (SharedSpaceManager restores chat)
```
All states inherit `ChatState` which provides virtual no-op methods: `OnClickOutside`, `OnClickInside`, `OnCloseRequested`, `OnFocusRequested`, `OnMinimizeRequested`, `OnToggleMembers`, `OnPointerEnter`, `OnPointerExit`. States override only the transitions they handle.
---
## Auto-Translation
Translation is orchestrated by `TranslationService` with modular components:
| Component | Role |
|-----------|------|
| `ITranslationService` | Central orchestrator for auto and manual translation |
| `ITranslationProvider` (`DclTranslationProvider`) | External API adapter |
| `IMessageProcessor` (`ChatMessageProcessor`) | Tokenizes complex messages, protects non-translatable parts |
| `IConversationTranslationPolicy` | Decides if auto-translate applies (global flag + per-conversation toggle) |
| `ITranslationCache` (`InMemoryTranslationCache`) | Prevents redundant API calls; capacity configured via `ChatConfig.TranslationCacheCapacity` (default 200) |
| `ITranslationMemory` (`InMemoryTranslationMemory`) | Tracks per-message translation state (`Original`, `Pending`, `Success`, `Failed`); capacity 200 |
### Configuration (ChatConfig ScriptableObject)
```csharp
[field: SerializeField] public int TranslationMaxRetries { get; set; } = 1;
[field: SerializeField] public float TranslationTimeoutSeconds { get; set; } = 10.0f;
public int TranslationMemoryCapacity = 200;
public int TranslationCacheCapacity = 200;
```
### Flow
1. `TranslationService.ProcessIncomingMessage()` checks `IConversationTranslationPolicy`
2. Creates `MessageTranslation` with `Pending` state in `ITranslationMemory`
3. Publishes `TranslationEvents.MessageTranslationRequested`
4. Checks `ITranslationCache` for existing result
5. Runs `RequiresProcessing()` -- if text has TMP tags, emojis, dates, or slash commands, uses `IMessageProcessor` (tokenize, protect, batch-translate); otherwise calls `ITranslationProvider` directly
6. Stores result in cache + memory, publishes `TranslationEvents.MessageTranslated`
7. On failure, sets `Failed` state, publishes `TranslationEvents.MessageTranslationFailed`
Concurrency: per-sender serialization via leader/follower gates, global cap of 10 in-flight translations.
---
## Encrypted History
Feature flag: `explorer-alfa-chat-history-local-storage`. Core classes in `Explorer/Assets/DCL/Chat/History/`.
### Storage Layout
```
{persistentDataPath}/c/
{Base64(AES(walletAddress))}/ <-- per-account folder
{Base64(AES("UserConversationSettings"))} <-- encrypted JSON: open channels + order
{Base64(AES(channelId))} <-- encrypted CSV per conversation
```
### Key Behaviors
- **Per-account isolation**: wallet address is the encryption key; folder name is `Base64(AES(address))`
- **Lazy file I/O**: files stay open for 5 seconds after last write, then auto-close
- **Background processing**: message queue processed on a thread pool via `UniTask.RunOnThreadPool`
- **Rehydration**: on first DM message, snapshots session messages, loads history from disk, replays session tail to avoid data loss
- **Nearby excluded**: only `ChatChannelType.USER` conversations are persisted
---
## Detailed Reference
For detailed code examples, see [reference.md](reference.md).
---
## Cross-References
- **mvc-and-ui-architecture** -- MVC controller pattern, `SharedSpaceManager` panel coordination
- **web-requests** -- `IWebRequestController` used by `DclTranslationProvider` and `GoToChatCommand` for API calls
- **feature-flags-and-configuration** -- `FeatureFlagsStrings.CHAT_HISTORY_LOCAL_STORAGE` gates history; `FeaturesRegistry` gates rate limiting and message buffering
No comments yet. Be the first to comment!