1local S = minetest.get_translator("testtools")
2local F = minetest.formspec_escape
3
4dofile(minetest.get_modpath("testtools") .. "/light.lua")
5
6-- TODO: Add a Node Metadata tool
7
8minetest.register_tool("testtools:param2tool", {
9	description = S("Param2 Tool") .."\n"..
10		S("Modify param2 value of nodes") .."\n"..
11		S("Punch: +1") .."\n"..
12		S("Sneak+Punch: +8") .."\n"..
13		S("Place: -1") .."\n"..
14		S("Sneak+Place: -8"),
15	inventory_image = "testtools_param2tool.png",
16	groups = { testtool = 1, disable_repair = 1 },
17	on_use = function(itemstack, user, pointed_thing)
18		local pos = minetest.get_pointed_thing_position(pointed_thing)
19		if pointed_thing.type ~= "node" or (not pos) then
20			return
21		end
22		local add = 1
23		if user then
24			local ctrl = user:get_player_control()
25			if ctrl.sneak then
26				add = 8
27			end
28		end
29		local node = minetest.get_node(pos)
30		node.param2 = node.param2 + add
31		minetest.swap_node(pos, node)
32	end,
33	on_place = function(itemstack, user, pointed_thing)
34		local pos = minetest.get_pointed_thing_position(pointed_thing)
35		if pointed_thing.type ~= "node" or (not pos) then
36			return
37		end
38		local add = -1
39		if user then
40			local ctrl = user:get_player_control()
41			if ctrl.sneak then
42				add = -8
43			end
44		end
45		local node = minetest.get_node(pos)
46		node.param2 = node.param2 + add
47		minetest.swap_node(pos, node)
48	end,
49})
50
51minetest.register_tool("testtools:node_setter", {
52	description = S("Node Setter") .."\n"..
53		S("Replace pointed node with something else") .."\n"..
54		S("Punch: Select pointed node") .."\n"..
55		S("Place on node: Replace node with selected node") .."\n"..
56		S("Place in air: Manually select a node"),
57	inventory_image = "testtools_node_setter.png",
58	groups = { testtool = 1, disable_repair = 1 },
59	on_use = function(itemstack, user, pointed_thing)
60		local pos = minetest.get_pointed_thing_position(pointed_thing)
61		if pointed_thing.type == "nothing" then
62			local meta = itemstack:get_meta()
63			meta:set_string("node", "air")
64			meta:set_int("node_param2", 0)
65			if user and user:is_player() then
66				minetest.chat_send_player(user:get_player_name(), S("Now placing: @1 (param2=@2)", "air", 0))
67			end
68			return itemstack
69		elseif pointed_thing.type ~= "node" or (not pos) then
70			return
71		end
72		local node = minetest.get_node(pos)
73		local meta = itemstack:get_meta()
74		meta:set_string("node", node.name)
75		meta:set_int("node_param2", node.param2)
76		if user and user:is_player() then
77			minetest.chat_send_player(user:get_player_name(), S("Now placing: @1 (param2=@2)", node.name, node.param2))
78		end
79		return itemstack
80	end,
81	on_secondary_use = function(itemstack, user, pointed_thing)
82		local meta = itemstack:get_meta()
83		local nodename = meta:get_string("node") or ""
84		local param2 = meta:get_int("node_param2") or 0
85
86		minetest.show_formspec(user:get_player_name(), "testtools:node_setter",
87			"size[4,4]"..
88			"field[0.5,1;3,1;nodename;"..F(S("Node name (itemstring):"))..";"..F(nodename).."]"..
89			"field[0.5,2;3,1;param2;"..F(S("param2:"))..";"..F(tostring(param2)).."]"..
90			"button_exit[0.5,3;3,1;submit;"..F(S("Submit")).."]"
91		)
92	end,
93	on_place = function(itemstack, user, pointed_thing)
94		local pos = minetest.get_pointed_thing_position(pointed_thing)
95		local meta = itemstack:get_meta()
96		local nodename = meta:get_string("node")
97		if nodename == "" and user and user:is_player() then
98			minetest.chat_send_player(user:get_player_name(), S("Punch a node first!"))
99			return
100		end
101		local param2 = meta:get_int("node_param2")
102		if not param2 then
103			param2 = 0
104		end
105		local node = { name = nodename, param2 = param2 }
106		if not minetest.registered_nodes[nodename] then
107			minetest.chat_send_player(user:get_player_name(), S("Cannot set unknown node: @1", nodename))
108			return
109		end
110		minetest.set_node(pos, node)
111	end,
112})
113
114minetest.register_on_player_receive_fields(function(player, formname, fields)
115	if formname == "testtools:node_setter" then
116		local playername = player:get_player_name()
117		local witem = player:get_wielded_item()
118		if witem:get_name() == "testtools:node_setter" then
119			if fields.nodename and fields.param2 then
120				local param2 = tonumber(fields.param2)
121				if not param2 then
122					return
123				end
124				local meta = witem:get_meta()
125				meta:set_string("node", fields.nodename)
126				meta:set_int("node_param2", param2)
127				player:set_wielded_item(witem)
128			end
129		end
130	end
131end)
132
133minetest.register_tool("testtools:remover", {
134	description = S("Remover") .."\n"..
135		S("Punch: Remove pointed node or object"),
136	inventory_image = "testtools_remover.png",
137	groups = { testtool = 1, disable_repair = 1 },
138	on_use = function(itemstack, user, pointed_thing)
139		local pos = minetest.get_pointed_thing_position(pointed_thing)
140		if pointed_thing.type == "node" and pos ~= nil then
141			minetest.remove_node(pos)
142		elseif pointed_thing.type == "object" then
143			local obj = pointed_thing.ref
144			if not obj:is_player() then
145				obj:remove()
146			else
147				minetest.chat_send_player(user:get_player_name(), S("Can't remove players!"))
148			end
149		end
150	end,
151})
152
153minetest.register_tool("testtools:falling_node_tool", {
154	description = S("Falling Node Tool") .."\n"..
155		S("Punch: Make pointed node fall") .."\n"..
156		S("Place: Move pointed node 2 units upwards, then make it fall"),
157	inventory_image = "testtools_falling_node_tool.png",
158	groups = { testtool = 1, disable_repair = 1 },
159	on_place = function(itemstack, user, pointed_thing)
160		-- Teleport node 1-2 units upwards (if possible) and make it fall
161		local pos = minetest.get_pointed_thing_position(pointed_thing)
162		if pointed_thing.type ~= "node" or (not pos) then
163			return
164		end
165		local ok = false
166		local highest
167		for i=1,2 do
168			local above = {x=pos.x,y=pos.y+i,z=pos.z}
169			local n2 = minetest.get_node(above)
170			local def2 = minetest.registered_nodes[n2.name]
171			if def2 and (not def2.walkable) then
172				highest = above
173			else
174				break
175			end
176		end
177		if highest then
178			local node = minetest.get_node(pos)
179			local metatable = minetest.get_meta(pos):to_table()
180			minetest.remove_node(pos)
181			minetest.set_node(highest, node)
182			local meta_highest = minetest.get_meta(highest)
183			meta_highest:from_table(metatable)
184			ok = minetest.spawn_falling_node(highest)
185		else
186			ok = minetest.spawn_falling_node(pos)
187		end
188		if not ok and user and user:is_player() then
189			minetest.chat_send_player(user:get_player_name(), S("Falling node could not be spawned!"))
190		end
191	end,
192	on_use = function(itemstack, user, pointed_thing)
193		local pos = minetest.get_pointed_thing_position(pointed_thing)
194		if pointed_thing.type ~= "node" or (not pos) then
195			return
196		end
197		local ok = minetest.spawn_falling_node(pos)
198		if not ok and user and user:is_player() then
199			minetest.chat_send_player(user:get_player_name(), S("Falling node could not be spawned!"))
200		end
201	end,
202})
203
204minetest.register_tool("testtools:rotator", {
205	description = S("Entity Rotator") .. "\n" ..
206		S("Rotate pointed entity") .."\n"..
207		S("Punch: Yaw") .."\n"..
208		S("Sneak+Punch: Pitch") .."\n"..
209		S("Aux1+Punch: Roll"),
210	inventory_image = "testtools_entity_rotator.png",
211	groups = { testtool = 1, disable_repair = 1 },
212	on_use = function(itemstack, user, pointed_thing)
213		if pointed_thing.type ~= "object" then
214			return
215		end
216		local obj = pointed_thing.ref
217		if obj:is_player() then
218			-- No player rotation
219			return
220		else
221			local axis = "y"
222			if user and user:is_player() then
223				local ctrl = user:get_player_control()
224				if ctrl.sneak then
225					axis = "x"
226				elseif ctrl.aux1 then
227					axis = "z"
228				end
229			end
230			local rot = obj:get_rotation()
231			rot[axis] = rot[axis] + math.pi/8
232			if rot[axis] > math.pi*2 then
233				rot[axis] = rot[axis] - math.pi*2
234			end
235			obj:set_rotation(rot)
236		end
237	end,
238})
239
240local mover_config = function(itemstack, user, pointed_thing)
241	if not (user and user:is_player()) then
242		return
243	end
244	local name = user:get_player_name()
245	local ctrl = user:get_player_control()
246	local meta = itemstack:get_meta()
247	local dist = 1.0
248	if meta:contains("distance") then
249		dist = meta:get_int("distance")
250	end
251	if ctrl.sneak then
252		dist = dist - 1
253	else
254		dist = dist + 1
255	end
256	meta:set_int("distance", dist)
257	minetest.chat_send_player(user:get_player_name(), S("distance=@1/10", dist*2))
258	return itemstack
259end
260
261minetest.register_tool("testtools:object_mover", {
262	description = S("Object Mover") .."\n"..
263		S("Move pointed object towards or away from you") .."\n"..
264		S("Punch: Move by distance").."\n"..
265		S("Sneak+Punch: Move by negative distance").."\n"..
266		S("Place: Increase distance").."\n"..
267		S("Sneak+Place: Decrease distance"),
268	inventory_image = "testtools_object_mover.png",
269	groups = { testtool = 1, disable_repair = 1 },
270	on_place = mover_config,
271	on_secondary_use = mover_config,
272	on_use = function(itemstack, user, pointed_thing)
273		if pointed_thing.type ~= "object" then
274			return
275		end
276		local obj = pointed_thing.ref
277		if not (user and user:is_player()) then
278			return
279		end
280		local yaw = user:get_look_horizontal()
281		local dir = minetest.yaw_to_dir(yaw)
282		local pos = obj:get_pos()
283		local pitch = user:get_look_vertical()
284		if pitch > 0.25 * math.pi then
285			dir.y = -1
286			dir.x = 0
287			dir.z = 0
288		elseif pitch < -0.25 * math.pi then
289			dir.y = 1
290			dir.x = 0
291			dir.z = 0
292		end
293		local ctrl = user:get_player_control()
294		if ctrl.sneak then
295			dir = vector.multiply(dir, -1)
296		end
297		local meta = itemstack:get_meta()
298		if meta:contains("distance") then
299			local dist = meta:get_int("distance")
300			dir = vector.multiply(dir, dist*0.2)
301		end
302		pos = vector.add(pos, dir)
303		obj:set_pos(pos)
304	end,
305})
306
307
308
309minetest.register_tool("testtools:entity_scaler", {
310	description = S("Entity Visual Scaler") .."\n"..
311		S("Scale visual size of entities") .."\n"..
312		S("Punch: Increase size") .."\n"..
313		S("Sneak+Punch: Decrease scale"),
314	inventory_image = "testtools_entity_scaler.png",
315	groups = { testtool = 1, disable_repair = 1 },
316	on_use = function(itemstack, user, pointed_thing)
317		if pointed_thing.type ~= "object" then
318			return
319		end
320		local obj = pointed_thing.ref
321		if obj:is_player() then
322			-- No player scaling
323			return
324		else
325			local diff = 0.1
326			if user and user:is_player() then
327				local ctrl = user:get_player_control()
328				if ctrl.sneak then
329					diff = -0.1
330				end
331			end
332			local prop = obj:get_properties()
333			if not prop.visual_size then
334				prop.visual_size = { x=1, y=1, z=1 }
335			else
336				prop.visual_size = { x=prop.visual_size.x+diff, y=prop.visual_size.y+diff, z=prop.visual_size.z+diff }
337				if prop.visual_size.x <= 0.1 then
338					prop.visual_size.x = 0.1
339				end
340				if prop.visual_size.y <= 0.1 then
341					prop.visual_size.y = 0.1
342				end
343				if prop.visual_size.z <= 0.1 then
344					prop.visual_size.z = 0.1
345				end
346			end
347			obj:set_properties(prop)
348		end
349	end,
350})
351
352local selections = {}
353local entity_list
354local function get_entity_list()
355	if entity_list then
356		return entity_list
357	end
358	local ents = minetest.registered_entities
359	local list = {}
360	for k,_ in pairs(ents) do
361		table.insert(list, k)
362	end
363	table.sort(list)
364	entity_list = list
365	return entity_list
366end
367minetest.register_tool("testtools:entity_spawner", {
368	description = S("Entity Spawner") .."\n"..
369		S("Spawns entities") .."\n"..
370		S("Punch: Select entity to spawn") .."\n"..
371		S("Place: Spawn selected entity"),
372	inventory_image = "testtools_entity_spawner.png",
373	groups = { testtool = 1, disable_repair = 1 },
374	on_place = function(itemstack, user, pointed_thing)
375		local name = user:get_player_name()
376		if pointed_thing.type == "node" then
377			if selections[name] then
378				local pos = pointed_thing.above
379				minetest.add_entity(pos, get_entity_list()[selections[name]])
380			else
381				minetest.chat_send_player(name, S("Select an entity first (with punch key)!"))
382			end
383		end
384	end,
385	on_use = function(itemstack, user, pointed_thing)
386		if pointed_thing.type == "object" then
387			return
388		end
389		if user and user:is_player() then
390			local list = table.concat(get_entity_list(), ",")
391			local name = user:get_player_name()
392			local sel = selections[name] or ""
393			minetest.show_formspec(name, "testtools:entity_list",
394				"size[9,9]"..
395				"textlist[0,0;9,8;entity_list;"..list..";"..sel..";false]"..
396				"button[0,8;4,1;spawn;Spawn entity]"
397			)
398		end
399	end,
400})
401
402local function prop_to_string(property)
403	if type(property) == "string" then
404		return "\"" .. property .. "\""
405	elseif type(property) == "table" then
406		return tostring(dump(property)):gsub("\n", "")
407	else
408		return tostring(property)
409	end
410end
411
412local property_formspec_data = {}
413local property_formspec_index = {}
414local selected_objects = {}
415local function get_object_properties_form(obj, playername)
416	if not playername then return "" end
417	local props = obj:get_properties()
418	local str = ""
419	property_formspec_data[playername] = {}
420	local proplist = {}
421	for k,_ in pairs(props) do
422		table.insert(proplist, k)
423	end
424	table.sort(proplist)
425	for p=1, #proplist do
426		local k = proplist[p]
427		local v = props[k]
428		local newline = ""
429		newline = k .. " = "
430		newline = newline .. prop_to_string(v)
431		str = str .. F(newline)
432		if p < #proplist then
433			str = str .. ","
434		end
435		table.insert(property_formspec_data[playername], k)
436	end
437	return str
438end
439
440local editor_formspec_selindex = {}
441
442local editor_formspec = function(playername, obj, value, sel)
443	if not value then
444		value = ""
445	end
446	if not sel then
447		sel = ""
448	end
449	local list = get_object_properties_form(obj, playername)
450	local title
451	if obj:is_player() then
452		title = S("Object properties of player “@1”", obj:get_player_name())
453	else
454		local ent = obj:get_luaentity()
455		title = S("Object properties of @1", ent.name)
456	end
457	minetest.show_formspec(playername, "testtools:object_editor",
458		"size[9,9]"..
459		"label[0,0;"..F(title).."]"..
460		"textlist[0,0.5;9,7.5;object_props;"..list..";"..sel..";false]"..
461		"field[0.2,8.75;8,1;value;"..F(S("Value"))..";"..F(value).."]"..
462		"field_close_on_enter[value;false]"..
463		"button[8,8.5;1,1;submit;"..F(S("Submit")).."]"
464	)
465end
466
467minetest.register_tool("testtools:object_editor", {
468	description = S("Object Property Editor") .."\n"..
469		S("Edit properties of objects") .."\n"..
470		S("Punch object: Edit object") .."\n"..
471		S("Punch air: Edit yourself"),
472	inventory_image = "testtools_object_editor.png",
473	groups = { testtool = 1, disable_repair = 1 },
474	on_use = function(itemstack, user, pointed_thing)
475		if user and user:is_player() then
476			local name = user:get_player_name()
477
478			if pointed_thing.type == "object" then
479				selected_objects[name] = pointed_thing.ref
480			elseif pointed_thing.type == "nothing" then
481				-- Use on yourself if pointing nothing
482				selected_objects[name] = user
483			else
484				-- Unsupported pointed thing
485				return
486			end
487
488			local sel = editor_formspec_selindex[name]
489			local val
490			if selected_objects[name] and selected_objects[name]:get_properties() then
491				local props = selected_objects[name]:get_properties()
492				local keys = property_formspec_data[name]
493				if property_formspec_index[name] and props then
494					local key = keys[property_formspec_index[name]]
495					val = prop_to_string(props[key])
496				end
497			end
498
499			editor_formspec(name, selected_objects[name], val, sel)
500		end
501	end,
502})
503
504local ent_parent = {}
505local ent_child = {}
506local DEFAULT_ATTACH_OFFSET_Y = 11
507
508local attacher_config = function(itemstack, user, pointed_thing)
509	if not (user and user:is_player()) then
510		return
511	end
512	if pointed_thing.type == "object" then
513		return
514	end
515	local name = user:get_player_name()
516	local ctrl = user:get_player_control()
517	local meta = itemstack:get_meta()
518	if ctrl.aux1 then
519		local rot_x = meta:get_float("rot_x")
520		if ctrl.sneak then
521			rot_x = rot_x - math.pi/8
522		else
523			rot_x = rot_x + math.pi/8
524		end
525		if rot_x > 6.2 then
526			rot_x = 0
527		elseif rot_x < 0 then
528			rot_x = math.pi * (15/8)
529		end
530		minetest.chat_send_player(name, S("rotation=@1", minetest.pos_to_string({x=rot_x,y=0,z=0})))
531		meta:set_float("rot_x", rot_x)
532	else
533		local pos_y
534		if meta:contains("pos_y") then
535			pos_y = meta:get_int("pos_y")
536		else
537			pos_y = DEFAULT_ATTACH_OFFSET_Y
538		end
539		if ctrl.sneak then
540			pos_y = pos_y - 1
541		else
542			pos_y = pos_y + 1
543		end
544		minetest.chat_send_player(name, S("position=@1", minetest.pos_to_string({x=0,y=pos_y,z=0})))
545		meta:set_int("pos_y", pos_y)
546	end
547	return itemstack
548end
549
550minetest.register_tool("testtools:object_attacher", {
551	description = S("Object Attacher") .."\n"..
552		S("Attach object to another") .."\n"..
553		S("Punch objects to first select parent object, then the child object to attach") .."\n"..
554		S("Punch air to select yourself") .."\n"..
555		S("Place: Incease attachment Y offset") .."\n"..
556		S("Sneak+Place: Decease attachment Y offset") .."\n"..
557		S("Aux1+Place: Incease attachment rotation") .."\n"..
558		S("Aux1+Sneak+Place: Decrease attachment rotation"),
559	inventory_image = "testtools_object_attacher.png",
560	groups = { testtool = 1, disable_repair = 1 },
561	on_place = attacher_config,
562	on_secondary_use = attacher_config,
563	on_use = function(itemstack, user, pointed_thing)
564		if user and user:is_player() then
565			local name = user:get_player_name()
566			local selected_object
567			if pointed_thing.type == "object" then
568				selected_object = pointed_thing.ref
569			elseif pointed_thing.type == "nothing" then
570				selected_object = user
571			else
572				return
573			end
574			local ctrl = user:get_player_control()
575			if ctrl.sneak then
576				if selected_object:get_attach() then
577					selected_object:set_detach()
578					minetest.chat_send_player(name, S("Object detached!"))
579				else
580					minetest.chat_send_player(name, S("Object is not attached!"))
581				end
582				return
583			end
584			local parent = ent_parent[name]
585			local child = ent_child[name]
586			local ename = S("<unknown>")
587			if not parent then
588				parent = selected_object
589				ent_parent[name] = parent
590			elseif not child then
591				child = selected_object
592				ent_child[name] = child
593			end
594			local entity = selected_object:get_luaentity()
595			if entity then
596				ename = entity.name
597			elseif selected_object:is_player() then
598				ename = selected_object:get_player_name()
599			end
600			if selected_object == parent then
601				minetest.chat_send_player(name, S("Parent object selected: @1", ename))
602			elseif selected_object == child then
603				minetest.chat_send_player(name, S("Child object selected: @1", ename))
604			end
605			if parent and child then
606				if parent == child then
607					minetest.chat_send_player(name, S("Can't attach an object to itself!"))
608					ent_parent[name] = nil
609					ent_child[name] = nil
610					return
611				end
612				local meta = itemstack:get_meta()
613				local y
614				if meta:contains("pos_y") then
615					y = meta:get_int("pos_y")
616				else
617					y = DEFAULT_ATTACH_OFFSET_Y
618				end
619				local rx = meta:get_float("rot_x") or 0
620				local offset = {x=0,y=y,z=0}
621				local angle = {x=rx,y=0,z=0}
622				child:set_attach(parent, "", offset, angle)
623				local check_parent = child:get_attach()
624				if check_parent then
625					minetest.chat_send_player(name, S("Object attached! position=@1, rotation=@2",
626						minetest.pos_to_string(offset), minetest.pos_to_string(angle)))
627				else
628					minetest.chat_send_player(name, S("Attachment failed!"))
629				end
630				ent_parent[name] = nil
631				ent_child[name] = nil
632			end
633		end
634	end,
635})
636
637-- Use loadstring to parse param as a Lua value
638local function use_loadstring(param, player)
639	-- For security reasons, require 'server' priv, just in case
640	-- someone is actually crazy enough to run this on a public server.
641	local privs = minetest.get_player_privs(player:get_player_name())
642	if not privs.server then
643		return false, "You need 'server' privilege to change object properties!"
644	end
645	if not param then
646		return false, "Failed: parameter is nil"
647	end
648	--[[ DANGER ZONE ]]
649	-- Interpret string as Lua value
650	local func, errormsg = loadstring("return (" .. param .. ")")
651	if not func then
652		return false, "Failed: " .. errormsg
653	end
654
655	-- Apply sandbox here using setfenv
656	setfenv(func, {})
657
658	-- Run it
659	local good, errOrResult = pcall(func)
660	if not good then
661		-- A Lua error was thrown
662		return false, "Failed: " .. errOrResult
663	end
664
665	-- errOrResult will be the value
666	return true, errOrResult
667end
668
669minetest.register_on_player_receive_fields(function(player, formname, fields)
670	if not (player and player:is_player()) then
671		return
672	end
673	if formname == "testtools:entity_list" then
674		local name = player:get_player_name()
675		if fields.entity_list then
676			local expl = minetest.explode_textlist_event(fields.entity_list)
677			if expl.type == "DCL" then
678				local pos = vector.add(player:get_pos(), {x=0,y=1,z=0})
679				selections[name] = expl.index
680				minetest.add_entity(pos, get_entity_list()[expl.index])
681				return
682			elseif expl.type == "CHG" then
683				selections[name] = expl.index
684				return
685			end
686		elseif fields.spawn and selections[name] then
687			local pos = vector.add(player:get_pos(), {x=0,y=1,z=0})
688			minetest.add_entity(pos, get_entity_list()[selections[name]])
689			return
690		end
691	elseif formname == "testtools:object_editor" then
692		local name = player:get_player_name()
693		if fields.object_props then
694			local expl = minetest.explode_textlist_event(fields.object_props)
695			if expl.type == "DCL" or expl.type == "CHG" then
696				property_formspec_index[name] = expl.index
697
698				local props = selected_objects[name]:get_properties()
699				local keys = property_formspec_data[name]
700				if (not property_formspec_index[name]) or (not props) then
701					return
702				end
703				local key = keys[property_formspec_index[name]]
704				editor_formspec_selindex[name] = expl.index
705				editor_formspec(name, selected_objects[name], prop_to_string(props[key]), expl.index)
706				return
707			end
708		end
709		if fields.key_enter_field == "value" or fields.submit then
710			local props = selected_objects[name]:get_properties()
711			local keys = property_formspec_data[name]
712			if (not property_formspec_index[name]) or (not props) then
713				return
714			end
715			local key = keys[property_formspec_index[name]]
716			if not key then
717				return
718			end
719			local success, str = use_loadstring(fields.value, player)
720			if success then
721				props[key] = str
722			else
723				minetest.chat_send_player(name, str)
724				return
725			end
726			selected_objects[name]:set_properties(props)
727			local sel = editor_formspec_selindex[name]
728			editor_formspec(name, selected_objects[name], prop_to_string(props[key]), sel)
729			return
730		end
731	end
732end)
733