> ## 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 parties skill for AI agents

> Let a Summer party play together: the leader presses Play, members follow into the same match. Hosted only. A Summer Engine skill for AI agents.

`summer-parties` 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. Let a Summer party play together: the leader presses Play, members follow into the same 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

* Friends in a Summer party should land in the same multiplayer match: party play, play with friends, squad up
* Adding invite-friends and party buttons, a party roster or ready checks to a Summer game's menu
* A party join fails with party\_member\_offline, party\_not\_searching or capability\_unavailable

## 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-parties --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 Summer party belongs to the player's Summer account, not to your game. The
Summer app creates parties, sends invites, kicks, transfers leadership and runs
party chat. Your game sees a read-only view through `Summer.client.party`:

* who is in the party, who leads, and which members have **this** game open;
* small text data your game shares with the party;
* whether the leader started a search in one of your queues.

To bring the party into a match, the **leader** joins
`SummerJoinTarget.party_queue(queue)`. Every other member joins the **same
target** when `search_started` fires. Summer puts them all in one match, each
with their own Session. The match itself is ordinary: the authority sees
normal `join` Commands and Sessions.

**Hosted only.** `is_available()` is `false` and every call fails with
`capability_unavailable`:

* in Local Play and editor runs;
* for guests;
* for accounts the platform keeps out of social features (`age_restricted`, under 13);
* in Summer apps without the `player.party@1` capability.

Outside a party, or when parties are unavailable, getters are empty and
`is_in_party()` is `false`. So the same Play button works everywhere: no party
means an ordinary `SummerJoinTarget.queue(...)` join. Build that path first.

This skill extends the Courtyard game from `multiplayer-project`
(`network/game.gd` as `Game`, `client/main.gd` with `command()`).

## Steps

### 1. Join from a Play button, not from `_ready`

A party member must not join on their own the moment the game starts. Move the
join out of `client/main.gd`'s `_ready` into `enter(target)`, and keep every
signal connection from `multiplayer-movement` and `multiplayer-state`:

```gdscript theme={null}
func _ready() -> void:
	spawner = Game.build_network(self, PlayerArchetype)
	# Connect before joining, or the first values arrive unseen.
	spawner.state_group_created.connect(_on_group_created)
	spawner.entity_spawned.connect(_on_entity_spawned)
	spawner.event_received.connect(_on_event)
	var initialized: SummerResult = await Summer.initialize().get_result_or_completed_signal()
	if not initialized.ok:
		push_error(initialized.message)
		return
	$Party.start()
	# With parties, the Play button calls $Party.play(). Without them (Local
	# Play, guests, apps without parties) the player plays solo right away.
	if not Summer.client.party.is_available():
		await enter(SummerJoinTarget.queue(Game.QUEUE))

## Joins a match through `target`, then enters it. True once the player is in.
func enter(target: SummerJoinTarget) -> bool:
	var join := Summer.client.join(target)
	# Hosted matchmaking may ask the player to accept a match.
	join.acceptance_required.connect(func(proposal: SummerMatchmakingProposal) -> void: proposal.accept())
	var joined: SummerResult = await join.get_result_or_completed_signal()
	if not joined.ok:
		push_warning("join failed: %s %s" % [joined.code, joined.message])
		return false
	if not spawner.is_network_ready():
		await spawner.network_ready
	var entered := await command({"c": "join"})
	if not entered.ok:
		push_error("could not enter the match: %s" % entered.error)
		return false
	print("COURTYARD_CLIENT_JOINED")
	return true
```

Move the Local Play bot hook from `multiplayer-testing` into `enter()`,
after the joined line.

### 2. Add the party node

`client/party.gd`, added as a child `Party` of `Main` in `client/main.tscn`.
It is client-only: authority scenes never touch `Summer.client`.

```gdscript theme={null}
extends Node
## Plays with the player's Summer party. The leader presses Play; every member
## follows into the same match. Without a party, Play joins the queue alone.

const Game = preload("res://network/game.gd")

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

## Main calls this right after Summer.initialize succeeds: a child's _ready
## runs before that, while Summer.client is still null.
func start() -> void:
	var party := Summer.client.party
	party.search_started.connect(_on_search_started)
	party.changed.connect(_render)
	_render()

## The Play button.
func play() -> void:
	var party := Summer.client.party
	if not party.is_in_party():
		await get_parent().enter(SummerJoinTarget.queue(Game.QUEUE))
	elif party.is_leader():
		await get_parent().enter(SummerJoinTarget.party_queue(Game.QUEUE))
	else:
		status.emit("Waiting for %s to press Play." % party.get_leader().display_name)

## The leader started a search: members follow with the same target.
func _on_search_started(queue: StringName) -> void:
	if not Summer.client.party.is_leader():
		await get_parent().enter(SummerJoinTarget.party_queue(queue))

func _render() -> void:
	var lines := PackedStringArray()
	for member in Summer.client.party.get_members():
		var tags := []
		if member.leader:
			tags.append("leader")
		if not member.in_game:
			tags.append("not in game")
		if member.get_data("ready") == "yes":
			tags.append("ready")
		lines.append("%s %s" % [member.display_name, ", ".join(tags)])
	roster_changed.emit(lines)

## The Invite button: show it only when the Summer app offers parties.
func invite() -> void:
	var shown: SummerResult = await Summer.client.party.show_invite_dialog().get_result_or_completed_signal()
	if not shown.ok:
		status.emit("Invites are not available right now.")

## A member's Ready toggle, visible to the whole party.
func set_ready(ready: bool) -> void:
	var saved: SummerResult = await Summer.client.party.set_member_data("ready", "yes" if ready else "").get_result_or_completed_signal()
	if not saved.ok:
		status.emit("Could not update ready: %s" % saved.code)
```

