---
id: gh-msw-scripting
name: "msw-scripting"
url: https://skills.yangsir.net/skill/gh-msw-scripting
author: msw-git
domain: game-dev
tags: ["scripting", "game-development", "debugging", "lua", "workflow"]
install_count: 2800
rating: 4.40 (120 reviews)
github: https://github.com/msw-git/msw-ai-coding-plugins-official/tree/main/plugins/msw-maker-base-skill/skills/msw-scripting
---

# msw-scripting

> 此技能专注于 MSW 脚本 (.mlua) 的编写、测试与调试，涵盖 mlua 语法、MSW 特有注解、生命周期、执行空间、属性同步、事件系统及文件工作流。它帮助开发者理解 MSW 脚本的独特机制，通过集成测试与调试循环，确保脚本的正确性和高效运行，从而加速游戏逻辑的开发与迭代。

**Stats**: 2,800 installs · 4.4/5 (120 reviews)

## Before / After 对比

### MSW 脚本开发效率

**Before**:

在没有 msw-scripting 之前，开发者需要手动查阅大量文档，反复试错才能理解 MSW 脚本的独特机制和 API 用法。这导致脚本开发周期长，运行时错误频发，调试过程耗时且效率低下。

**After**:

此技能提供全面的 mlua 语法、MSW 特有注解和工作流指导，帮助开发者快速掌握脚本编写规范。通过集成的测试与调试循环，该 Skill 大幅减少了运行时错误和调试时间，显著提升了脚本开发效率和质量。

| Metric | Before | After | Change |
|---|---|---|---|
| 调试时间 | 60分钟 | 10分钟 | -83% |

## Readme

# MSW Scripting (.mlua) — Framework + File Workflow + Playtest & Debugging

mlua is Lua-based, but it has MSW-specific annotations, a lifecycle, and an execution-space model.
General Lua knowledge alone will not produce working code. All work is done by **editing files in the workspace directly**,
and code is validated in the order **build logs → runtime logs**.

---

## 1. Core Principles (must follow)

### 1.1 Existing Script First

Before creating a new `.mlua`, glob/keyword-search under `./RootDesk/MyDesk/` for an existing script with the same purpose — **extending an existing file is always the first choice**. Duplicate implementations raise maintenance cost and conflict risk.

### 1.2 Folder Structure for New Scripts — Never Dump Files Flat

When a new `.mlua` is unavoidable, place it under a feature/category subfolder. **Required path shape**: `./RootDesk/MyDesk/<FeatureFolder>/<ScriptName>.mlua`.

- **Reuse** an existing subfolder if it fits (`Player/`, `UI/`, `Combat/`, `Inventory/`, …); glob `./RootDesk/MyDesk/` first.
- **Otherwise create** one named for the feature (PascalCase). All related scripts of one feature (Component/Logic/Event/Struct) stay together. Even a single-file feature gets its own folder.
- **Forbidden**: catch-all folders like `Scripts/`, `Misc/`, `Common/`, `New/`, `temp/`. A flat root makes rule §1.1 (search before creating) impossible.

Examples: `Inventory/InventoryManager.mlua`, `Combat/MeleeAttackComponent.mlua`, `UI/Popup/RewardPopupLogic.mlua`.

### 1.3 Never Guess APIs — Verify Before Writing

Guessing an MSW API name/param/return type **silently fails at runtime**. Required order: **`.d.mlua` for signature** → **`msw-search` for semantics/examples** if needed → write → LSP diagnose (auto-run).

The engine API lives under `./Environment/NativeScripts/`:

| Folder | Contents | Count |
|------|------|:-:|
| `Component/` | Engine components | 104 |
| `Service/` | System services | 46 |
| `Event/` | Event types | 202 |
| `Logic/` | Built-in logic | 9 |
| `Enum/` | Enumerations | 118 |
| `Misc/` | Utility types (Vector2, …) | 140 |

Known name → `Read ./Environment/NativeScripts/{folder}/{name}.d.mlua`. Unknown name → Grep keywords there.

### 1.4 Lint (LSP diagnostics)

`mlua-diagnose` hook runs LSP `diagnose` automatically after every `.mlua` create/modify. Iterate fix → re-edit until error-severity diagnostics reach zero.

### 1.5 `.codeblock` & Refresh

- `.codeblock` files are generated by Maker Refresh — never create/edit/delete manually.
- After any `.mlua` create/modify/rename/delete, call Maker MCP **`refresh`**. Refresh requires edit mode — `stop` first if playing.

### 1.6 MSW ≠ Unity — Do Not Reason From Intuition

Applying Unity/generic patterns directly **compiles fine but silently fails at runtime**. Common misconceptions:

| Unity intuition | MSW reality / Where it's covered |
|---|---|
| `gameObject` / `transform` from a global manager | `@Logic` has no `self.Entity` — see §3.2 (use property injection / `_EntityService`) |
| `OnMouseDown` / `BoxCollider2D` for clicks | Physics colliders never emit `TouchEvent` — World uses `TouchReceiveComponent` (§10); UI uses `ButtonComponent`/`UITouchReceiveComponent` |
| `OnCollisionEnter` + Rigidbody | Entity↔entity collisions need `TriggerComponent` + `TriggerEnter/Leave/Stay` event |
| UI field names (`interactable`/`text`/`color`) | MSW-specific names — check [`msw-ui-system/references/component-api.md`](../msw-ui-system/references/component-api.md). Common mappings: disable→`Enable`, text→`Text`, text color→`FontColor`, tint→`Color`. `ButtonComponent.Interactable` doesn't exist. |
| Attach multiple Rigidbody/Collider freely | **One Body per map type** — see [`msw-general/references/platform.md`](../msw-general/references/platform.md) §4 |
| Touch UI from server code | **UI is client-only** — server→UI goes via `@ExecSpace("Client")` RPC. Hosting `Server`/`ServerOnly`/`Multicast`/`@Sync` on a UI-attached Component silently no-ops with runtime warning. See [`msw-ui-system/references/runtime-patterns.md`](../msw-ui-system/references/runtime-patterns.md) |
| `Instantiate(prefab)` callable anywhere | `_SpawnService:SpawnByModelId(id, name, pos, parent)` — `parent` required, server-only — see §11 |
| `static` classes / hand-rolled singletons | `@Logic` is itself the singleton — call as `_ScriptName:Method()`, never instantiate — see §3.2 |

