summaryrefslogtreecommitdiff
path: root/init.lua
blob: 2bf67fb156113865c539119af34a7992ec9bb042 (plain)
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
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

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]
			if technical then
				table.insert(list, ("%s (%s)"):format(setting, def.type))
			else
				table.insert(list, def.shortdesc)
			end
		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
			local value = playersettings.getdefault(settingname)
			playersettings.set(name, settingname, value)
			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

function playersettings.get(name,setting)
	CHECK_TYPE("string", name, "player name")
	CHECK_TYPE("string", setting, "setting name")
	assert(playersettings.registered[setting],"No such setting: "..setting)

	local value = core.deserialize(storage:get_string(string.format("%s|%s",name,setting)))
	if value ~= nil then
		return value
	else
		return playersettings.getdefault(setting)
	end
end

function playersettings.set(name,setting,value)
	CHECK_TYPE("string", name, "player name")
	CHECK_TYPE("string", setting, "setting name")
	assert(playersettings.registered[setting],"No such setting: "..setting)

	local old = playersettings.get(name,setting)
	if old == value then
		return
	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 def.onchange then
		if not def.onchange(name,old,value) then return end
	end
	storage:set_string(string.format("%s|%s",name,setting),core.serialize(value))
	if def.afterchange then def.afterchange(name,old,value) 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")