Skip to content

Commit 6f2d9de

Browse files
committedOct 24, 2015
Improve Lua settings menu
* Add key settings to setting table and ignore them later This way they are added to the auto-generated minetest.conf.example * Add flags type * Add input validation for int, float and flags * Break in-game graphic settings into multiple sections * Parse settingtpes.txt in mods and games * Improve description for a lot of settings * Fix typos and wording in settingtypes.txt * Convert language setting to an enum
1 parent 2d207af commit 6f2d9de

File tree

5 files changed

+1359
-754
lines changed

5 files changed

+1359
-754
lines changed
 

‎builtin/mainmenu/tab_settings.lua

+271-79
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,36 @@
1717

1818
local FILENAME = "settingtypes.txt"
1919

20-
local function parse_setting_line(settings, line)
20+
local CHAR_CLASSES = {
21+
SPACE = "[%s]",
22+
VARIABLE = "[%w_%-%.]",
23+
INTEGER = "[-]?[%d]",
24+
FLOAT = "[-]?[%d%.]",
25+
FLAGS = "[%w_%-%.,]",
26+
}
27+
28+
-- returns error message, or nil
29+
local function parse_setting_line(settings, line, read_all, base_level)
2130
-- empty lines
22-
if line:match("^[%s]*$") then
31+
if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then
2332
-- clear current_comment so only comments directly above a setting are bound to it
2433
settings.current_comment = ""
2534
return
2635
end
2736

2837
-- category
29-
local category = line:match("^%[([^%]]+)%]$")
38+
local stars, category = line:match("^%[([%*]*)([^%]]+)%]$")
3039
if category then
31-
local level = 0
32-
local index = 1
33-
while category:sub(index, index) == "*" do
34-
level = level + 1
35-
index = index + 1
36-
end
37-
category = category:sub(index, -1)
3840
table.insert(settings, {
3941
name = category,
40-
level = level,
42+
level = stars:len() + base_level,
4143
type = "category",
4244
})
4345
return
4446
end
4547

4648
-- comment
47-
local comment = line:match("^#[%s]*(.*)$")
49+
local comment = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$")
4850
if comment then
4951
if settings.current_comment == "" then
5052
settings.current_comment = comment
@@ -54,9 +56,20 @@ local function parse_setting_line(settings, line)
5456
return
5557
end
5658

59+
local error_msg
60+
5761
-- settings
58-
local first_part, name, readable_name, setting_type =
59-
line:match("^(([%w%._-]+)[%s]+%(([^%)]*)%)[%s]+([%w_]+)[%s]*)")
62+
local first_part, name, readable_name, setting_type = line:match("^"
63+
-- this first capture group matches the whole first part,
64+
-- so we can later strip it from the rest of the line
65+
.. "("
66+
.. "([" .. CHAR_CLASSES.VARIABLE .. "+)" -- variable name
67+
.. CHAR_CLASSES.SPACE
68+
.. "%(([^%)]*)%)" -- readable name
69+
.. CHAR_CLASSES.SPACE
70+
.. "(" .. CHAR_CLASSES.VARIABLE .. "+)" -- type
71+
.. CHAR_CLASSES.SPACE .. "?"
72+
.. ")")
6073

6174
if first_part then
6275
if readable_name == "" then
@@ -65,14 +78,15 @@ local function parse_setting_line(settings, line)
6578
local remaining_line = line:sub(first_part:len() + 1)
6679

