跳到主内容
智客 ZICQ

技能库 智客分类:Agent 工作流 msw-scripting

Msw Scripting

编写MSW脚本(.mlua)外加集成播放测试和调试. 覆盖mlua语法,注释 (@Component/@Logic/@ExecSpace/@Sync),生命周期,执行空间,属性同步,事件系统,文件工作流程,构建-日志检查,错误分类,以及测试/调试循环. 关键词:脚本,mlua,lua,组件,逻辑,注释,ExecSpace,同步,事件,播放,测试,调试,生命周期.

10923 安装量

官方网址:skills.sh

技能介绍

先看中文介绍;官方 description 原文单独保留,不改写 SKILL.md。

做什么

编写MSW脚本(.mlua)外加集成播放测试和调试. 覆盖mlua语法,注释 (@Component/@Logic/@ExecSpace/@Sync),生命周期,执行空间,属性同步,事件系统,文件工作流程,构建-日志检查,错误分类,以及测试/调试循环. 关键词:脚本,mlua,lua,组件,逻辑,注释,ExecSpace,同步,事件,播放,测试,调试,生命周期.

何时用

官方 description 未单独写出 Use when。按规范,代理会在用户任务与这段 description 的关键词匹配时激活本技能。

代理如何加载

按 Agent Skills 渐进披露:启动时只加载 name 与 description(约 100 token);任务匹配后才读入整份 SKILL.md 正文;scripts/、references/、assets/ 仅在需要时再读。 本文件正文结构: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)。 其中含规范建议的小节:分步指令。

文件分析

文件分析:除 SKILL.md 外,正文引用了 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,属于带资源的技能包,这些文件按需再读。 正文约 701 行,超过规范建议的 500 行,激活时仍会整份载入。

官方 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 & 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

来源分类:skills.sh agent-skill

SKILL.md 与 Agent 调用

官方规范 ↗
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. 发现技能客户端向 Agent 提供名称与描述目录。
  2. 匹配与调用用户指定或任务匹配后,载入 SKILL.md 指令。
  3. 按需加载按步骤读取参考文档、使用脚本与素材。
指令中引用的文件 · 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

以下路径提取自原文;文件是否齐全请以来源仓库中的完整目录为准。

具体调用语法与可用工具以目标 Agent 客户端为准。 查看调用机制说明 ↗

安装这个技能

Skills CLI ↗

先选择目标 Agent 和安装范围,保留技能包的附属文件,安装后检查客户端能否发现该技能。

该技能引用了附属文件,请从来源获取完整目录;仅复制 SKILL.md 可能缺少依赖。

交给 Agent 安装

复制安装指令给支持 Agent Skills 的代理,确认其中的目标目录与客户端匹配。

把 Agent Skill「msw-scripting」安装到我的项目:SKILL.md 原文与官方 description 见 https://zicq.com/zh/skills/skl-3908885e2821258d-Msw-Scripting.html
请存为 .cursor/skills/msw-scripting/SKILL.md 或 .claude/skills/msw-scripting/SKILL.md,frontmatter 的 name 与 description 保持原样,不要改写。
该技能还带 scripts/、references/、assets/ 等文件,请从 https://github.com/msw-git/msw-ai-coding-plugins-official 取完整目录,不要只建一个 SKILL.md。

GitHub 完整包 ↗

终端安装 · Skills CLI

需要 Node.js 与 npx。先查看仓库技能列表,确认实际名称。

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'

CLI 会交互选择目标 Agent,默认安装到项目;用户级安装使用 -g。先通过查看命令核对仓库内容,再用 npx skills list 检查已安装技能。

阅读排版
--- 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**.

相关技能

Agent 工作流

Skill Creator

创造有效技能指南。 当用户想创造出新的技能(或更新现有的技能),以专业知识,工作流程,或工具集成来扩展克洛德的能力时,应该使用这种技能.

Agent 工作流

Clawdhub

使用ClawdHub CLI搜索,安装,更新并发布从taladhub.com的代理技能. 需要获取苍蝇上的新技能时使用,将安装的技能同步到最新版本或特定版本,或者发布 npm-instainddhub CLI 的新/更新的技能文件夹.

Agent 工作流

Agent Team Orchestration

管弦乐团多代理团队,任务设定周期,交接协议,审查工作流程. 使用时间: (1)建立2+特派员队伍,具有不同专业,(2)确定任务路线和生命周期(收录框_ spec_建设_审查_完成),(3)在特派员之间制定交接协议,(4)建立审查和质量关口,(5)管理特派员之间的交流和文物共享.

Agent 工作流

Superpowers

Spec-first,TDD,子代理驱动的软件开发工作流程. 当:(1)构建任何新功能或应用——触发脑暴_计划_子代理执行回路,(2)调试出一个bug或测试失败——触发系统性的根起过程,(3)用户说"让我们构建","帮助我计划","我想添加X",或"这个被打破",(4)完成一个功…