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 , you must search under 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
must be created,
organize it under a feature/category subfolder. Do
not drop scripts directly into
.
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., , , , , ). Run a glob/list on 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 , not at the root, so future additions have a home.
- Naming: folder = feature noun (, ); file = role-specific name reusing the feature noun where helpful (,
QuestTrackerComponent.mlua
).
- Do not create generic catch-all folders like , , , , . Pick a real feature name. If you genuinely have a project-wide utility, place it under a specific utility folder such as or only if that pattern already exists in the project.
Why: a flat
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 definition file directly (exact signatures):
The full engine API is defined as
files under
./Environment/NativeScripts/
:
| Folder | Contents | Files | Example |
|---|
| Engine components | 104 | TransformComponent.d.mlua
|
| System services | 46 | |
| Event types | 202 | |
| Built-in logic | 9 | |
| Enumerations | 118 | |
| Utility types | 135 | |
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 — skill (detailed descriptions, examples, implementation guides):
When
only contains signatures and lacks explanation, search for
parameter semantics, code examples, related APIs, and implementation guides.
Required order: confirm signature in
→ (if needed) look up details via
→ write code → LSP diagnose runs automatically (PostToolUse hook).
1.4 Lint (LSP diagnostics) — required after every script change
- Whenever you create or modify a file, the hook runs LSP automatically and feeds errors back.
- Repeat fix → re-edit until error-severity diagnostics reach zero.
1.5 files
- 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 , you must call Maker MCP .
- cannot run during play mode — first call to return to edit mode.
1.7 MSW ≠ Unity — Do Not Reason From Intuition
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 intuition | MSW reality | Correct path | Source |
|---|
| / to access the owning entity | has no (that is -only) | Inject via property Entity x = "uuid"
on the Logic, or use _EntityService:GetEntityByPath(...)
| , , |
| / is enough to receive clicks/touches | Physics colliders and Rigidbody do not emit | World: / UI: or | in TouchReceiveComponent.d.mlua
, §10 |
| + Rigidbody for collision callbacks | Entity-to-entity collisions use a separate + | Attach , then ConnectEvent(TriggerEnterEvent, ...)
| TriggerComponent.d.mlua:56-62
|
| Attach multiple Rigidbody/Collider freely | One Body per map type (MapleTile→Rigidbody, RectTile→Kinematicbody, SideViewRectTile→Sideviewbody) | Custom models include only the Body that matches the map type | msw-general/references/platform.md §4
. has all three with engine auto-activation. |
| Reference/modify UI objects from server code | UI entities exist only on the client — referencing them from server returns nil | Server→UI must go through an RPC | msw-general/references/ui.md:445-446
|
| callable anywhere | _SpawnService:SpawnByModelId(id, name, pos, parent)
— is required, server-only | Inside / RPC, pass as parent | (no default for parent) |
| classes / hand-rolled singletons | is itself an engine singleton | Call from other scripts as (e.g., , , ) — 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
engine source before writing code.
2. Paths and File Roles
| Target | Path | Agent action |
|---|
| User scripts | ./RootDesk/MyDesk/**/*.mlua
| Create / read / modify / delete directly |
| Auto-generated artifacts | | Do not touch (Refresh manages them) |
| Engine API definitions | ./Environment/NativeScripts/**
| Read-only (do not modify) |
| Models (component lists) | ./RootDesk/MyDesk/*.model
, , etc. | Edit when attaching scripts |
| Map instances | | Edit when attaching scripts to entities that exist only inside a map |
3. Script Types and Declarations
3.1 Component scripts ()
Scripts attached to an Entity. Use
to access the owning entity.
lua
@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:
- — generic component
- — attack system (Shape, AttackFast, OnAttack)
- — hit system (OnHit, HandleHitEvent)
3.2 Logic scripts ()
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
end
@ExecSpace("ServerOnly")
method void OnUpdate(number delta)
-- per-frame logic
end
end
- One per world (singleton)
- Accessed from other scripts as (underscore + script name)
- Supports properties — server→client sync behaves the same way
⚠️
Warning — does NOT have
The public members of the
parent class (
Environment/NativeScripts/Logic/Logic.d.mlua
) are
only /
/
/
/
. There is no
property,
, or
. The engine source (
) confirms this.
is
-exclusive (
readonly property Entity Entity
in
).
Code like
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()
/ / .
Decision: @Component vs @Logic — behavior attached to an entity →
; global singleton manager →
. A Logic's
runs
before Components'
.
3.3 Extend scripts (extending an existing component)
lua
@Component
script PlayerAttack extends AttackComponent
-- Override AttackComponent's methods
-- Call parent via __base:MethodName()
end
3.4 Other script types
| Annotation | Purpose | Notes |
|---|
| Define a custom event type | Declare event parameters |
| Define an item type | Inventory system |
| Behaviour Tree node | AI behavior trees |
| Define a state type | State machines |
| Struct / user type | Composite 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
| Feature | Syntax | Notes |
|---|
| continue | | Skip to next iteration in a loop (not in standard Lua) |
| Compound assignment | , , , , , , , | Multi-assign () is invalid; cannot be used as a function arg () |
| Bitwise operators | , , , | Compound forms also valid: , , , |
Restrictions
| Restriction | Description |
|---|
| No global variables | The keyword is not allowed. Values shared across scripts must be declared as Properties. |
| No coroutines | Lua's coroutine.create/resume/yield
is not available. |
| instead of | Call parent methods with , not . |
Built-in utility functions
| Function | Signature | Purpose |
|---|
| | Info-level log output |
| | Warning-level log output |
| | Error-level log output |
| | Pause script execution for the given seconds |
| isvalid(any object) → boolean
| Validity check (handles deletion / nil) |
| | Swap a table's keys and values |
| / | / | User profiling scopes |
5. Lifecycle
Component and Logic share the same lifecycle.
OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroy
Plus, on map transitions:
/
.
| Method | When it fires | Purpose |
|---|
| Right after creation | Initialize internal variables (rarely used) |
| Game start / activation | Wire up events, start timers, initial setup |
| Every frame | Movement, animation, input handling |
| Entering a map | Per-map initialization |
| Leaving a map | Per-map cleanup |
| Game end / deactivation | Disconnect events, clear timers (mandatory!) |
| On removal | Final cleanup (rarely used) |
Required pattern: anything connected in
must be released in
.
lua
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
return type: an
object. When storing it as a property, declare it as
. If you declare it as
,
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.
| ExecSpace | Runs on | Direction | Use case |
|---|
| Server | Server-internal only | Damage calc, state changes, spawning |
| Client | Client-internal only | UI updates, effects, sounds |
| Server | Client→Server RPC | Client requesting the server (attack, item use) |
| Client | Server→Client RPC | Server notifying a client (result UI, effects) |
| 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 |
|---|
| only |
| , , , , , , | , , or unspecified |
| All event handlers | , , or unspecified |
| Custom user methods | Any of , , , , |
Key rules
lua
-- 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)
— verifying the requester on the server
When an
method is called from the client, the server side can read the
caller's UserId from the local
variable. This is required for security checks.
lua
@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
function,
adding a UserId as the last argument at the call site routes execution to that user's client only.
lua
@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: , , , , , , , , , , , ,
- Not allowed:
- SyncTable generic parameters (k, v) must also be one of the allowed types above.
7. Property System
Basic property types
lua
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
, floats are
.
Entity / Component reference properties
lua
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:
- Look up the target entity's (UUID) in the / file.
- Hard-code that UUID as a string literal into the property default.
- Apply the same pattern to multiple-slot references (e.g., array-style references) — inject each as a string.
lua
@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
| Type | After map transition | Use case |
|---|
| Reference is dropped (nil) | References within the same map |
| Reference persists | When the reference must survive a map transition |
| Reference is dropped | References within the same map |
| Reference persists | When the reference must survive a map transition |
Multi-map games should prefer
/
.
/
is sufficient for single-map games.
Sync annotations
| Annotation | Behavior |
|---|
| Server → all clients |
| Server → only that user's client |
Both take no arguments.
lua
@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 value on the client does NOT propagate back to the server.
- Sync has network latency — not instantaneous.
- Cannot be synced: , (use instead).
caveat: only meaningful on a component attached to a PlayerEntity. If attached to any other entity, it behaves like a regular
. 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.
lua
-- Array form: SyncTable<ValueType>
@Sync
property SyncTable<number> Scores = {}
-- Dictionary form: SyncTable<KeyType, ValueType>
@Sync
property SyncTable<string, number> Stats = {}
- Use together with .
- Different from a plain — only is synchronized.
Temporary properties ()
exposes non-synced temporary properties created on the fly. No
declaration is required.
lua
-- Use for non-synced, frame-local state
self._T.accumulatedDamage = 0
self._T.lastAttackTime = 0
self._T.isCharging = false
- Cannot be '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.
callback
A callback automatically invoked on the client when a
property changes on the server.
lua
@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 — ExecSpace cannot be changed.
- : the changed property's name.
- : the new value.
- Available on both Component and Logic.
Property editor attributes
Control how the property is shown in the Maker editor inspector:
lua
@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)
lua
@EventSender("Self")
handler HandleHitEvent(HitEvent event)
local damage = event.TotalDamage
-- Receives events emitted by my own entity
end
parameters
| 1st parameter | 2nd parameter | Purpose |
|---|
| none | Events from my own entity |
| none | Events from the local player entity |
| entity ID (string) | Events from a specific entity |
| model ID (string) | Events from a specific model |
| service type name (e.g., ) | Service events |
| logic type name | Logic events |
lua
@EventSender("Service", "InputService")
handler HandleKeyDown(KeyDownEvent event)
-- Receives key events emitted by InputService
end
Dynamic event connect / disconnect
lua
-- Connect (in OnBeginPlay) — return type is an EventHandlerBase object
local eventHandler = entity:ConnectEvent(ButtonClickEvent, self.OnClick)
-- Disconnect (mandatory in OnEndPlay)
entity:DisconnectEvent(ButtonClickEvent, eventHandler)
⚠️
is called on Entity / Logic / Service — NOT on a Component
Only
three types expose
/
:
- (
Misc/Entity.d.mlua:96-104
)
- ()
- (
Service/Service.d.mlua:8-16
)
The public members of the
parent (
) are only
/
/
/
. So
no Component — including and — has its own . 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)
⚠️
vs — do not mix them up
| Declaration keyword | Use | Wiring |
|---|
| Static subscription — paired with the 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
as the callback to
compiles but never fires (E-V1-5). Conversely, putting a
underneath an
won't get statically wired. Rules:
- If is also present → use .
- If you will subscribe with → use .
Defining a CustomEvent — typed class style
The
only way to author a CustomEvent is the typed-class form:
+
with
fields. There is no inline factory like
CustomEvent("Name", { ... })
— that signature does not exist in mlua. Always declare an event class.
Define the event:
lua
@Event
script UserLogEvent extends EventType
property string userId = ""
property number logTime = 0
end
Dispatch from a Logic via
:
lua
@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
on the entity. The first argument is the event
Type (the class itself), and the callback must reference an existing method:
lua
@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
fields, and pass the instance to
:
lua
@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
| NativeEvent | CustomEvent |
|---|
| Definition | Built into the engine () | User-defined via @Event ... extends EventType
|
| Examples | HitEvent, ButtonClickEvent, StateChangedEvent | UserLogEvent, DamageDealtEvent (any class you declare) |
| Parameters | Fixed (see per-event spec) | fields you declare on the class |
| Reference | Environment/NativeScripts/Event/
| User code |
Common NativeEvent parameters
lua
-- 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
lua
-- 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
.
Method override
- Inside an -ing script, declaring a with the same signature as the parent overrides it.
- Built-in engine components allow override only for methods without .
- Call the parent original via (optional, position is flexible).
10. Input / Click Events — World vs UI (Do Not Confuse)
World touch — two approaches
| Approach | Receiver | Event (+ Hold/Release variants) | Connect on |
|---|
| Entity touch | An entity with | | |
| Screen touch | The whole screen (no component required) | | _InputService:ConnectEvent(...)
|
- Both events carry (int32) + (Vector2, screen coords).
- Entity touch: control the touch area via 's //. 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 : it may be a or raycast-priority issue. Combining + is more robust without configuration.
⚠️
Warning — / physics colliders do NOT emit
If you reason from Unity's
/
and just attach
/
/
to an entity,
no touch input will arrive. In MSW, world-entity touch reception is owned exclusively by
(
Environment/NativeScripts/Component/TouchReceiveComponent.d.mlua
—
/
/
exist only on this component).
| Component | Role | TouchEvent |
|---|
| , , Rigidbody/Kinematicbody, etc. | Physics collision / raycast | ❌ |
| Entity-entity overlap callbacks () | ❌ |
| Touch-input reception | ✅ |
- — auto-fits to the / 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.
- — adjust only when the sprite pivot is not at center.
RelayEventToBehind = true
(default) — forwards the event to entities behind. Set only for standalone objects you want to block from passing through.
Symptoms → diagnosis order:
- Is actually attached to the target entity? (Check / .)
- Is zero, or is the entity outside the rendered area?
- Is a front-most blocking with
RelayEventToBehind = false
?
- Did you call
entity:ConnectEvent(TouchEvent, handler)
in and store the handler in ? (If unstored, GC will collect it.)
Selection rule: "Which entity was touched" →
; "Where on the screen was touched (coords)" →
.
Clicks in UI
- For UI entities, use the + pattern.
- UI lives under and the 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
- For map-dependent logic, is safer and more readable.
Runtime entity spawning needs a model
- To create an entity at runtime, use ( / , etc.). A model (template) to spawn from must already exist.
- If a brand-new kind of object is needed, follow this order: design a → place it in a map or write spawn code.
parameter caveats
- 's is required — there is no default, so you must pass a map entity. Passing leaves the entity orphaned (not parented), and the engine logs
NativeIssue_NotRecommendedValue
.
- In contrast, defaults and may be omitted — the two methods have different signatures.
- Get the map entity via 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 .
- 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
(underscore + type name). Only the most common ones are listed.
| Service / Logic | Purpose |
|---|
| Spawn / despawn entities (, , ) |
| Timers (, , ) |
| Entity lookup (, , ) |
| Input state queries; receives |
| Look up resource RUIDs |
| Persistent data (player saves) — ⚠️ Credit-billed. Do not call in / short timers; use in loops. Details: references/datastorage.md |
| Random, time, string, and math utilities |
| Tween animations (MoveTo, ScaleTo, RotateTo) |
| UI coordinate conversions (e.g., ScreenToWorldPosition) — ClientOnly |
For the full list, read the
files directly:
./Environment/NativeScripts/Service/
(46 files) and
./Environment/NativeScripts/Logic/
(9 files). For domain details, search via
.
13. Math, Utilities, Reserved Words, Type Annotations
Math / utility examples
lua
-- 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:
| Class | Purpose | Notes |
|---|
| Dynamic array (1-based index) | Add / remove / search / sort |
| Read-only array | For data protection |
| Network-synced array | Auto-sync server↔client |
| Hash table (key-value) | Fast lookup by unique key |
| Read-only hash table | For data protection |
| Network-synced hash table | Auto-sync server↔client |
| Date/time | Format-string support |
| Time span | Days/hours/minutes/seconds/milliseconds |
| Regular expression | Match / search / replace |
| Localization | Current language, translated text lookup |
| 3D rotation | Avoids gimbal lock; smooth rotations |
| Integer 2D vector | Useful for grid coords |
| , | High-performance vector / color | In-place ops without new objects (when perf matters) |
| Inventory item | Quantity, icon RUID, data-table linkage |
For detailed APIs, browse
Environment/NativeScripts/
or query the
skill.
Type annotations (code hints)
-style annotations help editors with autocomplete and type inference.
They have no runtime effect — editor assist only.
lua
---@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:
,
,
,
,
,
,
,
,
,
.
lua
-- 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
| Need | Skill |
|---|
| Maker MCP (, , , , , etc.) | |
| MCP wiring, , API key setup | |
| Descriptions, examples, and implementation guides not in | |
Core debug order: build logs first → play → logs → stop → fix → diagnose → refresh → repeat.
15. Script Authoring Workflow (essentials)
- Search: scan
./RootDesk/MyDesk/**/*.mlua
→ if a similar script exists, modify it first; do not create new.
- Verify spec: Read the → if insufficient, use .
- 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 root.
- Write: create the file at the path chosen in step 3.
- Validate: hook auto-runs on save — fix until errors hit zero.
- Refresh: Maker MCP (if MCP is not connected, point the user to the button).
- (If needed) → reproduce → → .
Delete / rename: still requires
after the file change. Also clean up references in
/
.
16. Attaching Scripts (Components) to Entities
- Attach to a model template () — recommended: add the script-component entry to the array. Map instances inherit it automatically (must follow the JSON schema).
- Only for a specific map instance: add the component to the matching entity definition inside the file.
- Global models (e.g., under ): affect the entire project — confirm the blast radius before changing.
In all cases, do NOT edit
/
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
.
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
(and the script stack), do a
first-pass classification with the table below.
| Class | Common signs | Where to look |
|---|
| Script error | Stack trace with file name and line number | The exact line in the ; event/timing order |
| nil reference | attempt to index a nil value
, crash right before a field access | Init order, , 1-frame timing right after Spawn |
| component missing | Component field is nil; fails | array in ; typos in name/path |
| network / sync | Only client breaks; values mismatch; values converge after a delay | , only-on-server changes, , RPC flow |
To narrow down a cause: if logs alone are inconclusive, add
outputs to the
to inspect the relevant entity / component / property state.
17.3 Test-Result Report Format
When a playtest ends, summarize briefly in this format.
- Scenario name: one-line description of what was verified.
- Environment: map / mode (if known); whether a happened before playing.
- Steps: a summary of the execution order — input simulation, Lua runs, etc.
- Result: Pass / Fail / Blocked (e.g., couldn't enter play).
- Evidence: one or two key lines quoted from ; whether a screenshot exists (only if the user asked for one).
- Next action: candidate files to edit, repro conditions, whether to use on the next run.
17.4 Workflow Patterns
1) General playtest
- Prepare scripts / maps / models in edit mode.
- If the workspace was changed, call MCP .
- Check build errors with → if any, fix and refresh until clean.
- MCP → enter play mode.
- As needed, use / to reproduce input or UI clicks.
- As needed, use to inspect runtime state.
- As needed, use to inspect runtime logs.
- MCP → return to edit mode.
2) Regression test loop (fix loop)
- Edit files based on the previous failure cause.
- .
- Check build errors with → if any, fix and refresh until clean.
- For a clean repro, call then .
- Replay the same scenario via input / Lua.
- Use to confirm regression status.
- , then edit again if needed.
3) Error-analysis workflow
- First check build errors with — fix them before any runtime analysis.
- (optional) → .
- Reproduce the issue via / / in-game manipulation.
- Collect and map them to the error classification table above.
- If logs are insufficient, add outputs in the to inspect entity / property state.
- After , fix the code / / sync.
- → recheck build logs → to re-verify.
4) Runtime Lua debugging
- Add calls in the for the values you want to inspect.
- If you don't know the API, look it up first: search → .
- → to enter play mode.
- Collect output via and analyze.
- After analysis, → 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 -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
- : MCP tools, screenshot/logs policy, refresh rules, workspace and hierarchy.