6780
if setting_type == "int" then
68-
local default, min, max = remaining_line:match("^([%d]+)[%s]*([%d]*)[%s]*([%d]*)$")
81+
local default, min, max = remaining_line:match("^"
82+
-- first int is required, the last 2 are optional
83+
.. "(" .. CHAR_CLASSES.INTEGER .. "+)" .. CHAR_CLASSES.SPACE .. "?"
84+
.. "(" .. CHAR_CLASSES.INTEGER .. "*)" .. CHAR_CLASSES.SPACE .. "?"
85+
.. "(" .. CHAR_CLASSES.INTEGER .. "*)"
86+
.. "$")
6987
if default and tonumber(default) then
70-
if min == "" then
71-
min = nil
72-
end
73-
if max == "" then
74-
max = nil
75-
end
88+
min = tonumber(min)
89+
max = tonumber(max)
7690
table.insert(settings, {
7791
name = name,
7892
readable_name = readable_name,
@@ -83,21 +97,24 @@ local function parse_setting_line(settings, line)
8397
comment = settings.current_comment,
8498
})
8599
else
86-
core.log("error", "Found invalid int in " .. FILENAME .. ": " .. line)
100+
error_msg = "Invalid integer setting"
87101
end
88102

89-
elseif setting_type == "string" or setting_type == "flags" or setting_type == "noise_params" then
90-
local default = remaining_line:match("^[%s]*(.*)$")
103+
elseif setting_type == "string" or setting_type == "noise_params"
104+
or setting_type == "key" then
105+
local default = remaining_line:match("^(.*)$")
91106
if default then
92-
table.insert(settings, {
93-
name = name,
94-
readable_name = readable_name,
95-
type = setting_type,
96-
default = default,
97-
comment = settings.current_comment,
98-
})
107+
if setting_type ~= "key" or read_all then -- ignore key type if read_all is false
108+
table.insert(settings, {
109+
name = name,
110+
readable_name = readable_name,
111+
type = setting_type,
112+
default = default,
113+
comment = settings.current_comment,
114+
})
115+
end
99116
else
100-
core.log("error", "Found invalid string in " .. FILENAME .. ": " .. line)
117+
error_msg = "Invalid string setting"
101118
end
102119

103120
elseif setting_type == "bool" then
@@ -110,19 +127,19 @@ local function parse_setting_line(settings, line)
110127
comment = settings.current_comment,
111128
})
112129
else
113-
core.log("error", "Found invalid bool in " .. FILENAME .. ": " .. line)
130+
error_msg = "Invalid boolean setting"
114131
end
115132

116133
elseif setting_type == "float" then
117-
local default, min, max
118-
= remaining_line:match("^([%d%.]+)[%s]*([%d%.]*)[%s]*([%d%.]*)$")
134+
local default, min, max = remaining_line:match("^"
135+
-- first float is required, the last 2 are optional
136+
.. "(" .. CHAR_CLASSES.FLOAT .. "+)" .. CHAR_CLASSES.SPACE .. "?"
137+
.. "(" .. CHAR_CLASSES.FLOAT .. "*)" .. CHAR_CLASSES.SPACE .. "?"
138+
.. "(" .. CHAR_CLASSES.FLOAT .. "*)"
139+
.."$")
119140
if default and tonumber(default) then
120-
if min == "" then
121-
min = nil
122-
end
123-
if max == "" then
124-
max = nil
125-
end
141+
min = tonumber(min)
142+
max = tonumber(max)
126143
table.insert(settings, {
127144
name = name,
128145
readable_name = readable_name,
@@ -133,26 +150,26 @@ local function parse_setting_line(settings, line)
133150
comment = settings.current_comment,
134151
})
135152
else
136-
core.log("error", "Found invalid float in " .. FILENAME .. ": " .. line)
153+
error_msg = "Invalid float setting"
137154
end
138155

139156
elseif setting_type == "enum" then
140-
local default, values = remaining_line:match("^([^%s]+)[%s]+(.+)$")
157+
local default, values = remaining_line:match("^(.+)" .. CHAR_CLASSES.SPACE .. "(.+)$")
141158
if default and values ~= "" then
142159
table.insert(settings, {
143160
name = name,
144161
readable_name = readable_name,
145162
type = "enum",
146163
default = default,
147-
values = values:split(","),
164+
values = values:split(",", true),
148165
comment = settings.current_comment,
149166
})
150167
else
151-
core.log("error", "Found invalid enum in " .. FILENAME .. ": " .. line)
168+
error_msg = "Invalid enum setting"
152169
end
153170

