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