Skip to content
Snippets Groups Projects
Select Git revision
  • 9b70f7e285b8cc71b24bc2890d1259da775798cd
  • main default protected
  • touch-move
  • notification
  • better_error
  • waiting-room
6 results

QueueTab.vue

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    train.lua 18.35 KiB
    
    local advtrains_present = minetest.get_modpath("advtrains") and true or false
    local last_set_by = {}
    
    local find_neighbor_blocks -- defined later
    local update_neighbors --defined later
    local recalculate_line_to -- defined later
    local TRAVERSER_LIMIT = 1000
    
    local update_formspec = function(meta)
    	local line = meta:get_string("line")
    	local station = meta:get_string("station")
    	local index = meta:get_string("index")
    	local color = meta:get_string("color") or ""
    	local rail_pos = meta:get_string("rail_pos") or ""
    
    	local rail_btns = ""
    	if advtrains_present then
    		if rail_pos == "" then
    			rail_btns = "button_exit[4,3.5;2.5,1;set_rail_pos;Set rail]"
    		else
    			rail_btns = "button_exit[4,3.5;2.5,1;set_rail_pos;" .. rail_pos .. "]" ..
    				"button[6.5,3.5;1.5,1;clear_rail_pos;Clear rail]"
    		end
    	end
    
    	local prv = meta:get_string("prv_pos")
    	local path = meta:get_string("linepath_from_prv")
    	local nxt = meta:get_string("nxt_pos")
    
    	meta:set_string("infotext", "Train: Line=" .. line .. ", Station=" .. station ..
    		(prv ~= "" and (", prv="..prv) or "") ..
    		(path ~= "" and " (found line)" or "") ..
    		(nxt ~= "" and (", nxt="..nxt) or "") ..
    		(line ~= "" and prv == "" and nxt == "" and (", no neighbors found") or ""))
    
    	meta:set_string("formspec", "size[8,4;]" ..
    		-- col 1
    		"field[0,1;4,1;line;Line;" .. line .. "]" ..
    		"button_exit[4,1;4,1;save;Save]" ..
    
    		-- col 2
    		"field[0,2.5;4,1;station;Station;" .. station .. "]" ..
    		"field[4,2.5;4,1;index;Index;" .. index .. "]" ..
    
    		-- col 3
    		"field[0,3.5;4,1;color;Color;" .. color .. "]" ..
    		rail_btns
    	)
    
    end
    
    
    minetest.register_node("mapserver:train", {
    	description = "Mapserver Train",
    	tiles = {
    		"mapserver_train.png"
    	},
    	groups = {cracky=3,oddly_breakable_by_hand=3},
    	sounds = moditems.sound_glass(),
    	can_dig = mapserver.can_interact,
    
    	after_place_node = function(pos, placer, itemstack, pointed_thing)
    		local meta = minetest.get_meta(pos)
    
    		local last_index = 0
    		local last_line = ""
    		local last_color = ""
    
    		if minetest.is_player(placer) then
    			local name = placer:get_player_name()
    			if name ~= nil then
    				name = string.lower(name)
    				if last_set_by[name] ~= nil then
    					last_index = last_set_by[name].index + 5
    					last_line = last_set_by[name].line
    					last_color = last_set_by[name].color
    				else
    					last_set_by[name] = {}
    				end
    
    				last_set_by[name].index = last_index
    				last_set_by[name].line = last_line
    				last_set_by[name].color = last_color
    			end
    		end
    
    		meta:set_string("station", "")
    		meta:set_string("line", last_line)
    		meta:set_int("index", last_index)
    		meta:set_string("color", last_color)
    		meta:set_string("rail_pos", "")
    
    		update_neighbors(pos, meta, minetest.is_player(placer) and placer:get_player_name() or nil)
    
    		return mapserver.after_place_node(pos, placer, itemstack, pointed_thing)
    	end,
    
    	after_dig_node = function(pos, oldnode, oldmetadata, player)
    		local fake_meta = minetest.get_meta(pos)
    
    		-- TODO: why doesn't this work properly?
    
    		update_neighbors(pos, fake_meta, player:get_player_name())
    	end,
    
    	on_receive_fields = function(pos, formname, fields, sender)
    
    		if not mapserver.can_interact(pos, sender) then
    			return
    		end
    
    		local meta = minetest.get_meta(pos)
    		local name = sender:get_player_name()
    		local lname = string.lower(name)
    
    		if fields.save then
    			if last_set_by[lname] == nil then
    				last_set_by[lname] = {}
    			end
    
    			local index = tonumber(fields.index)
    			if index ~= nil then
    				index = index
    			end
    
    			meta:set_string("color", fields.color)
    			meta:set_string("line", fields.line)
    			meta:set_string("station", fields.station)
    			meta:set_int("index", index)
    
    			last_set_by[lname].color = fields.color
    			last_set_by[lname].line = fields.line
    			last_set_by[lname].station = fields.station
    			last_set_by[lname].index = index
    
    			update_neighbors(pos, meta, name)
    
    		elseif fields.clear_rail_pos then
    			meta:set_string("rail_pos", "")
    			update_neighbors(pos, meta, name)
    
    		elseif fields.set_rail_pos then
    			minetest.chat_send_player(name, "Please punch the nearest rail this train line follows.")
    			if last_set_by[lname] == nil then
    				last_set_by[lname] = {}
    			end
    			last_set_by[lname].waiting_for_rail = pos
    		end
    	end
    })
    
    minetest.register_on_punchnode(function(pos, node, sender, pointed_thing)
    	local name = sender:get_player_name()
    	local lname = string.lower(name)
    	local blockpos = nil
    	if last_set_by[lname] ~= nil and
    		last_set_by[lname].waiting_for_rail ~= nil then
    
    		blockpos = last_set_by[lname].waiting_for_rail
    	else
    		return
    	end
    	if not mapserver.can_interact(blockpos, sender) then
    		return
    	end
    
    	if blockpos and advtrains_present then
    		if vector.distance(pos, blockpos) <= 20 then
    			local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
    			if node_ok then
    				local meta = minetest.get_meta(blockpos)
    				meta:set_string("rail_pos", minetest.pos_to_string(pos))
    				update_neighbors(blockpos, meta, name)
    			else
    				minetest.chat_send_player(name, "This is not a rail! Aborted.")
    			end
    		else
    			minetest.chat_send_player(name, "Node is too far away. Aborted.")
    		end
    		last_set_by[lname].waiting_for_rail = nil
    	end
    end)
    
    if mapserver.enable_crafting then
    	minetest.register_craft({
    	    output = 'mapserver:train',
    	    recipe = {
    				{"", moditems.steel_ingot, ""},
    				{moditems.paper, moditems.goldblock, moditems.paper},
    				{"", moditems.glass, ""}
    			}
    	})
    end
    
    
    update_neighbors = function(pos, meta, name)
    	if meta == nil then
    		meta = minetest.get_meta(pos)
    	end
    	local line = meta:get_string("line")
    	local index = tonumber(meta:get_string("index"))
    	local rail_pos = meta:get_string("rail_pos")
    
    	-- if anything critical changed (pos/line/index) virtually remove us
    	local prv = minetest.string_to_pos(meta:get_string("prv_pos"))
    	local nxt = minetest.string_to_pos(meta:get_string("nxt_pos"))
    	local prv_meta = prv ~= nil and minetest.get_meta(prv) or nil
    	local nxt_meta = nxt ~= nil and minetest.get_meta(nxt) or nil
    
    	if prv ~= nil and prv_meta:get_string("line") ~= line and
    		nxt ~= nil and nxt_meta:get_string("line") ~= line then
    		if prv ~= nil and nxt == nil then
    			-- loose end
    			prv_meta:set_string("nxt_pos", "")
    			prv_meta:set_string("nxt_index", "")
    			prv_meta:set_string("nxt_rail_pos", "")
    		elseif prv == nil and nxt ~= nil then
    			-- loose end
    			nxt_meta:set_string("prv_pos", "")
    			nxt_meta:set_string("prv_index", "")
    			nxt_meta:set_string("prv_rail_pos", "")
    
    			nxt_meta:set_string("linepath_from_prv", "")
    		else
    			-- we were in the middle
    			prv_meta:set_string("nxt_pos", nxt)
    			prv_meta:set_string("nxt_index", meta:get_string("nxt_index"))
    			prv_meta:set_string("nxt_rail_pos", meta:get_string("nxt_rail_pos"))
    
    			nxt_meta:set_string("prv_pos", prv)
    			nxt_meta:set_string("prv_index", meta:get_string("prv_index"))
    			nxt_meta:set_string("prv_rail_pos", meta:get_string("prv_rail_pos"))
    
    			recalculate_line_to(prv, nxt, prv_meta, nxt_meta)
    		end
    
    		for _,m in ipairs({prv_meta, nxt_meta}) do
    			if m ~= nil then
    				update_formspec(m)
    			end
    		end
    
    		-- remove meta from self
    		meta:set_string("prv_pos", "")
    		meta:set_string("prv_index", "")
    		meta:set_string("prv_rail_pos", "")
    
    		meta:set_string("nxt_pos", "")
    		meta:set_string("nxt_index", "")
    		meta:set_string("nxt_rail_pos", "")
    
    		meta:set_string("linepath_from_prv", "")
    	end
    
    	if line == "" then
    		update_formspec(meta)
    		return
    	end
    
    	-- update or add us
    	-- repurposing prv, prv_meta etc. vars
    	local neighbors = find_neighbor_blocks(pos, meta, name)
    	prv = neighbors[1]
    	nxt = neighbors[2]
    	prv_meta = prv ~= nil and minetest.get_meta(prv.pos) or nil
    	nxt_meta = nxt ~= nil and minetest.get_meta(nxt.pos) or nil
    	-- if index or rail pos changed, recalculate line path
    	if prv ~= nil then
    		local old_nxt_pos = prv_meta:get_string("nxt_pos")
    		local old_nxt_index = tonumber(prv_meta:get_string("nxt_index"))
    		local old_nxt_rail_pos = prv_meta:get_string("nxt_rail_pos")
    
    		-- if old info on prev does not match us, set correct
    		if old_nxt_pos ~= (nxt == nil and "" or nxt.pos) then
    			if old_nxt_pos == pos then
    				-- phew, it's just us
    			elseif nxt ~= nil and old_nxt_pos == nxt.pos then
    				-- okay we are just freshly added
    				-- update the previous block
    				prv_meta:set_string("nxt_pos", minetest.pos_to_string(pos))
    			else
    				-- there are more nodes we don't know about!
    			end
    		end
    		if old_nxt_index ~= index then
    			-- index changed! since our position is still unchanged
    			-- (otherwise removing/re-adding above would have happened instead)
    			-- we just need to update the info, without linepath recalculation
    			prv_meta:set_int("nxt_index", index)
    		end
    		if old_nxt_rail_pos ~= rail_pos then
    			-- rail pos changed! definitely need linepath recalculation
    			prv_meta:set_string("nxt_rail_pos", rail_pos)
    			meta:set_string("linepath_from_prv", "")
    		end
    
    		meta:set_string("prv_pos", minetest.pos_to_string(prv.pos))
    		meta:set_int("prv_index", prv.index)
    		meta:set_string("prv_rail_pos", prv.rail_pos)
    	end
    	if nxt ~= nil then
    		local old_prv_pos = nxt_meta:get_string("prv_pos")
    		local old_prv_index = tonumber(nxt_meta:get_string("prv_index"))
    		local old_prv_rail_pos = nxt_meta:get_string("prv_rail_pos")
    
    		-- if old info on next does not match us, set correct
    		if old_prv_pos ~= (prv == nil and "" or prv.pos) then
    			if old_prv_pos == pos then
    				-- phew, it's just us
    			elseif prv ~= nil and old_prv_pos == prv.pos then
    				-- okay we are just freshly added
    				-- update the previous block
    				nxt_meta:set_string("prv_pos", minetest.pos_to_string(pos))
    				nxt_meta:set_string("linepath_from_prv", "")
    			else
    				-- there are more nodes we don't know about!
    			end
    		end
    		if old_prv_index ~= index then
    			-- index changed! since our position is still unchanged
    			-- (otherwise removing/re-adding above would have happened instead)
    			-- we just need to update the info, without linepath recalculation
    			nxt_meta:set_int("prv_index", index)
    		end
    		if old_prv_rail_pos ~= rail_pos then
    			-- rail pos changed! definitely need linepath recalculation
    			nxt_meta:set_string("prv_rail_pos", rail_pos)
    			nxt_meta:set_string("linepath_from_prv", "")
    		end
    
    		meta:set_string("nxt_pos", minetest.pos_to_string(nxt.pos))
    		meta:set_int("nxt_index", nxt.index)
    		meta:set_string("nxt_rail_pos", nxt.rail_pos)
    	end
    
    	if rail_pos ~= "" then
    		if prv ~= nil and prv.rail_pos ~= "" then
    			local line = recalculate_line_to(prv.pos, pos, prv_meta, meta)
    			if name then
    				if #line > 0 then
    					minetest.chat_send_player(name, "Found line from prv ("..tonumber(#line).."): "..table.concat(line, "->"))
    				else
    					minetest.chat_send_player(name, "Did not find line from prv.")
    				end
    			end
    		end
    		if nxt ~= nil and nxt.rail_pos ~= "" then
    			local line = recalculate_line_to(pos, nxt.pos, meta, nxt_meta)
    			if name then
    				if #line > 0 then
    					minetest.chat_send_player(name, "Found line to nxt ("..tonumber(#line).."): "..table.concat(line, "->"))
    				else
    					minetest.chat_send_player(name, "Did not find line to nxt.")
    				end
    			end
    		end
    	end
    
    	for _,m in ipairs({prv_meta, nxt_meta}) do
    		if m ~= nil then
    			update_formspec(m)
    		end
    	end
    	update_formspec(meta)
    end
    
    local nroot = function(root, num)
    	return num^(1/root)
    end
    
    -- Searching an area for nodes is expensive.
    -- Minetest limits the amount to 4,096,000 nodes.
    -- Because there is not a good way to form one cuboid to fit all major long-distance usecases
    -- and this will not be frequently executed on a server (only every time a player manually
    -- sets or updates a train map block) we take all we can with 3 separate ranges:
    -- - One layer for most applications in long, flat stretches, allowing for 3 nodes of up/down
    --   deviation, maxes out on 381 x and z deviation.
    -- - One smaller, but higher cuboid on top and bottom of it each, stretching 123 in every
    --   x and z direction and 67 up/down
    -- This should be very luxurious and prove enough for almost everything.
    local max_nodes = 4096000
    local cuboid_width_for_height = function(height)
    	return math.floor(math.sqrt(max_nodes / height))
    end
    local span_rectangle = function(pos, radius, height, v_offset, v_invert)
    	local v_dir = v_invert and -1 or 1
    	return { vector.add(pos, vector.multiply(vector.new(-radius, v_offset, -radius), v_dir)),
    			 vector.add(pos, vector.multiply(vector.new(radius, height+v_offset, radius), v_dir)) }
    end
    local halve_area = function(length)
    	return math.floor((length-1) / 2)
    end
    local twocube_length = math.floor(nroot(3, max_nodes*2))
    local flat_height = 7
    local flat_length = cuboid_width_for_height(flat_height)
    local cuboid_height = math.floor(twocube_length/3)
    local cuboid_length = cuboid_width_for_height(cuboid_height)
    local area_from_offset = function(pos, offset)
    	return {vector.subtract(pos, offset), vector.add(pos, offset)}
    end
    local get_volume = function(span)
    	local diff = vector.subtract(span[2], span[1])
    	return (math.abs(diff.x)+1) * (math.abs(diff.y)+1) * (math.abs(diff.z)+1)
    end
    
    find_neighbor_blocks = function(pos, meta, name)
    	if meta == nil then
    		meta = minetest.get_meta(pos)
    	end
    	local line = meta:get_string("line")
    	local index = tonumber(meta:get_string("index"))
    	local rail_pos = meta:get_string("rail_pos")
    
    	-- the offsets are chosen so that the resulting area is just under the maximum allowable size
    	local areas = {
    		flat = area_from_offset(pos, vector.new(halve_area(flat_length), halve_area(flat_height), halve_area(flat_length))),
    		upper_half = span_rectangle(pos, halve_area(cuboid_length), cuboid_height-1, halve_area(flat_height)+1),
    		lower_half = span_rectangle(pos, halve_area(cuboid_length), cuboid_height-1, halve_area(flat_height)+1, true)
    	}
    	local blocks = {}
    	for i,span in pairs(areas) do
    		if get_volume(span) > max_nodes then
    			minetest.chat_send_player(name, "Internal Error searching for nearby nodes: Invalid span "..i.." between "..minetest.pos_to_string(span[1]).." and "..minetest.pos_to_string(span[2]).." (volume of "..tostring(get_volume(span))..")")
    			minetest.log("error", "[mapserver_mod][trainlines] Internal Error searching for nearby nodes: Invalid span "..i.." between "..minetest.pos_to_string(span[1]).." and "..minetest.pos_to_string(span[2]).." (volume of "..tostring(get_volume(span))..")")
    			return {}
    		end
    		blocks[i] = minetest.find_nodes_in_area(span[1], span[2], "mapserver:train")
    	end
    	local prv = nil
    	local nxt = nil
    	local meta = nil
    
    	for _,span in pairs(blocks) do
    		for _,p in pairs(span) do
    			meta = minetest.get_meta(p)
    			if meta:get_string("line") == line then
    				local idx = tonumber(meta:get_string("index"))
    				if idx < index and
    					(prv == nil or idx > prv.index) then
    						prv = {
    							pos = p,
    							index = idx,
    							rail_pos = meta:get_string("rail_pos")
    						}
    				end
    				if idx > index and
    					(nxt == nil or idx < nxt.index) then
    						nxt = {
    							pos = p,
    							index = idx,
    							rail_pos = meta:get_string("rail_pos")
    						}
    				end
    			end
    		end
    	end
    
    	return {prv, nxt}
    end
    
    local clone = nil
    clone = function(tbl, n)
    	local out = {}
    	local i,v = next(tbl, nil)
    	while i do
    		if type(v) == "table" then
    			out[i] = clone(v, (n or 0)+1)
    		else
    			out[i] = v
    		end
    		i,v = next(tbl, i)
    	end
    	return out
    end
    
    recalculate_line_to = function(pos_a, pos_b, meta_a, meta_b)
    	if meta_a == nil then
    		meta_a = minetest.get_meta(pos_a)
    	end
    	if meta_b == nil then
    		meta_b = minetest.get_meta(pos_b)
    	end
    	local line = {}
    	local rail_pos_a = minetest.string_to_pos(meta_a:get_string("rail_pos"))
    	local rail_pos_b = minetest.string_to_pos(meta_b:get_string("rail_pos"))
    	local node_ok_a, conns_a, rhe_a, node_ok_b, conns_b, rhe_b
    	if rail_pos_a then
    		node_ok_a, conns_a, rhe_a = advtrains.get_rail_info_at(rail_pos_a, advtrains.all_tracktypes)
    		if rail_pos_b then
    			node_ok_b, conns_b, rhe_b = advtrains.get_rail_info_at(rail_pos_b, advtrains.all_tracktypes)
    		end
    	end
    	if not node_ok_a or not node_ok_b then
    		table.insert(line, node_ok_a and minetest.pos_to_string(rail_pos_a) or minetest.pos_to_string(pos_a))
    	else
    		-- depth first search for rail_pos_b,
    		-- vector.distance(step, rail_pos_b) is score
    
    		-- keep track of all visited positions to avoid going in circles
    		local visited_nodes = {}
    		-- heads of search positions: {pos=<pos>, score=<cached score>, steps=<nth node tried>, line=<line until pos>}
    		local progress = {}
    
    		-- put starting rail in, for every direction
    		for connid, conn in ipairs(conns_a) do
    			table.insert(progress, {
    				pos = rail_pos_a,
    				conns = conns_a,
    				connid = connid,
    				steps = 0,
    				score = vector.distance(rail_pos_a, rail_pos_b),
    				line = {}
    			})
    		end
    
    		while next(progress, nil) do
    			local min_idx = nil
    			local min_item = nil
    			-- try the node closest to the destination
    			for i,v in pairs(progress) do
    				if v.steps < TRAVERSER_LIMIT and
    					(min_item == nil or v.score < min_item.score) then
    					min_idx = i
    					min_item = v
    				end
    			end
    
    			-- check the adjacent rail
    			local adj_pos, adj_connid, conn_idx, nextrail_y, next_conns = advtrains.get_adjacent_rail(min_item.pos, min_item.conns, min_item.connid, advtrains.all_tracktypes)
    			if not adj_pos then
    				-- there is no rail, end-of-track
    				progress[min_idx] = nil
    			elseif visited_nodes[minetest.pos_to_string(adj_pos)..adj_connid] ~= nil then
    				-- already been here in this direction, no use repeating same steps
    				progress[min_idx] = nil
    			elseif minetest.pos_to_string(adj_pos) == minetest.pos_to_string(rail_pos_b) then
    				-- found destination!
    				-- set line and break loop
    				line = min_item.line
    				table.insert(line, minetest.pos_to_string(rail_pos_b))
    				break
    			else
    				-- remember we did this one to prevent circles
    				visited_nodes[minetest.pos_to_string(adj_pos)..adj_connid] = true
    
    				if min_item.steps > TRAVERSER_LIMIT then
    					print("went over traverser limit! "..minetest.pos_to_string(rail_pos_a).." → "..minetest.pos_to_string(adj_pos))
    				else
    					local inconn = next_conns[adj_connid]
    					-- query the next conns
    					local deg45 = AT_CMAX/8
    					for nconnid, nconn in ipairs(next_conns) do
    						local normed = (nconn.c-inconn.c)%AT_CMAX
    						-- only accept conns that turn 90deg at most
    						if normed >= deg45 and normed <= AT_CMAX-deg45 then
    							local line = clone(min_item.line)
    							if nconn.c ~= inconn.c then
    								table.insert(line, minetest.pos_to_string(adj_pos))
    							end
    							table.insert(progress, {
    								pos = adj_pos,
    								conns = next_conns,
    								connid = nconnid,
    								steps = min_item.steps + 1,
    								score = vector.distance(adj_pos, rail_pos_b),
    								line = line
    							})
    						end
    					end
    				end
    				-- we are done with this item
    				progress[min_idx] = nil
    			end
    		end
    	end
    	meta_b:set_string("linepath_from_prv", table.concat(line, ";"))
    	return line
    end