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