154171
elseif setting_type == "path" then
155-
local default = remaining_line:match("^[%s]*(.*)$")
172+
local default = remaining_line:match("^(.*)$")
156173
if default then
157174
table.insert(settings, {
158175
name = name,
@@ -162,50 +179,143 @@ local function parse_setting_line(settings, line)
162179
comment = settings.current_comment,
163180
})
164181
else
165-
core.log("error", "Found invalid path in " .. FILENAME .. ": " .. line)
182+
error_msg = "Invalid path setting"
166183
end
167184

168-
elseif setting_type == "key" then
169-
--ignore keys, since we have a special dialog for them
185+
elseif setting_type == "flags" then
186+
local default, possible = remaining_line:match("^"
187+
.. "(" .. CHAR_CLASSES.FLAGS .. "+)" .. CHAR_CLASSES.SPACE .. ""
188+
.. "(" .. CHAR_CLASSES.FLAGS .. "+)"
189+
.. "$")
190+
if default and possible then
191+
table.insert(settings, {
192+
name = name,
193+
readable_name = readable_name,
194+
type = "flags",
195+
default = default,
196+
possible = possible,
197+
comment = settings.current_comment,
198+
})
199+
else
200+
error_msg = "Invalid flags setting"
201+
end
170202

171203
-- TODO: flags, noise_params (, struct)
172204

173205
else
174-
core.log("error", "Found setting with invalid setting type in " .. FILENAME .. ": " .. line)
206+
error_msg = "Invalid setting type \"" .. type .. "\""
175207
end
176208
else
177-
core.log("error", "Found invalid line in " .. FILENAME .. ": " .. line)
209+
error_msg = "Invalid line"
178210
end
179211
-- clear current_comment since we just used it
180212
-- if we not just used it, then clear it since we only want comments
181213
-- directly above the setting to be bound to it
182214
settings.current_comment = ""
183-
end
184215

185-
local function parse_config_file()
186-
local file = io.open(core.get_builtin_path() .. DIR_DELIM .. FILENAME, "r")
187-
local settings = {}
188-
if not file then
189-
core.log("error", "Can't load " .. FILENAME)
190-
return settings
191-
end
216+
return error_msg
217+
end
192218

219+
local function parse_single_file(file, filepath, read_all, result, base_level)
193220
-- store this helper variable in the table so it's easier to pass to parse_setting_line()
194-
settings.current_comment = ""
221+
result.current_comment = ""
195222

196223
local line = file:read("*line")
197224
while line do
198-
parse_setting_line(settings, line)
225+
local error_msg = parse_setting_line(result, line, read_all, base_level)
226+
if error_msg then
227+
core.log("error", error_msg .. " in " .. filepath .. " \"" .. line .. "\"")
228+
end
199229
line = file:read("*line")
200230
end
201231

202-
settings.current_comment = nil
232+
result.current_comment = nil
233+
end
234+
235+
-- read_all: whether to ignore certain setting types for GUI or not
236+
-- parse_mods: whether to parse settingtypes.txt in mods and games
237+
local function parse_config_file(read_all, parse_mods)
238+
local builtin_path = core.get_builtin_path() .. DIR_DELIM .. FILENAME
239+
local file = io.open(builtin_path, "r")
240+
local settings = {}
241+
if not file then
242+
core.log("error", "Can't load " .. FILENAME)
243+
return settings
244+
end
245+
246+
parse_single_file(file, builtin_path, read_all, settings, 0)
203247

