123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- -- tdc.lua: Traitor debugging utility
- --
- -- Implements the "/td" chat command.
- -- Type "/td help" for info on how to use it.
- local TAB = ' '
- local function _keyw(msg)
- return minetest.colorize('#a0a0ff', msg)
- end
- local function _info(msg)
- return minetest.colorize('#ffff80', msg)
- end
- local function _stdout(msg)
- return minetest.colorize('#80ddee', msg)
- end
- local function _lsitem(item)
- return TAB .. '- ' .. item
- end
- function list_actions()
- local hdr = 'Available commands: '
- local msgtab = {}
- for cmd in pairs(tdc.actions) do
- table.insert(msgtab, _keyw(cmd))
- end
- return hdr .. table.concat(msgtab, '|')
- end
- -- return corpse count / info messages for a list <ctab> of node positions
- local function list_corpses(ctab)
- local node, meta
- local count = 0
- local corpses = {}
- for _, pos in ipairs(ctab) do
- node = minetest.get_node(pos)
- meta = minetest.get_meta(pos)
- if node and node.name and node.name ~= 'air' then
- local cinfo = meta:get_string('infotext')
- local cname = cinfo:match("[^']+") or node.name
- table.insert(corpses, _lsitem(cname .. ' ' .. minetest.pos_to_string(pos)))
- else
- table.insert(corpses, _lsitem('<nil> ' .. minetest.pos_to_string(pos)))
- end
- count = count + 1
- end
- return count, corpses
- end
- --[[ -------------------- tdc namespace -------------------- ]]--
- tdc = {}
- function tdc.fix_corpses(td_user, map)
- local pmap = map or '*'
- local count = 0
- for mapid, cplist in pairs(lobby.corpses) do
- if pmap == '*' then
- lobby.corpse_removal(mapid)
- lobby.corpses[mapid] = {}
- count = count + 1
- else
- if mapid:find(pmap) then
- lobby.corpse_removal(mapid)
- lobby.corpses[mapid] = {}
- count = count + 1
- minetest.chat_send_player(td_user,
- _info('Corpses in ' .. _keyw(mapid) .. ' fixed.'))
- break
- end
- end
- end
- if count == 0 then
- minetest.chat_send_player(td_user, _info('No maps found'))
- elseif pmap == '*' then
- minetest.chat_send_player(td_user,
- _info('Corpses in ' .. tostring(count) .. ' map(s) fixed.'))
- end
- end
- function tdc.fix_map(td_user, map)
- local pmap = map or '*'
- local count = 0
- for mapid, count in pairs(lobby.map) do
- if pmap == '*' then
- lobby.map[mapid] = 0
- lobby.update_maps(mapid)
- count = count + 1
- else
- if mapid:find(pmap) then
- lobby.map[mapid] = 0
- lobby.update_maps(mapid)
- count = count + 1
- minetest.chat_send_player(td_user,
- _info('Map status of ' .. _keyw(mapid) .. ' fixed.'))
- break
- end
- end
- end
- if count == 0 then
- minetest.chat_send_player(td_user, _info('No maps found'))
- elseif pmap == '*' then
- minetest.chat_send_player(td_user,
- _info('Status data for ' .. tostring(count) .. ' map(s) fixed.'))
- end
- end
- -- Return active players whose player name matches `expr`
- function tdc.active_players_matching(expr)
- local players = {}
- for _, player in pairs(minetest.get_connected_players()) do
- local pl_name = player:get_player_name()
- if pl_name:find(expr) then
- table.insert(players, player)
- end
- end
- return players
- end
- -- Return active map ids matching `expr`
- function tdc.active_mapids_matching(expr)
- local mapids = {}
- for mapid, count in pairs(lobby.map) do
- if mapid:find(expr) and count ~= nil and count > 0 then
- mapids[mapid] = true
- end
- end
- -- The lobby itself never gets into lobby.map, so we insert it manually,
- -- since we want to treat it just like a map.
- local mapid_lobby = 'lobby'
- if mapid_lobby:find(expr) then mapids[mapid_lobby] = true end
- return mapids
- end
- -- Return players currently visiting a map whose id is in the list `mapids`
- function tdc.players_visiting(mapids)
- local players = {}
- for pl_name, mapid in pairs(lobby.game) do
- local mid = mapid
- if mapid:find("_solo$") then
- mid = mapid:sub(1, -6)
- elseif mapid:find("_ghost$") then
- mid = mapid:sub(1, -7)
- elseif mapid:find("_builder$") then
- mid = mapid:sub(1, -9)
- end
- if mapids[mid] then
- table.insert(players, minetest.get_player_by_name(pl_name))
- end
- end
- return players
- end
- -- For each player in the `players` list, create an entry in the `index` table,
- -- having `player:get_player_name()` as keyword.
- function tdc.index_by_name(players, index)
- if players == nil or index == nil then return end
- for _, player in ipairs(players) do
- local pl_name = player:get_player_name()
- index[pl_name] = player
- end
- end
- -- Return a list of currently active players where either the player name
- -- or the map id they're currently visiting matches `expr`.
- function tdc.list_players_matching(expr)
- local players = tdc.active_players_matching(expr)
- local mapids = tdc.active_mapids_matching(expr)
- local visitors = tdc.players_visiting(mapids)
- local matches = {}
- -- index all matching players by their name
- tdc.index_by_name(players, matches)
- tdc.index_by_name(visitors, matches)
- return matches
- end
- -- Return a "footprint" representation of the player privileges `privs`
- function tdc.privs_footprint(privs)
- local s, t, b, c, w, p, m
- if privs.server then s = 's' else s = '-' end
- if privs.traitor_dev then t = 't' else t = '-' end
- if privs.builder then b = 'b' else b = '-' end
- if privs.creative then c = 'c' else c = '-' end
- if privs.worldeditor then w = 'w' else w = '-' end
- if privs.pro_player then p = 'p' else p = '-' end
- if privs.multihome then m = 'm' else m = '-' end
- return table.concat({s, t, b, c, w, p, m})
- end
- -- tdc.actions: enumerates the possible /td commands
- -- tdc.actions = { cmd_1 = def_1, cmd_2 = def_2, ... }
- --
- -- command definitions are maps:
- -- def_n = {
- -- info = (string, short description for "/td help")
- -- help = (0-arg function, returns a longer description for "/td help <action>")
- -- exec = (2-arg function, executes the action for player named <arg1> with the rest
- -- of the cmdline string passed in as <arg2>)
- -- }
- tdc.actions = {
- -- CMD: /td help
- help = {
- info = 'Show general help and list available commands',
- help = function()
- local msgtab = {
- 'Traitor debugging utility. Type "' .. _keyw('/td <cmd>') .. '" to execute a command.',
- 'Available commands:'
- }
- for cmd, action in pairs(tdc.actions) do
- table.insert(msgtab, string.format(TAB .. '%-10s\t%s', cmd, action.info))
- end
- table.insert(msgtab, '\nCommands can be abbreviated (e.g., "/td h" shows this help).')
- table.insert(msgtab, 'Type "' .. _keyw('/td help <cmd>') .. '" to get help for a specific command.')
- return table.concat(msgtab, '\n')
- end,
- exec = function(td_user, params)
- if params then
- local par1 = params:match('[%w_]+')
- if par1 and tdc.actions[par1] then
- minetest.chat_send_player(td_user, tdc.actions[par1].help())
- elseif par1 then
- -- TODO: find par1 as cmd prefix in tdc.actions
- local msg = _info('Unknown command "' .. par1 .. '"\n') .. list_actions()
- minetest.chat_send_player(td_user, msg)
- else
- minetest.chat_send_player(td_user, tdc.actions['help'].help())
- end
- else
- minetest.chat_send_player(td_user, tdc.actions['help'].help())
- end
- end,
- },
- -- CMD: /td corpses
- corpses = {
- info = 'Show info on corpses',
- help = function()
- local msgtab = {
- _info('Usage: /td corpses [<map>]'),
- 'Show corpses in map <map>. If <map> is omitted, list all corpses.',
- 'Params:',
- TAB .. '<map> map id to search, or "*" to list all corpses'
- }
- return table.concat(msgtab, '\n')
- end,
- exec = function(td_user, params)
- local map = params:match('[%w_]+') or '*'
- local hdr, msg, ccount, clist
- if params == '' then
- -- list all corpses instead
- hdr = _info('List of corpses:\n')
- for mid, ctab in pairs(lobby.corpses) do
- local corpses
- if ctab then ccount = list_corpses(ctab) end
- if ccount > 0 then
- msg = (msg or '') .. _lsitem(mid .. ': ' .. tostring(ccount)) .. '\n'
- end
- end
- else
- for mid, ctab in pairs(lobby.corpses) do
- if ctab and (mid:find(map) or map == '*') then
- hdr = _info('Corpses in ') .. _keyw(mid) .. ':\n'
- ccount, clist = list_corpses(ctab)
- if ccount > 0 then
- msg = (msg or '') .. hdr .. table.concat(clist, '\n') .. '\n'
- end
- hdr = ''
- if map ~= '*' then break end
- end
- end
- end
- if msg then
- msg = hdr .. msg
- else
- msg = _info('No corpses yet.')
- end
- minetest.chat_send_player(td_user, msg)
- minetest.log('action', minetest.strip_colors(msg))
- end,
- },
- -- CMD: /td maps
- maps = {
- info = 'Show maps currently visited by players',
- help = function()
- local msgtab = {
- _info('Usage: /td maps'),
- 'Show map names of all currently active game sessions.'
- }
- return table.concat(msgtab, '\n')
- end,
- exec = function(td_user, params)
- local msgtab = {_info('Active maps:')}
- local plr_count = 0
- local msg
- for mapid, plr_count in pairs(lobby.map) do
- local gh_count = 0
- local gh_map = mapid .. '_ghost'
- for _, mid in pairs(lobby.game) do
- if mid == gh_map then
- gh_count = gh_count + 1
- end
- end
- if plr_count > 0 or gh_count > 0 then
- table.insert(msgtab,
- _lsitem(mapid .. ': ' .. plr_count .. ' player(s)')
- .. ' / ' .. _keyw(tostring(gh_count) .. ' ghost(s)'))
- end
- end
- local clobby = 0
- for _, mid in pairs(lobby.game) do
- if mid == 'lobby' then
- clobby = clobby + 1
- end
- end
- if clobby > 0 then
- table.insert(msgtab, _lsitem('lobby: ' .. tostring(clobby) .. ' player(s)'))
- end
- msg = table.concat(msgtab, '\n')
- minetest.chat_send_player(td_user, msg)
- minetest.log('action', minetest.strip_colors(msg))
- end,
- },
- -- CMD: /td traitors
- traitors = {
- info = 'Show the traitors in each active map',
- help = function()
- local msgtab = {
- _info('Usage: /td traitors'),
- 'Show the names of all traitors in currently active game sessions'
- }
- return table.concat(msgtab, '\n')
- end,
- exec = function(td_user, params)
- local msg = ''
- for mapid, traitor in pairs(lobby.traitors) do
- -- table value could be nil if entry is to be GC'd
- if traitor then
- msg = msg .. _lsitem(mapid .. ': ' .. traitor) .. '\n'
- end
- end
- if msg == '' then
- msg = _info('No active traitors!')
- else
- msg = _info('Active traitors:\n') .. msg
- end
- minetest.chat_send_player(td_user, msg)
- minetest.log('action', minetest.strip_colors(msg))
- end,
- },
- -- CMD: /td players <id>
- players = {
- info = 'Show player attributes',
- help = function()
- local msgtab = {
- _info('Usage: /td players <expr>'),
- 'Show metadata of one or more players.',
- 'Params:',
- TAB .. '<expr> Part of a player name or map ID used as search expression.',
- TAB .. ' Any matching player name will be included in the result list.',
- TAB .. ' If <expr> is part of a map ID, lists all players currently',
- TAB .. ' visiting that map.',
- '\nNote: <expr> is restricted to alphanumeric chars and "_", for the sake of security.',
- '\nThe metadata of matching players is listed in a table with the following columns:',
- TAB .. 'Player displays the player\'s login name',
- TAB .. 'Map shows the map id the player is currently visiting',
- TAB .. 'Privs shows the state of some more widely used traitor privileges, which is',
- TAB .. ' a short string where each letter symbolizes a certain privilege',
- TAB .. ' (' ..
- _stdout('s') .. 'erver, ' ..
- _stdout('t') .. 'raitor_dev, ' ..
- _stdout('b') .. 'uilder, ' ..
- _stdout('c') .. 'reative, ' ..
- _stdout('w') .. 'orldeditor, ' ..
- _stdout('p') .. 'ro_player, ' ..
- _stdout('m') .. 'ultihome)',
- TAB .. ' If a player does not have a certain privilege, the privilege\'s letter',
- TAB .. ' is replaced by a \'-\' instead.',
- TAB .. 'Mode displays the player mode',
- TAB .. 'Tone shows the player\'s current tone color',
- TAB .. 'Spawn prints the player\'s current spawn position (if any)',
- }
- return table.concat(msgtab, '\n')
- end,
- exec = function(td_user, params)
- if not params or not params:find("[%w_]+") then
- minetest.chat_send_player(td_user, 'Missing argument, type "/td help players" for help.')
- else
- local p1, p2, expr = params:find('([%w_]+)')
- local matches = tdc.list_players_matching(expr)
- local count = 0
- local mtab = {
- _info('Player Map Privs Mode Tone Spawn'),
- '--------------------------------------------------------------------------------------',
- }
- -- sort list by player name
- local sorted = {}
- for name in pairs(matches) do table.insert(sorted, name) end
- table.sort(sorted)
- for _, name in pairs(sorted) do
- count = count + 1
- local player = matches[name]
- local attr = player:get_meta()
- local pl_name = _keyw(name)
- local padding = 20 - name:len()
- local pl_padding = string.rep(' ', padding)
- local pl_map = lobby.game[name] or '<nil>'
- local pl_privs = tdc.privs_footprint(minetest.get_player_privs(name))
- local pl_mode = attr:get_string('mode') or '<nil>'
- local pl_tone = attr:get_int('tone')
- local pl_spawn = attr:get_string('spawn_pos') or '<nil>'
- table.insert(mtab, string.format('%s%s %-20s %-7s %-7s %5d %-20s',
- pl_name, pl_padding, pl_map, pl_privs, pl_mode, pl_tone, pl_spawn)
- )
- end
- if count == 0 then
- minetest.chat_send_player(td_user, _info('No match'))
- else
- minetest.chat_send_player(td_user, table.concat(mtab, '\n'))
- end
- end
- end,
- },
- -- CMD: /td fix (corpses|map) <map>
- --[[
- fix corpses: call lobby.corpse_removal(<map>), then reset its poslist
- fix maps: set lobby.map[<map>] = 0, then call lobby.update_maps(<map>)
- --]]
- fix = {
- info = 'Fix internal data',
- help = function()
- local msgtab = {
- _info('Usage: /td fix <type> <map>'),
- 'Try to repair damages to game data.',
- 'Params:',
- TAB .. '<type> One of the following:',
- TAB .. ' ' .. _keyw('corpses') .. ' -- fix lobby.corpses',
- TAB .. ' ' .. _keyw('maps') .. ' -- fix lobby.map and player status',
- TAB .. '<map> map id to fix, or "*" to fix all active maps',
- '\nNote that you can abbreviate both <type> and <map> parameters to a',
- 'prefix, e.g. \"' .. _keyw('/td fix c *') .. '\" tries to fix corpses in all maps.'
- }
- return table.concat(msgtab, '\n')
- end,
- exec = function(td_user, params)
- local helpcmd = 'type "/td help fix" for help.'
- if not params then
- minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
- else
- local p1, p2, fix, map
- print (params)
- p1, p2, fix, map = params:find('(%w+)%s+([%w_*]+)')
- if not fix or not map then
- minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
- else
- if string.find('corpses', fix) then
- tdc.fix_corpses(td_user, map)
- elseif string.find('map', fix) then
- tdc.fix_map(td_user, map)
- else
- minetest.chat_send_player(td_user, _info('Unknown fix action, ' .. helpcmd))
- end
- end
- end
- end,
- },
- --CMD: /td mode(mode)
- mode = {
- info = 'Switch your play mode',
- help = function()
- local msgtab = {
- _info('Usage /td mode <mode>'),
- 'Changes your playing mode.',
- 'Params:',
- TAB .. '<mode> One of the following:',
- TAB .. 'builder, ghost, player, solo, traitor'
- }
- return table.concat(msgtab, '\n')
- end,
- exec = function(td_user, params)
- local helpcmd = 'type "/td help mode" for help.'
- if not params then
- minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
- else
- local modes = ' builder, ghost, player, solo, traitor'
- if string.find(modes, params) then
- local mode = string.sub(params, 2, -1)
- local player = minetest.get_player_by_name(td_user)
- local player_attributes = player:get_meta()
- player_attributes:set_string('mode', mode)
- minetest.chat_send_player(td_user, _info('Switched you to '..mode..' mode.'))
- else
- minetest.chat_send_player(td_user, _info('Missing or bad arguments, ' .. helpcmd))
- end
- end
- end,
- }
- }
- function tdc.exec(td_user, params)
- local cmd = nil
- if params and params ~= '' then
- local par1 = params:match('[%w_]+')
- local parN = params:sub(par1:len() + 1) or ''
- local cname
- if tdc.actions[par1] then
- cname = par1
- cmd = tdc.actions[par1]
- else
- -- try cmd prefix match
- for cmdname in pairs(tdc.actions) do
- if cmdname:find(par1) == 1 then
- cname = cmdname
- cmd = tdc.actions[cmdname]
- end
- end
- end
- if cname then
- minetest.log('action', td_user .. ' runs "/td ' .. cname .. parN .. '"')
- cmd.exec(td_user, parN)
- else
- minetest.chat_send_player(td_user,
- _info('Unknown command "' .. par1 .. '", type "/td help" for possible commands.'))
- end
- else
- minetest.chat_send_player(td_user, list_actions())
- end
- return true
- end
- minetest.register_chatcommand('td', {
- privs = {traitor_dev = true},
- params = '<cmd> [<args>]',
- description = 'Traitor debugging commands. Type "/td help" for a list of possible commands.',
- func = tdc.exec
- })
|