> ## Documentation Index
> Fetch the complete documentation index at: https://docs.summerengine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Summer world chat skill for AI agents

> Add match chat to a Summer game: one World channel shared by everyone in the match. Hosted only. A Summer Engine skill for AI agents.

`summer-world-chat` is a skill from the [Summer skill library](/mcp/skills): a guide your AI agent reads when the task matches, so it builds the good version on the first try. Add match chat to a Summer game: one World channel shared by everyone in the match. Hosted only.

| | |
| - | - |
| Status | Preview: not yet exercised in-engine by the Summer team |
| Domains | Multiplayer, UI |
| Version | `1.0.0` |
| Engine version | `>=4.6` |
| Recommended set | No, installed by `--all` or by name |

## When your agent uses it

* Adding in-game text chat, match chat or a chat box to a multiplayer Summer game
* Chat messages fail with text\_not\_allowed, rate\_limited or unavailable, or chat history looks incomplete
* Deciding where player chat should live instead of sending it through Commands or State

## Install

`summer setup` installs every skill for your agent. To add only this one:

```bash theme={null}
npx -y summer-engine@latest skills install summer-world-chat --agent claude-code
```

Swap `claude-code` for your agent, such as `codex` or `cursor`. See [Skills](/mcp/skills#quick-start) for every option.

## The skill

## Overview

A **World channel** is a text channel for the World (match) the player has
joined. Every player in that World can open the same channel by key, such as
`"match"`. The platform owns everything that matters about it:

* who may read it;
* the text filter;
* rate limits;
* 24-hour retention;
* blocks between players.

Your game owns only the chat UI. Messages never travel through your network
composition, and the authority neither sees nor writes them.

| Fact | Value |
| - | - |
| Text | Plain Unicode, 1 to 4000 characters. Always render it as plain text, never as markup |
| Channel keys | 1–64 of `A-Z a-z 0-9 _ . -`, starting with a letter or digit; at most 64 channels per World |
| History | Best effort, not a full log. A new handle shows the newest 2 messages; `load_older()` grows the window to at most 16 |
| Lifetime | A handle belongs to one World admission. After leaving or joining another World, open a new one |
| Not included | Direct messages (`summer-friends`), party chat (the Summer app) and messages written by your server |

**Hosted only.** Chat needs a joined hosted World and a Summer app whose Player
Host offers `player.channels`. A queue reservation is not enough: open the
channel after `enter()` succeeds. In Local Play and editor runs,
`check_open_readiness()` and `open_world_channel()` fail with `unavailable`.
Build the "chat hidden" path first.

This skill extends the Courtyard game from `multiplayer-project` and
`multiplayer-state`.

## Steps

### 1. Publish player names

Messages carry the author's Summer user id (`player_id`), not a name. The
authority already knows every verified name. Publish them in the shared match
document (`multiplayer-state`): in `authority/main.gd`'s `_join`, before
accepting, add

```gdscript theme={null}
	var players: Dictionary = match_doc.get("players", {})
	players[session.player.user_id] = session.player.display_name
	match_doc["players"] = players
	match_group.reset(Game.pack(match_doc))
```

On the client, copy `match_doc.get("players", {})` into the chat node's
`names` whenever the match document changes.

### 2. Add the chat node

`client/chat.gd`, added as a child `Chat` of `Main` in `client/main.tscn`:

```gdscript theme={null}
extends Node
## Match chat: one World channel that every player in this match shares.

signal lines_changed(lines: PackedStringArray)
signal status(text: String)

var chat: SummerWorldChannel
var names := {}   # player user id -> display name, from your match state

## Call after the player has entered the match.
func open() -> void:
	var channels := Summer.client.channels
	var ready := channels.check_open_readiness()
	if not ready.ok:
		status.emit("Chat is not available here.")   # hide the chat box
		return
	var opened := channels.open_world_channel("match")
	var result: SummerResult = await opened.get_result_or_completed_signal()
	if not result.ok:
		status.emit("Chat is not available here.")
		return
	chat = opened.get_channel()
	if chat == null:
		return   # the player already left this World
	chat.history_changed.connect(_render)
	chat.history_failed.connect(func(_failure: SummerResult) -> void: status.emit("Chat history is unavailable. Tap to retry."))
	chat.closed.connect(func(_reason: String) -> void:
		chat = null
		_render())
	_render()   # the first history may already be here

## The Retry tap after history_failed.
func retry_history() -> void:
	if chat != null:
		chat.refresh()

func send(text: String) -> void:
	if chat == null or text.strip_edges().is_empty():
		return
	var sent := chat.send_message(text)
	var result: SummerResult = await sent.get_result_or_completed_signal()
	if not result.ok and result.retryable and result.code != &"rate_limited":
		# An ambiguous failure may have been accepted. Retry the same send,
		# never send_message again: that would be a second message.
		sent = sent.retry()
		result = await sent.get_result_or_completed_signal()
	if result.ok:
		return
	match result.code:
		&"text_not_allowed":
			status.emit("That message was not sent.")
		&"rate_limited":
			status.emit("You are sending messages too fast.")
		_:
			status.emit("Message not sent.")

## The visible window always comes from get_messages(), never from appending.
func _render() -> void:
	var lines := PackedStringArray()
	if chat != null and chat.is_history_current():
		for message in chat.get_messages():
			var author := "Game" if message.author_kind == &"creator_system" else String(names.get(message.player_id, "Player"))
			lines.append("%s: %s" % [author, message.text])
	lines_changed.emit(lines)
```

Call `$Chat.open()` after `enter()` returns `true`. Wire it into the UI:

* **Messages:** a `Label` or `RichTextLabel` with `bbcode_enabled = false`, refilled from `lines_changed`.
* **Input:** a `LineEdit` whose `text_submitted` calls `$Chat.send(text)` and clears itself.
* **Status:** `status` feeds a status line; hide the whole box when chat is unavailable.

### 3. Rules the code above follows

* **Render, don't append.** `history_changed` means "replace the view from `get_messages()`". It also fires when a block or expiry removes messages, which must disappear from the screen.
* **`message_received` is for sounds and badges only.** It fires for newly seen messages, not for the initial or older history. Display still comes from `get_messages()`.
* **One `send_message` call is one message.** For an ambiguous retryable failure, call `retry()` on the same operation; it reuses the original idempotency key. A second `send_message` would post twice.
* **`text_not_allowed` stored nothing.** The filter refused it, and resending the same text fails again.
* **`rate_limited`.** About 120 messages a minute per player; tell them to slow down, don't loop.
* **Sequence gaps are normal.** Blocks, other recipients and expiry hide messages; never treat a gap as an error.
* **`creator_system` messages are from the game**, never from a player or from Summer staff.

### 4. Test

Locally, check that the hidden path works and nothing errors:

```
summer_project_setting key="summer/local_play/players" value=2
summer_play
summer_get_diagnostics
summer_stop
```

Then test real chat on Summer staging with two accounts in one match.

## Checklist

* [ ] Local Play: `open()` reports chat unavailable, the chat box is hidden, and there are no script errors.
* [ ] The channel opens only after the player entered the World.
* [ ] Hosted: a message from one player appears for the other, with the right name.
* [ ] A filtered message shows "not sent" and never appears.
* [ ] After leaving and joining another match, chat reopens on a new handle.
* [ ] Text is shown as plain text: `[b]hi[/b]` appears literally.

## Common mistakes

| Don't | Do | Why |
| - | - | - |
| Send chat through your own Commands or State | `Summer.client.channels` | Summer filters, rate-limits, applies blocks and expires text; your State does none of that |
| Open the channel right after `Summer.initialize` | Open after `enter()` succeeds | Only a joined World admission can open it |
| Append each message to a log | Re-render from `get_messages()` on `history_changed` | Blocked and expired text must disappear |
| Call `send_message` again after a timeout | `operation.retry()` | A new call is a second message |
| Render with BBCode enabled | Plain `Label`, or `bbcode_enabled = false` | Player text is never markup |
| Keep a handle across matches | Open a new channel per World | Handles never follow the player to another World |
| Show `player_id` | Map it to a name your authority publishes | Ids are not names |

## See also

* `multiplayer-state`: the match document that carries player names
* `summer-friends`: direct messages with friends
* `summer-parties`: party chat lives in the Summer app
* `multiplayer-testing`: what Local Play can and can't test

## From a shell

No Summer MCP tools in this session? Run each one as a command; when `summer --version` is missing or below 4.0, use `npx -y summer-engine@latest` in place of `summer`. Fields are flags in kebab-case (`--scene-path res://main.tscn`) or one `--input '<json>'`, and `summer ops describe <op>` prints the exact input. The `summer-cli` skill has the rest.

| In this skill | Command |
| - | - |
| `summer_get_diagnostics` | `summer engine get-diagnostics` |
| `summer_get_script_errors` | `summer engine get-script-errors` |
| `summer_play` | `summer engine play` |
| `summer_project_setting` | `summer engine project-setting` |
| `summer_stop` | `summer engine stop` |

## Related skills

* [`multiplayer`](/skills/multiplayer): Start here for Summer multiplayer: how authority, clients, Worlds and Sessions fit, and what comes next.
* [`multiplayer-state`](/skills/multiplayer-state): Authority-owned state in Summer multiplayer: shared and private State groups, Commands, Events.
* [`multiplayer-testing`](/skills/multiplayer-testing): Test a Summer multiplayer game with Local Play: several players, bad network, bots, a passing run.
* [`summer-friends`](/skills/summer-friends): Summer friends in a game: a consented friends list, profile and chat screens, direct messages. Hosted only.
* [`summer-parties`](/skills/summer-parties): Let a Summer party play together: the leader presses Play, members follow into the same match. Hosted only.

***

Source: [`library/skills/summer-world-chat/SKILL.md`](https://github.com/SummerEngine/summer/blob/main/library/skills/summer-world-chat/SKILL.md), rendered from `summer-engine@4.3.0` on npm.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.