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