便签整体链路

07/055 浏览

截图左上角的便签不是单独页面元素,而是排行榜弹窗打开时,作为背景层叠在遮罩上的“战绩便签墙”。
链路是:
  1. 服务端在真人对局结束后生成玩家便签
  2. 服务端从 serverCloud 拉取便签数据
  3. 客户端收到排行榜数据时一起收到 wallNotes
  4. main.lua 合并到本地缓存
  5. LobbyScreens.lua 把便签渲染到排行榜遮罩背景上
horizontal linehorizontal line

1. 客户端便签缓存

scripts/main.lua:48
lua
local wallCache_ = { version = 1, lastWallTime = 0, notes = {} } local wallNotes_ = {}
这里有两个层次:
  • wallCache_:本地持久缓存,按 userId 存每个玩家最新便签
  • wallNotes_:用于 UI 展示的数组,会按时间排序
horizontal linehorizontal line

2. 本地缓存文件路径

scripts/main.lua:55
lua
local WALL_CACHE_PATH = "bagua_wall_cache.json"
便签会缓存在本地 bagua_wall_cache.json,避免每次打开都完全空白。
horizontal linehorizontal line

3. 重建 UI 用的便签数组

scripts/main.lua:284
lua
local function RebuildWallNotes() wallNotes_ = {} for _, note in pairs(wallCache_.notes or {}) do wallNotes_[#wallNotes_ + 1] = note end table.sort(wallNotes_, function(a, b) return (a.time or 0) > (b.time or 0) end) end
作用:
  • wallCache_.notes 里取出所有便签
  • 转成数组
  • 按时间倒序排列
horizontal linehorizontal line

4. 读取本地便签缓存

scripts/main.lua:291
lua
local function LoadWallCache() if not fileSystem or not fileSystem:FileExists(WALL_CACHE_PATH) then RebuildWallNotes() return end local file = File(WALL_CACHE_PATH, FILE_READ) if not file or not file:IsOpen() then return end local text = file:ReadString() file:Close() local ok, data = pcall(function() return cjson.decode(text or "") end) if ok and type(data) == "table" then wallCache_.version = data.version or 1 wallCache_.lastWallTime = data.lastWallTime or 0 wallCache_.notes = type(data.notes) == "table" and data.notes or {} end RebuildWallNotes() end
游戏启动时会调用它,先把之前缓存的便签读回来。
调用位置在 scripts/main.lua:1698
lua
LoadWallCache()
horizontal linehorizontal line

5. 保存本地便签缓存

scripts/main.lua:311
lua
local function SaveWallCache() wallCache_.version = 1 wallCache_.notes = wallCache_.notes or {} local ok, text = pcall(function() return cjson.encode(wallCache_) end) if not ok or not text then return end local file = File(WALL_CACHE_PATH, FILE_WRITE) if file and file:IsOpen() then file:WriteString(text) file:Close() end end
只要收到新的便签,就会写回本地。
horizontal linehorizontal line

6. 合并服务器下发的便签

scripts/main.lua:326
lua
local function MergeWallNotes(notes, syncedTo) wallCache_.notes = wallCache_.notes or {} local changed = false for _, note in ipairs(notes or {}) do local userId = tostring(note.userId or "") if userId ~= "" and note.message and note.message ~= "" then local old = wallCache_.notes[userId] if old == nil or (note.time or 0) >= (old.time or 0) then wallCache_.notes[userId] = note changed = true elseif IsMeaningfulPlayerName(note.nickname) and not IsMeaningfulPlayerName(old.nickname) then old.nickname = note.nickname changed = true end if (note.time or 0) > (wallCache_.lastWallTime or 0) then wallCache_.lastWallTime = note.time changed = true end end end if syncedTo and syncedTo > (wallCache_.lastWallTime or 0) then wallCache_.lastWallTime = syncedTo changed = true end if changed then SaveWallCache() end RebuildWallNotes() end
关键逻辑:
  • userId 去重
  • 只保留每个玩家最新一条便签
  • message 为空的不展示
  • 收到新数据后保存本地缓存
  • 最后重建 wallNotes_
horizontal linehorizontal line

7. 排行榜更新时合并便签

scripts/main.lua:443
lua
local function OnLeaderboardUpdated(leaderboard) leaderboard_ = leaderboard or { entries = {}, myRank = 0, myWins = 0, myGames = 0 } MergeWallNotes(leaderboard_.wallNotes, leaderboard_.wallSyncedTo) if leaderboard_.wallNextStart and leaderboard_.wallNextStart > 0 then LobbyClient.RequestLeaderboard(wallSyncBaseTime_, leaderboard_.wallNextStart) return end leaderboardLoading_ = false if game_.screen == "menu" then CreateUI() end LobbyScreens.SetStatus("排行榜已更新。") end
排行榜返回时,便签数据也会一起返回。
如果 wallNextStart > 0,说明服务端还有下一页便签,客户端继续请求。
horizontal linehorizontal line

8. 把便签传给大厅 UI

scripts/main.lua:1634
lua
uiRoot_ = LobbyScreens.Main({ rooms = roomList_, connected = lobbyConnected_, playerName = GetPlayerName(), setPlayerName = SetPlayerName, startAI = StartAIBattle, createRoom = RequestCreateRoom, showJoin = ShowJoinRoomScreen, joinRoom = JoinRoomFromCard, refreshRooms = RequestRoomList, showLeaderboard = ShowLeaderboard, closeLeaderboard = CloseLeaderboard, leaderboard = leaderboard_, wallNotes = wallNotes_, leaderboardOpen = leaderboardOpen_, leaderboardLoading = leaderboardLoading_, exitGame = ExitGame, })
这里的关键字段是:
lua
wallNotes = wallNotes_,
也就是说,便签不是排行榜面板自己拉数据,而是 main.lua 把整理好的 wallNotes_ 传进去。
horizontal linehorizontal line

UI 便签渲染关键代码

9. 便签槽位

scripts/ui/LobbyScreens.lua:145
lua
local NOTE_SLOTS = { { left = "3%", top = "5%", w = 138, h = 94 }, { left = "21%", top = "7%", w = 118, h = 82 }, { left = "4%", top = "29%", w = 128, h = 86 }, { left = "9%", top = "53%", w = 170, h = 88 }, { left = "5%", top = "73%", w = 145, h = 96 }, { left = "23%", top = "77%", w = 96, h = 72 }, { right = "8%", top = "8%", w = 168, h = 82 }, { right = "3%", top = "32%", w = 154, h = 88 }, { right = "4%", top = "56%", w = 166, h = 82 }, { right = "10%", top = "72%", w = 134, h = 90 }, }
截图左上角红框里的便签,大概率就是第一个槽位:
lua
{ left = "3%", top = "5%", w = 138, h = 94 }
所以它出现在屏幕左上侧。
horizontal linehorizontal line

10. 便签颜色池

scripts/ui/LobbyScreens.lua:134
lua
local NOTE_COLORS = { { 255, 244, 177, 235 }, { 255, 221, 177, 235 }, { 222, 244, 190, 235 }, { 255, 214, 221, 235 }, { 213, 235, 255, 235 }, }
截图里的浅蓝便签,对应这里的:
lua
{ 213, 235, 255, 235 }
horizontal linehorizontal line

11. 稳定随机 hash

scripts/ui/LobbyScreens.lua:160
lua
local function NoteHash(note, salt) local text = tostring(note.userId or "") .. tostring(note.time or 0) .. tostring(salt or "") local value = 0 for i = 1, #text do value = (value * 33 + string.byte(text, i)) % 2147483647 end return value end
这个函数用于让每张便签的:
  • 颜色
  • 偏移
  • 旋转角度
  • 胶带旋转角度
  • 排列顺序
看起来随机,但对同一条便签保持稳定。
horizontal linehorizontal line

12. 选择便签展示顺序

scripts/ui/LobbyScreens.lua:167
lua
local function PickNotesForWall(notes) local picked = {} for _, note in ipairs(notes or {}) do picked[#picked + 1] = note end table.sort(picked, function(a, b) return NoteHash(a, "slot") < NoteHash(b, "slot") end) return picked end
这里没有按时间展示,而是按 hash 排列。
效果是:便签墙的位置更稳定,不会每次刷新都大幅跳动。
horizontal linehorizontal line

13. 创建单张便签

scripts/ui/LobbyScreens.lua:175
lua
local function CreateWallNote(note, slot, index) local color = NOTE_COLORS[(NoteHash(note, "color") % #NOTE_COLORS) + 1] local jitterX = (NoteHash(note, "x") % 45) - 22 local jitterY = (NoteHash(note, "y") % 35) - 17 local rotate = (NoteHash(note, "r") % 25) - 12 local tapeRotate = (NoteHash(note, "tape") % 11) - 5 return UI.Panel { position = "absolute", left = slot.left, right = slot.right, top = slot.top, width = slot.w, height = slot.h, padding = { 12, 10, 8, 10 }, gap = 4, translateX = jitterX, translateY = jitterY, rotate = rotate, transformOrigin = "center", backgroundColor = color, borderRadius = 8, borderWidth = 1, borderColor = { 112, 78, 45, 120 }, boxShadow = { { x = 4, y = 5, blur = 0, color = { 58, 37, 20, 95 } }, { x = -1, y = -1, blur = 0, color = { 255, 255, 230, 85 } }, }, pointerEvents = "none", children = { ... }, } end
这一段决定了便签的视觉效果:
  • position = "absolute":绝对定位
  • translateX / translateY:随机抖动
  • rotate:轻微旋转
  • backgroundColor:随机颜色
  • boxShadow:纸片阴影
  • pointerEvents = "none":不拦截点击
horizontal linehorizontal line

14. 便签上方胶带

scripts/ui/LobbyScreens.lua:201
lua
UI.Panel { position = "absolute", left = "34%", top = -8, width = 44, height = 15, rotate = tapeRotate, transformOrigin = "center", backgroundColor = { 245, 222, 169, 165 }, borderRadius = 4, borderWidth = 1, borderColor = { 116, 84, 48, 80 }, pointerEvents = "none", },
截图里便签顶部那条浅黄色胶带,就是这段代码画出来的。
horizontal linehorizontal line

15. 便签文字内容

scripts/ui/LobbyScreens.lua:216
lua
H.Text(note.nickname or "玩家", 11, C.ink, { width = "100%", textAlign = "left" }), H.Text(note.message or "再来一题", 12, C.ink, { width = "100%", flexGrow = 1, flexShrink = 1, textAlign = "center" }), H.Text(note.timeText or "", 9, C.inkSoft, { width = "100%", textAlign = "right" }),
截图里的三行:
text
耳内有灰 新手上桌 07-05 13:00
对应:
lua
note.nickname note.message note.timeText
horizontal linehorizontal line

16. 创建便签墙层

scripts/ui/LobbyScreens.lua:226
lua
local function CreateWallNotesLayer(callbacks) local notes = PickNotesForWall(callbacks.wallNotes or {}) local children = {} for i, note in ipairs(notes) do local slot = NOTE_SLOTS[((i - 1) % #NOTE_SLOTS) + 1] children[#children + 1] = CreateWallNote(note, slot, i) end return UI.Panel { position = "absolute", left = 0, right = 0, top = 0, bottom = 0, pointerEvents = "none", children = children, } end
这一层铺满屏幕,然后把每张便签放到预设槽位中。
注意:
lua
local slot = NOTE_SLOTS[((i - 1) % #NOTE_SLOTS) + 1]
如果便签超过 10 张,会循环使用槽位。
horizontal linehorizontal line

17. 便签层挂在排行榜遮罩里

scripts/ui/LobbyScreens.lua:296
lua
return UI.Panel { visible = callbacks.leaderboardOpen == true, position = "absolute", left = 0, right = 0, top = 0, bottom = 0, justifyContent = "center", alignItems = "center", backgroundColor = { 0, 0, 0, 140 }, pointerEvents = "auto", children = { CreateWallNotesLayer(callbacks), H.Frame { width = "86%", maxWidth = 460, padding = { 20, 18, 18, 18 }, gap = 9, alignItems = "center", backgroundColor = C.bgTop, children = rows, }, }, }
重点是这里:
lua
children = { CreateWallNotesLayer(callbacks), H.Frame { ... }, }
便签层在前,排行榜面板在后。
因此视觉上是:
  • 黑色半透明遮罩
  • 背景便签
  • 中央排行榜面板
这正是截图里的效果。
horizontal linehorizontal line

服务端生成便签关键代码

18. 便签云端 key

scripts/network/Server.lua:21
lua
local CLOUD_KEY_WALL_TIME = "bagua_wall_time" local CLOUD_KEY_WALL_MSG = "bagua_wall_msg" local CLOUD_KEY_WALL_TIME_TEXT = "bagua_wall_time_text"
服务端把便签拆成三个云端字段:
  • 更新时间
  • 文案
  • 显示时间文本
horizontal linehorizontal line

19. 根据战绩挑一句便签文案

scripts/network/Server.lua:700
lua
local function PickWallMessage(wins, games) wins = wins or 0 games = games or 0 local pool if games <= 0 then pool = { "第一题还没开", "等一个对手", "刚进大厅", "先观望一下", "新手上桌" } elseif wins <= 0 then pool = { "这题有点绕", "差点就答对", "还在找思路", "今天题不顺", "再给我一局" } elseif wins >= 5 and wins * 2 >= games * 3 then pool = { "稳中带猜", "对手不好骗", "答题很稳", "题路清晰", "有点东西" } elseif wins >= 3 then pool = { "破题小能手", "今天有点准", "题感在线", "越猜越顺", "已赢" .. tostring(wins) .. "局" } elseif games >= 8 then pool = { "老玩家了", "久坐大厅", "再来一题", "题海老手", "继续上桌" } else pool = { "小胜一局", "题感来了", "刚破一题", "手感还行", "这题我熟" } end return pool[math.random(1, #pool)] end
截图里的:
text
新手上桌
就来自这一组:
lua
pool = { "第一题还没开", "等一个对手", "刚进大厅", "先观望一下", "新手上桌" }
horizontal linehorizontal line

20. 保存便签到 serverCloud

scripts/network/Server.lua:720
lua
local function SaveWallNote(uid, nickname, wins, games) if uid == nil or serverCloud == nil then return end local now = os.time() local message = PickWallMessage(wins, games) local timeText = FormatWallTime(now) local ok, err = pcall(function() serverCloud:BatchSet(uid) :SetInt(CLOUD_KEY_WALL_TIME, now) :Set(CLOUD_KEY_WALL_MSG, message) :Set(CLOUD_KEY_WALL_TIME_TEXT, timeText) :Save("更新猜卦便签", { ok = function() print("[LobbyServer] Wall note saved") end, error = function(code, reason) print("[LobbyServer] Wall note save failed: " .. tostring(code) .. " " .. tostring(reason)) end, }) end) if not ok then print("[LobbyServer] Wall note save exception: " .. tostring(err)) end end
这里写入:
lua
bagua_wall_time bagua_wall_msg bagua_wall_time_text
horizontal linehorizontal line

21. 真人对局结束时更新便签

scripts/network/Server.lua:748
lua
local function RecordMatchStats(room, winnerSeat) for _, player in ipairs(room.players or {}) do if not player.isAI then AddCloudStat(player.userId, "bagua_games", 1) if player.seat == winnerSeat then AddCloudStat(player.userId, "bagua_wins", 1) else AddCloudStat(player.userId, "bagua_losses", 1) end if room.mode ~= "ai" then AddCloudStat(player.userId, CLOUD_KEY_PVP_GAMES, 1) if player.seat == winnerSeat then AddCloudStat(player.userId, CLOUD_KEY_PVP_WINS, 1) end SaveWallNoteWithDelta(player.userId, player.name, player.seat == winnerSeat and 1 or 0, 1) end end end end
这说明便签主要来自真人 PVP 战绩,不是 AI 对局。
horizontal linehorizontal line

22. 服务端拉取便签

scripts/network/Server.lua:771
lua
local function FetchWallNotes(lastWallTime, wallStart, callback) if serverCloud == nil then callback({}, 0, 0); return end wallStart = math.max(1, wallStart or 1) local threshold = math.max(0, (lastWallTime or 0) - WALL_OVERLAP_SECONDS) serverCloud:GetRankList(CLOUD_KEY_WALL_TIME, wallStart, WALL_FETCH_LIMIT, { ok = function(rankList) local notes = {} local userIds = {} local stoppedByThreshold = false for _, item in ipairs(rankList or {}) do local uid = item.userId local wallTime = item.iscore and item.iscore[CLOUD_KEY_WALL_TIME] or 0 if wallTime > threshold then notes[#notes + 1] = { userId = uid, nickname = "玩家", message = "", time = wallTime, timeText = "", wins = 0, games = 0 } userIds[#userIds + 1] = uid else stoppedByThreshold = true end end ... end, }) end
服务端用 CLOUD_KEY_WALL_TIME 排名拉取最近更新过便签的玩家。
horizontal linehorizontal line

23. 服务端补全便签详情

scripts/network/Server.lua:800
lua
serverCloud:BatchGet(note.userId) :Key(CLOUD_KEY_NICKNAME) :Key(CLOUD_KEY_WALL_MSG) :Key(CLOUD_KEY_WALL_TIME_TEXT) :Key(CLOUD_KEY_PVP_WINS) :Key(CLOUD_KEY_PVP_GAMES) :Fetch({ ok = function(scores, iscores) local customName = scores and scores[CLOUD_KEY_NICKNAME] or nil note.nickname = PreferName(customName, note.nickname) note.message = scores and scores[CLOUD_KEY_WALL_MSG] or "" note.timeText = scores and scores[CLOUD_KEY_WALL_TIME_TEXT] or FormatWallTime(note.time) note.wins = iscores and iscores[CLOUD_KEY_PVP_WINS] or 0 note.games = iscores and iscores[CLOUD_KEY_PVP_GAMES] or 0 finishOne() end, error = finishOne, })
这一步把便签真正需要显示的字段补齐:
lua
note.nickname note.message note.timeText
horizontal linehorizontal line

24. 服务端下发便签

scripts/network/Server.lua:230
lua
local function SendLeaderboard(connection, entries, myRank, myWins, myGames, wallNotes, wallSyncedTo, wallNextStart) local data = VariantMap() local count = math.min(LEADERBOARD_LIMIT, #(entries or {})) Shared.WriteMessage(data, Shared.MSG.LEADERBOARD, { Count = count, MyRank = myRank or 0, MyWins = myWins or 0, MyGames = myGames or 0, WallCount = #(wallNotes or {}), WallSyncedTo = wallSyncedTo or 0, WallNextStart = wallNextStart or 0, }) ... end
关键字段:
lua
WallCount WallSyncedTo WallNextStart
horizontal linehorizontal line

25. 逐条写入便签字段

scripts/network/Server.lua:251
lua
for i, note in ipairs(wallNotes or {}) do data["WallUserId" .. i] = Variant(tostring(note.userId or "")) data["WallNickname" .. i] = Variant(NormalizeName(note.nickname)) data["WallMessage" .. i] = Variant(note.message or "") data["WallTime" .. i] = Variant(note.time or 0) data["WallTimeText" .. i] = Variant(note.timeText or "") data["WallWins" .. i] = Variant(note.wins or 0) data["WallGames" .. i] = Variant(note.games or 0) end
这里是网络传输层,把数组形式的便签拆成:
text
WallUserId1 WallNickname1 WallMessage1 WallTime1 WallTimeText1 ...
horizontal linehorizontal line

客户端接收便签关键代码

26. 请求排行榜时带便签同步参数

scripts/network/Client.lua:83
lua
function Client.RequestLeaderboard(lastWallTime, wallStart) return SendToServer(Shared.MSG.REQUEST_LEADERBOARD, { LastWallTime = lastWallTime or 0, WallStart = wallStart or 1 }) end
客户端请求排行榜时,也告诉服务端:
  • 我已经同步到哪个时间
  • 我从第几条便签开始拉
horizontal linehorizontal line

27. 客户端解析便签

scripts/network/Client.lua:219
lua
local wallNotes = {} local wallCount = Shared.ReadInt(eventData, "WallCount", 0) for i = 1, wallCount do wallNotes[#wallNotes + 1] = { userId = Shared.ReadString(eventData, "WallUserId" .. i, ""), nickname = Shared.ReadString(eventData, "WallNickname" .. i, "玩家"), message = Shared.ReadString(eventData, "WallMessage" .. i, ""), time = Shared.ReadInt(eventData, "WallTime" .. i, 0), timeText = Shared.ReadString(eventData, "WallTimeText" .. i, ""), wins = Shared.ReadInt(eventData, "WallWins" .. i, 0), games = Shared.ReadInt(eventData, "WallGames" .. i, 0), } end
这里还原成客户端可用的 wallNotes 数组。
horizontal linehorizontal line

28. 客户端返回排行榜对象

scripts/network/Client.lua:233
lua
return { entries = entries, myRank = Shared.ReadInt(eventData, "MyRank", 0), myWins = Shared.ReadInt(eventData, "MyWins", 0), myGames = Shared.ReadInt(eventData, "MyGames", 0), wallNotes = wallNotes, wallSyncedTo = Shared.ReadInt(eventData, "WallSyncedTo", 0), wallNextStart = Shared.ReadInt(eventData, "WallNextStart", 0), }
这就是 main.luaOnLeaderboardUpdated() 收到的数据来源。
horizontal linehorizontal line

这张截图里的便签对应关系

截图红框里的便签:
text
耳内有灰 新手上桌 07-05 13:00
对应代码字段是:
lua
note.nickname = "耳内有灰" note.message = "新手上桌" note.timeText = "07-05 13:00"
来源分别是:
  • nickname:玩家昵称,来自 TapTap 昵称或自定义昵称
  • message:服务端根据战绩随机生成
  • timeText:服务端用 os.date("%m-%d %H:%M", timestamp) 格式化
视觉位置来自:
lua
NOTE_SLOTS
视觉随机性来自:
lua
NoteHash(note, "color") NoteHash(note, "x") NoteHash(note, "y") NoteHash(note, "r") NoteHash(note, "tape")
horizontal linehorizontal line

关键结论

当前便签系统不是静态装饰,而是“排行榜附带的玩家动态墙”:
  • 数据来自真人 PVP 战绩
  • 每个玩家保留最新一条
  • 打开排行榜时更新
  • 本地有缓存
  • UI 上以便签墙形式铺在排行榜遮罩背景上
  • 红框中的便签是 CreateWallNote() 渲染出来的单张 UI.Panel
3