Token导航 LogoToken导航

Agent Skill · 研究检索

msw-scripting(城市软件脚本)

msw-scripting 用于查找、检索和筛选相关信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要根据关键词、任务场景或来源线索快速定位候选结果时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

msw-scripting 是研究检索类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为调用外部服务;许可证信息还不明确,第三方扫描已通过,按安装量在研究检索类的 73,675 个 Skill 里排第 57,864。

总安装 423周安装 18GitHub Stars 公开资料未说明下载 148核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:msw-scripting(城市软件脚本)
来源仓库:https://github.com/msw-git/msw-ai-coding-plugins-official
仓库路径:skills/msw-scripting
安装命令:
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-scripting
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

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

这个 Skill 是做什么的

研究检索类 Skill 主要解决「找资料、核对事实、把来源整理清楚」的问题,通常会让助手先检索再总结。

msw-scripting 用于查找、检索和筛选相关信息,适合快速定位候选结果。适用于需要根据关键词或任务场景从来源线索中获取信息的场景。通过 npx skills add 命令安装,需结合原始 README 核验具体用法。安装前建议确认权限范围、维护状态及是否触发联网或文件读写操作。

适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

来源仓库内路径:skills/msw-scripting

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill…;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要调用外部服务,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

msw-scripting 怎么安装?

在终端执行 npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-scrip…。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

msw-scripting 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

msw-scripting 需要什么权限?

权限级别是调用外部服务。该 Skill 可能调用第三方服务、云服务或外部模型 API,使用前需要确认账号、额度、数据发送范围和服务条款。

msw-scripting 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

msw-scripting 用的人多吗?

公开数据是累计安装 423 次,近一周安装 18 次,下载 148 次。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

msw-scripting 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理研究检索类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

研究检索类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
find-skills

寻找技能 · Codex、Claude

31,199,81316,486操作浏览器MIT有提醒
self-improving-agent

自我改进 Agent · OpenClaw

16,464,0023,396接触敏感数据MIT-0通过
soultrace

灵魂追踪 · Codex、Claude

7,588,94412需要联网未明确有提醒
self-improving

自我完善 · OpenClaw

6,829,2841,046需要联网MIT-0通过
microsoft-foundry

微软代工厂 · Codex、Claude

6,614,144764调用外部服务未明确有提醒
msw-scripting

城市软件脚本 · 本页

423—调用外部服务未明确通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

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, you must search under ./RootDesk/MyDesk/ for an existing script with the same or similar purpose.
  • Use glob/keyword search (file names, symbols, comment keywords) as your discovery method.
  • Duplicate implementations raise maintenance cost and conflict risk. Extending (modifying an existing file) is always the first choice.

1.2 Folder Structure for New Scripts — Never Dump Files Flat

When extending an existing script is not possible and a new .mlua must be created, organize it under a feature/category subfolder. Do not drop scripts directly into ./RootDesk/MyDesk/.

