1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
|
local storage = core.get_mod_storage()
playersettings = {}
playersettings.registered = {}
playersettings.settingslist = {}
-- per-player values for formspec creation
playersettings.highlighted = {}
playersettings.show_technical_names = {}
playersettings.form = "formspec_version[4]"..
"size[15,12.5]" ..
"button[11, 9.5;3,1;bset;Set]"..
"button[12,11;2,1;breset;Reset]"..
"textlist[0.5,0.5;7,11;settinglist;%s;%d;false]"..
"checkbox[0.5,12;ctechnical;Show technical names;%s]"..
" box[8,1;6.5,4;#222]"..
"textarea[8,1;6.5,4;;%s;%s]"..
"%s"
local text_no_settings = [[
No settings currently exist.
To use the settings menu, one or more mods that provide some settings must be installed.
]]
playersettings.form_fallback = (
"formspec_version[4]"..
"size[10,4.5]"..
"textarea[0.5,0.5;9,2.5;;;" .. text_no_settings .."]"..
"button_exit[4,3;2,1;quit;Close]"
)
--- Returns: setting, def
local function get_setting_at(index)
local settingname = playersettings.settingslist[index]
if not settingname then
return nil, nil
end
return settingname, playersettings.registered[settingname]
end
local function table_concat_escaped(list, delim)
local esc = {}
for i, v in ipairs(list) do
esc[i] = core.formspec_escape(v)
end
return table.concat(esc, delim)
end
local setting_get_no_fallback -- (name, setting, require_existence)
function playersettings.openform(player)
if type(player) == "string" then player = core.get_player_by_name(player) end
local name = player:get_player_name()
if #playersettings.settingslist < 1 then
core.show_formspec(name,"playersettings:nosettings", playersettings.form_fallback)
return
end
local settingslist
do
local list = {}
local technical = playersettings.show_technical_names[name]
for _,setting in ipairs(playersettings.settingslist) do
local def = playersettings.registered[setting]
local row
if technical then
row = ("%s (%s)"):format(setting, def.type)
else
row = def.shortdesc
end
if (setting_get_no_fallback(name, setting, false)) ~= nil then
-- 'Value is set' indicator
row = row .. " *"
end
table.insert(list, row)
end
settingslist = table_concat_escaped(list, ",")
end
local settingname, def = get_setting_at(playersettings.highlighted[name])
if not def then
playersettings.highlighted[name] = 1
settingname, def = get_setting_at(1)
end
local settingvalue = playersettings.get(name, settingname)
-- Engine bug? Even though this code uses the correct values, when pressing
-- "Set" and then "Reset", the shown value does not update.
--print(def.type, settingvalue)
local extras = ""
if def.type == "boolean" then
extras = (
"checkbox[8,6.5;checkbox;Enabled;%s]" ..
"field_close_on_enter[field;false]"
):format(
settingvalue and "true" or "false"
)
elseif def.type == "string" then
extras = (
"field[8,6;6,1;field;Enter value:;%s]" ..
"textarea[8,7.5;6,2;;;Allowed values: %s\nCharacters limit: %d]" ..
"field_close_on_enter[field;false]"
):format(
core.formspec_escape(settingvalue),
core.formspec_escape(def.values or "any string"),
def.max
)
elseif def.type == "number" then
local allowed = "any number"
if def.min and def.max then
allowed = "numbers "..def.min.." to "..def.max
elseif def.max then
allowed = "numbers up to "..def.max
elseif def.min then
allowed = "numbers "..def.min.." and up"
end
if def.integer then
allowed = allowed.." (whole numbers only)"
end
extras = (
"field[8,6;6,1;field;Enter value:;%s]" ..
"textarea[8,7.5;6,2;;;Allowed values: %s]" ..
"field_close_on_enter[field;false]"
):format(
core.formspec_escape(settingvalue),
allowed
)
elseif def.type == "enum" then
local selected = table.indexof(def.values, settingvalue)
if selected == -1 then selected = 1 end
extras = ("dropdown[8,6;6,1;dropdown;%s;%d;true]"):format(
table_concat_escaped(table.copy(def.values), ","),
selected
)
end
local form = playersettings.form:format(
settingslist,
playersettings.highlighted[name],
playersettings.show_technical_names[name] and "true" or "false",
core.formspec_escape(def.shortdesc),
core.formspec_escape(def.longdesc),
extras
)
core.show_formspec(name,"playersettings:settings",form)
end
function playersettings.handleform(player,form,fields)
if form ~= "playersettings:settings" then return end
local name = player:get_player_name()
if fields.ctechnical then
playersettings.show_technical_names[name] = fields.ctechnical == "true"
playersettings.openform(player)
return
end
local settingname, def = get_setting_at(playersettings.highlighted[name])
if def then
if fields.breset then
playersettings.reset(name, settingname)
playersettings.openform(player)
return
end
-- May also be triggered by `fields.bset`
if def.type == "boolean" and fields.checkbox then
playersettings.set(name,settingname,fields.checkbox == "true")
elseif def.type == "enum" and fields.dropdown then
local value = def.values[tonumber(fields.dropdown)]
if value then
playersettings.set(name, settingname, value)
end
elseif def.type == "number" and fields.field then
local value = tonumber(fields.field)
if value
and ((not def.max) or (value <= def.max))
and ((not def.min) or (value >= def.min)) then
if def.integer then value = math.floor(value) end
playersettings.set(name,settingname,value)
end
elseif def.type == "string" and fields.field then
local value = fields.field:sub(1, def.max)
playersettings.set(name,settingname,value)
end
end
if fields.settinglist then
local exp = core.explode_textlist_event(fields.settinglist)
if exp.type == "CHG" then
playersettings.highlighted[name] = exp.index
playersettings.openform(player)
end
end
end
local function CHECK_TYPE(typename, value, readable)
local got = type(value)
assert(got == typename, ("Invalid %s (expected %s, got %s)"):format(readable, typename, got))
end
function playersettings.getdefault(setting)
local def = playersettings.registered[setting]
if def.default ~= nil then return def.default end
if def.type == "boolean" then
return false
elseif def.type == "number" then
return def.min or 0
elseif def.type == "string" then
return ""
end
end
--- Return #1: Value or `nil` if no value was found
--- Return #2: Database key
setting_get_no_fallback = function(name, setting, require_existence)
CHECK_TYPE("string", name, "player name")
CHECK_TYPE("string", setting, "setting name")
local key = ("%s|%s"):format(name, setting)
if require_existence then
assert(playersettings.registered[setting],"No such setting: "..setting)
end
return core.deserialize(storage:get_string(key)), key
end
function playersettings.get(name,setting)
local value = setting_get_no_fallback(name, setting, true)
if value ~= nil then
return value
else
return playersettings.getdefault(setting)
end
end
function playersettings.set(name,setting,value)
local old, key = setting_get_no_fallback(name, setting, true)
-- Compare value without the .get(...) default fallback
local is_different = old ~= value
if old == nil then
local default = playersettings.getdefault(setting)
-- Allow using the default value but do not fire callbacks if they are equal
is_different = value ~= default
end
local def = playersettings.registered[setting]
if def.type == "enum" then
-- Prohibit unknown values
if table.indexof(def.values, value) == -1 then
return
end
end
if is_different and def.onchange then
if not def.onchange(name,old,value) then return end
end
storage:set_string(key,core.serialize(value))
if is_different and def.afterchange then
def.afterchange(name,old,value)
end
end
function playersettings.reset(name, setting)
local old, key = setting_get_no_fallback(name, setting, false)
-- ^ 'false': Do not require 'setting' to be defined to allow removing unused settings.
local def = playersettings.registered[setting]
local default = def and playersettings.getdefault(setting)
local is_different = def and old ~= default
if is_different and def.onchange then
if not def.onchange(name,old,default) then return end
end
storage:set_string(key, "")
if is_different and def.afterchange then
def.afterchange(name,old,default)
end
end
function playersettings.register(setting,def)
CHECK_TYPE("string", setting, "setting name")
CHECK_TYPE("table", def, "setting definition")
assert(not playersettings.registered[setting],string.format("Setting %s is already defined",setting))
assert(type(def.type) == "string")
assert(type(def.shortdesc) == "string")
assert(type(def.longdesc) == "string")
assert(not def.onjoin or type(def.onjoin) == "function")
assert(not def.onchange or type(def.onchange) == "function")
assert(not def.afterchange or type(def.afterchange) == "function")
-- Type-specific checks
if def.type == "string" then
assert(type(def.values) == "string")
def.max = def.max or 200
elseif def.type == "enum" then
assert(type(def.values) == "table")
end
local default_type = (def.type == "enum") and "string" or def.type
assert(def.default == nil or type(def.default) == default_type)
table.insert(playersettings.settingslist,setting)
table.sort(playersettings.settingslist)
playersettings.registered[setting] = def
end
function playersettings.onjoin(player)
local name = player:get_player_name()
for setting,def in pairs(playersettings.registered) do
if def.onjoin then
def.onjoin(name,playersettings.get(name,setting))
end
end
end
local function on_leaveplayer(player)
-- Clean up memory
local name = player:get_player_name()
playersettings.highlighted[name] = nil
playersettings.show_technical_names[name] = nil
end
if core.global_exists("unified_inventory") then
unified_inventory.register_button("playersettings", {
action = playersettings.openform,
tooltip = "Settings",
type = "image",
image = "playersettings_settings_button.png"
})
end
core.register_chatcommand("settings",{
description = "Open player settings menu",
func = playersettings.openform,
})
core.register_on_joinplayer(playersettings.onjoin)
core.register_on_leaveplayer(on_leaveplayer)
core.register_on_player_receive_fields(playersettings.handleform)
-- For testing purposes only!
--dofile(core.get_modpath(core.get_current_modname()) .. "/examples.lua")
|