mcpField
M
msw-scripting

msw-scripting

github.com/msw-git開発3810.0k 回インストール3 週間前に更新
リポジトリ

msw-scripting について

msw-scriptingは、MapleStory Worldsの.mluaスクリプトを開発・検証するスキルです。ゲーム制作者向けに、MSW固有のAPI、スクリプト構成、ライフサイクル、ビルドから実行時までのデバッグを扱います。

主な機能

  1. 01ComponentとLogicスクリプトの開発
  2. 02.d.mlua定義によるAPI確認
  3. 03機能別フォルダーでの整理
  4. 04編集後のLSP診断
  5. 05ビルドログと実行ログでの検証
  6. 06ライフサイクルと実行空間の規則

活用シーン

  1. 01既存スクリプトへの動作追加
  2. 02機能専用スクリプトの作成
  3. 03スクリプトエラーや実行時障害の診断
  4. 04イベントやライフサイクル処理の実装

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/msw-scriptingを入力して読み込まれたか確認します。

同梱ファイルreferences/datastorage.mdreferences/verify-checklist.md

詳細

MSW スクリプティング(.mlua)— フレームワーク+ファイルワークフロー+プレイテストとデバッグ

mlua は Lua ベースですが、MSW 固有のアノテーション、ライフサイクル、実行空間モデルを備えています。 一般的な Lua の知識だけでは、動作するコードは書けません。すべての作業はワークスペース内のファイルを直接編集して行い、 コードはビルドログ → ランタイムログの順に検証します。


1. 基本原則(必ず遵守)

1.1 まず既存のスクリプトを確認する

新しい .mlua を作成する前に、./RootDesk/MyDesk/ 以下を glob/キーワード検索し、同じ目的の既存スクリプトがないか確認してください。既存ファイルの拡張を常に第一の選択肢とします。実装が重複すると、保守コストと競合リスクが高まります。

1.2 新しいスクリプトのフォルダー構成 — ファイルをルートにそのまま置かない

新しい .mlua が避けられない場合は、機能/カテゴリ別のサブフォルダーに配置してください。必須のパス形式:./RootDesk/MyDesk/<FeatureFolder>/<ScriptName>.mlua。

  • 適合する既存のサブフォルダー(Player/、UI/、Combat/、Inventory/ など)があれば再利用してください。まず ./RootDesk/MyDesk/ を glob で検索します。
  • 適合するものがなければ、機能名を付けたフォルダー(PascalCase)を作成してください。1 つの機能に関連するすべてのスクリプト(Component/Logic/Event/Struct)は、同じフォルダーにまとめます。単一ファイルの機能にも専用フォルダーが必要です。
  • 禁止:Scripts/、Misc/、Common/、New/、temp/ のような汎用フォルダー。ルートにファイルを直接置くと、ルール §1.1 の「作成前に検索する」ことができなくなります。

例:Inventory/InventoryManager.mlua、Combat/MeleeAttackComponent.mlua、UI/Popup/RewardPopupLogic.mlua。

1.3 API を推測しない — 書く前に検証する

MSW API の名前/パラメーター/戻り値の型を推測すると、実行時にエラーが出ずに失敗します。必須の順序:シグネチャは .d.mlua で確認 → 必要に応じて意味や例を msw-search で確認 → 記述 → LSP で診断(自動実行)。

エンジン API は ./Environment/NativeScripts/ 以下にあります:

フォルダー内容数
Component/エンジンコンポーネント104
Service/システムサービス46
Event/イベント型202
Logic/組み込みロジック9
Enum/列挙型118
Misc/ユーティリティ型(Vector2 など)140

名前が分かっている場合 → ./Environment/NativeScripts/{folder}/{name}.d.mlua を読みます。名前が不明な場合 → そこでキーワードを Grep します。

1.4 リント(LSP 診断)

mlua-diagnose フックが .mlua の作成/変更後に LSP の diagnose を自動実行します。エラー重大度の診断がゼロになるまで、修正 → 再編集を繰り返してください。

1.5 .codeblock とリフレッシュ
  • .codeblock ファイルは Maker Refresh によって生成されます。手動で作成/編集/削除しないでください。
  • .mlua の作成/変更/名前変更/削除後は、Maker MCP の refresh を呼び出してください。Refresh には編集モードが必要です。プレイ中の場合は、先に stop してください。
1.6 MSW ≠ Unity — 直感で判断しない

Unity/一般的なパターンをそのまま適用すると、コンパイルは正常に完了しますが、実行時に気付かないまま失敗します。よくある誤解:

Unity の直感MSW の実際/記載箇所
グローバルマネージャーから gameObject / transform を取得する@Logic に self.Entity はありません — §3.2 を参照してください(プロパティ注入/_EntityService を使用します)
クリックに OnMouseDown / BoxCollider2D を使う物理コライダーは TouchEvent を発生させません — World では TouchReceiveComponent(§10)、UI では ButtonComponent/UITouchReceiveComponent を使用します
OnCollisionEnter + Rigidbodyエンティティ間の衝突には TriggerComponent + TriggerEnter/Leave/Stay イベントが必要です
UI フィールド名(interactable/text/color)MSW 固有の名前です — msw-ui-system/references/component-api.md を確認してください。よくある対応関係:disable→Enable、text→Text、text color→FontColor、tint→Color。ButtonComponent.Interactable は存在しません。
Rigidbody/Collider を自由に複数アタッチするマップタイプごとに Body は 1 つだけです — msw-general/references/platform.md §4 を参照してください
サーバーコードから UI を操作するUI はクライアント専用です — サーバーから UI への処理は @ExecSpace("Client") RPC 経由で行います。UI にアタッチされた Component で Server/ServerOnly/Multicast/@Sync を使用すると、実行時警告が出て何も行われません。詳細は msw-ui-system/references/runtime-patterns.md を参照してください
Instantiate(prefab) をどこからでも呼び出せる_SpawnService:SpawnByModelId(id, name, pos, parent) を使用します — parent は必須で、サーバー専用です — §11 を参照してください
static クラス/手作りのシングルトン@Logic 自体がシングルトンです — _ScriptName:Method() として呼び出し、インスタンス化しないでください — §3.2 を参照してください