**Rule**: when tempted to apply a Unity pattern, stop and verify against `Environment/NativeScripts/*.d.mlua` first.

### 1.7 Builder Protocol Preflight — **MUST**

If this turn touches `.map` / `.model` / `.ui` (directly, or via spawn/entity-placement/UI-binding code in `.mlua`), **`Read` [`../msw-general/references/builder-protocol.md`](../msw-general/references/builder-protocol.md) first** (full file, every turn — do not skip on prior-turn memory). It consolidates the write-side contracts (`componentNames` sync, `typeKey` metadata, auto-lint, child-entity invariants, `placeModel` mirroring) for all three builders; knowing one builder doesn't cover another.

**Triggers** (broad on purpose): `_SpawnService` / `SpawnByModelId` / `SpawnByEntity`; any `.map`/`.model`/`.ui` change; calling `msw_map_builder.cjs` / `msw_model_builder.cjs` / `msw_ui_builder.cjs`; any "new monster/NPC/popup/map object" request; §11 or §16 work.

### 1.8 Method Documentation Comments — Inside the Body

Every `method` (lifecycle, RPC, event handler, user-defined) **must** have a description comment as the **first line inside the body**, never above the declaration. mlua's parser binds leading comments to the previous declaration, so an "above" comment is unreliable.

```lua
-- ✅ Correct
method void ApplyDamage(Entity target, number amount)
    -- Applies damage and triggers hit VFX.
    target:TakeDamage(amount)
end

-- ❌ Wrong — comment above the method
-- Applies damage...
method void ApplyDamage(Entity target, number amount)
    target:TakeDamage(amount)
end
```

---

## 2. Paths and File Roles

| Target | Path | Agent action |
|------|------|----------------|
| User scripts | `./RootDesk/MyDesk/**/*.mlua` | **Create / read / modify / delete directly** |
| Auto-generated artifacts | `*.codeblock` | **Do not touch** (Refresh manages them) |
| Engine API definitions | `./Environment/NativeScripts/**` | **Read-only** (do not modify) |
| Models (component lists) | `./RootDesk/MyDesk/*.model`, `./Global/*.model`, etc. | Edit `Components` **when attaching scripts** |
| Map instances | `./map/*.map` | Edit when attaching scripts to entities that exist only inside a map |

---

## 3. Script Types and Declarations

### 3.1 Component scripts (`@Component`)

Scripts attached to an Entity. Use `self.Entity` to access the owning entity.

```lua
@Component
script MyScript extends Component
    property number Speed = 5.0

    @ExecSpace("ServerOnly")
    method void OnBeginPlay()
        -- initialization (also: OnUpdate(delta), OnEndPlay)
    end
end
```

**Allowed parents**:
- `Component` — generic component
- `AttackComponent` — attack system (Shape, AttackFast, OnAttack)
- `HitComponent` — hit system (OnHit, HandleHitEvent)

### 3.2 Logic scripts (`@Logic`)

Global singletons. Run independently without an Entity. Use for game managers, UI managers, utilities, etc.

```lua
@Logic
script GameManager extends Logic
    @Sync property integer Score = 0

    @ExecSpace("ServerOnly")
    method void OnBeginPlay()
        -- global initialization (also: OnUpdate, OnEndPlay)
    end
end
```

- One per world (singleton)
- Accessed as `_<ExactScriptName>` — **no suffix stripping**. `TDHUDLogic.mlua` → `_TDHUDLogic` (not `_TDHUD`); `TowerDefenseConfig.mlua` → `_TowerDefenseConfig`. Heuristic stripping silently returns `nil`.
- Supports `@Sync` properties (server→client)
- Logic's `OnUpdate` runs **before** Components'.

> ⚠️ **`@Logic` has no `self.Entity`** — Logic parent only exposes `ConnectEvent`/`DisconnectEvent`/`IsClient`/`IsServer`/`SendEvent`. `self.Entity.xxx` compiles but is a runtime nil-access. To bind a world entity, inject via property (`property Entity x = "uuid"` / `property EntityRef x = ""`) or look it up with `_EntityService:GetEntityByPath(...)` / `:FindEntityByName(...)`. Property injection (UUID literal) is preferred. See §7.
>
> ⚠️ **`OnMapEnter` / `OnMapLeave` never fire on `@Logic`** — they're Component-only (see §5). Declaring them on a Logic is silent dead code.

