Learn SugarAdvanced

Sessions

Connect several JOIs and their Items through shared state that belongs to one chat.

A Session connects several JOIs and their linked Items inside one chat. It gives their actions shared state, while also keeping information that belongs to each participant separate.

Sessions are useful for games, collaborative stories, races, parties, encounters, or any experience where several Items need to affect the same ongoing activity.

A Session declaration defines the rules and starting state. It does not begin an activity by itself. Calling Session::start() creates one running Session instance in the current chat, with its own participants and state.

This lesson builds Moonlight Relay, a cooperative delivery race. Every player brings a courier Item. The team shares its current checkpoint and parcel total, while every courier has separate stamina.

Understand seats first

Every participant occupies a seat. A Seat value keeps the participant and their Session-specific state together:

PropertyMeaning
indexStable position in the Session, beginning at 1
joiThe Joi identity occupying the seat
hostThe linked Item that JOI brought
A name declared with seat varThat seat’s own value for the declaration

SELF is the seat belonging to the JOI currently acting. SEATS is the list containing one Seat value for every participant. Its exact Sugar type is List<Seat>, read “list of seats.”

If Lina brings a fox courier into seat 2, SELF.joi describes Lina, SELF.host describes the fox Item, and SELF.index is 2 while Lina acts.

Declare the Session

Sessions belong in App Main because every participating Item needs the same definition:

host mem var $stamina = 100;

embed relayStatus(checkpoint, parcels) -> "Moonlight Relay" {
  fields: (
    checkpoint -> "Checkpoint",
    parcels -> "Parcels delivered",
  ),
}

session moonlightRelay -> "Moonlight Relay" {
  config seats => 2..=4;

  var checkpoint = 1;
  var parcels = 0;

  seat var stamina <- $stamina;
  seat var deliveries = 0;

  view {
    show chip format("Stamina: {}", SELF.stamina) with progress => SELF.stamina;
    show embed relayStatus(checkpoint, parcels);
  }
}

Read the declaration from top to bottom:

  1. session moonlightRelay -> "Moonlight Relay" declares a Session named moonlightRelay and gives it the player-facing name Moonlight Relay. The internal name is what Sugar code uses; the display name is what the Session bar shows.
  2. config seats => 2..=4 allows two, three, or four occupied seats in each running instance.
  3. Plain var declarations create shared Session values. Every seat reads and changes the same checkpoint and parcels.
  4. seat var creates one value per seat. Each courier has separate stamina and deliveries.
  5. view chooses the Session information shown beside the chat composer.

The view is optional. It may show chips and App embeds, and it reads current state without changing it.

The display arrow is optional too. Without it, the Session bar shows the App name by itself instead of exposing the internal Session name.

Bind state to each Item

The bind arrow connects a seat value to hosted Memory:

seat var stamina <- $stamina;

When a courier joins, its saved $stamina becomes that seat’s stamina. Changes made through the Session are saved back to that same Item. Another courier in another seat keeps its own value.

A seat value without <-, such as deliveries, exists only for this Session.

Start and join

A linked Item action can open the lobby:

action openRelay -> "Open Moonlight Relay" {
  Session::start(moonlightRelay);
  text "{ACTOR.name} opens the Moonlight Relay lobby.";
}

This sends the action’s normal message and opens a lobby in the current chat. The JOI who opens it becomes the owner and occupies the first seat with the Item that ran the action. Other people join from the Session bar by choosing one of their Items linked to the same App. The owner starts play after the minimum number of seats is occupied.

With no second argument, Session::start(moonlightRelay) opens a public lobby that any JOI in the chat may join. Pass one Joi to invite only that JOI, or a List<Joi> to invite several:

action openPrivateRelay -> "Invite couriers" {
  var invitees: List<Joi> = () with {
    input: true,
    required: true,
    count: 1..=3,
  };

  Session::start(moonlightRelay, invitees);
  text "{ACTOR.name} opens a private Moonlight Relay lobby.";
}

Only those exact JOI identities can see and join an invited lobby. Every invitee must already belong to the chat, the owner cannot invite themself, and the owner plus the invitees must fit within the Session’s configured seat limit. Passing an explicit empty list creates a closed lobby; omitting the argument creates a public one.

Sessions only exist in chats. One chat and one host Item can each belong to one current Session. A JOI can participate in up to three Sessions, and a Session can contain up to six seats.

Session participation belongs to the active JOI identity. Hot-swapping to another JOI hides the first JOI’s Session and its Session actions. Swapping back restores them.

Add actions that use the shared state

Put use session inside an action that requires the Session:

action crossRooftops -> "Cross the rooftops" {
  use session moonlightRelay;

  let distance = Rand::pick(1..=3);
  SELF.stamina = clamp(SELF.stamina - 10, 0, 100);
  SELF.deliveries += distance;
  parcels += distance;

  show chip format("+{} parcels", distance);
  text "{SELF.joi.name} carries {distance} parcels across the rooftops for the team.";
}

This action becomes available only when its exact Item occupies a seat in an active moonlightRelay Session in that chat.

SELF.stamina changes only the acting courier’s seat. parcels changes the value shared by the entire Session. The next Session view shows both updates.

Use the same distinction whenever you design state:

  • Declare one ordinary var when there should be one value for the whole Session.
  • Declare seat var when every occupied seat needs its own value.
  • Bind a seat variable with <- when its changes should continue living on the host Item after the Session ends.

Find seats with closures

Because SEATS is a list of seats, it supports the methods from Working with lists:

let tiredCouriers: List<Seat> = SEATS.filter(|seat| seat.stamina < 30);
let nextCourier = SEATS.find(|seat| seat.index == checkpoint);
let courierNames = SEATS.map(|seat| seat.joi.name);

Code can select a seat directly with find(). Ask the player only when their choice is part of the experience.

Ask someone to choose seats

A normal input variable can present eligible seats:

var partner: Seat = None with {
  input: true,
  required: true,
  choices: SEATS.filter(|seat| seat != SELF),
};

For several seats, use List<Seat> and count:

var helpers: List<Seat> = () with {
  input: true,
  required: true,
  count: 1..=2,
  choices: SEATS.filter(|seat| seat != SELF && seat.stamina > 0),
};

choices decides which seats are available. count decides how many entries the resulting list may contain. Here the player must choose one or two eligible helpers.

This is different from min and max: those restrict the numeric value stored by a number variable. count restricts the number of entries stored by List<Joi> or List<Seat>.

Continue through a message

A Session action can attach an interaction to its result:

interaction recover(courier: Joi) when ACTOR.id == courier.id {
  SELF.stamina = clamp(SELF.stamina + 15, 0, 100);
  show toast "Stamina restored";
  "{ACTOR.name} catches their breath before the next checkpoint."
}

action findShortcut -> "Find a shortcut" {
  use session moonlightRelay;

  checkpoint += 1;
  show button recover(ACTOR) -> "Catch your breath" with maxUses => 1;
  text "{ACTOR.name} discovers a shortcut to checkpoint {checkpoint}.";
}

The button remains connected to the same Session. Only the JOI captured as courier can use it, and SELF resolves to that JOI’s current seat when the interaction runs.

Session information

SESSION describes the current Session:

  • SESSION.id: its unique ID
  • SESSION.name: its declaration name, such as "moonlightRelay"
  • SESSION.status: "LOBBY" before play begins or "ACTIVE" afterward

An unstarted lobby expires after one hour. An active Session expires after 24 hours without activity; new activity renews that time.

A non-owner can leave from the Session bar. The owner ends the Session instead. A Session action may also call Session::end(), which only the owner can complete.