Galaxy Objectives, Actors, and Conversation Metadata
Findings from the September 11, 2026 TRaynor01 investigation. This describes the current implementation and the inspected local client scripts, not complete retail SC2 conformance. See Galaxy scripting for execution/error boundaries and native coverage for the remaining work.
Ownership and Data Flow
| Concern | Current owner | Boundary |
|---|---|---|
| Mission conditions, objective progression, dialogue sequencing | Authored MapScript.galaxy and TriggerLibs |
Galaxy trigger callbacks |
| Objective identity, text, state, visibility, primary flag | game/galaxy/galaxy_ui.h |
Integer native IDs; no HUD output yet |
| Actor identity and scope relationships | game/galaxy/galaxy_actor.h |
Opaque handles; game callback hooks for presentation |
| Layered conversation catalog strings | common/sc2_map.c |
SC2_MapConversationField, installed as sc2_galaxy_conversation_field |
| Future objective/help UI | SC2 game HUD code | Existing svc_layout and generic client rendering |
Use WC3 games/warcraft-3/game/api/api_quest.h as the state-ownership reference: native wrappers mutate server-owned quest records
created/removed by G_MakeQuest / G_RemoveQuest; scripts choose mission policy. SC2 integer objective IDs are a different native
contract from WC3 quest handles. Do not share IDs or cast one domain's objects into the other.
Presentation should extend the SC2 HUD pipeline or game-authored entity output. The menu module must not become the owner of in-game objectives, help tips, or actor effects. Completing a native's script-state behavior does not establish that its presentation is rendered.
Objective Contract
The mounted map uses legacy ObjectiveCreate(name, description, state, primary). The mounted Core natives.galaxy instead declares
ObjectiveCreate3(name, description, state, visible, primary). The host supports both; its legacy adapter sets visibility true.
Do not rename the map's calls or assume its generated script and the mounted native declarations have the same API generation.
| Native/data | Behavior |
|---|---|
| Create / LastCreated | IDs begin at 1; 0 means invalid; last-created ID updates on creation |
| GetState | -1 unknown/invalid/destroyed; 0 hidden; 1 active; 2 completed; 3 failed |
| GetPrimary | Boolean; the old placeholder returned an objective handle |
| GetName / GetDescription | Retained string contents; invalid IDs currently return empty strings |
| SetName | Copies replacement text before VM arguments are released |
| SetState | Updates the referenced record; invalid IDs raise a protected script error |
| Destroy | Releases name/description and invalidates that record; IDs are not recycled |
| Capacity / reset | 256 creations per map; capacity exhaustion is a script error; galaxy_reset frees records |
ObjectiveLastCreated is creation history, not a search for the last live objective: destruction does not rewind it. Visibility and
primary are retained fields; player-group visibility, setters not in the binding table, objective HUD output, and save/load support
are not implemented by this change.
libCamp_gf_RegisterMissionObjective stores the objective integer in a campaign array and reads ObjectiveGetPrimary into a boolean
array. This is why a zero-ID or wrong-type placeholder is not a working implementation.
TRaynor01 starts by creating the logistical-HQ and Raynor-survival objectives, both active and primary. The optional holoboard
objective builds a progress name with IntToText. Galaxy text is mapped to VM string; IntToText must return decimal text.
Its former integer-zero placeholder failed the concatenation regression. The map initializes the holoboard target count to 6.
Actor ABI and Scope Lifecycle
Authoritative declaration in Mods/Core.SC2Mod/Base.SC2Data, TriggerLibs/natives.galaxy:
native actor ActorCreate(actorscope as, string actorName, string content1Name, string content2Name, string content3Name);
libNtve_gf_AttachActorToUnit calls it in this order:
- Get the unit scope with
ActorScopeFromUnit. - Create
SiteHosted, passing the attachment name as content1. - Retrieve it with
ActorFrom("::LastCreated")and sendRefSet ::scope.hostsite ::Self. - Create the requested actor (for the opening dialogue,
TalkIcon) under the unit scope. - Retrieve that actor and send
RefSet ::Host ::scope.hostsite.
The old host read argument one as the actor-name string and argument two as the scope. A live unit scope therefore hit
jass_checkstring's assertion. The crash was independent of the preceding missing ObjectiveCreate message.
Current state behavior:
sc2_unit_scopecaches a root scope for each valid Galaxy unit handle.- Each
ActorCreateallocates a child scope and inherits its parent scope's unit identity. ActorFrom("::LastCreated")returns the last actor if still live, otherwise null.ActorScopeFrom("::LastCreated")returns the last created scope;ActorScopeFromActorretrieves an actor's scope.- After the opening transmission waits, the script calls
ActorScopeKill(libNtve_gf_ActorScopeLastCreated()). - Killing a scope marks its descendants dead and clears affected actor records. Killing the icon's scope leaves its parent unit scope and sibling site scope intact. Repeating the same kill does not emit a second actor-destroy callback.
- Actor IDs and scopes are monotonic until reset: 1,024 actor creations; scope capacity is
MAX_GALAXY_UNITS + MAX_GALAXY_ACTORS.
The native ABI and scope bookkeeping are implemented; the full actor engine is not. Content1/2/3 are not yet retained/applied,
RefSet attachment semantics are not implemented locally, and SC2_InitGalaxyHost does not install actor create/send/destroy
presentation callbacks. Thus resolving TalkIcon identity does not prove a visible attached icon. Named actor/scope references
other than ::LastCreated raise errors; ActorFromScope and actor-region operations remain placeholders. Reusing destroyed slots,
recreating a killed unit root scope, and complete actor-message semantics still need lifecycle work.
See Actors and Models for catalog terminology. A Galaxy actor name is not automatically an M3 file path; follow the actor/model catalog and attachment data when wiring presentation.
Conversation Catalog Schema
The inspected campaign metadata comes from
Campaigns/LibertyStory.SC2Campaign/Base.SC2Data → GameData/ConversationStateData.xml.
The normal catalog loader applies dependency and map-local layers; it follows GameData.xml manifests and includes the conversation
file in its existing known-file path for layers without a manifest.
CConversationState id + Indices Id
-> group|index lookup key
-> SC2CONVERSATION plus repeated SC2CONVTEXT records
-> SC2_MapConversationField
-> sc2_galaxy_conversation_field
-> ConversationDataStateName / Text / ImagePath
| XML production | Internal lookup | Parsing contract |
|---|---|---|
CConversationState id="StoryTips" + Indices Id="Marine" |
StoryTips|Marine |
Case-sensitive query key |
Indices Name="..." or child <Name value="..."/> |
Name |
Schema-table scalar field |
Indices ImagePath="..." or child <ImagePath value="..."/> |
ImagePath |
Schema-table scalar field |
<InfoText Id="Description" Text="..."/> |
Text:Description |
Repeated keyed production |
InfoText with child Id / Text value tags |
Text:<ID> |
Same schema as compact attributes |
<InfoText Id="Loading Screen Restart 2"/> |
Present empty text value | Distinct from an unresolved field |
InfoText IDs may contain spaces. Name and Text:Name are separate concepts. The first implementation attempt only kept
Description; the bounded run then stopped at Maps|TRaynor01, Loading Screen Restart during initialization. Preserve all
InfoText IDs, not a hardcoded description slot.
Current layering preserves earlier scalar name/image values unless a later nonempty value replaces them. InfoText entries merge by ID and retain explicitly empty text. Consequently an explicit empty scalar override does not currently clear an earlier name/image; this is a limitation, not complete XML override semantics. Records use bounded buffers (combined key 128 bytes, text ID 64 bytes, name/image/text 256 bytes). Numeric index patches and general conversation-state inheritance are not implemented.
The local archive contains Indices index="0" with InfoText index="8" removed="1" in groups such as Missions, MapLast, and
StoryNews. These are ordinal/removal patches, not malformed named tutorial records. The current parser logs missing index IDs
for them; future support must resolve inherited ordinal identity and removal semantics, not invent replacement IDs.
A missing row/field logs its key and field and returns NULL; the Galaxy wrapper raises a protected script error. A present empty
field returns an empty string. Catalog strings live until catalog replacement/map shutdown, with cleanup in sc2_free_catalog.
Name and InfoText values are usually localization keys. StringExternal is still a placeholder passthrough, and help-panel natives
remain placeholders, so catalog lookup does not establish localized or visible tutorial UI. Conversation state get/set/persistence
is separate from this read-only metadata lookup.
Regression Evidence
| Test | What it verifies |
|---|---|
galaxy.vm_objective_lifecycle |
Distinct IDs, primary/state/text, progress concatenation, destruction, map reset |
galaxy.vm_actor_scope_first |
Live scope in arg 1, last-created identity, unequal opaque handles, idempotent icon cleanup |
sc2_map.sc2_map_loads_xml_objects_and_terrain |
Core/map fixture layering, both XML forms, arbitrary InfoText ID, unresolved inverse |
Fixture metadata lives under games/starcraft-2/tests/resources-src/Mods/Core.SC2Mod/Base.SC2Data/ and
Maps/Test/Tiny.SC2Map/Base.SC2Data/; both manifests include GameData/ConversationStateData.xml. Tests use the generated fixture
archive and do not depend on locally installed SC2 assets. The bounded runtime workflow
verifies script progression separately from these state tests.