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

> All of the game events emitted by Sand Raiders of Sophie.

# Sand Raiders of Sophie

<Info>`overlayed.sand.readyForGameEvents()` must be called before any events will be emitted.</Info>

## Retained vs transient

**Retained** events are held by the module, which replays the latest copy to a client that connects
late — so you never miss one by subscribing after the fact. **Transient** events are only seen live.

Some retained events are stored per subject rather than one overall. Those have a **key**: a later
event with the same key replaces the earlier one. `party_member_joined` is keyed on `account_id`,
`player_joined` on `platform_id`, `extraction_added` on `extraction_id`, and the trampler events on
`trampler_id`.

Retention has two levels. **Menu**-level events are held for the life of the process. **Match**-level
events are all dropped at `match_ended`, so a new expedition starts from a clean slate.

| Event                     | Retained | Level | Key                                                 |
| ------------------------- | -------- | ----- | --------------------------------------------------- |
| `logged_in`               | yes      | menu  |                                                     |
| `game_state_changed`      | yes      | menu  |                                                     |
| `party_changed`           | yes      | menu  |                                                     |
| `party_member_joined`     | yes      | menu  | `account_id`                                        |
| `party_member_left`       | no       | menu  | releases `party_member_joined`                      |
| `inventory_changed`       | yes      | menu  |                                                     |
| `inventory_item_changed`  | no       | menu  |                                                     |
| `tech_tree_changed`       | yes      | menu  |                                                     |
| `tech_tree_item_unlocked` | no       | menu  |                                                     |
| `walker_changed`          | yes      | menu  |                                                     |
| `match_started`           | yes      | match |                                                     |
| `match_ended`             | no       | match | releases every match-level event                    |
| `world_time_changed`      | yes      | match |                                                     |
| `player_joined`           | yes      | match | `platform_id`                                       |
| `extraction_added`        | yes      | match | `extraction_id`                                     |
| `extraction_removed`      | no       | match | releases `extraction_added`                         |
| `storm_changed`           | yes      | match |                                                     |
| `trampler_acquired`       | yes      | match | `trampler_id`                                       |
| `trampler_changed`        | yes      | match | `trampler_id`                                       |
| `trampler_lost`           | no       | match | releases `trampler_acquired` and `trampler_changed` |

`module_loaded`, `module_unloaded`, and `unsupported_game_version` are emitted for Sand Raiders too,
but they arrive on the universal module — see [Universal](/games/universal-events).

Positions are world space with `y` as height, ready to use as given.

<Note>
  The `type.ts` block under each event below describes that event's **`content`** object. The event
  you receive wraps it: `{ game, type, creation_time, content }`. So a handler reads
  `event.content.state` and `event.creation_time`.
</Note>

```ts theme={null}
import type { StormChangedEvent } from "@overlayed/app/games/sand";

overlay.sand.on("storm_changed", (event: StormChangedEvent) => {
	event.content.state; // the fields documented below
	event.creation_time; // envelope fields
});
```

### logged\_in

Retained at menu level. Emitted once and held for the life of the process.

`account_id` is the same identity as `party_member_joined.account_id`. `platform_id` is a SteamID64
on the Steam build.

<Warning>
  `account_id` and `player_joined.player_id` are **different ids** and never match, even though both
  identify an account. Use `platform_id` to tie a logged-in account to an in-match player.
</Warning>

<CodeGroup>
  ```ts type.ts theme={null}
  interface LoggedInEvent {
  	account_id: string;
  	display_name: string;
  	platform_id: string;
  	playfab_id: string;
  }
  ```

  ```json example.json theme={null}
  {
  	"account_id": "1717066",
  	"display_name": "GhettoAmmo",
  	"platform_id": "76561198996870832",
  	"playfab_id": "A77C293DA30BEDDE"
  }
  ```
</CodeGroup>

### game\_state\_changed

Retained at menu level, replaced on each change.

Expedition status, or `menu` when there is no expedition.

<CodeGroup>
  ```ts type.ts theme={null}
  interface GameStateChangedEvent {
  	state:
  		| "menu"
  		| "waiting_for_players"
  		| "matchmaking"
  		| "match_found"
  		| "in_progress"
  		| "loot_division"
  		| "extracted"
  		| "all_left"
  		| "aborted"
  		| "failed"
  		| "decayed"
  		| "invalid";
  }
  ```

  ```json example.json theme={null}
  {
  	"state": "in_progress"
  }
  ```
