Skip to main content
ZICQ

Skills ZICQ category:Agent Workflows msw-scripting

Msw Scripting

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.

10923 installs

Official URL:skills.sh

What this skill does

Intro in this page language first. The official description stays in its original wording; we do not rewrite SKILL.md.

What it does

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.

When to use it

The official description does not include a separate “Use when”. Per the spec, agents activate this skill when the task matches keywords in that description.

How agents load it

Per Agent Skills progressive disclosure: name and description load at startup (~100 tokens); the full SKILL.md body loads when the skill activates; scripts/, references/, and assets/ load only as needed. This file's sections: MSW Scripting (.mlua) — Framework + File Workflow + Playtest & Debugging; 1. Core Principles (must follow); 1.1 Existing Script First; 1.2 Folder Structure for New Scripts — Never Dump Files Flat; 1.3 Never Guess APIs — Verify Before Writing; 1.4 Lint (LSP diagnostics). It includes spec-recommended sections: step-by-step instructions.

File analysis

File analysis: besides SKILL.md, the body references references/component-api.md, references/platform.md, references/runtime-patterns.md, references/builder-protocol.md, references/builder-protocol-map.md, references/builder-protocol-model.md. Those resources load on demand. The body is about 701 lines, above the spec’s 500-line guidance; agents still load it in full on activation.

MSW Scripting (.mlua) — Framework + File Workflow + Playtest & Debugging1. Core Principles (must follow)1.1 Existing Script First1.2 Folder Structure for New Scripts — Never Dump Files Flat1.3 Never Guess APIs — Verify Before Writing1.4 Lint (LSP diagnostics)1.5 `.codeblock` & Refresh1.6 MSW ≠ Unity — Do Not Reason From Intuition1.7 Builder Protocol Preflight — **MUST**1.8 Method Documentation Comments — Inside the Body2. Paths and File Roles3. Script Types and Declarations

Source category:skills.sh agent-skill

SKILL.md & Agent activation

Official spec ↗
name
msw-scripting
description
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.
  1. DiscoverThe client exposes names and descriptions to the agent.
  2. ActivateYour request or the task context selects the skill and loads its instructions.
  3. Load resourcesReferenced scripts, documentation and assets are used when needed.
Files referenced by the instructions · 7
  • references/component-api.md
  • references/platform.md
  • references/runtime-patterns.md
  • references/builder-protocol.md
  • references/builder-protocol-map.md
  • references/builder-protocol-model.md
  • references/builder-protocol-ui.md

These paths are extracted from the text. Check the upstream package to verify the files exist.

Invocation syntax and available tools depend on your Agent client. Client integration guide ↗

Install this skill

Skills CLI ↗

Choose the target agent and installation scope, keep referenced package files, then verify the skill appears in the client's catalog.

This skill references supporting files. Retrieve the complete directory from the source; copying SKILL.md alone may leave missing dependencies.

Ask your Agent to install

Copy these instructions to a compatible agent and confirm the target directory matches your client.

Install the agent skill "msw-scripting" into my project. The full SKILL.md and official description are at https://zicq.com/en/skills/skl-3908885e2821258d-Msw-Scripting.html
Save it as .cursor/skills/msw-scripting/SKILL.md or .claude/skills/msw-scripting/SKILL.md and keep the frontmatter name and description exactly as-is.
This skill also ships scripts/, references/, or assets/ — fetch the whole folder from https://github.com/msw-git/msw-ai-coding-plugins-official instead of creating only a SKILL.md.

Full package on GitHub ↗

Install from the terminal · Skills CLI

Requires Node.js and npx. First inspect the repository's skill list to confirm the name.

npx skills add 'https://github.com/msw-git/msw-ai-coding-plugins-official' --list

npx skills add 'https://github.com/msw-git/msw-ai-coding-plugins-official' --skill 'msw-scripting'

The CLI lets you choose the agent interactively. The default scope is the project; use -g for user scope. Confirm package availability with the discovery command, then use npx skills list to inspect installed skills.