Required path shape: ./RootDesk/MyDesk/<FeatureFolder>/<ScriptName>.mlua (or a deeper nested path when the feature has sub-systems).

  • Reuse an existing subfolder if one already fits (e.g., Player/, UI/, Combat/, Tower/, Inventory/). Run a glob/list on ./RootDesk/MyDesk/ first to see what's already there.
  • If no fitting subfolder exists, create one named for the feature/system (PascalCase, matching the surrounding project's casing). Examples:

- ./RootDesk/MyDesk/Inventory/InventoryManager.mlua - ./RootDesk/MyDesk/Combat/MeleeAttackComponent.mlua - ./RootDesk/MyDesk/UI/Popup/RewardPopupLogic.mlua

  • Group related scripts together — Component, Logic, custom Event, and helper Struct for the same feature should sit in the same folder so the system is discoverable as a unit.
  • A single-file feature still gets its own folder. Even one script belongs in <FeatureFolder>/, not at the root, so future additions have a home.
  • Naming: folder = feature noun (TowerDefense/, Quest/); file = role-specific name reusing the feature noun where helpful (TowerSpawnerLogic.mlua, QuestTrackerComponent.mlua).
  • Do not create generic catch-all folders like Scripts/, Misc/, Common/, New/, temp/. Pick a real feature name. If you genuinely have a project-wide utility, place it under a specific utility folder such as Util/ or Shared/ only if that pattern already exists in the project.
Why: a flat MyDesk/ quickly becomes unsearchable and makes the "search before creating" rule (1.1) impossible to follow. A consistent feature-folder layout is what lets future agents (and humans) discover what already exists.

1.3 Never Guess APIs or Syntax — Verify Before Writing Code

If you guess an MSW API name, parameter, or return type, the call will silently fail at runtime. Before writing code, verify the spec using one of the methods below.

Method 1 — Read the .d.mlua definition file directly (exact signatures): The full engine API is defined as .d.mlua files under ./Environment/NativeScripts/:

FolderContentsFilesExample
Component/Engine components104TransformComponent.d.mlua
Service/System services46SpawnService.d.mlua
Event/Event types202HitEvent.d.mlua
Logic/Built-in logic9TweenLogic.d.mlua
Enum/Enumerations118BodyType.d.mlua
Misc/Utility types135Vector2.d.mlua
You know the API name → Read ./Environment/NativeScripts/{folder}/{name}.d.mlua
You don't know the name → Grep keywords in the NativeScripts folder

Method 2 — msw-search skill (detailed descriptions, examples, implementation guides): When .d.mlua only contains signatures and lacks explanation, search for parameter semantics, code examples, related APIs, and implementation guides.

Required order: confirm signature in .d.mlua → (if needed) look up details via msw-search → write code → LSP diagnose runs automatically (PostToolUse hook).

1.4 Lint (LSP diagnostics) — required after every script change

  • Whenever you create or modify a .mlua file, the mlua-diagnose hook runs LSP diagnose automatically and feeds errors back.
  • Repeat fix → re-edit until error-severity diagnostics reach zero.

1.5 .codeblock files

  • .codeblock files are generated automatically by Maker Refresh.
  • The agent must never create, edit, or delete them manually.

1.6 Refresh Timing

  • After creating, modifying, renaming, or deleting a .mlua, you must call Maker MCP refresh.
  • refresh cannot run during play mode — first call stop to return to edit mode.

1.7 MSW ≠ Unity — Do Not Reason From Intuition [VERIFIED]

MSW uses Lua + ECS + an MSW-specific execution-space model. Applying Unity or generic game-engine patterns directly will compile fine but silently fail at runtime. Common misconceptions:

Unity / generic intuitionMSW realityCorrect pathSource
MonoBehaviour.gameObject / this.transform to access the owning entity@Logic has no self.Entity (that is @Component-only)Inject via property Entity x = "uuid" on the Logic, or use _EntityService:GetEntityByPath(...)Logic.d.mlua, Component.d.mlua:16, mod/.../MODLogic.cs
OnMouseDown / BoxCollider2D is enough to receive clicks/touchesPhysics colliders and Rigidbody do not emit TouchEventWorld: TouchReceiveComponent / UI: ButtonComponent or UITouchReceiveComponentEmitTouchEvent in TouchReceiveComponent.d.mlua, §10
OnCollisionEnter + Rigidbody for collision callbacksEntity-to-entity collisions use a separate TriggerComponent + TriggerEnter/Leave/StayAttach TriggerComponent, then ConnectEvent(TriggerEnterEvent,...)TriggerComponent.d.mlua:56-62
Attach multiple Rigidbody/Collider freelyOne Body per map type (MapleTile→Rigidbody, RectTile→Kinematicbody, SideViewRectTile→Sideviewbody)Custom models include only the Body that matches the map typemsw-general/references/platform.md §4. DefaultPlayer has all three with engine auto-activation.
Reference/modify UI objects from server codeUI entities exist only on the client — referencing them from server @ExecSpace returns nilServer→UI must go through an @ExecSpace("Client") RPCmsw-general/references/ui.md:445-446
Instantiate(prefab) callable anywhere_SpawnService:SpawnByModelId(id, name, pos, parent) — parent is required, server-onlyInside ServerOnly/Server RPC, pass CurrentMap as parentSpawnService.d.mlua:22 (no default for parent)
static classes / hand-rolled singletons@Logic is itself an engine singletonCall from other scripts as _ScriptName:Method() (e.g., _TweenLogic, _UtilLogic, _ScreenMessageLogic) — never instantiate§3.2

Rule: any time you catch yourself thinking "Unity does it this way, so MSW probably does too", stop and verify against Environment/NativeScripts/*.d.mlua or the mod/ engine source before writing code.


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
    end

    @ExecSpace("ServerOnly")
    method void OnUpdate(number delta)
        -- per-frame logic
    end

    @ExecSpace("ServerOnly")
    method void OnEndPlay()
        -- cleanup
    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
    end

    @ExecSpace("ServerOnly")
    method void OnUpdate(number delta)
        -- per-frame logic
    end

end
  • One per world (singleton)
  • Accessed from other scripts as _GameManager (underscore + script name)
  • Supports @Sync properties — server→client sync behaves the same way
⚠️ Warning — @Logic does NOT have self.Entity [VERIFIED] The public members of the Logic parent class (Environment/NativeScripts/Logic/Logic.d.mlua) are only ConnectEvent / DisconnectEvent / IsClient / IsServer / SendEvent. There is no Entity property, GetOwner, or Owner. The engine source (mod/.../MODLogic.cs) confirms this. self.Entity is @Component-exclusive (readonly property Entity Entity in Component.d.mlua). Code like self.Entity.xxx inside a Logic compiles but produces a nil-access at runtime and silently fails. If a Logic needs to operate on a specific world entity, use one of these: ``lua @Logic script GameManager extends Logic property Entity spawnPoint = "uuid-string" -- inject UUID directly property EntityRef bossEntity = "" -- survives map transitions method void OnBeginPlay() local map = _EntityService:GetCurrentMap() -- current map local e = _EntityService:GetEntityByPath("/maps/Main/Entities/Boss") end end ` - **Property injection** (recommended): hard-code the UUID of an entity already placed in the map into the property default. - **Service lookup**: _EntityService:GetCurrentMap() / GetEntityByPath(...) / FindEntityByName(...)`.
Decision: @Component vs @Logic — behavior attached to an entity → @Component; global singleton manager → @Logic. A Logic's OnUpdate runs before Components' OnUpdate.

3.3 Extend scripts (extending an existing component)

@Component
script PlayerAttack extends AttackComponent
    -- Override AttackComponent's methods
    -- Call parent via __base:MethodName()
end

3.4 Other script types

AnnotationPurposeNotes
@EventDefine a custom event typeDeclare event parameters
@ItemDefine an item typeInventory system
@BTNodeBehaviour Tree nodeAI behavior trees
@StateDefine a state typeState machines
@StructStruct / user typeComposite data types

4. mlua Language Extensions (vs. plain Lua)

mlua is based on Lua 5.3 but differs in the following ways.

Added keywords / operators

FeatureSyntaxNotes
continuecontinueSkip to next iteration in a loop (not in standard Lua)
Compound assignment+=, -=, *=, /=, //=, %=, ^=, ..=Multi-assign (a, b += 1, 2) is invalid; cannot be used as a function arg (print(a += 1))
Bitwise operators&, `\, <<, >>`Compound forms also valid: &=, `\=, <<=, >>=`

Restrictions

RestrictionDescription
No global variablesThe global keyword is not allowed. Values shared across scripts must be declared as Properties.
No coroutinesLua's coroutine.create/resume/yield is not available.
__base instead of superCall parent methods with __base:MethodName(), not super.

Built-in utility functions

FunctionSignaturePurpose
log()log(any... args)Info-level log output
log_warning()log_warning(any... args)Warning-level log output
log_error()log_error(any... args)Error-level log output
wait()wait(number seconds)Pause script execution for the given seconds
isvalid()isvalid(any object) → booleanValidity check (handles deletion / nil)
enum()enum(table t) → tableSwap a table's keys and values
beginscope() / endscope()beginscope(string name) / endscope()User profiling scopes

5. Lifecycle

Component and Logic share the same lifecycle.

OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroy

Plus, on map transitions: OnMapEnter / OnMapLeave.

MethodWhen it firesPurpose
OnInitializeRight after creationInitialize internal variables (rarely used)
OnBeginPlayGame start / activationWire up events, start timers, initial setup
OnUpdate(delta)Every frameMovement, animation, input handling
OnMapEnterEntering a mapPer-map initialization
OnMapLeaveLeaving a mapPer-map cleanup
OnEndPlayGame end / deactivationDisconnect events, clear timers (mandatory!)
OnDestroyOn removalFinal cleanup (rarely used)

Required pattern: anything connected in OnBeginPlay must be released in OnEndPlay.

property any eventHandler = nil   -- store the EventHandlerBase returned by ConnectEvent
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

ConnectEvent return type: an EventHandlerBase object. When storing it as a property, declare it as any. If you declare it as integer, DisconnectEvent will fail to detach due to a type mismatch.


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

Key rules

-- Calling a ServerOnly function from the client → silently ignored (no error!)
@ExecSpace("ServerOnly")
method void TakeDamage(number amount)
    self.Hp = self.Hp - amount  -- runs only on the server
end

-- Server RPC: client calls → runs on the server
@ExecSpace("Server")
method void RequestAttack()
    -- client calls self:RequestAttack()
    -- actual execution happens on the server (network latency applies)
end

-- Client RPC: server calls → runs on that client
@ExecSpace("Client")
method void ShowDamageEffect(number damage)
    -- server calls self:ShowDamageEffect(50)
    -- runs on the targeted user's client
end

Typical server-client pattern

[Client]                    [Server]
  Detect input (ClientOnly)
       │
       └─── RequestAction() ──→ Validate + handle (ServerOnly)
                                     │
                                     ├─ State auto-syncs via @Sync
                                     │
       ←── ShowResult() ────────────┘  (Client RPC)
  Update UI (ClientOnly)

senderUserId — verifying the requester on the server

When an @ExecSpace("Server") method is called from the client, the server side can read the caller's UserId from the local senderUserId variable. This is required for security checks.

@ExecSpace("Server")
method void RequestBuyItem(integer itemId)
    -- Verify the requester: confirm the call came from the local client
    if senderUserId ~= self.Entity.PlayerComponent.UserId then
        log_warning("blocked request from a different client")
        return
    end
    self:ProcessPurchase(itemId)
end

Sending a Client RPC to a specific client only

When the server invokes an @ExecSpace("Client") function, adding a UserId as the last argument at the call site routes execution to that user's client only.

@ExecSpace("Client")
method void ShowReward(string itemName)
    log("reward earned: " .. itemName)
end

@ExecSpace("ServerOnly")
method void GiveReward(string playerId, string itemName)
    -- Show UI only on playerId's client
    self:ShowReward(itemName, playerId)
end
Do not add the UserId parameter to the function declaration. Add it only as the final argument at the call site.

Allowed parameter types across exec spaces

When functions are called across server↔client boundaries:

  • Allowed: string, integer, number, boolean, table, Vector2, Vector3, Vector4, Color, Entity, Component, EntityRef, ComponentRef
  • Not allowed: any
  • SyncTable generic parameters (k, v) must also be one of the allowed types above.

7. Property System

Basic property types

property number Speed = 5.0              -- floating point (float/double)
property integer Count = 0               -- integer
property string Name = "Player"          -- string
property boolean IsAlive = true          -- boolean
property Vector2 Direction = Vector2(0, 0)
property Vector3 Position = Vector3(0, 0, 0)
property Color Tint = Color(1, 1, 1, 1)
property any CustomData = nil            -- arbitrary type

Type-name reminder: integers are integer, floats are number.

Entity / Component reference properties

property Entity targetEntity = ""                    -- linked by UUID string
property Entity popup = "94a274e4-4111-40f1-924d-c95a3a1f14d5"
property ButtonComponent btnOk = "uuid-string"       -- specific component reference
property TextComponent txtScore = "uuid-string"

AI automation principle — inject UUID strings directly

The default value of an Entity/Component/EntityRef/ComponentRef property is a UUID string. The AI must NOT push the work onto the user (e.g., "drag it in the Maker editor"). Instead:

  1. Look up the target entity's id (UUID) in the .map / .ui file.
  2. Hard-code that UUID as a string literal into the .mlua property default.
  3. Apply the same pattern to multiple-slot references (e.g., wp0~wp7 array-style references) — inject each as a string.
@Logic
script WaypointPath extends Logic
    property Entity wp0 = "a1b2c3d4-...-000000000000"
    property Entity wp1 = "a1b2c3d4-...-000000000001"
    -- ... no drag required. Inject UUIDs from the map file as strings.
end
Note: drag-binding in the Maker editor is a convenience for human authors only. In an AI automation flow, UUID-string injection is the default path.

Entity vs EntityRef

TypeAfter map transitionUse case
EntityReference is dropped (nil)References within the same map
EntityRefReference persistsWhen the reference must survive a map transition
ComponentReference is droppedReferences within the same map
ComponentRefReference persistsWhen the reference must survive a map transition
Multi-map games should prefer EntityRef/ComponentRef. Entity/Component is sufficient for single-map games.

Sync annotations

AnnotationBehavior
@SyncServer → all clients
@TargetUserSyncServer → only that user's client

Both take no arguments.

@Sync
property number CurrentHp = 100
-- Server changes the value → automatically reflected on all clients

@TargetUserSync
property number PrivateScore = 0
-- Synced to the owning user's client only

Core rules:

  • Server → client, one direction only. Changing a @Sync value on the client does NOT propagate back to the server.
  • Sync has network latency — not instantaneous.
  • Cannot be synced: any, table (use SyncTable instead).

@TargetUserSync caveat: only meaningful on a component attached to a PlayerEntity. If attached to any other entity, it behaves like a regular @Sync. It pays off (saving bandwidth) for information that other users do not need to see, such as personal currency, achievements, or consumable counts.

SyncTable type

A table that can be synced. Supports both array and dictionary forms.

-- Array form: SyncTable<ValueType>
@Sync
property SyncTable<number> Scores = {}

-- Dictionary form: SyncTable<KeyType, ValueType>
@Sync
property SyncTable<string, number> Stats = {}
  • Use together with @Sync.
  • Different from a plain table — only SyncTable is synchronized.

Temporary properties (_T)

self._T exposes non-synced temporary properties created on the fly. No property declaration is required.

-- Use for non-synced, frame-local state
self._T.accumulatedDamage = 0
self._T.lastAttackTime = 0
self._T.isCharging = false
  • Cannot be @Sync'd — server and client each keep their own values.
  • Convenient because no property declaration is needed, but it does NOT show up in the editor inspector.

OnSyncProperty callback

A callback automatically invoked on the client when a @Sync property changes on the server.

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

Rules:

  • Fixed to ClientOnly — ExecSpace cannot be changed.
  • name: the changed property's name.
  • value: the new value.
  • Available on both Component and Logic.

Property editor attributes

Control how the property is shown in the Maker editor inspector:

@DisplayName("Display Name")           -- Override the name shown in the editor
property string InternalName = ""

@Description("Used for ~")             -- Inspector tooltip
property number Damage = 10

@MinValue(0)                           -- Min limit (number/integer)
@MaxValue(999)                         -- Max limit (number/integer)
@Delta(5)                              -- Step value for the mobile editor's +/- buttons
property integer Score = 0

@MaxLength(20)                         -- Max string length
property string Nickname = ""

@HideFromInspector                     -- Hide from the inspector
property any InternalState = nil

8. Event System / RPC

Static handler declaration (statically subscribed events)

@EventSender("Self")
handler HandleHitEvent(HitEvent event)
    local damage = event.TotalDamage
    -- Receives events emitted by my own entity
end

@EventSender parameters

1st parameter2nd parameterPurpose
"Self"noneEvents from my own entity
"LocalPlayer"noneEvents from the local player entity
"Entity"entity ID (string)Events from a specific entity
"Model"model ID (string)Events from a specific model
"Service"service type name (e.g., "InputService")Service events
"Logic"logic type nameLogic events
@EventSender("Service", "InputService")
handler HandleKeyDown(KeyDownEvent event)
    -- Receives key events emitted by InputService
end

Dynamic event connect / disconnect

-- Connect (in OnBeginPlay) — return type is an EventHandlerBase object
local eventHandler = entity:ConnectEvent(ButtonClickEvent, self.OnClick)
-- Disconnect (mandatory in OnEndPlay)
entity:DisconnectEvent(ButtonClickEvent, eventHandler)
⚠️ ConnectEvent is called on Entity / Logic / Service — NOT on a Component [VERIFIED] Only three types expose ConnectEvent / DisconnectEvent: - Entity (Misc/Entity.d.mlua:96-104) - Logic (Logic/Logic.d.mlua:7-15) - Service (Service/Service.d.mlua:8-16) The public members of the Component parent (Component.d.mlua) are only Enable / Entity / IsClient / IsServer. So no Component — including ButtonComponent and TriggerComponent — has its own ConnectEvent. Components only *emit* events; the entity they are attached to is what *receives* them. ``lua -- ❌ Wrong — Component has no ConnectEvent. Runtime nil access. self.Entity.ButtonComponent:ConnectEvent(ButtonClickEvent, self.OnClick) -- ✅ Correct — subscribe via the entity that owns the Component self.clickHandler = self.Entity:ConnectEvent(ButtonClickEvent, self.OnClick) -- ✅ Service events: subscribe on the service self.keyHandler = _InputService:ConnectEvent(KeyDownEvent, self.OnKeyDown) ``
⚠️ handler vs method void — do not mix them up [VERIFIED] | Declaration keyword | Use | Wiring | | --- | --- | --- | | handler Name(Ev event) | Static subscription — paired with the @EventSender(...) annotation. Engine wires it automatically. | Wired by declaration alone | | method void Name(Ev event) | Dynamic callback — wired at runtime via ConnectEvent(EvType, self.Name) | self.Entity:ConnectEvent(...) or _InputService:ConnectEvent(...) | Passing a handler as the callback to ConnectEvent compiles but never fires (E-V1-5). Conversely, putting a method void underneath an @EventSender won't get statically wired. Rules: - If @EventSender is also present → use handler. - If you will subscribe with ConnectEvent(...) → use method void.

Defining a CustomEvent — typed class style

The only way to author a CustomEvent is the typed-class form: @Event + extends EventType with property fields. There is no inline factory like CustomEvent("Name", {...}) — that signature does not exist in mlua. Always declare an event class.

Define the event:

@Event
script UserLogEvent extends EventType
    property string userId = ""
    property number logTime = 0
end

Dispatch from a Logic via SendEvent:

@Logic
script UserLogService extends Logic
    method void LogIn(string userId)
        local userLog = UserLogEvent()
        userLog.userId = userId
        userLog.logTime = DateTime.UtcNow.Elapsed
        _UserService:SendEvent(userLog)
    end
end

Receive via ConnectEvent on the entity. The first argument is the event Type (the class itself), and the callback must reference an existing method:

@Component
script UserLogComponent extends Component
    method void OnBeginPlay()
        self.Entity:ConnectEvent(UserLogEvent, self.OnUserLogEvent)
    end

    method void OnUserLogEvent(UserLogEvent event)
        log("User Id: " .. event.userId .. ", Login Time: " .. tostring(event.logTime))
    end
end

Sending events

Instantiate the typed event class, set its property fields, and pass the instance to SendEvent:

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

-- Send to my own entity
local dmg = DamageDealtEvent()
dmg.amount = 50
self.Entity:SendEvent(dmg)

-- Send to another entity
local heal = HealEvent()
heal.amount = 20
targetEntity:SendEvent(heal)

NativeEvent vs CustomEvent

NativeEventCustomEvent
DefinitionBuilt into the engine (.d.mlua)User-defined via @Event... extends EventType
ExamplesHitEvent, ButtonClickEvent, StateChangedEventUserLogEvent, DamageDealtEvent (any class you declare)
ParametersFixed (see per-event spec)property fields you declare on the class
ReferenceEnvironment/NativeScripts/Event/User code

Common NativeEvent parameters

-- HitEvent
event.TotalDamage      -- number: total damage
event.AttackerEntity   -- Entity: attacker
event.DamageType       -- DamageType: damage type

-- ButtonClickEvent
-- (no parameters; emitted by the entity)

-- StateChangedEvent
event.PrevState        -- string: previous state
event.CurState         -- string: current state

-- PlayerActionEvent
event.ActionName       -- string: action name

9. Validity Checks and Method Override

Validity checks

-- Entity validity (deletion / inactive check)
if isvalid(entity) then
    -- safe to access
end

-- Confirm a component exists
local comp = self.Entity.SomeComponent
if isvalid(comp) then
    -- only when the component is present
end

Caution: accessing a deleted entity is a runtime error. Always use isvalid().

Method override

  • Inside an extends-ing script, declaring a method with the same signature as the parent overrides it.
  • Built-in engine components allow override only for methods without ---@sealed.
  • Call the parent original via __base:MethodName(args) (optional, position is flexible).

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

World touch — two approaches

ApproachReceiverEvent (+ Hold/Release variants)Connect on
Entity touchAn entity with TouchReceiveComponentTouchEvententity:ConnectEvent(...)
Screen touchThe whole screen (no component required)ScreenTouchEvent_InputService:ConnectEvent(...)
  • Both events carry TouchId (int32) + TouchPoint (Vector2, screen coords).
  • Entity touch: control the touch area via TouchReceiveComponent's TouchArea/Offset/AutoFitToSize. Suited for per-object interactions (NPCs, items).
  • Screen touch: suited for coordinate-based interactions (tower placement, move target). For world coordinates, convert via _UILogic:ScreenToWorldPosition(event.TouchPoint). Filter UI clicks with _InputService:IsPointerOverUI().
  • If a map touch is not picked up by TouchEvent: it may be a TouchArea or raycast-priority issue. Combining ScreenTouchEvent + ScreenToWorldPosition() is more robust without configuration.
⚠️ Warning — BoxCollider2D / physics colliders do NOT emit TouchEvent [VERIFIED] If you reason from Unity's OnMouseDown / OnPointerClick and just attach BoxCollider2D / Rigidbody / TriggerComponent to an entity, no touch input will arrive. In MSW, world-entity touch reception is owned exclusively by TouchReceiveComponent (Environment/NativeScripts/Component/TouchReceiveComponent.d.mlua — EmitTouchEvent / EmitTouchHoldEvent / EmitTouchReleaseEvent exist only on this component). | Component | Role | TouchEvent | | --- | --- | --- | | BoxCollider2D, CircleCollider2D, Rigidbody/Kinematicbody, etc. | Physics collision / raycast | ❌ | | TriggerComponent | Entity-entity overlap callbacks (TriggerEnter/Exit) | ❌ | | TouchReceiveComponent | Touch-input reception | ✅ | Required setup (TouchReceiveComponent): - AutoFitToSize = true — auto-fits TouchArea to the SpriteRenderer/AvatarRenderer scale. Skips manual math. - TouchArea = Vector2(w, h) — when set manually, leave 10–20% slack beyond the sprite size (e.g., 1×1 sprite → 1.2×1.2). Too small leads to misaligned hit detection. - Offset — adjust only when the sprite pivot is not at center. - RelayEventToBehind = true (default) — forwards the event to entities behind. Set false only for standalone objects you want to block from passing through. Symptoms → diagnosis order: 1. Is TouchReceiveComponent actually attached to the target entity? (Check .map / .model.) 2. Is TouchArea zero, or is the entity outside the rendered area? 3. Is a front-most TouchReceiveComponent blocking with RelayEventToBehind = false? 4. Did you call entity:ConnectEvent(TouchEvent, handler) in OnBeginPlay and store the handler in property any? (If unstored, GC will collect it.)
Selection rule: "Which entity was touched" → TouchEvent; "Where on the screen was touched (coords)" → ScreenTouchEvent.

Clicks in UI

  • For UI entities, use the ButtonComponent + ButtonClickEvent pattern.
  • UI lives under ./ui/*.ui and the ui tree of the hierarchy.

What goes wrong if you mix them up

  • Putting only UI button events on a world object, or only world-touch components on UI, results in nothing happening.
  • Even if the requirement says "button," first decide whether it is a world object or a UI panel button.

11. Map Context and Entity Spawning

Prefer Entity.CurrentMap

  • For map-dependent logic, Entity.CurrentMap is safer and more readable.

Runtime entity spawning needs a model

  • To create an entity at runtime, use _SpawnService (SpawnByModelId / SpawnByEntity, etc.). A model (template) to spawn from must already exist.
  • If a brand-new kind of object is needed, follow this order: design a .model → place it in a map or write spawn code.

parent parameter caveats

  • SpawnByModelId's parent is required — there is no default, so you must pass a map entity. Passing nil leaves the entity orphaned (not parented), and the engine logs NativeIssue_NotRecommendedValue.
  • In contrast, SpawnByEntity defaults parent = nil and may be omitted — the two methods have different signatures.
  • Get the map entity via self.Entity.CurrentMap or _EntityService:GetEntitiesByPath("/maps/MapName").

Body components and direct Position writes

  • On entities with a Body component (Kinematicbody/Rigidbody/Sideviewbody), setting TransformComponent.WorldPosition directly will be overwritten by the physics engine on the next frame. This is a top cause of "movement doesn't work."
  • Per-frame movement: MovementComponent:MoveToDirection(direction, deltaTime).
  • Instant teleport: MovementComponent:SetPosition(pos) or the corresponding Body's SetPosition(pos).
  • Direct TransformComponent.WorldPosition writes are limited to entities without a Body (decorations, effects, etc.).
  • Do NOT remove the Body component as a workaround — tile collision and enter/leave events all become disabled, and the engine logs 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 / despawn entities (SpawnByModelId, SpawnByEntity, Despawn)
_TimerServiceTimers (SetTimer, SetTimerRepeat, ClearTimer)
_EntityServiceEntity lookup (GetEntity, GetEntities, GetEntitiesByPath)
_InputServiceInput state queries; receives ScreenTouchEvent
_ResourceServiceLook up resource RUIDs
_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

-- Random
local rand = _UtilLogic:RandomDouble()              -- 0.0 ~ 1.0
local randInt = _UtilLogic:RandomIntegerRange(1, 10) -- 1 ~ 10

-- Time
local elapsed = _UtilLogic.ElapsedSeconds           -- elapsed game time

-- Trig
local radian = math.rad(angle)
local x = math.cos(radian) * distance
local y = math.sin(radian) * distance

-- Distance
local diff = targetPos - myPos
local dist = math.sqrt(diff.x * diff.x + diff.y * diff.y)

mlua utility classes

Collections / utility types beyond the Lua standard library:

ClassPurposeNotes
ListDynamic array (1-based index)Add / remove / search / sort
ReadOnlyListRead-only arrayFor data protection
SyncListNetwork-synced arrayAuto-sync server↔client
DictionaryHash table (key-value)Fast lookup by unique key
ReadOnlyDictionaryRead-only hash tableFor data protection
SyncDictionaryNetwork-synced hash tableAuto-sync server↔client
DateTimeDate/timeFormat-string support
TimeSpanTime spanDays/hours/minutes/seconds/milliseconds
RegexRegular expressionMatch / search / replace
TranslatorLocalizationCurrent language, translated text lookup
Quaternion3D rotationAvoids gimbal lock; smooth rotations
Vector2IntInteger 2D vectorUseful for grid coords
FastVector2/3, FastColorHigh-performance vector / colorIn-place ops without new objects (when perf matters)
ItemInventory itemQuantity, icon RUID, data-table linkage
For detailed APIs, browse Environment/NativeScripts/ or query the msw-search skill.

Type annotations (code hints)

---@-style annotations help editors with autocomplete and type inference. They have no runtime effect — editor assist only.

---@type string
local name = GetPlayerName()

---@type table<string, Entity>
local entityMap = {}

---@param target Entity
---@param damage integer
---@return boolean
local function ApplyDamage(target, damage)
    return target ~= nil
end

Reserved words

Using mlua keywords as local variable names produces a parse error.

Forbidden as variable names: handler, property, method, script, end, extends, self, nil, true, false.

-- Wrong
local handler = entity:ConnectEvent(...)    -- 'handler' is a reserved word

-- Correct (ConnectEvent returns: EventHandlerBase object)
local eventHandler = entity:ConnectEvent(...)
local connHandler = entity:ConnectEvent(...)

14. External Tooling

NeedSkill
Maker MCP (refresh, logs, play, stop, screenshot, etc.)msw-general
MCP wiring, .mcp.json, API key setupmsw-env-setup
Descriptions, examples, and implementation guides not in .d.mluamsw-search

Core debug order: build logs first → play → logs → stop → fix → diagnose → refresh → repeat.


15. Script Authoring Workflow (essentials)

  1. Search: scan ./RootDesk/MyDesk/**/*.mlua → if a similar script exists, modify it first; do not create new.
  2. Verify spec: Read the .d.mlua → if insufficient, use msw-search.
  3. Decide path (see §1.2 — folder structure is mandatory): pick a feature/category subfolder. Reuse one if it already exists; otherwise create a new feature-named folder. Final path must look like ./RootDesk/MyDesk/<FeatureFolder>/<Name>.mlua — never write directly to ./RootDesk/MyDesk/ root.
  4. Write: create the file at the path chosen in step 3.
  5. Validate: mlua-diagnose hook auto-runs on save — fix until errors hit zero.
  6. Refresh: Maker MCP refresh (if MCP is not connected, point the user to the button).
  7. (If needed) play → reproduce → logs → stop.

Delete / rename: still requires refresh after the file change. Also clean up references in .model / .map.


16. Attaching Scripts (Components) to Entities

  • Attach to a model template (.model) — recommended: add the script-component entry to the Components array. Map instances inherit it automatically (must follow the .model JSON schema).
  • Only for a specific map instance: add the component to the matching entity definition inside the .map file.
  • Global models (e.g., DefaultPlayer under ./Global/): affect the entire project — confirm the blast radius before changing.

In all cases, do NOT edit .model / .map JSON directly — use the personal builder.


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 (the first step of every playtest)

Before entering play mode, you MUST inspect the build console (build logs) first. If there are build errors, scripts will not load or will behave unexpectedly, making runtime debugging meaningless.

1. After refresh → call logs(category="build")
2. Are there build errors?
   ├─ YES → from the error messages, identify file and line → edit the .mlua → refresh → recheck build logs (repeat until errors are zero)
   └─ NO → enter play → run runtime tests

If you run play with build errors:

  • Scripts with errors fail to load entirely — it behaves as if the component/logic isn't there.
  • Build errors may not appear in the runtime logs, making the cause extremely hard to find.
  • Most "the code looks correct but it doesn't work" reports come from missed build errors.
This step is mandatory in every workflow pattern (general test, regression test, error analysis).

17.2 Error Classification Table (by log / symptom)

When reading logs (and the script stack), do a first-pass classification with the table below.

ClassCommon signsWhere to look
Script errorStack trace with file name and line numberThe exact line in the .mlua; event/timing order
nil referenceattempt to index a nil value, crash right before a field accessInit order, isvalid, 1-frame timing right after Spawn
component missingComponent field is nil; GetComponent failsComponents array in .model; typos in name/path
network / syncOnly client breaks; values mismatch; values converge after a delay@Sync, only-on-server changes, ExecSpace, RPC flow

To narrow down a cause: if logs alone are inconclusive, add log() outputs to the .mlua to inspect the relevant entity / component / property state.

17.3 Test-Result Report Format

When a playtest ends, summarize briefly in this format.

  1. Scenario name: one-line description of what was verified.
  2. Environment: map / mode (if known); whether a refresh happened before playing.
  3. Steps: a summary of the execution order — input simulation, Lua runs, etc.
  4. Result: Pass / Fail / Blocked (e.g., couldn't enter play).
  5. Evidence: one or two key lines quoted from logs; whether a screenshot exists (only if the user asked for one).
  6. Next action: candidate files to edit, repro conditions, whether to use clear_logs on the next run.

17.4 Workflow Patterns

1) General playtest
  1. Prepare scripts / maps / models in edit mode.
  2. If the workspace was changed, call MCP refresh.
  3. Check build errors with logs(category="build") → if any, fix and refresh until clean.
  4. MCP play → enter play mode.
  5. As needed, use keyboard_input / mouse_input to reproduce input or UI clicks.
  6. As needed, use logs to inspect runtime state.
  7. As needed, use logs(category="runtime") to inspect runtime logs.
  8. MCP stop → return to edit mode.
2) Regression test loop (fix loop)
  1. Edit files based on the previous failure cause.
  2. refresh.
  3. Check build errors with logs(category="build") → if any, fix and refresh until clean.
  4. For a clean repro, call clear_logs then play.
  5. Replay the same scenario via input / Lua.
  6. Use logs(category="runtime") to confirm regression status.
  7. stop, then edit again if needed.