</CodeGroup>

### party\_changed

Retained at menu level, replaced on each change and released when the expedition ends. There is no
separate disband event. The party survives into a match, so this is not menu-only in the sense of
being dropped at `match_started`.

<CodeGroup>
  ```ts type.ts theme={null}
  interface PartyChangedEvent {
  	join_code: string;
  }
  ```

  ```json example.json theme={null}
  {
  	"join_code": "K7QP2M"
  }
  ```
</CodeGroup>

### party\_member\_joined

Retained at menu level, keyed on `account_id`, released by `party_member_left`. One per party member,
**including the local player**. Re-emitted with the same `account_id` when a member's details change,
which replaces the retained copy.

Carries the same identity fields as `logged_in`, plus the member's selected character and whether
they lead the party.

<Warning>
  `display_name` can arrive **empty**, most often for a member who joins while you are in a match. It
  is filled in by a later `party_member_joined` for the same `account_id`, so do not treat the first
  event for a member as final. A name that has been resolved is never replaced by an empty one.
</Warning>

<CodeGroup>
  ```ts type.ts theme={null}
  interface PartyMemberJoinedEvent {
  	account_id: string;
  	display_name: string;
  	platform_id: string;
  	playfab_id: string;
  	character_id: number;
  	is_captain: boolean;
  }
  ```

  ```json example.json theme={null}
  {
  	"account_id": "1717066",
  	"display_name": "GhettoAmmo",
  	"platform_id": "76561198996870832",
  	"playfab_id": "A77C293DA30BEDDE",
  	"character_id": 88421,
  	"is_captain": true
  }
  ```
</CodeGroup>

### party\_member\_left

Not retained. Releases the retained `party_member_joined` with the same `account_id`.

When the whole party disbands, one of these is emitted per member before `party_changed` is released.

<CodeGroup>
  ```ts type.ts theme={null}
  interface PartyMemberLeftEvent {
  	account_id: string;
  }
  ```

  ```json example.json theme={null}
  {
  	"account_id": "1717066"
  }
  ```
</CodeGroup>

### inventory\_changed

Retained at menu level, replaced on every change. A full storage snapshot.

<Warning>
  `crowns`, `mechanical_parts`, `pneumatic_parts`, and `computing_modules` are **totals for items that
  also appear in `items`**, not separate balances. `crowns` equals the `item_coinCrown` entry's
  amount. Adding them to the `items` total double-counts.
</Warning>

Currencies are ordinary items — they appear in `items` under their own definition and occupy slots.

`slots_used` counts one slot per stack whatever its size, and **can exceed `slots_total`**. Stacks
sharing a definition are summed into a single `items` entry, so the entry count can be lower than
`slots_used`.

The snapshot can stay unchanged for a while after a match before the next update arrives. The last
snapshot is held rather than being reported as empty.

<CodeGroup>
  ```ts type.ts theme={null}
  interface InventoryChangedEvent {
  	slots_used: number;
  	slots_total: number;
  	crowns: number;
  	mechanical_parts: number;
  	pneumatic_parts: number;
  	computing_modules: number;
  	items: Array<{
  		definition: string;
  		amount: number;
  	}>;
  }
  ```

  ```json example.json theme={null}
  {
  	"slots_used": 12,
  	"slots_total": 40,
  	"crowns": 2500,
  	"mechanical_parts": 8,
  	"pneumatic_parts": 3,
  	"computing_modules": 1,
  	"items": [
  		{
  			"definition": "item_coinCrown",
  			"amount": 2500
  		},
  		{
  			"definition": "item_resourceMetal_t1",
  			"amount": 8
  		}
  	]
  }
  ```
</CodeGroup>

### inventory\_item\_changed

Not retained. One per definition whose total moved, emitted alongside the replacing
`inventory_changed`.

A definition whose last row is gone reports `amount` 0 with a negative `delta`. Currency movement
arrives under its item definition — crowns as `item_coinCrown` — never under the aggregate name.

<CodeGroup>
  ```ts type.ts theme={null}
  interface InventoryItemChangedEvent {
  	definition: string;
  	amount: number;
  	delta: number;
  }
  ```

  ```json example.json theme={null}
  {
  	"definition": "item_coinCrown",
  	"amount": 2500,
  	"delta": 500
  }
  ```
</CodeGroup>