Readable layout
--- name: msw-scripting description: "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." --- # 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//.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`), **[`../msw-general/references/builder-protocol.md`](../msw-general/references/builder-protocol.md) (core) plus the per-builder file for each type touched ([`builder-protocol-map.md`](../msw-general/references/builder-protocol-map.md) / [`builder-protocol-model.md`](../msw-general/references/builder-protocol-model.md) / [`builder-protocol-ui.md`](../msw-general/references/builder-protocol-ui.md)) must be fully in context first** (`Read` the full files only if never loaded this session or lost to compaction — a memorized summary does not count as in context). The core carries the shared write-side contract and cross-flow; each per-builder file carries that builder's API, `typeKey` metadata, auto-lint, child-entity invariants, and `placeModel` mirroring; 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` plus existing `./Global/*.model` files in place | 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 `_` — **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. For per-map setup/cleanup either (1) move the behavior to a `@Component` on the map entity (preferred; the only option that works on the server), or (2) **client-side only**: poll `_UserService.LocalPlayer.CurrentMap` from `OnUpdate` — `LocalPlayer` is available only on the client (nil on the server). A Logic survives map transitions, so any timer / event handler / mutable state that should reset per map must be cleared via one of these — there is no automatic hook. > **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) | On the client, `OnMapEnter` also fires for other players already in the map — not only for the local player's own entry. **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 ``` **`OnMapEnter` pairs with `OnMapLeave` — the map-transition timer leak.** `OnMapEnter` fires on **every** map re-entry, and a Component whose entity survives transitions (player, persistent objects) is **not** destroyed on leave — so a timer/handler started in `OnMapEnter` stacks one more live copy per re-entry. Clear it in `OnMapLeave`, not `OnEndPlay` (which fires only on final removal, so it never runs between transitions). A **repeating** timer (`SetTimerRepeat`, or `SetTimer(..., isRepeat = true)`) left registered is what the engine reports as `[LEA-3051] MemoryLeak` at the next play-mode start, naming the create line; once-timers (`SetTimerOnce`) self-clear after firing and are never flagged. Rule: whatever `OnMapEnter` starts, `OnMapLeave` must clear. --- ## 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 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` 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 Scores -- array form, NO default literal @Sync property SyncTable Stats -- dict form, NO default literal ``` #### `SyncTable<...>` property — no default literal Declare `SyncTable` (array form) or `SyncTable` (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 Stats -- empty at construction @Sync property SyncTable 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` is not a user property type `SyncList` 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` (array form) or `SyncTable` (dict form) — see above. ### Temporary properties (`_T`) `self._T.` is non-synced, declaration-free ad-hoc state. Server and client keep their own values; never shown in inspector. Cannot be `@Sync`'d. > ⚠️ **`_T` is the ONLY declaration-free field.** Assigning to any other undeclared `self.` is a runtime error — `cannot set , no such field` — that kills the calling method (usually all of `OnBeginPlay`). Every `self.` you set must be a declared `property`, or go through `self._T.`. The build log stays clean except an easily-missed `LIA-1114` Info (see §17.2). ### `OnSyncProperty` callback Client-side hook fired when a `@Sync` property finishes synchronizing. **Must be `ClientOnly`** (cannot be changed). Available on Component and Logic. Not called for properties whose sync setting is None. ```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 property any clickHandler = nil 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 property table clickHandlers = {} 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` / `_`). `self.Entity.ButtonComponent:ConnectEvent(...)` runtime nils. > > ```lua > property any clickHandler = nil > property any keyHandler = nil > 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) + the matching per-builder protocol file 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` (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` | 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."`** — `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`. ### Built-in globals accessed WITHOUT the `_` prefix The `_Name` rule above applies to **Services and Logic only**. A few built-ins are exposed as plain globals — accessing them with a leading underscore is a runtime error (`nil` reference). | Global (correct) | Wrong | Purpose | |---|---|---| | `Environment` | ❌ `_Environment` | Execution-environment queries — `Environment:IsMakerPlay()` / `IsMakerEdit()` / `IsPlay()` / `IsPublishedPlay()` / `IsMobilePlatform()` / `IsPCPlatform()`, `WorldId` property. `GetApplicationVersion()` is ClientOnly (nil on server). | | `CollisionGroups` | ❌ `_CollisionGroups` | Table of `CollisionGroup` objects keyed by group name — `CollisionGroups.HitBox`, `.Monster`, `.Player`, etc. Built-ins: `Default` / `TriggerBox` / `HitBox` / `Interaction` / `Portal` / `Climbable`, plus any project-defined groups. Each entry has a `.Id` (string) and `:GetCollideGroups()`. | --- ## 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` 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 property number waveCountdown = 0 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**.

Related skills

Agent Workflows

Skill Creator

Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that exte…

Agent Workflows

Clawdhub

Use the ClawdHub CLI to search, install, update, and publish agent skills from clawdhub.com. Use when you need to fetch new skills on the fl…

Agent Workflows

Agent Team Orchestration

Orchestrate multi-agent teams with defined roles, task lifecycles, handoff protocols, and review workflows. Use when: (1) Setting up a team …

Agent Workflows

Superpowers

Spec-first, TDD, subagent-driven software development workflow. Use when: (1) building any new feature or app — triggers brainstorm → plan →…