--[[ Minecart ======== Copyright (C) 2019-2021 Joachim Stolberg MIT See license.txt for more information ]]-- -- for lazy programmers local M = minetest.get_meta local P2S = function(pos) if pos then return minetest.pos_to_string(pos) end end local P2H = minetest.hash_node_position local get_node_lvm = minecart.get_node_lvm local MAX_SPEED = 8 local SLOWDOWN = 0.3 local MAX_NODES = 100 --waypoint = { -- dot = travel direction, -- pos = destination pos, -- speed = 10 times the section speed (as int), -- limit = 10 times the speed limit (as int), --} -- -- waypoints = {facedir = waypoint,...} local tWaypoints = {} -- {pos_hash = waypoints, ...} local tRailsPower = { ["carts:rail"] = 0, ["carts:powerrail"] = 1, ["minecart:rail"] = 0, ["minecart:powerrail"] = 1, ["carts:brakerail"] = 0, } -- Real rails from the mod carts local tRails = { ["carts:rail"] = true, ["carts:powerrail"] = true, ["carts:brakerail"] = true, ["minecart:rail"] = true, ["minecart:powerrail"] = true, } -- Rails plus node carts. Used to find waypoints. Added via add_raillike_nodes local tRailsExt = { ["carts:rail"] = true, ["carts:powerrail"] = true, ["carts:brakerail"] = true, ["minecart:rail"] = true, ["minecart:powerrail"] = true, } local tSigns = { ["minecart:speed1"] = 1, ["minecart:speed2"] = 2, ["minecart:speed4"] = 4, ["minecart:speed8"] = 8, } -- Real rails from the mod carts local lRails = {"carts:rail", "carts:powerrail", "carts:brakerail", "minecart:rail", "minecart:powerrail"} -- Rails plus node carts used to find waypoints, , added via add_raillike_nodes local lRailsExt = {"carts:rail", "carts:powerrail", "carts:brakerail", "minecart:rail", "minecart:powerrail"} minecart.MAX_SPEED = MAX_SPEED minecart.lRails = lRails minecart.tRails = tRails minecart.tRailsExt = tRailsExt minecart.lRailsExt = lRailsExt local Dot2Dir = {} local Dir2Dot = {} local Facedir2Dir = {[0] = {x= 0, y=0, z= 1}, {x= 1, y=0, z= 0}, {x= 0, y=0, z=-1}, {x=-1, y=0, z= 0}, {x= 0, y=-1, z= 0}, {x= 0, y=1, z= 0}, } local flip = { [0] = 2, [1] = 3, [2] = 0, [3] = 1, [4] = 5, [5] = 4, } -- facedir = math.floor(dot / 4) -- y = (dot % 4) - 1 -- Create helper tables for facedir = 0,3 do for y = -1,1 do local dot = 1 + facedir * 4 + y local dir = vector.new(Facedir2Dir[facedir]) dir.y = y Dot2Dir[dot] = dir Dir2Dot[P2H(dir)] = dot end end local function dot2dir(dot) return vector.new(Dot2Dir[dot]) end local function facedir2dir(fd) return vector.new(Facedir2Dir[fd]) end minecart.dot2dir = dot2dir minecart.facedir2dir = facedir2dir ------------------------------------------------------------------------------- -- waypoint metadata ------------------------------------------------------------------------------- local function has_metadata(pos) return M(pos):contains("waypoints") end local function get_metadata(pos) local hash = P2H(pos) if tWaypoints[hash] then return tWaypoints[hash] end local s = M(pos):get_string("waypoints") if s ~= "" then tWaypoints[hash] = minetest.deserialize(s) return tWaypoints[hash] end end local function get_oldmetadata(meta) local s = meta:get_string("waypoints") if s ~= "" then return minetest.deserialize(s) end end local function set_metadata(pos, t) local hash = P2H(pos) tWaypoints[hash] = t local s = minetest.serialize(t) M(pos):set_string("waypoints", s) -- visualization local name = get_node_lvm(pos).name if name == "carts:rail" then minetest.swap_node(pos, {name = "minecart:rail"}) elseif name == "carts:powerrail" then minetest.swap_node(pos, {name = "minecart:powerrail"}) end end local function del_metadata(pos) local hash = P2H(pos) tWaypoints[hash] = nil local meta = M(pos) if meta:contains("waypoints") then meta:set_string("waypoints", "") -- visualization local name = get_node_lvm(pos).name if name == "minecart:rail" then minetest.swap_node(pos, {name = "carts:rail"}) elseif name == "minecart:powerrail" then minetest.swap_node(pos, {name = "carts:powerrail"}) end end end ------------------------------------------------------------------------------- -- find_next_waypoint ------------------------------------------------------------------------------- local function check_right(pos, facedir) local fdr = (facedir + 1) % 4 -- right local new_pos = vector.add(pos, facedir2dir(fdr)) local name = get_node_lvm(new_pos).name if tRailsExt[name] or tSigns[name] then return true end new_pos.y = new_pos.y - 1 if tRailsExt[get_node_lvm(new_pos).name] then return true end end local function check_left(pos, facedir) local fdl = (facedir + 3) % 4 -- left local new_pos = vector.add(pos, facedir2dir(fdl)) local name = get_node_lvm(new_pos).name if tRailsExt[name] or tSigns[name] then return true end new_pos.y = new_pos.y - 1 if tRailsExt[get_node_lvm(new_pos).name] then return true end end local function get_next_pos(pos, facedir, y) local new_pos = vector.add(pos, facedir2dir(facedir)) new_pos.y = new_pos.y + y local name = get_node_lvm(new_pos).name return tRailsExt[name] ~= nil, new_pos, tRailsPower[name] or 0 end local function is_ramp(pos) return tRailsExt[get_node_lvm({x = pos.x, y = pos.y + 1, z = pos.z}).name] ~= nil end -- Check also the next position to detect a ramp local function slope_detection(pos, facedir) local is_rail, new_pos = get_next_pos(pos, facedir, 0) if not is_rail then return is_ramp(new_pos) end end local function find_next_waypoint(pos, facedir, y) local cnt = 0 local name = get_node_lvm(pos).name local speed = tRailsPower[name] or 0 local is_rail, new_pos, _speed while cnt < MAX_NODES do is_rail, new_pos, _speed = get_next_pos(pos, facedir, y) speed = speed + _speed if not is_rail then return pos, y == 0 and is_ramp(new_pos), speed end if y == 0 then -- no slope if check_right(new_pos, facedir) then return new_pos, slope_detection(new_pos, facedir), speed elseif check_left(new_pos, facedir) then return new_pos, slope_detection(new_pos, facedir), speed end end pos = new_pos cnt = cnt + 1 end return new_pos, false, speed end ------------------------------------------------------------------------------- -- find_all_next_waypoints ------------------------------------------------------------------------------- local function check_front_up_down(pos, facedir) local new_pos = vector.add(pos, facedir2dir(facedir)) if tRailsExt[get_node_lvm(new_pos).name] then return 0 end new_pos.y = new_pos.y - 1 if tRailsExt[get_node_lvm(new_pos).name] then return -1 end new_pos.y = new_pos.y + 2 if tRailsExt[get_node_lvm(new_pos).name] then return 1 end end -- Search for rails in all 4 directions local function find_all_rails_nearby(pos) --print("find_all_rails_nearby") local tbl = {} for fd = 0, 3 do tbl[#tbl + 1] = check_front_up_down(pos, fd, true) end return tbl end -- Recalc the value based on waypoint length and slope local function recalc_speed(num_pow_rails, pos1, pos2, y) local num_norm_rails = vector.distance(pos1, pos2) - num_pow_rails local ratio, speed if y ~= 0 then num_norm_rails = math.floor(num_norm_rails / 1.41 + 0.5) end if y ~= -1 then if num_pow_rails == 0 then return num_norm_rails * -SLOWDOWN else ratio = math.floor(num_norm_rails / num_pow_rails) ratio = minecart.range(ratio, 0, 11) end else ratio = 3 + num_norm_rails * SLOWDOWN + num_pow_rails end if y == 1 then speed = 7 - ratio elseif y == -1 then speed = 15 - ratio else speed = 11 - ratio end return minecart.range(speed, 0, 8) end local function find_all_next_waypoints(pos) local wp = {} local dots = {} for facedir = 0,3 do local y = check_front_up_down(pos, facedir) if y then local new_pos, is_ramp, speed = find_next_waypoint(pos, facedir, y) --print("find_all_next_waypoints", P2S(new_pos), is_ramp, speed) local dot = 1 + facedir * 4 + y speed = recalc_speed(speed, pos, new_pos, y) * 10 wp[facedir] = {dot = dot, pos = new_pos, speed = speed, is_ramp = is_ramp} end end return wp end ------------------------------------------------------------------------------- -- get_waypoint ------------------------------------------------------------------------------- -- If ramp, stop 0.5 nodes earlier or later local function ramp_correction(pos, wp, facedir) if wp.is_ramp or pos.y < wp.pos.y then -- ramp detection local dir = facedir2dir(facedir) local pos = wp.pos wp.cart_pos = { x = pos.x - dir.x / 2, y = pos.y, z = pos.z - dir.z / 2} elseif pos.y > wp.pos.y then local dir = facedir2dir(facedir) local pos = wp.pos wp.cart_pos = { x = pos.x + dir.x / 2, y = pos.y, z = pos.z + dir.z / 2} end return wp end -- Returns waypoint and is_junction function minecart.get_waypoint(pos, facedir, ctrl, uturn) local t = get_metadata(pos) if not t then t = find_all_next_waypoints(pos) set_metadata(pos, t) end local left = (facedir + 3) % 4 local right = (facedir + 1) % 4 local back = (facedir + 2) % 4 if ctrl.right and t[right] then return t[right], t[facedir] ~= nil or t[left] ~= nil end if ctrl.left and t[left] then return t[left] , t[facedir] ~= nil or t[right] ~= nil end if t[facedir] then return ramp_correction(pos, t[facedir], facedir), false end if t[right] then return ramp_correction(pos, t[right], right), false end if t[left] then return ramp_correction(pos, t[left], left), false end if uturn and t[back] then return t[back], false end end ------------------------------------------------------------------------------- -- delete waypoints ------------------------------------------------------------------------------- local function delete_counterpart_metadata(pos, wp) for facedir = 0,3 do if wp[facedir] then del_metadata(wp[facedir].pos) end end del_metadata(pos) end local function delete_next_metadata(pos, facedir, y) local cnt = 0 while cnt <= MAX_NODES do local is_rail, new_pos = get_next_pos(pos, facedir, y) if not is_rail then return end if has_metadata(new_pos) then del_metadata(new_pos) end pos = new_pos cnt = cnt + 1 end if has_metadata(pos) then del_metadata(pos) end end function minecart.delete_waypoint(pos) if has_metadata(pos) then local wp = get_metadata(pos) delete_counterpart_metadata(pos, wp) return end for facedir = 0,3 do local y = check_front_up_down(pos, facedir) if y then local new_pos = vector.add(pos, facedir2dir(facedir)) new_pos.y = new_pos.y + y if has_metadata(new_pos) then local wp = get_metadata(new_pos) delete_counterpart_metadata(new_pos, wp) else delete_next_metadata(pos, facedir, y) end end end end ------------------------------------------------------------------------------- -- find next buffer (needed as starting position) ------------------------------------------------------------------------------- local function get_next_waypoints(pos) local t = get_metadata(pos) if not t then t = find_all_next_waypoints(pos) end return t end local function get_next_pos_and_facedir(waypoints, facedir) local cnt = 0 local newpos, newfacedir facedir = (facedir + 2) % 4 -- opposite dir for i = 0, 3 do if waypoints[i] then cnt = cnt + 1 if i ~= facedir then -- not the same way back newpos = vector.new(waypoints[i].pos) newfacedir = i end end end -- no junction and valid facedir if cnt < 3 and newfacedir then return newpos, newfacedir end end local function get_next_buffer(pos, facedir) facedir = (facedir + 2) % 4 -- opposite dir for i = 1,5 do -- limit search depth local waypoints = get_next_waypoints(pos) or {} local pos1, facedir1 = get_next_pos_and_facedir(waypoints, facedir) if pos1 then pos, facedir = pos1, facedir1 else return minecart.find_node_near_lvm(pos, 1, {"minecart:buffer"}) end end end carts:register_rail("minecart:rail", { description = "Rail", tiles = { "carts_rail_straight.png^minecart_waypoint.png", "carts_rail_curved.png^minecart_waypoint.png", "carts_rail_t_junction.png^minecart_waypoint.png", "carts_rail_crossing.png^minecart_waypoint.png" }, inventory_image = "carts_rail_straight.png", wield_image = "carts_rail_straight.png", groups = carts:get_rail_groups({not_in_creative_inventory = 1}), drop = "carts:rail", }, {}) carts:register_rail("minecart:powerrail", { description = "Powered Rail", tiles = { "carts_rail_straight_pwr.png^minecart_waypoint.png", "carts_rail_curved_pwr.png^minecart_waypoint.png", "carts_rail_t_junction_pwr.png^minecart_waypoint.png", "carts_rail_crossing_pwr.png^minecart_waypoint.png" }, inventory_image = "carts_rail_straight.png", wield_image = "carts_rail_straight.png", groups = carts:get_rail_groups({not_in_creative_inventory = 1}), drop = "carts:powerrail", }, {}) for name,_ in pairs(tRails) do minetest.override_item(name, { after_destruct = minecart.delete_waypoint, after_place_node = minecart.delete_waypoint, }) end ------------------------------------------------------------------------------- -- API functions ------------------------------------------------------------------------------- -- Return new cart pos and if an extra move cycle is needed function minecart.get_current_cart_pos_correction(curr_pos, curr_fd, curr_y, new_dot) if new_dot then local new_y = (new_dot % 4) - 1 local new_fd = math.floor(new_dot / 4) if curr_y == -1 or new_y == -1 then local new_fd = math.floor(new_dot / 4) local dir = facedir2dir(new_fd) return { x = curr_pos.x + dir.x / 2, y = curr_pos.y, z = curr_pos.z + dir.z / 2}, new_y == -1 elseif curr_y == 1 and curr_fd ~= new_fd then local dir = facedir2dir(new_fd) return { x = curr_pos.x + dir.x / 2, y = curr_pos.y, z = curr_pos.z + dir.z / 2}, true elseif curr_y == 1 or new_y == 1 then local dir = facedir2dir(curr_fd) return { x = curr_pos.x - dir.x / 2, y = curr_pos.y, z = curr_pos.z - dir.z / 2}, false end end return curr_pos, false end -- Called by carts, returns the speed value or nil function minecart.get_speedlimit(pos, facedir) local fd = (facedir + 1) % 4 -- right local new_pos = vector.add(pos, facedir2dir(fd)) local node = get_node_lvm(new_pos) if tSigns[node.name] and node.param2 == facedir then return tSigns[node.name] end fd = (facedir + 3) % 4 -- left new_pos = vector.add(pos, facedir2dir(fd)) node = get_node_lvm(new_pos) if tSigns[node.name] and node.param2 == facedir then return tSigns[node.name] end end -- Called by carts, to delete temporarily created waypoints function minecart.delete_cart_waypoint(pos) del_metadata(pos) end -- Called by signs, to delete the rail waypoints nearby function minecart.delete_signs_waypoint(pos) local node = minetest.get_node(pos) local facedir = (node.param2 + 1) % 4 -- right local new_pos = vector.add(pos, facedir2dir(facedir)) if tRailsExt[get_node_lvm(new_pos).name] then minecart.delete_waypoint(new_pos) end facedir = (node.param2 + 3) % 4 -- left new_pos = vector.add(pos, facedir2dir(facedir)) if tRailsExt[get_node_lvm(new_pos).name] then minecart.delete_waypoint(new_pos) end end function minecart.is_rail(pos) return tRails[get_node_lvm(pos).name] ~= nil end -- To register node cart names function minecart.add_raillike_nodes(name) tRailsExt[name] = true lRailsExt[#lRailsExt + 1] = name end -- minecart.get_next_buffer(pos, facedir) minecart.get_next_buffer = get_next_buffer -- minecart.del_metadata(pos) minecart.del_metadata = del_metadata --minetest.register_lbm({ -- label = "Delete waypoints", -- name = "minecart:del_meta", -- nodenames = {"minecart:rail", "minecart:powerrail"}, -- run_at_every_load = true, -- action = function(pos, node) -- del_metadata(pos) -- end, --})