tdc.lua 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. -- tdc.lua: Traitor debugging utility
  2. --
  3. -- Implements the "/td" chat command.
  4. -- Type "/td help" for info on how to use it.
  5. local TAB = ' '
  6. local function _keyw(msg)
  7. return minetest.colorize('#a0a0ff', msg)
  8. end
  9. local function _info(msg)
  10. return minetest.colorize('#ffff80', msg)
  11. end
  12. local function _stdout(msg)
  13. return minetest.colorize('#80ddee', msg)
  14. end
  15. local function _lsitem(item)
  16. return TAB .. '- ' .. item
  17. end
  18. function list_actions()
  19. local hdr = 'Available commands: '
  20. local msgtab = {}
  21. for cmd in pairs(tdc.actions) do
  22. table.insert(msgtab, _keyw(cmd))
  23. end
  24. return hdr .. table.concat(msgtab, '|')
  25. end
  26. -- return corpse count / info messages for a list <ctab> of node positions
  27. local function list_corpses(ctab)
  28. local node, meta
  29. local count = 0
  30. local corpses = {}
  31. for _, pos in ipairs(ctab) do
  32. node = minetest.get_node(pos)
  33. meta = minetest.get_meta(pos)
  34. if node and node.name and node.name ~= 'air' then
  35. local cinfo = meta:get_string('infotext')
  36. local cname = cinfo:match("[^']+") or node.name
  37. table.insert(corpses, _lsitem(cname .. ' ' .. minetest.pos_to_string(pos)))
  38. else
  39. table.insert(corpses, _lsitem('<nil> ' .. minetest.pos_to_string(pos)))
  40. end
  41. count = count + 1
  42. end
  43. return count, corpses
  44. end
  45. --[[ -------------------- tdc namespace -------------------- ]]--
  46. tdc = {}
  47. function tdc.fix_corpses(td_user, map)
  48. local pmap = map or '*'
  49. local count = 0
  50. for mapid, cplist in pairs(lobby.corpses) do
  51. if pmap == '*' then
  52. lobby.corpse_removal(mapid)
  53. lobby.corpses[mapid] = {}
  54. count = count + 1
  55. else
  56. if mapid:find(pmap) then
  57. lobby.corpse_removal(mapid)
  58. lobby.corpses[mapid] = {}
  59. count = count + 1
  60. minetest.chat_send_player(td_user,
  61. _info('Corpses in ' .. _keyw(mapid) .. ' fixed.'))
  62. break
  63. end
  64. end
  65. end
  66. if count == 0 then
  67. minetest.chat_send_player(td_user, _info('No maps found'))
  68. elseif pmap == '*' then
  69. minetest.chat_send_player(td_user,
  70. _info('Corpses in ' .. tostring(count) .. ' map(s) fixed.'))
  71. end
  72. end
  73. function tdc.fix_map(td_user, map)
  74. local pmap = map or '*'
  75. local count = 0
  76. for mapid, count in pairs(lobby.map) do
  77. if pmap == '*' then
  78. lobby.map[mapid] = 0
  79. lobby.update_maps(mapid)
  80. count = count + 1
  81. else
  82. if mapid:find(pmap) then
  83. lobby.map[mapid] = 0
  84. lobby.update_maps(mapid)
  85. count = count + 1
  86. minetest.chat_send_player(td_user,
  87. _info('Map status of ' .. _keyw(mapid) .. ' fixed.'))
  88. break
  89. end
  90. end
  91. end
  92. if count == 0 then
  93. minetest.chat_send_player(td_user, _info('No maps found'))
  94. elseif pmap == '*' then
  95. minetest.chat_send_player(td_user,
  96. _info('Status data for ' .. tostring(count) .. ' map(s) fixed.'))
  97. end
  98. end
  99. -- Return active players whose player name matches `expr`
  100. function tdc.active_players_matching(expr)
  101. local players = {}
  102. for _, player in pairs(minetest.get_connected_players()) do
  103. local pl_name = player:get_player_name()
  104. if pl_name:find(expr) then
  105. table.insert(players, player)
  106. end
  107. end
  108. return players
  109. end
  110. -- Return active map ids matching `expr`
  111. function tdc.active_mapids_matching(expr)
  112. local mapids = {}
  113. for mapid, count in pairs(lobby.map) do
  114. if mapid:find(expr) and count ~= nil and count > 0 then
  115. mapids[mapid] = true
  116. end
  117. end
  118. -- The lobby itself never gets into lobby.map, so we insert it manually,
  119. -- since we want to treat it just like a map.
  120. local mapid_lobby = 'lobby'
  121. if mapid_lobby:find(expr) then mapids[mapid_lobby] = true end
  122. return mapids
  123. end
  124. -- Return players currently visiting a map whose id is in the list `mapids`
  125. function tdc.players_visiting(mapids)
  126. local players = {}
  127. for pl_name, mapid in pairs(lobby.game) do
  128. local mid = mapid
  129. if mapid:find("_solo$") then
  130. mid = mapid:sub(1, -6)
  131. elseif mapid:find("_ghost$") then
  132. mid = mapid:sub(1, -7)
  133. elseif mapid:find("_builder$") then
  134. mid = mapid:sub(1, -9)
  135. end
  136. if mapids[mid] then
  137. table.insert(players, minetest.get_player_by_name(pl_name))
  138. end
  139. end
  140. return players
  141. end
  142. -- For each player in the `players` list, create an entry in the `index` table,
  143. -- having `player:get_player_name()` as keyword.
  144. function tdc.index_by_name(players, index)
  145. if players == nil or index == nil then return end
  146. for _, player in ipairs(players) do
  147. local pl_name = player:get_player_name()
  148. index[pl_name] = player
  149. end
  150. end
  151. -- Return a list of currently active players where either the player name
  152. -- or the map id they're currently visiting matches `expr`.
  153. function tdc.list_players_matching(expr)
  154. local players = tdc.active_players_matching(expr)
  155. local mapids = tdc.active_mapids_matching(expr)
  156. local visitors = tdc.players_visiting(mapids)
  157. local matches = {}
  158. -- index all matching players by their name
  159. tdc.index_by_name(players, matches)
  160. tdc.index_by_name(visitors, matches)
  161. return matches
  162. end
  163. -- Return a "footprint" representation of the player privileges `privs`
  164. function tdc.privs_footprint(privs)
  165. local s, t, b, c, w, p, m
  166. if privs.server then s = 's' else s = '-' end
  167. if privs.traitor_dev then t = 't' else t = '-' end
  168. if privs.builder then b = 'b' else b = '-' end
  169. if privs.creative then c = 'c' else c = '-' end
  170. if privs.worldeditor then w = 'w' else w = '-' end
  171. if privs.pro_player then p = 'p' else p = '-' end
  172. if privs.multihome then m = 'm' else m = '-' end
  173. return table.concat({s, t, b, c, w, p, m})
  174. end
  175. -- tdc.actions: enumerates the possible /td commands
  176. -- tdc.actions = { cmd_1 = def_1, cmd_2 = def_2, ... }
  177. --
  178. -- command definitions are maps:
  179. -- def_n = {
  180. -- info = (string, short description for "/td help")
  181. -- help = (0-arg function, returns a longer description for "/td help <action>")
  182. -- exec = (2-arg function, executes the action for player named <arg1> with the rest
  183. -- of the cmdline string passed in as <arg2>)
  184. -- }
  185. tdc.actions = {
  186. -- CMD: /td help
  187. help = {
  188. info = 'Show general help and list available commands',
  189. help = function()
  190. local msgtab = {
  191. 'Traitor debugging utility. Type "' .. _keyw('/td <cmd>') .. '" to execute a command.',
  192. 'Available commands:'
  193. }
  194. for cmd, action in pairs(tdc.actions) do
  195. table.insert(msgtab, string.format(TAB .. '%-10s\t%s', cmd, action.info))
  196. end
  197. table.insert(msgtab, '\nCommands can be abbreviated (e.g., "/td h" shows this help).')
  198. table.insert(msgtab, 'Type "' .. _keyw('/td help <cmd>') .. '" to get help for a specific command.')
  199. return table.concat(msgtab, '\n')
  200. end,
  201. exec = function(td_user, params)
  202. if params then
  203. local par1 = params:match('[%w_]+')
  204. if par1 and tdc.actions[par1] then
  205. minetest.chat_send_player(td_user, tdc.actions[par1].help())
  206. elseif par1 then
  207. -- TODO: find par1 as cmd prefix in tdc.actions
  208. local msg = _info('Unknown command "' .. par1 .. '"\n') .. list_actions()
  209. minetest.chat_send_player(td_user, msg)
  210. else
  211. minetest.chat_send_player(td_user, tdc.actions['help'].help())
  212. end
  213. else
  214. minetest.chat_send_player(td_user, tdc.actions['help'].help())
  215. end
  216. end,
  217. },
  218. -- CMD: /td corpses
  219. corpses = {
  220. info = 'Show info on corpses',
  221. help = function()
  222. local msgtab = {
  223. _info('Usage: /td corpses [<map>]'),
  224. 'Show corpses in map <map>. If <map> is omitted, list all corpses.',
  225. 'Params:',
  226. TAB .. '<map> map id to search, or "*" to list all corpses'
  227. }
  228. return table.concat(msgtab, '\n')
  229. end,
  230. exec = function(td_user, params)
  231. local map = params:match('[%w_]+') or '*'
  232. local hdr, msg, ccount, clist
  233. if params == '' then
  234. -- list all corpses instead
  235. hdr = _info('List of corpses:\n')
  236. for mid, ctab in pairs(lobby.corpses) do
  237. local corpses
  238. if ctab then ccount = list_corpses(ctab) end
  239. if ccount > 0 then
  240. msg = (msg or '') .. _lsitem(mid .. ': ' .. tostring(ccount)) .. '\n'
  241. end
  242. end
  243. else
  244. for mid, ctab in pairs(lobby.corpses) do
  245. if ctab and (mid:find(map) or map == '*') then
  246. hdr = _info('Corpses in ') .. _keyw(mid) .. ':\n'
  247. ccount, clist = list_corpses(ctab)
  248. if ccount > 0 then
  249. msg = (msg or '') .. hdr .. table.concat(clist, '\n') .. '\n'
  250. end
  251. hdr = ''
  252. if map ~= '*' then break end
  253. end
  254. end
  255. end
  256. if msg then
  257. msg = hdr .. msg
  258. else
  259. msg = _info('No corpses yet.')
  260. end
  261. minetest.chat_send_player(td_user, msg)
  262. minetest.log('action', minetest.strip_colors(msg))
  263. end,
  264. },
  265. -- CMD: /td maps
  266. maps = {
  267. info = 'Show maps currently visited by players',
  268. help = function()
  269. local msgtab = {
  270. _info('Usage: /td maps'),
  271. 'Show map names of all currently active game sessions.'
  272. }
  273. return table.concat(msgtab, '\n')
  274. end,
  275. exec = function(td_user, params)
  276. local msgtab = {_info('Active maps:')}
  277. local plr_count = 0
  278. local msg
  279. for mapid, plr_count in pairs(lobby.map) do
  280. local gh_count = 0
  281. local gh_map = mapid .. '_ghost'
  282. for _, mid in pairs(lobby.game) do
  283. if mid == gh_map then
  284. gh_count = gh_count + 1
  285. end
  286. end
  287. if plr_count > 0 or gh_count > 0 then
  288. table.insert(msgtab,
  289. _lsitem(mapid .. ': ' .. plr_count .. ' player(s)')
  290. .. ' / ' .. _keyw(tostring(gh_count) .. ' ghost(s)'))
  291. end
  292. end
  293. local clobby = 0
  294. for _, mid in pairs(lobby.game) do
  295. if mid == 'lobby' then
  296. clobby = clobby + 1
  297. end
  298. end
  299. if clobby > 0 then
  300. table.insert(msgtab, _lsitem('lobby: ' .. tostring(clobby) .. ' player(s)'))
  301. end
  302. msg = table.concat(msgtab, '\n')
  303. minetest.chat_send_player(td_user, msg)
  304. minetest.log('action', minetest.strip_colors(msg))
  305. end,
  306. },
  307. -- CMD: /td traitors
  308. traitors = {
  309. info = 'Show the traitors in each active map',
  310. help = function()
  311. local msgtab = {
  312. _info('Usage: /td traitors'),
  313. 'Show the names of all traitors in currently active game sessions'
  314. }
  315. return table.concat(msgtab, '\n')
  316. end,
  317. exec = function(td_user, params)
  318. local msg = ''
  319. for mapid, traitor in pairs(lobby.traitors) do
  320. -- table value could be nil if entry is to be GC'd
  321. if traitor then
  322. msg = msg .. _lsitem(mapid .. ': ' .. traitor) .. '\n'
  323. end
  324. end
  325. if msg == '' then
  326. msg = _info('No active traitors!')
  327. else
  328. msg = _info('Active traitors:\n') .. msg
  329. end
  330. minetest.chat_send_player(td_user, msg)
  331. minetest.log('action', minetest.strip_colors(msg))
  332. end,
  333. },
  334. -- CMD: /td players <id>
  335. players = {
  336. info = 'Show player attributes',
  337. help = function()
  338. local msgtab = {
  339. _info('Usage: /td players <expr>'),
  340. 'Show metadata of one or more players.',
  341. 'Params:',
  342. TAB .. '<expr> Part of a player name or map ID used as search expression.',
  343. TAB .. ' Any matching player name will be included in the result list.',
  344. TAB .. ' If <expr> is part of a map ID, lists all players currently',
  345. TAB .. ' visiting that map.',
  346. '\nNote: <expr> is restricted to alphanumeric chars and "_", for the sake of security.',
  347. '\nThe metadata of matching players is listed in a table with the following columns:',
  348. TAB .. 'Player displays the player\'s login name',
  349. TAB .. 'Map shows the map id the player is currently visiting',
  350. TAB .. 'Privs shows the state of some more widely used traitor privileges, which is',
  351. TAB .. ' a short string where each letter symbolizes a certain privilege',
  352. TAB .. ' (' ..
  353. _stdout('s') .. 'erver, ' ..
  354. _stdout('t') .. 'raitor_dev, ' ..
  355. _stdout('b') .. 'uilder, ' ..
  356. _stdout('c') .. 'reative, ' ..
  357. _stdout('w') .. 'orldeditor, ' ..
  358. _stdout('p') .. 'ro_player, ' ..
  359. _stdout('m') .. 'ultihome)',
  360. TAB .. ' If a player does not have a certain privilege, the privilege\'s letter',
  361. TAB .. ' is replaced by a \'-\' instead.',
  362. TAB .. 'Mode displays the player mode',
  363. TAB .. 'Tone shows the player\'s current tone color',
  364. TAB .. 'Spawn prints the player\'s current spawn position (if any)',
  365. }
  366. return table.concat(msgtab, '\n')
  367. end,
  368. exec = function(td_user, params)
  369. if not params or not params:find("[%w_]+") then
  370. minetest.chat_send_player(td_user, 'Missing argument, type "/td help players" for help.')
  371. else
  372. local p1, p2, expr = params:find('([%w_]+)')
  373. local matches = tdc.list_players_matching(expr)
  374. local count = 0
  375. local mtab = {
  376. _info('Player Map Privs Mode Tone Spawn'),
  377. '--------------------------------------------------------------------------------------',
  378. }
  379. -- sort list by player name
  380. local sorted = {}
  381. for name in pairs(matches) do table.insert(sorted, name) end
  382. table.sort(sorted)
  383. for _, name in pairs(sorted) do
  384. count = count + 1
  385. local player = matches[name]
  386. local attr = player:get_meta()
  387. local pl_name = _keyw(name)
  388. local padding = 20 - name:len()
  389. local pl_padding = string.rep(' ', padding)
  390. local pl_map = lobby.game[name] or '<nil>'
  391. local pl_privs = tdc.privs_footprint(minetest.get_player_privs(name))
  392. local pl_mode = attr:get_string('mode') or '<nil>'
  393. local pl_tone = attr:get_int('tone')
  394. local pl_spawn = attr:get_string('spawn_pos') or '<nil>'
  395. table.insert(mtab, string.format('%s%s %-20s %-7s %-7s %5d %-20s',
  396. pl_name, pl_padding, pl_map, pl_privs, pl_mode, pl_tone, pl_spawn)
  397. )
  398. end
  399. if count == 0 then
  400. minetest.chat_send_player(td_user, _info('No match'))
  401. else
  402. minetest.chat_send_player(td_user, table.concat(mtab, '\n'))
  403. end
  404. end
  405. end,
  406. },
  407. -- CMD: /td fix (corpses|map) <map>
  408. --[[
  409. fix corpses: call lobby.corpse_removal(<map>), then reset its poslist
  410. fix maps: set lobby.map[<map>] = 0, then call lobby.update_maps(<map>)
  411. --]]
  412. fix = {
  413. info = 'Fix internal data',
  414. help = function()
  415. local msgtab = {
  416. _info('Usage: /td fix <type> <map>'),
  417. 'Try to repair damages to game data.',
  418. 'Params:',
  419. TAB .. '<type> One of the following:',
  420. TAB .. ' ' .. _keyw('corpses') .. ' -- fix lobby.corpses',
  421. TAB .. ' ' .. _keyw('maps') .. ' -- fix lobby.map and player status',
  422. TAB .. '<map> map id to fix, or "*" to fix all active maps',
  423. '\nNote that you can abbreviate both <type> and <map> parameters to a',
  424. 'prefix, e.g. \"' .. _keyw('/td fix c *') .. '\" tries to fix corpses in all maps.'
  425. }
  426. return table.concat(msgtab, '\n')
  427. end,
  428. exec = function(td_user, params)
  429. local helpcmd = 'type "/td help fix" for help.'
  430. if not params then
  431. minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
  432. else
  433. local p1, p2, fix, map
  434. print (params)
  435. p1, p2, fix, map = params:find('(%w+)%s+([%w_*]+)')
  436. if not fix or not map then
  437. minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
  438. else
  439. if string.find('corpses', fix) then
  440. tdc.fix_corpses(td_user, map)
  441. elseif string.find('map', fix) then
  442. tdc.fix_map(td_user, map)
  443. else
  444. minetest.chat_send_player(td_user, _info('Unknown fix action, ' .. helpcmd))
  445. end
  446. end
  447. end
  448. end,
  449. },
  450. --CMD: /td mode(mode)
  451. mode = {
  452. info = 'Switch your play mode',
  453. help = function()
  454. local msgtab = {
  455. _info('Usage /td mode <mode>'),
  456. 'Changes your playing mode.',
  457. 'Params:',
  458. TAB .. '<mode> One of the following:',
  459. TAB .. 'builder, ghost, player, solo, traitor'
  460. }
  461. return table.concat(msgtab, '\n')
  462. end,
  463. exec = function(td_user, params)
  464. local helpcmd = 'type "/td help mode" for help.'
  465. if not params then
  466. minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
  467. else
  468. local modes = ' builder, ghost, player, solo, traitor'
  469. if string.find(modes, params) then
  470. local mode = string.sub(params, 2, -1)
  471. local player = minetest.get_player_by_name(td_user)
  472. local player_attributes = player:get_meta()
  473. player_attributes:set_string('mode', mode)
  474. minetest.chat_send_player(td_user, _info('Switched you to '..mode..' mode.'))
  475. else
  476. minetest.chat_send_player(td_user, _info('Missing or bad arguments, ' .. helpcmd))
  477. end
  478. end
  479. end,
  480. }
  481. }
  482. function tdc.exec(td_user, params)
  483. local cmd = nil
  484. if params and params ~= '' then
  485. local par1 = params:match('[%w_]+')
  486. local parN = params:sub(par1:len() + 1) or ''
  487. local cname
  488. if tdc.actions[par1] then
  489. cname = par1
  490. cmd = tdc.actions[par1]
  491. else
  492. -- try cmd prefix match
  493. for cmdname in pairs(tdc.actions) do
  494. if cmdname:find(par1) == 1 then
  495. cname = cmdname
  496. cmd = tdc.actions[cmdname]
  497. end
  498. end
  499. end
  500. if cname then
  501. minetest.log('action', td_user .. ' runs "/td ' .. cname .. parN .. '"')
  502. cmd.exec(td_user, parN)
  503. else
  504. minetest.chat_send_player(td_user,
  505. _info('Unknown command "' .. par1 .. '", type "/td help" for possible commands.'))
  506. end
  507. else
  508. minetest.chat_send_player(td_user, list_actions())
  509. end
  510. return true
  511. end
  512. minetest.register_chatcommand('td', {
  513. privs = {traitor_dev = true},
  514. params = '<cmd> [<args>]',
  515. description = 'Traitor debugging commands. Type "/td help" for a list of possible commands.',
  516. func = tdc.exec
  517. })