Activities and destinations
The activity and destination tables, how the client loads a level and picks a spawn point, how a mission is authored, public regions and waypoints.
An activity is one playable thing: a mission, a patrol zone, the Tower, a raid. A destination is the place it happens. The game describes both in package tables. It describes the level itself in a separate scenario tag, split into bubbles.
Field and function names are descriptive, not Bungie’s, unless a section says a name was shipped.
- The words activity, scenario, bubble, state, region, object and slot are defined in The world model.
- Slot types and the Auth and Sense bodies: The Activity Host protocol.
- The tables’ container, the investment root blob: Items, characters and inventory.
- Package storage and named tags: Packages.
The catalog
Four catalogs describe what exists. Each one lives under the investment root blob. The first three also have a display half with the strings, with the same rows in the same order.
catalog
rows
row form
activities
1170
16-byte index rows to variable-length records
activity types
54
inline rows; 16 bytes in the logic half, 128 in the display half
destinations
48
inline rows of 56 bytes
places
27
inline u32 definition hashes
A row index is the id the rest of the game uses. The objects and messages carry the index, not the hash.
The activity record
The logic half of an activity record holds these fields:
struct ActivityRecord { /* variable length, at least 320 bytes */
uint32_t definitionHash; /* +0x000 */
uint8_t unmapped0[4];
uint64_t requirementGroupCount; /* +0x008 */
int64_t requirementGroups; /* +0x010 relative; u32 group hashes */
uint8_t unmapped1[80];
int64_t internalName; /* +0x068 relative; for example "raid_gluttony_0" */
uint8_t unmapped2[8];
uint64_t flagWriteCount; /* +0x078 */
int64_t flagWrites; /* +0x080 relative; {i16 slot, u8 value} rows */
uint8_t unmapped3[24];
uint8_t releaseRows[16]; /* +0x0A0 release-band gates; reader unknown */
int32_t requiredLevel; /* +0x0B0 */
int32_t requiredPower; /* +0x0B4 */
int32_t secondTierLevel; /* +0x0B8 */
int32_t secondTierPower; /* +0x0BC */
uint8_t unmapped4[8];
uint64_t onwardLinkCount; /* +0x0C8 */
int64_t onwardLinks; /* +0x0D0 relative; 32-byte rows */
uint8_t unmapped5[2];
uint8_t activityType; /* +0x0DA row of the activity type table */
uint8_t unmapped6[5];
uint8_t destination; /* +0x0E0 row of the destination table */
};
Notes:
- The destination link is one byte at
+0x0E0, not a 16-bit field. - Power follows the item power curve:
requiredPower = max(requiredLevel * 10, 750). It holds on all 700 rows that set a requirement. So an activity’s power compares directly with a character’s. - Type 7 is
Raidand type 46 isDungeon. The activity rows and the type table agree. - There is no fireteam size field. The size is a display string on the activity type row.
- Entering an activity writes unlock flags. Every value in the write list is 2, which is “true”.
For example, Leviathan is activity 565, internal name raid_gluttony_0, type 7, destination 11. Its variants need power 750.
The activity type and its kind byte
The display-half type row is 128 bytes. Its signed byte at +120 is a kind. The client maps it to an action mode:
kind
action mode
1
2
3
1
anything else
0
Kind 1 covers one type, Social: the Tower and a few social spaces.
Only one type row has kind 0. It is the type of activity 0, orbit. The client uses that to ask “is this an orbit activity”.
Destinations and places
struct DestinationRow { /* 56 bytes, display half */
uint32_t definitionHash; /* +0x00 */
uint32_t nameBank; /* +0x04 string ref: name */
uint32_t nameHash; /* +0x08 */
uint32_t subtitleBank; /* +0x0C string ref: subtitle */
uint32_t subtitleHash; /* +0x10 */
uint8_t unmapped[20]; /* +0x14 */
uint64_t bubbleCount; /* +0x28 */
int64_t bubbles; /* +0x30 relative; 24-byte elements */
};
In the logic half the same row has the place index at +0x04, a u32 into the place table. The place table is 27 bare definition hashes. No display half is known for it.
The destination row lists bubbles, but this list is not what the world loads. The world loads the bubbles named by the activity’s scenario tag. For the Tower the two lists share only 4 of 8 hashes, in a different order.
The start-destination table
At sign-in the client picks where to go first from an 8-row table. Each row holds an activity index and an optional unlock expression. The client takes the first row with no expression or a passing one.
The last row has no expression and holds activity 0, orbit. So the pick always ends, and it ends on orbit unless an earlier row passes. The Tower row passes when two per-character flags are both true. A row before it passes when only the first is true, so the two flags move together.
Gates on an activity
Several gates exist. None of them blocks a launch in this build.
gate
where
what it does
onward links
activity +0x0C8
the launch check; all 457 shipped rows have an empty expression
requirement groups
29 rows, named by hash from activity +0x008
pick the “why is this locked” message
release rows
activity +0x0A0
lead with a release-band flag; reader unknown
type strings
activity type row
display text only
The requirement-group selector returns a message index, not a verdict. -1 means nothing failed. All callers are UI components. A content bypass in the account objects can short-circuit it to “nothing failed” before any expression runs.
The 29 requirement groups are mostly flag pairs. The NOT FLAG(19xx) pairs are pre-release locks that read false on a clean account. One group per expansion asks for full-version and expansion ownership; group 3D1F1FF1 is Shadowkeep. Value tests also appear: group E4EC2422 asks for character power of at least 799, group CB3928C9 gates The Menagerie on a value slot. The message each group selects is described on Collections, vendors and other catalogs.
The release rows are 246 unlock expressions over 138 activities. Every one leads with a release-band flag, slots 16840 to 16892. None of those slots has a backing byte in any object, so only the account’s override list can set them. What a release row gates is unverified, because its reader is not traced. Three raid and dungeon rows add a per-character NOT FLAG: Pit of Heresy, Prophecy and Garden of Salvation. That shape is assumed to be a completion lockout.
The fireteam size is a requirement line on the activity type row: a string pair plus an expression that decides whether the line shows. Raid line 0 reads Fireteam: 6 Players, Dungeon line 0 Fireteam: 1-3 Players. No numeric size exists in the catalog. Weekly lockout is not in the catalog either; it is server state.
The expression format is on Items, characters and inventory.
From activity to level data
The activity root
Each activity has a root tag of class 0x80808AAE, exactly 0x48 bytes. The install holds 468. Two fields matter:
field
target
+0x40
the scenario tag, class 0x80809994
+0x44
a 0x30-byte tag, class 0x80809BA3; a transition descriptor, contents not decoded
The client also finds tags by name. It builds names from the activity’s internal name and a fixed suffix list, then looks the name up as a named tag.
suffix
use
none
the activity root
:scenario_client
the scenario the client loads
:scenario_fah, :scenario_gah, :globals_host, :globals_client
other scenario and globals tags
:spaceflight_in, _filler, _out
travel sequences
:same_dest_in, _filler, _out
travel inside one destination
:no_ship_in, _filler, _out
travel without the ship
:root
built as map:<name>:root, the map root, not <activity>:root
For example, city_tower_social_d2:scenario_client is the Tower’s scenario tag.
Not every suffix ships. Which ones do is on Packages.
Bubbles in the scenario
The scenario tag holds the bubble list the world uses:
struct ScenarioBubbleTable { /* inside the scenario tag */
uint8_t header[80];
uint64_t bubbleCount; /* +0x50 0 to 64 */
int64_t bubbles; /* +0x58 relative; 24-byte elements */
};
struct ScenarioBubble { /* 24 bytes, class 0x8080924D */
uint32_t bubbleHash; /* +0x00 */
uint8_t pad[4];
uint64_t stateCount; /* +0x08 0 means the bubble has no state 0 */
int64_t states; /* +0x10 relative; 76-byte records */
};
struct SliceSetState { /* 76 bytes, class 0x8080924F */
uint8_t enabled; /* +0x00 */
uint8_t pad0[3];
uint32_t stateNameHash; /* +0x04 */
uint8_t unmapped[20];
uint32_t mapBubbleIndex; /* +0x1C the bubble's number on its map */
uint8_t flags[32]; /* +0x20 ORed into the world object */
uint32_t ownerBubbleHash; /* +0x40 */
uint32_t sliceSetTag; /* +0x44 */
uint8_t pad1[3];
uint8_t skipFlags; /* +0x4B 0 applies the flag block */
};
A bubble’s states each take one slice set. A bubble owns a run of 8 slice-set indices, so its first state’s index is bubble * 8. Sunrise has the same rule in src/middleware/content/packages/tables/region_reader.h:
constexpr uint32_t region_index(uint32_t bubbleIndex) {
return bubbleIndex * kSliceSetIndexFactor; /* kSliceSetIndexFactor == 8 */
}
When the activity starts, the server sends one state byte per bubble. Only two values are legal:
byte
effect
0
registers the bubble’s first state and adds the bubble
-1
skips the bubble
The client refuses any other value with a slice-set state error. It also refuses 0 when the bubble has no states. So the rule per bubble is state = (stateCount > 0) ? 0 : -1. That is also the client’s own default. For the Tower the eight values are 0 0 0 0 -1 -1 0 0.
Map bubbles and containers
A bubble also has a number on its map, separate from its ordinal in the scenario. Two activities on one map can give the same bubble different ordinals. The slice-set state record carries the map number at +0x1C.
A map root tag lists one record per bubble. It names them by 64-bit name hash, not by tag, so a walk must resolve them through the name hash table. Each record names a list of containers. A container carries a 32-byte bit mask, one bit per map bubble, and a list of member tags:
struct Container { /* header, class 0x80808A54 */
uint64_t size; /* +0x00 */
uint8_t bubbleMask[32]; /* +0x08 one bit per map bubble index */
uint64_t memberCount; /* +0x28 */
int64_t members; /* +0x30 relative; bare tag handles */
};
The mask decides in which bubbles the container’s members load. A mission’s bubble loads every map container whose mask meets the bubble row’s own 256-bit mask. That bubble mask also covers the shared and environment cells, so the atmosphere and time-of-day layers load with the mission.
The client code that tests a container’s own mask is not identified, so the spawn binding below is unverified in code. Two tag classes agree on the numbering in every map checked.
Spawn selection
Spawn points and spawn sets
Player spawn points live in package tags of class 0x80809162. The install holds 386 such tags and 8892 points.
struct SpawnPoint { /* 48 bytes */
float rotation[4]; /* +0x00 */
float position[4]; /* +0x10 */
uint32_t setNameHash; /* +0x20 FNV-1 of the lowercase set name */
uint8_t reserved[12]; /* +0x24 zero in every point */
};
Points group into named spawn sets. Two names matter:
name
hash
default
0x2EA8FB98
none
0x2CA33BDB
A spawn set is wrapped by a component, and the component is a member of a container. So the container’s bubble mask decides which bubbles offer the set. When a component loads, it registers its set with the spawn-point manager. The manager keeps a flat list. It never asks which bubble a set belongs to: the list is whatever loaded.
A set also attaches only when the package that holds it is one the destination loads. The mask and the package are two separate tests.
Picking a point
The spawn filter is one hash. The server’s move message and the activity start message carry it. The client replaces an empty or none hash with default.
void set_spawn_filter(SpawnPointMgr *mgr, uint32_t hash)
{
if (hash == 0x811C9DC5 || hash == HASH_NONE)
hash = HASH_DEFAULT;
mgr->filter = hash;
}
const SpawnPoint *pick_spawn_point(const SpawnPointMgr *mgr)
{
static const int categories[] = { 0, 3, 4, 5 };
for (int c = 0; c < 4; c++) {
for (const SpawnPoint *p = first_point(mgr, categories[c]); p; p = next_point(mgr, p)) {
switch (categories[c]) {
case 0: if (has_type67_reference(p)) return p; break;
case 3: if (p->setNameHash == mgr->filter) return p; break;
case 4: if (p->setNameHash == mgr->filter ||
(mgr->filter == HASH_DEFAULT && p->setNameHash == 0x811C9DC5))
return p;
break;
case 5: return p; /* accepts anything */
}
}
}
return NULL;
}
This is why a wrong set fails silently. If no point matches, category 5 hands back the first point that loaded, and the player lands in the wrong place. There is no error.
Maps differ in whether they declare a set named default:
content
declare default
campaign missions
64 of 65
adventures
all
social spaces
all
raids
1 of 11
Crucible maps
1 of 30
So a raid needs an explicit set. For free-roam destinations the client picks the arrival bubble and set itself. For raids and dungeons it sends the unset hash, and the server’s start message decides.
Example: the Leviathan entrance
The Leviathan entrance uses bubble 2 and spawn set 0x8029E4B4.
- Bubble ordinal 2 is map bubble 13.
- The activity package holds a copy of set
0x8029E4B4whose container carries bit 13. - The loaded slice set is 16, whose package name is
raid_gluttony_berth.
A set that is valid package data but not bound to bubble 2 places the player elsewhere. The client then requests the gardens slice set, not the entrance.
Sunrise applies the same two tests in src/state/activity/destination/activity_destination_spawn_binding.cpp. It uses the bubble mask, through the map-index table, to pick a bubble that offers the set (bounds checks removed here):
const std::size_t mapIndex = layout.bubbleMapIndices[bubble];
return (row.bubbleMask[mapIndex / 8] >> (mapIndex % 8) & 1U) != 0;
It also drops a set whose package the destination does not load, and sends the unset hash instead. A set named by an explicit override skips this package test.
How a campaign mission is authored
A campaign mission is a chain of small objects, one per objective step. Each step object’s slot table names the directive it shows, the dialogue cue it plays, the music it selects, the volume that ends it, and the volumes its line waits in. Reading those tables gives the step order and the presentation.
The rule that connects one step to the next is not in the packages. The client ships every part a mission needs, each named, and no wiring between them. The host supplies the order.
The object and its slot table
Every scenario object is a package tag of class 0x80809462.
struct ScenarioObject { /* header */
uint8_t unmapped0[8];
uint32_t mapNameHash; /* +0x08 FNV-1 of the map name; 0x29930BA4 is "edz" */
uint32_t registryKey; /* +0x0C the key every slot reference uses */
uint8_t unmapped1[16];
uint64_t slotCount; /* +0x20 field width unverified */
int64_t slotHeader; /* +0x28 relative, from +0x28, to a 16-byte header */
};
struct SlotRow { /* 8 bytes, at slotHeader + 16 */
uint32_t slotType;
uint32_t nameHash;
};
A row’s position is the slot index. For an ordinary slot the name hash is FNV-1 of the slot’s development name:
fnv1("_directive_trigger") == 0x2162521A;
fnv1("_nav_point") == 0x3769A1A0;
So a guessed name is checked by hashing it. A name that hashes right is exact.
Proxy slots
Three slot types have no descriptor. Their name-hash word holds a reference instead.
type
engine name
the word holds
69
directive_proxy
a directive hash in the scenario’s directive table
54
dialog_event_proxy
a cue hash in the scenario’s dialogue table
12
music_section_proxy
a music section hash
No client code that reads proxy slots is known. So proxy slots are authoring data, and the host acts on them.
Volumes
A step object usually carries several trigger volumes (type 60). They have three roles:
- Trigger volume. The step’s player-trigger slot (type 31) names it. It reports when the player enters.
- Filter volume. The step’s dialogue line waits in it. The host sends the cue with this volume as its filter, and the client plays the line when the player walks in.
- Waypoint volume. The host sends it with the directive. The client hides the step’s marker while the player is inside.
The dialogue table
The dialogue slot (type 53) names a table of class 0x80808D54.
- The cue array has rows of
{u32 cue hash, float}. A row’s position is the cue index that a dialogue body plays. - Each line is a 72-byte record, in cue order:
struct DialogueLine { /* 72 bytes, class 0x80808D23 */
uint8_t unmapped0[8];
uint32_t cueHash; /* +0x08 */
uint8_t unmapped1[8];
float pauseBefore; /* +0x14 unverified */
uint8_t unmapped2[8];
uint32_t stringContainer; /* +0x20 localized string container */
uint32_t stringHash; /* +0x24 */
float duration; /* +0x28 matches the spoken length; unverified */
uint8_t unmapped3[20];
uint32_t speakerHash; /* +0x40 */
uint8_t unmapped4[4];
};
The directive slot (type 68) names a directive table the same way. Both tables hold string references, so the mission’s text is package data.
Walking a step chain
The start and end rules below are assumed. They match the behavior of one mission.
/* Steps are read in the order of their directives' text. */
void run_step_chain(Host *host, const Step *steps, int count)
{
for (int i = 0; i < count; i++) {
const Step *s = &steps[i];
/* On start: objective text, marker and waypoint, then the line in its filter volume. */
send_directive(host, s->directive, s->navPoint, s->waypointVolume);
if (s->hasCue)
send_dialogue_cue(host, s->cue, s->filterVolume);
if (s->trigger)
arm_trigger(host, s->trigger);
/* A step ends when its trigger fires, or when its fight is won. A fight is skippable
unless a barrier holds the player, so a later trigger also ends it. */
wait_until(host, trigger_fired(s->trigger) ||
fight_won(s) ||
any_later_trigger_fired(steps, i + 1, count));
}
clear_directives(host); /* removes the last marker */
}
Two rules for the host:
- A marker needs its object registered on the client. Arming the step’s trigger registers it.
- A trigger fires once. To reuse it, the host arms it again with a newer generation.
Not authored in the step objects: the order of steps that share a directive, cues no step names, fights and spawns, and the choice between two filter volumes on one object.
Example: A Deadly Trial
The campaign mission A Deadly Trial is scenario 0x80B2E043, adventure_ginger. It has ten step objects. Three of them:
step
object
directive
cue
trigger
town
0x80B2ECF3
0xEC217779
0
slot 7, the town exit
roadblock
0x80B2E6DB
0x6FB8A85A
–
none; ends when the Walker tank is destroyed
revive
0x80B2EC6C
0x708B9351
7, 8
none; ends on a Ghost scan
Some cues are named by no step object. Two of them play from a device’s own scene.
Example: the Leviathan berth
The Leviathan berth, the raid’s first area, shows the same pattern at a larger scale. Its bubble has two states:
state
hash
what it holds
ordinal 0
0xD1B16771
the playable world
ordinal 1
0xE95965DF
the opening cinematic; no squads, no devices
Every observable in the berth maps to a named slot: squads, doors, levers, the directive, dialogue and music. The order of the lever puzzle is not on the client. Only 27 slot-to-slot references exist in all of Leviathan, and 26 of them point at a trigger volume. That count covers only references to trigger volumes, so other edges may exist. The behavior programs and slot types also carry no mission state.
The extra cutscene state is a pattern across the install. All 90 extra state rows carry the same marker flags, and 88 of them hold a cinematic slot. The flags’ reader is unknown, so the marker’s meaning is unverified.
The Director’s activity graphs
The Director (the map screen) is built from activity graphs. The catalog holds 26 graphs.
struct ActivityGraph { /* 152 bytes fixed, class 0x80805E79 */
uint32_t blobLength; /* +0x00 */
uint8_t pad0[4];
uint32_t artKey; /* +0x08 shared by graphs of one destination */
uint8_t name[8]; /* +0x0C string ref */
uint8_t subtitle[8]; /* +0x14 string ref */
uint8_t pad1[4];
uint8_t displayProgressions[16]; /* +0x20 */
uint8_t displayObjectives[16]; /* +0x30 */
uint8_t linkedGraphs[16]; /* +0x40 64-byte rows: the tiles */
uint8_t nodes[16]; /* +0x50 80-byte rows: activity nodes */
uint8_t connections[16]; /* +0x60 */
uint8_t artElements[16]; /* +0x70 */
uint8_t pad2[16];
uint32_t floatBlock; /* +0x90 tag of a float block */
uint8_t pad3[4];
};
The member names display_progressions, display_objectives, linked_graphs, nodes, connections and art_elements are Bungie’s. They match the FNV-1 hashes in the data.
- A
nodesrow holds the activities a destination’s own map page offers. - A
linked_graphsrow is one tile on a page. Only the top-level solar-system page has a full list, 15 tiles.
How a tile picks its target
A tile row holds a list of candidate targets. Each candidate names a graph or a node, and carries an unlock expression. The tile takes the first candidate that passes.
struct TileCandidate { /* 24 bytes */
uint8_t graphIndex; /* +0x00 0xFF means no graph */
uint8_t pad;
uint16_t nodeIndex; /* +0x02 0xFFFF means no node */
uint8_t pad2[4];
uint8_t unlockExpression[16]; /* +0x08 */
};
const TileCandidate *pick_tile_target(const EvalState *s, const TileRow *row)
{
for (uint64_t i = 0; i < row->candidateCount; i++) {
const TileCandidate *c = &row->candidates[i];
if (c->graphIndex == 0xFF && c->nodeIndex == 0xFFFF)
continue;
if (unlock_eval(s, (const ExprRecord *)c->unlockExpression))
return c; /* a graph opens a map page; a node points at one activity */
}
return NULL;
}
Most tiles have one ungated candidate: the destination’s map page. Two tiles carry two:
- The Moon’s first candidate points at its intro mission while a campaign flag is false. The second is the map page, taken once the flag is true.
- Io has the same two-candidate shape. Its first candidate points at the intro mission and passes only while two flags are set. With those flags false it takes the map page.
The tile row also has its own display expression, which decides whether the tile is drawn at all. Seven patrol tiles share one account flag in that expression, slot 1063, beside a second flag, slot 1087, read under NOT. Their names are unknown. Slot 1063 also feeds map-node placements and some vendor rows, so setting it has effects beyond the tiles. The display expression only sets one bit of the tile. Whether the tile is present at all depends on its graph and row resolving.
Public and private regions
A destination is split into regions. A private region belongs to one fireteam. A public region, such as a patrol space or a strike, is shared with players the matchmaker finds. Whether a region is public is package content, read when a slice-set transition starts.
The transition stages
A slice-set transition runs as a bit mask of eight stages. The stage names are the game’s own.
bit
stage
name
0x01
0
de-instantiating
0x02
1
synchronizing
0x04
2
pre-caching
0x08
3
matchmaking GS
0x10
4
citizen joining GS
0x20
5
starting public AH
0x40
6
joining public AH
0x80
7
switching
Stages 5 and 6 run the same function. It creates the public activity session and waits for it.
The transition also keeps a simulated phase: idle, hysteresis, matchmaking, matchmaking-abort, switching and switched, numbered 0 to 5. While a transition runs, the phase is 2, 3 or 4.
The region session state
The transition manager holds two region records, CURRENT and PENDING. Each has a session state: 0 disc, 1 ctng, 2 cntd.
A private region passes on its first test and never needs the pending state. A public region needs the pending state to be cntd before it can switch. Exactly one place in the client writes cntd for the pending region, with the reason text public AH instance ready. It runs only when the public activity session reaches status 6.
The three activity-session slots
The client keeps three activity-session slots:
slot
name
0
PRIVATE CURRENT
1
PUBLIC CURRENT
2
PUBLIC TARGET
The public route hangs on slot 2. A slot climbs a status ladder. The status names are the client’s own.
status
name
what moves it on
-1
NONE
a public slot is created only once slot 0 is at 6
1
startup_setting_parameters
the group session reaches state 4 to 9; after 1000 ms the slot fails with timed_out_waiting_for_session_stabilization
2
startup_preparing_to_create_client
nothing
3
startup_creating_client
an activity client exists and the BAP link is up
4
startup_requesting_host
activity message 10 arrives
5
startup_establishing_client
message 4 is accepted, then message 0
6
ok
–
7, 8
shutting_down, disposing
teardown
9 and up
errors
terminal
The group-session gate at status 1 is the one that matters on the public route. A public slot joins group 2’s target session, and the client never hosts that session itself. It reaches the state range only by joining a session another machine hosts.
A public session runs the destination’s free-roam activity
The public session does not run the mission. It runs the destination’s free-roam activity. A mission in the EDZ runs on the private session, and the public regions run edz_freeroam. Every free-roam activity carries template hash 0x0109ED6B at activity record +220. The public session takes the lowest-index named row with that template on the same destination.
destination
free-roam rows
9, EDZ
8 edz_freeroam
10
10 fleet_freeroam, 605 rally_terminal
18
42 dreaming_city_freeroam, 43 unnamed
20
40 tangled_shore_freeroam, 41 tangled_shore_reveal_freeroam
28
414 pandora_quests; 413 pandora_freeroam has a different template
A destination with no free-roam row keeps the source activity.
Waypoints in the world
The in-world waypoint is driven by live marker objects, not by the Director. The Director’s Add Waypoint and Remove Waypoint buttons change which existing marker may become the extra tracked marker. They do not create a marker.
- Activity and world objects register markers with a marker manager. The HUD list holds at most 16. Slot type 68’s Auth registers up to 13 of them.
- A marker is skipped when it is disabled, its position is not finite, its W lane is not 1.0, or its position is the zero sentinel.
- The client picks the nearest eligible candidate as the selected tracker. It passes the target bubble, the target position and the player position to a route request.
- The client generates the route itself. The solver maps the target bubble to a slice set, searches route topology loaded from packages, and also builds a 96-node graph from slot type 19’s Auth.
- If no topology is registered, there is no waypoint. If topology exists but no route is found, the waypoint is the target position itself.
Which inbound activity message creates the selected marker for a Director choice is unverified.
A scenario’s selection graph
A scenario can also group the activities it can launch into a selection graph. One example, the Infinite Forest graph 0xE6013100, has 11 nodes. Each node offers one or more activity hashes. The infinite_abyss node offers two variants: Haunted Forest (activity 78) and Firewalled Haunted Forest (activity 79).
- The nodes in this data have no edges. It is a grouping, not a flow.
- Each node carries a short sequence of native state values. What those values mean is unknown.
Open questions
- the meaning of the activity root’s
+0x44tag - the client code that tests a container’s bubble mask
- what pulls in an activity package that no scenario names
- what the release rows at activity
+0x0A0gate - what the Director toggle writes to make a marker eligible
- a client reader of proxy slots
- the meaning of the extra cutscene state’s marker flags
- the meaning of a selection graph’s native state values