> **Decision: @Component vs @Logic — by lifetime, not "is it global?"**
>
> | Scope | Pick | Why |
> |---|---|---|
> | World-wide, survives every map transition (account state, world event bus, global UI manager) | **`@Logic`** | Engine singleton; lives for whole world session. |
> | **Map-scoped** — only meaningful inside one map (quest controller, wave spawner, puzzle) | **`@Component` on the map entity** | Cleaned on map unload. Putting this in `@Logic` leaks state/timers across maps. |
> | One actor (monster AI, item pickup, player skill) | **`@Component`** on that entity |  |
>
> Ask: *"Still running after the player walks to another map?"* — Yes ⇒ `@Logic`; No (this map) ⇒ `@Component` on map entity; No (this actor) ⇒ `@Component` on actor.

### 3.3 Extend scripts

```lua
@Component
script PlayerAttack extends AttackComponent
    -- Override parent methods; call parent via __base:MethodName()
end
```

### 3.4 Other script types

`@Event` (custom event) · `@Item` (inventory) · `@BTNode` (behaviour tree) · `@State` (state machine) · `@Struct` (composite data type).

---

## 4. mlua Language Extensions (vs. plain Lua)

Based on Lua 5.3 with these differences:

**Added syntax**:
- `continue` — skip to next loop iteration.
- Compound assignment: `+=`, `-=`, `*=`, `/=`, `//=`, `%=`, `^=`, `..=` (and bitwise `&=`, `|=`, `<<=`, `>>=`). Multi-assign (`a, b += 1, 2`) and use as a function arg (`print(a += 1)`) are invalid.
- Bitwise operators: `&`, `|`, `<<`, `>>`.

**Restrictions**:
- **No globals** (`global` keyword forbidden) — share values via Properties.
- **No coroutines** (`coroutine.*`).
- Parent call is `__base:MethodName()`, not `super`.

**Built-in utility functions**:

| Function | Purpose |
|------|------|
| `log()` / `log_warning()` / `log_error()` | Logging at each severity |
| `wait(seconds)` | Pause script execution |
| `isvalid(obj) → boolean` | Validity (handles deletion/nil) |
| `enum(t) → table` | Swap keys and values |
| `beginscope(name)` / `endscope()` | Profiling scopes |

---

## 5. Lifecycle

```
OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroy
                                ↑
                  OnMapEnter / OnMapLeave (Component only, per transition)
```

| Method | When | Where | Purpose |
|--------|------|------|------|
| `OnInitialize` | After creation | Component + Logic | Init internal vars (rarely used) |
| `OnBeginPlay` | Game start | Component + Logic | **Wire events, start timers, initial setup** |
| `OnUpdate(delta)` | Every frame | Component + Logic (**Logic first**) | Movement, animation, input |
| `OnMapEnter` / `OnMapLeave` | Map transition | **Component only** (silent no-op on Logic) | Per-map init/cleanup |
| `OnEndPlay` | Game end | Component + Logic | **Disconnect events, clear timers (mandatory!)** |
| `OnDestroy` | Removal | Component + Logic | Final cleanup (rarely used) |

**Required pattern**: everything connected in `OnBeginPlay` must be released in `OnEndPlay` (events, timers).

```lua
property any eventHandler = nil   -- EventHandlerBase (must be 'any'; not integer)
property integer timerId = 0

method void OnBeginPlay()
    self.eventHandler = self.Entity:ConnectEvent(SomeEvent, self.OnSomeEvent)
    self.timerId = _TimerService:SetTimerRepeat(self.Tick, 1/60)
end
method void OnEndPlay()
    if self.eventHandler then self.Entity:DisconnectEvent(SomeEvent, self.eventHandler) end
    if self.timerId then _TimerService:ClearTimer(self.timerId) end
end
```

---

## 6. Execution Space (ExecSpace)

MSW is a server-client architecture. Every method must declare where it runs.

| ExecSpace | Runs on | Direction | Use case |
|-----------|----------|----------|------|
| `ServerOnly` | Server | Server-internal only | Damage calc, state changes, spawning |
| `ClientOnly` | Client | Client-internal only | UI updates, effects, sounds |
| `Server` | Server | Client→Server RPC | Client requesting the server (attack, item use) |
| `Client` | Client | Server→Client RPC | Server notifying a client (result UI, effects) |
| `Multicast` | All clients | Server→all clients | Global events (announcements, boss spawn) |
| *(unspecified)* | Caller side | Server→Server, Client→Client | Shared functions executed locally on either side |

### ExecSpace constraints on lifecycle methods

| Method | Allowed ExecSpace |
|--------|---------------|
| `OnSyncProperty` | **`ClientOnly` only** |
| `OnInitialize`, `OnBeginPlay`, `OnUpdate`, `OnEndPlay`, `OnDestroy`, `OnMapEnter`, `OnMapLeave` | `ServerOnly`, `ClientOnly`, or **unspecified** |
| All event handlers | `ServerOnly`, `ClientOnly`, or **unspecified** |
| Custom user methods | Any of `Server`, `Client`, `ServerOnly`, `ClientOnly`, `Multicast` |

### Typical server-client pattern

```
[Client]  input (ClientOnly) ──Request()──→ [Server] validate (ServerOnly)
                                                ├─ state auto-syncs via @Sync
[Client]  UI update (ClientOnly) ←──Show()──────┘ (Client RPC)
```

- `ServerOnly`: client call is silently ignored (no error).
- `Server`: client→server RPC (network latency).
- `Client`: server→client RPC; add UserId as the **last call-site arg** to target one client (do NOT add it to the declaration).

### `senderUserId` — verifying the requester

Inside an `@ExecSpace("Server")` body, the local `senderUserId` holds the caller client's UserId (server-assigned, not client-modifiable). Use it for security checks.

```lua
@ExecSpace("Server")
method void RequestBuyItem(integer itemId)
    if senderUserId ~= self.Entity.PlayerComponent.UserId then return end
    self:ProcessPurchase(itemId)
end
```