204248
file:close()
249+
250+
if parse_mods then
251+
-- Parse games
252+
local games_category_initialized = false
253+
local index = 1
254+
local game = gamemgr.get_game(index)
255+
while game do
256+
local path = game.path .. DIR_DELIM .. FILENAME
257+
local file = io.open(path, "r")
258+
if file then
259+
if not games_category_initialized then
260+
local translation = fgettext_ne("Games"), -- not used, but needed for xgettext
261+
table.insert(settings, {
262+
name = "Games",
263+
level = 0,
264+
type = "category",
265+
})
266+
games_category_initialized = true
267+
end
268+
269+
table.insert(settings, {
270+
name = game.name,
271+
level = 1,
272+
type = "category",
273+
})
274+
275+
parse_single_file(file, path, read_all, settings, 2)
276+
277+
file:close()
278+
end
279+
280+
index = index + 1
281+
game = gamemgr.get_game(index)
282+
end
283+
284+
-- Parse mods
285+
local mods_category_initialized = false
286+
local mods = {}
287+
get_mods(core.get_modpath(), mods)
288+
for _, mod in ipairs(mods) do
289+
local path = mod.path .. DIR_DELIM .. FILENAME
290+
local file = io.open(path, "r")
291+
if file then
292+
if not mods_category_initialized then
293+
local translation = fgettext_ne("Mods"), -- not used, but needed for xgettext
294+
table.insert(settings, {
295+
name = "Mods",
296+
level = 0,
297+
type = "category",
298+
})
299+
mods_category_initialized = true
300+
end
301+
302+
table.insert(settings, {
303+
name = mod.name,
304+
level = 1,
305+
type = "category",
306+
})
307+
308+
parse_single_file(file, path, read_all, settings, 2)
309+
310+
file:close()
311+
end
312+
end
313+
end
314+
205315
return settings
206316
end
207317

208-
local settings = parse_config_file()
318+
local settings = parse_config_file(false, true)
209319
local selected_setting = 1
210320

211321
local function get_current_value(setting)
@@ -236,16 +346,28 @@ local function create_change_setting_formspec(dialogdata)
236346

237347
local comment_text = ""
238348

239-
-- fgettext_ne("") doesn't have to return "", according to specification of gettext(3)
240349
if setting.comment == "" then
241350
comment_text = fgettext_ne("(No description of setting given)")
242351
else
243352
comment_text = fgettext_ne(setting.comment)
244353
end
245-
for _, comment_line in ipairs(comment_text:split("\n")) do
354+
for _, comment_line in ipairs(comment_text:split("\n", true)) do
246355
formspec = formspec .. "," .. core.formspec_escape(comment_line) .. ","
247356
end
248357

358+
if setting.type == "flags" then
359+
formspec = formspec .. ",,"
360+
.. "," .. fgettext("Please enter a comma seperated list of flags.") .. ","
361+
.. "," .. fgettext("Possible values are: ")
362+
.. core.formspec_escape(setting.possible:gsub(",", ", ")) .. ","
363+
elseif setting.type == "noise_params" then
364+
formspec = formspec .. ",,"
365+
.. "," .. fgettext("Format: <offset>, <scale>, (<spreadX>, <spreadY>, <spreadZ>), <seed>, <octaves>, <persistence>") .. ","
366+
.. "," .. fgettext("Optionally the lacunarity can be appended with a leading comma.") .. ","
367+
end
368+
369+
formspec = formspec:sub(1, -2) -- remove trailing comma
370+
249371
formspec = formspec .. ";1]"
250372

251373
if setting.type == "bool" then
@@ -256,7 +378,8 @@ local function create_change_setting_formspec(dialogdata)
256378
selected_index = 1
257379
end
258380
formspec = formspec .. "dropdown[0.5,3.5;3,1;dd_setting_value;"
259-
.. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";" .. selected_index .. "]"
381+
.. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";"
382+
.. selected_index .. "]"
260383

261384
elseif setting.type == "enum" then
262385
local selected_index = 0
@@ -285,8 +408,20 @@ local function create_change_setting_formspec(dialogdata)
285408

