dlg_settings_advanced.lua 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  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. local FILENAME = "settingtypes.txt"
  18. local CHAR_CLASSES = {
  19. SPACE = "[%s]",
  20. VARIABLE = "[%w_%-%.]",
  21. INTEGER = "[+-]?[%d]",
  22. FLOAT = "[+-]?[%d%.]",
  23. FLAGS = "[%w_%-%.,]",
  24. }
  25. -- returns error message, or nil
  26. local function parse_setting_line(settings, line, read_all, base_level, allow_secure)
  27. -- comment
  28. local comment = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$")
  29. if comment then
  30. if settings.current_comment == "" then
  31. settings.current_comment = comment
  32. else
  33. settings.current_comment = settings.current_comment .. "\n" .. comment
  34. end
  35. return
  36. end
  37. -- clear current_comment so only comments directly above a setting are bound to it
  38. -- but keep a local reference to it for variables in the current line
  39. local current_comment = settings.current_comment
  40. settings.current_comment = ""
  41. -- empty lines
  42. if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then
  43. return
  44. end
  45. -- category
  46. local stars, category = line:match("^%[([%*]*)([^%]]+)%]$")
  47. if category then
  48. table.insert(settings, {
  49. name = category,
  50. level = stars:len() + base_level,
  51. type = "category",
  52. })
  53. return
  54. end
  55. -- settings
  56. local first_part, name, readable_name, setting_type = line:match("^"
  57. -- this first capture group matches the whole first part,
  58. -- so we can later strip it from the rest of the line
  59. .. "("
  60. .. "([" .. CHAR_CLASSES.VARIABLE .. "+)" -- variable name
  61. .. CHAR_CLASSES.SPACE .. "*"
  62. .. "%(([^%)]*)%)" -- readable name
  63. .. CHAR_CLASSES.SPACE .. "*"
  64. .. "(" .. CHAR_CLASSES.VARIABLE .. "+)" -- type
  65. .. CHAR_CLASSES.SPACE .. "*"
  66. .. ")")
  67. if not first_part then
  68. return "Invalid line"
  69. end
  70. if name:match("secure%.[.]*") and not allow_secure then
  71. return "Tried to add \"secure.\" setting"
  72. end
  73. if readable_name == "" then
  74. readable_name = nil
  75. end
  76. local remaining_line = line:sub(first_part:len() + 1)
  77. if setting_type == "int" then
  78. local default, min, max = remaining_line:match("^"
  79. -- first int is required, the last 2 are optional
  80. .. "(" .. CHAR_CLASSES.INTEGER .. "+)" .. CHAR_CLASSES.SPACE .. "*"
  81. .. "(" .. CHAR_CLASSES.INTEGER .. "*)" .. CHAR_CLASSES.SPACE .. "*"
  82. .. "(" .. CHAR_CLASSES.INTEGER .. "*)"
  83. .. "$")
  84. if not default or not tonumber(default) then
  85. return "Invalid integer setting"
  86. end
  87. min = tonumber(min)
  88. max = tonumber(max)
  89. table.insert(settings, {
  90. name = name,
  91. readable_name = readable_name,
  92. type = "int",
  93. default = default,
  94. min = min,
  95. max = max,
  96. comment = current_comment,
  97. })
  98. return
  99. end
  100. if setting_type == "string" or setting_type == "noise_params"
  101. or setting_type == "key" or setting_type == "v3f" then
  102. local default = remaining_line:match("^(.*)$")
  103. if not default then
  104. return "Invalid string setting"
  105. end
  106. if setting_type == "key" and not read_all then
  107. -- ignore key type if read_all is false
  108. return
  109. end
  110. table.insert(settings, {
  111. name = name,
  112. readable_name = readable_name,
  113. type = setting_type,
  114. default = default,
  115. comment = current_comment,
  116. })
  117. return
  118. end
  119. if setting_type == "bool" then
  120. if remaining_line ~= "false" and remaining_line ~= "true" then
  121. return "Invalid boolean setting"
  122. end
  123. table.insert(settings, {
  124. name = name,
  125. readable_name = readable_name,
  126. type = "bool",
  127. default = remaining_line,
  128. comment = current_comment,
  129. })
  130. return
  131. end
  132. if setting_type == "float" then
  133. local default, min, max = remaining_line:match("^"
  134. -- first float is required, the last 2 are optional
  135. .. "(" .. CHAR_CLASSES.FLOAT .. "+)" .. CHAR_CLASSES.SPACE .. "*"
  136. .. "(" .. CHAR_CLASSES.FLOAT .. "*)" .. CHAR_CLASSES.SPACE .. "*"
  137. .. "(" .. CHAR_CLASSES.FLOAT .. "*)"
  138. .."$")
  139. if not default or not tonumber(default) then
  140. return "Invalid float setting"
  141. end
  142. min = tonumber(min)
  143. max = tonumber(max)
  144. table.insert(settings, {
  145. name = name,
  146. readable_name = readable_name,
  147. type = "float",
  148. default = default,
  149. min = min,
  150. max = max,
  151. comment = current_comment,
  152. })
  153. return
  154. end
  155. if setting_type == "enum" then
  156. local default, values = remaining_line:match("^"
  157. -- first value (default) may be empty (i.e. is optional)
  158. .. "(" .. CHAR_CLASSES.VARIABLE .. "*)" .. CHAR_CLASSES.SPACE .. "*"
  159. .. "(" .. CHAR_CLASSES.FLAGS .. "+)"
  160. .. "$")
  161. if not default or values == "" then
  162. return "Invalid enum setting"
  163. end
  164. table.insert(settings, {
  165. name = name,
  166. readable_name = readable_name,
  167. type = "enum",
  168. default = default,
  169. values = values:split(",", true),
  170. comment = current_comment,
  171. })
  172. return
  173. end
  174. if setting_type == "path" then
  175. local default = remaining_line:match("^(.*)$")
  176. if not default then
  177. return "Invalid path setting"
  178. end
  179. table.insert(settings, {
  180. name = name,
  181. readable_name = readable_name,
  182. type = "path",
  183. default = default,
  184. comment = current_comment,
  185. })
  186. return
  187. end
  188. if setting_type == "flags" then
  189. local default, possible = remaining_line:match("^"
  190. -- first value (default) may be empty (i.e. is optional)
  191. -- this is implemented by making the last value optional, and
  192. -- swapping them around if it turns out empty.
  193. .. "(" .. CHAR_CLASSES.FLAGS .. "+)" .. CHAR_CLASSES.SPACE .. "*"
  194. .. "(" .. CHAR_CLASSES.FLAGS .. "*)"
  195. .. "$")
  196. if not default or not possible then
  197. return "Invalid flags setting"
  198. end
  199. if possible == "" then
  200. possible = default
  201. default = ""
  202. end
  203. table.insert(settings, {
  204. name = name,
  205. readable_name = readable_name,
  206. type = "flags",
  207. default = default,
  208. possible = possible,
  209. comment = current_comment,
  210. })
  211. return
  212. end
  213. return "Invalid setting type \"" .. setting_type .. "\""
  214. end
  215. local function parse_single_file(file, filepath, read_all, result, base_level, allow_secure)
  216. -- store this helper variable in the table so it's easier to pass to parse_setting_line()
  217. result.current_comment = ""
  218. local line = file:read("*line")
  219. while line do
  220. local error_msg = parse_setting_line(result, line, read_all, base_level, allow_secure)
  221. if error_msg then
  222. core.log("error", error_msg .. " in " .. filepath .. " \"" .. line .. "\"")
  223. end
  224. line = file:read("*line")
  225. end
  226. result.current_comment = nil
  227. end
  228. -- read_all: whether to ignore certain setting types for GUI or not
  229. -- parse_mods: whether to parse settingtypes.txt in mods and games
  230. local function parse_config_file(read_all, parse_mods)
  231. local builtin_path = core.get_builtin_path() .. DIR_DELIM .. FILENAME
  232. local file = io.open(builtin_path, "r")
  233. local settings = {}
  234. if not file then
  235. core.log("error", "Can't load " .. FILENAME)
  236. return settings
  237. end
  238. parse_single_file(file, builtin_path, read_all, settings, 0, true)
  239. file:close()
  240. if parse_mods then
  241. -- Parse games
  242. local games_category_initialized = false
  243. local index = 1
  244. local game = gamemgr.get_game(index)
  245. while game do
  246. local path = game.path .. DIR_DELIM .. FILENAME
  247. local file = io.open(path, "r")
  248. if file then
  249. if not games_category_initialized then
  250. local translation = fgettext_ne("Games"), -- not used, but needed for xgettext
  251. table.insert(settings, {
  252. name = "Games",
  253. level = 0,
  254. type = "category",
  255. })
  256. games_category_initialized = true
  257. end
  258. table.insert(settings, {
  259. name = game.name,
  260. level = 1,
  261. type = "category",
  262. })
  263. parse_single_file(file, path, read_all, settings, 2, false)
  264. file:close()
  265. end
  266. index = index + 1
  267. game = gamemgr.get_game(index)
  268. end
  269. -- Parse mods
  270. local mods_category_initialized = false
  271. local mods = {}
  272. get_mods(core.get_modpath(), mods)
  273. for _, mod in ipairs(mods) do
  274. local path = mod.path .. DIR_DELIM .. FILENAME
  275. local file = io.open(path, "r")
  276. if file then
  277. if not mods_category_initialized then
  278. local translation = fgettext_ne("Mods"), -- not used, but needed for xgettext
  279. table.insert(settings, {
  280. name = "Mods",
  281. level = 0,
  282. type = "category",
  283. })
  284. mods_category_initialized = true
  285. end
  286. table.insert(settings, {
  287. name = mod.name,
  288. level = 1,
  289. type = "category",
  290. })
  291. parse_single_file(file, path, read_all, settings, 2, false)
  292. file:close()
  293. end
  294. end
  295. end
  296. return settings
  297. end
  298. local settings = parse_config_file(false, true)
  299. local selected_setting = 1
  300. local function get_current_value(setting)
  301. local value = core.setting_get(setting.name)
  302. if value == nil then
  303. value = setting.default
  304. end
  305. return value
  306. end
  307. local function create_change_setting_formspec(dialogdata)
  308. local setting = settings[selected_setting]
  309. local formspec = "size[10,5.2,true]" ..
  310. "button[5,4.5;2,1;btn_done;" .. fgettext("Save") .. "]" ..
  311. "button[3,4.5;2,1;btn_cancel;" .. fgettext("Cancel") .. "]" ..
  312. "tablecolumns[color;text]" ..
  313. "tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
  314. "table[0,0;10,3;info;"
  315. if setting.readable_name then
  316. formspec = formspec .. "#FFFF00," .. fgettext(setting.readable_name)
  317. .. " (" .. core.formspec_escape(setting.name) .. "),"
  318. else
  319. formspec = formspec .. "#FFFF00," .. core.formspec_escape(setting.name) .. ","
  320. end
  321. formspec = formspec .. ",,"
  322. local comment_text = ""
  323. if setting.comment == "" then
  324. comment_text = fgettext_ne("(No description of setting given)")
  325. else
  326. comment_text = fgettext_ne(setting.comment)
  327. end
  328. for _, comment_line in ipairs(comment_text:split("\n", true)) do
  329. formspec = formspec .. "," .. core.formspec_escape(comment_line) .. ","
  330. end
  331. if setting.type == "flags" then
  332. formspec = formspec .. ",,"
  333. .. "," .. fgettext("Please enter a comma seperated list of flags.") .. ","
  334. .. "," .. fgettext("Possible values are: ")
  335. .. core.formspec_escape(setting.possible:gsub(",", ", ")) .. ","
  336. elseif setting.type == "noise_params" then
  337. formspec = formspec .. ",,"
  338. .. "," .. fgettext("Format: <offset>, <scale>, (<spreadX>, <spreadY>, <spreadZ>), <seed>, <octaves>, <persistence>") .. ","
  339. .. "," .. fgettext("Optionally the lacunarity can be appended with a leading comma.") .. ","
  340. elseif setting.type == "v3f" then
  341. formspec = formspec .. ",,"
  342. .. "," .. fgettext_ne("Format is 3 numbers separated by commas and inside brackets.") .. ","
  343. end
  344. formspec = formspec:sub(1, -2) -- remove trailing comma
  345. formspec = formspec .. ";1]"
  346. if setting.type == "bool" then
  347. local selected_index
  348. if core.is_yes(get_current_value(setting)) then
  349. selected_index = 2
  350. else
  351. selected_index = 1
  352. end
  353. formspec = formspec .. "dropdown[0.5,3.5;3,1;dd_setting_value;"
  354. .. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";"
  355. .. selected_index .. "]"
  356. elseif setting.type == "enum" then
  357. local selected_index = 0
  358. formspec = formspec .. "dropdown[0.5,3.5;3,1;dd_setting_value;"
  359. for index, value in ipairs(setting.values) do
  360. -- translating value is not possible, since it's the value
  361. -- that we set the setting to
  362. formspec = formspec .. core.formspec_escape(value) .. ","
  363. if get_current_value(setting) == value then
  364. selected_index = index
  365. end
  366. end
  367. if #setting.values > 0 then
  368. formspec = formspec:sub(1, -2) -- remove trailing comma
  369. end
  370. formspec = formspec .. ";" .. selected_index .. "]"
  371. elseif setting.type == "path" then
  372. local current_value = dialogdata.selected_path
  373. if not current_value then
  374. current_value = get_current_value(setting)
  375. end
  376. formspec = formspec .. "field[0.5,4;7.5,1;te_setting_value;;"
  377. .. core.formspec_escape(current_value) .. "]"
  378. .. "button[8,3.75;2,1;btn_browser_path;" .. fgettext("Browse") .. "]"
  379. else
  380. -- TODO: fancy input for float, int, flags, noise_params, v3f
  381. local width = 10
  382. local text = get_current_value(setting)
  383. if dialogdata.error_message then
  384. formspec = formspec .. "tablecolumns[color;text]" ..
  385. "tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
  386. "table[5,3.9;5,0.6;error_message;#FF0000,"
  387. .. core.formspec_escape(dialogdata.error_message) .. ";0]"
  388. width = 5
  389. if dialogdata.entered_text then
  390. text = dialogdata.entered_text
  391. end
  392. end
  393. formspec = formspec .. "field[0.5,4;" .. width .. ",1;te_setting_value;;"
  394. .. core.formspec_escape(text) .. "]"
  395. end
  396. return formspec
  397. end
  398. local function handle_change_setting_buttons(this, fields)
  399. if fields["btn_done"] or fields["key_enter"] then
  400. local setting = settings[selected_setting]
  401. if setting.type == "bool" then
  402. local new_value = fields["dd_setting_value"]
  403. -- Note: new_value is the actual (translated) value shown in the dropdown
  404. core.setting_setbool(setting.name, new_value == fgettext("Enabled"))
  405. elseif setting.type == "enum" then
  406. local new_value = fields["dd_setting_value"]
  407. core.setting_set(setting.name, new_value)
  408. elseif setting.type == "int" then
  409. local new_value = tonumber(fields["te_setting_value"])
  410. if not new_value or math.floor(new_value) ~= new_value then
  411. this.data.error_message = fgettext_ne("Please enter a valid integer.")
  412. this.data.entered_text = fields["te_setting_value"]
  413. core.update_formspec(this:get_formspec())
  414. return true
  415. end
  416. if setting.min and new_value < setting.min then
  417. this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
  418. this.data.entered_text = fields["te_setting_value"]
  419. core.update_formspec(this:get_formspec())
  420. return true
  421. end
  422. if setting.max and new_value > setting.max then
  423. this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
  424. this.data.entered_text = fields["te_setting_value"]
  425. core.update_formspec(this:get_formspec())
  426. return true
  427. end
  428. core.setting_set(setting.name, new_value)
  429. elseif setting.type == "float" then
  430. local new_value = tonumber(fields["te_setting_value"])
  431. if not new_value then
  432. this.data.error_message = fgettext_ne("Please enter a valid number.")
  433. this.data.entered_text = fields["te_setting_value"]
  434. core.update_formspec(this:get_formspec())
  435. return true
  436. end
  437. core.setting_set(setting.name, new_value)
  438. elseif setting.type == "flags" then
  439. local new_value = fields["te_setting_value"]
  440. for _,value in ipairs(new_value:split(",", true)) do
  441. value = value:trim()
  442. local possible = "," .. setting.possible .. ","
  443. if not possible:find("," .. value .. ",", 0, true) then
  444. this.data.error_message = fgettext_ne("\"$1\" is not a valid flag.", value)
  445. this.data.entered_text = fields["te_setting_value"]
  446. core.update_formspec(this:get_formspec())
  447. return true
  448. end
  449. end
  450. core.setting_set(setting.name, new_value)
  451. else
  452. local new_value = fields["te_setting_value"]
  453. core.setting_set(setting.name, new_value)
  454. end
  455. core.setting_save()
  456. this:delete()
  457. return true
  458. end
  459. if fields["btn_cancel"] then
  460. this:delete()
  461. return true
  462. end
  463. if fields["btn_browser_path"] then
  464. core.show_file_open_dialog("dlg_browse_path", fgettext_ne("Select path"))
  465. end
  466. if fields["dlg_browse_path_accepted"] then
  467. this.data.selected_path = fields["dlg_browse_path_accepted"]
  468. core.update_formspec(this:get_formspec())
  469. end
  470. return false
  471. end
  472. local function create_settings_formspec(tabview, name, tabdata)
  473. local formspec = "size[12,6.5;true]" ..
  474. "tablecolumns[color;tree;text;text]" ..
  475. "tableoptions[background=#00000000;border=false]" ..
  476. "table[0,0;12,5.5;list_settings;"
  477. local current_level = 0
  478. for _, entry in ipairs(settings) do
  479. local name
  480. if not core.setting_getbool("main_menu_technical_settings") and entry.readable_name then
  481. name = fgettext_ne(entry.readable_name)
  482. else
  483. name = entry.name
  484. end
  485. if entry.type == "category" then
  486. current_level = entry.level
  487. formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
  488. elseif entry.type == "bool" then
  489. local value = get_current_value(entry)
  490. if core.is_yes(value) then
  491. value = fgettext("Enabled")
  492. else
  493. value = fgettext("Disabled")
  494. end
  495. formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
  496. .. value .. ","
  497. elseif entry.type == "key" then
  498. -- ignore key settings, since we have a special dialog for them
  499. else
  500. formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
  501. .. core.formspec_escape(get_current_value(entry)) .. ","
  502. end
  503. end
  504. if #settings > 0 then
  505. formspec = formspec:sub(1, -2) -- remove trailing comma
  506. end
  507. formspec = formspec .. ";" .. selected_setting .. "]" ..
  508. "button[0,6;4,1;btn_back;".. fgettext("< Back to Settings page") .. "]" ..
  509. "button[10,6;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
  510. "button[7,6;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
  511. "checkbox[0,5.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
  512. .. dump(core.setting_getbool("main_menu_technical_settings")) .. "]"
  513. return formspec
  514. end
  515. local function handle_settings_buttons(this, fields, tabname, tabdata)
  516. local list_enter = false
  517. if fields["list_settings"] then
  518. selected_setting = core.get_table_index("list_settings")
  519. if core.explode_table_event(fields["list_settings"]).type == "DCL" then
  520. -- Directly toggle booleans
  521. local setting = settings[selected_setting]
  522. if setting.type == "bool" then
  523. local current_value = get_current_value(setting)
  524. core.setting_setbool(setting.name, not core.is_yes(current_value))
  525. core.setting_save()
  526. return true
  527. else
  528. list_enter = true
  529. end
  530. else
  531. return true
  532. end
  533. end
  534. if fields["btn_edit"] or list_enter then
  535. local setting = settings[selected_setting]
  536. if setting.type ~= "category" then
  537. local edit_dialog = dialog_create("change_setting", create_change_setting_formspec,
  538. handle_change_setting_buttons)
  539. edit_dialog:set_parent(this)
  540. this:hide()
  541. edit_dialog:show()
  542. end
  543. return true
  544. end
  545. if fields["btn_restore"] then
  546. local setting = settings[selected_setting]
  547. if setting.type ~= "category" then
  548. core.setting_set(setting.name, setting.default)
  549. core.setting_save()
  550. core.update_formspec(this:get_formspec())
  551. end
  552. return true
  553. end
  554. if fields["btn_back"] then
  555. this:delete()
  556. return true
  557. end
  558. if fields["cb_tech_settings"] then
  559. core.setting_set("main_menu_technical_settings", fields["cb_tech_settings"])
  560. core.setting_save()
  561. core.update_formspec(this:get_formspec())
  562. return true
  563. end
  564. return false
  565. end
  566. function create_adv_settings_dlg()
  567. local dlg = dialog_create("settings_advanced",
  568. create_settings_formspec,
  569. handle_settings_buttons,
  570. nil)
  571. return dlg
  572. end
  573. -- Generate minetest.conf.example and settings_translation_file.cpp
  574. -- *** Please note ***
  575. -- There is text in minetest.conf.example that will not be generated from
  576. -- settingtypes.txt but must be preserved:
  577. -- The documentation of mapgen noise parameter formats (title plus 16 lines)
  578. -- Noise parameter 'mgv5_np_ground' in group format (13 lines)
  579. --assert(loadfile(core.get_mainmenu_path()..DIR_DELIM.."generate_from_settingtypes.lua"))(parse_config_file(true, false))