--[[ Minecart ======== Copyright (C) 2019-2023 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 S2P = minetest.string_to_pos local P2H = minetest.hash_node_position local H2P = minetest.get_position_from_hash local S = minecart.S local tCartsOnRail = minecart.CartsOnRail local Queue = {} local first = 0 local last = -1 local function push(cycle, item) last = last + 1 item.cycle = cycle Queue[last] = item end local function pop(cycle) if first > last then return end local item = Queue[first] if item.cycle < cycle then Queue[first] = nil -- to allow garbage collection first = first + 1 return item end end local function is_player_nearby(pos) for _, object in pairs(minetest.get_objects_inside_radius(pos, 64)) do if object:is_player() then return true end end end local function zombie_to_entity(pos, cart, checkpoint) local vel = {x = 0, y = 0, z = 0} local obj = minecart.add_entitycart(pos, cart.node_name, cart.entity_name, vel, cart.cargo, cart.owner, cart.userID) if obj then local entity = obj:get_luaentity() entity.reenter = checkpoint entity.junctions = cart.junctions entity.is_running = true entity.arrival_time = 0 cart.objID = entity.objID end end local function get_checkpoint(cart) local cp = cart.checkpoints[cart.idx] if not cp then cart.idx = #cart.checkpoints cp = cart.checkpoints[cart.idx] end local pos = H2P(cp[1]) return cp, cart.idx == #cart.checkpoints end -- Function returns the cart state ("running" / "stopped") and -- the station name or position string, or if cart is running, -- the distance to the query_pos. local function get_cart_state_and_loc(name, userID, query_pos) if tCartsOnRail[name] and tCartsOnRail[name][userID] then local cart = tCartsOnRail[name][userID] local pos = cart.last_pos or cart.pos if pos then local loc = minecart.get_buffer_name(cart.pos) or math.floor(vector.distance(pos, query_pos)) if cart.objID == 0 then return "stopped", minecart.get_buffer_name(cart.pos) or math.floor(vector.distance(pos, query_pos)), cart.node_name else return "running", math.floor(vector.distance(pos, query_pos)), cart.node_name end end end return "unknown", 0, "unknown" end -- Return the cart distance to the query_pos. local function get_cart_distance(name, userID, query_pos) if tCartsOnRail[name] and tCartsOnRail[name][userID] then local cart = tCartsOnRail[name][userID] if cart.last_pos or cart.pos then if cart.objID == 0 then -- stopped return math.floor(vector.distance(cart.pos or cart.last_pos, query_pos)) else return math.floor(vector.distance(cart.last_pos or cart.pos, query_pos)) end end end return 0 end local function get_cart_info(owner, userID, query_pos) local state, loc, name = get_cart_state_and_loc(owner, userID, query_pos) local cart_type = minecart.tCartTypes[name] or "unknown" if type(loc) == "number" then return "Cart #" .. userID .. " (" .. cart_type .. ") " .. state .. " " .. loc .. " m away " else return "Cart #" .. userID .. " (" .. cart_type .. ") " .. state .. " at ".. loc .. " " end end local function logging(cart, err) local s = string.format("[Minecart] Cart %s/%u %s!", cart.owner, cart.userID, err) minetest.log("warning", s) end -- check cart data local function valid_cart(cart) if cart.objID == nil or cart.objID == 0 then return false end if tCartsOnRail[cart.owner] and tCartsOnRail[cart.owner][cart.userID] then return true end logging(cart, "with invalid data") local entity = minetest.luaentities[cart.objID] if entity then entity.object:remove() end return false end local function monitoring(cycle) local cart = pop(cycle) -- All running cars while cart do if valid_cart(cart) then cart.idx = cart.idx + 1 local entity = minetest.luaentities[cart.objID] if entity then -- cart entity running local pos = entity.object:get_pos() if pos then cart.last_pos = vector.round(pos) else logging(cart, "without pos") end push(cycle, cart) elseif cart.checkpoints then local cp, last_cp = get_checkpoint(cart) if cp then cart.last_pos = H2P(cp[1]) --print("zombie " .. cart.userID .. " at " .. P2S(cart.last_pos)) if is_player_nearby(cart.last_pos) or last_cp then zombie_to_entity(cart.last_pos, cart, cp) end push(cycle, cart) else logging(cart, "as zombie got lost") end else local pos = cart.last_pos or cart.pos pos = minecart.add_nodecart(pos, cart.node_name, 0, cart.cargo, cart.owner, cart.userID) minecart.stop_monitoring(cart.owner, cart.userID, pos) logging(cart, "stopped at " .. (P2S(pos) or "unknown")) end elseif not cart.objID and tCartsOnRail[cart.owner] then -- Delete carts marked as "to be deleted" tCartsOnRail[cart.owner][cart.userID] = nil end cart = pop(cycle) end minetest.after(2, monitoring, cycle + 1) end minetest.after(5, monitoring, 2) function minecart.monitoring_add_cart(owner, userID, pos, node_name, entity_name) --print("monitoring_add_cart", owner, userID) tCartsOnRail[owner] = tCartsOnRail[owner] or {} tCartsOnRail[owner][userID] = { owner = owner, userID = userID, objID = 0, pos = pos, idx = 0, node_name = node_name, entity_name = entity_name, } minecart.store_carts() end function minecart.start_monitoring(owner, userID, pos, objID, checkpoints, junctions, cargo) --print("start_monitoring", owner, userID) if tCartsOnRail[owner] and tCartsOnRail[owner][userID] then tCartsOnRail[owner][userID].pos = pos tCartsOnRail[owner][userID].objID = objID tCartsOnRail[owner][userID].checkpoints = checkpoints tCartsOnRail[owner][userID].junctions = junctions tCartsOnRail[owner][userID].cargo = cargo tCartsOnRail[owner][userID].idx = 0 push(0, tCartsOnRail[owner][userID]) minecart.store_carts() end end function minecart.stop_monitoring(owner, userID, pos) --print("stop_monitoring", owner, userID) if tCartsOnRail[owner] and tCartsOnRail[owner][userID] then tCartsOnRail[owner][userID].pos = pos -- Mark as "stopped" tCartsOnRail[owner][userID].objID = 0 minecart.store_carts() end end function minecart.monitoring_remove_cart(owner, userID) --print("monitoring_remove_cart", owner, userID) if tCartsOnRail[owner] and tCartsOnRail[owner][userID] then -- Cart stopped? if tCartsOnRail[owner][userID].objID == 0 then -- Mark as "to be deleted" by monitoring (if part of monitoring) tCartsOnRail[owner][userID].objID = nil -- And delete directly in addition tCartsOnRail[owner][userID] = nil else -- Cart running -- Mark as "to be deleted" by monitoring tCartsOnRail[owner][userID].objID = nil end minecart.store_carts() end end function minecart.monitoring_valid_cart(owner, userID, pos, node_name) if tCartsOnRail[owner] and tCartsOnRail[owner][userID] and tCartsOnRail[owner][userID].pos then return vector.equals(tCartsOnRail[owner][userID].pos, pos) and tCartsOnRail[owner][userID].node_name == node_name end end function minecart.userID_available(owner, userID) return not tCartsOnRail[owner] or tCartsOnRail[owner][userID] == nil end function minecart.get_cart_monitoring_data(owner, userID) if tCartsOnRail[owner] then return tCartsOnRail[owner][userID] end end -- -- API functions -- -- Needed by storage to re-construct the queue after server start minecart.push = push minetest.register_chatcommand("mycart", { params = "", description = S("Output cart state and position, or a list of carts, if no cart number is given."), func = function(owner, param) local userID = tonumber(param) local query_pos = minetest.get_player_by_name(owner):get_pos() if userID then return true, get_cart_info(owner, userID, query_pos) elseif tCartsOnRail[owner] then -- Output a list with all numbers local tbl = {} for userID, cart in pairs(tCartsOnRail[owner]) do tbl[#tbl + 1] = userID end return true, S("List of carts") .. ": "..table.concat(tbl, ", ").." " end end }) minetest.register_chatcommand("stopcart", { params = "", description = S("Stop and return/drop a missing/running cart."), func = function(owner, param) local userID = tonumber(param) local player_pos = minetest.get_player_by_name(owner):get_pos() if userID then local data = minecart.get_cart_monitoring_data(owner, userID) if data and data.objID then local entity = minetest.luaentities[data.objID] --print("stopcart", userID, data.pos, data.objID, entity) if data.objID == 0 then -- Cart as node if data.pos then local meta = M(data.pos) if owner == meta:get_string("owner") and userID == meta:get_int("userID") then minecart.remove_nodecart(data.pos) end end elseif entity then -- Cart as entity minecart.remove_entity(entity, data.pos) else -- Cart as zombie/invalid/corrupted minetest.log("warning", "[Minecart] data.objID ~= 0, but no entity available!") end minetest.add_item(player_pos, ItemStack({name = data.node_name})) minecart.monitoring_remove_cart(owner, userID) return true, S("Cart") .. " " .. userID .. " " .. S("dropped") else return false, S("Cart") .. " " .. userID .. " " .. S("is not existing!") end else return false end end }) function minecart.cmnd_cart_state(name, userID) local state, loc = get_cart_state_and_loc(name, userID, {x=0, y=0, z=0}) return state end function minecart.cmnd_cart_location(name, userID, query_pos) local state, loc = get_cart_state_and_loc(name, userID, query_pos) return loc end function minecart.cmnd_cart_distance(name, userID, query_pos) return get_cart_distance(name, userID, query_pos) end function minecart.get_cart_list(pos, name) local userIDs = {} local carts = {} for userID, cart in pairs(tCartsOnRail[name] or {}) do userIDs[#userIDs + 1] = userID end table.sort(userIDs, function(a,b) return a < b end) for _, userID in ipairs(userIDs) do carts[#carts + 1] = get_cart_info(name, userID, pos) end return table.concat(carts, "\n") end minetest.register_on_mods_loaded(function() if minetest.global_exists("techage") then techage.icta_register_condition("cart_state", { title = "read cart state", formspec = { { type = "digits", name = "number", label = "cart number", default = "", }, { type = "label", name = "lbl", label = "Read state from one of your carts", }, }, button = function(data, environ) -- default button label local number = tonumber(data.number) or 0 return 'cart_state('..number..')' end, code = function(data, environ) local condition = function(env, idx) local number = tonumber(data.number) or 0 return minecart.cmnd_cart_state(environ.owner, number) end local result = function(val) return val ~= 0 end return condition, result end, }) techage.icta_register_condition("cart_location", { title = "read cart location", formspec = { { type = "digits", name = "number", label = "cart number", default = "", }, { type = "label", name = "lbl", label = "Read location from one of your carts", }, }, button = function(data, environ) -- default button label local number = tonumber(data.number) or 0 return 'cart_loc('..number..')' end, code = function(data, environ) local condition = function(env, idx) local number = tonumber(data.number) or 0 return minecart.cmnd_cart_location(environ.owner, number, env.pos) end local result = function(val) return val ~= 0 end return condition, result end, }) techage.lua_ctlr.register_function("cart_state", { cmnd = function(self, num) num = tonumber(num) or 0 return minecart.cmnd_cart_state(self.meta.owner, num) end, help = " $cart_state(num)\n".. " Read state from one of your carts.\n".. ' "num" is the cart number\n'.. ' example: sts = $cart_state(2)' }) techage.lua_ctlr.register_function("cart_location", { cmnd = function(self, num) num = tonumber(num) or 0 return minecart.cmnd_cart_location(self.meta.owner, num, self.meta.pos) end, help = " $cart_location(num)\n".. " Read location from one of your carts.\n".. ' "num" is the cart number\n'.. ' example: sts = $cart_location(2)' }) end end)