286409
else
287410
-- TODO: fancy input for float, int, flags, noise_params
288-
formspec = formspec .. "field[0.5,4;9.5,1;te_setting_value;;"
289-
.. core.formspec_escape(get_current_value(setting)) .. "]"
411+
local width = 10
412+
local text = get_current_value(setting)
413+
if dialogdata.error_message then
414+
formspec = formspec .. "tablecolumns[color;text]" ..
415+
"tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
416+
"table[5,4;5,1;error_message;#FF0000,"
417+
.. core.formspec_escape(dialogdata.error_message) .. ";0]"
418+
width = 5
419+
if dialogdata.entered_text then
420+
text = dialogdata.entered_text
421+
end
422+
end
423+
formspec = formspec .. "field[0.5,4;" .. width .. ",1;te_setting_value;;"
424+
.. core.formspec_escape(text) .. "]"
290425
end
291426
return formspec
292427
end
@@ -303,6 +438,52 @@ local function handle_change_setting_buttons(this, fields)
303438
local new_value = fields["dd_setting_value"]
304439
core.setting_set(setting.name, new_value)
305440

441+
elseif setting.type == "int" then
442+
local new_value = tonumber(fields["te_setting_value"])
443+
if not new_value or math.floor(new_value) ~= new_value then
444+
this.data.error_message = fgettext_ne("Please enter a valid integer.")
445+
this.data.entered_text = fields["te_setting_value"]
446+
core.update_formspec(this:get_formspec())
447+
return true
448+
end
449+
if setting.min and new_value < setting.min then
450+
this.data.error_message = fgettext_ne("The value must be greater than $1.", setting.min)
451+
this.data.entered_text = fields["te_setting_value"]
452+
core.update_formspec(this:get_formspec())
453+
return true
454+
end
455+
if setting.max and new_value > setting.max then
456+
this.data.error_message = fgettext_ne("The value must be lower than $1.", setting.max)
457+
this.data.entered_text = fields["te_setting_value"]
458+
core.update_formspec(this:get_formspec())
459+
return true
460+
end
461+
core.setting_set(setting.name, new_value)
462+
463+
elseif setting.type == "float" then
464+
local new_value = tonumber(fields["te_setting_value"])
465+
if not new_value then
466+
this.data.error_message = fgettext_ne("Please enter a valid number.")
467+
this.data.entered_text = fields["te_setting_value"]
468+
core.update_formspec(this:get_formspec())
469+
return true
470+
end
471+
core.setting_set(setting.name, new_value)
472+
473+
elseif setting.type == "flags" then
474+
local new_value = fields["te_setting_value"]
475+
for _,value in ipairs(new_value:split(",", true)) do
476+
value = value:trim()
477+
if not value:match(CHAR_CLASSES.FLAGS .. "+")
478+
or not setting.possible:match("[,]?" .. value .. "[,]?") then
479+
this.data.error_message = fgettext_ne("\"" .. value .. "\" is not a valid flag.")
480+
this.data.entered_text = fields["te_setting_value"]
481+
core.update_formspec(this:get_formspec())
482+
return true
483+
end
484+
end
485+
core.setting_set(setting.name, new_value)
486+
306487
else
307488
local new_value = fields["te_setting_value"]
308489
core.setting_set(setting.name, new_value)
@@ -345,7 +526,8 @@ local function create_settings_formspec(tabview, name, tabdata)
345526

346527
if entry.type == "category" then
347528
current_level = entry.level
348-
formspec = formspec .. "#FFFF00," .. current_level .. "," .. core.formspec_escape(name) .. ",,"
529+
formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
530+
349531
elseif entry.type == "bool" then
350532
local value = get_current_value(entry)
351533
if core.is_yes(value) then
@@ -355,6 +537,10 @@ local function create_settings_formspec(tabview, name, tabdata)
355537
end
356538
formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
357539
.. value .. ","
540+
541+
elseif entry.type == "key" then
542+
-- ignore key settings, since we have a special dialog for them
543+
358544
else
359545
formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
360546
.. core.formspec_escape(get_current_value(entry)) .. ","
@@ -431,6 +617,13 @@ local function handle_settings_buttons(this, fields, tabname, tabdata)
431617
return false
432618
end
433619

