Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
msw-git avatar

Msw Scripting

  • 4.3k installs
  • 33 repo stars
  • Updated July 29, 2026
  • msw-git/msw-ai-coding-plugins-official

msw-scripting is an agent skill that authors and debugs MapleStory Worlds .mlua scripts with MSW-specific annotations, API verification, and build-log-first playtest loops.

About

msw-scripting covers MapleStory Worlds .mlua authoring plus integrated playtest and debugging for Lua-based game logic. Core rules require searching ./RootDesk/MyDesk/ for an existing script before creating new files, placing unavoidable new scripts under feature subfolders such as Player, UI, or Combat, and never dumping scripts into flat Misc or temp folders. API names must be verified in ./Environment/NativeScripts .d.mlua signatures before writing code because guessed engine APIs fail silently at runtime. The skill documents @Component, @Logic, @ExecSpace, and @Sync annotations, lifecycle hooks, execution spaces, property sync, and the event system. Validation flows through build logs first, then runtime logs, with mlua-diagnose LSP checks after every edit until error-severity diagnostics reach zero. .codeblock files are generated by Maker Refresh and must not be edited manually. Developers invoke it for mlua syntax, component logic, play testing, debugging script errors, or adding event-driven behaviors in a MapleStory Worlds workspace.

  • Requires glob search for existing .mlua scripts under ./RootDesk/MyDesk/ before creating new ones.
  • Mandates feature subfolders like Player/, UI/, or Combat/ and forbids flat Misc or temp script dumps.
  • Verifies APIs in ./Environment/NativeScripts .d.mlua files before writing to avoid silent runtime failures.
  • Documents @Component, @Logic, @ExecSpace, @Sync annotations, lifecycle, exec spaces, and events.
  • Uses build logs then runtime logs for validation, with mlua-diagnose LSP checks after every edit.

Msw Scripting by the numbers

  • 4,325 all-time installs (skills.sh)
  • +540 installs in the week ending Aug 2, 2026 (Skillselion tracking)
  • Ranked #6 of 247 Game Development skills by installs in the Skillselion catalog
  • Security screen: MEDIUM risk (skills.sh audit)
  • Data as of Aug 2, 2026 (Skillselion catalog sync)
At a glance

msw-scripting capabilities & compatibility

Capabilities
.mlua script authoring with msw annotations · nativescripts api verification workflow · build log and runtime debugging loop
Use cases
debugging · orchestration · testing
From the docs

What msw-scripting says it does

Before creating a new `.mlua`, glob/keyword-search under `./RootDesk/MyDesk/` for an existing script with the same purpose
SKILL.md
Guessing an MSW API name/param/return type **silently fails at runtime**.
SKILL.md
code is validated in the order **build logs → runtime logs**.
SKILL.md
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-scripting

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs4.3k
repo stars33
Security audit2 / 3 scanners passed
Last updatedJuly 29, 2026
Repositorymsw-git/msw-ai-coding-plugins-official

How do I write or fix MapleStory Worlds .mlua scripts without guessing engine APIs or breaking the Maker build?

Author MapleStory Worlds .mlua scripts with correct annotations, lifecycle, exec spaces, build-log debugging, and playtest loops under ./RootDesk/MyDesk/.

Who is it for?

MapleStory Worlds authors creating or debugging .mlua component and logic scripts with annotation and exec-space rules enforced.

Skip if: Skip for DefaultPlayer model tuning or avatar costumes, which belong to msw-defaultplayer and msw-avatar skills.

When should I use this skill?

User mentions mlua, lua script, Component, Logic, ExecSpace, Sync, playtest, or debugging MapleStory Worlds scripts.

What you get

Feature-scoped .mlua scripts with verified NativeScripts APIs, zero LSP error diagnostics, and validated build and runtime logs.

  • Feature-scoped .mlua scripts
  • Zero-error LSP diagnostics
  • Validated playtest results

Files

SKILL.mdMarkdownGitHub ↗

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/:

FolderContentsCount
Component/Engine components104
Service/System services46
Event/Event types202
Logic/Built-in logic9
Enum/Enumerations118
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 intuitionMSW 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 clicksPhysics colliders never emit TouchEvent — World uses TouchReceiveComponent (§10); UI uses ButtonComponent/UITouchReceiveComponent
OnCollisionEnter + RigidbodyEntity↔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`. Common mappings: disable→Enable, text→Text, text color→FontColor, tint→Color. ButtonComponent.Interactable doesn't exist.
Attach multiple Rigidbody/Collider freelyOne Body per map type — see `msw-general/references/platform.md` §4
Touch UI from server codeUI 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`
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.

-- ✅ 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