### tech\_tree\_changed

Retained at menu level, replaced on every change. The full research tree state, never partial.

<Warning>
  `is_available` means **researchable right now** — requirements met and not yet bought. It is not
  "usable": an already-unlocked node reports `is_available` false, because there is nothing left to
  research on it. Treat `is_unlocked` and `is_available` as a state pair, not as independent flags.
</Warning>

`unlocked` counts the nodes with `is_unlocked` set, `total` is the node count in the tree, and
`nodes` is sorted by `id`. `tier` is depth in the tree, 0-based.

<CodeGroup>
  ```ts type.ts theme={null}
  interface TechTreeChangedEvent {
  	unlocked: number;
  	total: number;
  	nodes: Array<{
  		id: string;
  		name: string;
  		tier: number;
  		is_unlocked: boolean;
  		is_available: boolean;
  	}>;
  }
  ```

  ```json example.json theme={null}
  {
  	"unlocked": 2,
  	"total": 3,
  	"nodes": [
  		{
  			"id": "8f14e45f-ceea-467a-9575-0d1e1b0b2c33",
  			"name": "Cannon T4",
  			"tier": 3,
  			"is_unlocked": true,
  			"is_available": false
  		},
  		{
  			"id": "c4ca4238-a0b9-2382-0dcc-509a6f75849b",
  			"name": "Hull T2",
  			"tier": 1,
  			"is_unlocked": false,
  			"is_available": true
  		}
  	]
  }
  ```
</CodeGroup>

### tech\_tree\_item\_unlocked

Not retained. One per node that flipped from locked to unlocked, emitted alongside the replacing
`tech_tree_changed`.

Availability changes are **not** broadcast — a node becoming researchable is only visible in the
retained `tech_tree_changed` state.

<CodeGroup>
  ```ts type.ts theme={null}
  interface TechTreeItemUnlockedEvent {
  	id: string;
  	name: string;
  	tier: number;
  }
  ```

  ```json example.json theme={null}
  {
  	"id": "8f14e45f-ceea-467a-9575-0d1e1b0b2c33",
  	"name": "Cannon T4",
  	"tier": 3
  }
  ```
</CodeGroup>

### walker\_changed

Retained at menu level, replaced on each change. The trampler the account has built.

It is **not** updated during a match, but the retained copy carries across it, so the walker taken
into a match is still readable while the match runs.

`walker_id` is the same id `trampler_acquired.master_server_id` reports in a match, which is how the
two are matched up.

<Note>
  `blueprint_file` is a saved blueprint's file name, without its extension. It is **empty** when the
  walker was not built from a saved blueprint — the stock expedition walker, for instance — and is
  unrelated to `blueprint_unique_id`.
</Note>

`first_name_index` and `second_name_index` are opaque name indices, meaningful only as a pair. `0`,
`0` means the walker is unnamed.

<CodeGroup>
  ```ts type.ts theme={null}
  interface WalkerChangedEvent {
  	walker_id: number;
  	blueprint_unique_id: string;
  	blueprint_file: string;
  	first_name_index: number;
  	second_name_index: number;
  }
  ```

  ```json example.json theme={null}
  {
  	"walker_id": 4471,
  	"blueprint_unique_id": "b1d9e0f2-3c44-4a8e-9f21-77c0a5d6e881",
  	"blueprint_file": "Dune Runner",
  	"first_name_index": 12,
  	"second_name_index": 3
  }
  ```
</CodeGroup>

### match\_started

Retained at match level until `match_ended`.

`world_bounds` is the playable extent on the xz plane — use it to map world coordinates onto a
rendered map. `landmarks` are the named islands and forts, the places drawn on the in-game map.

`server_start_time` is Unix milliseconds and marks when the match began, not when you joined it. In
Voyage it can be days in the past.

<CodeGroup>
  ```ts type.ts theme={null}
  interface MatchStartedEvent {
  	match_id: number;
  	game_mode: "storm_dive" | "voyage" | "invalid";
  	seed: number;
  	server_start_time: number;
  	world_bounds: {
  		min_x: number;
  		min_z: number;
  		max_x: number;
  		max_z: number;
  	};
  	landmarks: Array<{
  		id: string;
  		name: string;
  		position: {
  			x: number;
  			y: number;
  			z: number;
  		};
  	}>;
  }
  ```

  ```json example.json theme={null}
  {
  	"match_id": 9001,
  	"game_mode": "storm_dive",
  	"seed": 12345,
  	"server_start_time": 1700000000000,
  	"world_bounds": {
  		"min_x": -4096,
  		"min_z": -4096,
  		"max_x": 4096,
  		"max_z": 4096
  	},
  	"landmarks": [
  		{
  			"id": "fort_alpha",
  			"name": "Fort Alpha",
  			"position": {
  				"x": 1024.5,
  				"y": 62,
  				"z": -337.25
  			}
  		}
  	]
  }
  ```
