MISSION SCRIPTING

The generated SDK modules

Sunrise reads the installed game data and writes plain Lua modules from it. Your script loads them with require. They hold every identity a script needs, so a script never types a raw id.

Sunrise writes these files. Never edit them. A change is lost on the next generation.

Where they are

<game>/bin/x64/Sunrise/
    scripts/                  your scripts, loaded as require("<folder>.<file>")
    sdk/
        catalog.bin           the native catalog; not Lua
        scenarios/            native world shards; not Lua
        lua/
            manifest.json     build identity and row counts
            missions.lua      name -> mission module
            activities.lua    name -> activity module
            missions/         one module per scenario
            activities/       one module per activity
            sunrise/
                activity_sdk.lua   shared enums and editor type hints
                behaviors.lua      compiled behavior tables

require searches sdk/lua first, then scripts. A script can never hide a generated module by using the same name.

missions.lua

A table from a readable name to a module name.

local missions = require("missions")
local mission = require(missions.MISSION_DEADZONE)   -- "missions.mission_deadzone_80b9ad31"
  • The key is the scenario name in upper case.
  • The value ends in the scenario tag, so it changes if the data changes. Always go through this table. Never type the long module name.

activities.lua and activities/*.lua

The same idea for activities.

local activities = require("activities")
local activity = require(activities.ADVENTURE_GINGER)

One activity module is a small table:

fieldtypemeaning
namestringinternal name; your script folder is named after it
display_namestringname shown to the player
idstringstable id, act/<index>/<hash>
indexintegeractivity row
definition_hashintegeractivity hash
activity_root_tagintegerpackage tag of the activity root
scenario_tagintegerscenario the activity runs
matchmaking_config_taginteger0xFFFFFFFF when absent
missiontablethe mission module, already loaded

A mission module

This is the module you use most. Load it once at the top of your script.

local missions = require("missions")
local mission = require(missions.MISSION_DEADZONE)
local Slot, Squad, Directive = mission.Slot, mission.Squad, mission.Directive

It has two kinds of tables. Row tables hold full records. Constant tables hold one identity per name. You pass constants to the API.

Row tables

tableone row
mission.states{id, ordinal, slice_set_index, region_index, hash, value}
mission.slots{id, name, index, type, component_class, sense_schema, auth_schema, auth_type, auth_min_bits, auth_max_bits, auth_dynamic, auth_writable, auth_component_offset}
mission.squads{id, slot, spawner_config, spawn_rule_config, members, anchors}
mission.scenes{id, slot, config_tag, resource_tag}
mission.tasks{id, task_slot, objective_slot, objective_bit, config_tag}
mission.trigger_volumesa list of {names, registry_key, slot_type, slot_index, position, minimum, maximum, shape_tag, shape_index, active}

Notes:

  • states rows are what initial_state and select_state take. Pass the whole row.
  • A squad members entry is {actor_class, behavior_config, default_faction}. All three are nil when the actor class is not known exactly.
  • A squad anchors entry is {x, y, z, point}, the authored spawn points.
  • slot in a squad or scene row is a catalog row number, not a slot index.
  • minimum and maximum of a trigger volume are the corners of its box, as the package stores them. The in-game Triggers page shows the same numbers as “world bounds”. Use them to get a rough idea of where a trigger is, then confirm it in game.

Constant tables

tablekeyvaluepass it to
mission.Slotslot keyslot id stringcontext:slot(...)
mission.Squadsquad keysquad id stringcontext:squad(...), context:cohort{...}
mission.Scenescene keyscene id stringcontext:scene(...)
mission.Tasktask keytask id stringreference only
mission.Statestate keythe state’s public valuereference only
mission.Authauth type, then slot keyslot id stringcontext:slot(...)
mission.TaskGroupobjective slot key, then GROUP_<n>{slot_row, group_index}task_group =
mission.ActorAbilityactor slot key, GROUP_<hash>, KEY_<hash>{slot_row, group_hash, request_hash}ability =
mission.DialogueCuedialogue slot key, then CUE_<n>cue numbercue =
mission.DialogueCueVariantsdialogue slot key, then cue numberlist of line textssearch only
mission.DialogueDefinitiondialogue slot key, then cue numberdialogue hashreference only
mission.Directivedirective key{id, slot_row, name_hash, element, title, description}directive =
mission.PerformanceStateperformance slot key, then STATE_<hash>{slot_row, name_hash, ordinal}state = in play_performance
mission.TriggerVolumevolume name in upper casea row of trigger_volumesreference only
mission.ActorCommandcommand nameselector numbercommand = in actor_command
mission.ActorCommandDefinitioncommand name{selector, payload}reference only
mission.ActorMessagemessage namemessage schema rowreference only
mission.FactionNONE, REMOVED, HOSTILE_TO_ALLfaction numbervalue = in actor_command
mission.SimulationEventevent nameevent type numberreference only
mission.SimulationEventDefinitionevent name{event_type, primary_schema, secondary_schema}reference only
mission.RuntimeFieldTypecodec family, then type nametype codereference only

How keys are made

  • A key is the authored name in upper case, with other characters folded to _.
  • A name used by two objects gets the object tag: O_CIV_0_80B4A6CB.
  • A name still not unique gets the slot index too: ..._0000E5DA.
  • A squad and its slot share one key. mission.Squad.SQ_X pairs with mission.Slot.SQ_X.
  • A scene and its slot share one key too.
  • A directive key comes from its title. Repeated titles get the name hash: RENDEZVOUS_WITH_HAWTHORNE_1DD5B5F6.
  • A state key is its id in upper case: STATE_<scenario>_<bubble>_<ordinal>_<entry>.

A key never depends on row order, so a key you wrote stays valid when the data is regenerated from the same install.

Guard against a missing key

Lua returns nil for a key that does not exist. A nil in a list ends ipairs early, and the mistake stays hidden. Check every key when the module loads.

local function one(value, name)
    assert(value ~= nil, "missing mission constant: " .. name)
    return value
end

local DOOR = one(mission.Slot.D_FRONT_DOOR, "front door")

lib/mission_lib.lua has this helper as lib.one, and lib.list for lists. See The shared script libraries.

sunrise/activity_sdk.lua

Shared tables every mission module loads. Load it yourself with require("sunrise.activity_sdk").

tablemeaning
EventKindthe event kind numbers; event.kind holds one
SlotTypea few named slot types: SQUAD = 1, SEQUENCE = 5, CINEMATIC = 6, DEVICE = 23, TRIGGER = 31, TASK = 38, SCENE = 43, TRIGGER_VOLUME = 60
AuthTypeper auth type name: {name, slot_type, schema, struct_bytes, min_bits, max_bits, component_offset, dynamic, writable}
CombatantLanePrimarySchemaatom lane schema ids
CombatantLaneSecondarySchemaatom secondary lane schema ids
AuthoredBehaviorFilterClassbehavior condition class ids
ObjectFilterPredicateSchemaobject filter predicate schema ids
ActorSequenceCatalogthe extracted actor sequence tables

The file also holds ---@class comments for the LuaLS editor plugin. They help with completion. They can lag behind the runtime. When a hint and the runtime disagree, Lua API reference is right.

sunrise/behaviors.lua

Compiled behavior tables: Program, Channel, Input, ChannelWrite, Owner, ActivityBinding, and the small enums InputSelector, InputRole and SubmissionKind.

  • It is about 25 MB of source. A script has a 64 MiB memory limit. Do not require it from a mission script unless you need it.
  • No current mission script uses it.

manifest.json

The build identity of the generated set.

keymeaning
sdk_build_idsha256:<64 hex>; the same value as context.sdk_build_id
format_versiongenerator format number
schemaschema name
countsrow counts per table, useful to see how much data exists

The editor setup

An editor with the Lua language server (LuaLS) can use the generated hints. Open the scripts folder and add the SDK folder as a library, for example in .luarc.json:

{
    "runtime.version": "Lua 5.4",
    "workspace.library": ["E:/Games/Destiny2/bin/x64/Sunrise/sdk/lua"]
}

Use the full path of your own install. The mission modules are large, so the editor may be slow to index them the first time.