修复"地图编辑器刷新数据丢失"


问题诊断过程
用户报告:编辑器建好房间 → 保存 → 刷新页面 → 进编辑器 → 数据全没了。
关键线索:用户发现"校园日"模式刷新后能看到房间,但一进编辑器就没了。
这直接指向了编辑器页面自身的初始化逻辑,而非持久化链路。
根因
地图编辑模式.lua 的 EnsureBlankEditorLayout() 设计意图是"首次打开编辑器提供空白画布",用模块级布尔 editorLayoutCleared_ 防重入。但 WASM 刷新 = Lua 模块重新加载 = 该标志重置为 false。
时序:
刷新 → 云端存档正确恢复 rooms/floorAreas → 用户点"地图编辑模式"
→ MapEditorMode.Create() → EnsureBlankEditorLayout()
→ editorLayoutCleared_ == false → 无条件清空 MapData → 数据没了校园日页面不调用此函数,所以数据正常。


关键代码(3 层修复)
第 1 层:根因修复 — 编辑器初始化守卫(v0.10.92)
scripts/页面/地图编辑模式.lua:563
luaEnsureBlankEditorLayout = function(context) if editorLayoutCleared_ then return end -- ★ 核心守卫:存档已恢复用户数据时,不清空 local data = MapData.data local hasUserContent = #(data.rooms or {}) > 0 or #(data.floorAreas or {}) > 0 or #(data.objects or {}) > 0 if hasUserContent then editorLayoutCleared_ = true -- 标记已处理 print("[MapEditor] 存档已含用户编辑数据,跳过空白初始化 rooms=" .. tostring(#(data.rooms or {})) .. " floorAreas=" .. tostring(#(data.floorAreas or {})) .. " objects=" .. tostring(#(data.objects or {}))) return end -- 真正的空白地图(首次使用),执行清空 data.rooms = {} data.objects = {} data.characters = {} data.floorAreas = {} data.gameplayMarks = {} editorLayoutCleared_ = true editorState_.status = "空白地图模式:右键拖动画布、滚轮缩放,然后从左侧开始放置房间。" context.store.MarkDirty("mapEditorBlank") end
通用规律:任何"首次初始化重置全局数据"的函数,都不能仅靠模块级一次性标志——WASM 刷新会重置所有模块级变量。必须先检查数据是否已被持久层填充。


第 2 层:云存档时序加固(v0.10.90)
scripts/系统/云存档.lua — 解决 WASM 环境下的异步时序问题。
(a)身份等待 + 超时回退
luafunction CloudSave.Load(Store, onDone) if not HasCloudApi() then (onDone or function() end)(false, "cloud_unavailable") return end loading_ = true loadStore_ = Store loadCallback_ = onDone or function() end if CanUseCloud() then StartLoadRequest() -- userId 已就绪,立即读 else print("[CloudSave] 等待用户身份后读取云端存档") -- Update() 里每帧检测 userId,就绪后调 StartLoadRequest() -- 超过 LOAD_IDENTITY_TIMEOUT(15s) 则回退本地 end end
(b)token 防迟到回调
lualocal function StartLoadRequest() loadToken_ = loadToken_ + 1 local token = loadToken_ clientCloud:Get(CLOUD_KEY, { ok = function(values) if not loading_ or token ~= loadToken_ then return -- ★ 过期回调,丢弃 end -- ... 正常处理 end, }) end
(c)latest-wins 写队列(active/pending 分离)
lualocal Flush Flush = function(now) if saving_ or not pendingPayload_ then return false end local payload = pendingPayload_ pendingPayload_ = nil activePayload_ = payload -- ★ 分离:正在写的 vs 排队等写的 saving_ = true activeWriteToken_ = activeWriteToken_ + 1 local token = activeWriteToken_ clientCloud:Set(CLOUD_KEY, { payload = payload }, { ok = function() if token ~= activeWriteToken_ then return end saving_ = false activePayload_ = nil if pendingPayload_ then Flush(os.time()) -- ★ 写完后如果还有排队的,继续写最新的 else NotifySaveWaiters(true, "cloud") end end, error = function(code, reason) if token ~= activeWriteToken_ then return end saving_ = false if not pendingPayload_ then pendingPayload_ = activePayload_ -- ★ 仅无更新时回填,旧不覆盖新 end activePayload_ = nil nextRetryAt_ = os.time() + SAVE_INTERVAL end, }) end
(d)savedAt/revision 新旧比较
lualocal function ApplyCloudPayload(Store, record) local cloudSavedAt = tonumber(record.updatedAt) or 0 local cloudRevision = tonumber(record.payload.saveMeta and record.payload.saveMeta.revision) or 0 local localSavedAt = Store.GetLoadedSaveTimestamp() local localRevision = Store.GetLoadedSaveRevision() -- ★ 拒绝较旧云端数据覆盖较新本地数据 if localSavedAt > 0 and cloudSavedAt > 0 and (cloudSavedAt < localSavedAt or (cloudSavedAt == localSavedAt and cloudRevision < localRevision)) then return false, "local_newer" end Store.Deserialize(record.payload) return true, "cloud" end


第 3 层:保存链路统一入口(v0.10.90)
scripts/main.lua:125 — 所有保存走同一个 SaveGame:
luaSaveGame = function(onDone) local localSaved = Store.Save() -- 本地 File 快照 if not localSaved then if onDone then onDone(false, "local_save_failed") end return false end CloudSave.Flush(Store, onDone) -- 云端同步,回调通知结果 return true end,
scripts/数据/存储.lua — 每次序列化带版本元数据:
luafunction Store.Serialize() local payload = DeepCopy(state) payload.mapSnapshot = MapData.GetSnapshot() nextSaveRevision_ = nextSaveRevision_ + 1 payload.saveMeta = { savedAt = os.time(), -- 时间戳 revision = nextSaveRevision_, -- 递增版本号 } loadedSaveTimestamp_ = payload.saveMeta.savedAt return payload end


经验提炼(抗体 AB-074 + AB-075)
| 编号 | 名称 | 核心教训 |
| AB-074 | WASM 云存档时序 | 本地 File 刷新即丢;保存成功以云端 Set 回调为准;userId 可能延迟就绪需等待+超时;读写都要 token 防迟到;latest-wins 队列分离 active/pending;savedAt/revision 防旧覆新 |
| AB-075 | 编辑器初始化守卫 | 模块级布尔标志在 WASM 刷新后重置;"首次初始化清空全局数据"必须先检查持久层是否已填充数据;不能仅靠一次性标志防重入 |
诊断方法论:当"页面 A 数据正常、页面 B 数据丢失"时,问题大概率在页面 B 的初始化逻辑,而非持久化链路本身。

