自动化与脚本
在 MSW/Maker 中编写、挂接、试玩与调试…
适用于 MSW(MapleStory Worlds)项目里用 mlua 编写 Component/Logic 脚本、挂到实体、刷新构建并通过 play/logs 验证行为的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
在 MSW/Maker 中编写、挂接、试特定
解决思路
- 创建 .mlua 前,先在 ./RootDesk/MyDesk/ 下 glob/关键词搜索同用途脚本;优先扩展现有文件,避免重复实现。
- 新 .mlua 必须放在 ./RootDesk/MyDesk/<FeatureFolder>/<ScriptName>.mlua;复用已有子文件夹,否则创建 PascalCase 功能文件夹;同一功能的 Component/Logic/Event/Struct 放一起;单文件功能也建文件夹;禁止 Scripts/Misc/Common/New/temp/ 等扁平的兜底文件夹。
- 不猜 MSW API:先读 ./Environment/NativeScripts/{folder}/{name}.d.mlua 查签名,必要时用 msw-search 查语义/示例,再写代码,再跑 LSP diagnose。
- 查 Engine API 时按目录:Component 104、Service 46、Event 202、Logic 9、Enum 118、Misc 140;已知名称读对应 .d.mlua,未知名称 grep 关键词。
- 每次创建/修改 .mlua 后,依赖 mlua-diagnose hook 自动跑 LSP diagnose;迭代修复到 error-severity diagnostics 为零。
- 不要手动创建/编辑/删除 .codeblock;每次 .mlua 创建/修改/重命名/删除后调用 Maker MCP refresh;refresh 需要 edit mode,若正在 play 先 stop。
- 不按 Unity/通用直觉推理;遇到 Unity 模式先停下并对照 Environment/NativeScripts/*.d.mlua 验证。
- 遇到 global manager 的 gameObject/transform 时:@Logic 没有 self.Entity,要用 property 注入或 _EntityService 查找。
- 遇到 OnMouseDown/BoxCollider2D 点击时:物理 collider 不发 TouchEvent;世界用 TouchReceiveComponent,UI 用 ButtonComponent/UITouchReceiveComponent。
- 遇到 OnCollisionEnter+Rigidbody 时:实体↔实体碰撞要用 TriggerComponent + TriggerEnter/Leave/Stay。
- 遇到 UI 字段名时:disable→Enable、text→Text、text color→FontColor、tint→Color;ButtonComponent.Interactable 不存在。
- 遇到 Rigidbody/Collider 时:每种地图类型只能有一个 Body。
- 从 server 操作 UI 时:UI 仅 client-only;server→UI 用 @ExecSpace("Client") RPC;Server/ServerOnly/Multicast/@Sync 挂在 UI-attached Component 上会静默 no-op 并运行时警告。
- 实例化对象时:用 _SpawnService:SpawnByModelId(id, name, pos, parent),parent 必填且 server-only,不要用 Instantiate(prefab)。
- 处理 static 类/手写 singleton 时:@Logic 本身就是 singleton,用 _ScriptName:Method() 调用,不要实例化。
- 若本轮触及 .map/.model/.ui 或 spawn/entity-placement/UI-binding 代码,先完整 Read ../msw-general/references/builder-protocol.md 核心加对应 builder-protocol-map.md/model/ui;仅当本次未加载或压缩丢失才 Read,记忆摘要不算 in context;触发词包括 _SpawnService/SpawnByModelId/SpawnByEntity、任何 .map/.model/.ui 改动、调用 msw_map_builder.cjs/msw_model_builder.cjs/msw_ui_builder.cjs、新建怪物/NPC/弹窗/地图对象请求、§11 或 §16 工作。
- 每个 method(生命周期、RPC、事件处理器、自定义)必须在方法体第一行写描述注释,不能写在声明上方;正确示例是 method void ApplyDamage(Entity target, number amount) 换行后 -- Applies damage and triggers hit VFX.。
- 用户脚本 ./RootDesk/MyDesk/**/*.mlua 可直接创建、读取、修改、删除。
- *.codeblock 是自动生成产物,绝不手动触碰。
- ./Environment/NativeScripts/** 是引擎 API 定义,只读,不修改。
- 挂脚本时改 .model 的 Components 数组,或用已存在的 ./Global/*.model;地图实例改 ./map/*.map。
- @Component 用于挂到 Entity,用 self.Entity 访问拥有者;允许 parent 为 Component、AttackComponent、HitComponent。
- @Logic 用于全局 singleton,不依赖 Entity,适合 game manager、UI manager、utility;每个 world 一个;访问为 _<ExactScriptName>,不剥后缀;TDHUDLogic.mlua→_TDHUDLogic,TowerDefenseConfig.mlua→_TowerDefenseConfig。
- @Logic 支持 @Sync 属性;Logic 的 OnUpdate 在 Component 的 OnUpdate 之前运行。
- @Logic 没有 self.Entity;需要绑定世界实体时用 property Entity x = "uuid" 或 property EntityRef x = "",或用 _EntityService:GetEntityByPath(...)/:FindEntityByName(...);优先 property 注入 UUID。
- 不要在 @Logic 上声明 OnMapEnter/OnMapLeave,它们不会触发,属于 Component-only 的静默死代码。
- 选择 @Component vs @Logic 按生命周期:跨地图世界级(account state、world event bus、global UI manager)用 @Logic;地图内(quest controller、wave spawner、puzzle)用 map entity 上的 @Component;单一 actor(monster AI、item pickup、player skill)用该 entity 上的 @Component;问“玩家走到另一张地图后还运行吗?”。
- extends 脚本:@Component script PlayerAttack extends AttackComponent,覆盖父方法,用 __base:MethodName() 调父类。
- 其他脚本类型:@Event、@Item、@BTNode、@State、@Struct。
- mlua 基于 Lua 5.3,新增 continue;复合赋值 +=、-=、*=、/=、//=、%=、^=、..=、&=、|=、<<=、>>=;位运算 &、|、<<、>>。
- mlua 限制:禁止 global;禁止 coroutine.*;父类调用用 __base:MethodName() 而不是 super。
- 使用内置工具函数 log()、log_warning()、log_error()、wait(seconds)、isvalid(obj)、enum(t)、beginscope(name)/endscope()。
- 生命周期顺序:OnInitialize→OnBeginPlay→OnUpdate(delta)→OnEndPlay→OnDestroy;OnMapEnter/OnMapLeave 仅 Component,按地图切换触发。
- 生命周期用途:OnInitialize 创建后初始化内部变量;OnBeginPlay 游戏开始连接事件、启动 timer、初始 setup;OnUpdate 每帧运行,Logic 先于 Component;OnMapEnter/OnMapLeave 仅 Component 做每地图初始化/清理;OnEndPlay 断开事件、清 timer;OnDestroy 做最终清理。
- 在 OnBeginPlay 连接的一切事件与 timer 必须在 OnEndPlay 释放;示例:property any eventHandler = nil、property integer timerId = 0、ConnectEvent/SetTimerRepeat,然后 DisconnectEvent/ClearTimer。
- ExecSpace 语义:ServerOnly 服务器内部;ClientOnly 客户端内部;Server 是 client→server RPC;Client 是 server→client RPC;Multicast 是 server→全部 client;未指定则调用方本地执行。
- 生命周期方法 ExecSpace 限制:OnSyncProperty 只能 ClientOnly;OnInitialize/OnBeginPlay/OnUpdate/OnEndPlay/OnDestroy/OnMapEnter/OnMapLeave 允许 ServerOnly、ClientOnly 或未指定;事件处理器允许 ServerOnly、ClientOnly 或未指定;自定义方法可用 Server、Client、ServerOnly、ClientOnly、Multicast。
- 典型 server-client 流程:Client input(ClientOnly)→Request()→Server validate(ServerOnly)→@Sync 自动同步→Client UI update(ClientOnly)←Show()(Client RPC)。
- ServerOnly 被 client 调用会静默忽略;Server 是 client→server RPC,有网络延迟;Client 是 server→client RPC,指定单个客户端时在调用点最后加 UserId 参数,不要加到声明。
- 在 @ExecSpace("Server") 方法体内用 senderUserId 验证请求者;例如 if senderUserId ~= self.Entity.PlayerComponent.UserId then return end。
- 任何 @ExecSpace(...) 方法不要用保留参数名 self、senderUserId、targetUserId、messageOwnerEntity;冲突时重命名,如 targetUserId→forUserId、senderUserId→fromUserId。
- 无 @ExecSpace 且需按端分支时用 self:IsServer() / self:IsClient() 方法调用,必须带括号;if self.IsServer then 永远 truthy 且 LSP 不报,会导致双端分支错误。
- 跨 server↔client RPC 参数允许 string、integer、number、boolean、table、Vector2/3/4、Color、Entity、Component、EntityRef、ComponentRef;any 不允许;引擎 enum 不能跨,typed 被 LSP 拒,any 运行时 LEA-3036 InvalidCast;用 string key 发送,在接收端 branch 后转回 enum;SyncTable<K,V> 泛型也必须来自允许列表。
- 基本属性类型:number、integer、string、boolean、Vector2/Vector3、Color(0.0~1.0)、any。
- Entity/Component 引用属性:property Entity targetEntity = "UUID"、property ButtonComponent btnOk = "uuid-string";AI 必须直接从 .map/.ui 读取 id 并硬编码为 string literal,不要让用户拖拽绑定。
- Entity/Component 引用在地图切换时丢失;EntityRef/ComponentRef 跨地图存活;多地图游戏优先 EntityRef/ComponentRef。
- @Sync 是 server→所有 client 的单向同步,客户端改动不回传且有延迟;@TargetUserSync 是 server→拥有者 client,适合 per-player 私有数据,非 PlayerEntity 退化为 @Sync;any/table 不能同步,用 SyncTable;二者无参数。
- SyncTable 声明不要默认字面量:@Sync property SyncTable<number> Scores、@Sync property SyncTable<string, number> Stats 不写 = ...;在 OnInitialize/OnBeginPlay 填充,如 self.Stats["hp"] = 100、self.Scores:Add(0);运行时不能赋 plain Lua table,只能逐字段赋值或调用 :Add/:Remove/:Clear。
- SyncList<V> 不是用户属性类型;它只是 native engine Component 的 readonly property,如 TagComponent.Tags、PhysicsColliderComponent.PolygonPoints、SkeletonRendererComponent.AnimationNames、JointComponent.Joints;可读并调用 :Add/:Remove/:Clear/.Count/:ToTable(),但自己的 @Component/@Logic 不能声明或实例化 SyncList;同步集合用 SyncTable。
- self._T.<name> 是唯一免声明临时状态,不同步、server/client 各自值、不显示 inspector,不能 @Sync;设置未声明 self.<name> 会运行时 cannot set <name>, no such field,杀死方法,build log 只有 LIA-1114 Info。
- OnSyncProperty(string name, any value) 在 @Sync 属性变化时客户端触发,必须 ClientOnly,可用 Component/Logic;例如 if name == "CurrentHp" then self:UpdateHpBar(value) end。
- 属性编辑器属性:@DisplayName、@Description、@MaxLength(20)、@HideFromInspector、@MinValue(0)、@MaxValue(999)、@Delta(5)。
- 静态订阅用 @EventSender(...) handler Name(Ev event) ... end;@EventSender 第一参可为 "Self"/"LocalPlayer"(无第二参)、"Entity",id、"Model",id、"Service",typeName、"Logic",typeName。
- 动态订阅用 ConnectEvent/DisconnectEvent;在 OnBeginPlay 连接,OnEndPlay 必须断开;如 property any clickHandler = nil、self.clickHandler = entity:ConnectEvent(ButtonClickEvent, self.OnClick)、entity:DisconnectEvent(ButtonClickEvent, self.clickHandler)。
- 需要捕获每个元素状态(card IDs、slot indexes)时,用闭包 handler,把返回的 EventHandlerBase 存表,OnEndPlay 逐个断开;例如 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 只 emit events;订阅用 owning Entity、_InputService、_<LogicName>;self.Entity.ButtonComponent:ConnectEvent(...) 会 runtime nil。
- handler 与 method void 区分:handler Name(Ev e) 配 @EventSender 并由声明接线;method void Name(Ev e) 是通过 ConnectEvent(EvType, self.Name) 的动态回调;混用会编译但永不触发 E-V1-5。
- 自定义事件唯一方式:@Event + script X extends EventType + property 字段;没有 inline factory;例如 DamageDealtEvent 有 property number amount = 0,发送 self.Entity:SendEvent(dmg),连接 self.Entity:ConnectEvent(DamageDealtEvent, self.OnDamage),第一参是 event Type。
- NativeEvent 如 HitEvent.TotalDamage/.AttackerEntity、ButtonClickEvent、StateChangedEvent.PrevState/.CurState,查 Environment/NativeScripts/Event/。
- 访问已删除实体会运行时错误,先 isvalid(entity)、isvalid(self.Entity.SomeComponent)。
- extends 脚本中同签名 method 覆盖父类;---@sealed 内置方法不能覆盖;用 __base:MethodName(args) 调父类。
- LEA-3014 SignatureMismatch:覆盖的“同签名”包括 @ExecSpace,必须与父类注解块 byte-identical,包括缺失;不要给父类无 ExecSpace 的方法加 @ExecSpace("ServerOnly")。
- 常见 AttackComponent/HitComponent hooks(CalcDamage、CalcCritical、GetCriticalDamageRate、GetDisplayHitCount、IsAttackTarget、IsHitTarget、OnAttack)没有 @ExecSpace,覆盖时不要注解;先读父类 .d.mlua 并逐字复制 annotation block;修 LEA-3014 改子类对齐父类,不是反过来。
- 世界触摸两法:TouchReceiveComponent on entity + TouchEvent/Hold/Release,Connect on entity,用于“哪个实体被碰”;ScreenTouchEvent + _InputService:ConnectEvent,用于“屏幕哪里”;两者带 TouchId int32 + TouchPoint Vector2;世界坐标用 _UILogic:ScreenToWorldPosition(event.TouchPoint);过滤 UI 用 _InputService:IsPointerOverUI();TouchEvent 不灵时用 ScreenTouchEvent+ScreenToWorldPosition fallback。
- 物理 collider(BoxCollider2D、CircleCollider2D、Rigidbody/Kinematicbody、TriggerComponent)不发 TouchEvent;只有 TouchReceiveComponent 发 TouchEvent/TouchHoldEvent/TouchReleaseEvent。
- TouchReceiveComponent 设置:AutoFitToSize = true 最简单;手动 TouchArea 留 10~20% slack;RelayEventToBehind = true 默认转发,仅需阻断设 false;不触发时依次查 TouchReceiveComponent 是否附在 .map/.model、TouchArea 非零且实体可见、前方实体是否 RelayEventToBehind = false 阻挡、handler 是否存 property any 防 GC。
- 选择规则:“哪个实体被碰”用 TouchEvent;“屏幕哪里”用 ScreenTouchEvent。
- PC 鼠标左右中键用 KeyDownEvent 而非 ScreenTouchEvent.TouchId == 2;ScreenTouchEvent 在 PC 仅左键 TouchId == 1,TouchId == 2 是移动端双指;PC 用 _InputService:ConnectEvent(KeyDownEvent, ...) 判断 event.key == KeyboardKey.Mouse0/Mouse1/Mouse2(323/324/325);同时支持移动端多点触控和 PC 就同时连 ScreenTouchEvent 和 KeyDownEvent,不会双触发。
- UI 点击对 ./ui/*.ui 和 ui tree 用 ButtonComponent + ButtonClickEvent;先判断目标是世界对象还是 UI 面板按钮,混用静默无效。
- 任何 spawn/.map/.model 工作前 Read builder-protocol.md + 对应 per-builder protocol 文件。
- 查询“地图里所有 X”或“名为 Y 的子实体”用 Entity lookup:Entity.Children 返回 ReadOnlyList<Entity>,调用 :ToTable() 迭代;Entity:GetChildByName(name, recursive=false);Entity:GetChild(id, recursive=false);Entity:GetChildComponentsByTypeName(typename, recursive=false);Entity:GetFirstChildComponentByTypeName(typename, recursive=false)。
- 优先用 self.Entity.CurrentMap 而不是 service 查询;例如 local map = self.Entity.CurrentMap; local units = map:GetChildComponentsByTypeName("script.MyUnit", false); for _, child in ipairs(self.Entity.Children:ToTable()) do log(child.Name) end。
- 集合名是 Children;ChildList/Childs/GetChildren() 是错的,可编译但 runtime nil 并给 LIA-1114 Info;运行时 spawn 的实体必须 parent 到 CurrentMap 才能被找到。
- 访问 native vs user component:entity.SomeComponent(dot)只对 engine-native 有效;user @Component 用 entity:GetComponent("script.MyUnit") 或 entity:GetFirstChildComponentByTypeName("script.MyUnit", true);user @Component typename 总是 "script.<FileBaseName>",MyUnit.mlua→"script.MyUnit",不管功能文件夹嵌套;entity.MyUnit(dot)返回 nil 并给 LIA-1114;跨脚本传 user-component refs 用 typed property(property MyUnit unit = "")并注入 UUID。
- 方法名是 GetComponent(重载 GetComponent(Type) 和 GetComponent(string typename));*ByTypeName 后缀只在 child 变体 GetChildComponentsByTypeName / GetFirstChildComponentByTypeName。
- GetComponent(string) 返回抽象 Component,成员访问退化动态派发,LSP 报 LIA-1114 Info 或 type mismatch Error;用 ---@type MyUnit 恢复静态类型,如 ---@type MyUnit local unit = self.Entity:GetComponent("script.MyUnit"); unit:DoSomething()。
- 运行时生成用 _SpawnService:SpawnByModelId(id, name, pos, parent),parent 必填,传 self.Entity.CurrentMap;SpawnByEntity 允许 parent = nil;必须先存在 .model 模板;新对象流程:author .model → spawn 或 place on map。
- 有 Body(Kinematic/Rigid/Sideview)的实体忽略直接写 TransformComponent.WorldPosition,物理下一帧覆盖;改用每帧 MovementComponent:MoveToDirection(dir, dt);本地传送 MovementComponent:SetPosition(pos) 或 body:SetPosition(Vector2);世界传送 body:SetWorldPosition(Vector2)(Kinematicbody on RectTile 地图标准绝对放置);直接写 Transform 仅限无 Body 实体;不要移除 Body 作为 workaround,会禁用 tile collision 和 enter/leave events(NativeIssue_MissingComponent)。
- 所有 service/logic 用 _Name 访问。常用:_SpawnService(SpawnByModelId/SpawnByEntity;没有 Despawn,删除用 Entity:Destroy()/Destroy(delaySeconds),两者 ControlOnly);_TimerService(SetTimer/SetTimerRepeat/ClearTimer);_EntityService(GetEntity/GetEntities/GetEntitiesByPath);_UserService(GetUsersByMapComponent(map.MapComponent) 返回该地图所有玩家,nil 表示无);_InputService(输入状态,接收 ScreenTouchEvent);_ResourceService(查 RUID,LoadAnimationClipAndWait(ruid) 同步加载并阻塞一帧,缓存结果,或 _ResourceService:PreloadAsync({ruid}, function() ... end));_DataStorageService(持久化,⚠️ 计费,不在 OnUpdate/短 timer 调用,循环中用 Batch*);_UtilLogic(随机、时间、字符串、数学);_TweenLogic(MoveTo/ScaleTo/RotateTo);_UILogic(坐标转换如 ScreenToWorldPosition,ClientOnly)。完整列表读 ./Environment/NativeScripts/Service/ 46 个和 Logic/ 9 个,领域细节用 msw-search。
- 内置全局不带 _ 前缀:Environment(IsMakerPlay/IsMakerEdit/IsPlay/IsPublishedPlay/IsMobilePlatform/IsPCPlatform、WorldId;GetApplicationVersion() 是 ClientOnly 在 server 为 nil)不是 _Environment;CollisionGroups(CollisionGroups.HitBox/.Monster/.Player 等,内置 Default/TriggerBox/HitBox/Interaction/Portal/Climbable 加项目组,每项有 .Id 和 :GetCollideGroups())不是 _CollisionGroups。
- 数学/工具:_UtilLogic:RandomDouble() 返回 0.0~1.0;_UtilLogic:RandomIntegerRange(1, 10) 为 inclusive;_UtilLogic.ElapsedSeconds/ServerElapsedSeconds 是世界实例寿命,不在 OnBeginPlay 重置,跨 Maker play sessions 继续走。
- mlua 工具类:List/ReadOnlyList/SyncList、Dictionary/ReadOnlyDictionary/SyncDictionary(Sync* 自动同步 server↔client)、DateTime、TimeSpan、Regex、Translator、Quaternion、Vector2Int、FastVector2/3、FastColor、Item;Dictionary/ReadOnlyDictionary/SyncDictionary 的 .Values/.Keys 返回 plain Lua table,用 ipairs 直接迭代,不需要 :GetValues()/:ToTable()/pairs(dict);List 类似但需先 :ToTable(),ReadOnlyList<T> 不是 Lua table;示例 for _, user in ipairs(_UserService.UserEntities.Values) do if isvalid(user) then ... end end。
- 每会话倒计时不要锚定 ElapsedSeconds;不要在 OnBeginPlay 设 self.deadline = _UtilLogic.ElapsedSeconds + 15;用 delta 驱动属性:OnBeginPlay 设 self.waveCountdown = 15,OnUpdate(delta) 若 >0 则减 delta,<=0 调 StartWave;需要 session-relative elapsed 时在 OnBeginPlay baseline self.startTime = _UtilLogic.ElapsedSeconds 再减,不要跨 session 比较原始 ElapsedSeconds。
- 类型注解 ---@type T、---@param、---@return 只给编辑器自动补全,无运行时效果。
- 保留字不能作标识符:handler、property、method、script、end、extends、self、nil、true、false;适用于 locals、parameters、properties、methods、dot-field(rec.handler)和裸 table key({ handler = ... });外部 string key 可用 rec["handler"],内部 key 优先重命名如 eventHandler。
- 外部工具:Maker MCP(refresh/logs/play/stop/screenshot/…)看 msw-general;.d.mlua 没有的 API 描述/示例/指南用 msw-search;MCP wiring/.mcp.json/API key 见 https://maplestoryworlds-creators.nexon.com/ko/docs?postId=1368;调试顺序 build logs → play → logs → stop → fix → diagnose → refresh → repeat。
- 脚本创作流程:Search 已有脚本→Verify spec(.d.mlua,必要时 msw-search)→Decide path(功能文件夹,禁止 MyDesk 根)→Write→Validate(mlua-diagnose 自动,修到 0 errors)→Refresh(Maker MCP refresh)→如需要 play→logs→stop。
- 删除/重命名 .mlua 后也要 refresh,并清理 .model/.map 中的引用。
- 挂脚本到实体:读 builder-protocol.md + 对应 per-builder;不要直接编辑 Components 原始 JSON;优先挂 .model:ModelBuilder.addComponent()/upsertComponent(),地图实例继承;只挂单个地图实例:MapBuilder.upsertComponent(name, "script.XXX", body);全局模型 ./Global/*.model 用 ModelBuilder + Maker Refresh 原地编辑,不在 Global/ 新建,新自定义模型放 RootDesk/MyDesk/Models/。
- 每次 play 前先 logs(kind="build");build error 会让脚本完全不加载且常不出现在 runtime logs;修→refresh→复查到 errors=0 再 play。
- 空 build logs 不等于 build OK;refresh 阶段 mlua conversion errors(弹窗 "An error occurred during mlua conversion")绕过 Build Console:refresh 报 ok,logs(kind="build") 为 0,错误只在 logs(kind="normal");如果 build logs 空但脚本仍加载失败或弹窗,读 logs(kind="normal"),不要循环 refresh→build-check;捕获原因前不要 clear_logs,否则清掉唯一副本。
- 错误分类:Script error 看 stack trace file+line;nil reference 看 "attempt to index a nil value",查 init order、isvalid、Spawn 后 1-frame timing;Component missing 查 .model Components 数组和 name typo;Sync/network 只有 client 坏、值不匹配或晚收敛时查 @Sync/ExecSpace/RPC flow;Info LIA 1113/1114/1115 是静态分析无法解析用户跨脚本引用(_LogicName、user @Component dot/method),build 仍过,视为 noise 用 log() 验证,但 LIA-1114 在赋值目标 self.<name>(未声明)是真运行时错误信号,声明 property 或 self._T;User type Symbol not found/type not found 先写 body .mlua,再 Maker refresh 生成 .codeblock;build-log cache 可能有一轮 stale,看下一次 diagnose。
- 测试结果报告简短包含:Scenario 一行、Env(map、是否 refreshed)、Steps(input/Lua)、Result(Pass/Fail/Blocked)、Evidence(1-2 行 log,必要时截图)、Next action。
- Playtest 循环:edit → refresh → logs(kind="build") →(有错修并再 refresh)→ clear_logs(可选)→ play → keyboard_input/mouse_input 复现 → logs(kind="normal") → 按错误分类表判断 → 不够则在 .mlua 加 log()、refresh、重放 → stop → fix → loop。
- Playtest 变体:首次 playtest 从 edit→refresh 开始;回归/修复循环 play 前 clear_logs 获取干净复现;mlua conversion error(弹窗或 build logs 空但脚本不加载)先 logs(kind="normal"),捕获前不要 clear_logs;错误分析先映射错误分类表,分类不明确才加 log();运行时值检视加 log(),API 未知先验证 spec。
- 最终验证:No errors ≠ Pass;报告 done 前收集正向 log() 证据证明预期逻辑实际执行;按 references/verify-checklist.md(Runtime→Code Review→Log Evidence→PASS/FAIL)。
- 相关技能:msw-general 负责 MCP 工具、截图/logs 策略、refresh 规则、workspace 和 hierarchy。
适用边界 · 注意事项
- 不适用于非 MSW/非 mlua 的普通 Lua、Unity 或其他引擎项目。
- 没有 Maker MCP、无法访问 ./Environment/NativeScripts/ 或不能执行 refresh/play/logs 时不要套用。
- 只做与 .mlua/.map/.model/.ui 无关的通用代码时不要套用。