620+
tab_settings = {
621+
name = "settings",
622+
caption = fgettext("Settings"),
623+
cbf_formspec = create_settings_formspec,
624+
cbf_button_handler = handle_settings_buttons,
625+
}
626+
434627
local function create_minetest_conf_example()
435628
local result = "# This file contains a list of all available settings and their default value for minetest.conf\n" ..
436629
"\n" ..
@@ -447,6 +640,7 @@ local function create_minetest_conf_example()
447640
"# http://wiki.minetest.net/\n" ..
448641
"\n"
449642

643+
local settings = parse_config_file(true, false)
450644
for _, entry in ipairs(settings) do
451645
if entry.type == "category" then
452646
if entry.level == 0 then
@@ -458,8 +652,8 @@ local function create_minetest_conf_example()
458652
result = result .. "# " .. entry.name .. "\n\n"
459653
end
460654
else
461-
if entry.comment_line ~= "" then
462-
for _, comment_line in ipairs(entry.comment:split("\n")) do
655+
if entry.comment ~= "" then
656+
for _, comment_line in ipairs(entry.comment:split("\n", true)) do
463657
result = result .."# " .. comment_line .. "\n"
464658
end
465659
end
@@ -473,6 +667,9 @@ local function create_minetest_conf_example()
473667
if entry.values then
474668
result = result .. " values: " .. table.concat(entry.values, ", ")
475669
end
670+
if entry.possible then
671+
result = result .. " possible values: " .. entry.possible:gsub(",", ", ")
672+
end
476673
result = result .. "\n"
477674
result = result .. "# " .. entry.name .. " = ".. entry.default .. "\n\n"
478675
end
@@ -485,6 +682,8 @@ local function create_translation_file()
485682
"// It conatins a bunch of fake gettext calls, to tell xgettext about the strings in config files\n" ..
486683
"// To update it, refer to the bottom of builtin/mainmenu/tab_settings.lua\n\n" ..
487684
"fake_function() {\n"
685+
686+
local settings = parse_config_file(true, false)
488687
for _, entry in ipairs(settings) do
489688
if entry.type == "category" then
490689
result = result .. "\tgettext(\"" .. entry.name .. "\");\n"
@@ -511,16 +710,9 @@ if false then
511710
end
512711

513712
if false then
514-
local file = io.open("src/settings_translation_file.c", "w")
713+
local file = io.open("src/settings_translation_file.cpp", "w")
515714
if file then
516715
file:write(create_translation_file())
517716
file:close()
518717
end
519718
end
520-
521-
tab_settings = {
522-
name = "settings",
523-
caption = fgettext("Settings"),
524-
cbf_formspec = create_settings_formspec,
525-
cbf_button_handler = handle_settings_buttons,
526-
}

‎builtin/settingtypes.txt

+418-263
Large diffs are not rendered by default.

‎doc/lua_api.txt

+7
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ e.g.
6464

6565
The game directory can contain the file minetest.conf, which will be used
6666
to set default settings when running the particular game.
67+
It can also contain a settingtypes.txt in the same format as the one in builtin.
68+
This settingtypes.txt will be parsed by the menu and the settings will be displayed in the "Games" category in the settings tab.
6769

6870
### Menu images
6971

@@ -125,6 +127,7 @@ Mod directory structure
125127
| |-- depends.txt
126128
| |-- screenshot.png
127129
| |-- description.txt
130+
| |-- settingtypes.txt
128131
| |-- init.lua
129132
| |-- models
130133
| |-- textures
@@ -155,6 +158,10 @@ A screenshot shown in modmanager within mainmenu.
155158
### `description.txt`
156159
A File containing description to be shown within mainmenu.
157160

161+
### `settingtypes.txt`
162+
A file in the same format as the one in builtin. It will be parsed by the
163+
settings menu and the settings will be displayed in the "Mods" category.
164+
158165
### `init.lua`
159166
The main Lua script. Running this script should register everything it
160167
wants to register. Subsequent execution depends on minetest calling the

‎minetest.conf.example

+432-247
Large diffs are not rendered by default.

‎src/settings_translation_file.c ‎src/settings_translation_file.cpp

+231-165
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.