TargetPathAgent action
User scripts./RootDesk/MyDesk/**/*.mluaCreate / read / modify / delete directly
Auto-generated artifacts*.codeblockDo 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/*.mapEdit 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.

@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.

@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

@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:

FunctionPurpose
log() / log_warning() / log_error()Logging at each severity
wait(seconds)Pause script execution
isvalid(obj) → booleanValidity (handles deletion/nil)
enum(t) → tableSwap keys and values
beginscope(name) / endscope()Profiling scopes

---

5. Lifecycle

OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroy
                                ↑
                  OnMapEnter / OnMapLeave (Component only, per transition)
MethodWhenWherePurpose
OnInitializeAfter creationComponent + LogicInit internal vars (rarely used)
OnBeginPlayGame startComponent + LogicWire events, start timers, initial setup
OnUpdate(delta)Every frameComponent + Logic (Logic first)Movement, animation, input
OnMapEnter / OnMapLeaveMap transitionComponent only (silent no-op on Logic)Per-map init/cleanup
OnEndPlayGame endComponent + LogicDisconnect events, clear timers (mandatory!)
OnDestroyRemovalComponent + LogicFinal cleanup (rarely used)

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

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.

ExecSpaceRuns onDirectionUse case
ServerOnlyServerServer-internal onlyDamage calc, state changes, spawning
ClientOnlyClientClient-internal onlyUI updates, effects, sounds
ServerServerClient→Server RPCClient requesting the server (attack, item use)
ClientClientServer→Client RPCServer notifying a client (result UI, effects)
MulticastAll clientsServer→all clientsGlobal events (announcements, boss spawn)
(unspecified)Caller sideServer→Server, Client→ClientShared functions executed locally on either side

ExecSpace constraints on lifecycle methods

MethodAllowed ExecSpace
OnSyncProperty`ClientOnly` only
OnInitialize, OnBeginPlay, OnUpdate, OnEndPlay, OnDestroy, OnMapEnter, OnMapLeaveServerOnly, ClientOnly, or unspecified
All event handlersServerOnly, ClientOnly, or unspecified
Custom user methodsAny 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.

@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.:

ReservedWhat the engine uses it for
selfMethod receiver
senderUserIdCalling client's UserId on @ExecSpace("Server") bodies
targetUserIdRecipient client's UserId (last call-site arg on @ExecSpace("Client") bodies — do NOT declare it; the engine appends it)
messageOwnerEntityOriginating entity for some service callbacks

Rename your own parameters when they collide (targetUserIdforUserId, senderUserIdfromUserId). 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.

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.

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

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.
@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:

@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.

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

Property editor attributes

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

---

8. Event System / RPC

Static subscription — @EventSender + handler

@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

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.

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 ConnectEventmethod void.

CustomEvent — typed class style

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

@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.

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

ApproachEventConnect onUse
Entity touchTouchReceiveComponent on entityTouchEvent (+Hold/Release)entity:ConnectEvent(...)"Which entity was touched" — NPCs, items
Screen touch — no componentScreenTouchEvent_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 triggerRead 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:

MemberReturnsUse
Entity.ChildrenReadOnlyList<Entity> (call :ToTable() to iterate)Immediate children
Entity:GetChildByName(name, recursive=false)EntityBy name
Entity:GetChild(id, recursive=false)EntityBy UUID
Entity:GetChildComponentsByTypeName(typename, recursive=false)table<Component>All matching descendants
Entity:GetFirstChildComponentByTypeName(typename, recursive=false)ComponentFirst match
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 ChildrenChildList/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

AccessWorks 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:

---@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 / LogicPurpose
_SpawnServiceSpawn entities (SpawnByModelId, SpawnByEntity). There is no `Despawn` method — remove spawned entities via Entity:Destroy() / Entity:Destroy(delaySeconds) (both ControlOnly).
_TimerServiceTimers (SetTimer, SetTimerRepeat, ClearTimer)
_EntityServiceEntity lookup (GetEntity, GetEntities, GetEntitiesByPath)
_UserServicePlayer 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.
_InputServiceInput state queries; receives ScreenTouchEvent
_ResourceServiceLook 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)
_DataStorageServicePersistent data (player saves) — *⚠️ Credit-billed. Do not call in `OnUpdate` / short timers; use `Batch` in loops. Details: references/datastorage.md**
_UtilLogicRandom, time, string, and math utilities
_TweenLogicTween animations (MoveTo, ScaleTo, RotateTo)
_UILogicUI 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

_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:

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. Validatemlua-diagnose hook auto-runs; fix until zero errors (§1.4). 6. Refresh — Maker MCP refresh (§1.5). 7. (If needed) playlogsstop (§17).

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

---

16. Attaching Scripts (Components) to Entities

§1.7 triggerRead 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

ClassSignsWhere to look
Script errorStack trace with file + lineExact .mlua line; event/timing order
nil referenceattempt to index a nil valueInit order, isvalid, 1-frame post-Spawn timing
Component missingnil component / GetComponent failsComponents array in .model; name typos
Sync / networkOnly 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:

ScenarioNotable steps
First playtestStart from edit → refresh.
Regression / fix loopclear_logs before play for a clean repro.
Error analysisAfter collecting runtime logs, map to §17.2 first; only add log() when classification is inconclusive.
Runtime value inspectionAdd 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 (Runtime → Code Review → Log Evidence → PASS/FAIL).

17.6 Related Skills

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

Related skills

How it compares

MSW-specific mlua authoring rules with API verification, not generic Lua scripting help.

FAQ

Where should new .mlua scripts live?

Under ./RootDesk/MyDesk/<FeatureFolder>/<ScriptName>.mlua after searching for an existing script to extend.

How do I avoid silent API failures?

Read the matching .d.mlua signature under ./Environment/NativeScripts before writing any engine API call.

What is the validation order?

Build logs first, then runtime logs, with mlua-diagnose LSP checks after every create or modify until errors reach zero.

Is Msw Scripting safe to install?

skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

Game Developmentintegrationstesting

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.