Token导航 LogoToken导航TokenDH.com
研究检索external-servicegithub未标认证来源可访问许可证需确认审计通过

msw-scripting城市软件脚本

Agent Skill

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

总安装

423

周安装

18

GitHub Stars

公开资料未说明

下载量

148
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 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

简介

msw-scripting 用于查找、检索和筛选相关信息,适合快速定位候选结果。

  • 适用于需要根据关键词或任务场景从来源线索中获取信息的场景。
  • 通过 npx skills add 命令安装,需结合原始 README 核验具体用法。
  • 安装前建议确认权限范围、维护状态及是否触发联网或文件读写操作。
  • 适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

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.mluaEmitTouchEvent / 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>.mluanever 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 → logsstop.

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. refreshrecheck build logsplay 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.mluamsw-search.
  3. refreshplay 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.

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

35.83%
按下载量换算53

Claude

30.84%
按下载量换算46

Cursor

17.79%
按下载量换算26

Gemini CLI

9.97%
按下载量换算15

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

external-service

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

安装前确认

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

来源信息

继续浏览同类 Skills