
Msw General
- 4.3k installs
- 33 repo stars
- Updated July 29, 2026
- msw-git/msw-ai-coding-plugins-official
msw-general is the foundation MapleStory Worlds skill covering model templates, ModelBuilder authoring, visual resource application, and animation state rules for polished games.
About
MSW General is the mandatory foundation skill for MapleStory Worlds game creation, integrating shared tools, domain knowledge, platform rules, and file authoring that every other MSW skill depends on. The core principle demands visual polish: entities need appropriate sprites, animations, and sounds via msw-search rather than default or empty SpriteRUID placeholders because functionality alone does not equal a finished game. Model work starts from validated templates in the skill-local models catalog covering monsters, NPCs, players, terrain, particles, sound, tile maps, and UI types rather than empty files. The workflow reads references/model.md fully, picks the closest template, loads it through ModelBuilder per the protocol reference, replaces EntryKey, Id, and Name identifiers, customizes Components and Values, and saves under typed subfolders like Models/Monsters rather than directly under MyDesk. Monster creation chooses Pattern A Soldier-style custom AI scripts or Pattern B MonsterCanonical with AIChaseComponent and mandatory StateComponent IsLegacy false. Any entity with StateAnimationComponent must also read animation-state.md for the state machine pipeline.
- Foundation skill required before any other MSW MapleStory Worlds creation work.
- Visual polish mandate: apply sprites and sounds via msw-search, never leave default SpriteRUID.
- ModelBuilder workflow loads validated templates from categorized models catalog.
- Monster Pattern A Soldier AI versus Pattern B AIChase with IsLegacy false requirements.
- Mandatory preflight read of model.md and animation-state.md for stateful entities.
Msw General by the numbers
- 4,328 all-time installs (skills.sh)
- +539 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #4 of 247 Game Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
msw-general capabilities & compatibility
- Capabilities
- modelbuilder template loading with entrykey id n · categorized models catalog for monsters npcs ter · visual polish enforcement via msw search sprite · monster pattern a and pattern b animation state · builder emitted value metadata avoiding hand wri
- Use cases
- orchestration · ui design · testing
What msw-general says it does
Read this FIRST before anything else in MSW.
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-generalAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.3k |
|---|---|
| repo stars | ★ 33 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 29, 2026 |
| Repository | msw-git/msw-ai-coding-plugins-official ↗ |
How do agents create polished MapleStory Worlds entities with correct model templates, sprites, and animation state instead of blank defaults?
Foundation MSW MapleStory Worlds skill for model authoring, resource search, animation states, and polished entity creation via ModelBuilder.
Who is it for?
Agents building MapleStory Worlds games who must read this skill first before monsters, NPCs, UI, or other MSW domain skills.
Skip if: Skip for non-MSW game engines or tasks outside MapleStory Worlds platform authoring.
When should I use this skill?
User creates MSW entities, .model files, MapleStory Worlds monsters, or needs msw-search resource application guidance.
What you get
Typed .model files under correct subfolders with applied sprites, sounds, validated builder metadata, and animation state configuration.
- typed .model entity files
- applied sprite and sound RUIDs
- animation state configuration
Files
MSW General — Foundation Skill
The foundation skill for MSW (MapleStory Worlds) creation, integrating shared tools, domain knowledge, platform rules, and file authoring. Every other MSW skill depends on it.
---
Core Principle: Visual Polish
MSW is a game creation platform. The goal is not a prototype where logic merely runs — it is a polished game players can enjoy.
So whatever entity you create — monster, NPC, tower, item, background object — search for and apply appropriate resources (sprites, animations, sounds) that match its role and personality. Do not leave the default sprite in place or leave SpriteRUID empty.
Resource application principle when creating an entity:
1. After creating the entity, use the `msw-search` skill to find sprites/animations that fit it 2. Apply the RUID of the found resource to SpriteRendererComponent so the entity is visually represented 3. If there is combat, also set hit/explosion effects; if there is interaction, set sound effects
Functionality implemented != finished. A polished game requires appropriate resources plus visual presentation.
---
When making a .model — catalog first
Do not start a new .model from an empty file. The skill-local `models/` folder contains validated templates organized by category — monsters (ChaseMonster/MoveMonster/StaticMonster), NPC (StaticNPC), players (Player/DefaultPlayer), terrain (Foothold/Ladder/Rope/Portal), map objects (MapObject/SkeletonMapObject/ItemAsset), particles (BasicParticle/SpriteParticle/AreaParticle/AnimationPlayer), sound (Sound/SoundEffect), tile map containers (TileMap/RectTileMap), UI (UIButton/UIText/UISprite/UIGroup, etc.), external media (WebSprite/YoutubePlayerWorld).
Workflow: `Read` [references/model.md](references/model.md) IN FULL FIRST (mandatory — see "Model Work Preflight — MUST" below) → pick the closest template from the catalog → load it via ModelBuilder (call protocol: `references/builder-protocol.md`) → replace the 3 identifiers (EntryKey, Id, Name) through the builder → customize Components/Values/Properties/Children through the builder → save under a typed subfolder of `RootDesk/MyDesk/Models/` (e.g. Models/Monsters/{Name}.model, never directly under MyDesk/) → refresh. Detailed catalog and builder procedure: references/model.md §2.
The builder emits the required value metadata, so agents do not need to read or hand-write .model format internals.For a monster, first pick a pattern in [references/animation-state.md §0](references/animation-state.md), then follow [references/monster.md §5](references/monster.md) for the recommended path.
- Pattern A (verified working canonical — Soldier reference setup, full source inlined in [`references/monster.md` §7](references/monster.md)): no template needed; assemble 11 components from scratch with a customscript.MyMonsterAI(SoldierAI-style) instead of AIChase/AIWander.StateComponent.IsLegacyleft at the default.
- Pattern B (`MonsterCanonical.model`):AIChaseComponent+ActionSheetpipeline.StateComponent.IsLegacy=falsemandatory. The other monster templates (ChaseMonster/MoveMonster/StaticMonster) leaveActionSheetempty and use defaults that silently fail under Pattern B (uppercase keys,SortingLayer="Default",IsLegacy=true).
>
For any entity that has `StateAnimationComponent` (monster/NPC) or `AvatarStateAnimationComponent` (player) — or any `.mlua` that calls `ChangeState` / `AddState` / `SetActionSheet` — also read [references/animation-state.md](references/animation-state.md). The state-machine ↔ animation pipeline, the two-pattern split, default-state registration rules,[LEA-3005]cause, andSetActionSheetvsChangeStatesemantics live there (not duplicated into per-entity docs).
---
Placing multiple entities — model first
If the same entity is going to appear twice or more in a map (5 monsters, 10 trees, 3 portals, …), author a `.model` first and place each instance via `modelId` rather than copy-pasting inline @components.
| Instance count of same composition | Choice |
|---|---|
| 1 (truly one-off decoration in a single map) | inline @components is acceptable |
| ≥2 | `.model` + `modelId` instances (default) |
Spawned at runtime (SpawnByModelId) | .model is required regardless of count |
Why this is the default:
- Edit once, propagate everywhere — change
SpriteRUID/HP/ActionSheetin the model and every instance updates. Inline copies require touching N entities each time. - Smaller, reviewable `.map` diffs —
modelIdinstances carry onlyTransformoverrides; inline copies bloat the map by hundreds of lines per entity. - Avoids drift — five inline copies silently diverge (one gets
IsLegacy: true, another forgetsSortingLayer: "MapLayer0"). The model anchors the canonical values. - Required for `SpawnByModelId` — without a registered model id, dynamic spawning fails.
Workflow: 1. Author .model under RootDesk/MyDesk/Models/{Category}/{Name}.model (see folder rule above). 2. Place each instance via MapBuilder (call protocol: `references/builder-protocol.md`) so ids, paths, componentNames, origin metadata, and per-instance component overrides stay synchronized. 3. refresh.
Details and the inline-vs-modelId comparison: references/entity.md "Two-Step Map Editing Workflow", references/model.md §1.
---
Entity Work Preflight — MUST
If the task involves an entity in any way, you must read [references/entity.md](references/entity.md) first. No exceptions.
---
Builder Protocol Preflight — MUST
If the task creates or modifies any `.map` / `.model` / `.ui` file — directly, or as a side effect of writing .mlua that spawns / places / binds — you must `Read` [references/builder-protocol.md](references/builder-protocol.md) IN FULL FIRST (no offset / limit). No exceptions.
builder-protocol.md is the single entry point that consolidates the call protocol of all three builders (MapBuilder / ModelBuilder / UIBuilder) into one document. Knowing only one builder's protocol and then invoking another builder's `.cjs` bypasses that builder's write-side contract (componentNames sync, Values typeKey metadata, write-time auto-lint, placeModel component mirroring, child entity invariants) — and the three are interlocked through cross-flow (model authoring → map placement → ui binding), which is why they share one document.
Triggers (intentionally broad — Read builder-protocol.md whenever any match):
.mapchanges (entity placement, component patching, tile / foothold inspection).modelchanges (new authoring, value / component / property / child edits).uichanges (new build, component CRUD, binding injection)- Any call to
MapBuilder/ModelBuilder/UIBuilder - Requests shaped like "entity-shaped" work — monster / NPC / projectile / map object / popup / HUD, etc.
- Any code using
_SpawnService(a spawnable model must be authored and placed first)
The domain refs (entity.md / model.md / msw-ui-system design references) are read alongside builder-protocol.md — they are not substitutes (domain context + call protocol are a pair). Do not skip on the grounds that you read it in a prior turn — re-read every turn.
---
Model Work Preflight — MUST
If the task involves authoring or editing a .model file in any way — including any call to ModelBuilder (any API), creating a new model from a template, mutating components/values/properties/children/event links on an existing model, or even a one-line tweak — you must `Read` [references/model.md](references/model.md) IN FULL FIRST (no offset/limit, no cat/Get-Content). No exceptions.
The builder's template catalog, the 3-identifier replacement rule (EntryKey / Id / Name), required value metadata, property/child/event-link API surface, and the typed save-folder layout under RootDesk/MyDesk/Models/ live only in `references/model.md`. Calling the builder without reading `references/model.md` first silently produces broken models (missing value metadata, mismatched identifiers, wrong save folder, default-value silent failures). Reading scattered template files or guessing the API from memory is not a substitute — re-confirm at the start of every new turn that touches a .model, even if a previous turn already loaded it.
---
Map Work Preflight (do this BEFORE any map work)
Before starting any map-related task — entity placement, spawn, movement scripts, model authoring, tile edits, etc. — you must complete these two steps in order:
1. Identify the target map — its path (./map/{mapname}.map), its root entity, and its location in the Hierarchy. 2. Use `MapBuilder` to read `MapComponent.TileMapMode` as a number (call protocol: `references/builder-protocol.md`). Keep the value in mind for the rest of the session.
| Value | Mode | Required Body | Runtime log on mismatch / missing Body |
|---|---|---|---|
0 | TileMap (MapleTile, side-view + Foothold) | RigidbodyComponent | [LEA-3004] MissingComponent : Entity is missing 'RigidbodyComponent'. |
1 | RectTileMap (RectTile, top-down) | KinematicbodyComponent | [LEA-3004] MissingComponent : Entity is missing 'KinematicbodyComponent'. |
2 | SideViewRectTileMap (SideViewRectTile, side-view tile) | SideviewbodyComponent | [LEA-3004] MissingComponent : Entity is missing 'SideviewbodyComponent'. |
Never start map work without knowing the current `TileMapMode`. The three modes differ completely in Body component, gravity, collision, and event stacks. A mismatch is almost never a compile-time error — it shows up either as a silent failure (entity doesn't move / passes through walls / invisible) or as one of the three [LEA-3004] MissingComponent runtime logs above. Whenever you see one of those three messages, suspect a TileMapMode ↔ entity Body mismatch first.
Recommending the right mode — MUST when starting a new map or when the current mode is clearly wrong for the user's goal
Whenever the user describes what game / map they want to build (new map authoring, "make a side-scroller", "I want a top-down dungeon", or you read the current .map and find its TileMapMode does not fit the user's intended gameplay), you must explicitly recommend the appropriate `TileMapMode` to the user and explain why before proceeding with any further entity / model / script work.
Use this decision matrix as the source of truth:
| User's intended game / gameplay | Recommend | Why |
|---|---|---|
| MapleStory-style side-scrolling action · jump · ladder · freely placed footholds (platformer) | `0` MapleTile | Side-view + gravity, FootholdComponent line-segment platforms — non-grid, freely placed platforms |
| Top-down RPG · maze · board game · dungeon crawler · Bomberman-style · RTS-style · farming sim | `1` RectTile | Top-down 4-directional free move, no gravity, square-tile grid |
| Tile-based side-scrolling platformer · Mario-style pixel action · side-view puzzle (square-tile side-view) | `2` SideViewRectTile | Side-view + gravity on a tile grid (not freely placed footholds) |
Procedure:
1. If the user has not yet told you what kind of game they want, ask before recommending a mode (one short question is enough — e.g. "Is it top-down, or side-scrolling (jump/ladder)? And is the terrain based on freely placed footholds, or a square tile grid?"). 2. Once the intent is clear, state the recommendation (mode number + name + one-sentence rationale) and the matching Body / map component the user will need (`platform.md` §4 mapping table). 3. Lock the choice in early — switching TileMapMode later wipes terrain, forces every Body / movement script to be re-checked, and may force re-painting all tiles (`platform.md` §4 "Cautions When Switching Map Type").
Changing TileMapMode — user action in Maker, not an AI file edit
The AI must never flip `MapComponent.TileMapMode` by editing the `.map` JSON directly. Mode switching requires swapping tile components, rebuilding footholds, converting tile-data formats, and resetting terrain — all internal Maker operations.
Guide the user to do this in the Maker editor:
1. Open the Maker editor's Hierarchy window. 2. Right-click the target map entity in the Hierarchy. 3. From the context menu, choose the "Switch ..." option that matches the target mode (Switch TileMap / RectTileMap / SideViewRectTileMap). Maker performs the conversion, swaps the tile component, and resets terrain as needed. 4. After the user confirms the switch is complete, call MCP `refresh`, then re-read MapComponent.TileMapMode to verify and re-check every dynamic entity's Body component against the new mode.
The AI's role in mode changes: recommend → wait for user to right-click-switch in Maker Hierarchy → refresh → fix Body components / scripts that no longer match. Never write a new value to TileMapMode from a file edit.The table above is a summary. The mode-switch procedure, the post-Body-swap checklist, and the silent-failure symptom dictionary beyond LEA-3004 live only in `references/platform.md` §4 (mapping + check protocol + switching policy) and `references/troubleshooting.md` (full symptom dictionary) — you must Read them when changing modes / swapping Body / debugging silent failures. Per-map-type detail patterns: `platform-maple.md` / `platform-rect.md` / `platform-sideview.md`. Tile painting: `references/tile.md`. Map Work Preflight: `references/entity.md`.
---
Platform Rules Preflight — MUST when any of these triggers fire
If any of the following triggers matches your task, Read the corresponding reference before editing code or proposing a plan. These triggers are intentionally broad — when in doubt, read. The "8 Core Rules" in this SKILL.md are a summary only; the symptom→cause→fix tables, per-map-type code patterns, the `MovementComponent` conversion formulas, and SortingLayer/SpriteRUID details live only in references.
| Trigger (keyword / situation) | File to read |
|---|---|
jump, gravity, movement, MoveVelocity, InputSpeed, JumpForce, WalkSpeed, SpeedFactor, foothold, patrol | The matching map type's `references/platform-maple.md` / `platform-rect.md` / `platform-sideview.md` (in full) + `references/platform.md` §10 |
spawn / _SpawnService / SpawnByModelId / SpawnByEntity / "summon a monster" / "runtime creation" | `references/platform.md` §8 + §8.5 |
| Screen coordinates / camera range / "is it on screen" / "pixel units" / OrthographicSize / world unit | `references/platform.md` §5 |
| Occluded / invisible / "should render on top" / SortingLayer / OrderInLayer / Z value | `references/platform.md` §6 + §7 |
LEA-3004 in the log / "won't move" / "floating in mid-air" / "stuck in wall" / "bouncing off" / "disappears off the map" / "falls off the foothold edge" | [`references/troubleshooting.md`](references/troubleshooting.md) (in full) first, then the matching map type's platform-{type}.md §7 |
LEA-3005 InvalidArgument 'stateName' / StateComponent / StateType / @State / ChangeState / AddState / AddCondition / ActionSheet / SetActionSheet / StateAnimationComponent / AvatarStateAnimationComponent / StateChangeEvent / "animation doesn't change" / "stays in stand/idle while moving" / "attack pose never plays" / "hit anim loops" / monster·NPC·player animation state work | [`references/animation-state.md`](references/animation-state.md) (in full) first, then `references/monster.md` (or the entity-specific doc) for entity-level composition |
shader / material / outline / glow / blur / pixelate / rainbow / tint / grayscale / vignette / screen filter / lens distortion / wave / ripple / distortion / dissolve / additive / blend mode / hologram / mask / post-process / MaterialID / MaterialId / ChangeMaterial / _MaterialService / .material file | [`references/material.md`](references/material.md) (in full) — then drive shader catalog / property names / per-component compatibility via mlua_Document_Retriever + mlua_API_Retriever MCP lookups (do NOT memorize) |
New map setup / new project / .config / CoreVersion verification / sector registration / folder metadata Refresh | `references/platform.md` §2 + §15 + §16 |
MapleTile (TileMapMode = 0) work — Foothold, Gravity, WalkSpeed, PredictFootholdEnd | `references/platform-maple.md` (in full) |
RectTile (TileMapMode = 1) work — SpeedFactor, 4-directional movement, Movable tiles, dynamic tiles | `references/platform-rect.md` (in full) |
SideViewRectTile (TileMapMode = 2) work — JumpSpeed/JumpDrag, wall detection (Normal), EnableDownJump | `references/platform-sideview.md` (in full) |
If two or more triggers match, read all of them. "I already saw the 8 Core Rules in SKILL.md" is not an excuse for skipping references.
---
8 Core Rules (must memorize)
1. If you don't align TileMapMode ↔ Body mapping, the entity will not move (no error) or raises [LEA-3004] MissingComponent at runtime → `references/platform.md` §4 (or if approaching by symptom, `references/troubleshooting.md`) 2. User scripts only work as a .mlua + .codeblock pair — .codeblock is generated by Maker Refresh 3. If SpriteRUID is an empty string, the entity is invisible on screen (no error) 4. When calling SpawnByModelId, not passing a map entity (self.Entity.CurrentMap) as parent causes a runtime error 5. Coordinates are in world units (1 unit = 100 px). Pixel values are off by 100x 6. Maker only scans RootDesk/ — user files placed in Global/ will not be recognized 7. Do not modify .d.mlua or .codeblock. 8. CoreVersion is 26.5.0.0 — do not work if there is a mismatch
---
MCP Tool Quick Reference (msw-maker-mcp)
| Tool | Purpose |
|---|---|
| play / stop | Enter / exit play mode |
| refresh | Sync Maker after file change (not allowed during play) |
| logs / clear_logs | Read / clear runtime and build logs |
| screenshot | Call only when explicitly requested by the user |
| keyboard_input / mouse_input | Simulate input in play mode |
On MCP call failure / "MCP connection" / "API Key" requests → guide the user to the official setup docs: https://maplestoryworlds-creators.nexon.com/ko/docs?postId=1368
---
Per-task routing — which reference to read
| File | Scope | When to read |
|---|---|---|
| workspace.md | World instance / Room / DataStorage, folder layout, file paths, Play mode, refresh, mid-workflow failure recovery | Workspace / instance / mode-transition work |
| platform.md (core) | 8 core rules, file authority + folder metadata, .mlua+.codeblock pair, TileMapMode↔Body mapping + LEA-3004, coordinate system / on-screen range, SortingLayer/OrderInLayer, SpriteRUID, SpawnByModelId usage / initialization order, MovementComponent per-map-type InputSpeed conversion formula, ECS, ID generation, .config, CoreVersion | TileMapMode mapping / mode switching / spawn / coordinates / RUID / SortingLayer / .config & CoreVersion / folder metadata — rules common to all map types |
| platform-maple.md | MapleTile (TileMapMode = 0) only — Foothold physics, Gravity/WalkSpeed/WalkJump, PredictFootholdEnd, IsOnGround, DownJump, FootholdEnter/LeaveEvent, MapleTile-only troubleshooting + checklist | Side-scrolling action / jump / ladder / freely placed footholds (MapleStory-style platformer) |
| platform-rect.md | RectTile (TileMapMode = 1) only — KinematicbodyComponent, SpeedFactor, free 4-directional movement, visual-only jump, Movable tile collision, ToCellPosition/ToWorldPosition, RectTileEnter/LeaveEvent, dynamic tiles (SetTile/BoxFill), RectTile-only troubleshooting + checklist | Top-down RPG / maze / board game / dungeon crawler / Bomberman-style / RTS / farming sim |
| platform-sideview.md | SideViewRectTile (TileMapMode = 2) only — SideviewbodyComponent, JumpSpeed/JumpDrag, EnableDownJump, wall detection (RectTileCollisionBeginEvent + Normal), GetUnderfootTile, SideView-only troubleshooting + checklist | Tile-based side-scrolling platformer / Mario-style pixel action / side-view puzzle |
| troubleshooting.md | Unified symptom dictionary — LEA-3004 table / "won't move" / "won't render" / "floating in mid-air" / "stuck in wall" / "disappears off the map" / "falls off the foothold edge" / "100× off" / "doesn't show in Maker" / "client-only sync" and other silent-failure symptom→cause→fix unified index | Symptom-first debugging — go here first when the user reports the above or [LEA-3004] appears in the log |
| authoring.md | Shared authoring principles across 5 file types (schema consistency, hand-edit hazards) | Entry point before any file authoring |
| tile.md | Tile painting — Maker UI domain, AI guides only | Tilemap work |
| **builder-protocol.md** | Unified call protocol for `.map` / `.model` / `.ui` — MapBuilder / ModelBuilder / UIBuilder API, snapshot workflow, cross-flow, coverage gaps, `false`-return handling, binding injection | Read every turn that mutates `.map` / `.model` / `.ui` (Builder Protocol Preflight) |
| entity.md | .map entity domain — Scope, RUID, TileMapMode preflight, modelId vs inline decision rule, coordinate / foothold / camera, runtime verification | .map editing / entity placement (read together with builder-protocol.md for the call protocol) |
| model.md | .model authoring domain — when to create, template catalog, component combinations, script-component lifecycle | Writing / editing .model (read together with builder-protocol.md for the call protocol) |
| monster.md | Monster canonical components, lowercase ActionSheet keys, mandatory IsLegacy / SortingLayer overrides, AI choice, HP/respawn, spawn position | Authoring a monster model |
| animation-state.md | StateComponent defaults & auto-registration, state-change pipeline, SetActionSheet vs ChangeState, StateType authoring (server-only, ParentComponent.Entity), StateAnimationComponent (monster/NPC) vs AvatarStateAnimationComponent (player), [LEA-3005] pitfalls | Any state / animation issue across monster, NPC, or player — read first whenever an entity's animation doesn't match its behavior |
| material.md | .material file anatomy, shader category index (10+ categories), applying MaterialID on renderer components via .model / .map / runtime ChangeMaterial, _MaterialService:ChangeMaterialProperty (ClientOnly), and the MCP-driven lookup loop (`mlua_Document_Retriever` / `mlua_API_Retriever`) that replaces memorizing the per-shader catalog | Any shader / material / visual effect work — outline, glow, blur, vignette, rainbow, hologram, blend mode, post-process, hit-flash, screen filter, etc. |
msw-ui-system skill (invoke via the Skill tool) | Single UI entry point. Design judgment (coordinates/anchors/pivot, UIGroup/CanvasGroup, component selection) + component property/method/event API + enum values + layout recipes + mlua runtime patterns (popup/toast/HP/grid/drag) + Runtime UI Caveats + UUID binding + `.ui` CJS UIBuilder invocation protocol (panel/text/sprite/button/slider/scroll/script/group/mask/grid/avatar/touch/skeleton/particle, anchor presets, component add/replace/patch/remove, write-time auto lint) | Any UI-related task / creating or editing .ui files — read FIRST |
msw-ui-system/references/templates files | UI structure pattern templates by complexity (simple popup, minimal HUD, multi-tab, shop/purchase flow) with .ui + .mlua examples and button handler patterns | Adding new UI groups/popups/HUD, structuring button handlers, or choosing a UI layout pattern (read directly after msw-ui-system) |
| dataset.md | UserDataSet / LocaleDataSet runtime, .userdataset + .csv pair, `_LocalizationService` is ClientOnly, serveronly | Datasets / i18n / translation |
---
Absolute Principles (apply to every task)
0. If the task involves an entity, read [references/entity.md](references/entity.md) first. No exceptions. 0-bis. If the user's request mentions any UI element (popup, HUD, button, toast, panel, dialog window, menu, tab, layout, screen, bar/gauge, slot) OR involves writing/editing `.ui` files, BEFORE proposing any plan, options, or questions to the user: 1. Invoke `msw-ui-system` via the `Skill` tool first — the single UI entry point (design judgment, component API, enums, layout recipes, runtime patterns, UUID binding, builder invocation protocol unified in one skill). 2. All `.ui` mutations must go through `msw-ui-system`'s `UIBuilder` — no direct raw JSON editing or grep. Read existing .ui files via UIBuilder's read-side API too. (Call protocol: `references/builder-protocol.md`.) 3. (Optional) If you need UI pattern templates (simple popup, minimal HUD, multi-tab, shop flow), Read/Glob the files under msw-ui-system/references/templates/ directly (`templates.md` + style-N-*/ + `ruid-map.md` + Popupbutton.mlua). No exceptions. 0-ter. If the task will create, modify, rename, or delete ANY `.mlua` file — including new scripts, edits to existing scripts, adding/removing a Component/@Logic/@Event/@State/@BTNode, wiring lifecycle methods (OnBeginPlay/OnUpdate/...), or even small one-line fixes — you MUST `Read` BOTH [`msw-scripting/SKILL.md`](../msw-scripting/SKILL.md) AND [`msw-scripting/references/verify-checklist.md`](../msw-scripting/references/verify-checklist.md) IN FULL FIRST (no offset/limit, no cat/Get-Content). Reading msw-general plus scattered .d.mlua files is not a substitute. This applies even when a previous turn already loaded msw-scripting — re-confirm at the start of the new turn. Trigger phrases are intentionally broad: if there is any chance the turn will touch a .mlua, treat it as triggered. No exceptions, no "I already know this", no shortcut via memory. 0-quater. *If the task involves spawning, movement, jump/gravity, coordinate placement, layer/order debugging, MapleTile/RectTile/SideViewRectTile-specific logic, OR you observe any silent-failure symptom (`[LEA-3004]` log, "won't move", "won't render", "floating in mid-air", "stuck in wall", "disappears off the map", "falls off the foothold edge", "100× off", "doesn't show in Maker", "client-only sync"), you MUST `Read` the matching `references/platform.md / [references/troubleshooting.md](references/troubleshooting.md) IN FULL FIRST** (no offset/limit). **Additionally, if the symptom is animation/state-related ([LEA-3005] log, 'stateName' is not a valid argument, animation doesn't match behavior, stuck in stand/idle clip while moving, attack pose never plays, hit loops, custom state never animates, anything touching StateComponent / StateType / ChangeState / AddState / ActionSheet / SetActionSheet / StateAnimationComponent), you MUST Read [references/animation-state.md](references/animation-state.md) IN FULL FIRST** before any code or model edit. The 8 Core Rules in this SKILL.md are summary only — **the symptom→cause→fix tables, per-map-type code patterns (Foothold patrol / RectTile 4-directional movement / SideView wall detection), the MovementComponent InputSpeed conversion formula, SpriteRUID, SortingLayer/OrderInLayer detail, SpawnByModelId initialization order, folder metadata Refresh policy, and CoreVersion policy live only in references**. **Trigger phrases are intentionally broad**: if there is any chance the turn touches one of these areas, treat as triggered. For the matching Read targets, follow the trigger table in the "Platform Rules Preflight — MUST" section above. No exceptions, no "I already know this from the 8 Core Rules", no shortcut via memory. 1. **Visual polish** — never leave SpriteRUID empty. Use msw-search to find resources. 2. **refresh after content file changes that Maker must ingest** (if in play mode, stop first). Folder-only changes do not need immediate refresh. 3. **Never modify Environment/.d.mlua` — API definitions are read-only. 4. Never create `.codeblock` by hand — Maker `refresh` generates it from `.mlua`. Folder metadata is also generated from real folders during refresh. 5. Do not create new user files in `Global/` — Maker will not recognize them. User files belong under `RootDesk/MyDesk/`. 6. Structured files prefer builders, and the call manual is one file — `.model` / `.ui` are builder-only; `.map` is builder-first. The call protocol for all three builders — `MapBuilder` / `ModelBuilder` / `UIBuilder` — is consolidated in [`references/builder-protocol.md`](references/builder-protocol.md). Read it every turn that mutates `.map` / `.model` / `.ui` (see Builder Protocol Preflight). Direct raw JSON edits are allowed only in the coverage-gap areas explicitly listed in builder-protocol.md — minimal scope + `refresh` + logs verification. 7. Entity reference binding (Entity/EntityRef property) — the AI injects the UUID string directly. Do not ask the user to drag in Maker. 8. Stop work on CoreVersion mismatch — first verify `CoreVersion` in `Environment/config` is `26.5.0.0`. 9. Call `screenshot` only when the user explicitly requests it. Never call it automatically after task completion. 10. If a workflow step fails mid-flow, stop later steps — fix the root cause first. 11. Two-or-more = make a model. Whenever the same entity composition is placed ≥2 times in a map, author a `.model` first and instance it via `modelId`. Inline `@components` duplication is reserved for genuine one-off entities. 12. Models live in typed subfolders. Save new `.model` files under a category subfolder of `RootDesk/MyDesk/Models/` (e.g. `Models/Monsters/`, `Models/NPCs/`, `Models/Terrain/`, `Models/MapObjects/`, `Models/Particles/`, `Models/UI/`) — never directly under `MyDesk/` or `Models/`. When a needed subfolder does not exist, create the folder only; Maker Refresh will generate folder metadata later (see [references/platform.md §2](references/platform.md)). 13. Translation is client-side only. `_LocalizationService` and `Translator` methods (`GetText` / `GetTextFormat`) are all `ClientOnly`. For server-originated localized messages, send the key over RPC and let the client resolve it. 14. Cross-platform tool selection — no shell for workspace exploration, use tools. Use the `Glob` / `Read` / `Grep` tools for all workspace file/folder exploration, reading, and search. `Bash` commands like `ls` / `dir` / `Get-ChildItem` / `gci` / `cat` / `type` / `Get-Content` / `gc` / `head` / `tail` / `find` / `where` / `grep` / `findstr` / `Select-String` are forbidden for workspace exploration — they are not compatible across Windows (PowerShell/Git Bash) and macOS (bash/zsh), due to shell/path-handling differences (notably, in bash a path like `D:\path\foo` has its backslashes consumed as escapes and collapses to `D:pathfoo`). Use `Bash` only for actual shell programs (`git` / `npm` / MCP / build scripts), and even then: (a) prefer workspace-relative paths, (b) if an absolute path is unavoidable, use forward slashes + double quotes (`"D:/path/to/map/"`, never pass `D:\...` form), (c) use POSIX commands only (`ls` / `mv` / `cp` / `rm`). If you see an error like `ls: cannot access 'D:path...': No such file or directory`, stop immediately and retry via `Glob` / `Read`. 15. `.model` files are builder-only — and the builder requires both [`references/model.md`](references/model.md) (domain) and [`references/builder-protocol.md`](references/builder-protocol.md) (call protocol) first. Do not inspect or edit `.model` JSON directly. Before using `ModelBuilder`, `Read` both documents IN FULL (no `offset` / `limit`) — both "Model Work Preflight" and "Builder Protocol Preflight" fire. Identifier / value-metadata / property / child / event-link consistency is only guaranteed when both documents are read together. 16. `.map` files are builder-first. Use `MapBuilder` for covered inspection and mutation so entity ids, paths, component names, origin metadata, and model instance mirrors stay consistent. If `MapBuilder` explicitly does not cover the required operation, make the smallest direct `.map` edit possible, then `refresh` and verify. Full API / require path / per-operation patterns / coverage gaps / `false`-return handling / cross-flow: [`references/builder-protocol.md`](references/builder-protocol.md) §1 + §4* (read alongside `references/entity.md` for domain context).
Animation State — StateComponent & State-Driven Animation
Cross-entity reference for the state → animation pipeline shared by monsters, NPCs, and players. Read this first whenever you see state-change or animation-swap bugs; entity-specific docs (monster.md, future npc.md etc.) build on top of these rules.
The pipeline is the same for every entity:
ChangeState(newState)
→ StateComponent updates CurrentStateName
→ fires StateChangeEvent
→ animation component reads its ActionSheet at this moment
→ raises AnimationClipEvent on the rendererThe renderer does not re-poll ActionSheet between transitions. The currently playing clip is whatever the last StateChangeEvent resolved to.
0. Two animation patterns — pick one before reading further
Monsters/NPCs drive their visible clip one of two ways. They are mutually exclusive; the rest of this doc and monster.md assume you've picked.
Pattern A — script-driven SpriteRUID assignment (proven working canonical)
The verified working sample (Soldier.model + script.SoldierAI, full source inlined in `monster.md` §7) takes this route. A custom Component holds its own state variable (e.g. CurrentAIState ∈ {"ROAM","STAND","SAY","ATTACK"}) and on each transition does self.Entity.SpriteRendererComponent.SpriteRUID = <clipRUID> directly. StateComponent is used only for IDLE ↔ DEAD transitions (so script.Monster.IsDead syncs and DeadEvent fires correctly). StateAnimationComponent + ActionSheet are still present in the .model for completeness, but the pipeline is bypassed.
In this pattern `StateComponent.IsLegacy` does not need to be set — Soldier.model leaves it at the default and animations work fine, because the script never relies on StateChangeEvent → ActionSheet → AnimationClipEvent.
Prefer Pattern A when:
- Behavior doesn't map cleanly to AIChase/AIWander (e.g. roam ↔ stand ↔ say ↔ attack with random idle picks, range-gated attack triggering).
- You want predictable clip swaps without depending on the
IsLegacyquirk below.
Pattern B — StateChangeEvent → ActionSheet pipeline (auto-swap)
StateComponent:ChangeState("MOVE") fires StateChangeEvent; StateAnimationComponent looks up ActionSheet[StateStringToAnimationKey(state)] and raises AnimationClipEvent. For this pipeline to actually swap clips, `StateComponent.IsLegacy = false` must be set on the `.model` — without it the events still fire but StateAnimationComponent ignores them (legacy mode pre-dates this pipeline). IsLegacy is a hidden property (not exposed via mlua_api_retriever) serialized in the .model JSON; default is `true` (legacy).
MonsterCanonical.model in ../models/ is configured for Pattern B (sets StateComponent.IsLegacy=false, includes AIChaseComponent).
Builder call (only relevant if you choose Pattern B):
b.value("StateComponent", "IsLegacy", false, "bool");When you see "nothing animates, no errors" with this pattern, this missing flag is the first thing to verify.
Symptoms of mixing the two patterns: AIChase forces velocity to zero outside its detection range while your custom script also tries to drive movement (see `monster.md` §5d "Do not use AIChase/AIWander together with a custom chase/movement script"); or you callChangeState("MOVE")but the clip never changes becauseIsLegacyis at its defaulttrue.
0.5. FSM (this doc) vs BT — picking the state-machine engine
This document covers the FSM path (StateComponent + @State StateType). MSW also natively supports a Behaviour Tree path (AIComponent + @BTNode) — see `../../msw-combat-system/references/ai-bt.md`.
| Engine | Fit |
|---|---|
FSM (StateComponent) | Simple enemies (3~5 states), player IDLE/HIT/DEAD, boss phase branching, automatic avatar animation sync (AvatarStateAnimationComponent). The pipeline this whole doc describes. |
BT (AIComponent) | Patrol + chase + attack composition, varied boss patterns, probability-weighted actions, Composite/Decorator reuse |
Both are native. If you want to drive behavior with states and bind those states to motion, prefer FSM. Use BT when the decision tree is deep or node reuse is essential. BT-driven entities still rely on StateComponent for animation sync — BT nodes call ChangeState to drive the same pipeline this doc describes.
This axis is orthogonal to §0's Pattern A/B. A custom-script monster (Pattern A — Soldier) can hold its own FSM-like state without going throughStateComponenttransitions at all. AStateComponent-driven monster (Pattern B) can be powered by either FSM (this doc) or BT (ai-bt.md).
1. Default states and who registers the rest
1.1. StateComponent API surface
@Component StateComponent
readonly @Sync property string CurrentStateName = "IDLE"
method boolean AddState(string stateName, Type stateType)
method boolean AddCondition(string stateName, string nextStateName, boolean reverseResult = false)
method boolean ChangeState(string stateName)
method void RemoveState(string name)
method void RemoveCondition(string stateName, string nextStateName)StateChangeEvent payload: CurrentStateName, PrevStateName, IsInitial. DeadEvent / ReviveEvent / StateChangeEvent are all auto-emitted by the engine — you only need to subscribe (see §5 for chaining). Failure modes of ChangeState (lowercase, unregistered, DEAD lock, wrong ExecSpace) are tabulated in §4.5.
1.2. Default registration
StateComponent ships with only `IDLE` and `DEAD`. Every other state name (MOVE, ATTACK, HIT, JUMP, …) only exists on an entity if a companion component registered it, or you called AddState yourself. Calling ChangeState("MOVE") on an entity that never had MOVE registered throws [LEA-3005] InvalidArgument : 'stateName' is not a valid argument.
| State | Registered by |
|---|---|
IDLE, DEAD | StateComponent itself (always) |
HIT | HitComponent (raised by HitEvent, auto-exits to IDLE ~0.5s) |
MOVE | AIChaseComponent, AIWanderComponent, or PlayerControllerComponent |
ATTACK, ATTACK_WAIT, JUMP, FALL, CLIMB, LADDER, CROUCH, SIT | PlayerControllerComponent only (players) |
Anything else (including ATTACK on monsters) | You — via AddState(name, StateType) server-side |
⚠ Monsters do not auto-register `ATTACK`.script.MonsterAttackdoes not register anATTACKstate — it just timer-attacks (AttackFast) while alive, irrespective ofStateComponent. The two working canonicals both ship with anattackkey in theirActionSheet(Pattern A — Soldier.model hasattackand the SoldierAI uses it as the directSpriteRUIDduring its self-managedATTACKstate; Pattern B — MonsterCanonical.model hasattackreserved for a future customATTACKstate), but the `attack` key alone does not get played by `script.MonsterAttack`. To actually swap to the attack clip you must either (Pattern A) setSpriteRendererComponent.SpriteRUIDdirectly in your script'sATTACKbranch, or (Pattern B)AddState("ATTACK", SomeStateType)yourself and callChangeState("ATTACK")so the ActionSheet pipeline picks theattackclip.
If you write a custom controller that strips AI/MonsterAttack/PlayerController, you must register the states you intend to enter:
-- Reusable no-op state for marker transitions whose only job is to drive ActionSheet.
@State
script MarkerState extends StateType
end@ExecSpace("ServerOnly")
method void OnBeginPlay()
local sc = self.Entity.StateComponent
sc:AddState("MOVE", MarkerState)
sc:AddState("ATTACK", MarkerState)
-- IDLE / DEAD always exist; HIT is registered by HitComponent if present.
end2. State name casing
State names are UPPERCASE at the StateComponent layer (IDLE/MOVE/ATTACK/HIT/DEAD). The engine uppercases names passed to AddState/ChangeState, but always pass uppercase yourself — mixed-case is a hidden source of "transition doesn't fire" bugs.
The animation lookup key is component-specific (see §6).
3. The SetActionSheet vs ChangeState trap
The single most common animation bug. SetActionSheet(key, ruid) only edits the mapping table. It does not retrigger the clip currently playing.
| Goal | Correct call | Wrong (silent failure) |
|---|---|---|
| Play the walk clip now | StateComponent:ChangeState("MOVE") | SetActionSheet("stand", moveRuid) while in IDLE — mapping updates but stand keeps playing |
| Play the attack clip now | StateComponent:ChangeState("ATTACK") | Same anti-pattern |
Randomize next HIT clip | Override StateStringToAnimationKey(stateName) and call SetActionSheet("hit", randomRuid) inside it before returning __base:StateStringToAnimationKey(stateName) | SetActionSheet("hit", randomRuid) from a timer — only affects the next HIT, not the one already playing |
| Permanently swap a clip | SetActionSheet(key, newRuid) once, then ChangeState to that state when you want it | — |
Rule: `ChangeState` plays animations; `SetActionSheet` only changes which RUID a future state transition will resolve to.
Reference implementation (random hit pick) lives in the engine API docs for StateAnimationComponent — fetch via mlua_api_retriever.
4. StateType authoring
Custom states (anything beyond the auto-registered set in §1) are written as StateType scripts.
1. Both @State and extends StateType are required. Without either, the script's .codeblock isn't generated and AddState silently receives a nil type. 2. Available hooks: OnEnter() / OnUpdate() / OnExit() / OnConditionCheck(string nextStateName) → boolean. 3. ⚠ OnUpdate on a StateType receives no `delta` (unlike Component/Logic OnUpdate(number delta)). Track elapsed time via _TimerService:GetTime() deltas or a one-shot timer in OnEnter. 4. ⚠ Inside hooks, the owning entity is reached via `self.ParentComponent.Entity` — self.Entity is nil because StateType is not a Component. Use this path to reach TransformComponent, SpriteRendererComponent, MovementComponent, etc. 5. Execution space — `AddState` / `AddCondition` / `ChangeState` are authority-restricted:
- These calls only succeed on the side that owns state-machine authority for the entity. Calling from the other side throws
[LEA-3022] InvalidExecSpace : The addition and removal of states and conditions are disabled in execution spaces where you have no permission.— you cannot register on both sides. - Monsters / NPCs: authority is on the server. Use
@ExecSpace("ServerOnly")(or run inside[server only]code). - Player avatar: authority is on the client (each player owns their own avatar). The official avatar custom-state example uses
[client only] OnBeginPlay()withAddState/ChangeStatefor exactly this reason. - Animation on the client still works even though monster
AddStateis server-only:StateChangeEventis declaredSpace: Server, Client(see the API doc), so a server-sideChangeStatecauses the event to fire on the client too, and the client'sStateAnimationComponent.ReceiveStateChangeEventreads the sync'dActionSheetto play the matching clip. The client does not need a separateAddStateregistration. - Guard
StateChangeEventhandlers that branch into custom states withif self:IsServer()when the branching logic is server-authoritative — but a plain animation-watching handler can readCurrentStateNameon either side.
6. Wire transitions with :AddCondition(from, to, reverseResult = false). Every frame the engine calls the from state's OnConditionCheck(to) with each candidate; transitions when it returns true (or false if reverseResult). 7. Map an ActionSheet action key for the new state — otherwise the state changes but the animation doesn't (see §6 for which ActionSheet).
@State
script WindupState extends StateType
property boolean done = false
method void OnEnter()
self.done = false
-- Tint sprite during wind-up using ParentComponent.Entity
-- self.ParentComponent.Entity.SpriteRendererComponent.Color = Color(1, 0.6, 0.6, 1)
_TimerService:SetTimerOnce(function() self.done = true end, 0.4)
end
method boolean OnConditionCheck(string nextStateName)
return self.done
end
end@Component
script WindupSetup extends Component
-- Monster authority is on the server. AddState/AddCondition/ChangeState must
-- run server-only — calling from client throws [LEA-3022] InvalidExecSpace.
-- The client's StateAnimationComponent still plays the matching ActionSheet
-- clip via the sync'd StateChangeEvent; no client-side AddState required.
@ExecSpace("ServerOnly")
method void OnBeginPlay()
local s = self.Entity.StateComponent
s:AddState("WINDUP", WindupState)
s:AddCondition("WINDUP", "ATTACK")
end
endFor the player avatar, the same calls are client-only instead — each player owns their avatar's state on their own client (see the official "Easy Control of Avatar Animation with ActionSheet" example).
Deprecated `StateComponent` overloads to watch out for in legacy code:
-AddState(string stateName, func updateFunction)— replaced byAddState(string, Type). Old samples callingAddState("NEW_ATTACK")(single-arg form) still work for pure marker states but go through the deprecated path; prefer aMarkerState extends StateTypefor new code.
-AddCondition(string, string, func, boolean)— replaced byAddCondition(string, string, boolean)+OnConditionCheckon theStateType.
>
Cleanup methods (mirrors of the above) exist onStateComponent/StateAnimationComponent:RemoveState(name),RemoveCondition(from, to),RemoveActionSheet(key)— use these when tearing down dynamically registered states/clips.
4.5. ChangeState semantics — return value / failure matrix
| Call | Result |
|---|---|
| Unregistered name | [LEA-3005] InvalidArgument : 'stateName' + returns false |
| Lowercase name | [LEA-3005] + UPPERCASE warning + returns false |
| Same as current state | Returns false (no transition, OnEnter is not re-called) |
CurrentStateName == "DEAD" and target is not "IDLE" | InvalidOperation + returns false — the DEAD lock |
| Wrong ExecSpace (monster/NPC called on client, or avatar called on server) | [LEA-3022] InvalidExecSpace + returns false (see §4 point 5) |
| Normal | OnExit(prev) → OnEnter(new) → EmitStateChangeEvent in that order. Returns true |
DEAD lock
When CurrentStateName == "DEAD", no transition to any state other than `IDLE` is allowed. The revive flow must go through ChangeState("IDLE"). A direct DEAD → REVIVE jump in a boss revive sequence silently fails — re-enter IDLE first, then chain to the revive state from StateChangeEvent (§5).
Auto transitions — AddCondition + OnConditionCheck
AddCondition(from, to, reverseResult = false) registers a per-frame check. Each frame the engine calls from state's OnConditionCheck(to) with each candidate to name and transitions when it returns true (or false when reverseResult = true). Multiple tos can be registered for the same from; they're checked in registration order and the first one passing wins — branch on nextStateName inside the check.
@State
script Phase1StateType extends StateType
method boolean OnConditionCheck(string nextStateName)
local monster = self.ParentComponent.Entity.MonsterScript
return monster.Hp <= monster.MaxHp * 0.5
end
end
-- Registration (server-side OnBeginPlay)
sc:AddState("PHASE1", Phase1StateType)
sc:AddState("PHASE2", Phase2StateType)
sc:AddCondition("PHASE1", "PHASE2") -- Auto-transitions when HP drops below halfFor a forced transition from script, just call sc:ChangeState("PHASE2") directly.
If thefromstate is the deprecatedAddState(name, func)form,AddConditionis silently rejected — migrate toAddState(name, XxxStateType)first.
5. Chaining states via StateChangeEvent
The canonical way to slot a custom state into the built-in flow (e.g. HIT → WINDUP → ATTACK) is to handle StateChangeEvent and force the next transition. Guard server-only because the custom state doesn't exist client-side:
@ExecSpace("ServerOnly")
@EventSender("Self")
handler HandleStateChangeEvent(StateChangeEvent event)
if event.CurrentStateName == "HIT" then
self.Entity.StateComponent:ChangeState("WINDUP")
end
endStateChangeEvent carries PrevStateName and CurrentStateName and is Space: Server, Client. DeadEvent (Space: Server, Client) fires additionally when entering DEAD. ReviveEvent (Space: Server only) fires when PlayerComponent:Respawn() is called on an entity that has both StateComponent and PlayerComponent — players use this to re-enter IDLE; monsters don't have it, so respawn logic for monsters re-enters IDLE manually (see script.Monster.Respawn in monster.md §6).
Use the modern handler syntax with @ExecSpace/@EventSender attributes (see canonical script.Monster's HandleHitEvent) — not the legacy inline [self] Handle… block.
6. Animation component differences
The state machine is shared. The animation component that reads it differs by entity type.
| Entity | Component | Lookup table | IsLegacy |
|---|---|---|---|
| Monster, NPC | StateAnimationComponent | ActionSheet (SyncDictionary<string,string>) — map state-derived key (lowercase) → AnimationClip RUID | StateAnimationComponent.IsLegacy is not exposed in builder; both canonicals (Soldier.model, MonsterCanonical.model) leave it unset. The load-bearing flag is StateComponent.IsLegacy (Pattern B requires false; Pattern A leaves the default — see §0). For Pattern A, ActionSheet is not consulted at runtime — clip swaps come from direct SpriteRendererComponent.SpriteRUID assignment in the script. |
| Player (avatar) | AvatarStateAnimationComponent (extends StateAnimationComponent) | StateToAvatarBodyActionSheet (SyncDictionary<string,string>) — map state name (UPPERCASE) → MapleAvatarBodyActionState value (e.g. walk, attack, rope, ladder, …) | Must be `false` for StateToAvatarBodyActionSheet to drive animation. IsLegacy is ReadOnly at runtime — set it on the .model JSON (builder side); the engine docs note the legacy ActionSheet path "is no longer supported and will be deleted at a later date". |
StateAnimationComponent itself is "monster/NPC only" per the engine docs — don't attach it to a player; use AvatarStateAnimationComponent.6a. Monster/NPC key conversion (StateAnimationComponent)
State → action key happens via StateStringToAnimationKey(stateName). Default behavior lowercases & maps:
| State | Default action key |
|---|---|
IDLE | stand |
MOVE | move |
JUMP | jump |
ATTACK | attack |
HIT | hit |
DEAD | die |
Custom states fall through to string.lower(stateName) unless you override StateStringToAnimationKey. Missing or typoed keys fail silently — the previous clip keeps playing.
To remap (e.g. ATTACK → attack2, random hit): either override StateStringToAnimationKey (preferred — keeps the swap synchronous with the transition) or call SetActionSheet(key, clipRuid) from script. Never edit the .model JSON.
6b. Player key conversion (AvatarStateAnimationComponent)
State → animation lookup uses the uppercase state name directly as the key into StateToAvatarBodyActionSheet. Default mapping (do not delete entries — missing keys break their animations):
| Key (state) | Value (MapleAvatarBodyActionState) | PlayRate |
|---|---|---|
IDLE | stand | 1.0 |
MOVE | walk | 1.68 |
ATTACK | attack | 1.33 |
HIT | hit | 1.0 |
CROUCH | crouch | 1.0 |
FALL | fall | 1.0 |
JUMP | fall | 1.0 |
CLIMB | rope | 1.0 |
LADDER | ladder | 1.0 |
DEAD | dead | 1.0 |
SIT | sit | 1.0 |
Avatars don't use animationclip RUIDs here — they use the predefined MapleAvatarBodyActionState strings (stand/walk/attack/alert/crouch/fall/sit/rope/ladder/dead/blink/fly/heal/hit; full enum has 14 non-Invalid members, see MapleAvatarBodyActionState via mlua_api_retriever). SetActionSheet(key, ruid) on the avatar variant inserts an AvatarBodyActionElement with AvatarBodyActionStateName = ruid and default PlayRate = 1 — adjust PlayRate by editing the element directly (or by editing the model's StateToAvatarBodyActionSheet entries) rather than re-setting via SetActionSheet, which resets it back to 1.
Player models also bring PlayerControllerComponent which auto-registers MOVE/ATTACK/CLIMB/LADDER/CROUCH/JUMP/FALL/SIT/ATTACK_WAIT and drives them from input. Removing PlayerControllerComponent breaks the default player flow. Two PlayerControllerComponent flags worth knowing:
AlwaysMovingState(Sync, bool) — whentrue, the walk animation plays unconditionally regardless of input/movement. Useful for cutscene/forced-idle-walk states.UseCustomScript(ReadOnly, bool) — whentrue, some PlayerControllerComponent features are disabled so your own scripts can take over input → state mapping. Set via the.modelfor fully custom avatar control.
7. Pitfall table
| Symptom | Root cause | Fix |
|---|---|---|
Nothing animates at all — sprite stuck on initial stand, neither MOVE/move nor DEAD/die clips switch, no error logs. State machine logs (CurrentStateName print) show transitions are happening. | You picked Pattern B (ActionSheet pipeline) but StateComponent.IsLegacy is missing/true on the .model. Legacy mode silently skips the StateChangeEvent → ActionSheet → AnimationClipEvent pipeline (see §0). | Either set StateComponent.IsLegacy = false on the .model, or switch to Pattern A and drive SpriteRUID directly from a script (canonical: script.SoldierAI). |
[LEA-3022] InvalidExecSpace : The addition and removal of states and conditions are disabled in execution spaces where you have no permission. | AddState / AddCondition / ChangeState called on the side that doesn't own state-machine authority (see §4). | For monster/NPC: wrap in @ExecSpace("ServerOnly"). For player avatar: use [client only]. Never call on both sides — one side will always throw. |
Custom-state animation never plays even though server logs show ChangeState("MY_STATE") succeeded | Pattern B: StateComponent.IsLegacy missing/true on the model (see §0). The pipeline is silently disabled in legacy mode regardless of whether the state is custom or built-in. | Set StateComponent.IsLegacy = false on the .model, or switch to Pattern A and call sprite.SpriteRUID = <ruid> from your StateType:OnEnter / handler. Do not mirror AddState on the client side — that throws [LEA-3022]. |
[LEA-3005] InvalidArgument : 'stateName' is not a valid argument | Target state not registered on this entity (see §1) | Add the companion component that registers it, or AddState(name, MarkerState) first (on the side calling ChangeState) |
Entity moves but stand clip keeps playing | MovementComponent was driven without ChangeState("MOVE") — and no AI component to auto-toggle | Add AIChase/AIWander, or call ChangeState("MOVE") / ChangeState("IDLE") from your controller |
SetActionSheet(...) did nothing visible | SetActionSheet only edits the mapping; doesn't retrigger the current clip | Call ChangeState to a state that resolves to the new mapping (§3) |
| State changes but animation doesn't | Action key missing from ActionSheet, or wrong-cased key for monster/NPC (must be lowercase) | Fix the key; verify with mlua_api_retriever for the component |
| Avatar custom state animations broken | AvatarStateAnimationComponent.IsLegacy flipped to true | Set back to false (canonical default) |
ChangeState("MY_STATE") does nothing on client | Custom states exist server-side only | Run from [server only]; guard event handlers with if self:IsServer() |
self.Entity is nil inside StateType | StateType is not a Component | Use self.ParentComponent.Entity |
| HIT state loops forever | Treating HIT as a sticky state | Don't — HitComponent auto-returns to IDLE ~0.5s after entering HIT |
| State change races animation (avatar) | Default state-change condition fired before desired clip showed | If you need exact frames, use the BodyActionStateChangeEvent / ActionStateChangedEvent path instead of ActionSheet |
8. Verification
Don't ask "did it appear" — walk the state cycle:
1. Spawn → CurrentStateName == "IDLE", idle clip plays. 2. Move begins → CurrentStateName == "MOVE", move clip plays. 3. Hit → CurrentStateName == "HIT", hit clip + damage skin → auto-return IDLE ~0.5s. 4. HP=0 (or ChangeState("DEAD")) → CurrentStateName == "DEAD", die clip, DeadEvent fires, IsAttackTarget rejects further hits. 5. Each custom state you registered: confirm ChangeState succeeds (no [LEA-3005]), CurrentStateName updates, and the mapped clip plays.
If a clip looks stuck, log CurrentStateName per frame from a server script — distinguishes "state didn't change" from "ActionSheet key wrong".
Pre-flight checklist
- [ ] Custom StateType scripts include both
@Stateandextends StateType - [ ] After
refresh, a.codeblockwith the same name exists next to the custom StateType.mlua— if missing, the annotation orextendsis missing - [ ] All state names are UPPERCASE at the StateComponent layer (
"ATTACK"✓,"attack"✗) - [ ] Before calling
ChangeState,AddStatewas called with that name inOnBeginPlay(or registered by a companion component per §1.2) - [ ] FSM operation methods use
@ExecSpace("ServerOnly")for monster/NPC, `[client only]` for player avatar (§4 point 5) — never both sides - [ ] No direct jumps from
DEADto states other thanIDLE(§4.5 DEAD lock) - [ ] Keys in
AvatarStateAnimationComponent.StateToAvatarBodyActionSheetexactly match the state names registered inStateComponent(UPPERCASE for avatar; lowercase-converted for monster/NPC ActionSheet, see §6a) - [ ] `StateType.OnUpdate()` declared without `delta` (the engine does not pass one; declaring
OnUpdate(number delta)silently setsdelta = nil) - [ ] `StateType.OnConditionCheck(string nextStateName)` declared with the argument (the engine passes the candidate
toname; omitting it disablesnextStateName-based branching) - [ ] When using auto transitions,
OnConditionCheckis a cheap check even though it runs every frame (heavy work belongs inOnUpdate) - [ ]
DisconnectEventexternal event handlers inOnEndPlay(if the StateType subscribed to events) - [ ] Pattern B:
StateComponent.IsLegacy = falseon the.model(§0). Default istrue(legacy) and silently disables the ActionSheet pipeline.
9. Reference — standard monster IDLE/PATROL/CHASE/ATTACK/HIT/DEAD FSM
A complete custom-FSM monster (Pattern B — uses ChangeState to drive StateAnimationComponent's ActionSheet). IDLE/DEAD always auto-exist; HIT is auto-registered by HitComponent if attached. The rest is server-side.
@Component
script MonsterFSM extends Component
@ExecSpace("ServerOnly")
method void OnBeginPlay()
local sc = self.Entity.StateComponent
sc:AddState("PATROL", PatrolStateType)
sc:AddState("CHASE", ChaseStateType)
sc:AddState("ATTACK", AttackStateType)
-- IDLE / DEAD always exist; HIT exists if HitComponent is attached.
-- Auto transitions — each StateType's OnConditionCheck inspects the trigger
sc:AddCondition("PATROL", "CHASE") -- player detected
sc:AddCondition("CHASE", "ATTACK") -- entered attack range
sc:AddCondition("ATTACK", "CHASE") -- cooldown ended
sc:AddCondition("CHASE", "PATROL") -- target lost (branch via nextStateName or reverseResult)
sc:ChangeState("PATROL")
end
endEach StateType's OnConditionCheck inspects distance / cooldown / target validity; OnUpdate handles in-state behavior. A clean separation:
@State
script AttackStateType extends StateType
property number Duration = 0.6
property number StartTime = 0
method void OnEnter()
self.StartTime = _TimerService:GetTime()
local entity = self.ParentComponent.Entity
-- Fire the attack resolution once on enter
entity.AttackComponent:AttackFast(BoxShape2D(1, 1), "monster_attack", CollisionGroups.Player)
end
method void OnUpdate()
if _TimerService:GetTime() - self.StartTime >= self.Duration then
self.ParentComponent:ChangeState("IDLE")
end
end
method void OnExit()
-- Cleanup (remove effects, etc.)
end
endThe same skeleton scales up to boss phase branching (PHASE1/PHASE2withOnConditionCheckreturningHp <= MaxHp * 0.5, see §4.5 example) and down to a 3-state minion (IDLE↔CHASE↔ATTACK). Match theActionSheetkeys (lowercase per §6a) to the state names you register here.
10. Cross-references
| Doc | Why |
|---|---|
| monster.md | Monster-specific composition, AI choices, HP/respawn, spawn, placement |
| `../../msw-combat-system/references/ai-bt.md` | The BT alternative to FSM (see §0.5) — AIComponent + Composite/Decorator + @BTNode |
mlua_api_retriever MCP | API for StateComponent, StateType, StateAnimationComponent, AvatarStateAnimationComponent, HitComponent, MovementComponent |
mlua_document_retriever MCP | Concept docs: "Controlling Entity Status", "Easy Control of Avatar Animation with ActionSheet", "Controlling Avatar Animations", "Setting and Controlling Player" |
msw-scripting skill | Authoring StateType/Component/event-handler .mlua scripts |
MSW File Authoring
Authoring guide for .map / .model / .ui / .dataset files and tile map assets in MSW world creation. Each file type has its own reference file — read only the topics you need.
⚠️ Schema consistency warning (important)
.map/.model/.ui/.tileset/.userdataset/.localedatasetare all large files with strict formats. Writing or modifying their JSON by hand easily produces silent failures like:
- Missing model value metadata, duplicate UUIDs, broken component/value consistency
- Mismatched tile map 2D array dimensions,TileMapMode↔ Body mismatch,tileIndexoffset
-.uianchor / pivot coordinate errors, missing RUID, parent-childpathmismatch
- Dataset row / column schema violations
>
Use the dedicated builder or skill for each file type before editing. The call protocol for `.map` / `.model` / `.ui` is consolidated into one entry point — [`builder-protocol.md`](builder-protocol.md). Read it every turn before any mutation..modelfiles are builder-only..mapfiles are builder-first: see builder-protocol.md §1 (read alongside `entity.md` for domain context) and use the builder for covered operations; direct.mapJSON edits are reserved for the explicit coverage gaps in §1 and must be minimal scope plus verified byrefresh/ logs.
>
Entity reference binding (Entity/EntityRef property) is injected by the AI as a UUID string directly — do not ask the user to drag in the Maker editor. Detail: msw-scripting §7 Entity/Component reference properties.---
Per-target Routing
| Task | File to read |
|---|---|
Edit tile map / tile set in .map (TileMapMode, tileMap array, .tileset) | tile.md |
Create or modify a .model template | model.md — builder-only |
Place entities in .map, spawn, parent-child, manage runtime components | entity.md |
.map / .model / .ui builder call protocol (unified) | builder-protocol.md |
.ui authoring, component API, enums, mlua runtime patterns | `msw-ui-system` skill (single UI entry point — design guide + component API + builder invocation + runtime patterns) |
.userdataset / .localedataset structure, types, runtime API | dataset.md |
Template catalog when creating a new .model | model.md §2.1 → ../models/*.model |
Authoring a monster (canonical components, ActionSheet, HitComponent, IsLegacy) | monster.md → ../models/MonsterCanonical.model |
Keyword → File Map
- tile, tile map, tileset, TileMapMode, RectTile, MapleTile, SideViewRectTile, tileIndex → `tile.md`
- model, .model, template, NPC model, player, Foothold, Ladder, Rope, Portal, MapObject, particle, Sound, UIButton, Values, Children, BaseModelId → `model.md` (+ builder +
../models/catalog) - create monster, monster ActionSheet, stand/move/attack/hit/die/jump, HitComponent, IsLegacy, CollisionGroup, AIChase, AIWander, script.Monster, script.MonsterAttack → `monster.md` (+
../models/MonsterCanonical.model) - entity, .map, placement, spawn, SpawnService, CurrentMap, componentNames, modelId reference, hierarchy, Foothold → `entity.md`
- UI, button, text, image, canvas, UITransform, anchoredPosition, AlignmentOption, UIGroup, DefaultShow, GridView, popup, anchor →
msw-ui-systemskill (design, component API, and builder integrated) - dataset, UserDataSet, LocaleDataSet, translation, table, .userdataset, .localedataset, DataService → `dataset.md`
---
Shared Principles (across all 5 file types)
Absolute Principles
1. Prefer the dedicated skill or builder — .model uses ModelBuilder; .map uses MapBuilder; .ui uses msw-ui-system; other files use their relevant reference/tooling. 2. Inject entity references as UUID strings directly — do not ask the user to drag in Maker. 3. MCP `refresh` after every file change (if in play mode, stop first). 4. *Never modify `Environment/.d.mlua** — API definitions are read-only. 5. **Never create or modify .codeblock by hand** — Maker refresh generates it from .mlua. 6. **Structured files prefer builders** — .model / .ui are builder-only. .map uses MapBuilder first; direct JSON edits are allowed only for unsupported gaps, with minimal scope and verification. 7. **Do not touch Global/common.gamelogic and the common entity** — these are special engine-managed entries. Do not edit the file's JSON directly, and do not attach components to the common entity (including via Maker AddComponent or runtime AddComponent). For global logic, **author a regular Logic script under RootDesk/MyDesk/`** and wire up an entry point.
UUID / ID Rules
- `.model` identifiers are managed by `ModelBuilder`. Use
fromTemplate()/renameModel()instead of editingEntryKeyor internal IDs directly. - Entity `id` in `.map` is managed by `MapBuilder`, kept consistent with path and component metadata.
- The id portion of `EntryKey` should be lowercase (e.g.,
model://mymonster,userdataset://itemtable). - When duplicating a file, always generate a new UUID with a cross-platform command:
node -e "console.log(require('node:crypto').randomUUID())".
RUID Rules
- Resources are identified by an RUID string. If
SpriteRUIDis empty, the entity is invisible on screen (no error). - In
.model, set RUIDs throughModelBuilder.value().SpriteRUIDis a plain string. - Use
msw-searchand_ResourceServicefor asset search. Replace temporary placeholders with real assets before deployment.
Representation Consistency
.modelvalue descriptors are generated byModelBuilder.value(). Pass an explicittypeKeyfor new or changed values..mapcomponent values use a different representation; useMapBuilderfor map edits.
TileMapMode ↔ Body ↔ Entity
- The map root's
MapComponent.TileMapMode(0/1/2) determines the entire movement / gravity / collision / tile system. - If an entity's Body-family component does not match the map, it does not move (no error).
- Mapping table and check protocol: platform.md §4.
Save Locations
- New user models go under `RootDesk/MyDesk/` (with a
Models/subfolder; folder metadata comes from Refresh). - Adding new `.model` files arbitrarily under `Global/` may cause Maker to not recognize them.
- Maps:
./map/, UI:./ui/, datasets: underRootDesk/MyDesk/.
Validation Loop
- `refresh` → `logs` → if needed, `play` → `logs` → `stop`.
- If a step fails, stop later steps — fix the cause and retry.
---
Per File Type Summary
.map tile map — tile.md
The 3 TileMapMode values (MapleTile/RectTile/SideViewRectTile) completely change the tile map component (TileMapComponent vs RectTileMapComponent), the array key (Tiles vs tileMap), and the TileSetRUID form (DataId object vs tileset:// string). Do not confuse tile coordinates (grid cells) with entity coordinates (world units).
.model template — model.md
The blueprint for an entity. Pick the closest template from the `../models/` catalog (validated starting points for monsters/NPCs/players/terrain/UI/particles/sound/tile maps, etc.), load it with ModelBuilder.fromTemplate(), and customize it with builder methods. Spawn at runtime via SpawnByModelId, or place in .map by modelId.
.map entity placement — entity.md
Add entity instances under .map's ContentProto.Entities. Use the modelId form (template reference + minimal override) or the inline form (@components listed in full). id/path/componentNames/jsonString.path consistency is mandatory. Runtime spawn uses self.Entity.CurrentMap as the parent.
.ui — `msw-ui-system`
Based on FHD 1920x1080 with the origin at center. Place via UITransformComponent.anchoredPosition + anchors (AlignmentOption, Anchors, Pivot) + OffsetMin/Max (do not touch Position). UIGroup separation principle, DefaultShow, Enable vs Visible, Connect / Disconnect event pairs. UI entities are client-only — server RPC / Sync do not work on them.
.dataset — dataset.md
UserDataSet / LocaleDataSet each consist of a `.userdataset`/`.localedataset` metadata wrapper + `.csv` sidecar pair. The CSV holds the actual tabular data (all cells are strings); the wrapper holds the EntryKey, name (runtime lookup key), and serveronly flag. Required column rules for LocaleDataSet: Key/Source/Note + locale columns. Runtime APIs: `_DataService:GetTable(name)` / `:GetCell` / `:GetRowCount` for UserDataSet, `_LocalizationService:GetText(key)` (ClientOnly) for LocaleDataSet. Prefer Maker UI for create / delete.
---
Related Skills / Documents
| Target | Purpose |
|---|---|
| platform.md (core) | TileMapMode ↔ Body, SpriteRUID, spawn, coordinates, folder metadata, ID generation, .config (common to all map types) |
| platform-maple.md / platform-rect.md / platform-sideview.md | Per-map-type physics, events, patterns, and checklists |
| troubleshooting.md | Symptom → cause → fix reference (e.g., LEA-3004) |
| workspace.md | Workspace / hierarchy / file path rules |
msw-scripting | Component/Logic, properties, lifecycle, @ExecSpace |
msw-defaultplayer | Player model, Values, Body components |
msw-search | RUID / asset / document search |
MSW Datasets (UserDataSet / LocaleDataSet)
Assets in MapleStory Worlds (MSW) for managing static game data and translation strings in tabular form.
Practical paths for manipulating datasets
1. Open the dataset view directly in the Maker editor UI (prefer this path for create / delete / row / column / cell editing). 2. Read and update at runtime from scripts (.mlua) via _DataService (UserDataSet table/cell access) and _LocalizationService (LocaleDataSet translation lookup, ClientOnly). Use for automation, validation, bulk changes.
---
Dataset Types Summary
| Type | Extension | Sidecar | EntryKey prefix | Use |
|---|---|---|---|---|
| UserDataSet | .userdataset | .csv | userdataset:// | Static game data (item tables, wave settings, balance, etc.) |
| LocaleDataSet | .localedataset | .csv | localedataset:// | Key-based multi-language translation tables |
---
File-Pair Structure
A dataset is not a single file. It consists of a metadata wrapper + a CSV sidecar that holds the actual data.
RootDesk/MyDesk/
├─ ItemTable.userdataset ← metadata (JSON wrapper)
└─ ItemTable.csv ← real data (CSV)- Editing cells in the Maker dataset editor updates the
.csv. - Changing column names or dataset properties updates the
.userdataset. - Both files must share the same base name and reside in the same directory.
Note: Older docs showcolumns/datasarrays inline insideContentProto.Json. Current Maker writes row data to the `.csv` sidecar instead and strips those keys from the.userdatasetJSON on save. The engine still falls back to inlinecolumns/datasif the CSV sidecar is missing, so older single-file datasets continue to load.
---
UserDataSet (.userdataset)
Metadata wrapper
{
"Id": "",
"GameId": "",
"EntryKey": "userdataset://93729dda-ef49-403b-86f7-982d08fc353f",
"ContentType": "x-mod/userdataset",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"name": "dataset_sample",
"id": "93729dda-ef49-403b-86f7-982d08fc353f",
"serveronly": false,
"syncDataSetWebUrl": "",
"dynamicloading": 0
}
}
}Key fields:
| Field | Meaning |
|---|---|
EntryKey | userdataset://<UUID> — UUID matches ContentProto.Json.id |
ContentType | Always x-mod/userdataset |
ContentProto.Json.name | Runtime lookup key. Used in _DataService:GetTable("<name>") and _DataService:GetCell("<name>", row, col) |
ContentProto.Json.id | UUID. Stays in sync with EntryKey |
ContentProto.Json.serveronly | true = not exposed to client |
ContentProto.Json.syncDataSetWebUrl | External sheet sync URL (Google Sheets, etc.). Leave empty if unused |
ContentProto.Json.dynamicloading | Runtime dynamic-load option (0 = Off) |
CSV sidecar
id,a,b,c
row1,1,2,foo
row2,3,4,barRules:
- UTF-8 encoding
- First line = column header, subsequent lines = data rows
- All cell values are strings (convert to number/boolean at runtime)
- Column names starting with
#are memo/comment only — do not use as game-logic keys - Blank cells may return empty string `""` rather than
nil— check for both - Row-identifier column name is free-form;
FindRowsearches by column name so only the header needs to be consistent
Type conversion pitfalls
-- string → number
local x = tonumber(ds:GetCell(1, "a")) or 0
-- Integer ID matching: MSW number is float, tostring(3) may yield "3.0"
local key = tostring(math.floor(itemId))
local row = ds:FindRow("id", key)Runtime Lua access
_DataService is the runtime entry point for UserDataSet access.
-- Table-object style: fetch once, call methods on it
local ds = _DataService:GetTable("ItemTable") -- returns UserDataSet
local count = ds:GetRowCount()
local value = ds:GetCell(1, "Price") -- 1-based row index, column name
local row = ds:GetRow(1) -- UserDataRow
local found = ds:FindRow("ItemID", "003") -- search by column value, returns UserDataRow or nil
local col = ds:GetColumn("Price") -- table<string>
local all = ds:GetAllRow() -- table<UserDataRow>
-- Service-direct style: pass dataset name on every call
local n = _DataService:GetRowCount("ItemTable")
local cell = _DataService:GetCell("ItemTable", 1, "Price")
local cell2 = _DataService:GetCell("ItemTable", 1, 3) -- col index also OK (1-based)UserDataSet and UserDataRow return all cell values as string; convert with tonumber() etc. as needed.
---
LocaleDataSet (.localedataset)
Metadata wrapper
Same structure as UserDataSet except:
- `ContentType`:
x-mod/localedataset - `EntryKey`:
localedataset://<UUID>
CSV column rules
| Column | Position | Role |
|---|---|---|
Key | 1st (required) | Lookup key |
Source | 2nd (required) | Source/reference text |
Note | 3rd (required) | Translator notes |
ko, en, … | 4th+ (at least one required) | Per-language translation columns |
The first three columns have fixed order and role; locale columns follow after them.
Runtime Lua access
LocaleDataSet is queried through `_LocalizationService` (all methods ClientOnly). The service reads from whichever LocaleDataSet asset is in the workspace; you do not specify a dataset name.
-- Current client locale (uses _LocalizationService.CurrentLocaleId column)
local text = _LocalizationService:GetText("ui_start")
-- Format placeholders {0}, {1}, …
local greet = _LocalizationService:GetTextFormat("hello_user", playerName)
-- Force a specific language column via Translator
local en = _LocalizationService:GetTranslatorForLocale("en")
local enText = en:GetText("ui_start")
local enFmt = en:GetTextFormat("hello_user", playerName)
-- Local (current-locale) Translator shortcut
local koText = _LocalizationService.LocalTranslator:GetText("ui_start")
-- TextComponent / TextGUIRendererComponent with IsLocalizationKey=true:
-- the component's own GetLocalizedText() (no args) uses its Text property as the key.
local rendered = self.Entity.TextComponent:GetLocalizedText()Calling _LocalizationService:GetText from a server-only context fails — translation is a client concern. For server-side localized messaging, send the key over RPC and let the client resolve it.---
Creating a Dataset
Recommended: Maker UI
Let the editor handle UUID generation, EntryKey consistency, and CSV creation.
Manual creation
1. Generate a UUID with a cross-platform command: node -e "console.log(require('node:crypto').randomUUID())" 2. Write <Name>.userdataset using the template above — fill in name, id, EntryKey 3. Write <Name>.csv — UTF-8, first line = header, then data rows 4. Place both in the same folder under RootDesk/MyDesk/ and hit Refresh in Maker
---
What Does NOT Work
| Approach | Status |
|---|---|
| HTTP RPC for dataset CRUD | Removed — old API no longer exists |
| Dedicated MCP tool for datasets | None — use Maker UI or script API only |
The agent must not assume any arbitrary HTTP RPC endpoint — only the two paths above (UI / script).
---
Recommended Use Cases
- Wave config (enemy composition, spawn intervals, type weights)
- Skill balance (cost, damage, cooldown, RUID)
- Item pricing and effects
- Boss pattern thresholds (HP %, range, interval)
- Multi-language UI strings (LocaleDataSet)
Separating balance data into .userdataset + .csv allows patching by swapping CSV alone — fast iteration without code changes.
MSW Entity — .map Placement & Runtime
Domain rules for entity instances inside .map files — which mode (TileMapMode) places things where, how coordinates / footholds / camera / RUID interact, the modelId vs inline decision, runtime lifecycle. .model template authoring is split out into model.md.
The actual call protocol for `.map` mutation (MapBuilder API, snapshot workflow, coverage gaps, `.map` / `.model` cross-flow) lives in [builder-protocol.md §1](builder-protocol.md). Re-read builder-protocol.md every turn that touches `.map`; this document supplies the domain context (why the calls look that way) and is read alongside it.
The legacy Maker RPC (curl) API has been removed. .map inspection and mutation go through scripts/map/msw_map_builder.cjs (= MapBuilder), followed by msw-maker-mcp verification tools.
---
File / Tool Overview
| Area | Path | Role |
|---|---|---|
| Map | ./map/*.map | Map root, footholds, tiles, all placed entities |
| User models | ./RootDesk/MyDesk/Models/{Category}/*.model (typed subfolder, e.g. Models/Monsters/) | Custom .model templates (model.md — never save directly under MyDesk/ or Models/) |
| System models | ./Global/*.model | Engine default templates (monster presets, Player, etc.) — read-only; copy into MyDesk/Models/{Category}/ to customize |
| UI | ./ui/*.ui | UI-only entities and widgets (`msw-ui-system`) |
Placing a monster — read monster.md first. The two verified working canonicals each have 11 components (Soldier.modelfor Pattern A — script-driven SpriteRUID;MonsterCanonical.modelfor Pattern B —AIChaseComponent+ ActionSheet pipeline).ActionSheetkeys are lowercase.IsLegacy: falseis mandatory onHitComponentfor both patterns; onStateComponent(and onAIChaseComponentif present) only for Pattern B — Pattern A leavesStateComponent.IsLegacyat the default. Mixing inline@componentswithmodelIdoverrides on a system monster model producesLEA-3046 InternalErrorat runtime; bake the values into a dedicated.modelinstead.
MCP tools are self-documenting when connected. If the user asks about MCP setup, share this link: https://maplestoryworlds-creators.nexon.com/ko/docs?postId=1368
---
Scope Concept (file-edit workflow)
1. Scope of "what shows up in the list"
- The editor hierarchy and the builder entity list for
./map/{mapname}.maponly contain entities belonging to that map instance. - When listing entities by opening a file, the currently edited map file is the scope. Placements in other maps live in other
.mapfiles.
2. Scope of ID / path-based access
- In runtime Lua, you can reference an entity in any map via a global path like `_EntityService:GetEntityByPath("/maps/map01/Monster01")`.
- Entity `id` (UUID) is also stored in the map file, and scripts can track it by the same ID (assuming the map is loaded).
Practical implication: "What's in this map?" → search ./map/{this}.map. "Find this entity across the whole world" → grep all .map files.
---
Component vs Logic
See msw-scripting §3 for type comparison, declaration syntax, and decision criteria. Behavior attached to an entity → Component; global singleton manager → Logic.---
StateComponent vs StateAnimationComponent
StateComponent
- Role: game-logic state machine (e.g.
Walk,Jump,Dead,Attack). - May not play animation directly — manages only state names and transition conditions.
- Controlled from scripts via
CurrentStateName,ChangeState(), etc. - The DefaultPlayer family uses
StateComponent+AvatarStateAnimationComponent.
StateAnimationComponent
- Role: visual state / action playback based on sprite / action sheet (
ActionSheet). - In monster / object models, handles the action name ↔ sprite sequence mapping.
- A common pattern is
actionSheetetc. in.modelPropertieslinking toStateAnimationComponent.
Difference: StateComponent = logical state; StateAnimationComponent = sprite animation data. In a model that has both, keep names and transition timing aligned.
---
TouchReceiveComponent vs ButtonComponent
See msw-scripting §10 for world input (TouchReceiveComponent + TouchEvent) vs UI input (ButtonComponent + ButtonClickEvent). Swapping them silently drops all input. Do not attach UI components to map entities.---
Entity.CurrentMap (strongly recommended)
At runtime, when spawning, parenting, or searching within the same map, use `self.Entity.CurrentMap` or an already-acquired map entity.
local map = self.Entity.CurrentMap
_SpawnService:SpawnByModelId("myenemy", "Enemy_1", position, map)- `SpawnService` parent must not be nil — yields LWA-3019 warnings and undefined behavior.
- Unless a special case requires another parent (such as out-of-map common), always pass the map entity.
- For file-only edits, reflect the spawn position in
.map'sTransformComponent.Position.
---
RUID (Resource Unique ID)
- MSW resources (sprites, tilesets, sounds) are identified by RUID strings.
- An empty `SpriteRendererComponent.SpriteRUID` means the entity is invisible (with no error).
- In
.modelValuesor.map@components, use either a string or{ "DataId": "hex..." }form — match the existing pattern in the same map / model.
Asset search: use the msw-search skill or _ResourceService API. Replace temporary placeholders with real assets before release (platform.md).
---
MapBuilder — call protocol lives in builder-protocol.md
.map snapshot workflow (get → edit → set), the API table, placement / patch / rename / remove / component CRUD / tile / foothold inspection, coverage gaps, Map Mode Rules, false-return handling — every detail of MapBuilder invocation is consolidated in the single entry point [builder-protocol.md §1](builder-protocol.md). Read it just before any .map work.
This document carries the why behind those calls — Scope, RUID, the meaning of the TileMapMode-to-Body mapping, the modelId vs inline decision rule, placement coordinates / footholds / camera visibility, runtime verification, and the constraint checklist.
Domain-side summary rules:
.mapEntitiesarrays are very large. Direct raw JSON editing is reserved for builder coverage-gap areas only — minimal scope plusrefresh+ logs verification.- Patch values through world-unit vectors (
pos: [x, y, z]), color helpers, and component override objects. Do not hand-write raw component JSON except as a builder argument for a single-component payload.
---
TileMapMode ↔ Movement Components
MapComponent.TileMapMode on the map root determines the entire movement / gravity / collision stack. If an entity's Body-family component does not match the map, it will not move (with no error) — and the engine will log one of the [LEA-3004] MissingComponent messages (platform.md §4).
Map Work Preflight (mandatory before any map task)
Before touching a map in any way (entity placement, spawn, movement scripts, applying models, tile edits, etc.), always confirm the following two items in order:
1. Identify which map you are working on and where it lives (e.g. the ./map/{mapname}.map path and its root entity). 2. Use `MapBuilder.read(...).getTileMapMode()` to read `MapComponent.TileMapMode` as a number, and keep the value in mind:
0→ MapleTile (side-view + Foothold; Body =RigidbodyComponent)1→ RectTile (top-down grid; Body =KinematicbodyComponent)2→ SideViewRectTile (side-view tile grid; Body =SideviewbodyComponent)
Do not proceed with model / entity / script work while the `TileMapMode` value is unknown or unclear. The three modes differ completely in Body component, events, gravity, and collision. A mismatch is not a compile-time error — it surfaces as a runtime [LEA-3004] MissingComponent log or as a silent failure where the entity simply refuses to move with no error at all.
Recommending the mode (when starting a new map or when the current mode is wrong for the user's goal)
For new map authoring — or whenever the current map's TileMapMode clearly does not fit the gameplay the user described — explicitly recommend the appropriate `TileMapMode` before doing any further entity / model / script work (do not silently proceed with whatever is already on disk).
Use this decision matrix:
| User's intended game / gameplay | Recommend | Why |
|---|---|---|
| MapleStory-style side-scrolling action · jump · ladder · freely placed footholds (platformer) | `0` MapleTile | Side-view + gravity + freely placed Foothold line segments |
| Top-down RPG · maze · board game · dungeon crawler · Bomberman-style · farming sim | `1` RectTile | Top-down 4-directional free move, no gravity, square-tile grid |
| Tile-based side-scrolling platformer · Mario-style pixel action · side-view puzzle | `2` SideViewRectTile | Side-view + gravity on a tile grid (not free footholds) |
If the user has not yet told you what kind of game they want, ask one short question first (e.g. "Is it top-down, or side-scrolling (jump/ladder)? Is it based on freely placed footholds, or a square tile grid?") and only then recommend.
Changing TileMapMode — user action in Maker, not an AI file edit
The AI must never write a new value into MapComponent.TileMapMode directly in the .map JSON. Mode switching swaps tile components, rebuilds footholds, and converts tile-data formats — Maker handles all of that internally.
Guide the user to switch the mode in the Maker editor as follows:
1. Open the Maker editor's Hierarchy window. 2. Right-click the target map entity in the Hierarchy. 3. Choose the "Switch ..." option that matches the target mode (Switch TileMap / RectTileMap / SideViewRectTileMap) from the context menu. 4. After the user reports the switch is complete, call MCP `refresh`, then re-read MapComponent.TileMapMode to confirm and re-check every dynamic entity's Body component against the new mode.
AI role on mode changes: recommend mode → wait for the user to right-click-switch in the Maker Hierarchy → refresh → fix Body components / scripts that no longer match. Do not flip TileMapMode from a file edit.Mapping table / check protocol / transition limits: platform.md §4. LEA-3004 and other silent-failure symptoms (won't move / won't render / floating in mid-air / stuck in a wall …): troubleshooting.md. Per-map-type code patterns: platform-maple.md / platform-rect.md / platform-sideview.md. Tile painting itself: tile.md.---
Two-Step Map Editing Workflow (create → place)
1. Create — define the .model under RootDesk/MyDesk/Models/{Category}/{Name}.model (typed subfolder; details in model.md §1, §2.2). 2. Place
MapBuilder.read(...)→map.placeModel(...)→map.write(...). Concrete call sequence, API tables, and option details live in builder-protocol.md §1 + §4.placeModel()returns the placed root entity id string, not the builder. Do not chain.write()after it.- `modelId` form (default — required for ≥2 instances):
placeModel()mirrors model components and applies per-instance overrides. - Inline form: use
sprite()/empty()only for truly one-off map-local entities. refresh.
modelId vs Inline — Decision Rule
| Situation | Form |
|---|---|
| Same composition placed ≥2 times in this map | `modelId` (always — author a .model first if none exists) |
| Same composition reused in another map | `modelId` |
Will be spawned at runtime via SpawnByModelId | `modelId` (required) |
| Truly one-off composition that will never recur | inline @components is acceptable |
When in doubt, choosemodelId. Five inline copies of "the same monster" silently drift apart over edits (one getsIsLegacy: true, another losesSortingLayer); the model anchors the canonical values and a single edit propagates.
---
Handling Entity Instances in .map
Common fields (must match)
- `id`: UUID v4 (with hyphens). Generate fresh for new entities.
- `path`:
/maps/{mapname}/{entityname}— parent-child hierarchy is the path prefix. - `componentNames`: comma-joined
@typevalues of@components, kept in sync. - `jsonString.path`: same as the outer
path. - `pathConstraints`: root
//, child///. - `displayOrder`: avoid overlap among siblings.
modelId entities
Use MapBuilder.placeModel() — it creates the model-instance metadata, keeps component names in sync, mirrors model components, and applies per-instance TransformComponent.Position and componentOverrides. For the call signature and option details, see builder-protocol.md §1.4 + §4.
Adding a new map to the world
- You may need to add
map://{mapId}toentriesinGlobal/SectorConfig.config.
---
Tile-map entity transform is locked
The map's tile-grid container — the entity carrying TileMapComponent (MapleTile) or RectTileMapComponent (RectTile / SideViewRectTile) — has its TransformComponent locked by the tile-map component itself. Writes to TransformComponent.Position / EulerAngles / Scale are silently rejected with a LWA-3047 NativeIssue_UnableToChange warning. The engine keeps this entity at a fixed origin ((0, 0, z), or a half-cell offset for odd-grid RectTile maps) so that tile coordinates and world coordinates stay in a known relationship.
This applies regardless of whether the entity was placed with `modelId` or as an inline `@components` block — the lock comes from the tile-map component, not from how the entity was authored. Moving the entity in .map JSON appears to take, but refresh reverts it to (0, 0, z); runtime Position = ... writes produce no visible movement and log [LWA-3047].
Workaround: do not try to move the tile-map entity. Anchor your game's coordinate system to the locked origin instead — keep gameplay anchors (grid origin, spawn points, path waypoints) in tile coordinates and convert via the tile-map component's helpers (e.g. RectTileMapComponent:ToWorldPosition(cellPos) — see `platform-rect.md` §3).
Symptoms when the rule is ignored:
- A
Positionwritten into.mapJSON reverts to(0, 0, z)after Makerrefresh. - Runtime
TransformComponent.Position = ...writes have no observable effect;logsshows[LWA-3047] UnableToChange. - Adding a custom child entity to the tile-map entity works, but the child's effective world position is still measured relative to the locked parent at
(0, 0).
This is by design — the tile-map entity is the canonical reference frame for tile↔world conversion. Decorations, spawn anchors, or overlays that need to be elsewhere should live as siblings under the map root, not as children of the tile-map entity.
---
Placement Coordinate Rules
Y (MapleTile + footholds)
- Align character / foothold-based entities to the top of the foothold.
- The `y` of each foothold's
StartPoint/EndPointinFootholdComponentis the platform height. - A small offset (+0.01 to 0.05) may be needed depending on sprite anchor / collider offset — verify with
play+screenshot.
Horizontal Spacing (multiple monsters / objects in a row)
- Always use the `modelId` form for repeated entities (see "Two-Step Map Editing Workflow → Decision Rule" above). The N instances should share one
.modeland differ only inTransformComponent.Position. - Space along X without overlap based on each entity's bound width (
TiledSize,BoxSize). - Even spacing:
x_i = x0 + i * (width + gap). - On the same foothold, share the same Y and only shift X.
RectTile / SideViewRectTile
- Grid-based placement — verify the conversion between
RectTileMapComponenttile coordinates and world coordinates (tile.md). - On RectTile (no gravity), assume Kinematicbody and move on the XY plane.
Camera Visible Area
- See the rough visible-world-unit table in platform.md §5 for PC / mobile — verify the start position is on-screen.
---
RPC → File-Based Replacement Table
| Old (RPC) | Current equivalent |
|---|---|
| Create entity | Author .model under RootDesk/MyDesk/Models/{Category}/ + place it with MapBuilder.placeModel() |
| Delete entity | MapBuilder.remove() |
| Change property | MapBuilder.patchComponent() for map instances or ModelBuilder values for templates |
| Add/remove component | MapBuilder.upsertComponent() / removeComponent() for one-off map-local instance changes |
| Register/edit/delete model | CRUD .model files under RootDesk/MyDesk/ (refresh) |
| List entities | MapBuilder.snapshot() / listEntities() |
---
Runtime Verification
For runtime state that's hard to know from files alone, use logs and log() in play mode.
Flow
1. Add log() in .mlua for the value to inspect 2. refresh → play → collect via logs 3. stop → edit files → repeat
When you don't know an API
1. `.d.mlua` — search Environment/NativeScripts/ for EntityService, SpawnService signatures 2. `msw-search` — API details, implementation guide
After work, **stop** to return to edit mode.
---
Constraint Rules Checklist
Files / Editor / MCP
1. Run `refresh` after file changes (not allowed during play — stop first). 2. Use `MapBuilder` first for `.map` work — direct raw JSON edits are only for explicitly unsupported gaps, and must be minimal plus verified. 3. *Do not modify `Environment/.d.mlua** — API definitions are read-only. 4. **Do not create or edit .codeblock manually** — Maker refresh generates it from .mlua. 5. **Take screenshot` only when the user explicitly asks or when identifying coordinates for input simulation.**
Physics / Movement / Map
6. TileMapMode ↔ Body components must match. 7. On MapleTile, placement Y is foothold-based; assumes gravity / Rigidbody. 8. On RectTile, do not expect vertical foothold physics — assumes Kinematicbody. 9. When inspecting or changing foothold data, use MapBuilder APIs so Id / Length / OwnerId consistency is centralized.
Render / Resource
10. If a visual is needed, do not leave `SpriteRUID` empty. 11. RUIDs must be project-registered resources — arbitrary strings are missing at runtime. 12. Match the form of TileSetRUID / sprite DataRef to existing maps.
Entity / Spawn / Hierarchy
13. Keep `id` / `path` / `componentNames` / `jsonString.path` consistent in `.map`. 14. `SpawnService` parent must not be nil — pass a map entity such as self.Entity.CurrentMap. 15. When referencing `modelId`, origin.entry_id = modelId, and origin.root_entity_id = the entity's own outer id (top-level instance). 16. Use `MapBuilder.placeModel()` for `modelId` instances — it mirrors model components and keeps componentNames in sync. Empty component names or partial component arrays silently remove components at runtime. 17. Child entities must have a `path` that is a prefix of the parent.
Input / UI Boundary
18. TouchReceive (world) vs Button (UI) — do not confuse input layers. 19. Keep the responsibilities of UI-only groups (the ui hierarchy) and map entities separated.
State / Animation
20. Do not confuse the roles of StateComponent (logic) vs StateAnimationComponent (sprite action). 21. Action-name strings must match across code, action sheet, and animation data.
Verification Loop
22. `refresh` → `logs` → `play` → `logs` → `stop`. 23. On intermediate failure, stop the following steps — fix the cause and retry.
---
Related Skills / Docs
| Doc | Purpose |
|---|---|
| builder-protocol.md §1 | `.map` call protocol — MapBuilder API, snapshot workflow, coverage gaps, `false`-return handling (read every turn that touches .map) |
| builder-protocol.md §4 | .model author → .map placement → refresh cross-flow |
| model.md | .model template authoring domain (when / catalog / component combinations) |
| tile.md | Tile maps / tilesets |
| `msw-ui-system` | UI authoring |
| platform.md (core) | TileMapMode ↔ Body mapping, spawn, RUID, coordinates, .directory, ID, .config (common to all map types) |
| platform-maple.md / platform-rect.md / platform-sideview.md | Per-map-type physics / events / patterns / checklists |
| troubleshooting.md | Symptom → cause → fix (LEA-3004, "won't move", "won't render" …) |
msw-defaultplayer | Player model / Values / components |
msw-scripting | Component / Logic, properties, lifecycle |
msw-search | RUID / asset / doc search |
Core principle of entity work: "models are templates, maps are instances, builder-protocol.md is the single call manual, MCP is for verification."
MSW .model Files — Authoring Domain
A .model is an entity template. This document carries the domain rules of .model authoring — when to create one, which template to start from, which component combinations fit which entity types, and the lifecycle order when a script component is bound to a model.
The actual call protocol for `.model` mutation — `ModelBuilder` API, fluent-chaining rules, `typeKey` values, validation (M030–M036), child entity invariants, event-link authoring, `.model` → `.map` cross-flow — lives in [builder-protocol.md §2](builder-protocol.md). Re-read builder-protocol.md every turn that touches `.model`.
0. Non-Negotiable Rule (summary)
- Do not inspect or edit
.modelJSON directly. NoRead/cat/Get-Content/grep/ manual JSON patches. - All read / create / update / write goes through
scripts/model/msw_model_builder.cjs(ModelBuilder). - The builder fully owns
EntryKey,ContentProto.Json.Id/Name, value type descriptors, inspector-property links, child model shape, and event-link preservation. - Concrete call patterns / API tables / chaining-safe vs non-builder returns /
typeKeyvalues / helper functions → builder-protocol.md §2.
1. When to Create a .model
Default rule: if the same entity composition will appear two or more times, author a .model and place instances via modelId. Runtime spawning with SpawnByModelId also requires a registered model.
| Situation | Choice |
|---|---|
Same composition placed >= 2 times in one map | Create .model |
| Same composition used across maps | Create .model |
Runtime spawn via SpawnByModelId | Create .model |
| Complex inspector-exposed defaults | Create .model |
| Truly one-off decoration used once | Inline map entity is acceptable |
Save user models under RootDesk/MyDesk/Models/{Category}/{Name}.model, never directly under MyDesk/, directly under Models/, or under Global/.
When creating a new folder, create the folder only. Maker Refresh generates folder metadata later.
2. Template Catalog
Never start from a blank model. Pick the closest template from the skill-local models/ folder, then load it with ModelBuilder.fromTemplate().
2.0 Template Path
Templates live in this skill's own models/ folder, sibling to scripts/ and references/. The ../models/<Name>.model notation in the tables below is a catalog identifier — not the literal string to pass to fromTemplate.
fromTemplate's first argument is resolved against process.cwd(), so always pass either an absolute path or a __dirname-derived path. Never guess. Templates are NOT under Global/, RootDesk/, MyDesk/, or a top-level Models/ — those are output locations. An error like model file not found: ./Global/<Name>.model means the path was fabricated; recompute it from the skill location, do not create a file there.
const path = require("path");
const templateDir = path.join(__dirname, "..", "models"); // from a script under scripts/model/
ModelBuilder.fromTemplate(path.join(templateDir, "ChaseMonster.model"), "MyMonster");Base
| Template | Use |
|---|---|
../models/TransformOnly.model | Empty entity with only TransformComponent |
Characters / Players
| Template | Use |
|---|---|
../models/Player.model | Player variant |
../models/DefaultPlayer.model | DefaultPlayer customization, usually with BaseModelId |
Monsters
Read `monster.md` before authoring a monster.
| Template | Use |
|---|---|
../models/MonsterCanonical.model | Default start for new monsters |
../models/ChaseMonster.model | Chasing side-view monster, with caveats in `monster.md` |
../models/MoveMonster.model | Patrol movement monster, with caveats in `monster.md` |
../models/StaticMonster.model | Stationary attacker, with caveats in `monster.md` |
NPC / Interaction
| Template | Use |
|---|---|
../models/StaticNPC.model | Static NPC with dialogue/name tag |
Terrain
| Template | Use |
|---|---|
../models/Foothold.model | MapleTile foothold |
../models/Ladder.model | Climbable ladder |
../models/Rope.model | Climbable rope |
../models/Portal.model | Map portal/teleport trigger |
Map Objects / Decoration
| Template | Use |
|---|---|
../models/MapObject.model | Generic decorative object |
../models/ParticleMapObject.model | Object with particles |
../models/SkeletonMapObject.model | Skeleton-based animated object |
../models/ItemAsset.model | Item display |
Particles / Effects
| Template | Use |
|---|---|
../models/BasicParticle.model | Generic particle |
../models/SpriteParticle.model | Sprite-sheet particle |
../models/AreaParticle.model | Area effect |
../models/AnimationPlayer.model | One-shot animation effect |
Sound
| Template | Use |
|---|---|
../models/Sound.model | Position-based sound |
../models/SoundEffect.model | One-shot SFX |
Tilemap Containers
| Template | Use |
|---|---|
../models/TileMap.model | MapleTile tile container |
../models/RectTileMap.model | RectTile/SideViewRectTile tile container |
../models/MapleMapLayer.model | Maple-style map layer |
../models/MapEmpty.model | Empty map container |
External Media / UI Prefabs
| Template | Use |
|---|---|
../models/WebSprite.model | External image URL |
../models/YoutubePlayerWorld.model | YouTube world object |
../models/UIButton.model | UI button prefab |
../models/UIText.model | Simple UI text prefab |
../models/UITextGUIRenderer.model | Text GUI renderer prefab |
../models/UISprite.model | UI sprite prefab |
../models/UIGroup.model | UI group prefab |
../models/UIEmpty.model | Empty UI prefab |
For full UI layout work, use the msw-ui-system skill instead of authoring UI models directly.
3. Builder Workflow / 4. API Quick Reference — see builder-protocol.md
The call sequence (fromTemplate / read / fluent mutate / write), per-method API signatures, chaining-safe vs false-return distinction, typeKey values (bool / int / ... / action_sheet), helpers (vector2 / vector3 / quaternion / dataRef / collisionGroup / actionSheet), Inspector Property / Child Entity tree (child shell schema, ParentId invariants, validation rules M030–M036), Event Link, and .model → .map cross-flow (ModelBuilder.write → MapBuilder.placeModel) — every invocation detail is consolidated in the single entry point [builder-protocol.md §2 + §4](builder-protocol.md).
This document covers only the domain side of .model authoring:
- When to create a
.model(§1) - Which template to start from (§2)
- Which component combinations fit which entity types (§5)
- Lifecycle order when a script component lives inside a
.model(§6) - Pre-completion checklist (§7)
5. Component Combinations
| Entity Type | Core Components |
|---|---|
| Visual object | TransformComponent, SpriteRendererComponent |
| MapleTile side-view moving monster | MovementComponent, RigidbodyComponent, StateComponent, HitComponent |
| RectTile top-down moving object | MovementComponent, KinematicbodyComponent |
| SideViewRectTile moving object | MovementComponent, SideviewbodyComponent |
| Interactive NPC | SpriteRendererComponent, TouchReceiveComponent |
| Attackable enemy | AttackComponent, HitComponent |
Body component must match the target map's TileMapMode; see `platform.md` §4.
6. Script Components
Custom script.XXX components in .model depend on the script type already being registered.
Required order:
1. Write the script .mlua. 2. Maker refresh. 3. Build or patch the .model through ModelBuilder. 4. Maker refresh again.
If this order is inconvenient, keep the .model native-only and attach the script at spawn time with entity:AddComponent("ScriptName").
7. Checklist
- [ ] Used
ModelBuilder.read()/snapshot()/fromTemplate(), not raw.modelreading. - [ ]
fromTemplatepath is absolute or__dirname-derived (§2.0); never./Global/...,./Models/..., or a guess. - [ ] Saved under
RootDesk/MyDesk/Models/{Category}/. - [ ] Created any needed folder only; left folder metadata to Maker Refresh.
- [ ] Picked the Body component matching
TileMapMode. - [ ] Set a real
SpriteRUIDwhen usingSpriteRendererComponent. - [ ] Used explicit
typeKeyfor new or changed values. - [ ] Called Maker
refreshafter write. - [ ] Checked logs after refresh/play.
8. Related Docs
| Doc | Purpose |
|---|---|
| builder-protocol.md §2 | `.model` call protocol — ModelBuilder API, chaining rules, `typeKey`, Child Entity, Event Link, validation (read every turn that touches .model) |
| builder-protocol.md §4 | .model → .map cross-flow (ModelBuilder.write → MapBuilder.placeModel → refresh) |
| `entity.md` | Placing the authored model in a map, spawn, runtime verification domain |
| `monster.md` | Monster-specific canonical defaults and pitfalls |
| platform.md (core) | File location rules, folder metadata, TileMapMode ↔ Body, ID generation |
| platform-maple.md / platform-rect.md / platform-sideview.md | Per-map-type Body / movement patterns |
msw-scripting | Authoring the .mlua scripts attached to models |
msw-search | Resource lookup such as SpriteRUID |
.model Schema Detail
Normal AI authoring must not use raw .model schema details.
Use `../model.md` and scripts/model/msw_model_builder.cjs instead:
- inspect with
ModelBuilder.read()/ModelBuilder.snapshot() - create with
ModelBuilder.fromTemplate() - patch with builder methods
- save with
write()
This file is intentionally kept as a compatibility stub so older links do not break. It does not expose raw field details because .model JSON should be treated as a builder-managed format.
Platform: MapleTile (Side-View Platformer) — TileMapMode = 0
When to read this file: When the .map you're working with has MapComponent.TileMapMode = `0` (MapleTile), or when working on MapleStory-style side-scrolling action (jump / ladder / free-position platforms).
This file was split from `platform.md` as a MapleTile-specific guide. Rules common to all map types (8 core, coordinate system, RUID, spawn, ID, .config) remain in `platform.md`. Other map types: `platform-rect.md` / `platform-sideview.md`.
---
1. Map Type at a Glance
| Item | Value |
|---|---|
| TileMapMode | 0 |
| Enum name | MapleTile |
| View | Side-view (side-scrolling) |
| Body component | `RigidbodyComponent` |
| Map component | TileMapComponent + FootholdComponent |
| Gravity | Yes (built-in, adjustable) |
| Terrain | Foothold (line segments) — non-grid free placement |
| Movement axes | Left/right + jump (Y = gravity) |
| Collision | Foothold collision |
| Representative genres | MapleStory-style platformer |
---
2. Grid / Physics / Properties
Grid Size
TileMapComponent.GridSize is fixed at `(0.45, 0.3)` (static readonly). Cannot be changed.
Physics System
MapleStory's unique Foothold-based physics.
- Gravity:
RigidbodyComponent.Gravity(has default, adjustable) - Walking on platforms:
WalkSpeed,WalkAcceleration,WalkDrag - Air movement:
AirAccelerationX,AirDecelerationX,FallSpeedMaxX/Y - Jump:
WalkJump(height),JumpBias(hang time) - Mass:
Mass(acceleration/deceleration responsiveness)
-- RigidbodyComponent key properties example setup
local rb = self.Entity.RigidbodyComponent
rb.Gravity = 30 -- gravity strength
rb.WalkSpeed = 3 -- max movement speed
rb.WalkJump = 6 -- jump height
rb.WalkAcceleration = 10 -- movement acceleration
rb.WalkDrag = 1 -- movement friction
rb.Mass = 1 -- mass---
3. Foothold System (Terrain & Collision)
- `FootholdComponent`: Manages all footholds on the map. Interacts with
RigidbodyComponent. - Footholds are line segments (StartPoint ~ EndPoint).
- Walking only happens on footholds — falls due to gravity without one.
DownJump(): Jump downward (fall through foothold)IsOnGround(): Check if standing on a footholdGetCurrentFoothold(): Get current foothold info under feetPredictFootholdEnd(distance, isForward): Predict distance to foothold end
-- Check if on a foothold
if self.Entity.RigidbodyComponent:IsOnGround() then
-- Logic that only runs while on a foothold
end
-- Foothold end detection (AI monster)
if self.Entity.RigidbodyComponent:PredictFootholdEnd(1, true) then
-- Within 1 unit of right edge → reverse direction
end---
4. Events
| Event | Triggered when |
|---|---|
FootholdEnterEvent | Landing on a foothold |
FootholdLeaveEvent | Leaving a foothold |
FootholdCollisionEvent | Colliding with a foothold |
RigidbodyAttachEvent | Attached via AttachTo |
RigidbodyDetachEvent | Detached via Detach |
---
5. Monster / NPC Development
Requirements: 1. Monster .model must include `RigidbodyComponent`. 2. Gravity = 0 means floating in mid-air → always set to positive. 3. WalkSpeed = 0 means cannot move. 4. Spawn Y must be above a foothold (below foothold = infinite fall).
-- MapleTile monster basic patrol pattern
@Component
script MonsterAI extends Component
property boolean movingRight = true
[server only]
void OnUpdate(number delta)
{
local rb = self.Entity.RigidbodyComponent
if rb == nil then return end
-- Only move while on a foothold
if rb:IsOnGround() == false then return end
-- Detect foothold end → reverse direction
if rb:PredictFootholdEnd(0.5, self.movingRight) then
self.movingRight = not self.movingRight
end
-- Set movement direction
local dir = 1
if self.movingRight == false then dir = -1 end
rb.MoveVelocity = Vector2(dir, 0)
}
end---
6. Special Features
- KinematicMove mode: Setting
RigidbodyComponent.KinematicMove = trueswitches to top-down movement mode (moves top-down style on a MapleTile map). - AttachTo / Detach: Attach to another entity (moving platforms, etc.).
- AddForce / SetForce: Apply physics-based forces (knockback, push).
---
7. MovementComponent — InputSpeed Conversion
MovementComponent is a high-level wrapper usable with all Body types. In MapleTile: InputSpeed is passed directly to Rigidbody.
| TileMapMode | Actual speed | Notes |
|---|---|---|
| MapleTile (this file) | InputSpeed passed directly to Rigidbody | — |
| RectTile | direction * InputSpeed / 1.2f | 1.2 divisor for migration compatibility |
| SideViewRectTile | direction.x * InputSpeed * 1.5f, Y preserved | Correction similar to Rigidbody |
InputSpeeddefault:1.0(MovementComponent's[MODProperty],@Sync)- The same
InputSpeed = 3feels different across map types.
local movement = self.Entity.MovementComponent
movement.InputSpeed = 3
movement.JumpForce = 1.5
movement:Jump()
movement:DownJump()
movement:MoveToDirection(Vector2(1, 0), delta)
movement:Stop()PlayerControllerComponent handles input → action mapping and internally uses MovementComponent. Default key mapping: arrows (movement), Alt/Space (jump), down+jump (down-jump). For custom movement, set PlayerControllerComponent.Enable = false then control the Body directly.
---
7. Troubleshooting (MapleTile Only)
| Symptom | Cause | Fix |
|---|---|---|
| Entity doesn't move (no error) | Body is not RigidbodyComponent | Body swap |
Log [LEA-3004] MissingComponent : Entity is missing 'RigidbodyComponent'. | Dynamic entity missing RigidbodyComponent | Add RigidbodyComponent to model/entity's @components |
| Monster floating in mid-air | Gravity = 0 | Set Gravity to positive |
| Monster falls off platform edge | No foothold-end handling | Reverse direction with PredictFootholdEnd |
| Monster disappears off-screen | Spawn Y is below foothold | Move spawn Y above foothold |
| Jump can't reach platform | WalkJump is less than foothold gap | Increase WalkJump |
Full symptom dictionary: `troubleshooting.md`. Recommended to compare there when confused with other map types.
---
8. Checklist
Common (All Map Types)
- [ ] Read
MapComponent.TileMapModeas a number directly from.mapand confirm it is 0 - [ ] Player.model Body is
RigidbodyComponentactive (DefaultPlayer handles this automatically) - [ ] Monster/NPC
.modelincludesRigidbodyComponent - [ ]
SpriteRendererComponent.SpriteRUIDis set - [ ] Spawn calls pass map entity as
parent(self.Entity.CurrentMap)
MapleTile Specific
- [ ]
RigidbodyComponent.Gravity> 0 (if not using default) - [ ] Monster spawn Y is above foothold
- [ ] Foothold-end handling logic (
PredictFootholdEndorIsolatedMove) - [ ]
WalkJumpprovides sufficient jump height for foothold gaps
---
9. Cross-references
- `platform.md` — 8 core, TileMapMode↔Body mapping table, coordinate system, RUID, spawn, ID
- `platform-rect.md` / `platform-sideview.md` — Other map types
- `troubleshooting.md` — Unified symptom dictionary
- `tile.md` — Tile painting (FootholdComponent editing, etc.)
- `entity.md` — Entity placement / Map Work Preflight
Platform: SideViewRectTile (Side-View on Tile Grid) — TileMapMode = 2
When to read this file: When the .map you're working with has MapComponent.TileMapMode = `2` (SideViewRectTile), or when working on tile-based side-scrolling platformer / Mario-style pixel action / side-view puzzle (square-tile side-view).
This file was split from `platform.md` as a SideViewRectTile-specific guide. Rules common to all map types (8 core, coordinate system, RUID, spawn, ID, .config) remain in `platform.md`. Other map types: `platform-maple.md` / `platform-rect.md`.
---
1. Map Type at a Glance
| Item | Value |
|---|---|
| TileMapMode | 2 |
| Enum name | SideViewRectTile |
| View | Side-view (side-scrolling) |
| Body component | `SideviewbodyComponent` |
| Map component | RectTileMapComponent |
| Gravity | Yes (built-in, no Gravity property) |
| Terrain | Square tile grid |
| Movement axes | Left/right + jump (Y = gravity) |
| Collision | Tile collision (Collision property) |
| Representative genres | Side-view action, tile-based platformer |
Key difference: RectTile's tile grid system + MapleTile's side-view gravity. A hybrid of both modes. Walks on square tiles with gravity instead of Foothold line segments.
---
2. Physics System
Combines RectTile's tile grid + side-view gravity.
- Gravity: Engine built-in (no separate
Gravityproperty — fall speed is adjusted viaJumpDrag). - Movement: Left/right + jump.
- Jump:
JumpSpeed(jump velocity),JumpDrag(fall speed). - Down-jump:
EnableDownJump,DownJumpSpeed.
-- SideviewbodyComponent key properties
local svb = self.Entity.SideviewbodyComponent
svb.JumpSpeed = 5 -- jump speed (higher = jumps higher)
svb.JumpDrag = 3 -- fall speed (higher = falls faster)
svb.EnableDownJump = true -- enable down-jump
svb.DownJumpSpeed = 3.3 -- down-jump rebound speed---
3. Terrain & Collision
- Uses `RectTileMapComponent` (same tile system as RectTile).
- Concept of standing on tiles + falling due to gravity.
IsOnGround(): Whether standing on a tile.GetUnderfootTile(): Info about the tile currently under feet.
-- Check underfoot tile
local svb = self.Entity.SideviewbodyComponent
local tileInfo = svb:GetUnderfootTile()
if tileInfo ~= nil then
log("Underfoot tile: " .. tileInfo.Name)
end---
4. Events
| Event | Triggered when |
|---|---|
RectTileEnterEvent | Entering a tile |
RectTileLeaveEvent | Leaving a tile |
RectTileCollisionBeginEvent | Collision tile contact begins (used for wall detection) |
RectTileCollisionEndEvent | Collision tile contact ends |
---
5. Monster / NPC Development
Requirements: 1. Monster .model must include `SideviewbodyComponent`. 2. Gravity is automatic → must spawn on top of tiles to avoid falling. 3. Left/right movement only (no top-down movement). 4. Movement driven by MoveVelocity.
-- SideViewRectTile monster basic patrol pattern
@Component
script MonsterWalk extends Component
@Sync
property boolean movingRight = true
[server only]
void OnUpdate(number delta)
{
local svb = self.Entity.SideviewbodyComponent
if svb == nil then return end
-- Only move while on the ground
if svb:IsOnGround() == false then return end
-- Set movement direction
local dir = 1
if self.movingRight == false then dir = -1 end
svb.MoveVelocity = Vector2(dir, 0)
}
-- Reverse direction on wall collision (no PredictFootholdEnd, so use collision events)
[self]
HandleRectTileCollisionBeginEvent(RectTileCollisionBeginEvent event)
{
local normal = event.Normal
if normal == Vector2.left or normal == Vector2.right then
self.movingRight = not self.movingRight
end
}
end---
6. Special Features
- Wall detection: Identify wall direction via
RectTileCollisionBeginEvent'sNormalvector. - Custom movement: Drive
MoveVelocitydirectly → implement slippery floors, acceleration/deceleration. - Dynamic tiles: Runtime tile manipulation available, same as RectTile.
---
7. MovementComponent — InputSpeed Conversion
In SideViewRectTile: direction.x * InputSpeed * 1.5f, Y preserves existing velocity (to avoid breaking gravity).
| TileMapMode | Actual speed | Notes |
|---|---|---|
| MapleTile | InputSpeed passed directly to Rigidbody | — |
| RectTile | direction * InputSpeed / 1.2f | Migration compatibility |
| SideViewRectTile (this file) | direction.x * InputSpeed * 1.5f, Y preserved | Correction similar to Rigidbody |
The same InputSpeed = 3 feels faster than RectTile (×1.5 acceleration).
local movement = self.Entity.MovementComponent
movement.InputSpeed = 3
movement.JumpForce = 1.5
movement:Jump()
movement:DownJump()---
7. Troubleshooting (SideViewRectTile Only)
| Symptom | Cause | Fix |
|---|---|---|
| Entity doesn't move (no error) | Body is not SideviewbodyComponent | Body swap |
Log [LEA-3004] MissingComponent : Entity is missing 'SideviewbodyComponent'. | Dynamic entity missing SideviewbodyComponent | Add SideviewbodyComponent to model/entity's @components |
| Floating in mid-air | Using KinematicbodyComponent (brought RectTile model as-is) | Switch to SideviewbodyComponent |
| Tile collision broken | Using RigidbodyComponent (brought MapleTile model as-is) | Switch to SideviewbodyComponent |
Error when calling PredictFootholdEnd | Not a Foothold system (MapleTile-only) | Use RectTileCollisionBeginEvent Normal for wall detection |
| Monster gets stuck in wall | No wall detection logic | Use RectTileCollisionBeginEvent + Normal |
| Down-jump doesn't work | EnableDownJump = false | Set EnableDownJump = true |
| Falls too fast / too slow | Inappropriate JumpDrag | Adjust JumpDrag |
Full symptom dictionary: `troubleshooting.md`.
---
8. Checklist
Common
- [ ] Read
MapComponent.TileMapModeas a number directly from.mapand confirm it is 2 - [ ] Player.model Body is
SideviewbodyComponentactive (DefaultPlayer handles this automatically) - [ ] Monster/NPC
.modelincludesSideviewbodyComponent - [ ]
SpriteRendererComponent.SpriteRUIDis set - [ ] Spawn calls pass map entity as
parent
SideViewRectTile Specific
- [ ] Confirmed using
SideviewbodyComponent(notRigidbody/Kinematicbody) - [ ] Spawn is on top of tiles (will fall due to gravity)
- [ ] Wall collision handling (
RectTileCollisionBeginEvent + Normal) - [ ]
EnableDownJump = trueset if down-jump is needed
---
9. Cross-references
- `platform.md` — 8 core, TileMapMode↔Body mapping table, coordinate system, RUID, spawn, ID
- `platform-maple.md` / `platform-rect.md` — Other map types
- `troubleshooting.md` — Unified symptom dictionary
- `tile.md` — Tile painting (Collision property editing, etc.)
- `entity.md` — Entity placement / Map Work Preflight
Related skills
FAQ
Can new .model files start empty?
No. Pick the closest validated template from the skill-local models catalog and load it via ModelBuilder.
Why is msw-search required after entity creation?
MSW targets polished games; entities need appropriate sprites and sounds instead of default or empty SpriteRUID values.
What is the MonsterCanonical Pattern B requirement?
Use AIChaseComponent with ActionSheet pipeline and set StateComponent IsLegacy to false; empty ActionSheet defaults fail silently.
Is Msw General safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.