---
title: "Lua Event Tutorial"
canonical: "https://wiki.sinsofasolarempire2.com/space/SSEFW/3170238513/Lua%20Event%20Tutorial"
format: markdown
---
# Lua Event Scripting Tutorial for Sins of a Solar Empire II

A beginner-friendly guide to writing scripted events — no programming experience required.

---

## Table of Contents

- [Part 1: Introduction](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#part-1-introduction)
- [Part 2: How Events Work (The Lifecycle)](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#part-2-how-events-work-the-lifecycle)
- [Part 3: Line-by-Line Walkthrough of test_event.lua](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#part-3-line-by-line-walkthrough-of-test_eventlua)
- [Part 4: Build Your Own Event](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#part-4-build-your-own-event)
- [Part 5: Reference](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#part-5-reference)
- [Appendix A: Lua Crash Course](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#appendix-a-lua-crash-course)

---

## Part 1: Introduction

### What is a Scripted Event?

A **scripted event** is something that happens during a game match that isn't hardcoded into the engine. Instead of the game developers writing C++ code for every in-game occurrence, the event system lets you write small scripts that the game engine runs at the right time.

Think of it like a recipe the game follows:

- "After 15 minutes, spawn a pirate fleet and send it to attack the richest player."
- "When a player colonizes their 5th planet, show a congratulations message."
- "Every 10 minutes, check if a ceasefire should be offered."

The pirate incursion system in the game is a real example of a scripted event — it spawns a Pirate King, picks a target player, sends waves of raiders, and cleans everything up when the incursion is defeated.

### What is Lua?

**Lua** (pronounced "LOO-ah") is a lightweight scripting language used by many games, including World of Warcraft, Roblox, and Garry's Mod. It was chosen for Sins 2 events because:

- **No compilation needed.** You edit a text file, restart the game, and your changes are live.
- **Simple syntax.** It reads almost like English.
- **Safe.** Scripts can't crash the game or access things they shouldn't.

### What You Need

Just a **text editor**. Notepad works, but a code editor like [Visual Studio Code](https://code.visualstudio.com/) is strongly recommended because it highlights Lua syntax and catches typos for you.

Your event scripts live in:

```
sins2_data/data/scripts/events/
```

---

## Part 2: How Events Work (The Lifecycle)

> **New to Lua?** If you've never written code before, read [Appendix A: Lua Crash Course](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#appendix-a-lua-crash-course) first, then come back here.

Every scripted event follows the same lifecycle. The game engine calls your Lua functions at specific moments, like a relay race where the engine passes the baton to your code and back.

### Lifecycle Flowchart

```
GAME STARTS
    |
    v
[1. REGISTER] ---- "Should this event exist in this match?"
    |                  Return true  -> continue
    |                  Return false -> event is skipped entirely
    v
[2. ON EVENT REGISTERED] ---- "One-time setup for the whole event"
    |                            (Optional) Set up shared state, calculate limits
    v
[3. INITIALIZE] <------ "Set up this specific instance"
    |                   Each instance gets its own state
    v
[4. SHOULD TRIGGER?] <------- checked every trigger_check_interval_seconds
    |         |
    |     false (keep waiting)
    |         |
    |         +-------> loop back to [4]
    |
    true (trigger!)
    |
    +-------> new trigger checker created --> back to [3. INITIALIZE]
    |         (so another instance can trigger later)
    |
    v
[5. ON START] ---- "The event begins!"
    |                Spawn units, show notifications, start timers
    v
[6. ON UPDATE] <------- called every update_interval_seconds
    |     (still running)
    |         |
    |         +-------> loop back to [6]
    |
    (complete or cancel)
    |
    +--> [7a. ON COMPLETE] ---- "The event finished successfully!"
    |                             Give rewards
    |
    +--> [7b. ON CANCEL] ---- "The event was cancelled!"
                                Refund if needed
    |
    v
[8. ON TEARDOWN] ---- "Clean up" ----> done
```

### What Each Phase Does

#### 1. Register (`register_event_function`)

Called once when the match starts. You decide whether this event should be loaded at all. For example, the pirate incursion only registers if there's a pirate faction in the game.

**Return:** `true` to register, `false` to skip.

#### 2. On Event Registered (`on_event_registered_function`) — *Optional*

Called once after registration succeeds. Use this for one-time setup that all instances share: calculating player-based limits, initializing counters, etc.

**Key difference from Initialize:** This runs *once for the whole event*, not once per instance.

#### 3. Initialize (`on_initialize_function`)

Called once for each **instance** that is created. Every event starts with at least one instance (the "trigger checker") whose job is to wait and decide when the event should fire.

**Key difference from On Event Registered:** This runs *once per instance*.

#### 4. Should Trigger (`should_trigger_function`)

Called periodically (every `trigger_check_interval_seconds`). When you return `true`, the engine starts the event.

**Important:** This must be **deterministic** — it must produce the same result on every player's computer in multiplayer. Don't use `math.random()`.

#### 5. On Start (`on_start_function`)

Called when the event fires. This is where the action begins — spawn units, display notifications, register timers.

#### 6. On Update (`on_update_function`)

Called periodically while the event is running (every `update_interval_seconds`). Check win/lose conditions, spawn reinforcement waves, update progress.

To end the event from here, call `context.complete()` or `context.cancel()`.

#### 7a. On Complete (`on_complete_function`)

Called when the event finishes successfully (via `context.complete()`). Give rewards, show victory messages, clean up.

#### 7b. On Cancel (`on_cancel_function`) — *Optional*

Called when the event is cancelled (via `context.cancel()`). Clean up spawned units, refund resources.

#### 8. On Teardown (`on_teardown_function`)

Called after complete or cancel, right before the instance is destroyed. Final cleanup — timers are automatically cleaned up, but you can do any last bookkeeping here.

### Understanding the `context` Variable

Every handler function you write receives a single argument: `context`. This is the **bridge between your script and the game engine**. You don't create it — the engine builds it for you and passes it in. Everything you need to interact with the game flows through `context`.

```lua
function my_event_on_start(context)
    -- 'context' is your window into the game world
end
```

Here's what's inside and why each part matters.

#### context.simulation — The Game World

`context.simulation` is the simulation object. This is how you **read and change the game state**: query players, spawn units, issue orders, display HUD text, and more.

```lua
-- Query the game
local current_time = context.simulation.current_time

-- Change the game
context.simulation:display_text("timer_label", "Hello!")
```

Notice the syntax difference: properties use a dot (`.current_time`), but method calls use a colon (`:display_text()`). This is a Lua convention — the colon automatically passes the object as the first argument.

#### context.shared — The Whiteboard

`context.shared` is a table that **all instances of your event can read and write**. If your event has three instances running simultaneously, they all see the same `context.shared`.

Use it for:

- Counters that track totals across instances ("how many times has this event fired?")
- Limits and caps ("only allow 3 concurrent incursions")
- Coordination between instances ("which players are already targeted?")

```lua
-- In on_event_registered (one-time setup):
context.shared.total_times_fired = 0
context.shared.max_concurrent = 3

-- In on_start (any instance can update it):
context.shared.total_times_fired = context.shared.total_times_fired + 1
```

**When to use shared:** Whenever multiple instances need to agree on something or share a limit.

#### context.instance — Your Personal Notebook

`context.instance` is a table that **belongs to this specific instance only**. Other instances can't see it. It's created fresh for each instance.

Use it for:

- This instance's progress and state ("what phase am I in?")
- Flags and counters specific to this run ("is the timer done?", "how many waves spawned?")
- IDs of units or targets this instance is tracking

```lua
-- In on_initialize:
context.instance.ready_to_trigger = false
context.instance.wave_count = 0

-- In on_start:
context.instance.pirate_king_id = spawned_unit.id
context.instance.current_phase = "raiding"
```

**When to use instance:** Anything that's specific to *this particular run* of the event.

#### Shared vs Instance — When to Use Which

Think of it like a classroom:

- `shared` is the whiteboard at the front — everyone can see it and write on it.
- `instance` is each student's personal notebook — private to them.

| Question | Use |
| --- | --- |
| "How many times has this event triggered total?" | `context.shared` |
| "How many waves has *this* incursion spawned?" | `context.instance` |
| "Is player 3 already being targeted by another instance?" | `context.shared` |
| "What planet is *this* instance attacking?" | `context.instance` |
| "What's the maximum number of concurrent instances?" | `context.shared` |
| "Is *this* instance ready to trigger?" | `context.instance` |

#### context.timers — Scheduling Future Work

`context.timers` lets you register callbacks that fire after a delay. This is how you create countdowns, periodic waves, and delayed actions.

```lua
context.timers.register({
    name = "my_timer",
    interval_seconds = 30.0,
    on_complete = "my_callback_function",
    is_repeating = false
})
```

Timers are tied to the instance that created them and are automatically cleaned up when the instance is destroyed. See the [Timers API](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#timers-api) reference for the full details.

#### context.complete() and context.cancel() — Ending the Event

These two functions are how you **end a running event instance**. They're only available inside `on_update`.

```lua
function my_event_on_update(context)
    if enemies_defeated then
        context.complete()  -- success! triggers on_complete
    elseif time_ran_out then
        context.cancel()    -- failure! triggers on_cancel
    end
end
```

- `context.complete()` — The event succeeded. Calls `on_complete`, then `on_teardown`.
- `context.cancel()` — The event was aborted. Calls `on_cancel`, then `on_teardown`.

#### context.show_notification — Talking to the Player

Shows a predefined notification in the game's HUD notification panel. Unlike `print()`, the player actually sees this.

```lua
context.show_notification(NOTIFY_PIRATE_INCURSION_STARTED, {})
context.show_notification(NOTIFY_ENEMY_UNITS_ARRIVED, {planet = some_planet})
```

See the [Notifications](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#notifications) reference for all available types and their parameters.

#### context.random_float() and context.random_integer() — Safe Randomness

These return **deterministic random numbers** — every player's computer in a multiplayer game generates the same "random" value, which keeps the game in sync.

```lua
local chance = context.random_float()           -- 0.0 to 1.0
local index = context.random_integer(1, 10)     -- 1 to 10 inclusive
```

**Never use **`math.random()` in event scripts. It produces different results on different computers, which causes multiplayer desyncs.

#### Read-Only Information

Context also provides several read-only fields with information about the current state:

| Field | What It Tells You |
| --- | --- |
| `context.event_name` | Your event's name (e.g., `"pirate_incursion"`) |
| `context.event_id` | Your event's versioned ID (e.g., `"pirate_incursion_v1"`) |
| `context.instance_id` | Unique number for this instance |
| `context.active_instance_count` | How many instances are currently running |
| `context.current_tick` | The simulation tick number (deterministic) |
| `context.elapsed_time` | Seconds since this instance started running |

These are useful for debug logging, conditional logic, and coordinating between instances.

#### What's Available Where?

Not every part of `context` is available in every handler. The engine only includes what makes sense for each phase:

|  | simulation | shared | instance | timers | complete/cancel | elapsed_time |
| --- | --- | --- | --- | --- | --- | --- |
| **register** | yes | — | — | — | — | — |
| **on_event_registered** | yes | yes | — | — | — | — |
| **on_initialize** | yes | yes | yes | yes | — | — |
| **should_trigger** | yes | — | yes | — | — | — |
| **on_start** | yes | yes | yes | yes | — | yes |
| **on_update** | yes | yes | yes | yes | yes | yes |
| **on_complete** | yes | yes | yes | yes | — | yes |
| **on_cancel** | yes | yes | yes | yes | — | yes |
| **on_teardown** | yes | yes | yes | yes | — | yes |

If you try to use something that isn't available (like `context.complete()` in `on_start`), it simply won't be there — you'll get a Lua `nil` error.

---

## Part 3: Line-by-Line Walkthrough of test_event.lua

Let's go through the test event file piece by piece. This is a working event you can use as a starting template.

### The Require Statement

```lua
local EventMetadata = require("event_metadata")
```

Every event script starts with this line. It loads the `EventMetadata` module, which provides the `create()` function you need to define your event's metadata.

### Timer Callback Functions

```lua
function test_event_initial_trigger_callback(context)
    print("[test_event] Initial timer completed - event is ready to trigger")
    context.instance.ready_to_trigger = true
end
```

This function is called by a timer (registered later). When the timer fires, it sets a flag (`ready_to_trigger`) on the instance. The trigger checker will see this flag and start the event.

**Try changing this:** Change the print message to something like `"Countdown complete!"` and watch for it in the log.

```lua
function test_event_auto_complete_callback(context)
    print("[test_event] Auto-complete timer finished - completing event")
    context.complete()
end
```

Another timer callback. This one automatically completes the event after a set time by calling `context.complete()`.

**Try changing this:** What happens if you call `context.cancel()` instead of `context.complete()`? (Hint: the `on_cancel` handler runs instead of `on_complete`.)

```lua
function test_event_cooldown_callback(context)
    print("[test_event] Cooldown timer completed - event is ready to trigger again")
    context.instance.ready_to_trigger = true
end
```

After the event completes, this timer starts a cooldown. When it fires, the event can trigger again.

### The Register Function

```lua
function test_event_register(context)
    return true
end
```

The simplest possible register function — always returns `true`, meaning "yes, include this event in every match."

**Try changing this:** Return `false` and the event will never load. You could also add a condition like checking a game setting.

### The On Event Registered Function

```lua
function test_event_on_event_registered(context)
    print("[test_event] Event registered - performing one-time setup")
    print("  Event ID: " .. context.event_id)

    context.shared.total_triggers_across_all_instances = 0
    context.shared.event_start_time = context.simulation.current_time

    print("  Shared state initialized")
end
```

One-time setup. This initializes two values in `context.shared`:

- A counter for how many times any instance has triggered.
- A timestamp of when the event was registered.

These values are visible to *all* instances.

### The Metadata Function

```lua
function get_event_metadata()
    local metadata = EventMetadata.create()

    metadata.event_name = "test_event"
    metadata.event_id = "test_event_v1"
```

Every event script **must** have a function called `get_event_metadata()`. This is the first thing the engine looks for when loading your script.

- `event_name` — How you reference the event in code (e.g., `Events.trigger(sim, "test_event")`).
- `event_id` — A versioned identifier. If you change your event drastically, bump the version (e.g., `test_event_v2`).

```lua
    metadata.register_event_function = "test_event_register"
    metadata.on_event_registered_function = "test_event_on_event_registered"
    metadata.on_initialize_function = "test_event_initialize"
    metadata.should_trigger_function = "test_event_should_trigger"
    metadata.on_start_function = "test_event_on_start"
    metadata.on_update_function = "test_event_on_update"
    metadata.on_complete_function = "test_event_on_complete"
    metadata.on_cancel_function = "test_event_on_cancel"
    metadata.on_teardown_function = "test_event_on_teardown"
```

These fields connect the lifecycle phases to your function names. The engine calls the function whose name you provide as a string. The function must be a global function defined in the same file.

```lua
    metadata.trigger_check_interval_seconds = 3.0
    metadata.update_interval_seconds = 1.0
```

- `trigger_check_interval_seconds` — How often (in seconds) the engine calls `should_trigger`. Here, every 3 seconds.
- `update_interval_seconds` — How often the engine calls `on_update` while the event is running. Here, every 1 second.

**Try changing this:** Set `trigger_check_interval_seconds = 1.0` for faster triggering, or `10.0` for slower.

```lua
    metadata.description = "Test event for validating metadata-driven registration"
    metadata.author = "System"
    metadata.priority = 1.0
    metadata.incompatible_event_ids = {}
```

Optional metadata:

- `description` — Human-readable explanation (shown in debug tools).
- `author` — Who wrote it.
- `priority` — Higher numbers trigger first when multiple events are ready. Default is `1.0`. **Every event must have a unique priority** — events that share the same priority value are all rejected at load time.
- `incompatible_event_ids` — List of `event_id` strings that can't run at the same time as this one.

```lua
    local valid, error_msg = EventMetadata.validate(metadata)
    if not valid then
        print("[test_event] ERROR: Metadata validation failed - " .. error_msg)
    end

    return metadata
end
```

Before returning, validate the metadata. This catches mistakes like forgetting to fill in a required field. The engine also validates, but checking early gives you a clearer error message.

### The Initialize Function

```lua
function test_event_initialize(context)
    print("[test_event] Initializing instance " .. context.instance_id)
    print("  Active instance count: " .. context.active_instance_count)

    context.instance.ready_to_trigger = false
    context.instance.instance_start_time = context.simulation.current_time
```

Per-instance setup. Each instance gets its own `context.instance` table. Here we initialize two values:

- `ready_to_trigger` — Starts as `false`; a timer will set it to `true` later.
- `instance_start_time` — When this instance was created.

```lua
    if context.active_instance_count == 0 then
        print("[test_event] First instance - registering 10 second initial delay timer")
        context.timers.register({
            name = "test_event_initial_trigger",
            interval_seconds = 10.0,
            on_complete = "test_event_initial_trigger_callback",
            is_repeating = false
        })
    else
        print("[test_event] Instance " .. context.instance_id .. " initialized (not trigger checker)")
    end
end
```

Only the first instance (the trigger checker) registers a timer. After 10 seconds, the timer calls `test_event_initial_trigger_callback`, which sets `ready_to_trigger = true`.

**Timer fields:**

- `name` — A unique name so you can cancel it later.
- `interval_seconds` — How long to wait.
- `on_complete` — The function name to call when the timer fires.
- `is_repeating` — `true` to fire repeatedly, `false` for one-shot.

**Try changing this:** Set `interval_seconds = 3.0` so the event triggers faster.

### The Should Trigger Function

```lua
function test_event_should_trigger(context)
    return context.instance.ready_to_trigger == true
end
```

The engine calls this every `trigger_check_interval_seconds`. It simply checks the flag that the timer set. When the flag is `true`, the event starts.

### The On Start Function

```lua
function test_event_on_start(context)
    print("[test_event] Event instance started!")
    print("  Instance ID: " .. context.instance_id)
    print("  Event ID: " .. context.event_id)
    print("  Elapsed time: " .. context.elapsed_time .. " seconds")
    print("  Current tick: " .. context.current_tick)
    print("  Game time: " .. context.simulation.current_time .. " seconds")

    context.instance.test_counter = 0
    context.instance.test_message = "Test event instance running"
    context.instance.ready_to_trigger = false

    context.shared.total_triggers_across_all_instances =
        (context.shared.total_triggers_across_all_instances or 0) + 1
    print("  Total triggers across all instances: " ..
        context.shared.total_triggers_across_all_instances)

    context.timers.register({
        name = "test_event_auto_complete",
        interval_seconds = 3.0,
        on_complete = "test_event_auto_complete_callback",
        is_repeating = false
    })
end
```

When the event starts:

1. Print debug info showing all the context fields available.
2. Initialize instance-specific tracking (`test_counter`, `test_message`).
3. Reset `ready_to_trigger` so it doesn't re-fire immediately.
4. Increment the shared counter.
5. Register a timer to auto-complete after 3 seconds.

**Try changing this:** Change the auto-complete timer to `10.0` seconds to make the event run longer and see more update ticks.

### The On Update Function

```lua
function test_event_on_update(context)
    context.instance.test_counter = (context.instance.test_counter or 0) + 1

    print("[test_event] Update #" .. context.instance.test_counter)
    print("  Elapsed: " .. context.elapsed_time .. " seconds")
    print("  Game time: " .. context.simulation.current_time .. " seconds")
end
```

Called every `update_interval_seconds` (1 second in this event). Increments a counter and prints the elapsed time. In a real event, you'd check conditions here and call `context.complete()` or `context.cancel()` when done.

### The On Complete Function

```lua
function test_event_on_complete(context)
    print("[test_event] Event instance completed!")
    print("  Instance ID: " .. context.instance_id)
    print("  Total updates for this instance: " .. (context.instance.test_counter or 0))
    print("  Total triggers across all instances: " ..
        (context.shared.total_triggers_across_all_instances or 0))
    print("  Final game time: " .. context.simulation.current_time .. " seconds")
end
```

Called when the event completes successfully. Prints a summary. In a real event, you might give rewards or show a notification here.

### The On Cancel Function

```lua
function test_event_on_cancel(context)
    print("[test_event] Event instance cancelled!")
    print("  Instance ID: " .. context.instance_id)
    print("  Updates before cancel: " .. (context.instance.test_counter or 0))
    print("  Total triggers across all instances: " ..
        (context.shared.total_triggers_across_all_instances or 0))
end
```

Called if the event is cancelled instead of completed. Same idea as `on_complete`, but for the failure/abort path.

### The On Teardown Function

```lua
function test_event_on_teardown(context)
    print("[test_event] Event teardown (Instance " .. context.instance_id .. ")")
    -- timers are automatically cleaned up
    print("[test_event] Teardown complete")
end
```

Final cleanup. Timers registered through `context.timers.register()` are automatically cancelled when the instance is destroyed, so you usually don't need to do much here.

---

## Part 4: Build Your Own Event

Let's create a simple event from scratch: a **"Welcome Commander"** event that fires 30 seconds into a match and prints a welcome message.

### Step 1: Create the File

Create a new file in your events folder:

```
sins2_data/data/scripts/events/welcome_commander.lua
```

### Step 2: Require EventMetadata

Start your file with:

```lua
local EventMetadata = require("event_metadata")
```

### Step 3: Write the Metadata Function

```lua
function get_event_metadata()
    local metadata = EventMetadata.create()

    -- Identity
    metadata.event_name = "welcome_commander"
    metadata.event_id = "welcome_commander_v1"

    -- Lifecycle functions
    metadata.register_event_function = "welcome_commander_register"
    metadata.on_initialize_function = "welcome_commander_initialize"
    metadata.should_trigger_function = "welcome_commander_should_trigger"
    metadata.on_start_function = "welcome_commander_on_start"
    metadata.on_update_function = "welcome_commander_on_update"
    metadata.on_complete_function = "welcome_commander_on_complete"
    metadata.on_teardown_function = "welcome_commander_on_teardown"

    -- Timing
    metadata.trigger_check_interval_seconds = 5.0
    metadata.update_interval_seconds = 1.0

    -- Info
    metadata.priority = 2.0  -- must be unique across all events!
    metadata.description = "Welcomes the player 30 seconds into the match"
    metadata.author = "YourName"

    -- Validate
    local valid, error_msg = EventMetadata.validate(metadata)
    if not valid then
        print("[welcome_commander] ERROR: " .. error_msg)
    end

    return metadata
end
```

Note: We didn't set `on_event_registered_function` or `on_cancel_function` — they're optional and default to `nil`.

### Step 4: Write the Register Function

```lua
function welcome_commander_register(context)
    -- Always register this event
    return true
end
```

### Step 5: Write the Initialize Function

```lua
function welcome_commander_initialize(context)
    print("[welcome_commander] Initialized! Waiting 30 seconds...")
    context.instance.ready_to_trigger = false

    -- Register a timer to fire after 30 seconds
    context.timers.register({
        name = "welcome_delay",
        interval_seconds = 30.0,
        on_complete = "welcome_commander_timer_done",
        is_repeating = false
    })
end
```

### Step 6: Write the Timer Callbacks

```lua
-- Called after 30 seconds — allows the event to trigger
function welcome_commander_timer_done(context)
    print("[welcome_commander] 30 seconds elapsed!")
    context.instance.ready_to_trigger = true
end

-- Called after the display duration — completes the event
function welcome_commander_display_done(context)
    print("[welcome_commander] Display duration elapsed, completing.")
    context.complete()
end
```

We need two callbacks: one to trigger the event after the initial delay, and one to complete it after the HUD message has been shown long enough. If we completed immediately in `on_start` or on the first `on_update`, the text would be set and cleared in the same frame — the player would never see it.

### Step 7: Write the Trigger Check

```lua
function welcome_commander_should_trigger(context)
    return context.instance.ready_to_trigger == true
end
```

### Step 8: Write the Start, Update, Complete, and Teardown Functions

```lua
local DISPLAY_DURATION_SECONDS = 10.0

function welcome_commander_on_start(context)
    print("[welcome_commander] Event started!")

    -- Display a message on the HUD that the player can actually see!
    -- "timer_label" is one of four text slots in the HUD's script window.
    context.simulation:display_text("timer_label", "Welcome, Commander!")
    context.simulation:display_text("timer_value", "The galaxy awaits your command.")

    -- Keep the message visible for 10 seconds, then auto-complete.
    -- If we completed immediately, the text would be set and cleared
    -- in the same frame and the player would never see it.
    context.timers.register({
        name = "welcome_display_timer",
        interval_seconds = DISPLAY_DURATION_SECONDS,
        on_complete = "welcome_commander_display_done",
        is_repeating = false
    })
end

function welcome_commander_on_update(context)
    -- Nothing to do while the message is showing.
    -- The display timer handles completion for us.
end

function welcome_commander_on_complete(context)
    print("[welcome_commander] Event completed!")

    -- Clear the HUD text (set to empty string to hide)
    context.simulation:display_text("timer_label", "")
    context.simulation:display_text("timer_value", "")
end

function welcome_commander_on_teardown(context)
    print("[welcome_commander] Teardown complete.")
end
```

### The Complete File

Here is the full `welcome_commander.lua` assembled:

```lua
local EventMetadata = require("event_metadata")

local DISPLAY_DURATION_SECONDS = 10.0

-- Timer callback: fires after 30 seconds to allow triggering
function welcome_commander_timer_done(context)
    print("[welcome_commander] 30 seconds elapsed!")
    context.instance.ready_to_trigger = true
end

-- Timer callback: fires after display duration to complete the event
function welcome_commander_display_done(context)
    print("[welcome_commander] Display duration elapsed, completing.")
    context.complete()
end

-- Register: always load this event
function welcome_commander_register(context)
    return true
end

-- Metadata: tell the engine about this event
function get_event_metadata()
    local metadata = EventMetadata.create()

    metadata.event_name = "welcome_commander"
    metadata.event_id = "welcome_commander_v1"

    metadata.register_event_function = "welcome_commander_register"
    metadata.on_initialize_function = "welcome_commander_initialize"
    metadata.should_trigger_function = "welcome_commander_should_trigger"
    metadata.on_start_function = "welcome_commander_on_start"
    metadata.on_update_function = "welcome_commander_on_update"
    metadata.on_complete_function = "welcome_commander_on_complete"
    metadata.on_teardown_function = "welcome_commander_on_teardown"

    metadata.trigger_check_interval_seconds = 5.0
    metadata.update_interval_seconds = 1.0

    metadata.priority = 2.0  -- must be unique across all events!
    metadata.description = "Welcomes the player 30 seconds into the match"
    metadata.author = "YourName"

    local valid, error_msg = EventMetadata.validate(metadata)
    if not valid then
        print("[welcome_commander] ERROR: " .. error_msg)
    end

    return metadata
end

-- Initialize: per-instance setup
function welcome_commander_initialize(context)
    print("[welcome_commander] Initialized! Waiting 30 seconds...")
    context.instance.ready_to_trigger = false

    context.timers.register({
        name = "welcome_delay",
        interval_seconds = 30.0,
        on_complete = "welcome_commander_timer_done",
        is_repeating = false
    })
end

-- Should trigger: check if timer has fired
function welcome_commander_should_trigger(context)
    return context.instance.ready_to_trigger == true
end

-- On start: display HUD message and start display timer
function welcome_commander_on_start(context)
    print("[welcome_commander] Event started!")

    context.simulation:display_text("timer_label", "Welcome, Commander!")
    context.simulation:display_text("timer_value", "The galaxy awaits your command.")

    -- Keep the message visible, then auto-complete
    context.timers.register({
        name = "welcome_display_timer",
        interval_seconds = DISPLAY_DURATION_SECONDS,
        on_complete = "welcome_commander_display_done",
        is_repeating = false
    })
end

-- On update: nothing to do while the message is showing
function welcome_commander_on_update(context)
end

-- On complete: clear the HUD text
function welcome_commander_on_complete(context)
    print("[welcome_commander] Event completed!")
    context.simulation:display_text("timer_label", "")
    context.simulation:display_text("timer_value", "")
end

-- On teardown: final cleanup
function welcome_commander_on_teardown(context)
    print("[welcome_commander] Teardown complete.")
end
```

### Step 9: Test It

1. Place the file in `sins2_data/data/scripts/events/`.
2. Launch the game using one of the methods from [How to See print() Output](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#how-to-see-print-output) so you can see your debug messages. The easiest way is to run the exe from VS Code's integrated terminal.
3. Start a new match and wait about 30 seconds. You should see "Welcome, Commander!" appear on the HUD for 10 seconds, then disappear. Check the console output for `[welcome_commander]` messages confirming the lifecycle ran.

### Ideas to Expand

Once the basic event works, try these modifications:

- **Show an in-game notification** using `context.show_notification()` so players actually see something happen (see [Notifications Reference](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#notifications) below). This is the real way to communicate with players — `print()` is just for developer debugging.
- **Repeat the event** every 5 minutes by setting `ready_to_trigger = true` again in `on_complete`.
- **Add a shared counter** to track how many times the event has fired across the match.

---

## Part 5: Reference

### Context Fields

The `context` table is passed to every handler function. Different fields are available in different phases.

| Field | Type | Available In | Description |
| --- | --- | --- | --- |
| `simulation` | userdata | All handlers | The simulation object for querying game state |
| `event_name` | string | All handlers | Your event's name (from metadata) |
| `event_id` | string | on_event_registered | Your event's versioned ID |
| `shared` | table | on_event_registered, on_initialize, on_start, on_update, on_complete, on_cancel, on_teardown | Shared state across all instances |
| `instance` | table | on_initialize, on_start, on_update, on_complete, on_cancel, on_teardown | Per-instance state |
| `instance_id` | integer | on_initialize, on_start, on_update, on_complete, on_cancel, on_teardown | Unique ID for this instance |
| `active_instance_count` | integer | on_initialize, on_start, on_update, on_complete, on_cancel, on_teardown | Number of currently running instances |
| `current_tick` | integer | All handlers | Current simulation tick (deterministic) |
| `elapsed_time` | float | on_start, on_update, on_complete, on_cancel, on_teardown | Seconds since this instance started |
| `complete()` | function | on_update | Call to mark the event as successfully completed |
| `cancel()` | function | on_update | Call to cancel the event |
| `timers` | table | on_initialize, on_start, on_update, on_complete, on_cancel, on_teardown | Timer API (see below) |
| `show_notification` | function | on_start, on_update, on_complete, on_cancel | Show a game notification |
| `random_float()` | function | Instance handlers | Returns a deterministic random float 0.0–1.0 |
| `random_integer(min, max)` | function | Instance handlers | Returns a deterministic random integer in [min, max] |

### Timers API

Timers let you schedule function calls in the future.

#### context.timers.register

Register a new timer from within an event handler:

```lua
context.timers.register({
    name = "my_timer",               -- unique name (string, required)
    interval_seconds = 10.0,         -- delay before firing (number, required)
    on_complete = "my_callback",     -- function name to call (string, required)
    is_repeating = false             -- fire once (false) or repeat (true)
})
```

The callback function receives the same `context` as other handlers:

```lua
function my_callback(context)
    print("Timer fired!")
end
```

#### context.timers.cancel

Cancel a timer by name:

```lua
context.timers.cancel("my_timer")
```

Safe to call even if the timer doesn't exist or has already fired.

#### Timers.is_active (Global API)

Check if a timer is still active:

```lua
if Timers.is_active(context.simulation, "my_timer") then
    print("Timer is still running")
end
```

**Note:** Timers registered through `context.timers.register()` are automatically cleaned up when the instance is destroyed. You don't need to cancel them in `on_teardown`.

### Events API

Control events programmatically using the global `Events` table.

#### Events.trigger

Manually trigger an event:

```lua
Events.trigger(context.simulation, "some_other_event")
```

Returns `true` if successfully triggered.

#### Events.complete

Complete a running event from outside:

```lua
Events.complete(context.simulation, "some_event")
```

#### Events.cancel

Cancel a running event from outside:

```lua
Events.cancel(context.simulation, "some_event")
```

#### Events.is_running

Check if an event is currently active:

```lua
if Events.is_running(context.simulation, "pirate_incursion") then
    print("Pirates are attacking!")
end
```

#### Events.get_elapsed_time

Get how long an event has been running:

```lua
local time = Events.get_elapsed_time(context.simulation, "pirate_incursion")
```

Returns `0` if the event isn't running.

### HUD Script Window (display_text)

The HUD has a built-in **script window** with four text slots that your event can write to. Unlike `print()`, this text appears **on screen in the actual game HUD** — players will see it.

#### simulation:display_text

Show raw text on the HUD:

```lua
context.simulation:display_text("timer_label", "Pirate Raid")
context.simulation:display_text("timer_value", "2:30 remaining")
```

To hide a label, set it to an empty string:

```lua
context.simulation:display_text("timer_label", "")
```

#### simulation:display_loc_text

Show localized text using a localization ID (for multi-language support):

```lua
context.simulation:display_loc_text("progress_label", "my_mod_event_progress_label")
```

The localization ID must exist in the game's localization files.

#### Available Label IDs

The script window has four text slots you can write to independently:

| Label ID | Intended Use |
| --- | --- |
| `"timer_label"` | Left-side label for a timer (e.g., "Time Remaining") |
| `"timer_value"` | Right-side value for a timer (e.g., "2:30") |
| `"progress_label"` | Left-side label for progress (e.g., "Enemies Defeated") |
| `"progress_value"` | Right-side value for progress (e.g., "3 / 10") |

These names suggest timer/progress usage, but you can put any text in any slot. The window automatically shows when any slot has text and hides when all slots are empty.

#### Example: Countdown Timer

```lua
function my_event_on_update(context)
    local remaining = 120 - context.elapsed_time
    if remaining <= 0 then
        context.simulation:display_text("timer_label", "")
        context.simulation:display_text("timer_value", "")
        context.complete()
    else
        local minutes = math.floor(remaining / 60)
        local seconds = math.floor(remaining % 60)
        context.simulation:display_text("timer_label", "Raid ends in:")
        context.simulation:display_text("timer_value",
            string.format("%d:%02d", minutes, seconds))
    end
end
```

**Important:** Remember to clear the text slots when your event completes or is cancelled, otherwise the text will stay on screen.

### Notifications

Show in-game notifications to players using predefined notification types.

#### Usage

```lua
context.show_notification(NOTIFICATION_CONSTANT, {parameter = value})
```

#### Available Notification Types

| Constant | Description | Parameters |
| --- | --- | --- |
| `NOTIFY_PLANET_BEING_BOMBED` | Planet under bombardment | `{planet = unit}` |
| `NOTIFY_PLANET_COLONIZED` | Planet colonized | `{planet = unit}` |
| `NOTIFY_PLANET_LOST` | Planet lost to enemy | `{planet = unit}` |
| `NOTIFY_ENEMY_UNITS_ARRIVED` | Enemy units arrived | `{planet = unit}` |
| `NOTIFY_PIRATE_UNITS_ARRIVED` | Pirate units arrived | `{planet = unit}` |
| `NOTIFY_INSURGENT_UNITS_ARRIVED` | Insurgent units arrived | `{planet = unit}` |
| `NOTIFY_PLANET_DISCOVERED` | New planet discovered | `{planet = unit}` |
| `NOTIFY_NPC_DISCOVERED` | NPC discovered | `{planet = unit}` |
| `NOTIFY_DERELICT_LOOT_DISCOVERED` | Derelict loot found | `{loot_unit_id = id}` |
| `NOTIFY_UNIT_LEVELED_UP` | Unit leveled up | `{planet = unit}` |
| `NOTIFY_PIRATE_INCURSION_STARTED` | Pirate incursion begins | `{}` |
| `NOTIFY_PIRATE_INCURSION_PLAYER_TARGETED` | Player targeted by pirates | `{player_id = id}` |
| `NOTIFY_PIRATE_KING_ARRIVED` | Pirate King arrived | `{planet = unit}` |
| `NOTIFY_PIRATE_INCURSION_TARGET_CHANGED` | Pirate target changed | `{player_id = id}` |
| `NOTIFY_PIRATE_INCURSION_ENDED` | Pirate incursion over | `{}` |

#### Example

```lua
context.show_notification(NOTIFY_PIRATE_INCURSION_STARTED, {})
context.show_notification(NOTIFY_ENEMY_UNITS_ARRIVED, {planet = some_planet_unit})
```

**Performance note:** Always use the `NOTIFY_*` constants rather than constructing notification types at runtime. The constants use pre-computed hashes for fast comparison.

### Common Patterns

#### Pattern: Timer-Based Triggering

The most common pattern — use a timer to delay the first trigger:

```lua
function my_event_initialize(context)
    context.instance.ready_to_trigger = false
    context.timers.register({
        name = "initial_delay",
        interval_seconds = 60.0,
        on_complete = "my_event_ready_callback",
        is_repeating = false
    })
end

function my_event_ready_callback(context)
    context.instance.ready_to_trigger = true
end

function my_event_should_trigger(context)
    return context.instance.ready_to_trigger == true
end
```

#### Pattern: Auto-Complete After Duration

Run the event for a fixed time, then complete:

```lua
function my_event_on_start(context)
    -- ... do event setup ...

    context.timers.register({
        name = "auto_complete",
        interval_seconds = 30.0,
        on_complete = "my_event_auto_complete",
        is_repeating = false
    })
end

function my_event_auto_complete(context)
    context.complete()
end
```

#### Pattern: Condition-Based Completion

Check a condition every update and complete when it's met:

```lua
function my_event_on_update(context)
    local enemy_count = get_remaining_enemies(context)
    if enemy_count == 0 then
        print("[my_event] All enemies defeated!")
        context.complete()
    end
end
```

#### Pattern: Repeating Event with Cooldown

Make an event trigger, run, complete, and then trigger again after a cooldown:

```lua
function my_event_on_complete(context)
    -- ... give rewards ...

    -- Start cooldown timer, then allow re-trigger
    context.timers.register({
        name = "cooldown",
        interval_seconds = 120.0,
        on_complete = "my_event_cooldown_done",
        is_repeating = false
    })
end

function my_event_cooldown_done(context)
    context.instance.ready_to_trigger = true
end
```

#### Pattern: Shared State for Instance Coordination

Use `context.shared` to coordinate between multiple instances:

```lua
function my_event_on_event_registered(context)
    context.shared.active_count = 0
    context.shared.max_concurrent = 3
end

function my_event_should_trigger(context)
    if context.shared.active_count >= context.shared.max_concurrent then
        return false
    end
    return context.instance.ready_to_trigger == true
end

function my_event_on_start(context)
    context.shared.active_count = context.shared.active_count + 1
    -- ... event logic ...
end

function my_event_on_complete(context)
    context.shared.active_count = context.shared.active_count - 1
end
```

#### Pattern: Safe Nil Checks

Always guard against `nil` when reading state that might not be set yet:

```lua
-- Use (value or default) to provide fallbacks
local counter = (context.instance.counter or 0) + 1
local name = context.instance.name or "unknown"

-- Check before using
if context.instance.target_id then
    -- safe to use target_id
end
```

### Metadata Fields Quick Reference

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `api_version` | float | Yes | `0.5` | API version (don't change) |
| `event_name` | string | Yes | `""` | Event name for code references |
| `event_id` | string | Yes | `""` | Versioned ID for compatibility |
| `register_event_function` | string | Yes | `""` | Registration check function |
| `on_event_registered_function` | string | No | `nil` | One-time setup function |
| `on_initialize_function` | string | Yes | `""` | Per-instance setup function |
| `should_trigger_function` | string | Yes | `""` | Trigger check function |
| `on_start_function` | string | Yes | `""` | Event start function |
| `on_update_function` | string | Yes | `""` | Periodic update function |
| `on_complete_function` | string | Yes | `""` | Completion handler |
| `on_cancel_function` | string | No | `nil` | Cancellation handler |
| `on_teardown_function` | string | Yes | `""` | Final cleanup function |
| `on_unit_death_function` | string | No | `nil` | Tracked unit death handler |
| `trigger_check_interval_seconds` | float | Yes | `1.0` | Seconds between trigger checks |
| `update_interval_seconds` | float | Yes | `0.1` | Seconds between updates |
| `on_update_function_initial_delay` | float | No | `0.0` | Delay before first update |
| `description` | string | No | `""` | Human-readable description |
| `author` | string | No | `""` | Author name |
| `priority` | float | No | `1.0` | Trigger priority (higher = first, **must be unique**) |
| `incompatible_event_ids` | table | No | `{}` | Event IDs that can't co-exist |
| `max_concurrent_instances` | integer | No | `-1` | Max simultaneous instances (-1 = unlimited, hard cap: 32) |

### Glossary

| Term | Definition |
| --- | --- |
| **Event** | A scripted occurrence during a match, defined by a Lua file |
| **Instance** | A single running copy of an event. One event can have multiple instances |
| **Trigger checker** | The first instance created for an event; its job is to decide when the event should fire |
| **Context** | The table passed to every handler function, containing simulation state and event info |
| **Shared state** | Data in `context.shared` visible to all instances of the same event |
| **Instance state** | Data in `context.instance` private to one specific instance |
| **Metadata** | The configuration returned by `get_event_metadata()` that describes your event |
| **Handler** | A function called by the engine at a specific lifecycle phase |
| **Timer** | A scheduled callback that fires after a delay |
| **Simulation** | The game's simulation object; used to query and modify game state |
| **Gravity well** | The area of space around a planet or star |
| **Deterministic** | Producing the same result on every player's computer — required for multiplayer sync |
| **NPC** | Non-player character (Minor faction) |

### Next Steps

Once you're comfortable with the basics, study `pirate_incursion.lua` in this same folder for a real-world example that demonstrates:

- Spawning and tracking units with `spawn_units_definition` and unit trackers
- Multi-phase state machines (staging, raiding, advancing)
- Issuing move orders to units with `simulation:issue_move_order()`
- Coordinating multiple instances with shared state
- Player targeting and economic score queries
- Notification system usage
- AI threat registration for coordinated defense

---

## Appendix A: Lua Crash Course

This appendix covers the bare minimum Lua you need to write event scripts. If you already know Lua or another programming language, you can skim or skip this.

### Comments

Comments are notes for humans. The game ignores them.

```lua
-- This is a single-line comment

--[[
   This is a
   multi-line comment
]]
```

### Variables and Types

A **variable** is a named container that holds a value.

```lua
local name = "Pirate King"     -- string (text)
local health = 100             -- number (integer)
local speed = 3.5              -- number (decimal)
local is_alive = true          -- boolean (true or false)
local target = nil             -- nil (nothing / no value)
```

The `local` keyword means "this variable only exists in this area of code." Always use `local` unless you have a reason not to.

#### Strings

Strings are text wrapped in quotes. You can glue strings together with `..` (two dots):

```lua
local greeting = "Hello, " .. "world!"   -- "Hello, world!"
local count = 5
print("Wave " .. count .. " spawned!")   -- "Wave 5 spawned!"
```

Use `tostring()` to safely convert any value to a string:

```lua
local number_value = 42
print("The answer is " .. tostring(number_value))
```

### Tables

A **table** is Lua's one-size-fits-all container. It can act as a list (array) or a dictionary (key-value pairs).

#### As a list (array):

```lua
local ships = {"frigate", "cruiser", "capital_ship"}

print(ships[1])   -- "frigate"   (Lua arrays start at 1, not 0!)
print(ships[2])   -- "cruiser"
print(#ships)     -- 3           (# gives the length)
```

#### As a dictionary (key-value pairs):

```lua
local config = {
    wave_count = 5,
    delay_seconds = 30.0,
    event_name = "my_event"
}

print(config.wave_count)       -- 5
print(config["delay_seconds"]) -- 30.0  (alternate syntax)
```

#### Adding to tables:

```lua
-- Add to end of a list
local items = {}
table.insert(items, "sword")
table.insert(items, "shield")
-- items is now {"sword", "shield"}

-- Add a key-value pair
local state = {}
state.counter = 0
state.is_ready = false
```

### Functions

A **function** is a reusable block of code with a name.

```lua
-- Defining a function
function say_hello(player_name)
    print("Hello, " .. player_name .. "!")
end

-- Calling a function
say_hello("Admiral")   -- prints: Hello, Admiral!
```

Functions can **return** a value:

```lua
function add(a, b)
    return a + b
end

local result = add(3, 7)   -- result is 10
```

Functions can be stored as `local` too:

```lua
local function calculate_supply(wave_number)
    return 50 + (wave_number * 25)
end
```

### If / Else

Make decisions based on conditions:

```lua
local health = 75

if health > 50 then
    print("Healthy")
elseif health > 25 then
    print("Wounded")
else
    print("Critical!")
end
```

#### Comparison operators:

| Operator | Meaning |
| --- | --- |
| `==` | equals |
| `~=` | not equals |
| `<` | less than |
| `>` | greater than |
| `<=` | less than or equal to |
| `>=` | greater than or equal to |

#### Logical operators:

```lua
if health > 0 and is_alive then
    print("Still fighting!")
end

if health <= 0 or not is_alive then
    print("Defeated!")
end
```

### Loops

#### For loop (counting):

```lua
for i = 1, 5 do
    print("Wave " .. i)
end
-- Prints: Wave 1, Wave 2, Wave 3, Wave 4, Wave 5
```

#### For loop (over a table):

```lua
local ships = {"frigate", "cruiser", "capital_ship"}
for index, ship_name in ipairs(ships) do
    print(index .. ": " .. ship_name)
end
```

### print() for Debugging

`print()` writes a message to the game's **executable console** — a developer-only window that most players will never see. It does **not** show anything in the game UI. Think of it as a developer scratchpad, not a way to talk to the player.

```lua
print("[my_event] Starting wave 3")
print("[my_event] Player health: " .. tostring(health))
```

To show something to the player in-game, use `context.simulation:display_text()` or `context.show_notification()` (covered in [Part 5: Reference](https://stardock.atlassian.net/wiki/spaces/SSEFW/pages/3170238513/Duplicate+of+Lua+Event+Tutorial#part-5-reference)).

**Tip:** Always prefix your print messages with your event name in brackets (like `[my_event]`) so you can find them in the console output.

#### How to See print() Output

The game executable writes `print()` output to **stdout** — the standard output stream. By default, when you launch the game through Steam or Epic, this output goes nowhere visible. Here are two ways to capture it:

**Finding your game folder**

First, you need to know where the game is installed:

- **Steam:** Right-click the game in your Library > **Manage** > **Browse local files**. This opens the game folder in Windows Explorer.
- **Epic:** Click the three dots next to the game > **Manage** > look for the install path, or use the folder icon.

The game executable is called `sins2.exe` and lives in that folder.

**Option A: Launch the game from VS Code's terminal (easiest)**

1. Open VS Code.
2. Open the integrated terminal (`Ctrl+`` or **View > Terminal**).
3. Navigate to your game's install folder and run the executable:
  (Your path may differ — use the path from "Browse local files" above.)
4. All `print()` output appears right in the VS Code terminal panel, scrollable and searchable with `Ctrl+F`.

**Option B: Pipe output to a log file**

Open a command prompt or PowerShell, navigate to the game folder, and run:

```
.\sins2.exe > event_log.txt 2>&1
```

This redirects all output to `event_log.txt` in the same folder. Open that file in VS Code — it auto-refreshes as new lines are written, so you can watch it update in real time. Use `Ctrl+F` to search for your event name.

**Which option should I use?**

- **Option A** is better for interactive debugging — you see output in real time and can scroll back.
- **Option B** is better if you want a persistent log you can search through after the game closes.

### require()

`require()` loads another Lua file and gives you what it returns:

```lua
local EventMetadata = require("event_metadata")
```

Every event script **must** require `event_metadata` — it's how you tell the engine about your event.

### Further Reading

This crash course only covers what you need for event scripts. If you want to learn Lua more thoroughly, the official reference is:

- [Lua 5.4 Reference Manual](https://www.lua.org/manual/5.4/) — the complete language specification
- [Programming in Lua (first edition)](https://www.lua.org/pil/contents.html) — a free online book that teaches Lua step by step