Skip to content
Back to skills

Summer Parties

ASecurity

Let a Summer party play together: the leader presses Play, members follow into the same match. Hosted only.

  • 77 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
ai-agentsrustgoshellreactnodetesting

Works with

  • claude code
  • cursor
  • cli
  • mcp

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add SummerEngine/summer --skill summer-parties --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Summer Parties?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Summer Parties
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/summerengine-summer-parties/badge)](https://www.skillsdirectory.com/skills/summerengine-summer-parties)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: summer-parties
description: "Let a Summer party play together: the leader presses Play, members follow into the same match. Hosted only."
license: MIT
compatibility: [Cursor, Claude Code, Windsurf, Codex]
category: multiplayer-and-networking
user-invocable: true
allowed-tools: Read Grep Edit Write summer_get_script_errors summer_project_setting summer_play summer_stop summer_get_diagnostics
paths: ["**/*.gd", "summer.build.json"]
---

# /summer-parties — play together with a Summer party

## 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
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
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

<!-- shell-commands: generated from the operation catalog by npm run generate:registry; do not edit by hand -->
## 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` |
<!-- /shell-commands -->

Files in this skill

  • SKILL.md11.3 KB
  • resource.yaml888 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.