chatcmdbuilder.lua 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. ChatCmdBuilder = {}
  2. function ChatCmdBuilder.new(name, func, def)
  3. def = def or {}
  4. local cmd = ChatCmdBuilder.build(func)
  5. cmd.def = def
  6. def.func = cmd.run
  7. minetest.register_chatcommand(name, def)
  8. return cmd
  9. end
  10. local STATE_READY = 1
  11. local STATE_PARAM = 2
  12. local STATE_PARAM_TYPE = 3
  13. local bad_chars = {}
  14. bad_chars["("] = true
  15. bad_chars[")"] = true
  16. bad_chars["."] = true
  17. bad_chars["%"] = true
  18. bad_chars["+"] = true
  19. bad_chars["-"] = true
  20. bad_chars["*"] = true
  21. bad_chars["?"] = true
  22. bad_chars["["] = true
  23. bad_chars["^"] = true
  24. bad_chars["$"] = true
  25. local function escape(char)
  26. if bad_chars[char] then
  27. return "%" .. char
  28. else
  29. return char
  30. end
  31. end
  32. local dprint = function() end
  33. ChatCmdBuilder.types = {
  34. pos = "%(? *(%-?[%d.]+) *, *(%-?[%d.]+) *, *(%-?[%d.]+) *%)?",
  35. text = "(.+)",
  36. number = "(%-?[%d.]+)",
  37. int = "(%-?[%d]+)",
  38. word = "([^ ]+)",
  39. alpha = "([A-Za-z]+)",
  40. modname = "([a-z0-9_]+)",
  41. alphascore = "([A-Za-z_]+)",
  42. alphanumeric = "([A-Za-z0-9]+)",
  43. username = "([A-Za-z0-9-_]+)",
  44. }
  45. function ChatCmdBuilder.build(func)
  46. local cmd = {
  47. _subs = {}
  48. }
  49. function cmd:sub(route, func, def)
  50. dprint("Parsing " .. route)
  51. def = def or {}
  52. if string.trim then
  53. route = string.trim(route)
  54. end
  55. local sub = {
  56. pattern = "^",
  57. params = {},
  58. func = func
  59. }
  60. -- End of param reached: add it to the pattern
  61. local param = ""
  62. local param_type = ""
  63. local should_be_eos = false
  64. local function finishParam()
  65. if param ~= "" and param_type ~= "" then
  66. dprint(" - Found param " .. param .. " type " .. param_type)
  67. local pattern = ChatCmdBuilder.types[param_type]
  68. if not pattern then
  69. error("Unrecognised param_type=" .. param_type)
  70. end
  71. sub.pattern = sub.pattern .. pattern
  72. table.insert(sub.params, param_type)
  73. param = ""
  74. param_type = ""
  75. end
  76. end
  77. -- Iterate through the route to find params
  78. local state = STATE_READY
  79. local catching_space = false
  80. local match_space = " " -- change to "%s" to also catch tabs and newlines
  81. local catch_space = match_space.."+"
  82. for i = 1, #route do
  83. local c = route:sub(i, i)
  84. if should_be_eos then
  85. error("Should be end of string. Nothing is allowed after a param of type text.")
  86. end
  87. if state == STATE_READY then
  88. if c == ":" then
  89. dprint(" - Found :, entering param")
  90. state = STATE_PARAM
  91. param_type = "word"
  92. catching_space = false
  93. elseif c:match(match_space) then
  94. print(" - Found space")
  95. if not catching_space then
  96. catching_space = true
  97. sub.pattern = sub.pattern .. catch_space
  98. end
  99. else
  100. catching_space = false
  101. sub.pattern = sub.pattern .. escape(c)
  102. end
  103. elseif state == STATE_PARAM then
  104. if c == ":" then
  105. dprint(" - Found :, entering param type")
  106. state = STATE_PARAM_TYPE
  107. param_type = ""
  108. elseif c:match(match_space) then
  109. print(" - Found whitespace, leaving param")
  110. state = STATE_READY
  111. finishParam()
  112. catching_space = true
  113. sub.pattern = sub.pattern .. catch_space
  114. elseif c:match("%W") then
  115. dprint(" - Found nonalphanum, leaving param")
  116. state = STATE_READY
  117. finishParam()
  118. sub.pattern = sub.pattern .. escape(c)
  119. else
  120. param = param .. c
  121. end
  122. elseif state == STATE_PARAM_TYPE then
  123. if c:match(match_space) then
  124. print(" - Found space, leaving param type")
  125. state = STATE_READY
  126. finishParam()
  127. catching_space = true
  128. sub.pattern = sub.pattern .. catch_space
  129. elseif c:match("%W") then
  130. dprint(" - Found nonalphanum, leaving param type")
  131. state = STATE_READY
  132. finishParam()
  133. sub.pattern = sub.pattern .. escape(c)
  134. else
  135. param_type = param_type .. c
  136. end
  137. end
  138. end
  139. dprint(" - End of route")
  140. finishParam()
  141. sub.pattern = sub.pattern .. "$"
  142. dprint("Pattern: " .. sub.pattern)
  143. table.insert(self._subs, sub)
  144. end
  145. if func then
  146. func(cmd)
  147. end
  148. cmd.run = function(name, param)
  149. for i = 1, #cmd._subs do
  150. local sub = cmd._subs[i]
  151. local res = { string.match(param, sub.pattern) }
  152. if #res > 0 then
  153. local pointer = 1
  154. local params = { name }
  155. for j = 1, #sub.params do
  156. local param = sub.params[j]
  157. if param == "pos" then
  158. local pos = {
  159. x = tonumber(res[pointer]),
  160. y = tonumber(res[pointer + 1]),
  161. z = tonumber(res[pointer + 2])
  162. }
  163. table.insert(params, pos)
  164. pointer = pointer + 3
  165. elseif param == "number" or param == "int" then
  166. table.insert(params, tonumber(res[pointer]))
  167. pointer = pointer + 1
  168. else
  169. table.insert(params, res[pointer])
  170. pointer = pointer + 1
  171. end
  172. end
  173. if table.unpack then
  174. -- lua 5.2 or later
  175. return sub.func(table.unpack(params))
  176. else
  177. -- lua 5.1 or earlier
  178. return sub.func(unpack(params))
  179. end
  180. end
  181. end
  182. return false, "Invalid command"
  183. end
  184. return cmd
  185. end
  186. local function run_tests()
  187. if not (ChatCmdBuilder.build(function(cmd)
  188. cmd:sub("bar :one and :two:word", function(name, one, two)
  189. if name == "singleplayer" and one == "abc" and two == "def" then
  190. return true
  191. end
  192. end)
  193. end)).run("singleplayer", "bar abc and def") then
  194. error("Test 1 failed")
  195. end
  196. local move = ChatCmdBuilder.build(function(cmd)
  197. cmd:sub("move :target to :pos:pos", function(name, target, pos)
  198. if name == "singleplayer" and target == "player1" and
  199. pos.x == 0 and pos.y == 1 and pos.z == 2 then
  200. return true
  201. end
  202. end)
  203. end).run
  204. if not move("singleplayer", "move player1 to 0,1,2") then
  205. error("Test 2 failed")
  206. end
  207. if not move("singleplayer", "move player1 to (0,1,2)") then
  208. error("Test 3 failed")
  209. end
  210. if not move("singleplayer", "move player1 to 0, 1,2") then
  211. error("Test 4 failed")
  212. end
  213. if not move("singleplayer", "move player1 to 0 ,1, 2") then
  214. error("Test 5 failed")
  215. end
  216. if not move("singleplayer", "move player1 to 0, 1, 2") then
  217. error("Test 6 failed")
  218. end
  219. if not move("singleplayer", "move player1 to 0 ,1 ,2") then
  220. error("Test 7 failed")
  221. end
  222. if not move("singleplayer", "move player1 to ( 0 ,1 ,2)") then
  223. error("Test 8 failed")
  224. end
  225. if move("singleplayer", "move player1 to abc,def,sdosd") then
  226. error("Test 9 failed")
  227. end
  228. if move("singleplayer", "move player1 to abc def sdosd") then
  229. error("Test 10 failed")
  230. end
  231. if not (ChatCmdBuilder.build(function(cmd)
  232. cmd:sub("does :one:int plus :two:int equal :three:int", function(name, one, two, three)
  233. if name == "singleplayer" and one + two == three then
  234. return true
  235. end
  236. end)
  237. end)).run("singleplayer", "does 1 plus 2 equal 3") then
  238. error("Test 11 failed")
  239. end
  240. local checknegint = ChatCmdBuilder.build(function(cmd)
  241. cmd:sub("checknegint :x:int", function(name, x)
  242. return x
  243. end)
  244. end).run
  245. if checknegint("checker","checknegint -2") ~= -2 then
  246. error("Test 12 failed")
  247. end
  248. local checknegnumber = ChatCmdBuilder.build(function(cmd)
  249. cmd:sub("checknegnumber :x:number", function(name, x)
  250. return x
  251. end)
  252. end).run
  253. if checknegnumber("checker","checknegnumber -3.3") ~= -3.3 then
  254. error("Test 13 failed")
  255. end
  256. local checknegpos = ChatCmdBuilder.build(function(cmd)
  257. cmd:sub("checknegpos :pos:pos", function(name, pos)
  258. return pos
  259. end)
  260. end).run
  261. local negpos = checknegpos("checker","checknegpos (-13.3,-4.6,-1234.5)")
  262. if negpos.x ~= -13.3 or negpos.y ~= -4.6 or negpos.z ~= -1234.5 then
  263. error("Test 14 failed")
  264. end
  265. local checktypes = ChatCmdBuilder.build(function(cmd)
  266. cmd:sub("checktypes :int:int :number:number :pos:pos :word:word :text:text", function(name, int, number, pos, word, text)
  267. return int, number, pos.x, pos.y, pos.z, word, text
  268. end)
  269. end).run
  270. local int, number, posx, posy, posz, word, text
  271. int, number, posx, posy, posz, word, text = checktypes("checker","checktypes -1 -2.4 (-3,-5.3,6.12) some text to finish off with")
  272. --dprint(int, number, posx, posy, posz, word, text)
  273. if int ~= -1 or number ~= -2.4 or posx ~= -3 or posy ~= -5.3 or posz ~= 6.12 or word ~= "some" or text ~= "text to finish off with" then
  274. error("Test 15 failed")
  275. end
  276. dprint("All tests passed")
  277. end
  278. if not minetest then
  279. run_tests()
  280. end