552 lines
14 KiB
Lua
552 lines
14 KiB
Lua
--[[
|
|
|
|
Minecart
|
|
========
|
|
|
|
Copyright (C) 2019-2020 Joachim Stolberg
|
|
|
|
MIT
|
|
See license.txt for more information
|
|
|
|
]]--
|
|
|
|
-- Some notes:
|
|
-- 1) Entity IDs are volatile. For each server restart all carts get new IDs.
|
|
-- 2) Monitoring is performed for entities only. Stopped carts in form of
|
|
-- real nodes need no monitoring.
|
|
-- 3) But nodes at stations have to call 'node_at_station' to be "visible"
|
|
-- for the chat commands
|
|
|
|
|
|
-- 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 S = minecart.S
|
|
|
|
if 0 then
|
|
local CartsOnRail = minecart.CartsOnRail -- from storage.lua
|
|
local get_route = minecart.get_route -- from storage.lua
|
|
local NodesAtStation = {}
|
|
|
|
--
|
|
-- Helper functions
|
|
--
|
|
local function get_pos_vel(item)
|
|
if item.start_time and item.start_key then -- cart on recorded route
|
|
local run_time = minetest.get_gametime() - item.start_time
|
|
local route = get_route(item.start_key)
|
|
if route then
|
|
local waypoints = route.waypoints
|
|
if not waypoints or not next(waypoints) then
|
|
return item.last_pos, {x = 0, y = 0, z = 0}, true
|
|
end
|
|
if run_time >= #waypoints then
|
|
local waypoint = waypoints[#waypoints]
|
|
return S2P(waypoint[1]), S2P(waypoint[2]), true -- time to appear
|
|
else
|
|
local waypoint = waypoints[run_time]
|
|
return S2P(waypoint[1]), S2P(waypoint[2]), false
|
|
end
|
|
end
|
|
end
|
|
return item.last_pos, {x = 0, y = 0, z = 0}, true
|
|
end
|
|
|
|
local function is_player_nearby(pos)
|
|
for _, object in pairs(minetest.get_objects_inside_radius(pos, 30)) do
|
|
if object:is_player() then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Monitoring API functions
|
|
--
|
|
function minecart.add_to_monitoring(myID, pos, entity_name, owner, userID)
|
|
print("add_to_monitoring")
|
|
if myID and pos and entity_name and owner and userID then
|
|
local start_key = lib.get_route_key(pos)
|
|
if start_key then
|
|
CartsOnRail[myID] = {
|
|
start_key = start_key,
|
|
last_pos = pos,
|
|
entity_name = entity_name,
|
|
owner = owner, -- needed for query API
|
|
userID = userID, -- needed for query API
|
|
stopped = true,
|
|
}
|
|
minecart.store_carts()
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
function minecart.monitoring_start_cart(self, pos, myID)
|
|
print("monitoring_start_cart")
|
|
if myID then
|
|
local item = CartsOnRail[myID]
|
|
if item and item.stopped then
|
|
local start_key = lib.get_route_key(pos)
|
|
if start_key then
|
|
local route = minecart.get_route(start_key)
|
|
if route then
|
|
item.start_key = start_key
|
|
item.junctions = route.junctions
|
|
item.dest_pos = S2P(route.dest_pos)
|
|
item.cargo = self.cargo or {}
|
|
item.stopped = false
|
|
item.start_time = minetest.get_gametime()
|
|
minecart.store_carts()
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function minecart.monitoring_stop_cart(myID)
|
|
print("monitoring_stop_cart")
|
|
if myID then
|
|
local item = CartsOnRail[myID]
|
|
if item and not item.stopped then
|
|
item.start_time = nil
|
|
item.stopped = true
|
|
item.time_to_appear = nil
|
|
minecart.store_carts()
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- When cart entity is removed
|
|
function minecart.remove_from_monitoring(myID)
|
|
print("remove_from_monitoring")
|
|
if myID then
|
|
CartsOnRail[myID] = nil
|
|
minecart.store_carts()
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Additional API functions
|
|
--
|
|
-- For the emergency "back to start"
|
|
function minecart.get_start_pos_vel(myID)
|
|
local item = CartsOnRail[myID]
|
|
print(1)
|
|
if item then
|
|
print(2)
|
|
local route = get_route(item.start_key)
|
|
if route then
|
|
print(3)
|
|
local waypoints = route.waypoints
|
|
if waypoints and next(waypoints) then
|
|
print(4)
|
|
local waypoint = waypoints[1]
|
|
if waypoint then
|
|
print(5)
|
|
local pos = S2P(waypoint[1])
|
|
local vel = S2P(waypoint[2])
|
|
vel = vector.multiply(vel, -2)
|
|
return pos, vel, item.start_key
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function minecart.get_dest_pos(myID)
|
|
local item = CartsOnRail[myID]
|
|
if item then
|
|
return item.dest_pos
|
|
end
|
|
end
|
|
|
|
-- Called after cart number formspec is closed
|
|
function minecart.update_userID(myID, userID)
|
|
if CartsOnRail[myID] then
|
|
CartsOnRail[myID].userID = userID
|
|
end
|
|
end
|
|
|
|
|
|
-- For node carts at stations
|
|
function minecart.node_at_station(owner, userID, pos)
|
|
print("node_at_station", owner, userID, P2S(pos))
|
|
NodesAtStation[owner] = NodesAtStation[owner] or {}
|
|
NodesAtStation[owner][userID] = pos
|
|
end
|
|
|
|
--
|
|
-- Monitoring
|
|
--
|
|
-- Copy item data to entity cart
|
|
local function item_to_entity(pos, vel, item)
|
|
print("item_to_entity", item.owner, item.userID)
|
|
pos = lib.find_rail_node(pos)
|
|
if pos then
|
|
-- Add cart to map
|
|
local obj = minetest.add_entity(pos, item.entity_name or "minecart:cart", nil)
|
|
-- Determine ID
|
|
local myID = lib.get_object_id(obj)
|
|
if myID then
|
|
-- Copy item data to cart entity
|
|
local entity = obj:get_luaentity()
|
|
entity.owner = item.owner or "unknown"
|
|
entity.userID = item.userID or 0
|
|
entity.cargo = item.cargo or {}
|
|
entity.myID = myID
|
|
obj:set_nametag_attributes({color = "#FFFF00", text = entity.owner..": "..entity.userID})
|
|
-- Start cart
|
|
obj:set_velocity(vel)
|
|
obj:set_rotation({x = 0, y = 0, z = 0})
|
|
return myID
|
|
else
|
|
-- should never happen
|
|
minetest.log("error", "[minecart] Entity has no ID")
|
|
end
|
|
end
|
|
end
|
|
|
|
local function entity_to_item(entity, item)
|
|
print("entity_to_item", item.owner, item.userID)
|
|
item.cargo = entity.cargo
|
|
-- Remove entity from map
|
|
entity.object:remove()
|
|
-- Stop sound
|
|
if entity.sound_handle then
|
|
minetest.sound_stop(entity.sound_handle)
|
|
entity.sound_handle = nil
|
|
end
|
|
end
|
|
|
|
local function debug(item, pos, entity)
|
|
print("Cart:",
|
|
item.userID,
|
|
P2S(vector.round(pos)),
|
|
item.stopped and "stopped" or "running",
|
|
entity and "entity" or "virtualized",
|
|
item.entity_name)
|
|
end
|
|
|
|
local function monitoring()
|
|
local to_be_added = {}
|
|
local to_be_removed = {}
|
|
|
|
for key, item in pairs(CartsOnRail) do
|
|
local present = minetest.get_player_by_name(item.owner or "") ~= nil
|
|
local appear = item.time_to_appear or item.stopped
|
|
local entity = minetest.luaentities[key]
|
|
if entity then -- cart entity running
|
|
local pos = entity.object:get_pos()
|
|
local vel = entity.object:get_velocity()
|
|
if pos and vel then
|
|
--debug(item, pos, entity)
|
|
item.last_pos = pos
|
|
if not is_player_nearby(pos) and not appear then
|
|
entity_to_item(entity, item)
|
|
end
|
|
else
|
|
minetest.log("error", "[minecart] Entity issues")
|
|
end
|
|
--entity.stopped = false -- force to stop monitoring
|
|
elseif present then -- no cart running
|
|
local pos, vel, time_to_appear = get_pos_vel(item)
|
|
if pos and vel then
|
|
--debug(item, pos, entity)
|
|
item.last_pos = pos
|
|
if is_player_nearby(pos) or appear then
|
|
local myID = item_to_entity(pos, vel, item)
|
|
if myID then
|
|
item.time_to_appear = time_to_appear
|
|
to_be_added[myID] = table.copy(item)
|
|
to_be_removed[#to_be_removed + 1] = key
|
|
end
|
|
end
|
|
else
|
|
minetest.log("error", "[minecart] Cart got lost")
|
|
to_be_removed[#to_be_removed + 1] = key
|
|
end
|
|
end
|
|
item.time_to_appear = nil
|
|
end
|
|
|
|
-- table maintenance
|
|
local is_changed = false
|
|
for key,val in pairs(to_be_added) do
|
|
CartsOnRail[key] = val
|
|
is_changed = true
|
|
end
|
|
for _,key in ipairs(to_be_removed) do
|
|
CartsOnRail[key] = nil
|
|
is_changed = true
|
|
end
|
|
if is_changed then
|
|
minecart.store_carts()
|
|
end
|
|
minetest.after(1, monitoring)
|
|
end
|
|
-- delay the start to prevent cart disappear into nirvana
|
|
minetest.register_on_mods_loaded(function()
|
|
minetest.after(10, monitoring)
|
|
end)
|
|
|
|
|
|
--
|
|
-- API functions
|
|
--
|
|
|
|
-- Return a list of carts with current position and speed.
|
|
function minecart.get_cart_list()
|
|
local tbl = {}
|
|
for id, item in pairs(CartsOnRail) do
|
|
local pos, speed = get_pos_vel(item)
|
|
tbl[#tbl+1] = {pos = pos, speed = speed, id = id}
|
|
end
|
|
return tbl
|
|
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.
|
|
function minecart.get_cart_state_and_loc(name, userID, query_pos)
|
|
-- First check if node cart is at any station
|
|
local cart_pos = NodesAtStation[name] and NodesAtStation[name][userID]
|
|
if cart_pos then
|
|
return "stopped", lib.get_station_name(cart_pos)
|
|
end
|
|
-- Then check all running carts
|
|
for id, item in pairs(CartsOnRail) do
|
|
if item.owner == name and item.userID == userID then
|
|
local loc = lib.get_station_name(item.last_pos) or
|
|
math.floor(vector.distance(item.last_pos, query_pos))
|
|
return (item.stopped and "stopped") or "running", loc
|
|
end
|
|
end
|
|
return "unknown", 0
|
|
end
|
|
|
|
minetest.register_chatcommand("mycart", {
|
|
params = "<cart-num>",
|
|
description = "Output cart state and position, or a list of carts, if no cart number is given.",
|
|
func = function(name, param)
|
|
local userID = tonumber(param)
|
|
local query_pos = minetest.get_player_by_name(name):get_pos()
|
|
|
|
if userID then
|
|
local state, loc = minecart.get_cart_state_and_loc(name, userID, query_pos)
|
|
if type(loc) == "number" then
|
|
return true, "Cart #" .. userID .. " " .. state .. " " .. loc .. " m away "
|
|
else
|
|
return true, "Cart #" .. userID .. " " .. state .. " at ".. loc .. " "
|
|
end
|
|
return false, "Cart #" .. userID .. " is unknown "
|
|
else
|
|
-- Output a list with all numbers
|
|
local tbl = {}
|
|
for userID, pos in pairs(NodesAtStation[name] or {}) do
|
|
tbl[#tbl + 1] = userID
|
|
end
|
|
for id, item in pairs(CartsOnRail) do
|
|
if item.owner == name then
|
|
tbl[#tbl + 1] = item.userID
|
|
end
|
|
end
|
|
return true, "List of carts: "..table.concat(tbl, ", ").." "
|
|
end
|
|
end
|
|
})
|
|
|
|
function minecart.cmnd_cart_state(name, userID)
|
|
local state, loc = minecart.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 = minecart.get_cart_state_and_loc(name, userID, query_pos)
|
|
return loc
|
|
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)
|
|
|
|
end -- if 0
|
|
|
|
function minecart.monitoring(self)
|
|
-- if not self.ctrl then
|
|
-- self.ctrl = minecart.get_next_ctrl(self.route.pos)
|
|
-- end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-------------------------------------------------------------------------------
|
|
-------------------------------------------------------------------------------
|
|
local tRunningCarts = {}
|
|
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 monitoring(cycle)
|
|
local cart = pop(cycle)
|
|
|
|
while cart and cart.objID do
|
|
local entity = minetest.luaentities[cart.objID]
|
|
if entity then -- cart entity running
|
|
cart.last_pos = vector.round(entity.object:get_pos())
|
|
--print("monitoring", cycle, cart.userID, P2S(cart.pos))
|
|
push(cycle, cart)
|
|
else
|
|
local pos = cart.last_pos or cart.start_pos
|
|
minecart.add_nodecart(pos, cart.node_name, 0, cart.cargo, cart.owner, cart.userID)
|
|
print("cart to node", cycle, cart.userID, P2S(pos))
|
|
end
|
|
cart = pop(cycle)
|
|
end
|
|
minetest.after(2, monitoring, cycle + 1)
|
|
end
|
|
|
|
minetest.after(2, monitoring, 1)
|
|
|
|
|
|
function minecart.start_monitoring(owner, userID, objID, pos, node_name, entity_name, cargo)
|
|
--print("start_monitoring", owner, userID)
|
|
tRunningCarts[owner] = tRunningCarts[owner] or {}
|
|
tRunningCarts[owner][userID] = {
|
|
owner = owner,
|
|
userID = userID,
|
|
objID = objID,
|
|
start_pos = pos,
|
|
node_name = node_name,
|
|
entity_name = entity_name,
|
|
cargo = cargo,
|
|
section = {},
|
|
}
|
|
push(0, tRunningCarts[owner][userID])
|
|
end
|
|
|
|
function minecart.stop_monitoring(owner, userID)
|
|
print("stop_monitoring", owner, userID)
|
|
if tRunningCarts[owner] and tRunningCarts[owner][userID] then
|
|
tRunningCarts[owner][userID].objID = nil
|
|
end
|
|
end
|
|
|
|
|
|
function minecart.get_junction(self, pos, dir)
|
|
local junctions = CartsOnRail[self.myID] and CartsOnRail[self.myID].junctions
|
|
if junctions then
|
|
local data = junctions[P2S(pos)]
|
|
if data then
|
|
return data[1], data[2]
|
|
end
|
|
data = junctions[P2S(vector.subtract(pos, dir))]
|
|
if data then
|
|
return data[1], data[2]
|
|
end
|
|
end
|
|
return dir
|
|
end
|
|
|