下载 App
发布时间
发布时间
耳内有灰
CharData 内部格式已经是"合并版"
你提议的合并格式,其实和 CharData:ToTable() 已有的序列化格式几乎完全一致:
CharData 内部结构(已有):
{
    id: "dad_player",
    name: "爸爸",
    width: 32, height: 32,
    animations: {
        idle: {
            front: { fps:4, frames: [ {pixels: {"10,4":[54,27,30,254],...}} ] },
            back:  { fps:4, frames: [ {pixels: {...}} ] },
            side:  { fps:4, frames: [ {pixels: {...}} ] },
        },
        walk: { ... }
    }
}
这意味着不需要发明新格式——直接用 CharData 已有的 ToTable()/FromTable() 就是天然的合并格式。
耳内有灰
对话框和列表都正常了。但有两个问题:
导入了 0 个像素 — 日志显示 (128.0x128.0 -> 32x32) 但 导入完成: 0 个像素
KTX 格式错误 — Unsupported texture format in KTX file,说明 cache:GetResource("Image", ...) 加载了 GPU 压缩格式,GetPixel 无法正确读取
需要改用 Image() 直接加载文件来读取原始像素数据。
Image 有 IsCompressed() 方法。问题是 cache:GetResource("Image", ...) 可能自动加载了 KTX 压缩格式。需要用 Image:new() 创建并手动从文件加载,绕过缓存的压缩格式。
我可以用 cache:GetFile() 获取文件,然后用 Image:new() 创建 Image 对象并从文件 load。
Bot Avatar
现在修改 CharData:ImportFromImage,改用 Image:new() + cache:GetFile() 加载原始 PNG 数据。
构建成功。修复方案:
改用 cache:GetFile() + Image:new() + image:Load(file) 加载原始 PNG 数据
绕过 cache:GetResource("Image") 可能返回的 KTX GPU 压缩格式
加了 IsCompressed() 检查作为额外保险
可以预览测试——再次点击「导入」选择一张图片,应该能看到像素被正确导入到画布上了。
耳内有灰
完全确认了!所有路径都走 beginPreloading(),也就是说 currentScene_ 一定会被改为 "loading"。预加载完成后调用 setupGameUI(),但该函数从未恢复 currentScene_ = "game"。
根因确认:currentScene_ 在预加载后停留在 "loading",导致 HandleKeyDown 的 loading 分支直接 return(吞掉所有非 ESC 键),HandleUpdate 的 game 分支也永远不进入。
修复方案很简单:在 setupGameUI() 末尾恢复 currentScene_ = "game"。同时可以移除之前所有不必要的 ScriptObject WASD 变通方案,因为根因不是事件订阅机制的问题——而是 currentScene_ 状态错误。
Bot Avatar
根因找到了:setupGameUI() 函数没有恢复 currentScene_ = "game"。
流程如下:
EnterGame() → currentScene_ = "game"
beginPreloading() → currentScene_ = "loading"
预加载完成 → 调用 setupGameUI()
setupGameUI() → 设置 UI,但 currentScene_ 仍然是 "loading"
这导致 HandleKeyDown 中 currentScene_ == "loading" 分支直接 return(吞掉所有非 ESC 键),HandleUpdate 中 currentScene_ == "game" 分支也永远进不去。
编辑器模式没有预加载流程,所以 currentScene_ 始终是 "editor",一切正常。
现在修复:在 setupGameUI() 末尾恢复 currentScene_。
耳内有灰
分析两个核心 bug:
Bug 1 - 加载界面不显示:beginPreloading() 的"快速路径"调用 PreloadOne() 时,实际执行了加载(不是仅检查缓存),所以 15 个纹理全部同步加载完,allCached=true,直接跳到 setupGameUI(),跳过了加载界面。
Bug 2 - 预加载无效,地图仍然渐进加载:预加载用的 NVG 上下文来自 ImageCache.GetContext(),但当 MapCanvas:Render(nvg) 被调用时,它传入的 nvg 参数再次调用 TerrainRegistry.SetNVG(nvg) — 如果这两个 nvg 的 Lua userdata 身份不同(即使指向同一个底层对象),nvg_ ~= nvg 判断为 true,整个 texCache_ 被清空!
解决方案:
移除快速路径,添加一个仅检查缓存的方法(不加载)
不在 beginPreloading 中设置 NVG 上下文,而是让预加载在 HandleUpdate 中进行,并确保使用的 NVG 上下文与 MapCanvas 后续使用的完全相同
但根本问题是:我们无法保证 ImageCache.GetContext() 返回的 userdata 与 Widget Render(nvg) 回调中的 nvg 是同一个 Lua 对象引用。让我先验证这一点。
耳内有灰
问题原因:拆除屋檐/柱子时调用的是旧版全局方法 mapData:UninstallRoof(),它只检查已弃用的 self.roofInstalled 全局字段。而实际屋檐数据存储在 wallParts[wk].roofInstalled 中。
修复:从 piece.data.wallKey 解析出 anchorRow, anchorCol,改为调用 mapData:UninstallWallParts(row, col),这个方法会正确清除对应墙段的 pillarsInstalled 和 roofInstalled。
耳内有灰
现在完全理解了。问题的根因是评论被 applyChange 加了一次,又在 flushWriteQueue 的 RMW 合并中加了第二次:
服务端双重插入流程:
enqueueWrite(brickId, "comment", ...)
调用 applyChange() → 第1次 table.insert(brickCache_[key].comments, ...)
调用 broadcastBrickUpdate() → 广播给客户端(此时 comments 含 1 条新评论)
将 change 入队到 writeQueue_
flushWriteQueue() 触发 RMW:
从云端读取旧数据 bricks[key]
遍历 batch 中的 change → action == "comment" → 第2次 table.insert(bricks[key].comments, ...)
写回云端(此时 comments 含 2 条新评论)
虽然 flushWriteQueue 本身不再广播,但下次客户端做 SYNC_ALL 或者服务端缓存 brickCache_ 和云端数据产生不一致时,重复数据就会显现。
更重要的是,applyChange 修改了 brickCache_ 中的 comments 数组(第1次插入),然后 broadcastBrickUpdate 用的就是这个被修改后的对象引用。之后 flushWriteQueue 在云端数据上又插了一次(第2次),但因为 brickCache_ 已经被直接修改了,下次 SYNC_ALL 或其他读取可能会拿到重复数据。
不过实际上,让我再想想。broadcastBrickUpdate 广播时已经包含了新评论。客户端收到后 mergeBrickData 用 brick.comments = cloudData.comments 替换。同时 main.lua 本地也已经 insert 了一条。
核心问题是:main.lua 本地 insert 了一条,然后服务器广播回来(也包含那条),mergeBrickData 做了整体替换。这个替换其实是正确的,因为服务端的那一条就是对的。
但 rebuildContent() 在本地 insert 后被立即调用了,显示了 N+1 条。然后 mergeBrickData 触发时(来自 broadcastBrickUpdate),brick.comments 被替换为服务端版本(也是 N+1 条),但 rebuildContent 可能并没有被再次调用…
等等,mergeBrickData 本身不触发 UI 刷新。所以视觉上应该只有 main.lua 本地 insert 后的那次 rebuildContent。
让我重新思考——用户明确说发了 1 条但显示了 2 条。截图中 2楼和3楼都是相同内容 “测试”。说明数据中确实有 2 条。
耳内有灰
WASM 下完整链路
玩家打开分享链接 (WASM)
  → background_match 秒连 → ServerReady
  → Server 实例启动
  → serverCloud:Get(0, "wall_bricks_all")   ← 1 次读,全量加载
  → SYNC_ALL → 客户端合并 → 壁上看到所有人的留言 ✅
