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