]> git.lizzy.rs Git - dragonfireclient.git/blob - builtin/mainmenu/dlg_settings_advanced.lua
Advanced settings: Fix noise parameter flags (#7819)
[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 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 = pkgmgr.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 = pkgmgr.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         -- Final formspec will be created at the end of this function
536         -- Default values below, may be changed depending on setting type
537         local width = 10
538         local height = 3.5
539         local description_height = 3
540         local formspec = ""
541
542         -- Setting-specific formspec elements
543         if setting.type == "bool" then
544                 local selected_index = 1
545                 if core.is_yes(get_current_value(setting)) then
546                         selected_index = 2
547                 end
548                 formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;"
549                                 .. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";"
550                                 .. selected_index .. "]"
551                 height = height + 1.25
552
553         elseif setting.type == "enum" then
554                 local selected_index = 0
555                 formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;"
556                 for index, value in ipairs(setting.values) do
557                         -- translating value is not possible, since it's the value
558                         --  that we set the setting to
559                         formspec = formspec ..  core.formspec_escape(value) .. ","
560                         if get_current_value(setting) == value then
561                                 selected_index = index
562                         end
563                 end
564                 if #setting.values > 0 then
565                         formspec = formspec:sub(1, -2) -- remove trailing comma
566                 end
567                 formspec = formspec .. ";" .. selected_index .. "]"
568                 height = height + 1.25
569
570         elseif setting.type == "path" or setting.type == "filepath" then
571                 local current_value = dialogdata.selected_path
572                 if not current_value then
573                         current_value = get_current_value(setting)
574                 end
575                 formspec = "field[0.28," .. height + 0.15 .. ";8,1;te_setting_value;;"
576                                 .. core.formspec_escape(current_value) .. "]"
577                                 .. "button[8," .. height - 0.15 .. ";2,1;btn_browser_"
578                                 .. setting.type .. ";" .. fgettext("Browse") .. "]"
579                 height = height + 1.15
580
581         elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
582                 local t = get_current_np_group(setting)
583                 local dimension = 3
584                 if setting.type == "noise_params_2d" then
585                         dimension = 2
586                 end
587
588                 -- More space for 3x3 fields
589                 description_height = description_height - 1.5
590                 height = height - 1.5
591
592                 local fields = {}
593                 local function add_field(x, name, label, value)
594                         fields[#fields + 1] = ("field[%f,%f;3.3,1;%s;%s;%s]"):format(
595                                 x, height, name, label, core.formspec_escape(value or "")
596                         )
597                 end
598                 -- First row
599                 height = height + 0.3
600                 add_field(0.3, "te_offset", "Offset", t[1])
601                 add_field(3.6, "te_scale",  "Scale",  t[2])
602                 add_field(6.9, "te_seed",   "Seed",   t[6])
603                 height = height + 1.1
604
605                 -- Second row
606                 add_field(0.3, "te_spreadx", "X spread", t[3])
607                 if dimension == 3 then
608                         add_field(3.6, "te_spready", "Y spread", t[4])
609                 else
610                         fields[#fields + 1] = "label[4," .. height - 0.2 .. ";2D Noise]"
611                 end
612                 add_field(6.9, "te_spreadz", "Z spread", t[5])
613                 height = height + 1.1
614
615                 -- Third row
616                 add_field(0.3, "te_octaves", "Octaves",     t[7])
617                 add_field(3.6, "te_persist", "Persistance", t[8])
618                 add_field(6.9, "te_lacun",   "Lacunarity",  t[9])
619                 height = height + 1.1
620
621
622                 local enabled_flags = flags_to_table(t[10])
623                 local flags = {}
624                 for _, name in ipairs(enabled_flags) do
625                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
626                         flags[name] = true
627                 end
628                 for _, name in ipairs(setting.flags) do
629                         local checkbox_name = "cb_" .. name
630                         local is_enabled = flags[name] == true -- to get false if nil
631                         checkboxes[checkbox_name] = is_enabled
632                 end
633                 -- Flags
634                 formspec = table.concat(fields)
635                                 .. "checkbox[0.5," .. height - 0.6 .. ";cb_defaults;defaults;" -- defaults
636                                 .. tostring(flags["defaults"] == true) .. "]" -- to get false if nil
637                                 .. "checkbox[5," .. height - 0.6 .. ";cb_eased;eased;" -- eased
638                                 .. tostring(flags["eased"] == true) .. "]"
639                                 .. "checkbox[5," .. height - 0.15 .. ";cb_absvalue;absvalue;" -- absvalue
640                                 .. tostring(flags["absvalue"] == true) .. "]"
641                 height = height + 1
642
643         elseif setting.type == "v3f" then
644                 local val = get_current_value(setting)
645                 local v3f = {}
646                 for line in val:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
647                         table.insert(v3f, line)
648                 end
649
650                 height = height + 0.3
651                 formspec = formspec
652                                 .. "field[0.3," .. height .. ";3.3,1;te_x;X;" -- X
653                                 .. core.formspec_escape(v3f[1] or "") .. "]"
654                                 .. "field[3.6," .. height .. ";3.3,1;te_y;Y;" -- Y
655                                 .. core.formspec_escape(v3f[2] or "") .. "]"
656                                 .. "field[6.9," .. height .. ";3.3,1;te_z;Z;" -- Z
657                                 .. core.formspec_escape(v3f[3] or "") .. "]"
658                 height = height + 1.1
659
660         elseif setting.type == "flags" then
661                 local enabled_flags = flags_to_table(get_current_value(setting))
662                 local flags = {}
663                 for _, name in ipairs(enabled_flags) do
664                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
665                         flags[name] = true
666                 end
667                 local flags_count = #setting.possible
668                 local max_height = flags_count / 4
669
670                 -- More space for flags
671                 description_height = description_height - 1
672                 height = height - 1
673
674                 local fields = {} -- To build formspec
675                 for i, name in ipairs(setting.possible) do
676                         local x = 0.5
677                         local y = height + i / 2 - 0.75
678                         if i - 1 >= flags_count / 2 then -- 2nd column
679                                 x = 5
680                                 y = y - max_height
681                         end
682                         local checkbox_name = "cb_" .. name
683                         local is_enabled = flags[name] == true -- to get false if nil
684                         checkboxes[checkbox_name] = is_enabled
685
686                         fields[#fields + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format(
687                                 x, y, checkbox_name, name, tostring(is_enabled)
688                         )
689                 end
690                 formspec = table.concat(fields)
691                 height = height + max_height + 0.25
692
693         else
694                 -- TODO: fancy input for float, int
695                 local text = get_current_value(setting)
696                 if dialogdata.error_message and dialogdata.entered_text then
697                         text = dialogdata.entered_text
698                 end
699                 formspec = "field[0.28," .. height + 0.15 .. ";" .. width .. ",1;te_setting_value;;"
700                                 .. core.formspec_escape(text) .. "]"
701                 height = height + 1.15
702         end
703
704         -- Box good, textarea bad. Calculate textarea size from box.
705         local function create_textfield(size, label, text, bg_color)
706                 local textarea = {
707                         x = size.x + 0.3,
708                         y = size.y,
709                         w = size.w + 0.25,
710                         h = size.h * 1.16 + 0.12
711                 }
712                 return ("box[%f,%f;%f,%f;%s]textarea[%f,%f;%f,%f;;%s;%s]"):format(
713                         size.x, size.y, size.w, size.h, bg_color or "#000",
714                         textarea.x, textarea.y, textarea.w, textarea.h,
715                         core.formspec_escape(label), core.formspec_escape(text)
716                 )
717
718         end
719
720         -- When there's an error: Shrink description textarea and add error below
721         if dialogdata.error_message then
722                 local error_box = {
723                         x = 0,
724                         y = description_height - 0.4,
725                         w = width - 0.25,
726                         h = 0.5
727                 }
728                 formspec = formspec ..
729                         create_textfield(error_box, "", dialogdata.error_message, "#600")
730                 description_height = description_height - 0.75
731         end
732
733         -- Get description field
734         local description_box = {
735                 x = 0,
736                 y = 0.2,
737                 w = width - 0.25,
738                 h = description_height
739         }
740
741         local setting_name = setting.name
742         if setting.readable_name then
743                 setting_name = fgettext_ne(setting.readable_name) ..
744                         " (" .. setting.name .. ")"
745         end
746
747         local comment_text = ""
748         if setting.comment == "" then
749                 comment_text = fgettext_ne("(No description of setting given)")
750         else
751                 comment_text = fgettext_ne(setting.comment)
752         end
753
754         return (
755                 "size[" .. width .. "," .. height + 0.25 .. ",true]" ..
756                 create_textfield(description_box, setting_name, comment_text) ..
757                 formspec ..
758                 "button[" .. width / 2 - 2.5 .. "," .. height - 0.4 .. ";2.5,1;btn_done;" ..
759                         fgettext("Save") .. "]" ..
760                 "button[" .. width / 2 .. "," .. height - 0.4 .. ";2.5,1;btn_cancel;" ..
761                         fgettext("Cancel") .. "]"
762         )
763 end
764
765 local function handle_change_setting_buttons(this, fields)
766         local setting = settings[selected_setting]
767         if fields["btn_done"] or fields["key_enter"] then
768                 if setting.type == "bool" then
769                         local new_value = fields["dd_setting_value"]
770                         -- Note: new_value is the actual (translated) value shown in the dropdown
771                         core.settings:set_bool(setting.name, new_value == fgettext("Enabled"))
772
773                 elseif setting.type == "enum" then
774                         local new_value = fields["dd_setting_value"]
775                         core.settings:set(setting.name, new_value)
776
777                 elseif setting.type == "int" then
778                         local new_value = tonumber(fields["te_setting_value"])
779                         if not new_value or math.floor(new_value) ~= new_value then
780                                 this.data.error_message = fgettext_ne("Please enter a valid integer.")
781                                 this.data.entered_text = fields["te_setting_value"]
782                                 core.update_formspec(this:get_formspec())
783                                 return true
784                         end
785                         if setting.min and new_value < setting.min then
786                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
787                                 this.data.entered_text = fields["te_setting_value"]
788                                 core.update_formspec(this:get_formspec())
789                                 return true
790                         end
791                         if setting.max and new_value > setting.max then
792                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
793                                 this.data.entered_text = fields["te_setting_value"]
794                                 core.update_formspec(this:get_formspec())
795                                 return true
796                         end
797                         core.settings:set(setting.name, new_value)
798
799                 elseif setting.type == "float" then
800                         local new_value = tonumber(fields["te_setting_value"])
801                         if not new_value then
802                                 this.data.error_message = fgettext_ne("Please enter a valid number.")
803                                 this.data.entered_text = fields["te_setting_value"]
804                                 core.update_formspec(this:get_formspec())
805                                 return true
806                         end
807                         if setting.min and new_value < setting.min then
808                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
809                                 this.data.entered_text = fields["te_setting_value"]
810                                 core.update_formspec(this:get_formspec())
811                                 return true
812                         end
813                         if setting.max and new_value > setting.max then
814                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
815                                 this.data.entered_text = fields["te_setting_value"]
816                                 core.update_formspec(this:get_formspec())
817                                 return true
818                         end
819                         core.settings:set(setting.name, new_value)
820
821                 elseif setting.type == "flags" then
822                         local values = {}
823                         for _, name in ipairs(setting.possible) do
824                                 if checkboxes["cb_" .. name] then
825                                         table.insert(values, name)
826                                 end
827                         end
828
829                         checkboxes = {}
830
831                         local new_value = table.concat(values, ", ")
832                         core.settings:set(setting.name, new_value)
833
834                 elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
835                         local np_flags = {}
836                         for _, name in ipairs(setting.flags) do
837                                 if checkboxes["cb_" .. name] then
838                                         table.insert(np_flags, name)
839                                 end
840                         end
841
842                         checkboxes = {}
843
844                         if setting.type == "noise_params_2d" then
845                                  fields["te_spready"] = fields["te_spreadz"]
846                         end
847                         local new_value = {
848                                 offset = fields["te_offset"],
849                                 scale = fields["te_scale"],
850                                 spread = {
851                                         x = fields["te_spreadx"],
852                                         y = fields["te_spready"],
853                                         z = fields["te_spreadz"]
854                                 },
855                                 seed = fields["te_seed"],
856                                 octaves = fields["te_octaves"],
857                                 persistence = fields["te_persist"],
858                                 lacunarity = fields["te_lacun"],
859                                 flags = table.concat(np_flags, ", ")
860                         }
861                         core.settings:set_np_group(setting.name, new_value)
862
863                 elseif setting.type == "v3f" then
864                         local new_value = "("
865                                         .. fields["te_x"] .. ", "
866                                         .. fields["te_y"] .. ", "
867                                         .. fields["te_z"] .. ")"
868                         core.settings:set(setting.name, new_value)
869
870                 else
871                         local new_value = fields["te_setting_value"]
872                         core.settings:set(setting.name, new_value)
873                 end
874                 core.settings:write()
875                 this:delete()
876                 return true
877         end
878
879         if fields["btn_cancel"] then
880                 this:delete()
881                 return true
882         end
883
884         if fields["btn_browser_path"] then
885                 core.show_path_select_dialog("dlg_browse_path",
886                         fgettext_ne("Select directory"), false)
887         end
888
889         if fields["btn_browser_filepath"] then
890                 core.show_path_select_dialog("dlg_browse_path",
891                         fgettext_ne("Select file"), true)
892         end
893
894         if fields["dlg_browse_path_accepted"] then
895                 this.data.selected_path = fields["dlg_browse_path_accepted"]
896                 core.update_formspec(this:get_formspec())
897         end
898
899         if setting.type == "flags"
900                         or setting.type == "noise_params_2d"
901                         or setting.type == "noise_params_3d" then
902                 for name, value in pairs(fields) do
903                         if name:sub(1, 3) == "cb_" then
904                                 checkboxes[name] = value == "true"
905                         end
906                 end
907         end
908
909         return false
910 end
911
912 local function create_settings_formspec(tabview, name, tabdata)
913         local formspec = "size[12,5.4;true]" ..
914                         "tablecolumns[color;tree;text,width=28;text]" ..
915                         "tableoptions[background=#00000000;border=false]" ..
916                         "field[0.3,0.1;10.2,1;search_string;;" .. core.formspec_escape(search_string) .. "]" ..
917                         "field_close_on_enter[search_string;false]" ..
918                         "button[10.2,-0.2;2,1;search;" .. fgettext("Search") .. "]" ..
919                         "table[0,0.8;12,3.5;list_settings;"
920
921         local current_level = 0
922         for _, entry in ipairs(settings) do
923                 local name
924                 if not core.settings:get_bool("main_menu_technical_settings") and entry.readable_name then
925                         name = fgettext_ne(entry.readable_name)
926                 else
927                         name = entry.name
928                 end
929
930                 if entry.type == "category" then
931                         current_level = entry.level
932                         formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
933
934                 elseif entry.type == "bool" then
935                         local value = get_current_value(entry)
936                         if core.is_yes(value) then
937                                 value = fgettext("Enabled")
938                         else
939                                 value = fgettext("Disabled")
940                         end
941                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
942                                         .. value .. ","
943
944                 elseif entry.type == "key" then
945                         -- ignore key settings, since we have a special dialog for them
946
947                 elseif entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then
948                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
949                                         .. core.formspec_escape(get_current_np_group_as_string(entry)) .. ","
950
951                 else
952                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
953                                         .. core.formspec_escape(get_current_value(entry)) .. ","
954                 end
955         end
956
957         if #settings > 0 then
958                 formspec = formspec:sub(1, -2) -- remove trailing comma
959         end
960         formspec = formspec .. ";" .. selected_setting .. "]" ..
961                         "button[0,4.9;4,1;btn_back;".. fgettext("< Back to Settings page") .. "]" ..
962                         "button[10,4.9;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
963                         "button[7,4.9;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
964                         "checkbox[0,4.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
965                                         .. dump(core.settings:get_bool("main_menu_technical_settings")) .. "]"
966
967         return formspec
968 end
969
970 local function handle_settings_buttons(this, fields, tabname, tabdata)
971         local list_enter = false
972         if fields["list_settings"] then
973                 selected_setting = core.get_table_index("list_settings")
974                 if core.explode_table_event(fields["list_settings"]).type == "DCL" then
975                         -- Directly toggle booleans
976                         local setting = settings[selected_setting]
977                         if setting and setting.type == "bool" then
978                                 local current_value = get_current_value(setting)
979                                 core.settings:set_bool(setting.name, not core.is_yes(current_value))
980                                 core.settings:write()
981                                 return true
982                         else
983                                 list_enter = true
984                         end
985                 else
986                         return true
987                 end
988         end
989
990         if fields.search or fields.key_enter_field == "search_string" then
991                 if search_string == fields.search_string then
992                         if selected_setting > 0 then
993                                 -- Go to next result on enter press
994                                 local i = selected_setting + 1
995                                 local looped = false
996                                 while i > #settings or settings[i].type == "category" do
997                                         i = i + 1
998                                         if i > #settings then
999                                                 -- Stop infinte looping
1000                                                 if looped then
1001                                                         return false
1002                                                 end
1003                                                 i = 1
1004                                                 looped = true
1005                                         end
1006                                 end
1007                                 selected_setting = i
1008                                 core.update_formspec(this:get_formspec())
1009                                 return true
1010                         end
1011                 else
1012                         -- Search for setting
1013                         search_string = fields.search_string
1014                         settings, selected_setting = filter_settings(full_settings, search_string)
1015                         core.update_formspec(this:get_formspec())
1016                 end
1017                 return true
1018         end
1019
1020         if fields["btn_edit"] or list_enter then
1021                 local setting = settings[selected_setting]
1022                 if setting and setting.type ~= "category" then
1023                         local edit_dialog = dialog_create("change_setting", create_change_setting_formspec,
1024                                         handle_change_setting_buttons)
1025                         edit_dialog:set_parent(this)
1026                         this:hide()
1027                         edit_dialog:show()
1028                 end
1029                 return true
1030         end
1031
1032         if fields["btn_restore"] then
1033                 local setting = settings[selected_setting]
1034                 if setting and setting.type ~= "category" then
1035                         core.settings:remove(setting.name)
1036                         core.settings:write()
1037                         core.update_formspec(this:get_formspec())
1038                 end
1039                 return true
1040         end
1041
1042         if fields["btn_back"] then
1043                 this:delete()
1044                 return true
1045         end
1046
1047         if fields["cb_tech_settings"] then
1048                 core.settings:set("main_menu_technical_settings", fields["cb_tech_settings"])
1049                 core.settings:write()
1050                 core.update_formspec(this:get_formspec())
1051                 return true
1052         end
1053
1054         return false
1055 end
1056
1057 function create_adv_settings_dlg()
1058         local dlg = dialog_create("settings_advanced",
1059                                 create_settings_formspec,
1060                                 handle_settings_buttons,
1061                                 nil)
1062
1063                                 return dlg
1064 end
1065
1066 -- Uncomment to generate minetest.conf.example and settings_translation_file.cpp
1067 -- For RUN_IN_PLACE the generated files may appear in the bin folder
1068
1069 --assert(loadfile(core.get_builtin_path().."mainmenu"..DIR_DELIM.."generate_from_settingtypes.lua"))(parse_config_file(true, false))