dlg_settings_advanced.lua 32 KB

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