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 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 Raid and type 46 is Dungeon. 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 0x8029E4B4 whose 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 nodes row holds the activities a destination’s own map page offers.
  • A linked_graphs row 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 +0x44 tag
  • 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 +0x0A0 gate
  • 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