]> git.lizzy.rs Git - dragonfireclient.git/blob - builtin/mainmenu/dlg_settings_advanced.lua
Merge branch 'master' of https://github.com/minetest/minetest
[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                                         readable_name = mod.title,
399                                         level = 1,
400                                         type = "category",
401                                 })
402
403                                 parse_single_file(file, path, read_all, settings, 2, false)
404
405                                 file:close()
406                         end
407                 end
408
409                 -- Parse clientmods
410                 local clientmods_category_initialized = false
411                 local clientmods = {}
412                 get_mods(core.get_clientmodpath(), "clientmods", clientmods)
413                 for _, clientmod in ipairs(clientmods) do
414                         local path = clientmod.path .. DIR_DELIM .. FILENAME
415                         local file = io.open(path, "r")
416                         if file then
417                                 if not clientmods_category_initialized then
418                                         fgettext_ne("Clientmods") -- not used, but needed for xgettext
419                                         table.insert(settings, {
420                                                 name = "Clientmods",
421                                                 level = 0,
422                                                 type = "category",
423                                         })
424                                         clientmods_category_initialized = true
425                                 end
426
427                                 table.insert(settings, {
428                                         name = clientmod.name,
429                                         level = 1,
430                                         type = "category",
431                                 })
432
433                                 parse_single_file(file, path, read_all, settings, 2, false)
434
435                                 file:close()
436                         end
437                 end
438         end
439
440         return settings
441 end
442
443 local function filter_settings(settings, searchstring)
444         if not searchstring or searchstring == "" then
445                 return settings, -1
446         end
447
448         -- Setup the keyword list
449         local keywords = {}
450         for word in searchstring:lower():gmatch("%S+") do
451                 table.insert(keywords, word)
452         end
453
454         local result = {}
455         local category_stack = {}
456         local current_level = 0
457         local best_setting = nil
458         for _, entry in pairs(settings) do
459                 if entry.type == "category" then
460                         -- Remove all settingless categories
461                         while #category_stack > 0 and entry.level <= current_level do
462                                 table.remove(category_stack, #category_stack)
463                                 if #category_stack > 0 then
464                                         current_level = category_stack[#category_stack].level
465                                 else
466                                         current_level = 0
467                                 end
468                         end
469
470                         -- Push category onto stack
471                         category_stack[#category_stack + 1] = entry
472                         current_level = entry.level
473                 else
474                         -- See if setting matches keywords
475                         local setting_score = 0
476                         for k = 1, #keywords do
477                                 local keyword = keywords[k]
478
479                                 if string.find(entry.name:lower(), keyword, 1, true) then
480                                         setting_score = setting_score + 1
481                                 end
482
483                                 if entry.readable_name and
484                                                 string.find(fgettext(entry.readable_name):lower(), keyword, 1, true) then
485                                         setting_score = setting_score + 1
486                                 end
487
488                                 if entry.comment and
489                                                 string.find(fgettext_ne(entry.comment):lower(), keyword, 1, true) then
490                                         setting_score = setting_score + 1
491                                 end
492                         end
493
494                         -- Add setting to results if match
495                         if setting_score > 0 then
496                                 -- Add parent categories
497                                 for _, category in pairs(category_stack) do
498                                         result[#result + 1] = category
499                                 end
500                                 category_stack = {}
501
502                                 -- Add setting
503                                 result[#result + 1] = entry
504                                 entry.score = setting_score
505
506                                 if not best_setting or
507                                                 setting_score > result[best_setting].score then
508                                         best_setting = #result
509                                 end
510                         end
511                 end
512         end
513         return result, best_setting or -1
514 end
515
516 local full_settings = parse_config_file(false, true)
517 local search_string = ""
518 local settings = full_settings
519 local selected_setting = 1
520
521 local function get_current_value(setting)
522         local value = core.settings:get(setting.name)
523         if value == nil then
524                 value = setting.default
525         end
526         return value
527 end
528
529 local function get_current_np_group(setting)
530         local value = core.settings:get_np_group(setting.name)
531         if value == nil then
532                 return setting.values
533         end
534         local p = "%g"
535         return {
536                 p:format(value.offset),
537                 p:format(value.scale),
538                 p:format(value.spread.x),
539                 p:format(value.spread.y),
540                 p:format(value.spread.z),
541                 p:format(value.seed),
542                 p:format(value.octaves),
543                 p:format(value.persistence),
544                 p:format(value.lacunarity),
545                 value.flags
546         }
547 end
548
549 local function get_current_np_group_as_string(setting)
550         local value = core.settings:get_np_group(setting.name)
551         if value == nil then
552                 return setting.default
553         end
554         return ("%g, %g, (%g, %g, %g), %g, %g, %g, %g"):format(
555                 value.offset,
556                 value.scale,
557                 value.spread.x,
558                 value.spread.y,
559                 value.spread.z,
560                 value.seed,
561                 value.octaves,
562                 value.persistence,
563                 value.lacunarity
564         ) .. (value.flags ~= "" and (", " .. value.flags) or "")
565 end
566
567 local checkboxes = {} -- handle checkboxes events
568
569 local function create_change_setting_formspec(dialogdata)
570         local setting = settings[selected_setting]
571         -- Final formspec will be created at the end of this function
572         -- Default values below, may be changed depending on setting type
573         local width = 10
574         local height = 3.5
575         local description_height = 3
576         local formspec = ""
577
578         -- Setting-specific formspec elements
579         if setting.type == "bool" then
580                 local selected_index = 1
581                 if core.is_yes(get_current_value(setting)) then
582                         selected_index = 2
583                 end
584                 formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;"
585                                 .. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";"
586                                 .. selected_index .. "]"
587                 height = height + 1.25
588
589         elseif setting.type == "enum" then
590                 local selected_index = 0
591                 formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;"
592                 for index, value in ipairs(setting.values) do
593                         -- translating value is not possible, since it's the value
594                         --  that we set the setting to
595                         formspec = formspec ..  core.formspec_escape(value) .. ","
596                         if get_current_value(setting) == value then
597                                 selected_index = index
598                         end
599                 end
600                 if #setting.values > 0 then
601                         formspec = formspec:sub(1, -2) -- remove trailing comma
602                 end
603                 formspec = formspec .. ";" .. selected_index .. "]"
604                 height = height + 1.25
605
606         elseif setting.type == "path" or setting.type == "filepath" then
607                 local current_value = dialogdata.selected_path
608                 if not current_value then
609                         current_value = get_current_value(setting)
610                 end
611                 formspec = "field[0.28," .. height + 0.15 .. ";8,1;te_setting_value;;"
612                                 .. core.formspec_escape(current_value) .. "]"
613                                 .. "button[8," .. height - 0.15 .. ";2,1;btn_browser_"
614                                 .. setting.type .. ";" .. fgettext("Browse") .. "]"
615                 height = height + 1.15
616
617         elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
618                 local t = get_current_np_group(setting)
619                 local dimension = 3
620                 if setting.type == "noise_params_2d" then
621                         dimension = 2
622                 end
623
624                 -- More space for 3x3 fields
625                 description_height = description_height - 1.5
626                 height = height - 1.5
627
628                 local fields = {}
629                 local function add_field(x, name, label, value)
630                         fields[#fields + 1] = ("field[%f,%f;3.3,1;%s;%s;%s]"):format(
631                                 x, height, name, label, core.formspec_escape(value or "")
632                         )
633                 end
634                 -- First row
635                 height = height + 0.3
636                 add_field(0.3, "te_offset", fgettext("Offset"), t[1])
637                 add_field(3.6, "te_scale",  fgettext("Scale"),  t[2])
638                 add_field(6.9, "te_seed",   fgettext("Seed"),   t[6])
639                 height = height + 1.1
640
641                 -- Second row
642                 add_field(0.3, "te_spreadx", fgettext("X spread"), t[3])
643                 if dimension == 3 then
644                         add_field(3.6, "te_spready", fgettext("Y spread"), t[4])
645                 else
646                         fields[#fields + 1] = "label[4," .. height - 0.2 .. ";" ..
647                                         fgettext("2D Noise") .. "]"
648                 end
649                 add_field(6.9, "te_spreadz", fgettext("Z spread"), t[5])
650                 height = height + 1.1
651
652                 -- Third row
653                 add_field(0.3, "te_octaves", fgettext("Octaves"),     t[7])
654                 add_field(3.6, "te_persist", fgettext("Persistence"), t[8])
655                 add_field(6.9, "te_lacun",   fgettext("Lacunarity"),  t[9])
656                 height = height + 1.1
657
658
659                 local enabled_flags = flags_to_table(t[10])
660                 local flags = {}
661                 for _, name in ipairs(enabled_flags) do
662                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
663                         flags[name] = true
664                 end
665                 for _, name in ipairs(setting.flags) do
666                         local checkbox_name = "cb_" .. name
667                         local is_enabled = flags[name] == true -- to get false if nil
668                         checkboxes[checkbox_name] = is_enabled
669                 end
670                 -- Flags
671                 formspec = table.concat(fields)
672                                 .. "checkbox[0.5," .. height - 0.6 .. ";cb_defaults;"
673                                 --[[~ "defaults" is a noise parameter flag.
674                                 It describes the default processing options
675                                 for noise settings in main menu -> "All Settings". ]]
676                                 .. fgettext("defaults") .. ";" -- defaults
677                                 .. tostring(flags["defaults"] == true) .. "]" -- to get false if nil
678                                 .. "checkbox[5," .. height - 0.6 .. ";cb_eased;"
679                                 --[[~ "eased" is a noise parameter flag.
680                                 It is used to make the map smoother and
681                                 can be enabled in noise settings in
682                                 main menu -> "All Settings". ]]
683                                 .. fgettext("eased") .. ";" -- eased
684                                 .. tostring(flags["eased"] == true) .. "]"
685                                 .. "checkbox[5," .. height - 0.15 .. ";cb_absvalue;"
686                                 --[[~ "absvalue" is a noise parameter flag.
687                                 It is short for "absolute value".
688                                 It can be enabled in noise settings in
689                                 main menu -> "All Settings". ]]
690                                 .. fgettext("absvalue") .. ";" -- absvalue
691                                 .. tostring(flags["absvalue"] == true) .. "]"
692                 height = height + 1
693
694         elseif setting.type == "v3f" then
695                 local val = get_current_value(setting)
696                 local v3f = {}
697                 for line in val:gmatch("[+-]?[%d.+-eE]+") do -- All numeric characters
698                         table.insert(v3f, line)
699                 end
700
701                 height = height + 0.3
702                 formspec = formspec
703                                 .. "field[0.3," .. height .. ";3.3,1;te_x;"
704                                 .. fgettext("X") .. ";" -- X
705                                 .. core.formspec_escape(v3f[1] or "") .. "]"
706                                 .. "field[3.6," .. height .. ";3.3,1;te_y;"
707                                 .. fgettext("Y") .. ";" -- Y
708                                 .. core.formspec_escape(v3f[2] or "") .. "]"
709                                 .. "field[6.9," .. height .. ";3.3,1;te_z;"
710                                 .. fgettext("Z") .. ";" -- Z
711                                 .. core.formspec_escape(v3f[3] or "") .. "]"
712                 height = height + 1.1
713
714         elseif setting.type == "flags" then
715                 local current_flags = flags_to_table(get_current_value(setting))
716                 local flags = {}
717                 for _, name in ipairs(current_flags) do
718                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
719                         if name:sub(1, 2) == "no" then
720                                 flags[name:sub(3)] = false
721                         else
722                                 flags[name] = true
723                         end
724                 end
725                 local flags_count = #setting.possible / 2
726                 local max_height = math.ceil(flags_count / 2) / 2
727
728                 -- More space for flags
729                 description_height = description_height - 1
730                 height = height - 1
731
732                 local fields = {} -- To build formspec
733                 local j = 1
734                 for _, name in ipairs(setting.possible) do
735                         if name:sub(1, 2) ~= "no" then
736                                 local x = 0.5
737                                 local y = height + j / 2 - 0.75
738                                 if j - 1 >= flags_count / 2 then -- 2nd column
739                                         x = 5
740                                         y = y - max_height
741                                 end
742                                 j = j + 1;
743                                 local checkbox_name = "cb_" .. name
744                                 local is_enabled = flags[name] == true -- to get false if nil
745                                 checkboxes[checkbox_name] = is_enabled
746
747                                 fields[#fields + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format(
748                                         x, y, checkbox_name, name, tostring(is_enabled)
749                                 )
750                         end
751                 end
752                 formspec = table.concat(fields)
753                 height = height + max_height + 0.25
754
755         else
756                 -- TODO: fancy input for float, int
757                 local text = get_current_value(setting)
758                 if dialogdata.error_message and dialogdata.entered_text then
759                         text = dialogdata.entered_text
760                 end
761                 formspec = "field[0.28," .. height + 0.15 .. ";" .. width .. ",1;te_setting_value;;"
762                                 .. core.formspec_escape(text) .. "]"
763                 height = height + 1.15
764         end
765
766         -- Box good, textarea bad. Calculate textarea size from box.
767         local function create_textfield(size, label, text, bg_color)
768                 local textarea = {
769                         x = size.x + 0.3,
770                         y = size.y,
771                         w = size.w + 0.25,
772                         h = size.h * 1.16 + 0.12
773                 }
774                 return ("box[%f,%f;%f,%f;%s]textarea[%f,%f;%f,%f;;%s;%s]"):format(
775                         size.x, size.y, size.w, size.h, bg_color or "#000",
776                         textarea.x, textarea.y, textarea.w, textarea.h,
777                         core.formspec_escape(label), core.formspec_escape(text)
778                 )
779
780         end
781
782         -- When there's an error: Shrink description textarea and add error below
783         if dialogdata.error_message then
784                 local error_box = {
785                         x = 0,
786                         y = description_height - 0.4,
787                         w = width - 0.25,
788                         h = 0.5
789                 }
790                 formspec = formspec ..
791                         create_textfield(error_box, "", dialogdata.error_message, "#600")
792                 description_height = description_height - 0.75
793         end
794
795         -- Get description field
796         local description_box = {
797                 x = 0,
798                 y = 0.2,
799                 w = width - 0.25,
800                 h = description_height
801         }
802
803         local setting_name = setting.name
804         if setting.readable_name then
805                 setting_name = fgettext_ne(setting.readable_name) ..
806                         " (" .. setting.name .. ")"
807         end
808
809         local comment_text
810         if setting.comment == "" then
811                 comment_text = fgettext_ne("(No description of setting given)")
812         else
813                 comment_text = fgettext_ne(setting.comment)
814         end
815
816         return (
817                 "size[" .. width .. "," .. height + 0.25 .. ",true]" ..
818                 create_textfield(description_box, setting_name, comment_text) ..
819                 formspec ..
820                 "button[" .. width / 2 - 2.5 .. "," .. height - 0.4 .. ";2.5,1;btn_done;" ..
821                         fgettext("Save") .. "]" ..
822                 "button[" .. width / 2 .. "," .. height - 0.4 .. ";2.5,1;btn_cancel;" ..
823                         fgettext("Cancel") .. "]"
824         )
825 end
826
827 local function handle_change_setting_buttons(this, fields)
828         local setting = settings[selected_setting]
829         if fields["btn_done"] or fields["key_enter"] then
830                 if setting.type == "bool" then
831                         local new_value = fields["dd_setting_value"]
832                         -- Note: new_value is the actual (translated) value shown in the dropdown
833                         core.settings:set_bool(setting.name, new_value == fgettext("Enabled"))
834
835                 elseif setting.type == "enum" then
836                         local new_value = fields["dd_setting_value"]
837                         core.settings:set(setting.name, new_value)
838
839                 elseif setting.type == "int" then
840                         local new_value = tonumber(fields["te_setting_value"])
841                         if not new_value or math.floor(new_value) ~= new_value then
842                                 this.data.error_message = fgettext_ne("Please enter a valid integer.")
843                                 this.data.entered_text = fields["te_setting_value"]
844                                 core.update_formspec(this:get_formspec())
845                                 return true
846                         end
847                         if setting.min and new_value < setting.min then
848                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
849                                 this.data.entered_text = fields["te_setting_value"]
850                                 core.update_formspec(this:get_formspec())
851                                 return true
852                         end
853                         if setting.max and new_value > setting.max then
854                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
855                                 this.data.entered_text = fields["te_setting_value"]
856                                 core.update_formspec(this:get_formspec())
857                                 return true
858                         end
859                         core.settings:set(setting.name, new_value)
860
861                 elseif setting.type == "float" then
862                         local new_value = tonumber(fields["te_setting_value"])
863                         if not new_value then
864                                 this.data.error_message = fgettext_ne("Please enter a valid number.")
865                                 this.data.entered_text = fields["te_setting_value"]
866                                 core.update_formspec(this:get_formspec())
867                                 return true
868                         end
869                         if setting.min and new_value < setting.min then
870                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
871                                 this.data.entered_text = fields["te_setting_value"]
872                                 core.update_formspec(this:get_formspec())
873                                 return true
874                         end
875                         if setting.max and new_value > setting.max then
876                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
877                                 this.data.entered_text = fields["te_setting_value"]
878                                 core.update_formspec(this:get_formspec())
879                                 return true
880                         end
881                         core.settings:set(setting.name, new_value)
882
883                 elseif setting.type == "flags" then
884                         local values = {}
885                         for _, name in ipairs(setting.possible) do
886                                 if name:sub(1, 2) ~= "no" then
887                                         if checkboxes["cb_" .. name] then
888                                                 table.insert(values, name)
889                                         else
890                                                 table.insert(values, "no" .. name)
891                                         end
892                                 end
893                         end
894
895                         checkboxes = {}
896
897                         local new_value = table.concat(values, ", ")
898                         core.settings:set(setting.name, new_value)
899
900                 elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
901                         local np_flags = {}
902                         for _, name in ipairs(setting.flags) do
903                                 if checkboxes["cb_" .. name] then
904                                         table.insert(np_flags, name)
905                                 end
906                         end
907
908                         checkboxes = {}
909
910                         if setting.type == "noise_params_2d" then
911                                  fields["te_spready"] = fields["te_spreadz"]
912                         end
913                         local new_value = {
914                                 offset = fields["te_offset"],
915                                 scale = fields["te_scale"],
916                                 spread = {
917                                         x = fields["te_spreadx"],
918                                         y = fields["te_spready"],
919                                         z = fields["te_spreadz"]
920                                 },
921                                 seed = fields["te_seed"],
922                                 octaves = fields["te_octaves"],
923                                 persistence = fields["te_persist"],
924                                 lacunarity = fields["te_lacun"],
925                                 flags = table.concat(np_flags, ", ")
926                         }
927                         core.settings:set_np_group(setting.name, new_value)
928
929                 elseif setting.type == "v3f" then
930                         local new_value = "("
931                                         .. fields["te_x"] .. ", "
932                                         .. fields["te_y"] .. ", "
933                                         .. fields["te_z"] .. ")"
934                         core.settings:set(setting.name, new_value)
935
936                 else
937                         local new_value = fields["te_setting_value"]
938                         core.settings:set(setting.name, new_value)
939                 end
940                 core.settings:write()
941                 this:delete()
942                 return true
943         end
944
945         if fields["btn_cancel"] then
946                 this:delete()
947                 return true
948         end
949
950         if fields["btn_browser_path"] then
951                 core.show_path_select_dialog("dlg_browse_path",
952                         fgettext_ne("Select directory"), false)
953         end
954
955         if fields["btn_browser_filepath"] then
956                 core.show_path_select_dialog("dlg_browse_path",
957                         fgettext_ne("Select file"), true)
958         end
959
960         if fields["dlg_browse_path_accepted"] then
961                 this.data.selected_path = fields["dlg_browse_path_accepted"]
962                 core.update_formspec(this:get_formspec())
963         end
964
965         if setting.type == "flags"
966                         or setting.type == "noise_params_2d"
967                         or setting.type == "noise_params_3d" then
968                 for name, value in pairs(fields) do
969                         if name:sub(1, 3) == "cb_" then
970                                 checkboxes[name] = value == "true"
971                         end
972                 end
973         end
974
975         return false
976 end
977
978 local function create_settings_formspec(tabview, _, tabdata)
979         local formspec = "size[12,5.4;true]" ..
980                         "tablecolumns[color;tree;text,width=28;text]" ..
981                         "tableoptions[background=#00000000;border=false]" ..
982                         "field[0.3,0.1;10.2,1;search_string;;" .. core.formspec_escape(search_string) .. "]" ..
983                         "field_close_on_enter[search_string;false]" ..
984                         "button[10.2,-0.2;2,1;search;" .. fgettext("Search") .. "]" ..
985                         "table[0,0.8;12,3.5;list_settings;"
986
987         local current_level = 0
988         for _, entry in ipairs(settings) do
989                 local name
990                 if not core.settings:get_bool("show_technical_names") and entry.readable_name then
991                         name = fgettext_ne(entry.readable_name)
992                 else
993                         name = entry.name
994                 end
995
996                 if entry.type == "category" then
997                         current_level = entry.level
998                         formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
999
1000                 elseif entry.type == "bool" then
1001                         local value = get_current_value(entry)
1002                         if core.is_yes(value) then
1003                                 value = fgettext("Enabled")
1004                         else
1005                                 value = fgettext("Disabled")
1006                         end
1007                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
1008                                         .. value .. ","
1009
1010                 elseif entry.type == "key" then --luacheck: ignore
1011                         -- ignore key settings, since we have a special dialog for them
1012
1013                 elseif entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then
1014                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
1015                                         .. core.formspec_escape(get_current_np_group_as_string(entry)) .. ","
1016
1017                 else
1018                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
1019                                         .. core.formspec_escape(get_current_value(entry)) .. ","
1020                 end
1021         end
1022
1023         if #settings > 0 then
1024                 formspec = formspec:sub(1, -2) -- remove trailing comma
1025         end
1026         formspec = formspec .. ";" .. selected_setting .. "]" ..
1027                         "button[0,4.9;4,1;btn_back;".. fgettext("< Back to Settings page") .. "]" ..
1028                         "button[10,4.9;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
1029                         "button[7,4.9;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
1030                         "checkbox[0,4.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
1031                                         .. dump(core.settings:get_bool("show_technical_names")) .. "]"
1032
1033         return formspec
1034 end
1035
1036 local function handle_settings_buttons(this, fields, tabname, tabdata)
1037         local list_enter = false
1038         if fields["list_settings"] then
1039                 selected_setting = core.get_table_index("list_settings")
1040                 if core.explode_table_event(fields["list_settings"]).type == "DCL" then
1041                         -- Directly toggle booleans
1042                         local setting = settings[selected_setting]
1043                         if setting and setting.type == "bool" then
1044                                 local current_value = get_current_value(setting)
1045                                 core.settings:set_bool(setting.name, not core.is_yes(current_value))
1046                                 core.settings:write()
1047                                 return true
1048                         else
1049                                 list_enter = true
1050                         end
1051                 else
1052                         return true
1053                 end
1054         end
1055
1056         if fields.search or fields.key_enter_field == "search_string" then
1057                 if search_string == fields.search_string then
1058                         if selected_setting > 0 then
1059                                 -- Go to next result on enter press
1060                                 local i = selected_setting + 1
1061                                 local looped = false
1062                                 while i > #settings or settings[i].type == "category" do
1063                                         i = i + 1
1064                                         if i > #settings then
1065                                                 -- Stop infinte looping
1066                                                 if looped then
1067                                                         return false
1068                                                 end
1069                                                 i = 1
1070                                                 looped = true
1071                                         end
1072                                 end
1073                                 selected_setting = i
1074                                 core.update_formspec(this:get_formspec())
1075                                 return true
1076                         end
1077                 else
1078                         -- Search for setting
1079                         search_string = fields.search_string
1080                         settings, selected_setting = filter_settings(full_settings, search_string)
1081                         core.update_formspec(this:get_formspec())
1082                 end
1083                 return true
1084         end
1085
1086         if fields["btn_edit"] or list_enter then
1087                 local setting = settings[selected_setting]
1088                 if setting and setting.type ~= "category" then
1089                         local edit_dialog = dialog_create("change_setting",
1090                                         create_change_setting_formspec, handle_change_setting_buttons)
1091                         edit_dialog:set_parent(this)
1092                         this:hide()
1093                         edit_dialog:show()
1094                 end
1095                 return true
1096         end
1097
1098         if fields["btn_restore"] then
1099                 local setting = settings[selected_setting]
1100                 if setting and setting.type ~= "category" then
1101                         core.settings:remove(setting.name)
1102                         core.settings:write()
1103                         core.update_formspec(this:get_formspec())
1104                 end
1105                 return true
1106         end
1107
1108         if fields["btn_back"] then
1109                 this:delete()
1110                 return true
1111         end
1112
1113         if fields["cb_tech_settings"] then
1114                 core.settings:set("show_technical_names", fields["cb_tech_settings"])
1115                 core.settings:write()
1116                 core.update_formspec(this:get_formspec())
1117                 return true
1118         end
1119
1120         return false
1121 end
1122
1123 function create_adv_settings_dlg()
1124         local dlg = dialog_create("settings_advanced",
1125                                 create_settings_formspec,
1126                                 handle_settings_buttons,
1127                                 nil)
1128
1129                                 return dlg
1130 end
1131
1132 -- Uncomment to generate 'minetest.conf.example' and 'settings_translation_file.cpp'.
1133 -- For RUN_IN_PLACE the generated files may appear in the 'bin' folder.
1134 -- See comment and alternative line at the end of 'generate_from_settingtypes.lua'.
1135
1136 --assert(loadfile(core.get_builtin_path().."mainmenu"..DIR_DELIM..
1137 --              "generate_from_settingtypes.lua"))(parse_config_file(true, false))