302 lines
9.4 KiB
Lua
302 lines
9.4 KiB
Lua
-- random-geek's colored chat CSM
|
|
|
|
local MESSAGE_TYPES = {"chat", "me", "join", "leave", "dm"}
|
|
local DEFAULT_COLOR = "#FFFFFF"
|
|
|
|
local data = minetest.get_mod_storage()
|
|
local guiRow = 1 -- Which row in the GUI is selected
|
|
|
|
-- Make sure all our defaults are in place.
|
|
for _, type in ipairs(MESSAGE_TYPES) do
|
|
local key = "default_" .. type
|
|
if data:get_string(key) == "" then
|
|
data:set_string(key, DEFAULT_COLOR)
|
|
end
|
|
end
|
|
|
|
-- Removes Minetest \x1b escape sequences (colors, translations) from a string.
|
|
--
|
|
-- This may give wrong output for malformed strings or backslash-escaped right
|
|
-- parentheses within an escape string. See unescape_enriched() in
|
|
-- minetest/src/util/string.h for a correct implementation.
|
|
local function remove_escapes(str)
|
|
-- Remove \x1b followed by anything in parentheses
|
|
str = string.gsub(str, "\x1b%(.-%)", "")
|
|
-- Remove \x1b followed by a single character
|
|
str = string.gsub(str, "\x1b.", "")
|
|
return str
|
|
end
|
|
|
|
-- Test remove_escapes()
|
|
-- assert(remove_escapes("\x1b\xb1(Escapes\x1b() are\x1bE cool!\x1b(T@__Az-1\\\\))") == "(Escapes are cool!)")
|
|
|
|
-- Find the type and source of a chat message.
|
|
local function message_info(richMsg)
|
|
-- Strip any translation data to get a plaintext English message.
|
|
local msg = remove_escapes(richMsg)
|
|
local type, name
|
|
|
|
if string.sub(msg, 1, 1) == "<" then -- Normal chat messages (<player> message)
|
|
type = "chat"
|
|
name = string.match(msg, "^<([1-9A-Za-z-_]+)> ")
|
|
elseif string.sub(msg, 1, 2) == "* " then -- /me messages (* player message)
|
|
type = "me"
|
|
name = string.match(msg, "^%* ([1-9A-Za-z-_]+) ")
|
|
elseif string.sub(msg, 1, 4) == "*** " then -- Join/leave messages (*** player joined/left the game.)
|
|
local tempName, typeStr = string.match(msg, "^%*%*%* ([1-9A-Za-z-_]+) (%a+)")
|
|
name = tempName
|
|
if typeStr == "joined" then
|
|
type = "join"
|
|
elseif typeStr == "left" then
|
|
type = "leave"
|
|
end
|
|
elseif string.sub(msg, 1, 8) == "DM from " then -- Direct messages (DM from player: message)
|
|
type = "dm"
|
|
-- PM was switched to DM in Minetest 5.1.0, this supports both.
|
|
name = string.match(msg, "^DM from ([1-9A-Za-z-_]+): ")
|
|
end
|
|
|
|
-- Note: either may be nil.
|
|
if type and name then
|
|
return {type = type, name = name}
|
|
end
|
|
-- Unrecognized message types will return nil.
|
|
end
|
|
|
|
local function table_contains(tab, target)
|
|
for _, val in ipairs(tab) do
|
|
if val == target then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Set player/default color.
|
|
-- name: player name or default_something
|
|
-- color: HTML string, hex color ('#' will be prepended if necessary), or nil to delete entry.
|
|
local function set_color(name, color)
|
|
if not name or name == "" then
|
|
minetest.display_chat_message("Player or setting name required.")
|
|
return
|
|
elseif not string.match(name, "^[1-9A-Za-z-_]+$") then
|
|
minetest.display_chat_message(string.format("Invalid player or setting name '%s'.", name))
|
|
return
|
|
elseif color == "" then
|
|
minetest.display_chat_message("Color (hex color or color name) required.")
|
|
return
|
|
end
|
|
|
|
local key
|
|
if string.sub(name, 1, 8) == "default_" then
|
|
if not table_contains(MESSAGE_TYPES, string.sub(name, 9)) then
|
|
minetest.display_chat_message(string.format("No setting called '%s'.", name))
|
|
return
|
|
end
|
|
|
|
if not color then
|
|
minetest.display_chat_message("Cannot delete defaults!")
|
|
return
|
|
end
|
|
key = name
|
|
else
|
|
-- Note: Commands/GUI omit the player prefix.
|
|
key = "player_" .. name
|
|
end
|
|
|
|
-- Check color if it exists.
|
|
if color then
|
|
-- Prepend '#' to hex colors if necessary.
|
|
local newColor = color
|
|
if tonumber(newColor, 16) then
|
|
newColor = "#" .. newColor
|
|
end
|
|
|
|
if not minetest.colorspec_to_colorstring(newColor) then
|
|
minetest.display_chat_message(string.format("Invalid color name '%s'.", color))
|
|
return
|
|
end
|
|
|
|
data:set_string(key, newColor)
|
|
minetest.display_chat_message(
|
|
string.format("Set color for %s to %s.", name, minetest.colorize(newColor, newColor))
|
|
)
|
|
else -- Delete player color entry
|
|
data:set_string(key, "")
|
|
minetest.display_chat_message(string.format("Deleted color for %s.", name))
|
|
end
|
|
end
|
|
|
|
-- Return a nicely sorted array of {name, color} pairs.
|
|
local function get_list()
|
|
local list = data:to_table().fields
|
|
local arr = {}
|
|
|
|
-- List players, excluding player_ prefix.
|
|
for key, color in pairs(list) do
|
|
if string.sub(key, 1, 7) == "player_" then
|
|
local name = string.sub(key, 8)
|
|
arr[#arr + 1] = {name, color}
|
|
end
|
|
end
|
|
|
|
-- Sort alphabetically.
|
|
table.sort(
|
|
arr,
|
|
function(a, b)
|
|
return a[1] < b[1]
|
|
end
|
|
)
|
|
|
|
-- List defaults at end
|
|
for _, type in ipairs(MESSAGE_TYPES) do
|
|
local key = "default_" .. type
|
|
local color = list[key]
|
|
arr[#arr + 1] = {key, color}
|
|
end
|
|
|
|
return arr
|
|
end
|
|
|
|
local function get_formspec(modify, defaultPlayer, defaultColor)
|
|
if not modify then -- Fetch main screen
|
|
local list = get_list()
|
|
|
|
-- Convert list to formspec-friendly format
|
|
local tableRows = {}
|
|
for _, row in ipairs(list) do
|
|
tableRows[#tableRows + 1] = row[1] .. "," .. row[2] .. "," .. row[2]
|
|
end
|
|
local tableString = table.concat(tableRows, ",")
|
|
|
|
return [[
|
|
formspec_version[5]
|
|
size[8,9]
|
|
label[0.5,0.6;Colored Chat]
|
|
button[0.5,1;2.1,0.8;main_modify;Modify...]
|
|
button[2.9,1;2.2,0.8;main_delete;Delete]
|
|
button[5.4,1;2.1,0.8;main_add;Add...]
|
|
tablecolumns[text;color;text]
|
|
table[0.5,2.1;7,5.3;main_table;]] .. tableString .. [[;]] .. guiRow .. [[]
|
|
button_exit[0.5,7.7;2.1,0.8;exit;Exit]
|
|
tooltip[main_modify;Change the color for the selected element]
|
|
tooltip[main_delete;Delete the selected element]
|
|
tooltip[main_add;Add a color definition]
|
|
]]
|
|
else -- Fetch modify screen
|
|
return [[
|
|
formspec_version[5]
|
|
size[8,3.2]
|
|
field[0.5,0.8;3.4,0.8;mod_player;Player;]] .. defaultPlayer .. [[]
|
|
field[4.1,0.8;3.4,0.8;mod_color;HTML/hex color;]] .. defaultColor .. [[]
|
|
button[5,1.9;2.5,0.8;mod_set;Set]
|
|
button[0.5,1.9;2.5,0.8;mod_cancel;Cancel]
|
|
]]
|
|
end
|
|
end
|
|
|
|
minetest.register_on_formspec_input(function(formname, fields)
|
|
-- Ignore potential formspecs from other mods.
|
|
if string.sub(formname, 1, 10) ~= "chatcolor:" then
|
|
return
|
|
end
|
|
|
|
-- Update the selected row index if needed.
|
|
if fields.main_table then
|
|
local event = minetest.explode_table_event(fields.main_table)
|
|
if event.type == "CHG" or event.type == "DCL" then
|
|
guiRow = event.row
|
|
end
|
|
end
|
|
|
|
if fields.main_delete then
|
|
local list = get_list()
|
|
local key = list[guiRow][1]
|
|
set_color(key, nil)
|
|
minetest.show_formspec("chatcolor:maingui", get_formspec())
|
|
elseif fields.main_modify then
|
|
local row = get_list()[guiRow]
|
|
-- Get formspec and send selected name to modify screen
|
|
minetest.show_formspec("chatcolor:modify", get_formspec(true, row[1], row[2]))
|
|
elseif fields.main_add then
|
|
minetest.show_formspec("chatcolor:modify", get_formspec(true, "", ""))
|
|
elseif (fields.mod_set or fields.key_enter) and fields.mod_player and fields.mod_color then
|
|
set_color(fields.mod_player, fields.mod_color)
|
|
minetest.show_formspec("chatcolor:maingui", get_formspec())
|
|
elseif fields.mod_cancel then
|
|
minetest.show_formspec("chatcolor:maingui", get_formspec())
|
|
end
|
|
end)
|
|
|
|
minetest.register_chatcommand("colors", {
|
|
params = "",
|
|
description = "Display colored chat GUI.",
|
|
func = function(param)
|
|
guiRow = 1 -- Select first row of table
|
|
minetest.show_formspec("chatcolor:maingui", get_formspec())
|
|
end
|
|
})
|
|
|
|
minetest.register_chatcommand("setcolor", { -- Assign a color to chat messages from a specific person
|
|
params = "<name> <color>",
|
|
description = "Colorize a specified player's chat messages.",
|
|
func = function(param)
|
|
local args = string.split(param, " ")
|
|
-- If color is empty, pass an empty string to avoid deleting the entry.
|
|
set_color(args[1], args[2] or "")
|
|
end
|
|
})
|
|
|
|
minetest.register_chatcommand("delcolor", {
|
|
params = "<name>",
|
|
description = "Set a specified player's chat messages to the default color.",
|
|
func = function(param)
|
|
set_color(param, nil)
|
|
end
|
|
})
|
|
|
|
minetest.register_chatcommand("listcolors", {
|
|
params = "",
|
|
description = "List player/color pairs.",
|
|
func = function(param)
|
|
local list = get_list()
|
|
for _, row in ipairs(list) do
|
|
minetest.display_chat_message(row[1] .. ", " .. minetest.colorize(row[2], row[2]))
|
|
end
|
|
end
|
|
})
|
|
|
|
-- I don't remember if or why `register_on_mods_loaded` was necessary.
|
|
minetest.register_on_mods_loaded(function()
|
|
minetest.register_on_receiving_chat_message(function(message)
|
|
local plain = minetest.strip_colors(message)
|
|
local info = message_info(plain)
|
|
|
|
if info then -- Normal chat/me/join messages
|
|
local color = data:get_string("player_" .. info.name)
|
|
if color == "" then -- If no color, set to default
|
|
color = data:get_string("default_" .. info.type)
|
|
end
|
|
|
|
local colorized = minetest.colorize(color, plain)
|
|
minetest.display_chat_message(colorized)
|
|
return true -- Override the original chat
|
|
elseif string.sub(plain, 1, 2) == "# " then -- /status message
|
|
local colorized = plain
|
|
|
|
local list = data:to_table().fields
|
|
for key, color in pairs(list) do
|
|
if string.sub(key, 1, 7) == "player_" then
|
|
local playerName = string.sub(key, 8)
|
|
-- Replace plain name with colored version
|
|
colorized = string.gsub(colorized, playerName, minetest.colorize(color, playerName))
|
|
end
|
|
end
|
|
|
|
minetest.display_chat_message(colorized)
|
|
return true -- Override the original chat
|
|
end
|
|
end)
|
|
end)
|