.agents/skills/galaxy-triggers-and-functions/SKILL.md
Trigger declaration, event registration, async execution via TriggerExecute, the static parameter pattern for functions that use Wait, trigger management, cinematic sequencer queue, and common event types in Galaxy script. Use when creating triggers, attaching events, executing async functions, or building the trigger init chain. Do not use for unit-specific events (use galaxy-units-and-groups) or dialog events (use galaxy-ui-and-dialogs).
npx skillsauth add KimPlaybit/Starcraft-2-Editor-Skills galaxy-triggers-and-functionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Resource | URL |
|---|---|
| Native function reference | https://mapster.talv.space/galaxy/reference |
| Galaxy script tutorial | https://s2editor-guides.readthedocs.io/New_Tutorials/03_Trigger_Editor/058_GalaxyScript/ |
| Multithreading guide | https://s2editor-guides.readthedocs.io/New_Tutorials/03_Trigger_Editor/057_Multithreading_with_Action_Definitions/ |
| Trigger Debugger guide | https://s2editor-guides.readthedocs.io/New_Tutorials/03_Trigger_Editor/053_Trigger_Debugger/ |
| Optimizing Code guide | https://s2editor-guides.readthedocs.io/New_Tutorials/03_Trigger_Editor/056_Optimizing_Code/ |
| Galaxy syntax definition | https://github.com/Talv/vscode-sc2-galaxy/blob/master/syntaxes/galaxy.json |
| SC2-IngameDevTools (PRIMARY — #1 codebase) | https://github.com/abrahamYG/SC2-IngameDevTools/tree/main/DevToolsIngame.SC2Mod/Script |
| SSF codebase (secondary style) | https://github.com/Cristall/SC2-SwarmSpecialForces/tree/main/SwarmSpecialForces.SC2Map/scripts |
| Alcyone Frontlines codebase | https://github.com/KimPlaybit/Alcyone_Frontlines/tree/master/ProximaFrontlines.SC2Mod/scripts |
| NativeLib | TriggerLibs/NativeLib.galaxy (sc2galaxy VS Code extension) |
| SC2Mapster wiki | https://sc2mapster.wiki.gg/ |
| Triggers overview (wiki) | https://sc2mapster.wiki.gg/wiki/Triggers |
// Basic function
int Plus(int i, int j) {
int result = i + j; // locals declared at TOP of function, before any logic
return result;
}
// void function (no return value)
void DoSomething(string lp_name, int lp_count) {
// implementation
}
// File-private function (cannot be called from other files)
static bool IsInternal() {
return true;
}
// Native function declaration (maps to engine internals)
native int AIGetRawGasNumSpots(int player, int town);
// Custom typedef / function pointer type
void MyCallback_t();
typedef funcref<MyCallback_t> MyCallbackRef;
Rules:
var++ is not valid. Use var += 1.static makes a function file-private./* */ block comments—only // line comments.A trigger is a named callback function registered to fire on certain game events.
The SC2-IngameDevTools codebase uses a clean, consistent pattern for registering triggers by string name inside each module's _Init() function. This is the pattern to follow:
// Handler functions always have the bool(bool,bool) signature
bool BehaviorContainerSendHandler(bool a, bool b) {
// ... handler logic
return true;
}
bool BehaviorListBoxFilterQuery(bool a, bool b) {
int player = EventPlayer();
playergroup pg = PlayerGroupSingle(player);
string val = DialogControlGetPropertyAsString(
ListBoxFilter.editbox, c_triggerControlPropertyEditText, player);
ItemList_FilterListRebuild(ItemList, ListBoxFilter, val, pg);
return true;
}
void BehaviorHandler_Init() {
trigger t;
// Register trigger by STRING name — the string must exactly match the function name
t = TriggerCreate("BehaviorContainerSendHandler");
TriggerAddEventDialogControl(t, c_playerAny, BehaviorContainer.addButton,
c_triggerControlEventTypeClick);
TriggerAddEventDialogControl(t, c_playerAny, BehaviorContainer.removeButton,
c_triggerControlEventTypeClick);
// Filter query trigger
t = TriggerCreate("BehaviorListBoxFilterQuery");
TriggerAddEventDialogControl(t, c_playerAny, ListBoxFilter.editbox,
c_triggerControlEventTypeTextChanged);
}
Key rules from this pattern:
bool FunctionName(bool a, bool b).TriggerCreate("FunctionName") — the string is the exact function name as written in code._Init() creates all its own triggers and hooks its own events.trigger t; is declared as a local and reused for multiple registrations in the same _Init().TriggerAddEventChatMessage(t, c_playerAny, "commandString", false).TriggerAddEventGeneric(handler, "EventName") + TriggerSendEvent("EventName").// Chat command registration (from DevTools/ChatCommand.galaxy)
trigger DevTools_ChatCommand;
void DevTools_ChatCommand_Init() {
DevTools_ChatCommand = TriggerCreate("DevTools_ChatCommand_Func");
// Commands registered later via DevTools_ChatCommandCreate():
// TriggerAddEventChatMessage(DevTools_ChatCommand, c_playerAny, cmd, false);
// TriggerAddEventGeneric(handler, "DevTools_ChatCommand.Exec."+cmd);
}
void DevTools_ChatCommandCreate(string cmd, trigger handler, text description) {
ItemListAdd(ItemList, cmd);
TriggerAddEventChatMessage(DevTools_ChatCommand, c_playerAny, cmd, false);
TriggerAddEventGeneric(handler, "DevTools_ChatCommand.Exec."+cmd);
DevTools_ChatCommandSetDescription(cmd, description);
}
void DevTools_ChatCommandSend(string cmd) {
TriggerSendEvent("DevTools_ChatCommand.Exec."+cmd);
}
// In the header (_h.galaxy) — replace libXXXXXXXX_ with your mod's auto-generated library prefix:
trigger libXXXXXXXX_gt_SpawnUnits;
trigger libXXXXXXXX_gt_WinTeam1;
// 1. The logic function — bool (bool testConds, bool runActions)
bool gt_SpawnUnits_Func(bool testConds, bool runActions) {
// Conditions block
if (testConds) {
if (!(someCondition)) { return false; }
}
// Actions block
if (!runActions) { return true; }
// ... do work ...
return true;
}
// 2. The init function — registers event(s) onto the trigger
void gt_SpawnUnits_Init() {
gt_SpawnUnits = TriggerCreate("gt_SpawnUnits_Func");
TriggerEnable(gt_SpawnUnits, true);
// attach one or more events:
TriggerAddEventTimePeriodic(gt_SpawnUnits, 10.0, c_timeGame);
}
trigger MyGlobalTrigger;
bool MyTrigger_Func(bool testConds, bool runActions) {
UIDisplayMessage(PlayerGroupSingle(EventPlayer()), c_messageAreaChat,
StringToText(EventChatMessage()));
return true;
}
void MyTrigger_Init() {
MyGlobalTrigger = TriggerCreate("MyTrigger_Func");
TriggerAddEventChatMessage(MyGlobalTrigger, c_playerAny, "echo", false);
}
| Function | Signature | Purpose |
|---|---|---|
| TriggerCreate | trigger (string funcName) | Create trigger from function name string |
| TriggerExecute | void (trigger t, bool checkConds, bool runActions) | Run trigger immediately |
| TriggerEnable | void (trigger t, bool enabled) | Turn on/off |
| TriggerIsEnabled | bool (trigger t) | Check if on |
| TriggerDestroy | void (trigger t) | Remove trigger |
| TriggerStop | void (trigger t) | Stop executing trigger |
| TriggerGetCurrent | trigger () | Currently running trigger |
| TriggerEvaluate | bool (trigger t) | Run condition check only |
| TriggerGetExecCount | int (trigger t) | How many times it has fired |
| TriggerActiveCount | int () | Count of currently active triggers |
When a function needs Wait() or long-running logic, wrap it in a trigger. Use static file-scope variables as parameter registers, capturing them into locals immediately:
// Declare param register at file scope
static int Utility_DelayedTextTagDestroyer_ParamTextTag;
static trigger Utility_DelayedTextTagDestroyer_Trigger;
void Utility_DelayedTextTagCreate(text inText, color inColor, point position, playergroup pg, fixed offset) {
// Store param in static, then kick the trigger
Utility_DelayedTextTagDestroyer_ParamTextTag = TextTagCreate(TextWithColor(inText, inColor), 24, position, offset, true, false, pg);
TextTagSetVelocity(Utility_DelayedTextTagDestroyer_ParamTextTag, 1.0, 90.0);
TriggerExecute(Utility_DelayedTextTagDestroyer_Trigger, false, false);
}
bool Utility_DelayedTextTagDestroyer(bool testCond, bool runActions) {
// Capture static into local BEFORE any Wait() — the static may be overwritten
int textTag = Utility_DelayedTextTagDestroyer_ParamTextTag;
Wait(3.5, c_timeGame);
TextTagDestroy(textTag);
return true;
}
// Init: register the trigger once
void Utilities_Init() {
Utility_DelayedTextTagDestroyer_Trigger = TriggerCreate("Utility_DelayedTextTagDestroyer");
}
Key rule: Always copy the static into a local at the very top of the handler body, before any Wait(). Otherwise a concurrent call will overwrite the static before your thread reads it.
TriggerAddEventMapInit(myTrigger); // map loads
TriggerAddEventTimeElapsed(myTrigger, 5.0, c_timeGame); // after 5 seconds
TriggerAddEventTimePeriodic(myTrigger, 3.0, c_timeGame); // every 3 seconds
TriggerAddEventTimer(myTrigger, myTimer); // when a timer expires
TriggerAddEventUnitDied(myTrigger, null, false);
TriggerAddEventUnitCreated(myTrigger, null, "", "");
TriggerAddEventUnitGainLevel(myTrigger, null);
TriggerAddEventUnitGainExperience(myTrigger, null);
TriggerAddEventUnitBehaviorChange(myTrigger, null, "", 0);
TriggerAddEventUnitRegion(myTrigger, null, someRegion, true);
TriggerAddEventUnitOrder(myTrigger, null, null);
TriggerAddEventUnitBecomesIdle(myTrigger, null);
TriggerAddEventUnitProperty(myTrigger, null, c_unitPropLife);
// Range-based proximity events
TriggerAddEventUnitRange(myTrigger, lv_unit, lv_nearUnit, 5.0, true); // unit enters/exits radius of another unit
TriggerAddEventUnitRangePoint(myTrigger, null, lv_point, 8.0, true); // any unit enters/exits radius of a point
// Unit is issued a specific ability order
TriggerAddEventUnitOrder(myTrigger, null, AbilityCommand("move", 0));
// Unit is attacked (EventUnitTarget() returns the attacker)
TriggerAddEventUnitAttacked(myTrigger, null);
unit lv_attacker = EventUnitTarget(); // inside handler
// Unit loaded/unloaded as cargo
TriggerAddEventUnitCargo(myTrigger, null, false); // false = load event, true = unload
// Unit takes fatal damage (use with UnitDied for damage-source filtering)
TriggerAddEventUnitDamaged(myTrigger, null, c_unitDamageTypeAny, c_unitDamageFatal, null);
// Event accessors inside the trigger func:
unit lv_unit = EventUnit();
int lv_player = EventPlayer();
TriggerAddEventChatMessage(myTrigger, c_playerAny, "!cmd", false);
TriggerAddEventDialogControl(myTrigger, c_playerAny, myButton, c_triggerControlEventTypeClick);
TriggerAddEventPlayerJoin(myTrigger);
TriggerAddEventPlayerLeft(myTrigger, c_playerAny, c_gameOverLeave);
TriggerAddEventPlayerPropChange(myTrigger, c_playerAny, c_playerPropMinerals);
TriggerAddEventKeyPressed(myTrigger, c_playerAny, 'A', true, 0);
TriggerAddEventUpgradeLevelChanged(myTrigger, c_playerAny);
TriggerSendEvent("MyCustomEvent");
TriggerAddEventGeneric(myTrigger, c_playerAny, "MyCustomEvent");
string lv_name = EventGenericName(); // inside handler
Wait blocks the current trigger thread without blocking others:
Wait(0.5, c_timeGame); // wait 0.5 game seconds
Wait(2.0, c_timeReal); // wait 2 real seconds
timer lv_t = TimerCreate();
TimerStart(lv_t, 10.0, false, c_timeGame); // one-shot after 10s
TimerStart(lv_t, 5.0, true, c_timeGame); // repeating every 5s
TimerPause(lv_t, true);
fixed lv_remaining = TimerGetDuration(lv_t);
libNtve_gf_StopTimer(lv_t);
// Fire a trigger when it expires:
TriggerAddEventTimer(myTrigger, lv_t);
timer lv_fired = EventTimer(); // in handler
In SSF-style maps, main() registers a single map-init trigger. Each module registers its own triggers in a dedicated _TriggerCreate() function:
// scripts/main.galaxy
void main() {
TriggerAddEventMapInit(TriggerCreate("MapInit_Main"));
}
// scripts/MapInit.galaxy
bool MapInit_Main(bool testCond, bool runActions) {
MapInit_ActivePlayers();
SSFCustomUI_Init();
PartTerran_TriggerCreate(); // each module registers its own triggers
PartProtoss_TriggerCreate();
PartZerg_TriggerCreate();
return true;
}
// scripts/PartTerran.galaxy
void PartTerran_TriggerCreate() {
TriggerAddEventUnitDied(TriggerCreate("PartTerran_SomeHandler"), null, false);
}
// Replace libXXXXXXXX_ with your mod's auto-generated library prefix.
void libXXXXXXXX_InitTriggers() {
libXXXXXXXX_gt_SpawnUnits_Init();
libXXXXXXXX_gt_WinTeam1_Init();
// ... all _Init calls ...
}
The trigger queue serializes long-running cinematic triggers so they don't overlap.
// Enter the queue at the START of a cinematic trigger
TriggerQueueEnter();
// ... all cinematic steps ...
TriggerQueueExit(); // release queue at END
// Pause / resume the queue
TriggerQueuePause(true); // block next trigger from starting
TriggerQueuePause(false); // resume
// Discard pending items
TriggerQueueClear(c_triggerQueueRemove);
// Check if the queue is empty (useful in victory checks)
bool lv_empty = TriggerQueueIsEmpty();
// File-private (not callable from outside)
static bool HelperFunc() { return true; }
// Public (callable from any file that is compiled together)
bool PublicFunc() { return HelperFunc(); }
Functions can be declared before they are defined (forward declarations).
As long as files are all included intoMapScript.galaxy, you can call any function from any file without a localinclude.
Galaxy does not have true parallel execution. The editor implements cooperative multithreading via time-slicing:
Wait().Wait(), control returns to the parent thread (or the next pending event).This "fakes" parallelism by rapidly switching linear control around Wait() boundaries. It is not faster than sequential code — in fact, threaded code runs slower due to the overhead of managing thread state.
Source: Multithreading With Action Definitions
// Run trigger NOW in the SAME thread (blocks until done)
TriggerExecute(myTrigger, false, false);
// Run trigger in its OWN thread (returns immediately; trigger runs concurrently)
TriggerExecute(myTrigger, false, true); // third arg = separate thread
The static-param pattern (see the Async section above) is the correct way to pass parameters into a trigger-based async function.
When the GUI editor creates a threaded action definition, it generates:
// 1. Global parameter registers (one per action-def parameter)
int auto_gf_MyActionDef_lp_param;
// 2. Global trigger variable (created once, reused)
trigger auto_gf_MyActionDef_Trigger = null;
// 3. The trigger function body — captures params into locals BEFORE any Wait
bool auto_gf_MyActionDef_TriggerFunc(bool testConds, bool runActions) {
int lp_param = auto_gf_MyActionDef_lp_param; // capture to local immediately
Wait(5.0, c_timeGame);
// use lp_param here safely
return true;
}
// 4. Wrapper called at each invocation — copies args into global registers, fires trigger
void gf_MyActionDef(int lp_param) {
auto_gf_MyActionDef_lp_param = lp_param; // write to global register
if (auto_gf_MyActionDef_Trigger == null) {
auto_gf_MyActionDef_Trigger = TriggerCreate("auto_gf_MyActionDef_TriggerFunc");
}
TriggerExecute(auto_gf_MyActionDef_Trigger, false, true); // run in new thread
}
Key rule: Capture the global parameter register into a local variable at the very first line of the thread function, before any Wait(). Otherwise a new call can overwrite the global before the thread reads it.
Wait()-based timelines (e.g., five marines each on a 5-second lifecycle running in parallel).Timer-based trigger is usually simpler and cheaper.testing
SC2 Data Editor — Wizards for automating complex or repetitive data creation/modification in XML. Use when creating .BlizWiz XML files to define templates for generating catalog entries (units, abilities, effects, actors, etc.) with user inputs, conditions, validations, and macros. Covers wizard elements (input, entry, condition, validate, macro), string evaluation (tokens, catalog references, arithmetic), and file placement. Always reference the Data Wizard Documentation for syntax. Do not use for direct data editing (use other sc2data-* skills) or Galaxy scripting.
data-ai
SC2 Data Editor — Units, Abilities, Movers, Turrets, Requirements, and Races in XML. Use when creating or modifying units (CUnit/CUnitHero), abilities (CAbilEffectTarget, CAbilEffectInstant, CAbilResearch, etc.), movement (CMover), turrets (CTurret), or tech requirements in GameData XML files. Always consult the catalogsData.xsd schema for exact fields and structure — do not assume unsupported fields exist. Do not use for Actors/visuals (use sc2data-actors-visuals), damage/effects (use sc2data-effects-weapons), or Galaxy scripting (use the galaxy-* skills).
data-ai
SC2 Data Editor — Effects, Weapons, Upgrades, and the damage chain in XML. Use when creating or modifying CEffect* (damage, search, apply behavior, launch missile, set), CWeapon, CUpgrade, or the full chain from weapon through to damage application. Also covers TargetFind, TargetSort, and Footprints. Always consult the catalogsData.xsd schema for exact fields and structure — do not assume unsupported fields exist. Do not use for actors/visuals (sc2data-actors-visuals) or unit/ability containers (sc2data-units-abilities).
testing
SC2 Data Editor — Behaviors (buffs, debuffs, auras, timers) and Validators (conditional tests) in XML. Use when creating or modifying CBehavior* (buff, attribute modifier, unit tracker, reveal) or CValidator* (unit type, unit order, comparison, combine) entries. Also covers behavior stacking, duration, Vitals modification, and how validators gate effects, abilities, and behaviors. Always consult the catalogsData.xsd schema for exact fields and structure — do not assume unsupported fields exist. Do not use for applying a behavior via an effect (use sc2data-effects-weapons) or actor visuals tied to a behavior (use sc2data-actors-visuals).