]> git.lizzy.rs Git - dragonfireclient.git/blob - builtin/mainmenu/dlg_settings_advanced.lua
83f9054463a0a6add6194260fe0120d21181d063
[dragonfireclient.git] / builtin / mainmenu / dlg_settings_advanced.lua
1 --Minetest
2 --Copyright (C) 2015 PilzAdam
3 --
4 --This program is free software; you can redistribute it and/or modify
5 --it under the terms of the GNU Lesser General Public License as published by
6 --the Free Software Foundation; either version 2.1 of the License, or
7 --(at your option) any later version.
8 --
9 --This program is distributed in the hope that it will be useful,
10 --but WITHOUT ANY WARRANTY; without even the implied warranty of
11 --MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 --GNU Lesser General Public License for more details.
13 --
14 --You should have received a copy of the GNU Lesser General Public License along
15 --with this program; if not, write to the Free Software Foundation, Inc.,
16 --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 local FILENAME = "settingtypes.txt"
19
20 local CHAR_CLASSES = {
21         SPACE = "[%s]",
22         VARIABLE = "[%w_%-%.]",
23         INTEGER = "[+-]?[%d]",
24         FLOAT = "[+-]?[%d%.]",
25         FLAGS = "[%w_%-%.,]",
26 }
27
28 local function flags_to_table(flags)
29         return flags:gsub("%s+", ""):split(",", true) -- Remove all spaces and split
30 end
31
32 -- returns error message, or nil
33 local function parse_setting_line(settings, line, read_all, base_level, allow_secure)
34
35         -- strip carriage returns (CR, /r)
36         line = line:gsub("\r", "")
37
38         -- comment
39         local comment = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$")
40         if comment then
41                 if settings.current_comment == "" then
42                         settings.current_comment = comment
43                 else
44                         settings.current_comment = settings.current_comment .. "\n" .. comment
45                 end
46                 return
47         end
48
49         -- clear current_comment so only comments directly above a setting are bound to it
50         -- but keep a local reference to it for variables in the current line
51         local current_comment = settings.current_comment
52         settings.current_comment = ""
53
54         -- empty lines
55         if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then
56                 return
57         end
58
59         -- category
60         local stars, category = line:match("^%[([%*]*)([^%]]+)%]$")
61         if category then
62                 table.insert(settings, {
63                         name = category,
64                         level = stars:len() + base_level,
65                         type = "category",
66                 })
67                 return
68         end
69
70         -- settings
71         local first_part, name, readable_name, setting_type = line:match("^"
72                         -- this first capture group matches the whole first part,
73                         --  so we can later strip it from the rest of the line
74                         .. "("
75                                 .. "([" .. CHAR_CLASSES.VARIABLE .. "+)" -- variable name
76                                 .. CHAR_CLASSES.SPACE .. "*"
77                                 .. "%(([^%)]*)%)"  -- readable name
78                                 .. CHAR_CLASSES.SPACE .. "*"
79                                 .. "(" .. CHAR_CLASSES.VARIABLE .. "+)" -- type
80                                 .. CHAR_CLASSES.SPACE .. "*"
81                         .. ")")
82
83         if not first_part then
84                 return "Invalid line"
85         end
86
87         if name:match("secure%.[.]*") and not allow_secure then
88                 return "Tried to add \"secure.\" setting"
89         end
90
91         if readable_name == "" then
92                 readable_name = nil
93         end
94         local remaining_line = line:sub(first_part:len() + 1)
95
96         if setting_type == "int" then
97                 local default, min, max = remaining_line:match("^"
98                                 -- first int is required, the last 2 are optional
99                                 .. "(" .. CHAR_CLASSES.INTEGER .. "+)" .. CHAR_CLASSES.SPACE .. "*"
100                                 .. "(" .. CHAR_CLASSES.INTEGER .. "*)" .. CHAR_CLASSES.SPACE .. "*"
101                                 .. "(" .. CHAR_CLASSES.INTEGER .. "*)"
102                                 .. "$")
103
104                 if not default or not tonumber(default) then
105                         return "Invalid integer setting"
106                 end
107
108                 min = tonumber(min)
109                 max = tonumber(max)
110                 table.insert(settings, {
111                         name = name,
112                         readable_name = readable_name,
113                         type = "int",
114                         default = default,
115                         min = min,
116                         max = max,
117                         comment = current_comment,
118                 })
119                 return
120         end
121
122         if setting_type == "string"
123                         or setting_type == "key" or setting_type == "v3f" then
124                 local default = remaining_line:match("^(.*)$")
125
126                 if not default then
127                         return "Invalid string setting"
128                 end
129                 if setting_type == "key" and not read_all then
130                         -- ignore key type if read_all is false
131                         return
132                 end
133
134                 table.insert(settings, {
135                         name = name,
136                         readable_name = readable_name,
137                         type = setting_type,
138                         default = default,
139                         comment = current_comment,
140                 })
141                 return
142         end
143
144         if setting_type == "noise_params_2d"
145                         or setting_type == "noise_params_3d" then
146                 local default = remaining_line:match("^(.*)$")
147
148                 if not default then
149                         return "Invalid string setting"
150                 end
151
152                 local values = {}
153                 local ti = 1
154                 local index = 1
155                 for match in default:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
156                         index = default:find("[+-]?[%d.-e]+", index) + match:len()
157                         table.insert(values, match)
158                         ti = ti + 1
159                         if ti > 9 then
160                                 break
161                         end
162                 end
163                 index = default:find("[^, ]", index)
164                 local flags = ""
165                 if index then
166                         flags = default:sub(index)
167                         default = default:sub(1, index - 3) -- Make sure no flags in single-line format
168                 end
169                 table.insert(values, flags)
170
171                 table.insert(settings, {
172                         name = name,
173                         readable_name = readable_name,
174                         type = setting_type,
175                         default = default,
176                         default_table = {
177                                 offset = values[1],
178                                 scale = values[2],
179                                 spread = {
180                                         x = values[3],
181                                         y = values[4],
182                                         z = values[5]
183                                 },
184                                 seed = values[6],
185                                 octaves = values[7],
186                                 persistence = values[8],
187                                 lacunarity = values[9],
188                                 flags = values[10]
189                         },
190                         values = values,
191                         comment = current_comment,
192                         noise_params = true,
193                         flags = flags_to_table("defaults,eased,absvalue")
194                 })
195                 return
196         end
197
198         if setting_type == "bool" then
199                 if remaining_line ~= "false" and remaining_line ~= "true" then
200                         return "Invalid boolean setting"
201                 end
202
203                 table.insert(settings, {
204                         name = name,
205                         readable_name = readable_name,
206                         type = "bool",
207                         default = remaining_line,
208                         comment = current_comment,
209                 })
210                 return
211         end
212
213         if setting_type == "float" then
214                 local default, min, max = remaining_line:match("^"
215                                 -- first float is required, the last 2 are optional
216                                 .. "(" .. CHAR_CLASSES.FLOAT .. "+)" .. CHAR_CLASSES.SPACE .. "*"
217                                 .. "(" .. CHAR_CLASSES.FLOAT .. "*)" .. CHAR_CLASSES.SPACE .. "*"
218                                 .. "(" .. CHAR_CLASSES.FLOAT .. "*)"
219                                 .."$")
220
221                 if not default or not tonumber(default) then
222                         return "Invalid float setting"
223                 end
224
225                 min = tonumber(min)
226                 max = tonumber(max)
227                 table.insert(settings, {
228                         name = name,
229                         readable_name = readable_name,
230                         type = "float",
231                         default = default,
232                         min = min,
233                         max = max,
234                         comment = current_comment,
235                 })
236                 return
237         end
238
239         if setting_type == "enum" then
240                 local default, values = remaining_line:match("^"
241                                 -- first value (default) may be empty (i.e. is optional)
242                                 .. "(" .. CHAR_CLASSES.VARIABLE .. "*)" .. CHAR_CLASSES.SPACE .. "*"
243                                 .. "(" .. CHAR_CLASSES.FLAGS .. "+)"
244                                 .. "$")
245
246                 if not default or values == "" then
247                         return "Invalid enum setting"
248                 end
249
250                 table.insert(settings, {
251                         name = name,
252                         readable_name = readable_name,
253                         type = "enum",
254                         default = default,
255                         values = values:split(",", true),
256                         comment = current_comment,
257                 })
258                 return
259         end
260
261         if setting_type == "path" or setting_type == "filepath" then
262                 local default = remaining_line:match("^(.*)$")
263
264                 if not default then
265                         return "Invalid path setting"
266                 end
267
268                 table.insert(settings, {
269                         name = name,
270                         readable_name = readable_name,
271                         type = setting_type,
272                         default = default,
273                         comment = current_comment,
274                 })
275                 return
276         end
277
278         if setting_type == "flags" then
279                 local default, possible = remaining_line:match("^"
280                                 -- first value (default) may be empty (i.e. is optional)
281                                 -- this is implemented by making the last value optional, and
282                                 -- swapping them around if it turns out empty.
283                                 .. "(" .. CHAR_CLASSES.FLAGS .. "+)" .. CHAR_CLASSES.SPACE .. "*"
284                                 .. "(" .. CHAR_CLASSES.FLAGS .. "*)"
285                                 .. "$")
286
287                 if not default or not possible then
288                         return "Invalid flags setting"
289                 end
290
291                 if possible == "" then
292                         possible = default
293                         default = ""
294                 end
295
296                 table.insert(settings, {
297                         name = name,
298                         readable_name = readable_name,
299                         type = "flags",
300                         default = default,
301                         possible = flags_to_table(possible),
302                         comment = current_comment,
303                 })
304                 return
305         end
306
307         return "Invalid setting type \"" .. setting_type .. "\""
308 end
309
310 local function parse_single_file(file, filepath, read_all, result, base_level, allow_secure)
311         -- store this helper variable in the table so it's easier to pass to parse_setting_line()
312         result.current_comment = ""
313
314         local line = file:read("*line")
315         while line do
316                 local error_msg = parse_setting_line(result, line, read_all, base_level, allow_secure)
317                 if error_msg then
318                         core.log("error", error_msg .. " in " .. filepath .. " \"" .. line .. "\"")
319                 end
320                 line = file:read("*line")
321         end
322
323         result.current_comment = nil
324 end
325
326 -- read_all: whether to ignore certain setting types for GUI or not
327 -- parse_mods: whether to parse settingtypes.txt in mods and games
328 local function parse_config_file(read_all, parse_mods)
329         local settings = {}
330
331         do
332                 local builtin_path = core.get_builtin_path() .. FILENAME
333                 local file = io.open(builtin_path, "r")
334                 if not file then
335                         core.log("error", "Can't load " .. FILENAME)
336                         return settings
337                 end
338
339                 parse_single_file(file, builtin_path, read_all, settings, 0, true)
340
341                 file:close()
342         end
343
344         if parse_mods then
345                 -- Parse games
346                 local games_category_initialized = false
347                 local index = 1
348                 local game = pkgmgr.get_game(index)
349                 while game do
350                         local path = game.path .. DIR_DELIM .. FILENAME
351                         local file = io.open(path, "r")
352                         if file then
353                                 if not games_category_initialized then
354                                         fgettext_ne("Games") -- not used, but needed for xgettext
355                                         table.insert(settings, {
356                                                 name = "Games",
357                                                 level = 0,
358                                                 type = "category",
359                                         })
360                                         games_category_initialized = true
361                                 end
362
363                                 table.insert(settings, {
364                                         name = game.name,
365                                         level = 1,
366                                         type = "category",
367                                 })
368
369                                 parse_single_file(file, path, read_all, settings, 2, false)
370
371                                 file:close()
372                         end
373
374                         index = index + 1
375                         game = pkgmgr.get_game(index)
376                 end
377
378                 -- Parse mods
379                 local mods_category_initialized = false
380                 local mods = {}
381                 get_mods(core.get_modpath(), "mods", mods)
382                 for _, mod in ipairs(mods) do
383                         local path = mod.path .. DIR_DELIM .. FILENAME
384                         local file = io.open(path, "r")
385                         if file then
386                                 if not mods_category_initialized then
387                                         fgettext_ne("Mods") -- not used, but needed for xgettext
388                                         table.insert(settings, {
389                                                 name = "Mods",
390                                                 level = 0,
391                                                 type = "category",
392                                         })
393                                         mods_category_initialized = true
394                                 end
395
396                                 table.insert(settings, {
397                                         name = mod.name,
398                                         level = 1,
399                                         type = "category",
400                                 })
401
402                                 parse_single_file(file, path, read_all, settings, 2, false)
403
404                                 file:close()
405                         end
406                 end
407         end
408
409         return settings
410 end
411
412 local function filter_settings(settings, searchstring)
413         if not searchstring or searchstring == "" then
414                 return settings, -1
415         end
416
417         -- Setup the keyword list
418         local keywords = {}
419         for word in searchstring:lower():gmatch("%S+") do
420                 table.insert(keywords, word)
421         end
422
423         local result = {}
424         local category_stack = {}
425         local current_level = 0
426         local best_setting = nil
427         for _, entry in pairs(settings) do
428                 if entry.type == "category" then
429                         -- Remove all settingless categories
430                         while #category_stack > 0 and entry.level <= current_level do
431                                 table.remove(category_stack, #category_stack)
432                                 if #category_stack > 0 then
433                                         current_level = category_stack[#category_stack].level
434                                 else
435                                         current_level = 0
436                                 end
437                         end
438
439                         -- Push category onto stack
440                         category_stack[#category_stack + 1] = entry
441                         current_level = entry.level
442                 else
443                         -- See if setting matches keywords
444                         local setting_score = 0
445                         for k = 1, #keywords do
446                                 local keyword = keywords[k]
447
448                                 if string.find(entry.name:lower(), keyword, 1, true) then
449                                         setting_score = setting_score + 1
450                                 end
451
452                                 if entry.readable_name and
453                                                 string.find(fgettext(entry.readable_name):lower(), keyword, 1, true) then
454                                         setting_score = setting_score + 1
455                                 end
456
457                                 if entry.comment and
458                                                 string.find(fgettext_ne(entry.comment):lower(), keyword, 1, true) then
459                                         setting_score = setting_score + 1
460                                 end
461                         end
462
463                         -- Add setting to results if match
464                         if setting_score > 0 then
465                                 -- Add parent categories
466                                 for _, category in pairs(category_stack) do
467                                         result[#result + 1] = category
468                                 end
469                                 category_stack = {}
470
471                                 -- Add setting
472                                 result[#result + 1] = entry
473                                 entry.score = setting_score
474
475                                 if not best_setting or
476                                                 setting_score > result[best_setting].score then
477                                         best_setting = #result
478                                 end
479                         end
480                 end
481         end
482         return result, best_setting or -1
483 end
484
485 local full_settings = parse_config_file(false, true)
486 local search_string = ""
487 local settings = full_settings
488 local selected_setting = 1
489
490 local function get_current_value(setting)
491         local value = core.settings:get(setting.name)
492         if value == nil then
493                 value = setting.default
494         end
495         return value
496 end
497
498 local function get_current_np_group(setting)
499         local value = core.settings:get_np_group(setting.name)
500         local t = {}
501         if value == nil then
502                 t = setting.values
503         else
504                 table.insert(t, value.offset)
505                 table.insert(t, value.scale)
506                 table.insert(t, value.spread.x)
507                 table.insert(t, value.spread.y)
508                 table.insert(t, value.spread.z)
509                 table.insert(t, value.seed)
510                 table.insert(t, value.octaves)
511                 table.insert(t, value.persistence)
512                 table.insert(t, value.lacunarity)
513                 table.insert(t, value.flags)
514         end
515         return t
516 end
517
518 local function get_current_np_group_as_string(setting)
519         local value = core.settings:get_np_group(setting.name)
520         if value == nil then
521                 return setting.default
522         end
523         return ("%g, %g, (%g, %g, %g), %g, %g, %g, %g"):format(
524                 value.offset,
525                 value.scale,
526                 value.spread.x,
527                 value.spread.y,
528                 value.spread.z,
529                 value.seed,
530                 value.octaves,
531                 value.persistence,
532                 value.lacunarity
533         ) .. (value.flags ~= "" and (", " .. value.flags) or "")
534 end
535
536 local checkboxes = {} -- handle checkboxes events
537
538 local function create_change_setting_formspec(dialogdata)
539         local setting = settings[selected_setting]
540         -- Final formspec will be created at the end of this function
541         -- Default values below, may be changed depending on setting type
542         local width = 10
543         local height = 3.5
544         local description_height = 3
545         local formspec = ""
546
547         -- Setting-specific formspec elements
548         if setting.type == "bool" then
549                 local selected_index = 1
550                 if core.is_yes(get_current_value(setting)) then
551                         selected_index = 2
552                 end
553                 formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;"
554                                 .. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";"
555                                 .. selected_index .. "]"
556                 height = height + 1.25
557
558         elseif setting.type == "enum" then
559                 local selected_index = 0
560                 formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;"
561                 for index, value in ipairs(setting.values) do
562                         -- translating value is not possible, since it's the value
563                         --  that we set the setting to
564                         formspec = formspec ..  core.formspec_escape(value) .. ","
565                         if get_current_value(setting) == value then
566                                 selected_index = index
567                         end
568                 end
569                 if #setting.values > 0 then
570                         formspec = formspec:sub(1, -2) -- remove trailing comma
571                 end
572                 formspec = formspec .. ";" .. selected_index .. "]"
573                 height = height + 1.25
574
575         elseif setting.type == "path" or setting.type == "filepath" then
576                 local current_value = dialogdata.selected_path
577                 if not current_value then
578                         current_value = get_current_value(setting)
579                 end
580                 formspec = "field[0.28," .. height + 0.15 .. ";8,1;te_setting_value;;"
581                                 .. core.formspec_escape(current_value) .. "]"
582                                 .. "button[8," .. height - 0.15 .. ";2,1;btn_browser_"
583                                 .. setting.type .. ";" .. fgettext("Browse") .. "]"
584                 height = height + 1.15
585
586         elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
587                 local t = get_current_np_group(setting)
588                 local dimension = 3
589                 if setting.type == "noise_params_2d" then
590                         dimension = 2
591                 end
592
593                 -- More space for 3x3 fields
594                 description_height = description_height - 1.5
595                 height = height - 1.5
596
597                 local fields = {}
598                 local function add_field(x, name, label, value)
599                         fields[#fields + 1] = ("field[%f,%f;3.3,1;%s;%s;%s]"):format(
600                                 x, height, name, label, core.formspec_escape(value or "")
601                         )
602                 end
603                 -- First row
604                 height = height + 0.3
605                 add_field(0.3, "te_offset", fgettext("Offset"), t[1])
606                 add_field(3.6, "te_scale",  fgettext("Scale"),  t[2])
607                 add_field(6.9, "te_seed",   fgettext("Seed"),   t[6])
608                 height = height + 1.1
609
610                 -- Second row
611                 add_field(0.3, "te_spreadx", fgettext("X spread"), t[3])
612                 if dimension == 3 then
613                         add_field(3.6, "te_spready", fgettext("Y spread"), t[4])
614                 else
615                         fields[#fields + 1] = "label[4," .. height - 0.2 .. ";" ..
616                                         fgettext("2D Noise") .. "]"
617                 end
618                 add_field(6.9, "te_spreadz", fgettext("Z spread"), t[5])
619                 height = height + 1.1
620
621                 -- Third row
622                 add_field(0.3, "te_octaves", fgettext("Octaves"),     t[7])
623                 add_field(3.6, "te_persist", fgettext("Persistence"), t[8])
624                 add_field(6.9, "te_lacun",   fgettext("Lacunarity"),  t[9])
625                 height = height + 1.1
626
627
628                 local enabled_flags = flags_to_table(t[10])
629                 local flags = {}
630                 for _, name in ipairs(enabled_flags) do
631                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
632                         flags[name] = true
633                 end
634                 for _, name in ipairs(setting.flags) do
635                         local checkbox_name = "cb_" .. name
636                         local is_enabled = flags[name] == true -- to get false if nil
637                         checkboxes[checkbox_name] = is_enabled
638                 end
639                 -- Flags
640                 formspec = table.concat(fields)
641                                 .. "checkbox[0.5," .. height - 0.6 .. ";cb_defaults;"
642                                 --[[~ "defaults" is a noise parameter flag.
643                                 It describes the default processing options
644                                 for noise settings in main menu -> "All Settings". ]]
645                                 .. fgettext("defaults") .. ";" -- defaults
646                                 .. tostring(flags["defaults"] == true) .. "]" -- to get false if nil
647                                 .. "checkbox[5," .. height - 0.6 .. ";cb_eased;"
648                                 --[[~ "eased" is a noise parameter flag.
649                                 It is used to make the map smoother and
650                                 can be enabled in noise settings in
651                                 main menu -> "All Settings". ]]
652                                 .. fgettext("eased") .. ";" -- eased
653                                 .. tostring(flags["eased"] == true) .. "]"
654                                 .. "checkbox[5," .. height - 0.15 .. ";cb_absvalue;"
655                                 --[[~ "absvalue" is a noise parameter flag.
656                                 It is short for "absolute value".
657                                 It can be enabled in noise settings in
658                                 main menu -> "All Settings". ]]
659                                 .. fgettext("absvalue") .. ";" -- absvalue
660                                 .. tostring(flags["absvalue"] == true) .. "]"
661                 height = height + 1
662
663         elseif setting.type == "v3f" then
664                 local val = get_current_value(setting)
665                 local v3f = {}
666                 for line in val:gmatch("[+-]?[%d.+-eE]+") do -- All numeric characters
667                         table.insert(v3f, line)
668                 end
669
670                 height = height + 0.3
671                 formspec = formspec
672                                 .. "field[0.3," .. height .. ";3.3,1;te_x;"
673                                 .. fgettext("X") .. ";" -- X
674                                 .. core.formspec_escape(v3f[1] or "") .. "]"
675                                 .. "field[3.6," .. height .. ";3.3,1;te_y;"
676                                 .. fgettext("Y") .. ";" -- Y
677                                 .. core.formspec_escape(v3f[2] or "") .. "]"
678                                 .. "field[6.9," .. height .. ";3.3,1;te_z;"
679                                 .. fgettext("Z") .. ";" -- Z
680                                 .. core.formspec_escape(v3f[3] or "") .. "]"
681                 height = height + 1.1
682
683         elseif setting.type == "flags" then
684                 local current_flags = flags_to_table(get_current_value(setting))
685                 local flags = {}
686                 for _, name in ipairs(current_flags) do
687                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
688                         if name:sub(1, 2) == "no" then
689                                 flags[name:sub(3)] = false
690                         else
691                                 flags[name] = true
692                         end
693                 end
694                 local flags_count = #setting.possible / 2
695                 local max_height = math.ceil(flags_count / 2) / 2
696
697                 -- More space for flags
698                 description_height = description_height - 1
699                 height = height - 1
700
701                 local fields = {} -- To build formspec
702                 local j = 1
703                 for _, name in ipairs(setting.possible) do
704                         if name:sub(1, 2) ~= "no" then
705                                 local x = 0.5
706                                 local y = height + j / 2 - 0.75
707                                 if j - 1 >= flags_count / 2 then -- 2nd column
708                                         x = 5
709                                         y = y - max_height
710                                 end
711                                 j = j + 1;
712                                 local checkbox_name = "cb_" .. name
713                                 local is_enabled = flags[name] == true -- to get false if nil
714                                 checkboxes[checkbox_name] = is_enabled
715
716                                 fields[#fields + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format(
717                                         x, y, checkbox_name, name, tostring(is_enabled)
718                                 )
719                         end
720                 end
721                 formspec = table.concat(fields)
722                 height = height + max_height + 0.25
723
724         else
725                 -- TODO: fancy input for float, int
726                 local text = get_current_value(setting)
727                 if dialogdata.error_message and dialogdata.entered_text then
728                         text = dialogdata.entered_text
729                 end
730                 formspec = "field[0.28," .. height + 0.15 .. ";" .. width .. ",1;te_setting_value;;"
731                                 .. core.formspec_escape(text) .. "]"
732                 height = height + 1.15
733         end
734
735         -- Box good, textarea bad. Calculate textarea size from box.
736         local function create_textfield(size, label, text, bg_color)
737                 local textarea = {
738                         x = size.x + 0.3,
739                         y = size.y,
740                         w = size.w + 0.25,
741                         h = size.h * 1.16 + 0.12
742                 }
743                 return ("box[%f,%f;%f,%f;%s]textarea[%f,%f;%f,%f;;%s;%s]"):format(
744                         size.x, size.y, size.w, size.h, bg_color or "#000",
745                         textarea.x, textarea.y, textarea.w, textarea.h,
746                         core.formspec_escape(label), core.formspec_escape(text)
747                 )
748
749         end
750
751         -- When there's an error: Shrink description textarea and add error below
752         if dialogdata.error_message then
753                 local error_box = {
754                         x = 0,
755                         y = description_height - 0.4,
756                         w = width - 0.25,
757                         h = 0.5
758                 }
759                 formspec = formspec ..
760                         create_textfield(error_box, "", dialogdata.error_message, "#600")
761                 description_height = description_height - 0.75
762         end
763
764         -- Get description field
765         local description_box = {
766                 x = 0,
767                 y = 0.2,
768                 w = width - 0.25,
769                 h = description_height
770         }
771
772         local setting_name = setting.name
773         if setting.readable_name then
774                 setting_name = fgettext_ne(setting.readable_name) ..
775                         " (" .. setting.name .. ")"
776         end
777
778         local comment_text
779         if setting.comment == "" then
780                 comment_text = fgettext_ne("(No description of setting given)")
781         else
782                 comment_text = fgettext_ne(setting.comment)
783         end
784
785         return (
786                 "size[" .. width .. "," .. height + 0.25 .. ",true]" ..
787                 create_textfield(description_box, setting_name, comment_text) ..
788                 formspec ..
789                 "button[" .. width / 2 - 2.5 .. "," .. height - 0.4 .. ";2.5,1;btn_done;" ..
790                         fgettext("Save") .. "]" ..
791                 "button[" .. width / 2 .. "," .. height - 0.4 .. ";2.5,1;btn_cancel;" ..
792                         fgettext("Cancel") .. "]"
793         )
794 end
795
796 local function handle_change_setting_buttons(this, fields)
797         local setting = settings[selected_setting]
798         if fields["btn_done"] or fields["key_enter"] then
799                 if setting.type == "bool" then
800                         local new_value = fields["dd_setting_value"]
801                         -- Note: new_value is the actual (translated) value shown in the dropdown
802                         core.settings:set_bool(setting.name, new_value == fgettext("Enabled"))
803
804                 elseif setting.type == "enum" then
805                         local new_value = fields["dd_setting_value"]
806                         core.settings:set(setting.name, new_value)
807
808                 elseif setting.type == "int" then
809                         local new_value = tonumber(fields["te_setting_value"])
810                         if not new_value or math.floor(new_value) ~= new_value then
811                                 this.data.error_message = fgettext_ne("Please enter a valid integer.")
812                                 this.data.entered_text = fields["te_setting_value"]
813                                 core.update_formspec(this:get_formspec())
814                                 return true
815                         end
816                         if setting.min and new_value < setting.min then
817                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
818                                 this.data.entered_text = fields["te_setting_value"]
819                                 core.update_formspec(this:get_formspec())
820                                 return true
821                         end
822                         if setting.max and new_value > setting.max then
823                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
824                                 this.data.entered_text = fields["te_setting_value"]
825                                 core.update_formspec(this:get_formspec())
826                                 return true
827                         end
828                         core.settings:set(setting.name, new_value)
829
830                 elseif setting.type == "float" then
831                         local new_value = tonumber(fields["te_setting_value"])
832                         if not new_value then
833                                 this.data.error_message = fgettext_ne("Please enter a valid number.")
834                                 this.data.entered_text = fields["te_setting_value"]
835                                 core.update_formspec(this:get_formspec())
836                                 return true
837                         end
838                         if setting.min and new_value < setting.min then
839                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
840                                 this.data.entered_text = fields["te_setting_value"]
841                                 core.update_formspec(this:get_formspec())
842                                 return true
843                         end
844                         if setting.max and new_value > setting.max then
845                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
846                                 this.data.entered_text = fields["te_setting_value"]
847                                 core.update_formspec(this:get_formspec())
848                                 return true
849                         end
850                         core.settings:set(setting.name, new_value)
851
852                 elseif setting.type == "flags" then
853                         local values = {}
854                         for _, name in ipairs(setting.possible) do
855                                 if name:sub(1, 2) ~= "no" then
856                                         if checkboxes["cb_" .. name] then
857                                                 table.insert(values, name)
858                                         else
859                                                 table.insert(values, "no" .. name)
860                                         end
861                                 end
862                         end
863
864                         checkboxes = {}
865
866                         local new_value = table.concat(values, ", ")
867                         core.settings:set(setting.name, new_value)
868
869                 elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
870                         local np_flags = {}
871                         for _, name in ipairs(setting.flags) do
872                                 if checkboxes["cb_" .. name] then
873                                         table.insert(np_flags, name)
874                                 end
875                         end
876
877                         checkboxes = {}
878
879                         if setting.type == "noise_params_2d" then
880                                  fields["te_spready"] = fields["te_spreadz"]
881                         end
882                         local new_value = {
883                                 offset = fields["te_offset"],
884                                 scale = fields["te_scale"],
885                                 spread = {
886                                         x = fields["te_spreadx"],
887                                         y = fields["te_spready"],
888                                         z = fields["te_spreadz"]
889                                 },
890                                 seed = fields["te_seed"],
891                                 octaves = fields["te_octaves"],
892                                 persistence = fields["te_persist"],
893                                 lacunarity = fields["te_lacun"],
894                                 flags = table.concat(np_flags, ", ")
895                         }
896                         core.settings:set_np_group(setting.name, new_value)
897
898                 elseif setting.type == "v3f" then
899                         local new_value = "("
900                                         .. fields["te_x"] .. ", "
901                                         .. fields["te_y"] .. ", "
902                                         .. fields["te_z"] .. ")"
903                         core.settings:set(setting.name, new_value)
904
905                 else
906                         local new_value = fields["te_setting_value"]
907                         core.settings:set(setting.name, new_value)
908                 end
909                 core.settings:write()
910                 this:delete()
911                 return true
912         end
913
914         if fields["btn_cancel"] then
915                 this:delete()
916                 return true
917         end
918
919         if fields["btn_browser_path"] then
920                 core.show_path_select_dialog("dlg_browse_path",
921                         fgettext_ne("Select directory"), false)
922         end
923
924         if fields["btn_browser_filepath"] then
925                 core.show_path_select_dialog("dlg_browse_path",
926                         fgettext_ne("Select file"), true)
927         end
928
929         if fields["dlg_browse_path_accepted"] then
930                 this.data.selected_path = fields["dlg_browse_path_accepted"]
931                 core.update_formspec(this:get_formspec())
932         end
933
934         if setting.type == "flags"
935                         or setting.type == "noise_params_2d"
936                         or setting.type == "noise_params_3d" then
937                 for name, value in pairs(fields) do
938                         if name:sub(1, 3) == "cb_" then
939                                 checkboxes[name] = value == "true"
940                         end
941                 end
942         end
943
944         return false
945 end
946
947 local function create_settings_formspec(tabview, _, tabdata)
948         local formspec = "size[12,5.4;true]" ..
949                         "tablecolumns[color;tree;text,width=28;text]" ..
950                         "tableoptions[background=#00000000;border=false]" ..
951                         "field[0.3,0.1;10.2,1;search_string;;" .. core.formspec_escape(search_string) .. "]" ..
952                         "field_close_on_enter[search_string;false]" ..
953                         "button[10.2,-0.2;2,1;search;" .. fgettext("Search") .. "]" ..
954                         "table[0,0.8;12,3.5;list_settings;"
955
956         local current_level = 0
957         for _, entry in ipairs(settings) do
958                 local name
959                 if not core.settings:get_bool("main_menu_technical_settings") and entry.readable_name then
960                         name = fgettext_ne(entry.readable_name)
961                 else
962                         name = entry.name
963                 end
964
965                 if entry.type == "category" then
966                         current_level = entry.level
967                         formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
968
969                 elseif entry.type == "bool" then
970                         local value = get_current_value(entry)
971                         if core.is_yes(value) then
972                                 value = fgettext("Enabled")
973                         else
974                                 value = fgettext("Disabled")
975                         end
976                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
977                                         .. value .. ","
978
979                 elseif entry.type == "key" then --luacheck: ignore
980                         -- ignore key settings, since we have a special dialog for them
981
982                 elseif entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then
983                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
984                                         .. core.formspec_escape(get_current_np_group_as_string(entry)) .. ","
985
986                 else
987                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
988                                         .. core.formspec_escape(get_current_value(entry)) .. ","
989                 end
990         end
991
992         if #settings > 0 then
993                 formspec = formspec:sub(1, -2) -- remove trailing comma
994         end
995         formspec = formspec .. ";" .. selected_setting .. "]" ..
996                         "button[0,4.9;4,1;btn_back;".. fgettext("< Back to Settings page") .. "]" ..
997                         "button[10,4.9;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
998                         "button[7,4.9;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
999                         "checkbox[0,4.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
1000                                         .. dump(core.settings:get_bool("main_menu_technical_settings")) .. "]"
1001
1002         return formspec
1003 end
1004
1005 local function handle_settings_buttons(this, fields, tabname, tabdata)
1006         local list_enter = false
1007         if fields["list_settings"] then
1008                 selected_setting = core.get_table_index("list_settings")
1009                 if core.explode_table_event(fields["list_settings"]).type == "DCL" then
1010                         -- Directly toggle booleans
1011                         local setting = settings[selected_setting]
1012                         if setting and setting.type == "bool" then
1013                                 local current_value = get_current_value(setting)
1014                                 core.settings:set_bool(setting.name, not core.is_yes(current_value))
1015                                 core.settings:write()
1016                                 return true
1017                         else
1018                                 list_enter = true
1019                         end
1020                 else
1021                         return true
1022                 end
1023         end
1024
1025         if fields.search or fields.key_enter_field == "search_string" then
1026                 if search_string == fields.search_string then
1027                         if selected_setting > 0 then
1028                                 -- Go to next result on enter press
1029                                 local i = selected_setting + 1
1030                                 local looped = false
1031                                 while i > #settings or settings[i].type == "category" do
1032                                         i = i + 1
1033                                         if i > #settings then
1034                                                 -- Stop infinte looping
1035                                                 if looped then
1036                                                         return false
1037                                                 end
1038                                                 i = 1
1039                                                 looped = true
1040                                         end
1041                                 end
1042                                 selected_setting = i
1043                                 core.update_formspec(this:get_formspec())
1044                                 return true
1045                         end
1046                 else
1047                         -- Search for setting
1048                         search_string = fields.search_string
1049                         settings, selected_setting = filter_settings(full_settings, search_string)
1050                         core.update_formspec(this:get_formspec())
1051                 end
1052                 return true
1053         end
1054
1055         if fields["btn_edit"] or list_enter then
1056                 local setting = settings[selected_setting]
1057                 if setting and setting.type ~= "category" then
1058                         local edit_dialog = dialog_create("change_setting",
1059                                         create_change_setting_formspec, handle_change_setting_buttons)
1060                         edit_dialog:set_parent(this)
1061                         this:hide()
1062                         edit_dialog:show()
1063                 end
1064                 return true
1065         end
1066
1067         if fields["btn_restore"] then
1068                 local setting = settings[selected_setting]
1069                 if setting and setting.type ~= "category" then
1070                         core.settings:remove(setting.name)
1071                         core.settings:write()
1072                         core.update_formspec(this:get_formspec())
1073                 end
1074                 return true
1075         end
1076
1077         if fields["btn_back"] then
1078                 this:delete()
1079                 return true
1080         end
1081
1082         if fields["cb_tech_settings"] then
1083                 core.settings:set("main_menu_technical_settings", fields["cb_tech_settings"])
1084                 core.settings:write()
1085                 core.update_formspec(this:get_formspec())
1086                 return true
1087         end
1088
1089         return false
1090 end
1091
1092 function create_adv_settings_dlg()
1093         local dlg = dialog_create("settings_advanced",
1094                                 create_settings_formspec,
1095                                 handle_settings_buttons,
1096                                 nil)
1097
1098                                 return dlg
1099 end
1100
1101 -- Uncomment to generate 'minetest.conf.example' and 'settings_translation_file.cpp'.
1102 -- For RUN_IN_PLACE the generated files may appear in the 'bin' folder.
1103 -- See comment and alternative line at the end of 'generate_from_settingtypes.lua'.
1104
1105 --assert(loadfile(core.get_builtin_path().."mainmenu"..DIR_DELIM..
1106 --              "generate_from_settingtypes.lua"))(parse_config_file(true, false))