Skip to content

Commit 0a0aecd

Browse files
authored
Clientside player notes (beyond-all-reason#1224)
Old feature request: https://discord.com/channels/549281623154229250/1452517663103520840 Players can right-click a player name and choose ``Add Player Notes``. The note is saved locally in a .json and shown at the bottom of that player’s hover tooltip. Note: Notes are not sent to the server, they are local state but can be transferred if the .json file is shared. - Limits notes to 100 characters. Displays pop up warning if note exceeds char count - File path for saved .json file ``Beyond-All-Reason\data\playerNotes.json`` - Accounts by `account_<accountID>` and, with `name_<username>` as a fallback. <img width="225" height="127" alt="image" src="https://github.com/user-attachments/assets/33bf6ce1-441a-4d59-a522-16b109c022bd" /> <img width="313" height="300" alt="image" src="https://github.com/user-attachments/assets/f19dfd7c-0889-4064-b4aa-9488c10c294b" /> Codex used with for coding assistance
1 parent d4a20b3 commit 0a0aecd

2 files changed

Lines changed: 160 additions & 1 deletion

File tree

LuaMenu/widgets/api_user_handler.lua

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ local UserLevelToImageConfFunction
113113
local votedUsers = {} -- 2023-06-29 FB: ToDo: Does not get reset, if user leaves battle during vote, but has no impact
114114
local usersAllowedToVote = {}
115115

116+
local playerNotes
117+
116118
--------------------------------------------------------------------------------
117119
--------------------------------------------------------------------------------
118120
-- Globally Applicable Utilities
@@ -357,6 +359,128 @@ local function GetUserClanImage(userName, userControl)
357359
return file, needDownload
358360
end
359361

362+
local function TrimPlayerNote(note)
363+
if type(note) ~= "string" then
364+
return ""
365+
end
366+
return note:gsub("^%s+", ""):gsub("%s+$", ""):sub(1, 100)
367+
end
368+
369+
local function GetPlayerNoteKey(userName, userInfo)
370+
userInfo = userInfo or {}
371+
if userInfo.accountID ~= nil and tostring(userInfo.accountID) ~= "" then
372+
return "account_" .. tostring(userInfo.accountID)
373+
end
374+
return "name_" .. tostring(userName or "")
375+
end
376+
377+
local function CleanPlayerNotes(rawNotes)
378+
local cleanNotes = {}
379+
if type(rawNotes) ~= "table" then
380+
return cleanNotes
381+
end
382+
383+
for key, value in pairs(rawNotes) do
384+
if type(key) == "string" and type(value) == "string" then
385+
local cleanNote = TrimPlayerNote(value)
386+
if cleanNote ~= "" then
387+
cleanNotes[key] = cleanNote
388+
end
389+
end
390+
end
391+
return cleanNotes
392+
end
393+
394+
local function SavePlayerNotes()
395+
local notesFile = io.open("playerNotes.json", "w")
396+
if not notesFile then
397+
Spring.Echo("Unable to save player notes to playerNotes.json")
398+
return
399+
end
400+
notesFile:write(Json.encode(playerNotes or {}))
401+
notesFile:close()
402+
end
403+
404+
local function LoadPlayerNotes()
405+
if playerNotes then
406+
return playerNotes
407+
end
408+
409+
playerNotes = {}
410+
if VFS.FileExists("playerNotes.json") then
411+
local rawNotes = VFS.LoadFile("playerNotes.json")
412+
if rawNotes and rawNotes ~= "" then
413+
local success, decodedNotes = pcall(Json.decode, rawNotes)
414+
if success then
415+
playerNotes = CleanPlayerNotes(decodedNotes)
416+
else
417+
Spring.Echo("Unable to read player notes from playerNotes.json")
418+
end
419+
end
420+
end
421+
422+
return playerNotes
423+
end
424+
425+
local function GetPlayerNote(userName, userInfo)
426+
local notes = LoadPlayerNotes()
427+
local noteKey = GetPlayerNoteKey(userName, userInfo)
428+
local note = notes[noteKey]
429+
if (not note or note == "") and userInfo and userInfo.accountID ~= nil then
430+
note = notes["name_" .. tostring(userName or "")]
431+
end
432+
if type(note) == "string" and note ~= "" then
433+
return note
434+
end
435+
end
436+
437+
local function SetPlayerNote(userName, userInfo, note)
438+
local notes = LoadPlayerNotes()
439+
local cleanNote = TrimPlayerNote(note)
440+
if type(note) == "string" and #(note:gsub("^%s+", ""):gsub("%s+$", "")) > 100 and WG.Chobby and WG.Chobby.InformationPopup then
441+
WG.Chobby.InformationPopup("Player notes are limited to 100 characters. Your note was shortened to 100 characters.", {width = 420, height = 180})
442+
end
443+
local noteKey = GetPlayerNoteKey(userName, userInfo)
444+
local nameNoteKey = "name_" .. tostring(userName or "")
445+
if cleanNote ~= "" then
446+
notes[noteKey] = cleanNote
447+
if nameNoteKey ~= noteKey then
448+
notes[nameNoteKey] = nil
449+
end
450+
else
451+
notes[noteKey] = nil
452+
notes[nameNoteKey] = nil
453+
end
454+
455+
-- Client-side only: persisted to a local file and never sent through lobby.
456+
SavePlayerNotes()
457+
end
458+
459+
local function OpenPlayerNotesWindow(userName, userInfo)
460+
if not WG.TextEntryWindow then
461+
if WG.Chobby and WG.Chobby.InformationPopup then
462+
WG.Chobby.InformationPopup("Text entry is not available.")
463+
end
464+
return
465+
end
466+
467+
WG.TextEntryWindow.CreateTextEntryWindow({
468+
defaultValue = GetPlayerNote(userName, userInfo) or "",
469+
caption = "Add Player Notes",
470+
labelCaption = "Local note for " .. userName .. ". 100 character limit. Leave blank to remove the note.",
471+
hint = "Enter a local player note, up to 100 characters",
472+
height = 260,
473+
width = 520,
474+
oklabel = "Save",
475+
OnAccepted = function(newNote)
476+
SetPlayerNote(userName, userInfo, newNote)
477+
end,
478+
OnOpen = function(editBox)
479+
editBox:SelectAll()
480+
end
481+
})
482+
end
483+
360484
local function GetUserComboBoxOptions(userName, isInBattle, control, showTeamColor, showSide)
361485
local Configuration = WG.Chobby.Configuration
362486
local info = control.lobby:GetUser(userName) or {}
@@ -400,6 +524,7 @@ local function GetUserComboBoxOptions(userName, isInBattle, control, showTeamCol
400524
if bs.aiLib then comboOptions[#comboOptions + 1] = "Clone AI" end
401525
if bs.aiLib and bs.owner == myUserName and isInBattle then comboOptions[#comboOptions + 1] = "Remove" end
402526
if not itsme and not info.isBot and not bs.aiLib then comboOptions[#comboOptions + 1] = "Report User" end
527+
if not (info.isBot or bs.aiLib) then comboOptions[#comboOptions + 1] = "Add Player Notes" end
403528
comboOptions[#comboOptions + 1] = "Copy Name"
404529
if (iAmBoss or iPlay) and not (control.isSingleplayer or bs.aiLib or info.isBot) and isInBattle then comboOptions[#comboOptions + 1] = "\255\128\128\128" .. "--------------"
405530
comboOptions[#comboOptions + 1] = isBoss and "Disable Boss" or "Make Boss" end
@@ -1285,6 +1410,9 @@ local function GetUserControls(userName, opts)
12851410
local chatWindow = WG.Chobby.interfaceRoot.OpenPrivateChat(userName)
12861411
elseif selectedName == "Copy Name" then
12871412
Spring.SetClipboard(userName)
1413+
elseif selectedName == "Add Player Notes" then
1414+
local latestUserInfo = userControls.lobby:GetUser(userName) or userInfo or {}
1415+
OpenPlayerNotesWindow(userName, latestUserInfo)
12881416
elseif selectedName == "Kickban" then
12891417
local function YesFunc()
12901418
lobby:SayBattle("!kickban "..userName)
@@ -2004,7 +2132,9 @@ end
20042132
local userHandler = {
20052133
CountryShortnameToFlag = CountryShortnameToFlag,
20062134
GetUserRankImage = GetUserRankImage,
2007-
GetClanImage = GetClanImage
2135+
GetClanImage = GetClanImage,
2136+
GetPlayerNote = GetPlayerNote,
2137+
SetPlayerNote = SetPlayerNote
20082138
}
20092139

20102140
function userHandler.SetTooltipBattle(battle)

LuaMenu/widgets/gui_tooltip.lua

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,10 @@ local function GetUserTooltip(userName, userInfo, userBattleInfo, inBattleroom)
650650
end
651651

652652
local width = 240
653+
local playerNote = WG.UserHandler and WG.UserHandler.GetPlayerNote and WG.UserHandler.GetPlayerNote(userName, userInfo)
654+
if playerNote then
655+
width = 320
656+
end
653657
if not userTooltip.mainControl then
654658
userTooltip.mainControl = Chili.Control:New {
655659
x = 0,
@@ -993,6 +997,31 @@ local function GetUserTooltip(userName, userInfo, userBattleInfo, inBattleroom)
993997
userTooltip.battleInfoHolder:Hide()
994998
end
995999

1000+
if playerNote then
1001+
if not userTooltip.playerNote then
1002+
userTooltip.playerNote = Chili.TextBox:New{
1003+
x = 6,
1004+
y = offset,
1005+
width = width - 12,
1006+
height = 20,
1007+
align = "left",
1008+
parent = userTooltip.mainControl,
1009+
objectOverrideFont = Configuration:GetFont(2, "Nimbus2", {font = "fonts/n019003l.pfb"}),
1010+
objectOverrideHintFont = Configuration:GetFont(2, "Nimbus2"),
1011+
text = "",
1012+
}
1013+
else
1014+
userTooltip.playerNote:Show()
1015+
end
1016+
userTooltip.playerNote:SetText("Note: " .. playerNote)
1017+
userTooltip.playerNote:SetPos(nil, offset, width - 12)
1018+
userTooltip.playerNote:UpdateLayout()
1019+
local noteLines = userTooltip.playerNote.physicalLines and #userTooltip.playerNote.physicalLines or 1
1020+
offset = offset + 20 * math.max(1, noteLines)
1021+
elseif userTooltip.playerNote then
1022+
userTooltip.playerNote:Hide()
1023+
end
1024+
9961025
-- Debug Mode
9971026
if Configuration.debugMode then
9981027

0 commit comments

Comments
 (0)