3) Error-analysis workflow
  1. First check build errors with logs(category="build") — fix them before any runtime analysis.
  2. clear_logs (optional) → play.
  3. Reproduce the issue via keyboard_input / mouse_input / in-game manipulation.
  4. Collect logs(category="runtime") and map them to the error classification table above.
  5. If logs are insufficient, add log() outputs in the .mlua to inspect entity / property state.
  6. After stop, fix the code / .model / sync.
  7. refresh → recheck build logs → play to re-verify.
4) Runtime Lua debugging
  1. Add log() calls in the .mlua for the values you want to inspect.
  2. If you don't know the API, look it up first: search .d.mlua → msw-search.
  3. refresh → play to enter play mode.
  4. Collect output via logs and analyze.
  5. After analysis, stop → edit → repeat.

17.5 Final Verification Before Completion (PASS/FAIL)

Before reporting "done" to the user, you must pass the following checklist:

Principle: "No errors ≠ Pass." You need positive log()-based evidence that the intended logic actually executed.

Full checklist: references/verify-checklist.md (Step 1 Runtime Execution → Step 2 Code Review → Step 3 Log Evidence → Step 4 PASS/FAIL Verdict)

17.6 Related Skills

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

平台分布

Codex

35.83%
按下载量换算53

Claude

30.84%
按下载量换算46

Cursor

17.79%
按下载量换算26

Gemini CLI

9.97%
按下载量换算15

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

调用外部服务

该 Skill 可能调用第三方服务、云服务或外部模型 API,使用前需要确认账号、额度、数据发送范围和服务条款。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills