chatcolor/init.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)