</CodeGroup>

### match\_ended

Not retained. The game state was left — every retained match-level event is dropped with it.

<CodeGroup>
  ```ts type.ts theme={null}
  interface MatchEndedEvent {
  }
  ```

  ```json example.json theme={null}
  {}
  ```
</CodeGroup>

### world\_time\_changed

Retained at match level, replaced on each change.

Fires when the in-game hour rolls over.

<CodeGroup>
  ```ts type.ts theme={null}
  interface WorldTimeChangedEvent {
  	hour: number;
  }
  ```

  ```json example.json theme={null}
  {
  	"hour": 13
  }
  ```
</CodeGroup>

### player\_joined

Retained at match level, keyed on `platform_id`. One per player on the in-game player list, which
keeps players after they disconnect. Re-emitted to replace the retained copy if their details change.

There is **no counterpart leave event** — a player who drops stays on the list.

`player_id` is the id `trampler_acquired.creator_account_id` also reports, so the two can be matched
directly. It is **not** `logged_in.account_id` and will never equal it; match on `platform_id` to tie
a player to the logged-in account.

<CodeGroup>
  ```ts type.ts theme={null}
  interface PlayerJoinedEvent {
  	player_id: number;
  	username: string;
  	platform_id: string;
  	playfab_id: string;
  }
  ```

  ```json example.json theme={null}
  {
  	"player_id": 1717066,
  	"username": "GhettoAmmo",
  	"platform_id": "76561198996870832",
  	"playfab_id": "A77C293DA30BEDDE"
  }
  ```
</CodeGroup>

### extraction\_added

Retained at match level, keyed on `extraction_id`, released by `extraction_removed`. In Storm Dive
these arrive through the match rather than all at once, so do not expect the full set at
`match_started`.

`radius` is the extraction zone radius in world units.

<CodeGroup>
  ```ts type.ts theme={null}
  interface ExtractionAddedEvent {
  	extraction_id: number;
  	radius: number;
  	position: {
  		x: number;
  		y: number;
  		z: number;
  	};
  }
  ```

  ```json example.json theme={null}
  {
  	"extraction_id": 3,
  	"radius": 25.5,
  	"position": {
  		"x": 1024.5,
  		"y": 62,
  		"z": -337.25
  	}
  }
  ```
</CodeGroup>

### extraction\_removed

Not retained. Releases the retained `extraction_added` with the same `extraction_id`.

<CodeGroup>
  ```ts type.ts theme={null}
  interface ExtractionRemovedEvent {
  	extraction_id: number;
  }
  ```

  ```json example.json theme={null}
  {
  	"extraction_id": 3
  }
  ```
</CodeGroup>

### storm\_changed

Retained at match level, replaced on each change. Storm Dive only.

`delay` is the pause before the circle starts closing; `moving` is the close itself.

`storm_changed` fires on stage and state transitions only, so the live circle is not streamed to you.
To draw a closing circle, interpolate from `center`/`radius` toward
`next_center`/`next_radius` over the time remaining until `end_time`. At the next event, `center` and
`radius` equal the previous `next_center` and `next_radius`.

```ts theme={null}
import type { StormChangedEvent } from "@overlayed/app/games/sand";

function stormAt(event: StormChangedEvent, now: number) {
	const { content } = event;

	if (content.state !== "moving") {
		return { center: content.center, radius: content.radius };
	}

	const remaining = content.end_time - now;
	const total = content.end_time - event.creation_time;
	const progress = total <= 0 ? 1 : Math.min(1, Math.max(0, 1 - remaining / total));

	return {
		center: {
			x: content.center.x + (content.next_center.x - content.center.x) * progress,
			y: content.center.y + (content.next_center.y - content.center.y) * progress,
			z: content.center.z + (content.next_center.z - content.center.z) * progress,
		},
		radius: content.radius + (content.next_radius - content.radius) * progress,
	};
}
```

