Add a .mailmap file
[oweals/minetest.git] / builtin / common / misc_helpers.lua
1 -- Minetest: builtin/misc_helpers.lua
2
3 --------------------------------------------------------------------------------
4 function basic_dump(o)
5         local tp = type(o)
6         if tp == "number" then
7                 return tostring(o)
8         elseif tp == "string" then
9                 return string.format("%q", o)
10         elseif tp == "boolean" then
11                 return tostring(o)
12         elseif tp == "nil" then
13                 return "nil"
14         -- Uncomment for full function dumping support.
15         -- Not currently enabled because bytecode isn't very human-readable and
16         -- dump's output is intended for humans.
17         --elseif tp == "function" then
18         --      return string.format("loadstring(%q)", string.dump(o))
19         else
20                 return string.format("<%s>", tp)
21         end
22 end
23
24 --------------------------------------------------------------------------------
25 -- Dumps values in a line-per-value format.
26 -- For example, {test = {"Testing..."}} becomes:
27 --   _["test"] = {}
28 --   _["test"][1] = "Testing..."
29 -- This handles tables as keys and circular references properly.
30 -- It also handles multiple references well, writing the table only once.
31 -- The dumped argument is internal-only.
32
33 function dump2(o, name, dumped)
34         name = name or "_"
35         -- "dumped" is used to keep track of serialized tables to handle
36         -- multiple references and circular tables properly.
37         -- It only contains tables as keys.  The value is the name that
38         -- the table has in the dump, eg:
39         -- {x = {"y"}} -> dumped[{"y"}] = '_["x"]'
40         dumped = dumped or {}
41         if type(o) ~= "table" then
42                 return string.format("%s = %s\n", name, basic_dump(o))
43         end
44         if dumped[o] then
45                 return string.format("%s = %s\n", name, dumped[o])
46         end
47         dumped[o] = name
48         -- This contains a list of strings to be concatenated later (because
49         -- Lua is slow at individual concatenation).
50         local t = {}
51         for k, v in pairs(o) do
52                 local keyStr
53                 if type(k) == "table" then
54                         if dumped[k] then
55                                 keyStr = dumped[k]
56                         else
57                                 -- Key tables don't have a name, so use one of
58                                 -- the form _G["table: 0xFFFFFFF"]
59                                 keyStr = string.format("_G[%q]", tostring(k))
60                                 -- Dump key table
61                                 table.insert(t, dump2(k, keyStr, dumped))
62                         end
63                 else
64                         keyStr = basic_dump(k)
65                 end
66                 local vname = string.format("%s[%s]", name, keyStr)
67                 table.insert(t, dump2(v, vname, dumped))
68         end
69         return string.format("%s = {}\n%s", name, table.concat(t))
70 end
71
72 --------------------------------------------------------------------------------
73 -- This dumps values in a one-line format, like serialize().
74 -- For example, {test = {"Testing..."}} becomes {["test"] = {[1] = "Testing..."}}
75 -- This supports tables as keys, but not circular references.
76 -- It performs poorly with multiple references as it writes out the full
77 -- table each time.
78 -- The dumped argument is internal-only.
79
80 function dump(o, dumped)
81         -- Same as "dumped" in dump2.  The difference is that here it can only
82         -- contain boolean (and nil) values since multiple references aren't
83         -- handled properly.
84         dumped = dumped or {}
85         if type(o) == "table" then
86                 if dumped[o] then
87                         return "<circular reference>"
88                 end
89                 dumped[o] = true
90                 local t = {}
91                 for k, v in pairs(o) do
92                         k = dump(k, dumped)
93                         v = dump(v, dumped)
94                         table.insert(t, string.format("[%s] = %s", k, v))
95                 end
96                 return string.format("{%s}", table.concat(t, ", "))
97         else
98                 return basic_dump(o)
99         end
100 end
101
102 --------------------------------------------------------------------------------
103 function string:split(sep)
104         local sep, fields = sep or ",", {}
105         local pattern = string.format("([^%s]+)", sep)
106         self:gsub(pattern, function(c) fields[#fields+1] = c end)
107         return fields
108 end
109
110 --------------------------------------------------------------------------------
111 function file_exists(filename)
112         local f = io.open(filename, "r")
113         if f==nil then
114                 return false
115         else
116                 f:close()
117                 return true
118         end
119 end
120
121 --------------------------------------------------------------------------------
122 function string:trim()
123         return (self:gsub("^%s*(.-)%s*$", "%1"))
124 end
125
126 assert(string.trim("\n \t\tfoo bar\t ") == "foo bar")
127
128 --------------------------------------------------------------------------------
129 function math.hypot(x, y)
130         local t
131         x = math.abs(x)
132         y = math.abs(y)
133         t = math.min(x, y)
134         x = math.max(x, y)
135         if x == 0 then return 0 end
136         t = t / x
137         return x * math.sqrt(1 + t * t)
138 end
139
140 --------------------------------------------------------------------------------
141 function get_last_folder(text,count)
142         local parts = text:split(DIR_DELIM)
143
144         if count == nil then
145                 return parts[#parts]
146         end
147
148         local retval = ""
149         for i=1,count,1 do
150                 retval = retval .. parts[#parts - (count-i)] .. DIR_DELIM
151         end
152
153         return retval
154 end
155
156 --------------------------------------------------------------------------------
157 function cleanup_path(temppath)
158
159         local parts = temppath:split("-")
160         temppath = ""
161         for i=1,#parts,1 do
162                 if temppath ~= "" then
163                         temppath = temppath .. "_"
164                 end
165                 temppath = temppath .. parts[i]
166         end
167
168         parts = temppath:split(".")
169         temppath = ""
170         for i=1,#parts,1 do
171                 if temppath ~= "" then
172                         temppath = temppath .. "_"
173                 end
174                 temppath = temppath .. parts[i]
175         end
176
177         parts = temppath:split("'")
178         temppath = ""
179         for i=1,#parts,1 do
180                 if temppath ~= "" then
181                         temppath = temppath .. ""
182                 end
183                 temppath = temppath .. parts[i]
184         end
185
186         parts = temppath:split(" ")
187         temppath = ""
188         for i=1,#parts,1 do
189                 if temppath ~= "" then
190                         temppath = temppath
191                 end
192                 temppath = temppath .. parts[i]
193         end
194
195         return temppath
196 end
197
198 function core.formspec_escape(text)
199         if text ~= nil then
200                 text = string.gsub(text,"\\","\\\\")
201                 text = string.gsub(text,"%]","\\]")
202                 text = string.gsub(text,"%[","\\[")
203                 text = string.gsub(text,";","\\;")
204                 text = string.gsub(text,",","\\,")
205         end
206         return text
207 end
208
209
210 function core.splittext(text,charlimit)
211         local retval = {}
212
213         local current_idx = 1
214
215         local start,stop = string.find(text," ",current_idx)
216         local nl_start,nl_stop = string.find(text,"\n",current_idx)
217         local gotnewline = false
218         if nl_start ~= nil and (start == nil or nl_start < start) then
219                 start = nl_start
220                 stop = nl_stop
221                 gotnewline = true
222         end
223         local last_line = ""
224         while start ~= nil do
225                 if string.len(last_line) + (stop-start) > charlimit then
226                         table.insert(retval,last_line)
227                         last_line = ""
228                 end
229
230                 if last_line ~= "" then
231                         last_line = last_line .. " "
232                 end
233
234                 last_line = last_line .. string.sub(text,current_idx,stop -1)
235
236                 if gotnewline then
237                         table.insert(retval,last_line)
238                         last_line = ""
239                         gotnewline = false
240                 end
241                 current_idx = stop+1
242
243                 start,stop = string.find(text," ",current_idx)
244                 nl_start,nl_stop = string.find(text,"\n",current_idx)
245
246                 if nl_start ~= nil and (start == nil or nl_start < start) then
247                         start = nl_start
248                         stop = nl_stop
249                         gotnewline = true
250                 end
251         end
252
253         --add last part of text
254         if string.len(last_line) + (string.len(text) - current_idx) > charlimit then
255                         table.insert(retval,last_line)
256                         table.insert(retval,string.sub(text,current_idx))
257         else
258                 last_line = last_line .. " " .. string.sub(text,current_idx)
259                 table.insert(retval,last_line)
260         end
261
262         return retval
263 end
264
265 --------------------------------------------------------------------------------
266
267 if INIT == "game" then
268         local dirs1 = {9, 18, 7, 12}
269         local dirs2 = {20, 23, 22, 21}
270
271         function core.rotate_and_place(itemstack, placer, pointed_thing,
272                                 infinitestacks, orient_flags)
273                 orient_flags = orient_flags or {}
274
275                 local unode = core.get_node_or_nil(pointed_thing.under)
276                 if not unode then
277                         return
278                 end
279                 local undef = core.registered_nodes[unode.name]
280                 if undef and undef.on_rightclick then
281                         undef.on_rightclick(pointed_thing.under, unode, placer,
282                                         itemstack, pointed_thing)
283                         return
284                 end
285                 local pitch = placer:get_look_pitch()
286                 local fdir = core.dir_to_facedir(placer:get_look_dir())
287                 local wield_name = itemstack:get_name()
288
289                 local above = pointed_thing.above
290                 local under = pointed_thing.under
291                 local iswall = (above.y == under.y)
292                 local isceiling = not iswall and (above.y < under.y)
293                 local anode = core.get_node_or_nil(above)
294                 if not anode then
295                         return
296                 end
297                 local pos = pointed_thing.above
298                 local node = anode
299
300                 if undef and undef.buildable_to then
301                         pos = pointed_thing.under
302                         node = unode
303                         iswall = false
304                 end
305
306                 if core.is_protected(pos, placer:get_player_name()) then
307                         core.record_protection_violation(pos,
308                                         placer:get_player_name())
309                         return
310                 end
311
312                 local ndef = core.registered_nodes[node.name]
313                 if not ndef or not ndef.buildable_to then
314                         return
315                 end
316
317                 if orient_flags.force_floor then
318                         iswall = false
319                         isceiling = false
320                 elseif orient_flags.force_ceiling then
321                         iswall = false
322                         isceiling = true
323                 elseif orient_flags.force_wall then
324                         iswall = true
325                         isceiling = false
326                 elseif orient_flags.invert_wall then
327                         iswall = not iswall
328                 end
329
330                 if iswall then
331                         core.set_node(pos, {name = wield_name,
332                                         param2 = dirs1[fdir+1]})
333                 elseif isceiling then
334                         if orient_flags.force_facedir then
335                                 core.set_node(pos, {name = wield_name,
336                                                 param2 = 20})
337                         else
338                                 core.set_node(pos, {name = wield_name,
339                                                 param2 = dirs2[fdir+1]})
340                         end
341                 else -- place right side up
342                         if orient_flags.force_facedir then
343                                 core.set_node(pos, {name = wield_name,
344                                                 param2 = 0})
345                         else
346                                 core.set_node(pos, {name = wield_name,
347                                                 param2 = fdir})
348                         end
349                 end
350
351                 if not infinitestacks then
352                         itemstack:take_item()
353                         return itemstack
354                 end
355         end
356
357
358 --------------------------------------------------------------------------------
359 --Wrapper for rotate_and_place() to check for sneak and assume Creative mode
360 --implies infinite stacks when performing a 6d rotation.
361 --------------------------------------------------------------------------------
362
363
364         core.rotate_node = function(itemstack, placer, pointed_thing)
365                 core.rotate_and_place(itemstack, placer, pointed_thing,
366                                 core.setting_getbool("creative_mode"),
367                                 {invert_wall = placer:get_player_control().sneak})
368                 return itemstack
369         end
370 end
371
372 --------------------------------------------------------------------------------
373 function core.explode_table_event(evt)
374         if evt ~= nil then
375                 local parts = evt:split(":")
376                 if #parts == 3 then
377                         local t = parts[1]:trim()
378                         local r = tonumber(parts[2]:trim())
379                         local c = tonumber(parts[3]:trim())
380                         if type(r) == "number" and type(c) == "number" and t ~= "INV" then
381                                 return {type=t, row=r, column=c}
382                         end
383                 end
384         end
385         return {type="INV", row=0, column=0}
386 end
387
388 --------------------------------------------------------------------------------
389 function core.explode_textlist_event(evt)
390         if evt ~= nil then
391                 local parts = evt:split(":")
392                 if #parts == 2 then
393                         local t = parts[1]:trim()
394                         local r = tonumber(parts[2]:trim())
395                         if type(r) == "number" and t ~= "INV" then
396                                 return {type=t, index=r}
397                         end
398                 end
399         end
400         return {type="INV", index=0}
401 end
402
403 --------------------------------------------------------------------------------
404 function core.explode_scrollbar_event(evt)
405         local retval = core.explode_textlist_event(evt)
406
407         retval.value = retval.index
408         retval.index = nil
409
410         return retval
411 end
412
413 --------------------------------------------------------------------------------
414 function core.pos_to_string(pos)
415         return "(" .. pos.x .. "," .. pos.y .. "," .. pos.z .. ")"
416 end
417
418 --------------------------------------------------------------------------------
419 -- mainmenu only functions
420 --------------------------------------------------------------------------------
421 if INIT == "mainmenu" then
422         function core.get_game(index)
423                 local games = game.get_games()
424
425                 if index > 0 and index <= #games then
426                         return games[index]
427                 end
428
429                 return nil
430         end
431
432         function fgettext(text, ...)
433                 text = core.gettext(text)
434                 local arg = {n=select('#', ...), ...}
435                 if arg.n >= 1 then
436                         -- Insert positional parameters ($1, $2, ...)
437                         result = ''
438                         pos = 1
439                         while pos <= text:len() do
440                                 newpos = text:find('[$]', pos)
441                                 if newpos == nil then
442                                         result = result .. text:sub(pos)
443                                         pos = text:len() + 1
444                                 else
445                                         paramindex = tonumber(text:sub(newpos+1, newpos+1))
446                                         result = result .. text:sub(pos, newpos-1) .. tostring(arg[paramindex])
447                                         pos = newpos + 2
448                                 end
449                         end
450                         text = result
451                 end
452                 return core.formspec_escape(text)
453         end
454 end
455