ルール:Unity のパターンを適用したくなったら、いったん止めて、まず Environment/NativeScripts/*.d.mlua で確認してください。

1.7 ビルダープロトコルの事前確認 — 必須

このターンで .map/.model/.ui に触れる場合(直接、または .mlua のスポーン/エンティティ配置/UI バインドコード経由のいずれも)、../msw-general/references/builder-protocol.md(コア)と、対象タイプごとのビルダーファイル(builder-protocol-map.md/builder-protocol-model.md/builder-protocol-ui.md)を、あらかじめすべてコンテキストに読み込んでおく必要があります(このセッションでまだ読み込んでいない場合、または圧縮によって失われた場合に限り、ファイル全体を Read してください。記憶した要約はコンテキストに読み込んだことにはなりません)。コアには共通の書き込み契約とフロー間の連携が記載されています。各ビルダーファイルには、そのビルダーの API、typeKey メタデータ、自動 lint、子エンティティの不変条件、placeModel のミラーリングが記載されています。あるビルダーについて知っていても、別のビルダーについて把握したことにはなりません。

該当するケース(意図的に広く設定しています):_SpawnService/SpawnByModelId/SpawnByEntity、.map/.model/.ui の変更、msw_map_builder.cjs/msw_model_builder.cjs/msw_ui_builder.cjs の呼び出し、「新しいモンスター/NPC/ポップアップ/マップオブジェクト」の依頼、§11 または §16 の作業。

1.8 メソッドのドキュメントコメント — 本体の内側

すべての method(ライフサイクル、RPC、イベントハンドラー、ユーザー定義メソッド)には、宣言の上ではなく、本体の最初の行に説明コメントが必要です。mlua のパーサーは先行するコメントを直前の宣言に紐付けるため、「上に置いた」コメントは確実に機能しません。

-- ✅ Correct
method void ApplyDamage(Entity target, number amount)
    -- Applies damage and triggers hit VFX.
    target:TakeDamage(amount)
end

-- ❌ Wrong — comment above the method
-- Applies damage...
method void ApplyDamage(Entity target, number amount)
    target:TakeDamage(amount)
end

2. パスとファイルの役割

対象パスエージェントの操作
ユーザースクリプト./RootDesk/MyDesk/**/*.mlua直接作成/読み取り/変更/削除
自動生成アーティファクト*.codeblock触らないでください(Refresh が管理します)
エンジン API 定義./Environment/NativeScripts/**読み取り専用(変更しないでください)
モデル(コンポーネント一覧)./RootDesk/MyDesk/**/*.model および既存の ./Global/*.model ファイルスクリプトをアタッチするときに Components を編集
マップインスタンス./map/*.mapマップ内にしか存在しないエンティティにスクリプトをアタッチするときに編集

3. スクリプトの種類と宣言

3.1 コンポーネントスクリプト(@Component)

エンティティにアタッチするスクリプトです。所有するエンティティにアクセスするには self.Entity を使います。

@Component
script MyScript extends Component
    property number Speed = 5.0

    @ExecSpace("ServerOnly")
    method void OnBeginPlay()
        -- initialization (also: OnUpdate(delta), OnEndPlay)
    end
end

許可される親:

  • Component — 汎用コンポーネント
  • AttackComponent — 攻撃システム(Shape、AttackFast、OnAttack)
  • HitComponent — 被弾システム(OnHit、HandleHitEvent)
3.2 ロジックスクリプト(@Logic)

グローバルシングルトンです。エンティティなしで独立して実行されます。ゲームマネージャー、UI マネージャー、ユーティリティなどに使います。

@Logic
script GameManager extends Logic
    @Sync property integer Score = 0

    @ExecSpace("ServerOnly")
    method void OnBeginPlay()
        -- global initialization (also: OnUpdate, OnEndPlay)
    end
end
  • ワールドにつき 1 つ(シングルトン)
  • _<ExactScriptName> としてアクセスします。接尾辞は削除されません。TDHUDLogic.mlua → _TDHUDLogic(_TDHUD ではありません)、TowerDefenseConfig.mlua → _TowerDefenseConfig です。接尾辞を削除する推測処理は、警告なしに nil を返します。
  • @Sync プロパティ(サーバー→クライアント)をサポートします
  • Logic の OnUpdate は Component の OnUpdate より先に実行されます。

⚠️ @Logic には self.Entity がありません — Logic の親が公開するのは ConnectEvent/DisconnectEvent/IsClient/IsServer/SendEvent のみです。self.Entity.xxx はコンパイルできますが、実行時に nil アクセスが発生します。ワールドエンティティにバインドするには、プロパティ(property Entity x = "uuid"/property EntityRef x = "")で注入するか、_EntityService:GetEntityByPath(...)/:FindEntityByName(...) で検索してください。UUID リテラルによるプロパティ注入を推奨します。§7 を参照してください。

⚠️ @Logic では OnMapEnter/OnMapLeave は実行されません — これらは Component 専用です(§5 を参照)。Logic で宣言しても、警告なしに実行されないコードになります。マップごとのセットアップ/クリーンアップを行うには、次のいずれかを使ってください。(1)マップエンティティ上の @Component に動作を移す(推奨。サーバー上で機能する唯一の方法)、または(2)クライアント側のみ:OnUpdate から _UserService.LocalPlayer.CurrentMap をポーリングする — LocalPlayer が利用できるのはクライアントのみです(サーバーでは nil)。Logic はマップ遷移後も存続するため、マップごとにリセットすべきタイマー/イベントハンドラー/可変状態は、これらの方法のいずれかでクリアする必要があります。自動フックはありません。

判断基準:@Component と @Logic —「グローバルか」ではなく存続期間で選ぶ

スコープ選択肢理由
ワールド全体で、すべてのマップ遷移後も存続する(アカウント状態、ワールドイベントバス、グローバル UI マネージャー)@Logicエンジンのシングルトンで、ワールドセッション全体を通して存続します。
マップ単位 — 1 つのマップ内でのみ意味を持つ(クエストコントローラー、ウェーブスポナー、パズル)マップエンティティ上の @Componentマップのアンロード時にクリーンアップされます。これを @Logic に置くと、状態/タイマーがマップ間で残ります。
1 つのアクター(モンスター AI、アイテムの拾得、プレイヤースキル)そのエンティティ上の @Component

次のように考えてください。「プレイヤーが別のマップに移動した後も実行され続けるか?」 — はい ⇒ @Logic、いいえ(このマップ)⇒ マップエンティティ上の @Component、いいえ(このアクター)⇒ アクター上の @Component。

3.3 拡張スクリプト
@Component
script PlayerAttack extends AttackComponent
    -- Override parent methods; call parent via __base:MethodName()
end
3.4 その他のスクリプトの種類

@Event(カスタムイベント) · @Item(インベントリ) · @BTNode(ビヘイビアツリー) · @State(ステートマシン) · @Struct(複合データ型)。


4. mlua 言語拡張(通常の Lua との違い)

Lua 5.3 をベースに、以下の違いがあります。

追加された構文:

  • continue — 次のループ反復に進みます。
  • 複合代入:+=、-=、*=、/=、//=、%=、^=、..=(およびビット演算の&=、|=、<<=、>>=)。複数代入(a, b += 1, 2)や関数の引数としての使用(print(a += 1))は無効です。
  • ビット演算子:&、|、<<、>>。

制約:

  • グローバル変数は使えません(global キーワードは禁止)— 値は Properties を介して共有します。
  • コルーチンは使えません(coroutine.*)。
  • 親クラスの呼び出しには super ではなく __base:MethodName() を使います。

組み込みユーティリティ関数:

関数用途
log() / log_warning() / log_error()各レベルのログ出力
wait(seconds)スクリプトの実行を一時停止します
isvalid(obj) → boolean有効性(削除や nil に対応)
enum(t) → tableキーと値を入れ替えます
beginscope(name) / endscope()プロファイリングスコープ

5. ライフサイクル

OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroy
                                ↑
                  OnMapEnter / OnMapLeave (Component only, per transition)
メソッドタイミング場所用途
OnInitialize作成後Component + Logic内部変数を初期化します(ほとんど使用しません)
OnBeginPlayゲーム開始時Component + Logicイベントの接続、タイマーの開始、初期設定
OnUpdate(delta)毎フレームComponent + Logic(Logic が先)移動、アニメーション、入力
OnMapEnter / OnMapLeaveマップ遷移時Component のみ(Logic では何も行わずに終了)マップごとの初期化/クリーンアップ
OnEndPlayゲーム終了時Component + Logicイベントの切断、タイマーのクリア(必須!)
OnDestroy削除時Component + Logic最終クリーンアップ(ほとんど使用しません)

クライアントでは、OnMapEnter はローカルプレイヤー自身の入場時だけでなく、すでにマップ内にいる他のプレイヤーに対しても呼び出されます。

必須パターン:OnBeginPlay で接続したものはすべて、OnEndPlay で解放する必要があります(イベント、タイマー)。

property any eventHandler = nil   -- EventHandlerBase (must be 'any'; not integer)
property integer timerId = 0

method void OnBeginPlay()
    self.eventHandler = self.Entity:ConnectEvent(SomeEvent, self.OnSomeEvent)
    self.timerId = _TimerService:SetTimerRepeat(self.Tick, 1/60)
end
method void OnEndPlay()
    if self.eventHandler then self.Entity:DisconnectEvent(SomeEvent, self.eventHandler) end
    if self.timerId then _TimerService:ClearTimer(self.timerId) end
end

OnMapEnter と OnMapLeave は対になります — マップ遷移時のタイマーリーク。 OnMapEnter はマップに再入場するたびに呼び出されます。また、遷移後もエンティティが存続する Component(プレイヤー、永続オブジェクト)は、マップ退出時に破棄されません。そのため、OnMapEnter で開始したタイマー/ハンドラーは、再入場のたびに有効なコピーが 1 つずつ積み重なります。OnEndPlay ではなく OnMapLeave でクリアしてください(OnEndPlay は最終的な削除時にのみ呼び出されるため、遷移の間には実行されません)。登録されたままの繰り返しタイマー(SetTimerRepeat、または SetTimer(..., isRepeat = true))は、次回のプレイモード開始時にエンジンから [LEA-3051] MemoryLeak として報告され、作成行が示されます。1 回限りのタイマー(SetTimerOnce)は発火後に自動的にクリアされるため、警告されることはありません。ルール:OnMapEnter で開始するものはすべて、OnMapLeave でクリアする必要があります。


6. 実行空間(ExecSpace)

MSW はサーバー・クライアントアーキテクチャです。すべてのメソッドで実行場所を宣言する必要があります。

ExecSpace実行場所方向用途
ServerOnlyサーバーサーバー内部のみダメージ計算、状態変更、スポーン
ClientOnlyクライアントクライアント内部のみUI 更新、エフェクト、サウンド
Serverサーバークライアント→サーバー RPCクライアントからサーバーへのリクエスト(攻撃、アイテム使用)
Clientクライアントサーバー→クライアント RPCサーバーからクライアントへの通知(結果 UI、エフェクト)
Multicastすべてのクライアントサーバー→すべてのクライアントグローバルイベント(アナウンス、ボスのスポーン)
(未指定)呼び出し元の側サーバー→サーバー、クライアント→クライアントどちらの側でもローカルで実行される共有関数
ライフサイクルメソッドの ExecSpace 制約
メソッド許可される ExecSpace
OnSyncPropertyClientOnly のみ
OnInitialize、OnBeginPlay、OnUpdate、OnEndPlay、OnDestroy、OnMapEnter、OnMapLeaveServerOnly、ClientOnly、または未指定
すべてのイベントハンドラーServerOnly、ClientOnly、または未指定
カスタムユーザーメソッドServer、Client、ServerOnly、ClientOnly、Multicast のいずれか
一般的なサーバー・クライアントのパターン
[Client]  input (ClientOnly) ──Request()──→ [Server] validate (ServerOnly)
                                                ├─ state auto-syncs via @Sync
[Client]  UI update (ClientOnly) ←──Show()──────┘ (Client RPC)
  • ServerOnly:クライアントからの呼び出しは黙って無視されます(エラーは発生しません)。
  • Server:クライアント→サーバーの RPC(ネットワーク遅延が発生します)。
  • Client:サーバー→クライアントの RPC。特定のクライアントを対象にするには、呼び出し側の最後の引数として UserId を追加します(宣言には追加しないでください)。
senderUserId — リクエスト元の検証

@ExecSpace("Server") の本体内では、ローカル変数 senderUserId に呼び出し元クライアントの UserId が格納されます(サーバーが割り当てるため、クライアントは変更できません)。セキュリティチェックに使用してください。

@ExecSpace("Server")
method void RequestBuyItem(integer itemId)
    if senderUserId ~= self.Entity.PlayerComponent.UserId then return end
    self:ProcessPurchase(itemId)
end
予約済みのパラメーター名 — name is unavailable

4 つのパラメーター名は RPC マーシャラー用に予約されており、@ExecSpace(...) メソッドで独自のパラメーター名として使用できません。LSP はスクリプトを '<name>' name is unavailable. と表示してブロックします。

予約名エンジンでの用途
selfメソッドのレシーバー
senderUserId@ExecSpace("Server") の本体における呼び出し元クライアントの UserId
targetUserId受信側クライアントの UserId(@ExecSpace("Client") の本体では呼び出し側の最後の引数。宣言しないでください。エンジンが追加します)
messageOwnerEntity一部のサービスコールバックの発生元エンティティ

独自のパラメーター名が予約名と衝突する場合は、名前を変更してください(targetUserId → forUserId、senderUserId → fromUserId)。self はレシーバーであり、別名にできません。無関係なパラメーターには別の名前を選んでください。

手動分岐 — IsServer() / IsClient() はプロパティではなくメソッドです

@ExecSpace のないメソッド(呼び出した側に応じて実行される)で、サイドごとに異なる処理が必要な場合は、self:IsServer() / self:IsClient() で分岐します。どちらも Component と Logic で method boolean IsServer() / method boolean IsClient() として宣言されているため、値を参照するのではなく呼び出す必要があります。

if self:IsServer() then ... end   -- ✅ method call → boolean
if self.IsServer    then ... end  -- ❌ method object itself → always truthy

ドットを使い、かっこを省略する形式は、静かにバグを引き起こします。LSP は警告せず、スクリプトはコンパイルされますが、メソッドオブジェクトが真として評価されるため、「if」の条件が常に成立し、クライアント専用のコードもサーバーで実行されます(またはその逆)。クラッシュではなく、「両方の分岐が両方のサイドで実行される」という症状が現れます。必ずコロン呼び出し(self:IsServer())を使用してください。

境界を越えて渡せるパラメーター型

サーバー↔クライアントの RPC で使用できる型:string、integer、number、boolean、table、Vector2/3/4、Color、Entity、Component、EntityRef、ComponentRef。any は使用できません。 エンジンの列挙型も境界を越えられません。型指定して渡す場合は LSP がエンジンの列挙型をパラメーターとして拒否し、any を介して渡す場合も実行時に LEA-3036 InvalidCast が発生します。一般的な回避策は、送信側で選択肢を string キーとしてエンコードし、受信側で分岐してローカルで列挙型に変換することです。SyncTable<k,v> の型引数も、許可された型のリストから選ぶ必要があります。


7. プロパティシステム

基本型

number(float/double — 整数は別の型 integer)、string、boolean、Vector2/Vector3、Color(r、g、b、a は 0.0~1.0)、any。

property number Speed = 5.0
property integer Count = 0
property Vector2 Direction = Vector2(0, 0)
property Color Tint = Color(1, 1, 1, 1)
Entity / Component 参照プロパティ
property Entity targetEntity = "94a274e4-4111-40f1-924d-c95a3a1f14d5"   -- UUID string literal
property ButtonComponent btnOk = "uuid-string"                          -- typed component ref

AI は UUID を直接挿入する必要があります — .map/.ui から id を読み取り、文字列リテラルとしてハードコードしてください。エディター上でユーザーにドラッグしてバインドさせないでください(これは人間が作成する際の利便性のための機能です)。

Entity と EntityRef の違い

Entity / Component 参照は、マップの切り替え時に破棄されます。EntityRef / ComponentRef はマップを切り替えても維持されます。複数のマップを使用するゲームでは、こちらを優先してください。

同期アノテーション
  • @Sync — サーバー → すべてのクライアント。一方向です。クライアント側での変更はサーバーに伝播しません。ネットワーク遅延が発生します。
  • @TargetUserSync — サーバー → 所有ユーザーのクライアントのみ。通貨や実績など、プレイヤーごとの非公開データに便利です。PlayerEntity 以外では、通常の @Sync にフォールバックします。
  • 同期できないもの:any、table — 代わりに SyncTable を使用してください。
  • どちらも引数は不要です。
@Sync property number CurrentHp = 100
@TargetUserSync property number PrivateScore = 0
@Sync property SyncTable<number> Scores              -- array form, NO default literal
@Sync property SyncTable<string, number> Stats       -- dict form, NO default literal
SyncTable<...> プロパティ — デフォルトのリテラルは指定しない

SyncTable<V>(配列形式)または SyncTable<K, V>(辞書形式)を、= ... 初期化子なしで宣言します。エンジンは SyncTable プロパティの = スロットを型情報の管理用に予約し、実行時にプロパティを空のコレクションとして自動初期化します。リテラル(= {}、= { key = val }、= nil)を記述しても黙って破棄されます。これは誤解を招く不要な記述であり、実際のデフォルト値ではありません。また、コードブロックを経由して再保存すると削除されます。

初期エントリーは OnInitialize / OnBeginPlay で設定します。

@Sync property SyncTable<string, number> Stats       -- empty at construction
@Sync property SyncTable<number> Scores              -- empty at construction

method void OnBeginPlay()
    if self:IsServer() then
        self.Stats["hp"] = 100
        self.Stats["mp"] = 50
        self.Scores:Add(0)
    end
end

実行時に通常の Lua テーブルを SyncTable プロパティに代入することも拒否されます。このプロパティが受け付けるのは専用のプロキシだけです。フィールド単位で変更する(self.Stats[k] = v)か、メソッド(self.Scores:Add(v) / :Remove(v) / :Clear())を呼び出してください。

SyncList<V> はユーザープロパティ型ではありません

SyncList<V> は、ネイティブエンジンの Component 上の読み取り専用プロパティとしてのみ公開されます(例:TagComponent.Tags、PhysicsColliderComponent.PolygonPoints、SkeletonRendererComponent.AnimationNames、各種 JointComponent.Joints)。ユーザースクリプトからは、これらを読み取り、メソッド(:Add(v)、:Remove(v)、:Clear()、.Count、:ToTable())を呼び出せますが、独自の @Component / @Logic で property SyncList<...> X を宣言したり、SyncList(...) をインスタンス化したりすることはできません。

独自スクリプトで同期コレクションを使う場合は、SyncTable<V>(配列形式)または SyncTable<K, V>(辞書形式)を使用してください。上記を参照してください。

一時プロパティ(_T)

self._T.<name> は、宣言なしで使用できる非同期のアドホック状態です。サーバーとクライアントはそれぞれ独自の値を保持します。インスペクターには表示されません。@Sync を付けることはできません。

⚠️ 宣言なしで使えるフィールドは _T だけです。 それ以外の未宣言の self.<name> に代入すると、実行時エラー cannot set <name>, no such field が発生し、呼び出し元のメソッド(通常は OnBeginPlay 全体)が停止します。設定するすべての self.<name> は、property として宣言するか、self._T.<name> を通じて使用する必要があります。ビルドログには、見落としやすい LIA-1114 Info(§17.2 を参照)以外は出力されません。

OnSyncProperty コールバック

@Sync プロパティの同期が完了したときに呼び出されるクライアント側のフックです。ClientOnly が必須です(変更できません)。Component と Logic で使用できます。同期設定が None のプロパティについては呼び出されません。

@ExecSpace("ClientOnly")
method void OnSyncProperty(string name, any value)
    if name == "CurrentHp" then self:UpdateHpBar(value) end
end
プロパティエディター属性
@DisplayName("...") @Description("...") @MaxLength(20) @HideFromInspector
@MinValue(0) @MaxValue(999) @Delta(5)   -- Delta = mobile +/- step

8. イベントシステム / RPC

静的サブスクリプション — @EventSender + handler
@EventSender("Self") handler HandleHitEvent(HitEvent event) ... end
@EventSender("Service", "InputService") handler HandleKeyDown(KeyDownEvent event) ... end

@EventSender の第 1 引数:"Self" / "LocalPlayer"(第 2 引数なし) · "Entity"、id / "Model"、id / "Service"、typeName / "Logic"、typeName。

動的サブスクリプション — ConnectEvent / DisconnectEvent
property any clickHandler = nil
self.clickHandler = entity:ConnectEvent(ButtonClickEvent, self.OnClick)  -- OnBeginPlay
entity:DisconnectEvent(ButtonClickEvent, self.clickHandler)              -- OnEndPlay (mandatory)

要素ごとに状態(カード ID、スロットインデックス)を保持する場合は、クロージャハンドラーを使用します。返された EventHandlerBase をテーブルに保存し、OnEndPlay でそれぞれ切断してください。

property table clickHandlers = {}

for _, id in ipairs(cardIds) do
    local capturedId = id
    local h = e:ConnectEvent(ButtonClickEvent, function() self:OnCardClicked(capturedId) end)
    table.insert(self.clickHandlers, { entity = e, handler = h })
end

⚠️ ConnectEvent は Entity / Logic / Service にあります。Component にはありません。 Component はイベントを送信するだけです。サブスクリプションは、それを所有する Entity(または _InputService / _<LogicName>)で行ってください。self.Entity.ButtonComponent:ConnectEvent(...) は実行時に nil になります。

property any clickHandler = nil
property any keyHandler = nil
self.clickHandler = self.Entity:ConnectEvent(ButtonClickEvent, self.OnClick)
self.keyHandler   = _InputService:ConnectEvent(KeyDownEvent, self.OnKeyDown)

⚠️ handler と method void の使い分け — handler Name(Ev e) は @EventSender(...) と組み合わせ、宣言によって接続されます。method void Name(Ev e) は、ConnectEvent(EvType, self.Name) で接続する動的コールバックです。混在させてもコンパイルは通りますが、イベントは発火しません(E-V1-5)。@EventSender がある場合は handler を使い、ConnectEvent を呼び出す場合は method void を使ってください。

CustomEvent — 型付きクラス形式

作成する方法は、@Event と extends EventType を組み合わせ、property フィールドを定義する方法だけです。インラインファクトリーはありません。

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

local dmg = DamageDealtEvent(); dmg.amount = 50
self.Entity:SendEvent(dmg)                                 -- via Entity / Logic / Service
self.Entity:ConnectEvent(DamageDealtEvent, self.OnDamage)  -- first arg = event Type

method void OnDamage(DamageDealtEvent event) log(event.amount) end

NativeEvent(エンジン提供。例:HitEvent.TotalDamage/.AttackerEntity、ButtonClickEvent、StateChangedEvent.PrevState/.CurState)については、Environment/NativeScripts/Event/ を参照してください。


9. 有効性チェックとメソッドのオーバーライド

有効性チェック

削除済みのエンティティにアクセスすると実行時エラーになります。必ず最初にisvalid()を確認してください。

if isvalid(entity) then ... end
if isvalid(self.Entity.SomeComponent) then ... end
メソッドのオーバーライド

extendsするスクリプトで、親と同じシグネチャを持つmethodを定義すると、そのメソッドがオーバーライドされます。---@sealedとマークされた組み込みエンジンメソッドはオーバーライドできません。親の元のメソッドは__base:MethodName(args)で呼び出します。

⚠️ LEA-3014 SignatureMismatch — ExecSpaceは親と一致させる必要があります

「同じシグネチャ」には@ExecSpaceも含まれます。オーバーライドのアノテーションブロックは、親のものと完全に一致させる必要があります。アノテーションがない場合は、その状態も含めて一致させてください。親にアノテーションがないのに「サーバー側で実行するため」に@ExecSpace("ServerOnly")を追加すると、実行時にLEA-3014が発生します。

よくある問題:AttackComponent / HitComponentのダメージフック(CalcDamage、CalcCritical、GetCriticalDamageRate、GetDisplayHitCount、IsAttackTarget、IsHitTarget、OnAttack)は、すべて@ExecSpaceなしで宣言されています。アノテーションなしでオーバーライドしてください。サーバー側のヒット処理だけが呼び出すため、安全です。

手順:.d.mlua(§1.3)で親を確認し、アノテーションブロックをそのままコピーしてください。LEA-3014を修正するには、子の@ExecSpaceを親に合わせてください。逆に親を変更してはいけません。


10. 入力/クリックイベント — ワールドとUI(混同しないでください)

ワールドのタッチ — 2つの方法
方法イベント接続先用途
エンティティのタッチ — エンティティにTouchReceiveComponentを設定TouchEvent(+Hold/Release)entity:ConnectEvent(...)「どのエンティティに触れたか」— NPC、アイテム
画面のタッチ — コンポーネントなしScreenTouchEvent_InputService:ConnectEvent(...)「画面のどこか」— 配置、移動先

どちらのイベントもTouchId(int32)とTouchPoint(Vector2の画面座標)を持ちます。ワールド座標を取得するには、_UILogic:ScreenToWorldPosition(event.TouchPoint)を使います。UIのクリックを除外するには、_InputService:IsPointerOverUI()を使います。TouchEventが機能しない場合、設定不要の代替手段としてScreenTouchEventとScreenToWorldPositionを使えます。

⚠️ 物理コライダーはTouchEventを発生させません — BoxCollider2D、CircleCollider2D、Rigidbody/Kinematicbody、TriggerComponentはいずれもタッチ入力を受け取りません。TouchEvent/TouchHoldEvent/TouchReleaseEventを発生させるのは**TouchReceiveComponent**だけです。

設定:最も簡単なのはAutoFitToSize = true(TouchAreaのサイズをSprite/Avatarのスケールに自動調整)にする方法です。手動で設定する場合は、TouchAreaをスプライトより10~20%大きくしてください。RelayEventToBehind = true(デフォルト)はイベントを後方に伝達します。ブロックする場合に限りfalseに設定してください。

イベントが発生しない場合は、次の順に確認してください:(1) TouchReceiveComponentが実際に追加されている(.map/.model内);(2) TouchAreaがゼロ以外で、エンティティが画面に表示されている;(3) 前方のエンティティがRelayEventToBehind = falseでイベントをブロックしていない;(4) ハンドラーがproperty anyに保存されている(保存されていないとGCで解放されます)。

選択の目安:「どのエンティティに触れたか」→ TouchEvent、「画面のどこか」→ ScreenTouchEvent。

PCのマウスボタン(左/右/中央) — ScreenTouchEvent.TouchId == 2ではなくKeyDownEventを使う

PCではScreenTouchEventは左ボタンでのみ発生します(TouchId == 1)。TouchId == 2はモバイルの2本指タッチです。これを使って右クリックを読み取る方法はMakerシミュレーターでは動作しますが、実際のPCでは入力が一切検出されません。PCのマウスボタンには_InputService:ConnectEvent(KeyDownEvent, ...)を使い、event.key == KeyboardKey.Mouse0/Mouse1/Mouse2で分岐してください(左=323、右=324、中央=325)。モバイルのマルチタッチとPCの両方に対応するには、ScreenTouchEventとKeyDownEventの両方に接続します。イベントが二重に発生することはありません(モバイルには右クリックがなく、PCではTouchId == 2が発生しません)。

UIのクリック

UIエンティティ(./ui/*.ui、uiツリー)には、**ButtonComponentとButtonClickEvent**を使います。ワールドオブジェクトにUIイベントを設定したり、その逆を行ったりしても、何も起こりません。まず、対象がワールドオブジェクトなのかUIパネルのボタンなのかを判断してください。


11. マップコンテキストとエンティティのスポーン

§1.7 トリガー — スポーン/.map/.model の作業を行う前に、builder-protocol.md と、対応するビルダー別プロトコルファイルを Read してください。

子エンティティの走査

「マップ内のすべての X」や「Y という名前の子」などの問い合わせには、Entity の検索ツールキットを使用してください。

メンバー戻り値用途
Entity.ChildrenReadOnlyList<Entity>(反復するには :ToTable() を呼び出します)直下の子
Entity:GetChildByName(name, recursive=false)Entity名前で検索
Entity:GetChild(id, recursive=false)EntityUUID で検索
Entity:GetChildComponentsByTypeName(typename, recursive=false)table<Component>条件に一致するすべての子孫
Entity:GetFirstChildComponentByTypeName(typename, recursive=false)Component最初に一致したもの
local map = self.Entity.CurrentMap                              -- prefer this over service lookup
local units = map:GetChildComponentsByTypeName("script.MyUnit", false)
for _, child in ipairs(self.Entity.Children:ToTable()) do log(child.Name) end

コレクション名は Children です。ChildList/Childs/GetChildren() は誤りです(コンパイル時エラー、実行時に nil、LIA-1114 Info)。実行時にスポーンしたエンティティを検索可能にするには、CurrentMap の子として設定する必要があります。

ネイティブコンポーネントとユーザーコンポーネントへのアクセス
アクセス方法対象
entity.SomeComponent(ドット)エンジンネイティブのみ(TransformComponent、ButtonComponent など)
entity:GetComponent("script.MyUnit")ユーザーの @Component(任意)
entity:GetFirstChildComponentByTypeName("script.MyUnit", true)子孫にあるユーザーの @Component

ユーザーの @Component の typename は常に "script.<FileBaseName>" です。MyUnit.mlua → "script.MyUnit" となり、feature フォルダーの階層には左右されません。entity.MyUnit(ドット)は nil を返し、LIA-1114 が発生します。スクリプト間でユーザーコンポーネントへの参照を渡すには、型付きプロパティ(property MyUnit unit = "")を宣言し、UUID を注入してください。

⚠ メソッド名は GetComponent です(GetComponent(Type) と GetComponent(string typename) のオーバーロードです。Environment/NativeScripts/Misc/Entity.d.mlua を参照)。*ByTypeName サフィックスがあるのは子エンティティ用のバリアント(GetChildComponentsByTypeName/GetFirstChildComponentByTypeName)のみです。

GetComponent(string) の戻り値は抽象型の Component であるため、戻り値のメンバーアクセスは動的ディスパッチになり、LSP は LIA-1114 Info を出します(または、具体的なユーザー @Component を引数に取る関数へ値を渡すと type mismatch Error が発生します)。静的型付けを復元するには、---@type でキャストしてください。

---@type MyUnit
local unit = self.Entity:GetComponent("script.MyUnit")
unit:DoSomething()  -- LSP now type-checks against MyUnit
実行時のスポーン
  • _SpawnService:SpawnByModelId(id, name, pos, parent) を使用してください。parent は必須です(デフォルト値はありません)。self.Entity.CurrentMap を渡します。SpawnByEntity は異なり、parent = nil が許可されます。
  • .model テンプレートは事前に存在している必要があります。新しいオブジェクトの作成手順は、.model を作成 → マップ上にスポーンまたは配置です。
Body コンポーネントと Position の直接書き込み

Body(Kinematic/Rigid/Sideview)を持つエンティティは、TransformComponent.WorldPosition への直接書き込みを無視します。物理演算により、次のフレームで上書きされます。代わりに次を使用してください。

  • フレームごとの移動:MovementComponent:MoveToDirection(dir, dt)
  • テレポート(ローカル):MovementComponent:SetPosition(pos) または body:SetPosition(Vector2)
  • テレポート(ワールド):body:SetWorldPosition(Vector2) — RectTile マップ上の Kinematicbody に対する標準的な絶対位置指定
  • Transform への直接書き込みが可能なのは、Body を持たないエンティティ(装飾、エフェクト)のみです。

回避策として Body を削除してはいけません。タイルとの衝突および進入/退出イベントが無効になります(NativeIssue_MissingComponent)。


12. よく使用するサービス/ロジック

すべてのサービスとロジックには、_Name(アンダースコア+型名)を使ってアクセスします。最も一般的なもののみを記載しています。

サービス/ロジック用途
_SpawnServiceエンティティを生成します(SpawnByModelId、SpawnByEntity)。Despawn メソッドはありません。生成したエンティティは Entity:Destroy()/Entity:Destroy(delaySeconds) で削除してください(どちらも ControlOnly)。
_TimerServiceタイマー(SetTimer、SetTimerRepeat、ClearTimer)
_EntityServiceエンティティの検索(GetEntity、GetEntities、GetEntitiesByPath)
_UserServiceプレイヤーの検索(GetUsersByMapComponent(map.MapComponent) は、指定されたマップ上に現在いるすべてのプレイヤーを返します。これは「このマップ上のプレイヤーを検索する」標準的な呼び出しであり、Soldier の FindNearestPlayer で使用されています)。ユーザーがいない場合は nil を返します。
_InputService入力状態の問い合わせ。ScreenTouchEvent を受信します。
_ResourceServiceリソースの RUID を検索します。LoadAnimationClipAndWait(ruid) は AnimationClip を同期的に読み込みます(1 フレーム分処理がブロックされるため、結果をキャッシュしてください。ブロックを避けたい場合は、_ResourceService:PreloadAsync({ruid}, function() ... end) でラップします)。
_DataStorageService永続データ(プレイヤーのセーブデータ) — ⚠️ 利用料金が発生します。OnUpdate/短いタイマー内では呼び出さず、ループでは Batch* を使用してください。詳細:references/datastorage.md
_UtilLogic乱数、時間、文字列、数学のユーティリティ
_TweenLogicトゥイーンアニメーション(MoveTo、ScaleTo、RotateTo)
_UILogicUI 座標の変換(例:ScreenToWorldPosition) — ClientOnly

一覧の全体については、.d.mlua ファイルを直接確認してください:./Environment/NativeScripts/Service/(46 ファイル)および ./Environment/NativeScripts/Logic/(9 ファイル)。ドメインの詳細は、msw-search で検索してください。

_ 接頭辞なしでアクセスする組み込みグローバル

上記の _Name ルールが適用されるのは、サービスとロジックのみです。一部の組み込み機能は通常のグローバルとして公開されています。先頭にアンダースコアを付けてアクセスすると、実行時エラー(nil 参照)が発生します。

グローバル(正しい形式)誤った形式用途
Environment❌ _Environment実行環境の問い合わせ — Environment:IsMakerPlay()/IsMakerEdit()/IsPlay()/IsPublishedPlay()/IsMobilePlatform()/IsPCPlatform()、WorldId プロパティ。GetApplicationVersion() は ClientOnly です(サーバーでは nil)。
CollisionGroups❌ _CollisionGroupsグループ名をキーとする CollisionGroup オブジェクトのテーブル — CollisionGroups.HitBox、.Monster、.Player など。組み込みグループ:Default/TriggerBox/HitBox/Interaction/Portal/Climbable に加え、プロジェクトで定義したグループ。各エントリーは .Id(文字列)と :GetCollideGroups() を持ちます。

13. 数学、ユーティリティ、予約語、型注釈

数学/ユーティリティの例
_UtilLogic:RandomDouble()             -- 0.0~1.0
_UtilLogic:RandomIntegerRange(1, 10)  -- inclusive

-- ElapsedSeconds / ServerElapsedSeconds: world-instance lifetime, NOT reset on OnBeginPlay.
-- They keep ticking across repeated play sessions in the Maker editor.
-- For per-session countdowns, see "Per-session timers" below.
_UtilLogic.ElapsedSeconds
_UtilLogic.ServerElapsedSeconds
mlua ユーティリティクラス

Lua 標準ライブラリ以外のコレクション:List/ReadOnlyList/SyncList、Dictionary/ReadOnlyDictionary/SyncDictionary(Sync* 系はサーバー↔クライアント間で自動同期します)。その他のユーティリティ型:DateTime、TimeSpan、Regex、Translator、Quaternion、Vector2Int、FastVector2/3/FastColor(パフォーマンス向上のため、インプレースで操作します)、Item(インベントリ)。

Dictionary/ReadOnlyDictionary/SyncDictionary の .Values/.Keys は通常の Lua table を返すため、ipairs でそのまま反復処理できます。:GetValues()/:ToTable() や pairs(dict) のラッパーは不要です。リストも同様ですが、最初に :ToTable() が必要です(ReadOnlyList<T> は Lua の table ではありません)。

-- 接続中のすべてのプレイヤー(サーバー側の一斉配信)
for _, user in ipairs(_UserService.UserEntities.Values) do
    if isvalid(user) then ... end
end

詳細な API については、Environment/NativeScripts/ を確認するか、msw-search で検索してください。

セッションごとのタイマー — ElapsedSeconds を基準にしない

落とし穴:OnBeginPlay で self.deadline = _UtilLogic.ElapsedSeconds + 15 とすることです。ワールドインスタンスは複数回の Maker プレイセッションにわたって存続するため、次のプレイでは保存された期限が過去になり、すぐに発火します。

セッションごとのカウントダウンには、OnUpdate で delta に基づいてプロパティを減算します。

property number waveCountdown = 0
method void OnBeginPlay() self.waveCountdown = 15 end
method void OnUpdate(number delta)
    if self.waveCountdown > 0 then
        self.waveCountdown = self.waveCountdown - delta
        if self.waveCountdown <= 0 then self:StartWave() end
    end
end

セッション開始からの経過時間を使う場合は、OnBeginPlay で基準値を設定し(self.startTime = _UtilLogic.ElapsedSeconds)、そこから差し引きます。セッションをまたいで生の ElapsedSeconds を比較しないでください。

型注釈(コードヒント)

---@type T / ---@param / ---@return はエディターの自動補完のみを提供します。実行時の効果はありません。

予約語

識別子として使用できない語:handler、property、method、script、end、extends、self、nil、true、false。

ローカル変数、パラメーター、プロパティ、メソッド、ドットフィールド名(rec.handler)、および引用符で囲まないテーブルキー({ handler = ... })に適用されます。外部文字列キーを角括弧と引用符で囲む(rec["handler"])ことは問題ありませんが、内部キーは名前を変更することを推奨します(例:eventHandler)。


14. 外部ツール

  • Maker MCP(refresh/logs/play/stop/screenshot/…):msw-general スキル。
  • .d.mlua に記載されていない API の説明/例/ガイド:msw-search スキル。
  • MCP の接続設定/.mcp.json/API キーの設定:https://maplestoryworlds-creators.nexon.com/ko/docs?postId=1368 を共有してください。

デバッグの順序:ビルドログ → play → logs → stop → 修正 → 診断 → refresh → 繰り返し。


15. スクリプト作成ワークフロー

  1. 既存のスクリプトを検索します(§1.1)。似たスクリプトがあれば変更します。
  2. 仕様を確認します(§1.3)。まず .d.mlua を参照し、情報が足りなければ msw-search を使います。
  3. パスを決定します(§1.2)。機能フォルダーを必ず使用し、MyDesk/ のルートには書き込みません。
  4. 作成します。
  5. 検証します。mlua-diagnose フックが自動実行されます。エラーがゼロになるまで修正します(§1.4)。
  6. 更新します。Maker MCP の refresh を実行します(§1.5)。
  7. (必要に応じて) play → logs → stop を実行します(§17)。

削除/名前変更を行った場合も、refresh と .model/.map 内の参照のクリーンアップが必要です。


16. エンティティへのスクリプト(コンポーネント)の追加

§1.7 のトリガー — 最初に builder-protocol.md と、対応するビルダー別プロトコルファイルを読みます。Components 配列を生の JSON として編集しないでください。

  • .model に追加(推奨):ModelBuilder.addComponent() / upsertComponent()。マップインスタンスは継承します。
  • 1 つのマップインスタンスのみに追加:MapBuilder.upsertComponent(name, "script.XXX", body)。
  • グローバルモデル(./Global/*.model):既存のグローバルテンプレートはプロジェクト全体に影響するため、Maker Refresh を実行し、ModelBuilder を使ってその場で編集します。Global/ の下に新しいファイルを作成せず、新しいカスタムモデルは RootDesk/MyDesk/Models/ の下に作成します。

17. プレイテストとデバッグ

Maker のプレイモードで動作を確認し、実行時ログ、スクリーンショット、入力のシミュレーションを使ってバグの原因を絞り込む手順です。

MCP ツールの一覧、プレイモードの制約、更新ルールについては msw-general を参照してください。

17.1 まずビルドログを必ず確認する

play を実行する前に、必ず logs(kind="build") を実行してください。ビルドエラーがあるとスクリプト全体の読み込みに失敗し(コンポーネントやロジックが存在しないかのように動作します)、多くの場合、実行時ログには表示されません。「コードは正しそうなのに動かない」という報告の多くは、ビルドエラーの見落としが原因です。エラーを修正 → リフレッシュ → エラーがゼロになるまで再確認してから、プレイしてください。

⚠️ ビルドログが空でも、ビルドが正常とは限りません。 リフレッシュ時に発生する mlua の変換エラー(Maker のポップアップ 「An error occurred during mlua conversion」)は、ビルドコンソールを経由しません。refresh は ok と報告し、logs(kind="build") は 0 件のまま、エラーテキストは logs(kind="normal") にのみ記録されます。ビルドログが空なのにスクリプトの読み込みに失敗する場合、またはそのポップアップが報告された場合は、refresh→ビルド確認を繰り返すのではなく、次に logs(kind="normal") を確認してください。原因を記録するまでは clear_logs を実行しないでください。ログを消去すると normal-log の領域も消去され、変換エラーの唯一の記録が失われます。

⚠️ 古いビルドエラーは、refresh の再実行では消えません。 Refresh が再処理するのは、変更時刻が進んだファイルのみです。そのため、間に編集せずに refresh を再実行しても再コンパイルは行われず、ビルドログのタイムスタンプもエラーも同じままです。ファイル間のシグネチャ変更が典型的な原因です(あるスクリプトでパラメーターリストを変更し、別のスクリプトにある呼び出し元を変更していない場合、古いシグネチャに対する引数不一致の LEA-1102 が残ります)。実際に一度サイクルを回すには、呼び出し元ファイルに無害な編集(コメントなど)を加え、refresh を実行し、その結果を確認してください。

17.2 エラーの分類
分類兆候確認箇所
スクリプトエラーファイル名と行番号を含むスタックトレース該当する .mlua の行、イベント/タイミングの順序
nil 参照attempt to index a nil value初期化の順序、isvalid、Spawn 後 1 フレームのタイミング
コンポーネントが見つからないコンポーネントが nil / GetComponent が失敗.model の Components 配列、名前のタイプミス
同期/ネットワーククライアントでのみ不具合が起きる、値が一致しない、または反映が遅れる@Sync、ExecSpace、RPC の流れ
Info LIA 1113/1114/1115(読み取り/呼び出し箇所での誤検知)静的解析でユーザー定義のクロススクリプト参照(_LogicName、ユーザー定義 @Component のドット/メソッド)を解決できません。ビルドは成功します(errors=0/warnings=0)。ノイズとして扱い、log() で確認してください。例外:代入先(<name> が宣言されていない self.<name> = ...)に対する LIA-1114 は、実際の実行時エラーを示しています。プレイ時に cannot set <name>, no such field が発生するため、property を宣言するか self._T を使用してください(§7)。実際の問題が見えなくなる場合は、次の logs 呼び出しで対象をより高い重要度に絞ってください。
ユーザー定義型の Symbol not found / type not foundユーザー定義型の本体 .mlua が存在する前に、使用箇所を作成しています。まず本体 .mlua を作成し、その後 Maker の refresh を実行して .codeblock を再生成してください。ビルドログのキャッシュに古い結果が 1 サイクル分残ることがあります。次の diagnose の結果で判断してください(ファイルに変更がない状態での refresh は、新しいサイクルにはなりません。§17.1)。

ログだけで判断できない場合は、.mlua に [VRF] を接頭辞に付けた log() を追加して、エンティティ/コンポーネント/プロパティの状態を確認してください(接頭辞を統一すると、後でログを読む際に対象を絞り込めます)。

17.3 テスト結果の報告

簡潔にまとめます:シナリオ(1 行) · 環境(マップ、リフレッシュ済みか) · 手順(入力/Lua) · 結果(Pass/Fail/Blocked) · 証拠(ログ 1~2 行、依頼があればスクリーンショット) · 次のアクション。

複数の問題が見つかった場合は、重大度の高い順に並べてください(プレイを妨げる問題 > 動作の誤り > 見た目の問題)。各問題には再現手順と、§17.2 の該当する分類を記載してください。

17.4 ワークフロー

すべてのプレイテストシナリオで共通のループを使用します。

edit → refresh → logs(kind="build")  ──┐
                                           ↓ (errors? fix and refresh again)
                  clear_logs (optional) → play
                                           ↓
                   reproduce (one maker_execute_script probe, or simulated input)
                                           ↓
                   logs(kind="normal") → classify with §17.2 table
                                           ↓ (insufficient? add log() in .mlua, refresh, replay)
                                          stop → fix → loop

再現の効率化 — ツール呼び出しごとにモデルとの往復が発生し、その出力はセッションが終わるまでコンテキストに残ります。長い keyboard_input/mouse_input の連鎖より、シナリオを実行して [VRF] ログを出力する単一の maker_execute_script プローブを優先してください。実際の入力経路自体をテストする場合に限り、実入力をシミュレートします。1 回のサイクルですべてを検証できるよう、プレイを始める前に、そのターンで発生するすべての変更に [VRF] マーカーを設定してください。

バリエーション — 入口条件は異なりますが、ループは同じです。

シナリオ主な手順
初回のプレイテスト編集から開始 → 更新します。
リグレッション/修正ループクリーンな再現のため、play の前に clear_logs を実行します。
mlua の変換エラー(ポップアップが表示される場合、またはビルドログが空なのにスクリプトが読み込まれない場合)まず logs(kind="normal") を実行します — 変換エラーはビルドログに記録されません(§17.1)。エラーを取得するまでは clear_logs を実行しないでください。
エラー分析ランタイムログを収集した後、まず §17.2 に照らして分類します。分類が確定できない場合に限り、log() を追加します。
ランタイム値の検査log() の呼び出しを追加します。API が不明な場合は、呼び出しを追加する前に仕様(§1.3)を確認してください。
17.5 最終検証(PASS/FAIL)

「エラーがないこと ≠ 合格」です。 完了を報告する前に、意図したロジックが実際に実行されたことを示す肯定的な [VRF] log() の証拠を収集してください。完全なチェックリスト:references/verify-checklist.md(ランタイム → コードレビュー → ログの証拠 → PASS/FAIL)。ターンごとに 1 回実行し、そのターンのすべての変更を 1 回のランタイムサイクルで確認します。

17.6 関連スキル

msw-general — MCP ツール、スクリーンショット/ログのポリシー、更新ルール、ワークスペースと階層。

コメント