### Reserved parameter names — `name is unavailable`

Four parameter names are reserved for the RPC marshaller and cannot be used as your own parameter names on any `@ExecSpace(...)` method. The LSP blocks the script with `'<name>' name is unavailable.`:

| Reserved | What the engine uses it for |
|---|---|
| `self` | Method receiver |
| `senderUserId` | Calling client's UserId on `@ExecSpace("Server")` bodies |
| `targetUserId` | Recipient client's UserId (last call-site arg on `@ExecSpace("Client")` bodies — do NOT declare it; the engine appends it) |
| `messageOwnerEntity` | Originating entity for some service callbacks |

Rename your own parameters when they collide (`targetUserId` → `forUserId`, `senderUserId` → `fromUserId`). `self` is the receiver and cannot be aliased — pick any other name for an unrelated parameter.

### Manual branching — `IsServer()` / `IsClient()` are **methods**, not properties

When a method has no `@ExecSpace` (runs on whichever side called it) and needs different paths per side, branch with `self:IsServer()` / `self:IsClient()`. Both are declared as `method boolean IsServer()` / `method boolean IsClient()` on `Component` and `Logic` — they must be **called**, not read.

```lua
if self:IsServer() then ... end   -- ✅ method call → boolean
if self.IsServer    then ... end  -- ❌ method object itself → always truthy
```

The dot-without-parens form is a silent bug: the LSP doesn't flag it, the script compiles, and the "if" always enters because a method object is truthy — so client-only code runs on the server too (or vice versa). The symptom is "both branches execute on both sides," not a crash. Use colon-call (`self:IsServer()`) every time.

### Cross-boundary parameter types

Allowed across server↔client RPC: `string`, `integer`, `number`, `boolean`, `table`, `Vector2/3/4`, `Color`, `Entity`, `Component`, `EntityRef`, `ComponentRef`. **`any` not allowed.** Engine enums also do not cross — neither typed (the LSP rejects engine enum types as parameters) nor smuggled via `any` (runtime `LEA-3036 InvalidCast`). Standard workaround: encode the choice as a `string` key on the sender, branch on the receiver, and convert back to the enum locally. `SyncTable<k,v>` generics must also be from the allowed list.

---

## 7. Property System

### Basic types

`number` (float/double — integers are separate type `integer`), `string`, `boolean`, `Vector2`/`Vector3`, `Color` (r,g,b,a in 0.0~1.0), `any`.

```lua
property number Speed = 5.0
property integer Count = 0
property Vector2 Direction = Vector2(0, 0)
property Color Tint = Color(1, 1, 1, 1)
```

### Entity / Component reference properties

```lua
property Entity targetEntity = "94a274e4-4111-40f1-924d-c95a3a1f14d5"   -- UUID string literal
property ButtonComponent btnOk = "uuid-string"                          -- typed component ref
```

