misc_helpers.lua 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  1. -- Minetest: builtin/misc_helpers.lua
  2. --------------------------------------------------------------------------------
  3. -- Localize functions to avoid table lookups (better performance).
  4. local string_sub, string_find = string.sub, string.find
  5. local math = math
  6. --------------------------------------------------------------------------------
  7. local function basic_dump(o)
  8. local tp = type(o)
  9. if tp == "number" then
  10. return tostring(o)
  11. elseif tp == "string" then
  12. return string.format("%q", o)
  13. elseif tp == "boolean" then
  14. return tostring(o)
  15. elseif tp == "nil" then
  16. return "nil"
  17. -- Uncomment for full function dumping support.
  18. -- Not currently enabled because bytecode isn't very human-readable and
  19. -- dump's output is intended for humans.
  20. --elseif tp == "function" then
  21. -- return string.format("loadstring(%q)", string.dump(o))
  22. elseif tp == "userdata" then
  23. return tostring(o)
  24. else
  25. return string.format("<%s>", tp)
  26. end
  27. end
  28. local keywords = {
  29. ["and"] = true,
  30. ["break"] = true,
  31. ["do"] = true,
  32. ["else"] = true,
  33. ["elseif"] = true,
  34. ["end"] = true,
  35. ["false"] = true,
  36. ["for"] = true,
  37. ["function"] = true,
  38. ["goto"] = true, -- Lua 5.2
  39. ["if"] = true,
  40. ["in"] = true,
  41. ["local"] = true,
  42. ["nil"] = true,
  43. ["not"] = true,
  44. ["or"] = true,
  45. ["repeat"] = true,
  46. ["return"] = true,
  47. ["then"] = true,
  48. ["true"] = true,
  49. ["until"] = true,
  50. ["while"] = true,
  51. }
  52. local function is_valid_identifier(str)
  53. if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then
  54. return false
  55. end
  56. return true
  57. end
  58. --------------------------------------------------------------------------------
  59. -- Dumps values in a line-per-value format.
  60. -- For example, {test = {"Testing..."}} becomes:
  61. -- _["test"] = {}
  62. -- _["test"][1] = "Testing..."
  63. -- This handles tables as keys and circular references properly.
  64. -- It also handles multiple references well, writing the table only once.
  65. -- The dumped argument is internal-only.
  66. function dump2(o, name, dumped)
  67. name = name or "_"
  68. -- "dumped" is used to keep track of serialized tables to handle
  69. -- multiple references and circular tables properly.
  70. -- It only contains tables as keys. The value is the name that
  71. -- the table has in the dump, eg:
  72. -- {x = {"y"}} -> dumped[{"y"}] = '_["x"]'
  73. dumped = dumped or {}
  74. if type(o) ~= "table" then
  75. return string.format("%s = %s\n", name, basic_dump(o))
  76. end
  77. if dumped[o] then
  78. return string.format("%s = %s\n", name, dumped[o])
  79. end
  80. dumped[o] = name
  81. -- This contains a list of strings to be concatenated later (because
  82. -- Lua is slow at individual concatenation).
  83. local t = {}
  84. for k, v in pairs(o) do
  85. local keyStr
  86. if type(k) == "table" then
  87. if dumped[k] then
  88. keyStr = dumped[k]
  89. else
  90. -- Key tables don't have a name, so use one of
  91. -- the form _G["table: 0xFFFFFFF"]
  92. keyStr = string.format("_G[%q]", tostring(k))
  93. -- Dump key table
  94. t[#t + 1] = dump2(k, keyStr, dumped)
  95. end
  96. else
  97. keyStr = basic_dump(k)
  98. end
  99. local vname = string.format("%s[%s]", name, keyStr)
  100. t[#t + 1] = dump2(v, vname, dumped)
  101. end
  102. return string.format("%s = {}\n%s", name, table.concat(t))
  103. end
  104. --------------------------------------------------------------------------------
  105. -- This dumps values in a one-statement format.
  106. -- For example, {test = {"Testing..."}} becomes:
  107. -- [[{
  108. -- test = {
  109. -- "Testing..."
  110. -- }
  111. -- }]]
  112. -- This supports tables as keys, but not circular references.
  113. -- It performs poorly with multiple references as it writes out the full
  114. -- table each time.
  115. -- The indent field specifies a indentation string, it defaults to a tab.
  116. -- Use the empty string to disable indentation.
  117. -- The dumped and level arguments are internal-only.
  118. function dump(o, indent, nested, level)
  119. local t = type(o)
  120. if not level and t == "userdata" then
  121. -- when userdata (e.g. player) is passed directly, print its metatable:
  122. return "userdata metatable: " .. dump(getmetatable(o))
  123. end
  124. if t ~= "table" then
  125. return basic_dump(o)
  126. end
  127. -- Contains table -> true/nil of currently nested tables
  128. nested = nested or {}
  129. if nested[o] then
  130. return "<circular reference>"
  131. end
  132. nested[o] = true
  133. indent = indent or "\t"
  134. level = level or 1
  135. local ret = {}
  136. local dumped_indexes = {}
  137. for i, v in ipairs(o) do
  138. ret[#ret + 1] = dump(v, indent, nested, level + 1)
  139. dumped_indexes[i] = true
  140. end
  141. for k, v in pairs(o) do
  142. if not dumped_indexes[k] then
  143. if type(k) ~= "string" or not is_valid_identifier(k) then
  144. k = "["..dump(k, indent, nested, level + 1).."]"
  145. end
  146. v = dump(v, indent, nested, level + 1)
  147. ret[#ret + 1] = k.." = "..v
  148. end
  149. end
  150. nested[o] = nil
  151. if indent ~= "" then
  152. local indent_str = "\n"..string.rep(indent, level)
  153. local end_indent_str = "\n"..string.rep(indent, level - 1)
  154. return string.format("{%s%s%s}",
  155. indent_str,
  156. table.concat(ret, ","..indent_str),
  157. end_indent_str)
  158. end
  159. return "{"..table.concat(ret, ", ").."}"
  160. end
  161. --------------------------------------------------------------------------------
  162. function string.split(str, delim, include_empty, max_splits, sep_is_pattern)
  163. delim = delim or ","
  164. if delim == "" then
  165. error("string.split separator is empty", 2)
  166. end
  167. max_splits = max_splits or -2
  168. local items = {}
  169. local pos, len = 1, #str
  170. local plain = not sep_is_pattern
  171. max_splits = max_splits + 1
  172. repeat
  173. local np, npe = string_find(str, delim, pos, plain)
  174. np, npe = (np or (len+1)), (npe or (len+1))
  175. if (not np) or (max_splits == 1) then
  176. np = len + 1
  177. npe = np
  178. end
  179. local s = string_sub(str, pos, np - 1)
  180. if include_empty or (s ~= "") then
  181. max_splits = max_splits - 1
  182. items[#items + 1] = s
  183. end
  184. pos = npe + 1
  185. until (max_splits == 0) or (pos > (len + 1))
  186. return items
  187. end
  188. --------------------------------------------------------------------------------
  189. function table.indexof(list, val)
  190. for i, v in ipairs(list) do
  191. if v == val then
  192. return i
  193. end
  194. end
  195. return -1
  196. end
  197. --------------------------------------------------------------------------------
  198. function table.keyof(tb, val)
  199. for k, v in pairs(tb) do
  200. if v == val then
  201. return k
  202. end
  203. end
  204. return nil
  205. end
  206. --------------------------------------------------------------------------------
  207. function string:trim()
  208. return self:match("^%s*(.-)%s*$")
  209. end
  210. local formspec_escapes = {
  211. ["\\"] = "\\\\",
  212. ["["] = "\\[",
  213. ["]"] = "\\]",
  214. [";"] = "\\;",
  215. [","] = "\\,",
  216. ["$"] = "\\$",
  217. }
  218. function core.formspec_escape(text)
  219. -- Use explicit character set instead of dot here because it doubles the performance
  220. return text and string.gsub(text, "[\\%[%];,$]", formspec_escapes)
  221. end
  222. function core.wrap_text(text, max_length, as_table)
  223. local result = {}
  224. local line = {}
  225. if #text <= max_length then
  226. return as_table and {text} or text
  227. end
  228. local line_length = 0
  229. for word in text:gmatch("%S+") do
  230. if line_length > 0 and line_length + #word + 1 >= max_length then
  231. -- word wouldn't fit on current line, move to next line
  232. table.insert(result, table.concat(line, " "))
  233. line = {word}
  234. line_length = #word
  235. else
  236. table.insert(line, word)
  237. line_length = line_length + 1 + #word
  238. end
  239. end
  240. table.insert(result, table.concat(line, " "))
  241. return as_table and result or table.concat(result, "\n")
  242. end
  243. --------------------------------------------------------------------------------
  244. if INIT == "game" then
  245. local dirs1 = {9, 18, 7, 12}
  246. local dirs2 = {20, 23, 22, 21}
  247. function core.rotate_and_place(itemstack, placer, pointed_thing,
  248. infinitestacks, orient_flags, prevent_after_place)
  249. orient_flags = orient_flags or {}
  250. local unode = core.get_node_or_nil(pointed_thing.under)
  251. if not unode then
  252. return
  253. end
  254. local undef = core.registered_nodes[unode.name]
  255. local sneaking = placer and placer:get_player_control().sneak
  256. if undef and undef.on_rightclick and not sneaking then
  257. return undef.on_rightclick(pointed_thing.under, unode, placer,
  258. itemstack, pointed_thing)
  259. end
  260. local fdir = placer and core.dir_to_facedir(placer:get_look_dir()) or 0
  261. local above = pointed_thing.above
  262. local under = pointed_thing.under
  263. local iswall = (above.y == under.y)
  264. local isceiling = not iswall and (above.y < under.y)
  265. if undef and undef.buildable_to then
  266. iswall = false
  267. end
  268. if orient_flags.force_floor then
  269. iswall = false
  270. isceiling = false
  271. elseif orient_flags.force_ceiling then
  272. iswall = false
  273. isceiling = true
  274. elseif orient_flags.force_wall then
  275. iswall = true
  276. isceiling = false
  277. elseif orient_flags.invert_wall then
  278. iswall = not iswall
  279. end
  280. local param2 = fdir
  281. if iswall then
  282. param2 = dirs1[fdir + 1]
  283. elseif isceiling then
  284. if orient_flags.force_facedir then
  285. param2 = 20
  286. else
  287. param2 = dirs2[fdir + 1]
  288. end
  289. else -- place right side up
  290. if orient_flags.force_facedir then
  291. param2 = 0
  292. end
  293. end
  294. local old_itemstack = ItemStack(itemstack)
  295. local new_itemstack = core.item_place_node(itemstack, placer,
  296. pointed_thing, param2, prevent_after_place)
  297. return infinitestacks and old_itemstack or new_itemstack
  298. end
  299. --------------------------------------------------------------------------------
  300. --Wrapper for rotate_and_place() to check for sneak and assume Creative mode
  301. --implies infinite stacks when performing a 6d rotation.
  302. --------------------------------------------------------------------------------
  303. core.rotate_node = function(itemstack, placer, pointed_thing)
  304. local name = placer and placer:get_player_name() or ""
  305. local invert_wall = placer and placer:get_player_control().sneak or false
  306. return core.rotate_and_place(itemstack, placer, pointed_thing,
  307. core.is_creative_enabled(name),
  308. {invert_wall = invert_wall}, true)
  309. end
  310. end
  311. --------------------------------------------------------------------------------
  312. function core.explode_table_event(evt)
  313. if evt ~= nil then
  314. local parts = evt:split(":")
  315. if #parts == 3 then
  316. local t = parts[1]:trim()
  317. local r = tonumber(parts[2]:trim())
  318. local c = tonumber(parts[3]:trim())
  319. if type(r) == "number" and type(c) == "number"
  320. and t ~= "INV" then
  321. return {type=t, row=r, column=c}
  322. end
  323. end
  324. end
  325. return {type="INV", row=0, column=0}
  326. end
  327. --------------------------------------------------------------------------------
  328. function core.explode_textlist_event(evt)
  329. if evt ~= nil then
  330. local parts = evt:split(":")
  331. if #parts == 2 then
  332. local t = parts[1]:trim()
  333. local r = tonumber(parts[2]:trim())
  334. if type(r) == "number" and t ~= "INV" then
  335. return {type=t, index=r}
  336. end
  337. end
  338. end
  339. return {type="INV", index=0}
  340. end
  341. --------------------------------------------------------------------------------
  342. function core.explode_scrollbar_event(evt)
  343. local retval = core.explode_textlist_event(evt)
  344. retval.value = retval.index
  345. retval.index = nil
  346. return retval
  347. end
  348. --------------------------------------------------------------------------------
  349. function core.rgba(r, g, b, a)
  350. return a and string.format("#%02X%02X%02X%02X", r, g, b, a) or
  351. string.format("#%02X%02X%02X", r, g, b)
  352. end
  353. --------------------------------------------------------------------------------
  354. function core.pos_to_string(pos, decimal_places)
  355. local x = pos.x
  356. local y = pos.y
  357. local z = pos.z
  358. if decimal_places ~= nil then
  359. x = string.format("%." .. decimal_places .. "f", x)
  360. y = string.format("%." .. decimal_places .. "f", y)
  361. z = string.format("%." .. decimal_places .. "f", z)
  362. end
  363. return "(" .. x .. "," .. y .. "," .. z .. ")"
  364. end
  365. --------------------------------------------------------------------------------
  366. function core.string_to_pos(value)
  367. if value == nil then
  368. return nil
  369. end
  370. value = value:match("^%((.-)%)$") or value -- strip parentheses
  371. local x, y, z = value:trim():match("^([%d.-]+)[,%s]%s*([%d.-]+)[,%s]%s*([%d.-]+)$")
  372. if x and y and z then
  373. x = tonumber(x)
  374. y = tonumber(y)
  375. z = tonumber(z)
  376. return vector.new(x, y, z)
  377. end
  378. return nil
  379. end
  380. --------------------------------------------------------------------------------
  381. do
  382. local rel_num_cap = "(~?-?%d*%.?%d*)" -- may be overly permissive as this will be tonumber'ed anyways
  383. local num_delim = "[,%s]%s*"
  384. local pattern = "^" .. table.concat({rel_num_cap, rel_num_cap, rel_num_cap}, num_delim) .. "$"
  385. local function parse_area_string(pos, relative_to)
  386. local pp = {}
  387. pp.x, pp.y, pp.z = pos:trim():match(pattern)
  388. return core.parse_coordinates(pp.x, pp.y, pp.z, relative_to)
  389. end
  390. function core.string_to_area(value, relative_to)
  391. local p1, p2 = value:match("^%((.-)%)%s*%((.-)%)$")
  392. if not p1 then
  393. return
  394. end
  395. p1 = parse_area_string(p1, relative_to)
  396. p2 = parse_area_string(p2, relative_to)
  397. if p1 == nil or p2 == nil then
  398. return
  399. end
  400. return p1, p2
  401. end
  402. end
  403. --------------------------------------------------------------------------------
  404. function table.copy(t, seen)
  405. local n = {}
  406. seen = seen or {}
  407. seen[t] = n
  408. for k, v in pairs(t) do
  409. n[(type(k) == "table" and (seen[k] or table.copy(k, seen))) or k] =
  410. (type(v) == "table" and (seen[v] or table.copy(v, seen))) or v
  411. end
  412. return n
  413. end
  414. function table.insert_all(t, other)
  415. if table.move then -- LuaJIT
  416. return table.move(other, 1, #other, #t + 1, t)
  417. end
  418. for i=1, #other do
  419. t[#t + 1] = other[i]
  420. end
  421. return t
  422. end
  423. function table.key_value_swap(t)
  424. local ti = {}
  425. for k,v in pairs(t) do
  426. ti[v] = k
  427. end
  428. return ti
  429. end
  430. function table.shuffle(t, from, to, random)
  431. from = from or 1
  432. to = to or #t
  433. random = random or math.random
  434. local n = to - from + 1
  435. while n > 1 do
  436. local r = from + n-1
  437. local l = from + random(0, n-1)
  438. t[l], t[r] = t[r], t[l]
  439. n = n-1
  440. end
  441. end
  442. --------------------------------------------------------------------------------
  443. -- mainmenu only functions
  444. --------------------------------------------------------------------------------
  445. if core.gettext then -- for client and mainmenu
  446. function fgettext_ne(text, ...)
  447. text = core.gettext(text)
  448. local arg = {n=select('#', ...), ...}
  449. if arg.n >= 1 then
  450. -- Insert positional parameters ($1, $2, ...)
  451. local result = ''
  452. local pos = 1
  453. while pos <= text:len() do
  454. local newpos = text:find('[$]', pos)
  455. if newpos == nil then
  456. result = result .. text:sub(pos)
  457. pos = text:len() + 1
  458. else
  459. local paramindex =
  460. tonumber(text:sub(newpos+1, newpos+1))
  461. result = result .. text:sub(pos, newpos-1)
  462. .. tostring(arg[paramindex])
  463. pos = newpos + 2
  464. end
  465. end
  466. text = result
  467. end
  468. return text
  469. end
  470. function fgettext(text, ...)
  471. return core.formspec_escape(fgettext_ne(text, ...))
  472. end
  473. end
  474. local ESCAPE_CHAR = string.char(0x1b)
  475. function core.get_color_escape_sequence(color)
  476. return ESCAPE_CHAR .. "(c@" .. color .. ")"
  477. end
  478. function core.get_background_escape_sequence(color)
  479. return ESCAPE_CHAR .. "(b@" .. color .. ")"
  480. end
  481. function core.colorize(color, message)
  482. local lines = tostring(message):split("\n", true)
  483. local color_code = core.get_color_escape_sequence(color)
  484. for i, line in ipairs(lines) do
  485. lines[i] = color_code .. line
  486. end
  487. return table.concat(lines, "\n") .. core.get_color_escape_sequence("#ffffff")
  488. end
  489. function core.strip_foreground_colors(str)
  490. return (str:gsub(ESCAPE_CHAR .. "%(c@[^)]+%)", ""))
  491. end
  492. function core.strip_background_colors(str)
  493. return (str:gsub(ESCAPE_CHAR .. "%(b@[^)]+%)", ""))
  494. end
  495. function core.strip_colors(str)
  496. return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", ""))
  497. end
  498. function core.translate(textdomain, str, ...)
  499. local start_seq
  500. if textdomain == "" then
  501. start_seq = ESCAPE_CHAR .. "T"
  502. else
  503. start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")"
  504. end
  505. local arg = {n=select('#', ...), ...}
  506. local end_seq = ESCAPE_CHAR .. "E"
  507. local arg_index = 1
  508. local translated = str:gsub("@(.)", function(matched)
  509. local c = string.byte(matched)
  510. if string.byte("1") <= c and c <= string.byte("9") then
  511. local a = c - string.byte("0")
  512. if a ~= arg_index then
  513. error("Escape sequences in string given to core.translate " ..
  514. "are not in the correct order: got @" .. matched ..
  515. "but expected @" .. tostring(arg_index))
  516. end
  517. if a > arg.n then
  518. error("Not enough arguments provided to core.translate")
  519. end
  520. arg_index = arg_index + 1
  521. return ESCAPE_CHAR .. "F" .. arg[a] .. ESCAPE_CHAR .. "E"
  522. elseif matched == "n" then
  523. return "\n"
  524. else
  525. return matched
  526. end
  527. end)
  528. if arg_index < arg.n + 1 then
  529. error("Too many arguments provided to core.translate")
  530. end
  531. return start_seq .. translated .. end_seq
  532. end
  533. function core.get_translator(textdomain)
  534. return function(str, ...) return core.translate(textdomain or "", str, ...) end
  535. end
  536. --------------------------------------------------------------------------------
  537. -- Returns the exact coordinate of a pointed surface
  538. --------------------------------------------------------------------------------
  539. function core.pointed_thing_to_face_pos(placer, pointed_thing)
  540. -- Avoid crash in some situations when player is inside a node, causing
  541. -- 'above' to equal 'under'.
  542. if vector.equals(pointed_thing.above, pointed_thing.under) then
  543. return pointed_thing.under
  544. end
  545. local eye_height = placer:get_properties().eye_height
  546. local eye_offset_first = placer:get_eye_offset()
  547. local node_pos = pointed_thing.under
  548. local camera_pos = placer:get_pos()
  549. local pos_off = vector.multiply(
  550. vector.subtract(pointed_thing.above, node_pos), 0.5)
  551. local look_dir = placer:get_look_dir()
  552. local offset, nc
  553. local oc = {}
  554. for c, v in pairs(pos_off) do
  555. if nc or v == 0 then
  556. oc[#oc + 1] = c
  557. else
  558. offset = v
  559. nc = c
  560. end
  561. end
  562. local fine_pos = {[nc] = node_pos[nc] + offset}
  563. camera_pos.y = camera_pos.y + eye_height + eye_offset_first.y / 10
  564. local f = (node_pos[nc] + offset - camera_pos[nc]) / look_dir[nc]
  565. for i = 1, #oc do
  566. fine_pos[oc[i]] = camera_pos[oc[i]] + look_dir[oc[i]] * f
  567. end
  568. return fine_pos
  569. end
  570. function core.string_to_privs(str, delim)
  571. assert(type(str) == "string")
  572. delim = delim or ','
  573. local privs = {}
  574. for _, priv in pairs(string.split(str, delim)) do
  575. privs[priv:trim()] = true
  576. end
  577. return privs
  578. end
  579. function core.privs_to_string(privs, delim)
  580. assert(type(privs) == "table")
  581. delim = delim or ','
  582. local list = {}
  583. for priv, bool in pairs(privs) do
  584. if bool then
  585. list[#list + 1] = priv
  586. end
  587. end
  588. table.sort(list)
  589. return table.concat(list, delim)
  590. end
  591. function core.is_nan(number)
  592. return number ~= number
  593. end
  594. --[[ Helper function for parsing an optionally relative number
  595. of a chat command parameter, using the chat command tilde notation.
  596. Parameters:
  597. * arg: String snippet containing the number; possible values:
  598. * "<number>": return as number
  599. * "~<number>": return relative_to + <number>
  600. * "~": return relative_to
  601. * Anything else will return `nil`
  602. * relative_to: Number to which the `arg` number might be relative to
  603. Returns:
  604. A number or `nil`, depending on `arg.
  605. Examples:
  606. * `core.parse_relative_number("5", 10)` returns 5
  607. * `core.parse_relative_number("~5", 10)` returns 15
  608. * `core.parse_relative_number("~", 10)` returns 10
  609. ]]
  610. function core.parse_relative_number(arg, relative_to)
  611. if not arg then
  612. return nil
  613. elseif arg == "~" then
  614. return relative_to
  615. elseif string.sub(arg, 1, 1) == "~" then
  616. local number = tonumber(string.sub(arg, 2))
  617. if not number then
  618. return nil
  619. end
  620. if core.is_nan(number) or number == math.huge or number == -math.huge then
  621. return nil
  622. end
  623. return relative_to + number
  624. else
  625. local number = tonumber(arg)
  626. if core.is_nan(number) or number == math.huge or number == -math.huge then
  627. return nil
  628. end
  629. return number
  630. end
  631. end
  632. --[[ Helper function to parse coordinates that might be relative
  633. to another position; supports chat command tilde notation.
  634. Intended to be used in chat command parameter parsing.
  635. Parameters:
  636. * x, y, z: Parsed x, y, and z coordinates as strings
  637. * relative_to: Position to which to compare the position
  638. Syntax of x, y and z:
  639. * "<number>": return as number
  640. * "~<number>": return <number> + player position on this axis
  641. * "~": return player position on this axis
  642. Returns: a vector or nil for invalid input or if player does not exist
  643. ]]
  644. function core.parse_coordinates(x, y, z, relative_to)
  645. if not relative_to then
  646. x, y, z = tonumber(x), tonumber(y), tonumber(z)
  647. return x and y and z and { x = x, y = y, z = z }
  648. end
  649. local rx = core.parse_relative_number(x, relative_to.x)
  650. local ry = core.parse_relative_number(y, relative_to.y)
  651. local rz = core.parse_relative_number(z, relative_to.z)
  652. return rx and ry and rz and { x = rx, y = ry, z = rz }
  653. end