玩家点"发送"
  → RemoteEvent → Server 入队
  → FlushWriteQueue: Get → 合并 → Set   ← 写完即持久化
  → broadcastBrickUpdate → 客户端立即看到 ✅
玩家刷新页面 / 重新进入
  → 新 Server 实例启动
  → serverCloud:Get(0, "wall_bricks_all")   ← 读到上次写入的数据
  → SYNC_ALL → 留言还在 ✅
每个环节都是确定性的:
预加载:1 次 Get,不依赖注册表,不会遗漏
写入:RMW 写完 serverCloud:Set 返回即入云,不存在"队列没 flush"的窗口
重进:新实例同一个 key 直接读,数据完整
对比当前方案为什么 WASM 下会丢
当前方案的断裂点:
  serverCloud.list:Add("brick_31", data)  ← 数据入云了
  registerBrickId(31)                      ← 异步,可能没写完
  玩家刷新页面                              ← 服务器销毁
  新服务器 → 读注册表 → 没有 31 → 不去加载 → 留言"消失"
单 key 整包方案没有这个断裂点,因为数据和索引是同一个东西。
旧代码处理
serverCloud.list + 注册表的代码注释保留,不删。后面如果需要按交互类型做审计日志(谁在什么时间点了什么赞),list 的逐条记录模式更合适,可以从冷宫捞回来做"热数据整包 + 冷数据 list 归档"的双写。
确认方向没问题的话,我就开始改 Server.lua。
耳内有灰
修改总结:
main.lua 中 EnterGame() 的加载策略改为:
优先从静态文件加载 — MapData.Load("data/default_map.json"),这个文件会随构建打包,所有用户(包括分享链接)都能读到
静态文件不存在时回退 clientCloud — 保留原有的云端加载逻辑,方便本地开发调试
clientCloud 也不可用时 — 显示友好提示"地图未就绪"
保存逻辑也做了防护 — saveMapToCloud() 会检查 clientCloud 是否存在,不存在时跳过云端保存,避免报错。
关键点:我之前的修改只在**“玩家入口”**(EnterGame())中添加了静态文件加载,编辑器仍然从 clientCloud 读取——所以分享链接上的编辑器看到的是不同用户的 clientCloud 数据。
请你在分享链接上点击**“玩家入口”**按钮测试——那里才是从静态文件 default_map.json 加载地图的路径。
不过,编辑器也应该在启动时自动加载静态地图作为默认内容。让我加上这个逻辑。
问题原因是 MapData.Load() 使用 fileSystem:FileExists() 检查文件存在性——这个 API 检查的是原始文件系统路径,在 WASM 中无法找到已打包的资源文件。改用 cache:GetFile() 后,会从引擎的资源缓存中搜索,可以正确找到构建打包的文件。
耳内有灰
实现内容总结:
测量牌匾区域 — 通过 PIL 分析 roof_mid.png(152×217),确定牌匾木板区域为 x=20…132, y=104…142,转换为比例常量
添加比例常量(约 1786 行):
PLAQUE_LEFT_RATIO = 0.1316,PLAQUE_RIGHT_RATIO = 0.8684
PLAQUE_TOP_RATIO = 0.4793,PLAQUE_BOTTOM_RATIO = 0.6544
牌匾隐形框+悬停检测(_RenderRoof 步骤 7c):
根据中段位置 (roofLeft + capW, roofTop) 和中段尺寸 (midW, roofH) 乘以比例,计算牌匾矩形
将鼠标屏幕坐标通过 ScreenToContent → 逆旋转 → 逆缩放,转换到 counter-rotation local 空间
鼠标在牌匾矩形内时,绘制 2px 红色描边边框
请预览测试 — 鼠标移到屋檐中段的牌匾区域上时应该出现红色边框。