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