<CodeGroup>
  ```ts type.ts theme={null}
  interface StormChangedEvent {
  	stage: number;
  	total_stages: number;
  	state: "idle" | "delay" | "moving" | "invalid";
  	end_time: number;
  	radius: number;
  	center: {
  		x: number;
  		y: number;
  		z: number;
  	};
  	next_radius: number;
  	next_center: {
  		x: number;
  		y: number;
  		z: number;
  	};
  }
  ```

  ```json example.json theme={null}
  {
  	"stage": 1,
  	"total_stages": 6,
  	"state": "moving",
  	"end_time": 1700000060000,
  	"radius": 500,
  	"center": {
  		"x": 0,
  		"y": 62,
  		"z": 0
  	},
  	"next_radius": 250,
  	"next_center": {
  		"x": 128.5,
  		"y": 62,
  		"z": -64.25
  	}
  }
  ```
</CodeGroup>

### trampler\_acquired

Retained at match level, keyed on `trampler_id`, released by `trampler_lost`. One per trampler the
account owns — the one you started the match with, and any captured later.

This event is identity only. The trampler's position arrives on `trampler_changed`, emitted at the
same moment.

`trampler_id` is scoped to the match and means nothing outside it. `master_server_id` is stable
across matches, and is **`0`** when the trampler has no such id. When it is non-zero it matches
`walker_changed.walker_id`, which is how you tell that the trampler you are flying is the walker you
built:

```ts theme={null}
import type { TramplerAcquiredEvent, WalkerChangedEvent } from "@overlayed/app/games/sand";

function isFlownWalker(walker: WalkerChangedEvent, trampler: TramplerAcquiredEvent) {
	return trampler.content.master_server_id !== 0 && trampler.content.master_server_id === walker.content.walker_id;
}
```

<Warning>
  `creator_account_id` is who **authored the blueprint**, not who is driving it. On a captured
  trampler it names the other player. It matches `player_joined.player_id`, not
  `logged_in.account_id`.
</Warning>

`blueprint` is the blueprint name — `EXPEDITION_WALKER` for the stock walker.

<CodeGroup>
  ```ts type.ts theme={null}
  interface TramplerAcquiredEvent {
  	trampler_id: number;
  	master_server_id: number;
  	blueprint: string;
  	creator_account_id: number;
  	first_name_index: number;
  	second_name_index: number;
  }
  ```

  ```json example.json theme={null}
  {
  	"trampler_id": 12,
  	"master_server_id": 4471,
  	"blueprint": "EXPEDITION_WALKER",
  	"creator_account_id": 1717066,
  	"first_name_index": 12,
  	"second_name_index": 3
  }
  ```
</CodeGroup>

### trampler\_changed

Retained at match level, keyed on `trampler_id`, released by `trampler_lost`.

Emitted on acquisition and then about **every 10 seconds** for as long as the trampler is owned,
whether or not it moved. It is a heartbeat, not a movement feed — do not treat a new event as proof
of motion, or the gap between two as a movement duration.

`yaw` is degrees about the up axis. `speed` is the magnitude of `velocity`, supplied so you do not
have to compute it.

<CodeGroup>
  ```ts type.ts theme={null}
  interface TramplerChangedEvent {
  	trampler_id: number;
  	position: {
  		x: number;
  		y: number;
  		z: number;
  	};
  	yaw: number;
  	speed: number;
  	velocity: {
  		x: number;
  		y: number;
  		z: number;
  	};
  }
  ```

  ```json example.json theme={null}
  {
  	"trampler_id": 12,
  	"position": {
  		"x": 1024.5,
  		"y": 62,
  		"z": -337.25
  	},
  	"yaw": 137.5,
  	"speed": 8.25,
  	"velocity": {
  		"x": 8.25,
  		"y": 0,
  		"z": 0
  	}
  }
  ```
</CodeGroup>

### trampler\_lost

Not retained. Releases both `trampler_acquired` and `trampler_changed` for the same `trampler_id`.

This arrives promptly rather than waiting for the next `trampler_changed`, so a trampler can be
treated as gone as soon as you see it.

<CodeGroup>
  ```ts type.ts theme={null}
  interface TramplerLostEvent {
  	trampler_id: number;
  }
  ```

  ```json example.json theme={null}
  {
  	"trampler_id": 12
  }
  ```
</CodeGroup>
