Claude
Skills
Sign in
Back

msw-scripting

Included with Lifetime
$97 forever

Authoring MSW scripts (.mlua) plus integrated playtest and debugging. Covers mlua syntax, annotations (@Component/@Logic/@ExecSpace/@Sync), lifecycle, exec spaces, property sync, event system, file workflow, build-log inspection, error classification, and the test/debug loop. Keywords: script, mlua, lua, Component, Logic, annotation, ExecSpace, Sync, event, play, test, debug, lifecycle.

Design

What this skill does


# 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

Related in Design