]> git.lizzy.rs Git - minetest.git/blob - builtin/mainmenu/dlg_settings_advanced.lua
Advanced settings: Add range check for float type
[minetest.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 line in default:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
152                         index = default:find("[+-]?[%d.-e]+", index) + line:len()
153                         table.insert(values, line)
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 builtin_path = core.get_builtin_path() .. FILENAME
326         local file = io.open(builtin_path, "r")
327         local settings = {}
328         if not file then
329                 core.log("error", "Can't load " .. FILENAME)
330                 return settings
331         end
332
333         parse_single_file(file, builtin_path, read_all, settings, 0, true)
334
335         file:close()
336
337         if parse_mods then
338                 -- Parse games
339                 local games_category_initialized = false
340                 local index = 1
341                 local game = gamemgr.get_game(index)
342                 while game do
343                         local path = game.path .. DIR_DELIM .. FILENAME
344                         local file = io.open(path, "r")
345                         if file then
346                                 if not games_category_initialized then
347                                         local translation = fgettext_ne("Games"), -- not used, but needed for xgettext
348                                         table.insert(settings, {
349                                                 name = "Games",
350                                                 level = 0,
351                                                 type = "category",
352                                         })
353                                         games_category_initialized = true
354                                 end
355
356                                 table.insert(settings, {
357                                         name = game.name,
358                                         level = 1,
359                                         type = "category",
360                                 })
361
362                                 parse_single_file(file, path, read_all, settings, 2, false)
363
364                                 file:close()
365                         end
366
367                         index = index + 1
368                         game = gamemgr.get_game(index)
369                 end
370
371                 -- Parse mods
372                 local mods_category_initialized = false
373                 local mods = {}
374                 get_mods(core.get_modpath(), mods)
375                 for _, mod in ipairs(mods) do
376                         local path = mod.path .. DIR_DELIM .. FILENAME
377                         local file = io.open(path, "r")
378                         if file then
379                                 if not mods_category_initialized then
380                                         local translation = fgettext_ne("Mods"), -- not used, but needed for xgettext
381                                         table.insert(settings, {
382                                                 name = "Mods",
383                                                 level = 0,
384                                                 type = "category",
385                                         })
386                                         mods_category_initialized = true
387                                 end
388
389                                 table.insert(settings, {
390                                         name = mod.name,
391                                         level = 1,
392                                         type = "category",
393                                 })
394
395                                 parse_single_file(file, path, read_all, settings, 2, false)
396
397                                 file:close()
398                         end
399                 end
400         end
401
402         return settings
403 end
404
405 local function filter_settings(settings, searchstring)
406         if not searchstring or searchstring == "" then
407                 return settings, -1
408         end
409
410         -- Setup the keyword list
411         local keywords = {}
412         for word in searchstring:lower():gmatch("%S+") do
413                 table.insert(keywords, word)
414         end
415
416         local result = {}
417         local category_stack = {}
418         local current_level = 0
419         local best_setting = nil
420         for _, entry in pairs(settings) do
421                 if entry.type == "category" then
422                         -- Remove all settingless categories
423                         while #category_stack > 0 and entry.level <= current_level do
424                                 table.remove(category_stack, #category_stack)
425                                 if #category_stack > 0 then
426                                         current_level = category_stack[#category_stack].level
427                                 else
428                                         current_level = 0
429                                 end
430                         end
431
432                         -- Push category onto stack
433                         category_stack[#category_stack + 1] = entry
434                         current_level = entry.level
435                 else
436                         -- See if setting matches keywords
437                         local setting_score = 0
438                         for k = 1, #keywords do
439                                 local keyword = keywords[k]
440
441                                 if string.find(entry.name:lower(), keyword, 1, true) then
442                                         setting_score = setting_score + 1
443                                 end
444
445                                 if entry.readable_name and
446                                                 string.find(fgettext(entry.readable_name):lower(), keyword, 1, true) then
447                                         setting_score = setting_score + 1
448                                 end
449
450                                 if entry.comment and
451                                                 string.find(fgettext_ne(entry.comment):lower(), keyword, 1, true) then
452                                         setting_score = setting_score + 1
453                                 end
454                         end
455
456                         -- Add setting to results if match
457                         if setting_score > 0 then
458                                 -- Add parent categories
459                                 for _, category in pairs(category_stack) do
460                                         result[#result + 1] = category
461                                 end
462                                 category_stack = {}
463
464                                 -- Add setting
465                                 result[#result + 1] = entry
466                                 entry.score = setting_score
467
468                                 if not best_setting or
469                                                 setting_score > result[best_setting].score then
470                                         best_setting = #result
471                                 end
472                         end
473                 end
474         end
475         return result, best_setting or -1
476 end
477
478 local full_settings = parse_config_file(false, true)
479 local search_string = ""
480 local settings = full_settings
481 local selected_setting = 1
482
483 local function get_current_value(setting)
484         local value = core.settings:get(setting.name)
485         if value == nil then
486                 value = setting.default
487         end
488         return value
489 end
490
491 local function get_current_np_group(setting)
492         local value = core.settings:get_np_group(setting.name)
493         local t = {}
494         if value == nil then
495                 t = setting.values
496         else
497                 table.insert(t, value.offset)
498                 table.insert(t, value.scale)
499                 table.insert(t, value.spread.x)
500                 table.insert(t, value.spread.y)
501                 table.insert(t, value.spread.z)
502                 table.insert(t, value.seed)
503                 table.insert(t, value.octaves)
504                 table.insert(t, value.persistence)
505                 table.insert(t, value.lacunarity)
506                 table.insert(t, value.flags)
507         end
508         return t
509 end
510
511 local function get_current_np_group_as_string(setting)
512         local value = core.settings:get_np_group(setting.name)
513         local t
514         if value == nil then
515                 t = setting.default
516         else
517                 t = value.offset .. ", " ..
518                         value.scale .. ", (" ..
519                         value.spread.x .. ", " ..
520                         value.spread.y .. ", " ..
521                         value.spread.z .. "), " ..
522                         value.seed .. ", " ..
523                         value.octaves .. ", " ..
524                         value.persistence .. ", " ..
525                         value.lacunarity .. ", " ..
526                         value.flags
527         end
528         return t
529 end
530
531 local checkboxes = {} -- handle checkboxes events
532
533 local function create_change_setting_formspec(dialogdata)
534         local setting = settings[selected_setting]
535         local height = 5.2
536         if setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
537                 -- Three flags, checkboxes on 2 columns, with a vertical space of 1/2 unit
538                 height = 8.7
539         elseif setting.type == "flags" then
540                 -- Checkboxes on 2 columns, with a vertical space of 1/2 unit
541                 height = 5.2 + math.ceil(#setting.possible / 2) / 2
542         end
543         local formspec = "size[10," .. height .. ",true]" ..
544                         "button[5," .. height - 0.7 .. ";2,1;btn_done;" .. fgettext("Save") .. "]" ..
545                         "button[3," .. height - 0.7 .. ";2,1;btn_cancel;" .. fgettext("Cancel") .. "]" ..
546                         "tablecolumns[color;text]" ..
547                         "tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
548                         "table[0,0;10,3;info;"
549
550         if setting.readable_name then
551                 formspec = formspec .. "#FFFF00," .. fgettext(setting.readable_name)
552                                 .. " (" .. core.formspec_escape(setting.name) .. "),"
553         else
554                 formspec = formspec .. "#FFFF00," .. core.formspec_escape(setting.name) .. ","
555         end
556
557         formspec = formspec .. ",,"
558
559         local comment_text = ""
560
561         if setting.comment == "" then
562                 comment_text = fgettext_ne("(No description of setting given)")
563         else
564                 comment_text = fgettext_ne(setting.comment)
565         end
566         for _, comment_line in ipairs(comment_text:split("\n", true)) do
567                 formspec = formspec .. "," .. core.formspec_escape(comment_line) .. ","
568         end
569
570         formspec = formspec:sub(1, -2) -- remove trailing comma
571
572         formspec = formspec .. ";1]"
573
574         if setting.type == "bool" then
575                 local selected_index
576                 if core.is_yes(get_current_value(setting)) then
577                         selected_index = 2
578                 else
579                         selected_index = 1
580                 end
581                 formspec = formspec .. "dropdown[0.5,3.5;3,1;dd_setting_value;"
582                                 .. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";"
583                                 .. selected_index .. "]"
584
585         elseif setting.type == "enum" then
586                 local selected_index = 0
587                 formspec = formspec .. "dropdown[0.5,3.5;3,1;dd_setting_value;"
588                 for index, value in ipairs(setting.values) do
589                         -- translating value is not possible, since it's the value
590                         --  that we set the setting to
591                         formspec = formspec ..  core.formspec_escape(value) .. ","
592                         if get_current_value(setting) == value then
593                                 selected_index = index
594                         end
595                 end
596                 if #setting.values > 0 then
597                         formspec = formspec:sub(1, -2) -- remove trailing comma
598                 end
599                 formspec = formspec .. ";" .. selected_index .. "]"
600
601         elseif setting.type == "path" or setting.type == "filepath" then
602                 local current_value = dialogdata.selected_path
603                 if not current_value then
604                         current_value = get_current_value(setting)
605                 end
606                 formspec = formspec .. "field[0.5,4;7.5,1;te_setting_value;;"
607                                 .. core.formspec_escape(current_value) .. "]"
608                                 .. "button[8,3.75;2,1;btn_browser_" .. setting.type .. ";" .. fgettext("Browse") .. "]"
609
610         elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
611                 local t = get_current_np_group(setting)
612                 local dimension = 3
613                 if setting.type == "noise_params_2d" then
614                         dimension = 2
615                 end
616
617                 formspec = formspec
618                                 .. "label[0,2.5;(" .. dimension .. "D Noise)]"
619                                 .. "field[0.5,4;3.3,1;te_offset;Offset;" -- Offset
620                                 .. core.formspec_escape(t[1] or "") .. "]"
621                                 .. "field[3.8,4;3.3,1;te_scale;Scale;" -- Scale
622                                 .. core.formspec_escape(t[2] or "") .. "]"
623                                 .. "field[7.1,4;3.3,1;te_seed;Seed;" -- Seed
624                                 .. core.formspec_escape(t[6] or "") .. "]"
625                                 .. "label[0.5,4.7;Spread]" -- Spread
626                                 .. "field[2.0,5;2.8,1;te_spreadx;X;"
627                                 .. core.formspec_escape(t[3] or "") .. "]"
628                                 .. "field[4.8,5;2.8,1;te_spready;Y;"
629                                 .. core.formspec_escape(t[4] or "") .. "]"
630                                 .. "field[7.6,5;2.8,1;te_spreadz;Z;"
631                                 .. core.formspec_escape(t[5] or "") .. "]"
632                                 .. "field[0.5,6;3.3,1;te_octaves;Octaves;" -- Octaves
633                                 .. core.formspec_escape(t[7] or "") .. "]"
634                                 .. "field[3.8,6;3.3,1;te_persist;Persistance;" -- Persistance
635                                 .. core.formspec_escape(t[8] or "") .. "]"
636                                 .. "field[7.1,6;3.3,1;te_lacun;Lacunarity;" -- Lacunarity
637                                 .. core.formspec_escape(t[9] or "") .. "]"
638
639                 local enabled_flags = flags_to_table(t[10])
640                 local flags = {}
641                 for _, name in ipairs(enabled_flags) do
642                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
643                         flags[name] = true
644                 end
645                 -- Flags
646                 formspec = formspec
647                                 .. "checkbox[0.5,6.5;cb_defaults;defaults;" -- defaults
648                                 .. tostring(flags["defaults"] == true) .. "]" -- to get false if nil
649                                 .. "checkbox[5,6.5;cb_eased;eased;" -- eased
650                                 .. tostring(flags["eased"] == true) .. "]"
651                                 .. "checkbox[5,7.0;cb_absvalue;absvalue;" -- absvalue
652                                 .. tostring(flags["absvalue"] == true) .. "]"
653
654         elseif setting.type == "v3f" then
655                 local val = get_current_value(setting)
656                 local v3f = {}
657                 for line in val:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
658                         table.insert(v3f, line)
659                 end
660
661                 formspec = formspec
662                                 .. "field[0.5,4;3.3,1;te_x;X;" -- X
663                                 .. core.formspec_escape(v3f[1] or "") .. "]"
664                                 .. "field[3.8,4;3.3,1;te_y;Y;" -- Y
665                                 .. core.formspec_escape(v3f[2] or "") .. "]"
666                                 .. "field[7.1,4;3.3,1;te_z;Z;" -- Z
667                                 .. core.formspec_escape(v3f[3] or "") .. "]"
668
669         elseif setting.type == "flags" then
670                 local enabled_flags = flags_to_table(get_current_value(setting))
671                 local flags = {}
672                 for _, name in ipairs(enabled_flags) do
673                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
674                         flags[name] = true
675                 end
676                 local flags_count = #setting.possible
677                 for i, name in ipairs(setting.possible) do
678                         local x = 0.5
679                         local y = 3.5 + i / 2
680                         if i - 1 >= flags_count / 2 then -- 2nd column
681                                 x = 5
682                                 y = y - flags_count / 4
683                         end
684                         local checkbox_name = "cb_" .. name
685                         local is_enabled = flags[name] == true -- to get false if nil
686                         checkboxes[checkbox_name] = is_enabled
687                         formspec = formspec .. "checkbox["
688                                         .. x .. "," .. y
689                                         .. ";" .. checkbox_name .. ";"
690                                         .. name .. ";" .. tostring(is_enabled) .. "]"
691                 end
692
693         else
694                 -- TODO: fancy input for float, int
695                 local width = 10
696                 local text = get_current_value(setting)
697                 if dialogdata.error_message then
698                         formspec = formspec .. "tablecolumns[color;text]" ..
699                         "tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
700                         "table[5,3.9;5,0.6;error_message;#FF0000,"
701                                         .. core.formspec_escape(dialogdata.error_message) .. ";0]"
702                         width = 5
703                         if dialogdata.entered_text then
704                                 text = dialogdata.entered_text
705                         end
706                 end
707                 formspec = formspec .. "field[0.5,4;" .. width .. ",1;te_setting_value;;"
708                                 .. core.formspec_escape(text) .. "]"
709         end
710         return formspec
711 end
712
713 local function handle_change_setting_buttons(this, fields)
714         local setting = settings[selected_setting]
715         if fields["btn_done"] or fields["key_enter"] then
716                 if setting.type == "bool" then
717                         local new_value = fields["dd_setting_value"]
718                         -- Note: new_value is the actual (translated) value shown in the dropdown
719                         core.settings:set_bool(setting.name, new_value == fgettext("Enabled"))
720
721                 elseif setting.type == "enum" then
722                         local new_value = fields["dd_setting_value"]
723                         core.settings:set(setting.name, new_value)
724
725                 elseif setting.type == "int" then
726                         local new_value = tonumber(fields["te_setting_value"])
727                         if not new_value or math.floor(new_value) ~= new_value then
728                                 this.data.error_message = fgettext_ne("Please enter a valid integer.")
729                                 this.data.entered_text = fields["te_setting_value"]
730                                 core.update_formspec(this:get_formspec())
731                                 return true
732                         end
733                         if setting.min and new_value < setting.min then
734                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
735                                 this.data.entered_text = fields["te_setting_value"]
736                                 core.update_formspec(this:get_formspec())
737                                 return true
738                         end
739                         if setting.max and new_value > setting.max then
740                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
741                                 this.data.entered_text = fields["te_setting_value"]
742                                 core.update_formspec(this:get_formspec())
743                                 return true
744                         end
745                         core.settings:set(setting.name, new_value)
746
747                 elseif setting.type == "float" then
748                         local new_value = tonumber(fields["te_setting_value"])
749                         if not new_value then
750                                 this.data.error_message = fgettext_ne("Please enter a valid number.")
751                                 this.data.entered_text = fields["te_setting_value"]
752                                 core.update_formspec(this:get_formspec())
753                                 return true
754                         end
755                         if setting.min and new_value < setting.min then
756                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
757                                 this.data.entered_text = fields["te_setting_value"]
758                                 core.update_formspec(this:get_formspec())
759                                 return true
760                         end
761                         if setting.max and new_value > setting.max then
762                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
763                                 this.data.entered_text = fields["te_setting_value"]
764                                 core.update_formspec(this:get_formspec())
765                                 return true
766                         end
767                         core.settings:set(setting.name, new_value)
768
769                 elseif setting.type == "flags" then
770                         local values = {}
771                         for _, name in ipairs(setting.possible) do
772                                 if checkboxes["cb_" .. name] then
773                                         table.insert(values, name)
774                                 end
775                         end
776
777                         checkboxes = {}
778
779                         local new_value = table.concat(values, ", ")
780                         core.settings:set(setting.name, new_value)
781
782                 elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
783                         local np_flags = {}
784                         for _, name in ipairs(setting.flags) do
785                                 if checkboxes["cb_" .. name] then
786                                         table.insert(np_flags, name)
787                                 end
788                         end
789
790                         checkboxes = {}
791
792                         local new_value = {
793                                 offset = fields["te_offset"],
794                                 scale = fields["te_scale"],
795                                 spread = {
796                                         x = fields["te_spreadx"],
797                                         y = fields["te_spready"],
798                                         z = fields["te_spreadz"]
799                                 },
800                                 seed = fields["te_seed"],
801                                 octaves = fields["te_octaves"],
802                                 persistence = fields["te_persist"],
803                                 lacunarity = fields["te_lacun"],
804                                 flags = table.concat(np_flags, ", ")
805                         }
806                         core.settings:set_np_group(setting.name, new_value)
807
808                 elseif setting.type == "v3f" then
809                         local new_value = "("
810                                         .. fields["te_x"] .. ", "
811                                         .. fields["te_y"] .. ", "
812                                         .. fields["te_z"] .. ")"
813                         core.settings:set(setting.name, new_value)
814
815                 else
816                         local new_value = fields["te_setting_value"]
817                         core.settings:set(setting.name, new_value)
818                 end
819                 core.settings:write()
820                 this:delete()
821                 return true
822         end
823
824         if fields["btn_cancel"] then
825                 this:delete()
826                 return true
827         end
828
829         if fields["btn_browser_path"] then
830                 core.show_path_select_dialog("dlg_browse_path",
831                         fgettext_ne("Select directory"), false)
832         end
833
834         if fields["btn_browser_filepath"] then
835                 core.show_path_select_dialog("dlg_browse_path",
836                         fgettext_ne("Select file"), true)
837         end
838
839         if fields["dlg_browse_path_accepted"] then
840                 this.data.selected_path = fields["dlg_browse_path_accepted"]
841                 core.update_formspec(this:get_formspec())
842         end
843
844         if setting.type == "flags"
845                         or setting.type == "noise_params_2d"
846                         or setting.type == "noise_params_3d" then
847                 for name, value in pairs(fields) do
848                         if name:sub(1, 3) == "cb_" then
849                                 checkboxes[name] = value == "true"
850                         end
851                 end
852         end
853
854         return false
855 end
856
857 local function create_settings_formspec(tabview, name, tabdata)
858         local formspec = "size[12,6.5;true]" ..
859                         "tablecolumns[color;tree;text,width=32;text]" ..
860                         "tableoptions[background=#00000000;border=false]" ..
861                         "field[0.3,0.1;10.2,1;search_string;;" .. core.formspec_escape(search_string) .. "]" ..
862                         "field_close_on_enter[search_string;false]" ..
863                         "button[10.2,-0.2;2,1;search;" .. fgettext("Search") .. "]" ..
864                         "table[0,0.8;12,4.5;list_settings;"
865
866         local current_level = 0
867         for _, entry in ipairs(settings) do
868                 local name
869                 if not core.settings:get_bool("main_menu_technical_settings") and entry.readable_name then
870                         name = fgettext_ne(entry.readable_name)
871                 else
872                         name = entry.name
873                 end
874
875                 if entry.type == "category" then
876                         current_level = entry.level
877                         formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
878
879                 elseif entry.type == "bool" then
880                         local value = get_current_value(entry)
881                         if core.is_yes(value) then
882                                 value = fgettext("Enabled")
883                         else
884                                 value = fgettext("Disabled")
885                         end
886                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
887                                         .. value .. ","
888
889                 elseif entry.type == "key" then
890                         -- ignore key settings, since we have a special dialog for them
891
892                 elseif entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then
893                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
894                                         .. core.formspec_escape(get_current_np_group_as_string(entry)) .. ","
895
896                 else
897                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
898                                         .. core.formspec_escape(get_current_value(entry)) .. ","
899                 end
900         end
901
902         if #settings > 0 then
903                 formspec = formspec:sub(1, -2) -- remove trailing comma
904         end
905         formspec = formspec .. ";" .. selected_setting .. "]" ..
906                         "button[0,6;4,1;btn_back;".. fgettext("< Back to Settings page") .. "]" ..
907                         "button[10,6;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
908                         "button[7,6;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
909                         "checkbox[0,5.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
910                                         .. dump(core.settings:get_bool("main_menu_technical_settings")) .. "]"
911
912         return formspec
913 end
914
915 local function handle_settings_buttons(this, fields, tabname, tabdata)
916         local list_enter = false
917         if fields["list_settings"] then
918                 selected_setting = core.get_table_index("list_settings")
919                 if core.explode_table_event(fields["list_settings"]).type == "DCL" then
920                         -- Directly toggle booleans
921                         local setting = settings[selected_setting]
922                         if setting and setting.type == "bool" then
923                                 local current_value = get_current_value(setting)
924                                 core.settings:set_bool(setting.name, not core.is_yes(current_value))
925                                 core.settings:write()
926                                 return true
927                         else
928                                 list_enter = true
929                         end
930                 else
931                         return true
932                 end
933         end
934
935         if fields.search or fields.key_enter_field == "search_string" then
936                 if search_string == fields.search_string then
937                         if selected_setting > 0 then
938                                 -- Go to next result on enter press
939                                 local i = selected_setting + 1
940                                 local looped = false
941                                 while i > #settings or settings[i].type == "category" do
942                                         i = i + 1
943                                         if i > #settings then
944                                                 -- Stop infinte looping
945                                                 if looped then
946                                                         return false
947                                                 end
948                                                 i = 1
949                                                 looped = true
950                                         end
951                                 end
952                                 selected_setting = i
953                                 core.update_formspec(this:get_formspec())
954                                 return true
955                         end
956                 else
957                         -- Search for setting
958                         search_string = fields.search_string
959                         settings, selected_setting = filter_settings(full_settings, search_string)
960                         core.update_formspec(this:get_formspec())
961                 end
962                 return true
963         end
964
965         if fields["btn_edit"] or list_enter then
966                 local setting = settings[selected_setting]
967                 if setting and setting.type ~= "category" then
968                         local edit_dialog = dialog_create("change_setting", create_change_setting_formspec,
969                                         handle_change_setting_buttons)
970                         edit_dialog:set_parent(this)
971                         this:hide()
972                         edit_dialog:show()
973                 end
974                 return true
975         end
976
977         if fields["btn_restore"] then
978                 local setting = settings[selected_setting]
979                 if setting and setting.type ~= "category" then
980                         if setting.type == "noise_params_2d"
981                                         or setting.type == "noise_params_3d" then
982                                 core.settings:set_np_group(setting.name, setting.default_table)
983                         else
984                                 core.settings:set(setting.name, setting.default)
985                         end
986                         core.settings:write()
987                         core.update_formspec(this:get_formspec())
988                 end
989                 return true
990         end
991
992         if fields["btn_back"] then
993                 this:delete()
994                 return true
995         end
996
997         if fields["cb_tech_settings"] then
998                 core.settings:set("main_menu_technical_settings", fields["cb_tech_settings"])
999                 core.settings:write()
1000                 core.update_formspec(this:get_formspec())
1001                 return true
1002         end
1003
1004         return false
1005 end
1006
1007 function create_adv_settings_dlg()
1008         local dlg = dialog_create("settings_advanced",
1009                                 create_settings_formspec,
1010                                 handle_settings_buttons,
1011                                 nil)
1012
1013                                 return dlg
1014 end
1015
1016 -- Uncomment to generate minetest.conf.example and settings_translation_file.cpp
1017 -- For RUN_IN_PLACE the generated files may appear in the bin folder
1018
1019 --assert(loadfile(core.get_builtin_path().."mainmenu"..DIR_DELIM.."generate_from_settingtypes.lua"))(parse_config_file(true, false))