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