**AI must inject UUIDs directly** — read `id` from `.map`/`.ui` and hard-code as string literal. Never ask the user to drag-bind in the editor (that's a human-author convenience).

### Entity vs EntityRef

`Entity` / `Component` references are **dropped on map transition**. `EntityRef` / `ComponentRef` **survive** map transitions — prefer for multi-map games.

### Sync annotations

- `@Sync` — server → all clients. One-way; client-side change does NOT propagate back. Has network latency.
- `@TargetUserSync` — server → owning user's client only. Useful for per-player private data (currency, achievements). On a non-PlayerEntity it falls back to plain `@Sync`.
- **Cannot be synced**: `any`, `table` — use `SyncTable` instead.
- Both take no arguments.

```lua
@Sync property number CurrentHp = 100
@TargetUserSync property number PrivateScore = 0
@Sync property SyncTable<number> Scores              -- array form, NO default literal
@Sync property SyncTable<string, number> Stats       -- dict form, NO default literal
```

#### `SyncTable<...>` property — no default literal

Declare `SyncTable<V>` (array form) or `SyncTable<K, V>` (dict form) **without** an `= ...` initializer. The engine reserves the `=` slot of a SyncTable property for its own type bookkeeping and auto-initializes the property to an **empty collection** at runtime. Any literal you write (`= {}`, `= { key = val }`, `= nil`) is silently dropped — it is misleading noise, not a real default, and a round-trip through the codeblock will erase it.

Populate initial entries in `OnInitialize` / `OnBeginPlay`:

```lua
@Sync property SyncTable<string, number> Stats       -- empty at construction
@Sync property SyncTable<number> Scores              -- empty at construction

method void OnBeginPlay()
    if self:IsServer() then
        self.Stats["hp"] = 100
        self.Stats["mp"] = 50
        self.Scores:Add(0)
    end
end
```

Assigning a plain Lua table to a `SyncTable` property at runtime is also rejected — the property accepts only its own proxy. Mutate it field by field (`self.Stats[k] = v`) or call its methods (`self.Scores:Add(v)` / `:Remove(v)` / `:Clear()`).

#### `SyncList<V>` is not a user property type

`SyncList<V>` is exposed only as a **readonly property on native engine Components** (e.g. `TagComponent.Tags`, `PhysicsColliderComponent.PolygonPoints`, `SkeletonRendererComponent.AnimationNames`, the various `JointComponent.Joints`). User scripts can **read** these and call their methods (`:Add(v)`, `:Remove(v)`, `:Clear()`, `.Count`, `:ToTable()`), but cannot declare `property SyncList<...> X` on their own `@Component` / `@Logic` and cannot instantiate `SyncList(...)`.

For synced collections in your own scripts, use `SyncTable<V>` (array form) or `SyncTable<K, V>` (dict form) — see above.

### Temporary properties (`_T`)

`self._T.<name>` is non-synced, declaration-free ad-hoc state. Server and client keep their own values; never shown in inspector. Cannot be `@Sync`'d.

### `OnSyncProperty` callback

Client-side hook fired when a `@Sync` property changes. **Must be `ClientOnly`** (cannot be changed). Available on Component and Logic.

```lua
@ExecSpace("ClientOnly")
method void OnSyncProperty(string name, any value)
    if name == "CurrentHp" then self:UpdateHpBar(value) end
end
```

### Property editor attributes

```lua
@DisplayName("...") @Description("...") @MaxLength(20) @HideFromInspector
@MinValue(0) @MaxValue(999) @Delta(5)   -- Delta = mobile +/- step
```

---

## 8. Event System / RPC

### Static subscription — `@EventSender` + `handler`

```lua
@EventSender("Self") handler HandleHitEvent(HitEvent event) ... end
@EventSender("Service", "InputService") handler HandleKeyDown(KeyDownEvent event) ... end
```

`@EventSender` 1st arg: `"Self"` / `"LocalPlayer"` (no 2nd arg) · `"Entity"`,id / `"Model"`,id / `"Service"`,typeName / `"Logic"`,typeName.

### Dynamic subscription — `ConnectEvent` / `DisconnectEvent`

```lua
self.clickHandler = entity:ConnectEvent(ButtonClickEvent, self.OnClick)  -- OnBeginPlay
entity:DisconnectEvent(ButtonClickEvent, self.clickHandler)              -- OnEndPlay (mandatory)
```

For per-element captured state (card IDs, slot indexes), use a closure handler; store the returned `EventHandlerBase` in a table and disconnect each in `OnEndPlay`.

```lua
for _, id in ipairs(cardIds) do
    local capturedId = id
    local h = e:ConnectEvent(ButtonClickEvent, function() self:OnCardClicked(capturedId) end)
    table.insert(self.clickHandlers, { entity = e, handler = h })
end
```

> ⚠️ **`ConnectEvent` is on Entity / Logic / Service — NOT Component.** Components only *emit* events; subscribe on the owning **Entity** (or `_InputService` / `_<LogicName>`). `self.Entity.ButtonComponent:ConnectEvent(...)` runtime nils.
>
> ```lua
> self.clickHandler = self.Entity:ConnectEvent(ButtonClickEvent, self.OnClick)
> self.keyHandler   = _InputService:ConnectEvent(KeyDownEvent, self.OnKeyDown)
> ```

> ⚠️ **`handler` vs `method void`** — `handler Name(Ev e)` pairs with `@EventSender(...)` and is wired by declaration. `method void Name(Ev e)` is the dynamic callback wired via `ConnectEvent(EvType, self.Name)`. Mixing them compiles but never fires (E-V1-5). If `@EventSender` is present → `handler`; if you'll call `ConnectEvent` → `method void`.



### CustomEvent — typed class style

The **only** way to author one is `@Event` + `extends EventType` with `property` fields. There is no inline factory.

```lua
@Event
script DamageDealtEvent extends EventType
    property number amount = 0
end

local dmg = DamageDealtEvent(); dmg.amount = 50
self.Entity:SendEvent(dmg)                                 -- via Entity / Logic / Service
self.Entity:ConnectEvent(DamageDealtEvent, self.OnDamage)  -- first arg = event Type

method void OnDamage(DamageDealtEvent event) log(event.amount) end
```

NativeEvent (engine-provided, e.g., `HitEvent.TotalDamage/.AttackerEntity`, `ButtonClickEvent`, `StateChangedEvent.PrevState/.CurState`) — see `Environment/NativeScripts/Event/`.

---

## 9. Validity Checks and Method Override

### Validity checks

Accessing a deleted entity is a runtime error — always `isvalid()` first.

```lua
if isvalid(entity) then ... end
if isvalid(self.Entity.SomeComponent) then ... end
```

### Method override

In an `extends`-ing script, a `method` with the same signature as the parent overrides it. Built-in engine methods marked `---@sealed` cannot be overridden. Call the parent original via `__base:MethodName(args)`.

#### ⚠️ LEA-3014 `SignatureMismatch` — ExecSpace must match the parent

"Same signature" **includes `@ExecSpace`**. The override must be **byte-identical** to the parent's annotation block — including the **absence** of one. Adding `@ExecSpace("ServerOnly")` to "make it server-side" when the parent has none → runtime LEA-3014.

**Common offenders**: AttackComponent / HitComponent damage hooks (`CalcDamage`, `CalcCritical`, `GetCriticalDamageRate`, `GetDisplayHitCount`, `IsAttackTarget`, `IsHitTarget`, `OnAttack`) are all declared **without** `@ExecSpace`. Override with no annotation — they're still safe because the server-side hit pipeline is the only caller.

**Workflow**: read the parent in `.d.mlua` (§1.3) and copy its annotation block verbatim. Fix LEA-3014 by aligning the child's `@ExecSpace` to the parent's, never the reverse.

---

## 10. Input / Click Events — World vs UI (Do Not Confuse)

### World touch — two approaches

| Approach | Event | Connect on | Use |
|------|------|----------|-----|
| **Entity touch** — `TouchReceiveComponent` on entity | `TouchEvent` (+Hold/Release) | `entity:ConnectEvent(...)` | "Which entity was touched" — NPCs, items |
| **Screen touch** — no component | `ScreenTouchEvent` | `_InputService:ConnectEvent(...)` | "Where on the screen" — placement, move target |

Both events carry `TouchId` (int32) + `TouchPoint` (Vector2 screen coord). For world coords, `_UILogic:ScreenToWorldPosition(event.TouchPoint)`. Filter UI clicks with `_InputService:IsPointerOverUI()`. If `TouchEvent` misses, `ScreenTouchEvent` + `ScreenToWorldPosition` is the no-config fallback.

> ⚠️ **Physics colliders do NOT emit `TouchEvent`** — `BoxCollider2D`, `CircleCollider2D`, Rigidbody/Kinematicbody, and `TriggerComponent` all do not deliver touch input. Only **`TouchReceiveComponent`** emits `TouchEvent`/`TouchHoldEvent`/`TouchReleaseEvent`.
>
> **Setup**: `AutoFitToSize = true` (auto-fits TouchArea to the Sprite/Avatar scale) is the simplest path. Manual `TouchArea` should leave 10–20% slack beyond the sprite. `RelayEventToBehind = true` (default) forwards through; set `false` only to block.
>
> **Not firing? Check, in order**: (1) `TouchReceiveComponent` actually attached (in `.map` / `.model`); (2) `TouchArea` non-zero and entity visible on screen; (3) no front entity blocking with `RelayEventToBehind = false`; (4) handler stored in a `property any` (otherwise GC'd).

> **Selection rule**: "Which entity was touched" → `TouchEvent`; "Where on the screen" → `ScreenTouchEvent`.

### PC mouse buttons (left/right/middle) — use `KeyDownEvent`, not `ScreenTouchEvent.TouchId == 2`

`ScreenTouchEvent` fires on PC only for the left button (`TouchId == 1`); `TouchId == 2` is mobile two-finger touch — reading right-click through it works in the Maker simulator but is **silent no-input on real PCs**. For PC mouse buttons, `_InputService:ConnectEvent(KeyDownEvent, ...)` and branch on `event.key == KeyboardKey.Mouse0` / `Mouse1` / `Mouse2` (Left = 323, Right = 324, Middle = 325). To support both mobile multi-touch and PC, connect both `ScreenTouchEvent` and `KeyDownEvent` — they don't double-fire (no mobile right-click; no PC `TouchId == 2`).

### UI clicks

For UI entities (`./ui/*.ui`, `ui` tree), use **`ButtonComponent` + `ButtonClickEvent`**. Putting UI events on a world object (or vice versa) silently does nothing — decide first whether the target is a world object or a UI panel button.

---

## 11. Map Context and Entity Spawning

> **§1.7 trigger** — `Read` [builder-protocol.md](../msw-general/references/builder-protocol.md) before any spawn / `.map` / `.model` work.

### Children traversal

For "all X in the map" / "child named Y" queries, use `Entity`'s lookup toolkit:

| Member | Returns | Use |
|---|---|---|
| `Entity.Children` | `ReadOnlyList<Entity>` (call `:ToTable()` to iterate) | Immediate children |
| `Entity:GetChildByName(name, recursive=false)` | `Entity` | By name |
| `Entity:GetChild(id, recursive=false)` | `Entity` | By UUID |
| `Entity:GetChildComponentsByTypeName(typename, recursive=false)` | `table<Component>` | All matching descendants |
| `Entity:GetFirstChildComponentByTypeName(typename, recursive=false)` | `Component` | First match |

```lua
local map = self.Entity.CurrentMap                              -- prefer this over service lookup
local units = map:GetChildComponentsByTypeName("script.MyUnit", false)
for _, child in ipairs(self.Entity.Children:ToTable()) do log(child.Name) end
```

The collection is `Children` — `ChildList`/`Childs`/`GetChildren()` are wrong (compile, runtime nil, `LIA-1114` Info). Runtime-spawned entities must be parented under `CurrentMap` to be findable.

### Native vs user component access

| Access | Works on |
|---|---|
| `entity.SomeComponent` (dot) | **Engine-native only** (`TransformComponent`, `ButtonComponent`, …) |
| `entity:GetComponent("script.MyUnit")` | User `@Component` (any) |
| `entity:GetFirstChildComponentByTypeName("script.MyUnit", true)` | User `@Component` on descendant |

User `@Component` typename is **always `"script.<FileBaseName>"`** — `MyUnit.mlua` → `"script.MyUnit"`, regardless of feature-folder nesting. `entity.MyUnit` (dot) returns `nil` with `LIA-1114`. To pass user-component refs between scripts, declare a typed property (`property MyUnit unit = ""`) and inject UUID.

> ⚠ The method is `GetComponent` (overloaded as `GetComponent(Type)` and `GetComponent(string typename)` — see `Environment/NativeScripts/Misc/Entity.d.mlua`). The `*ByTypeName` suffix exists only on the **child** variants (`GetChildComponentsByTypeName` / `GetFirstChildComponentByTypeName`).

`GetComponent(string)` returns the abstract `Component` type, so member access on the result drops to dynamic dispatch and the LSP raises `LIA-1114` Info (or a `type mismatch` Error when the value is passed to a function whose signature expects the concrete user `@Component`). Cast with `---@type` to restore static typing:

```lua
---@type MyUnit
local unit = self.Entity:GetComponent("script.MyUnit")
unit:DoSomething()  -- LSP now type-checks against MyUnit
```

### Spawning at runtime

- Use `_SpawnService:SpawnByModelId(id, name, pos, parent)` — **`parent` is required** (no default). Pass `self.Entity.CurrentMap`. `SpawnByEntity` differs — `parent = nil` is allowed.
- A `.model` template must already exist. New-object flow: **author `.model` → spawn or place on map**.

### Body components vs direct Position writes

Entities with a Body (Kinematic/Rigid/Sideview) ignore direct `TransformComponent.WorldPosition` writes — physics overwrites them next frame. Use instead:
- Per-frame: `MovementComponent:MoveToDirection(dir, dt)`
- Teleport (local): `MovementComponent:SetPosition(pos)` or `body:SetPosition(Vector2)`
- Teleport (world): `body:SetWorldPosition(Vector2)` — the standard absolute-place call for Kinematicbody on RectTile maps
- Direct Transform writes are OK only for Body-less entities (decorations, effects).

**Do NOT remove the Body as a workaround** — disables tile collision and enter/leave events (`NativeIssue_MissingComponent`).

---

## 12. Frequently Used Services / Logic

All services and logic are accessed via `_Name` (underscore + type name). Only the most common ones are listed.

| Service / Logic | Purpose |
|-------------|------|
| `_SpawnService` | Spawn entities (`SpawnByModelId`, `SpawnByEntity`). **There is no `Despawn` method** — remove spawned entities via `Entity:Destroy()` / `Entity:Destroy(delaySeconds)` (both `ControlOnly`). |
| `_TimerService` | Timers (`SetTimer`, `SetTimerRepeat`, `ClearTimer`) |
| `_EntityService` | Entity lookup (`GetEntity`, `GetEntities`, `GetEntitiesByPath`) |
| `_UserService` | Player lookup (`GetUsersByMapComponent(map.MapComponent)` returns all players currently on the given map — canonical "find players on this map" call, used by `Soldier`'s `FindNearestPlayer`). Returns `nil` when no users. |
| `_InputService` | Input state queries; receives `ScreenTouchEvent` |
| `_ResourceService` | Look up resource RUIDs; `LoadAnimationClipAndWait(ruid)` synchronously loads an AnimationClip (block for one frame — cache the result; wrap in `_ResourceService:PreloadAsync({ruid}, function() ... end)` if you want to avoid the block) |
| `_DataStorageService` | Persistent data (player saves) — **⚠️ Credit-billed. Do not call in `OnUpdate` / short timers; use `Batch*` in loops. Details: [references/datastorage.md](references/datastorage.md)** |
| `_UtilLogic` | Random, time, string, and math utilities |
| `_TweenLogic` | Tween animations (MoveTo, ScaleTo, RotateTo) |
| `_UILogic` | UI coordinate conversions (e.g., ScreenToWorldPosition) — ClientOnly |

> For the full list, read the `.d.mlua` files directly: `./Environment/NativeScripts/Service/` (46 files) and `./Environment/NativeScripts/Logic/` (9 files). For domain details, search via `msw-search`.

---

## 13. Math, Utilities, Reserved Words, Type Annotations

### Math / utility examples

```lua
_UtilLogic:RandomDouble()             -- 0.0~1.0
_UtilLogic:RandomIntegerRange(1, 10)  -- inclusive

-- ElapsedSeconds / ServerElapsedSeconds: world-instance lifetime, NOT reset on OnBeginPlay.
-- They keep ticking across repeated play sessions in the Maker editor.
-- For per-session countdowns, see "Per-session timers" below.
_UtilLogic.ElapsedSeconds
_UtilLogic.ServerElapsedSeconds
```

### mlua utility classes

Collections beyond Lua stdlib: `List` / `ReadOnlyList` / `SyncList`, `Dictionary` / `ReadOnlyDictionary` / `SyncDictionary` (Sync* variants auto-sync server↔client). Other utility types: `DateTime`, `TimeSpan`, `Regex`, `Translator`, `Quaternion`, `Vector2Int`, `FastVector2/3` / `FastColor` (in-place ops for perf), `Item` (inventory).

> `.Values` / `.Keys` on `Dictionary` / `ReadOnlyDictionary` / `SyncDictionary` returns a plain Lua `table` — iterate with `ipairs` directly. No `:GetValues()` / `:ToTable()` / `pairs(dict)` wrapper needed. Lists are similar but require `:ToTable()` first (`ReadOnlyList<T>` is not a Lua table).
>
> ```lua
> -- All connected players (server-side fan-out)
> for _, user in ipairs(_UserService.UserEntities.Values) do
>     if isvalid(user) then ... end
> end
> ```

> Detailed APIs in `Environment/NativeScripts/` or via `msw-search`.

### Per-session timers — never anchor on `ElapsedSeconds`

Trap: `self.deadline = _UtilLogic.ElapsedSeconds + 15` in `OnBeginPlay`. The world instance survives multiple Maker play sessions, so the saved deadline is in the past on the next play and fires immediately.

For per-session countdowns, decrement a `delta`-driven property in `OnUpdate`:

```lua
method void OnBeginPlay() self.waveCountdown = 15 end
method void OnUpdate(number delta)
    if self.waveCountdown > 0 then
        self.waveCountdown = self.waveCountdown - delta
        if self.waveCountdown <= 0 then self:StartWave() end
    end
end
```

For session-relative elapsed time, baseline in `OnBeginPlay` (`self.startTime = _UtilLogic.ElapsedSeconds`) and subtract. Never compare raw `ElapsedSeconds` across sessions.

### Type annotations (code hints)

`---@type T` / `---@param` / `---@return` give editor autocomplete only — **no runtime effect**.

### Reserved words

Forbidden as identifiers: `handler`, `property`, `method`, `script`, `end`, `extends`, `self`, `nil`, `true`, `false`.

Applies to locals, parameters, properties, methods, dot-field names (`rec.handler`), and **bare** table keys (`{ handler = ... }`). Bracket-quoting an external string key (`rec["handler"]`) is fine, but prefer renaming internal keys (e.g., `eventHandler`).

---

## 14. External Tooling

- Maker MCP (`refresh`/`logs`/`play`/`stop`/`screenshot`/…): **`msw-general`** skill.
- API descriptions/examples/guides not in `.d.mlua`: **`msw-search`** skill.
- MCP wiring / `.mcp.json` / API key setup: share https://maplestoryworlds-creators.nexon.com/ko/docs?postId=1368

Debug order: **build logs → play → logs → stop → fix → diagnose → refresh → repeat**.

---

## 15. Script Authoring Workflow

1. **Search** existing scripts (§1.1) — modify if a similar one exists.
2. **Verify spec** (§1.3) — `.d.mlua` first, `msw-search` if insufficient.
3. **Decide path** (§1.2) — feature-folder mandatory; never write to `MyDesk/` root.
4. **Write**.
5. **Validate** — `mlua-diagnose` hook auto-runs; fix until zero errors (§1.4).
6. **Refresh** — Maker MCP `refresh` (§1.5).
7. **(If needed)** `play` → `logs` → `stop` (§17).

Delete/rename also requires `refresh` + cleanup of references in `.model` / `.map`.

---

## 16. Attaching Scripts (Components) to Entities

> **§1.7 trigger** — `Read` [builder-protocol.md](../msw-general/references/builder-protocol.md) first. Never edit `Components` arrays as raw JSON.

- **Attach to `.model` (preferred)**: `ModelBuilder.addComponent()` / `upsertComponent()`. Map instances inherit.
- **Attach to one map instance only**: `MapBuilder.upsertComponent(name, "script.XXX", body)`.
- **Global models** (`./Global/`, e.g., `DefaultPlayer`): read-only by policy and affect the entire project. Copy into `RootDesk/MyDesk/Models/` first, then patch via `ModelBuilder`.

---

## 17. Playtesting and Debugging

The procedure for verifying behavior in **play mode** in Maker, then narrowing down bugs with **runtime logs, screenshots, and simulated input**.

> For the MCP tool list, play-mode constraints, and refresh rules, see `msw-general`.

### 17.1 Always Check Build Logs First

**Before every `play`, run `logs(category="build")`**. Build errors make scripts fail to load entirely (the component/logic behaves as if missing), and they often **don't appear in runtime logs** — most "code looks correct but doesn't work" reports trace to a missed build error. Fix → refresh → recheck until errors are zero, then play.

### 17.2 Error Classification

| Class | Signs | Where to look |
|------|-----------|-----------|
| **Script error** | Stack trace with file + line | Exact `.mlua` line; event/timing order |
| **nil reference** | `attempt to index a nil value` | Init order, `isvalid`, 1-frame post-Spawn timing |
| **Component missing** | nil component / `GetComponent` fails | `Components` array in `.model`; name typos |
| **Sync / network** | Only client breaks, values mismatch or converge late | `@Sync`, `ExecSpace`, RPC flow |
| **`Info` LIA 1113/1114/1115** (false positives) | Static-analysis can't resolve user cross-script refs (`_LogicName`, user `@Component` dot/method). Build still passes (errors=0/warnings=0) | Treat as noise; verify with `log()`. Scope next `logs` call to higher severity if they drown real issues. |
| **User type `Symbol not found` / `type not found`** | Usage site authored before the user-type body `.mlua` exists. | Write the body `.mlua` first, then Maker `refresh` to regenerate the `.codeblock`. Build-log cache can hold one stale cycle — judge by the next diagnose. |

If logs are inconclusive, add `log()` in `.mlua` to inspect entity/component/property state.

### 17.3 Test-Result Report

Summarize briefly: **Scenario** (one line) · **Env** (map, refreshed?) · **Steps** (input/Lua) · **Result** (Pass/Fail/Blocked) · **Evidence** (1–2 log lines, screenshot if requested) · **Next action**.

### 17.4 Workflow

One unified loop for every playtest scenario:

```
edit → refresh → logs(category="build")  ──┐
                                           ↓ (errors? fix and refresh again)
                  clear_logs (optional) → play
                                           ↓
                   keyboard_input / mouse_input to reproduce
                                           ↓
                   logs(category="runtime") → classify with §17.2 table
                                           ↓ (insufficient? add log() in .mlua, refresh, replay)
                                          stop → fix → loop
```

**Variants** — same loop, different entry conditions:

| Scenario | Notable steps |
|---|---|
| **First playtest** | Start from edit → refresh. |
| **Regression / fix loop** | `clear_logs` before `play` for a clean repro. |
| **Error analysis** | After collecting runtime logs, map to §17.2 first; only add `log()` when classification is inconclusive. |
| **Runtime value inspection** | Add `log()` calls; if API is unknown, verify spec (§1.3) before adding the call. |

### 17.5 Final Verification (PASS/FAIL)

**"No errors ≠ Pass."** Before reporting done, gather positive `log()`-based evidence that the intended logic actually executed. Full checklist: [references/verify-checklist.md](references/verify-checklist.md) (Runtime → Code Review → Log Evidence → PASS/FAIL).

### 17.6 Related Skills

`msw-general` — MCP tools, screenshot/logs policy, refresh rules, workspace and hierarchy.


---
*Source: https://skills.yangsir.net/skill/gh-msw-scripting*
*Markdown mirror: https://skills.yangsir.net/api/skill/gh-msw-scripting/markdown*