Connect `roster_changed` to a `Label` or `ItemList`, and `status` to a status
line. Show the buttons by state:

| Button | Show when |
| - | - |
| Play | not in a party, or leader. A member sees "waiting for the leader" instead |
| Invite friends | `Summer.client.party.is_available()` |
| Party | `is_available()` and `is_in_party()`: `show_party_dialog()` opens leave, kick and leadership in the Summer app |
| Ready | a member in a party |

### 3. Share choices with party data

Party data is plain text that your game reads from every member's copy of the
party. Use it for lobby-style choices before the match starts:

* **Leader's choices** (map, mode): `party.set_data(key, value)`, read with `party.get_data(key)`. Only the leader can write it; a member gets `not_party_leader`.
* **Each member's own choices** (ready, character): `party.set_member_data(key, value)`, read with `member.get_data(key)`.

Limits:

| What | Limit |
| - | - |
| Keys | 1–32 characters from `a-z 0-9 _ . -` |
| Values | at most 256 UTF-8 bytes of text, no control characters; an empty value deletes the key |
| Leader keys | 16 per game |
| Member keys | 8 per member per game |

Values pass the Summer chat text filter, so `text_not_allowed` is possible.
Writes return `SummerOperation`. A conflict is retried once by the engine, then
fails with retryable `party_version_conflict`. React to `data_changed(key)` and
`member_data_changed(member, key)`, or just re-render on `changed`.

Party data is a lobby convenience, not game state. Once the match starts, the
authority owns everything (`multiplayer-state`). It never sees party data;
send choices that matter in your `join` Command and validate them there.

### 4. Handle join failures

`enter()` returns `false` with the code in the warning. For a party join:

| Code | Who | What to show |
| - | - | - |
| `party_member_offline` (retryable) | leader | "Everyone in your party needs the game open." `member.in_game` tells you who |
| `party_not_searching` (retryable) | member | Nothing; wait for the next `search_started` |
| `party_not_found` | anyone | The party ended; fall back to a solo Play |
| `capability_unavailable` | anyone | Parties aren't offered here; use the solo path |

A member can follow only while the leader's search is running: from
`search_started` until `search_ended`.

### 5. Test

Local Play cannot create a party, so `is_available()` is `false` and each
client joins solo from `_ready`. Test that path locally, then the party path
on Summer staging with real accounts.

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

## Checklist

* [ ] Local Play: `Summer.client.party.is_available()` is `false`, Play joins through the plain queue, and no script errors appear.
* [ ] Invite and Party buttons are hidden when `is_available()` is `false`.
* [ ] A member's Play button doesn't join on its own.
* [ ] Hosted: the leader presses Play, and every member with the game open lands in the same match.
* [ ] A member who closes the game makes the leader's Play fail with `party_member_offline`, and the message names who is missing.
* [ ] Ready and leader choices appear on every member's screen.

## Common mistakes

| Don't | Do | Why |
| - | - | - |
| Join in `_ready` while parties are available | Join from Play via `enter(target)` | A member joining alone splits the party |
| Members join `SummerJoinTarget.queue(...)` | Members join `party_queue(queue)` on `search_started` | Only the party target attaches them to the leader's search |
| Show Invite when `is_available()` is false | Hide it | Local Play, guests and under-13 accounts have no parties |
| Build your own invite list or party chat | `show_invite_dialog()` / `show_party_dialog()` | The Summer app owns invites, kicks, leadership and chat |
| Hold `SummerPartyMember` objects | Key UI by `member.user_id` and re-read on `changed` | Each snapshot replaces the member objects |
| Trust party data on the authority | Send choices in the `join` Command and validate | The authority never sees party data |
| Put secrets or long text in party data | Short public choices (`map`, `ready`) | 256 bytes, filtered, visible to every member |
| `await op.completed` | `await op.get_result_or_completed_signal()` | Fast failures complete before you can connect |

## See also

* `summer-matchmaking`: queues, sizes, teams and the accept prompt
* `summer-friends`: friends list, profiles and messages
* `multiplayer-project`: the client entry scene this extends
* `multiplayer-testing`: Local Play, and what needs staging

## 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-project`](/skills/multiplayer-project): Set up a Summer multiplayer project: entry scenes, World and queue files, joining, a first Local Play.
* [`multiplayer-testing`](/skills/multiplayer-testing): Test a Summer multiplayer game with Local Play: several players, bad network, bots, a passing run.
* [`summer-matchmaking`](/skills/summer-matchmaking): Get players into matches with Summer matchmaking: queues, teams, accept prompts, join progress and failures.
* [`summer-friends`](/skills/summer-friends): Summer friends in a game: a consented friends list, profile and chat screens, direct messages. Hosted only.
* [`summer-world-chat`](/skills/summer-world-chat): Add match chat to a Summer game: one World channel shared by everyone in the match. Hosted only.

***

Source: [`library/skills/summer-parties/SKILL.md`](https://github.com/SummerEngine/summer/blob/main/library/skills/summer-parties/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.