settingtypes.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. --Luanti
  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. settingtypes = {}
  18. -- A Setting type is a table with the following keys:
  19. --
  20. -- name: Identifier
  21. -- readable_name: Readable title
  22. -- type: Category
  23. --
  24. -- name = mod.name,
  25. -- readable_name = mod.title,
  26. -- level = 1,
  27. -- type = "category", "int", "string", ""
  28. -- }
  29. local FILENAME = "settingtypes.txt"
  30. local CHAR_CLASSES = {
  31. SPACE = "[%s]",
  32. VARIABLE = "[%w_%-%.]",
  33. INTEGER = "[+-]?[%d]",
  34. FLOAT = "[+-]?[%d%.]",
  35. FLAGS = "[%w_%-%.,]",
  36. }
  37. local function flags_to_table(flags)
  38. return flags:gsub("%s+", ""):split(",", true) -- Remove all spaces and split
  39. end
  40. -- returns error message, or nil
  41. local function parse_setting_line(settings, line, read_all, base_level, allow_secure)
  42. -- strip carriage returns (CR, /r)
  43. line = line:gsub("\r", "")
  44. -- comment
  45. local comment_match = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$")
  46. if comment_match then
  47. settings.current_comment[#settings.current_comment + 1] = comment_match
  48. return
  49. end
  50. -- clear current_comment so only comments directly above a setting are bound to it
  51. -- but keep a local reference to it for variables in the current line
  52. local current_comment = settings.current_comment
  53. settings.current_comment = {}
  54. -- empty lines
  55. if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then
  56. return
  57. end
  58. -- category
  59. local stars, category = line:match("^%[([%*]*)([^%]]+)%]$")
  60. if category then
  61. local category_level = stars:len() + base_level
  62. if settings.current_hide_level then
  63. if settings.current_hide_level < category_level then
  64. -- Skip this category, it's inside a hidden category.
  65. return
  66. else
  67. -- The start of this category marks the end of a hidden category.
  68. settings.current_hide_level = nil
  69. end
  70. end
  71. if not read_all and category:sub(1, 5) == "Hide:" then
  72. -- This category is hidden.
  73. settings.current_hide_level = category_level
  74. return
  75. end
  76. table.insert(settings, {
  77. name = category,
  78. level = category_level,
  79. type = "category",
  80. })
  81. return
  82. end
  83. if settings.current_hide_level then
  84. -- Ignore this line, we're inside a hidden category.
  85. return
  86. end
  87. -- settings
  88. local first_part, name, readable_name, setting_type = line:match("^"
  89. -- this first capture group matches the whole first part,
  90. -- so we can later strip it from the rest of the line
  91. .. "("
  92. .. "([" .. CHAR_CLASSES.VARIABLE .. "+)" -- variable name
  93. .. CHAR_CLASSES.SPACE .. "*"
  94. .. "%(([^%)]*)%)" -- readable name
  95. .. CHAR_CLASSES.SPACE .. "*"
  96. .. "(" .. CHAR_CLASSES.VARIABLE .. "+)" -- type
  97. .. CHAR_CLASSES.SPACE .. "*"
  98. .. ")")
  99. if not first_part then
  100. return "Invalid line"
  101. end
  102. if name:match("secure%.[.]*") and not allow_secure then
  103. return "Tried to add \"secure.\" setting"
  104. end
  105. local requires = {}
  106. local last_line = #current_comment > 0 and current_comment[#current_comment]:trim()
  107. if last_line and last_line:lower():sub(1, 9) == "requires:" then
  108. local parts = last_line:sub(10):split(",")
  109. current_comment[#current_comment] = nil
  110. for _, part in ipairs(parts) do
  111. part = part:trim()
  112. local value = true
  113. if part:sub(1, 1) == "!" then
  114. value = false
  115. part = part:sub(2):trim()
  116. end
  117. requires[part] = value
  118. end
  119. end
  120. if readable_name == "" then
  121. readable_name = nil
  122. end
  123. local remaining_line = line:sub(first_part:len() + 1)
  124. local comment = table.concat(current_comment, "\n"):trim()
  125. if setting_type == "int" then
  126. local default, min, max = remaining_line:match("^"
  127. -- first int is required, the last 2 are optional
  128. .. "(" .. CHAR_CLASSES.INTEGER .. "+)" .. CHAR_CLASSES.SPACE .. "*"
  129. .. "(" .. CHAR_CLASSES.INTEGER .. "*)" .. CHAR_CLASSES.SPACE .. "*"
  130. .. "(" .. CHAR_CLASSES.INTEGER .. "*)"
  131. .. "$")
  132. if not default or not tonumber(default) then
  133. return "Invalid integer setting"
  134. end
  135. min = tonumber(min)
  136. max = tonumber(max)
  137. table.insert(settings, {
  138. name = name,
  139. readable_name = readable_name,
  140. type = "int",
  141. default = default,
  142. min = min,
  143. max = max,
  144. requires = requires,
  145. comment = comment,
  146. })
  147. return
  148. end
  149. if setting_type == "string"
  150. or setting_type == "key" or setting_type == "v3f" then
  151. local default = remaining_line:match("^(.*)$")
  152. if not default then
  153. return "Invalid string setting"
  154. end
  155. if setting_type == "key" and not read_all then
  156. -- ignore key type if read_all is false
  157. return
  158. end
  159. table.insert(settings, {
  160. name = name,
  161. readable_name = readable_name,
  162. type = setting_type,
  163. default = default,
  164. requires = requires,
  165. comment = comment,
  166. })
  167. return
  168. end
  169. if setting_type == "noise_params_2d"
  170. or setting_type == "noise_params_3d" then
  171. local default = remaining_line:match("^(.*)$")
  172. if not default then
  173. return "Invalid string setting"
  174. end
  175. local values = {}
  176. local ti = 1
  177. local index = 1
  178. for match in default:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
  179. index = default:find("[+-]?[%d.-e]+", index) + match:len()
  180. table.insert(values, match)
  181. ti = ti + 1
  182. if ti > 9 then
  183. break
  184. end
  185. end
  186. index = default:find("[^, ]", index)
  187. local flags = ""
  188. if index then
  189. flags = default:sub(index)
  190. end
  191. table.insert(values, flags)
  192. table.insert(settings, {
  193. name = name,
  194. readable_name = readable_name,
  195. type = setting_type,
  196. default = default,
  197. default_table = {
  198. offset = values[1],
  199. scale = values[2],
  200. spread = {
  201. x = values[3],
  202. y = values[4],
  203. z = values[5]
  204. },
  205. seed = values[6],
  206. octaves = values[7],
  207. persistence = values[8],
  208. lacunarity = values[9],
  209. flags = values[10]
  210. },
  211. values = values,
  212. requires = requires,
  213. comment = comment,
  214. noise_params = true,
  215. flags = flags_to_table("defaults,eased,absvalue")
  216. })
  217. return
  218. end
  219. if setting_type == "bool" then
  220. if remaining_line ~= "false" and remaining_line ~= "true" then
  221. return "Invalid boolean setting"
  222. end
  223. table.insert(settings, {
  224. name = name,
  225. readable_name = readable_name,
  226. type = "bool",
  227. default = remaining_line,
  228. requires = requires,
  229. comment = comment,
  230. })
  231. return
  232. end
  233. if setting_type == "float" then
  234. local default, min, max = remaining_line:match("^"
  235. -- first float is required, the last 2 are optional
  236. .. "(" .. CHAR_CLASSES.FLOAT .. "+)" .. CHAR_CLASSES.SPACE .. "*"
  237. .. "(" .. CHAR_CLASSES.FLOAT .. "*)" .. CHAR_CLASSES.SPACE .. "*"
  238. .. "(" .. CHAR_CLASSES.FLOAT .. "*)"
  239. .."$")
  240. if not default or not tonumber(default) then
  241. return "Invalid float setting"
  242. end
  243. min = tonumber(min)
  244. max = tonumber(max)
  245. table.insert(settings, {
  246. name = name,
  247. readable_name = readable_name,
  248. type = "float",
  249. default = default,
  250. min = min,
  251. max = max,
  252. requires = requires,
  253. comment = comment,
  254. })
  255. return
  256. end
  257. if setting_type == "enum" then
  258. local default, values = remaining_line:match("^"
  259. -- first value (default) may be empty (i.e. is optional)
  260. .. "(" .. CHAR_CLASSES.VARIABLE .. "*)" .. CHAR_CLASSES.SPACE .. "*"
  261. .. "(" .. CHAR_CLASSES.FLAGS .. "+)"
  262. .. "$")
  263. if not default or values == "" then
  264. return "Invalid enum setting"
  265. end
  266. table.insert(settings, {
  267. name = name,
  268. readable_name = readable_name,
  269. type = "enum",
  270. default = default,
  271. values = values:split(",", true),
  272. requires = requires,
  273. comment = comment,
  274. })
  275. return
  276. end
  277. if setting_type == "path" or setting_type == "filepath" then
  278. local default = remaining_line:match("^(.*)$")
  279. if not default then
  280. return "Invalid path setting"
  281. end
  282. table.insert(settings, {
  283. name = name,
  284. readable_name = readable_name,
  285. type = setting_type,
  286. default = default,
  287. requires = requires,
  288. comment = comment,
  289. })
  290. return
  291. end
  292. if setting_type == "flags" then
  293. local default, possible = remaining_line:match("^"
  294. -- first value (default) may be empty (i.e. is optional)
  295. -- this is implemented by making the last value optional, and
  296. -- swapping them around if it turns out empty.
  297. .. "(" .. CHAR_CLASSES.FLAGS .. "+)" .. CHAR_CLASSES.SPACE .. "*"
  298. .. "(" .. CHAR_CLASSES.FLAGS .. "*)"
  299. .. "$")
  300. if not default or not possible then
  301. return "Invalid flags setting"
  302. end
  303. if possible == "" then
  304. possible = default
  305. default = ""
  306. end
  307. table.insert(settings, {
  308. name = name,
  309. readable_name = readable_name,
  310. type = "flags",
  311. default = default,
  312. possible = flags_to_table(possible),
  313. requires = requires,
  314. comment = comment,
  315. })
  316. return
  317. end
  318. return "Invalid setting type \"" .. setting_type .. "\""
  319. end
  320. local function parse_single_file(file, filepath, read_all, result, base_level, allow_secure)
  321. -- store this helper variable in the table so it's easier to pass to parse_setting_line()
  322. result.current_comment = {}
  323. result.current_hide_level = nil
  324. local line = file:read("*line")
  325. while line do
  326. local error_msg = parse_setting_line(result, line, read_all, base_level, allow_secure)
  327. if error_msg then
  328. core.log("error", error_msg .. " in " .. filepath .. " \"" .. line .. "\"")
  329. end
  330. line = file:read("*line")
  331. end
  332. result.current_comment = nil
  333. result.current_hide_level = nil
  334. end
  335. --- Returns table of setting types
  336. --
  337. -- @param read_all Whether to ignore certain setting types for GUI or not
  338. -- @parse_mods Whether to parse settingtypes.txt in mods and games
  339. function settingtypes.parse_config_file(read_all, parse_mods)
  340. local settings = {}
  341. do
  342. local builtin_path = core.get_builtin_path() .. FILENAME
  343. local file = io.open(builtin_path, "r")
  344. if not file then
  345. core.log("error", "Can't load " .. FILENAME)
  346. return settings
  347. end
  348. parse_single_file(file, builtin_path, read_all, settings, 0, true)
  349. file:close()
  350. end
  351. if parse_mods then
  352. -- Parse games
  353. local games_category_initialized = false
  354. for _, game in ipairs(pkgmgr.games) do
  355. local path = game.path .. DIR_DELIM .. FILENAME
  356. local file = io.open(path, "r")
  357. if file then
  358. if not games_category_initialized then
  359. fgettext_ne("Content: Games") -- not used, but needed for xgettext
  360. table.insert(settings, {
  361. name = "Content: Games",
  362. level = 0,
  363. type = "category",
  364. })
  365. games_category_initialized = true
  366. end
  367. table.insert(settings, {
  368. name = game.path,
  369. readable_name = game.title,
  370. level = 1,
  371. type = "category",
  372. })
  373. parse_single_file(file, path, read_all, settings, 2, false)
  374. file:close()
  375. end
  376. end
  377. -- Parse mods
  378. pkgmgr.load_all()
  379. local mods_category_initialized = false
  380. local mods = pkgmgr.global_mods:get_list()
  381. table.sort(mods, function(a, b) return a.name < b.name end)
  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("Content: Mods") -- not used, but needed for xgettext
  388. table.insert(settings, {
  389. name = "Content: Mods",
  390. level = 0,
  391. type = "category",
  392. })
  393. mods_category_initialized = true
  394. end
  395. table.insert(settings, {
  396. name = mod.path,
  397. readable_name = mod.title or mod.name,
  398. level = 1,
  399. type = "category",
  400. })
  401. parse_single_file(file, path, read_all, settings, 2, false)
  402. file:close()
  403. end
  404. end
  405. -- Parse client mods
  406. local clientmods_category_initialized = false
  407. local clientmods = {}
  408. pkgmgr.get_mods(core.get_clientmodpath(), "clientmods", clientmods)
  409. for _, mod in ipairs(clientmods) do
  410. local path = mod.path .. DIR_DELIM .. FILENAME
  411. local file = io.open(path, "r")
  412. if file then
  413. if not clientmods_category_initialized then
  414. fgettext_ne("Client Mods") -- not used, but needed for xgettext
  415. table.insert(settings, {
  416. name = "Client Mods",
  417. level = 0,
  418. type = "category",
  419. })
  420. clientmods_category_initialized = true
  421. end
  422. table.insert(settings, {
  423. name = mod.path,
  424. readable_name = mod.title or mod.name,
  425. level = 1,
  426. type = "category",
  427. })
  428. parse_single_file(file, path, read_all, settings, 2, false)
  429. file:close()
  430. end
  431. end
  432. end
  433. return settings
  434. end