# Simple Chat for s&box

Drop-in multiplayer chat. Three files, no external dependencies. Works in any s&box project that uses the standard `Component.INetworkListener` join flow.

```
simple-chat/
├── SimpleChat.cs           singleton state (messages, channels, fade, unread)
├── SimpleChatSender.cs     networked RPC relay (one per player)
└── SimpleChatPanel.razor   UI panel (markup, lifecycle, CSS)
```

## What's in the box

- WoW-style chat: messages fade out 30s after last activity, full history when opened.
- Three tabs: All / General / System (any sender named `"System"` or `"Server"` routes to System).
- Per-channel unread badges that clear when the tab is viewed.
- Local input echo: bind a key, type, Enter to send, Esc to cancel.
- Per-message text color via the `textHex` parameter on `ReceiveMessage`.

## Not included (on purpose)

- No localization. Channel names render as English literals.
- No profanity filter. Add one in `SimpleChat.Send` if you want it.
- No slash commands. Anything starting with `/` is sent verbatim.
- No dev/admin badges or per-user name colors. Every sender renders the same. Edit `.sc-sender` in the panel CSS for custom styling.
- No chat bubbles above player heads.

If you need any of these, wrap or fork. The API is small.

## Install

### 1. Copy the three files into your project's `Code/` folder

s&box compiles everything under `Code/`. A subfolder (`Code/SimpleChat/`) is fine and makes removal cleaner later.

### 2. Add `SimpleChat` to a scene GameObject

Create a GameObject in your scene (call it *Simple Chat* or whatever) and attach the `SimpleChat` component. `NetworkMode` doesn't matter; chat state is per-client.

`MaxMessages`, `MaxMessageLength`, `FadeAfter`, `FadeDuration` are exposed as `[Property]` fields. Defaults are reasonable.

### 3. Add `SimpleChatSender` to your player prefab

Open the prefab you `NetworkSpawn` for each player and attach `SimpleChatSender`. It's a single `[Rpc.Broadcast]` method.

### 4. Mount the panel in your HUD razor

```razor
@inherits PanelComponent

<root>
    <!-- ...your other HUD bits... -->

    <SimpleChatPanel Open="@ChatOpen" OnSubmitted="@CloseChat" />
</root>

@code {
    bool ChatOpen { get; set; }

    void CloseChat()
    {
        ChatOpen = false;
        StateHasChanged();
    }

    protected override void OnUpdate()
    {
        // Open chat. Bind "chat" to a key in ProjectSettings/Input.config
        // (Y or Enter are typical).
        if ( !ChatOpen && Input.Pressed( "chat" ) )
        {
            ChatOpen = true;
            StateHasChanged();
        }

        if ( ChatOpen && Input.EscapePressed )
        {
            ChatOpen = false;
            StateHasChanged();
        }
    }
}
```

The panel handles input focus and `SimpleChat.IsOpen` sync internally. The HUD only owns the open flag.

## Sending and receiving

### Local player message

The panel's `TextEntry` calls this on Enter. To send programmatically:

```csharp
SimpleChat.Instance?.Send( "Hello world" );
```

### Server-style announcement

Append a message to the local log directly:

```csharp
SimpleChat.Instance?.ReceiveMessage(
    sender: "System",
    text:   "Round starts in 10 seconds.",
    textHex: "#ffd84d" );
```

For broadcast, call `ReceiveMessage` from inside an RPC you control (mirror what `SimpleChatSender.BroadcastChatMessage` does). For a local-only notice, call directly.

Sender `"System"` and `"Server"` route to the System tab; everything else routes to General.

## API

```csharp
SimpleChat.Instance.Send( string text );
SimpleChat.Instance.ReceiveMessage( string sender, string text, string textHex = null, ulong senderSteamId = 0 );
SimpleChat.Instance.MarkRead( SimpleChat.ChatChannel channel );
SimpleChat.Instance.UnreadFor( SimpleChat.ChatChannel channel );

SimpleChat.Instance.IsOpen        // panel writes this; you can read.
SimpleChat.Instance.Opacity       // 0..1 fade alpha.
SimpleChat.Instance.IsVisible     // open or in fade window.
SimpleChat.Instance.Messages      // newest at the end.
```

## Extending

Four extension points let you wire game-specific logic without forking the library. Set them once at startup (e.g., in your GameManager's `OnStart`).

### Filter or rewrite outgoing messages

Return null to drop, or return modified text to replace.

```csharp
SimpleChat.Instance.OnBeforeSend = text =>
{
    if ( text.Contains( "spoiler" ) ) return null;     // drop
    return ProfanityFilter.Clean( text );              // rewrite
};
```

### Slash commands

Return true if your code handled the command. Anything else falls through and sends as a regular message.

```csharp
SimpleChat.Instance.SlashCommandHandler = args =>
{
    switch ( args[0].ToLower() )
    {
        case "roll":
            int n = args.Length > 1 && int.TryParse( args[1], out var x ) ? x : 100;
            SimpleChat.Instance.ReceiveMessage( "System",
                $"{Connection.Local.DisplayName} rolled {Random.Shared.Int(1, n)}" );
            return true;
        case "help":
            SimpleChat.Instance.ReceiveMessage( "System", "Commands: /roll [max]" );
            return true;
    }
    return false;
};
```

### Sender badges and name colors

Return a `SenderStyle` for any badge or color treatment. Return null for default rendering. Called once per rendered message.

```csharp
SimpleChat.Instance.StyleHook = msg =>
{
    if ( msg.SenderSteamId == 76561897985896519UL )
        return new SimpleChat.SenderStyle( Tag: "Dev",  ColorHex: "#c8a2ff" );
    if ( msg.SenderSteamId == 76561189390393201UL )
        return new SimpleChat.SenderStyle( Tag: "QA",   ColorHex: "#9333ea" );
    return null;
};
```

The panel renders `[Tag]` in front of the name and tints both in `ColorHex`.

### Listen to traffic

Multi-cast events. Use for mod logs, Discord bridges, achievement triggers.

```csharp
SimpleChat.Instance.OnMessageReceived += msg =>
    Log.Info( $"[chat] {msg.Sender}: {msg.Text}" );

SimpleChat.Instance.OnMessageSent += msg =>
    DiscordBridge.Forward( msg );
```

### Rate limit

`MinSendIntervalSeconds` (default 0.5s) is a `[Property]` on the `SimpleChat` GameObject. Local-only throttle — a modified client can bypass it. Don't lean on this for anti-spam at the network level; pair it with server-side validation if abuse becomes a problem.

## Files

| File | Lines | Responsibility |
|---|---|---|
| `SimpleChat.cs` | ~155 | Singleton state, fade, channel classification |
| `SimpleChatSender.cs` | ~25 | Per-player RPC |
| `SimpleChatPanel.razor` | ~265 | UI |

## Customising

All CSS lives in the panel's `<style>` block. Every selector is prefixed `sc-` so it won't clash. Override individual rules in your own razor file, or fork and edit in place.

If you add fields the renderer reads from `SimpleChat`, fold them into `BuildHash` so the panel repaints when they change.

## Removing

`SimpleChat`, `SimpleChatSender`, `SimpleChatPanel` don't reference each other beyond the singleton. Delete the three files and the only dangling reference is your own HUD's call site (the `<SimpleChatPanel>` markup and the `ChatOpen` field).
