local dbg if moddebug then dbg=moddebug.dbg("railcarts") else dbg={v1=function() end,v2=function() end,v3=function() end} end cartbase = {} --- Set a new direction -- This updates the passed in state, but also sets the cart's orientation and -- modifies the linked player's view. -- @param state The current cart state, which will be updated. -- Only the direction component is relevant. -- If nil, the player's direction is set directly rather -- than amended (e.g. boarding cart) -- @param newdir The new direction function cartbase.setdirection(self, state, newdir) self.object:setyaw(direction_to_yaw(newdir)) if self.linkedplayer then if state then local dd = newdir - state.direction self.addplayeryaw = self.addplayeryaw - dd * (math.pi / 2) else self.linkedplayer:set_look_yaw(direction_to_yaw(newdir)) end end if state then state.direction = newdir else self.direction = newdir end end --- Determine if the cart is empty -- Empty means it can be picked up, which is not the case if there's -- someone riding in it, or cargo on board, etc. -- @return True if it's empty function cartbase.is_empty(self) if self.linkedplayer ~= nil then return false end if self.carttype == "cargo" then if not self.inventory:is_empty("main") then return false end elseif self.carttype == "boring" then if not self.inventory:is_empty("main") then return false end if not self.inventory:is_empty("materials") then return false end end return true end --- Handle step processing for a cart --@param self The cart --@param dtime Time since last call, in seconds function cartbase.on_step_handler(self, dtime) if self.skipnextstep then self.skipnextstep = false self.dtime_debt = self.dtime_debt + dtime return end -- No need to do any processing in a wait state. self.loadwait = self.loadwait + dtime if self.wait > 0 then self.wait = self.wait - dtime if self.wait >= 0 then return end -- Note, adding the negative remainder - we now process just the -- portion of the step left after the wait finished. dtime = dtime + self.wait self.wait = 0 end -- Add on 'dtime debt' (caused by us skipping slices on a previous -- step) dtime = dtime + self.dtime_debt self.dtime_debt = 0 self.lastmove = self.lastmove + dtime local oldpos if self.pos then oldpos = {x=self.pos.x, y=self.pos.y, z=self.pos.z} end -- Keep track of this over all step slices and do it once, otherwise -- it goes wrong if it happens twice (i.e. very fast over two close -- together curves) self.addplayeryaw = 0 -- Maximum length of time processed in a singleslice. This is based on -- the maximum cart speed and ensures it doesn't move more than half (or -- exactly!) a node length in one slice call. -- Theoretically, the speed could increase during the first slice and -- make the movement too large in the second, but I'm not worrying about -- that for now. TODO local max_update_time = 1 if self.speed > 0 then max_update_time = 0.45 / self.speed end -- Do as many slices as we need to to keep the maximum distance moved -- below the threshold local nomore = false while(dtime > max_update_time and not nomore) do nomore = cartbase.update(self, max_update_time) dtime = dtime - max_update_time end if dtime >= 0 and not nomore then cartbase.update(self, dtime) dtime = 0 end if nomore then -- TODO - seeing if skipping an extra step resolves the mesecons -- delay which occasionally causes rails to not be switched -- in time self.skipnextstep = true end self.dtime_debt = dtime if self.addplayeryaw ~= 0 and self.linkedplayer then local curyaw = self.linkedplayer:get_look_yaw() if curyaw then -- Correct for api insanity curyaw = curyaw - math.pi / 2 curyaw = curyaw + self.addplayeryaw local c = math.pi * 2 while curyaw > c do curyaw = curyaw - c end while curyaw < 0 do curyaw = curyaw + c end self.linkedplayer:set_look_yaw(curyaw) end end -- Call setpos if the position has changed if not oldpos or not vector.equals(self.pos, oldpos) then self.object:setpos(self.pos) end end --- Handle update for a cart. --@param self The cart --@param dtime Time since last call, in seconds, which should always be within -- low enough that the cart doesn't >= 0.5 nodes. --@return True if something happened which means no further step slices should -- be processed during this step. (Currently, this is specifically only -- when a digiline message is sent, because a common usage of that is -- to pass information to a luacontroller, which then uses mesecons to -- switch a rail. This will not necessarily have happened until the next -- server step - and hopefully by then!) function cartbase.update(self, dtime) local no_more_slices = false -- A little hack to stop double-grabbing. After self.object:remove() this -- handler can still get called, because of our step-slicing. if self.dead then return no_more_slices end local state = {} if not self.pos then self.pos = self.object:getpos() end state.pos = self.pos -- state.nodepos is the rounded node position, i.e. the centre of the node box state.nodepos = vector.round(state.pos) state.direction = self.direction state.speed = self.speed state.railstatus = get_railstatus(state.pos, state.direction, state.speed) -- Stop here if we're on an unloaded block. Hopefully it will load and we can just carry on. if is_unloaded(state.railstatus.railtype) then if self.speed == 0 then -- This can happen at speed 0, because we're inactive. Could have -- been placed by an active autolauncher, near the boundary with -- an inactive block, for example. self.wait = 2 else -- It shouldn't ever happen when moving though, so long as we have -- autonomous support. dbg.v2("Waiting for current block load ("..state.railstatus.railtype..") at "..minetest.pos_to_string(state.pos)) self.wait = 1 end return no_more_slices end -- Eject a player if necessary if (state.railstatus.eject or state.railstatus.onautolauncher) and self.linkedplayer ~= nil then dbg.v1("Ejecting player from cart") self.linkedplayer:set_detach() self.linkedplayer:setpos(state.pos) self.linkedplayer = nil end -- Grab the cart into something's inventory if necessary -- (only allowed when there is no passenger or cargo) if state.railstatus.grab and cartbase.is_empty(self) then dbg.v1("Node "..minetest.pos_to_string(state.railstatus.grab).." grabbing "..self.carttype.." cart") local meta = minetest.get_meta(state.railstatus.grab) local inv = meta:get_inventory() if self.name == "railcarts:cart_ent" then inv:add_item("main", ItemStack("railcarts:cart")) elseif self.name == "railcarts:boring_cart_ent" then inv:add_item("main", ItemStack("railcarts:boring_cart")) else inv:add_item("main", ItemStack("railcarts:cargo_cart")) end self.object:remove() self.dead = true return no_more_slices end -- Set speed/direction from launcher if state.railstatus.launch then cartbase.setdirection(self, state, state.railstatus.launch) state.speed = LAUNCH_CART_SPEED elseif state.railstatus.autolaunch and self.linkedplayer ~= nil then cartbase.setdirection(self, state, state.railstatus.autolaunch) state.speed = LAUNCH_CART_SPEED end -- Handle loading/unloading from/to a hopper if (self.carttype == "cargo" or self.carttype == "boring") and state.railstatus.hopper and self.speed == 0 then local hpos = state.railstatus.hopper local meta = minetest.get_meta(hpos) if meta then local frominv = meta:get_inventory() local toinv = self.inventory local desc = "load" -- For now at least, loading is the default, and unloading happens -- when there is an autolauncher below. if state.railstatus.onautolauncher then local t = frominv frominv = toinv toinv = t desc = "unload" end if frominv:is_empty("main") then if self.loadwait > 600 and state.railstatus.autolaunch and desc == "load" and not toinv:is_empty("main") then cartbase.setdirection(self, state, state.railstatus.autolaunch) state.speed = LAUNCH_CART_SPEED dbg.v3("Cart at "..minetest.pos_to_string(hpos).." waited 10 minutes for more cargo - launching") else if toinv:is_empty("main") then self.loadwait = 0 dbg.v3("Hopper and cart at "..minetest.pos_to_string(hpos).." are empty, waiting.") else dbg.v3("Hopper/cart at "..minetest.pos_to_string(hpos).." is empty, waiting (for "..math.floor(self.loadwait).."s)") end self.wait = 5 end else self.loadwait = 0 for i, stack in ipairs(frominv:get_list("main")) do if stack:get_name() ~= "" then if toinv:room_for_item("main", stack) then -- Important to overwrite the slot here, not remove_item, -- otherwise if there are two items the same with metadata -- the wrong one can be removed. frominv:set_stack("main", i, ItemStack(nil)) toinv:add_item("main", stack) dbg.v1("Cart "..desc.."ed "..stack:get_count().." "..stack:get_name().." at "..minetest.pos_to_string(hpos)) -- Counts as a move, for the purposes of remaining -- active self.lastmove = 0 self.wait = 5 break else if desc == "load" then if state.railstatus.autolaunch then cartbase.setdirection(self, state, state.railstatus.autolaunch) state.speed = LAUNCH_CART_SPEED dbg.v3("Cart at "..minetest.pos_to_string(hpos).." is full - launching") else dbg.v3("Cart at "..minetest.pos_to_string(hpos).." is full") self.wait = 30 end else dbg.v3("Hopper at "..minetest.pos_to_string(hpos).." is full, can't"..desc) self.wait = 30 end end break end end end end else self.loadwait = 0 end -- For safety - should never happen... if state.railstatus.railtype == "inv" and state.speed ~= 0 then dbg.v1("Stopping cart, it's off the rails at "..minetest.pos_to_string(self.pos).." "..minetest.pos_to_string(vector.round(self.pos))) state.speed = 0 end -- Move the cart if state.speed > 0 then -- Get the status of the rails at our next location cartbase.get_next_railstatus(self, state, false) local drop = false if state.nextrailstatus.railtype == "inv" then -- If there's no next rail, maybe there's a drop (a.k.a a downward slope) cartbase.get_next_railstatus(self, state, true) if state.nextrailstatus.railtype ~= "inv" then drop = true end end -- Stop here if we're entering an unloaded block. Hopefully it will load -- and we can just carry on if is_unloaded(state.nextrailstatus.railtype) then dbg.v2("Waiting for next block load ("..state.nextrailstatus.railtype..") at ".. minetest.pos_to_string(state.nextrailstatus.unloadedpos).. " : block = ".. math.floor(state.nextrailstatus.unloadedpos.x / 16)..",".. math.floor(state.nextrailstatus.unloadedpos.y / 16)..",".. math.floor(state.nextrailstatus.unloadedpos.z / 16)) return no_more_slices end local movedist = state.speed * dtime local xz = direction_to_xz(state.direction) local axis, oaxis if xz.x ~= 0 then axis = "x" oaxis = "z" else axis = "z" oaxis = "x" end local movein = false if (xz[axis] > 0 and state.pos[axis] < state.nodepos[axis]) or (xz[axis] < 0 and state.pos[axis] > state.nodepos[axis]) then movein = true end dbg.v3("Cart on "..state.railstatus.railtype.." at "..minetest.pos_to_string(state.pos).. ", dir:"..state.direction.." speed:"..state.speed.. " next rail:"..state.nextrailstatus.railtype.. " "..(drop and "(drop)" or "").. " "..(movein and "(movein)" or "(moveout)")) state.pos[axis] = state.pos[axis] + movedist * xz[axis] if movein then if (xz[axis] > 0 and state.pos[axis] >= state.nodepos[axis]) or (xz[axis] < 0 and state.pos[axis] <= state.nodepos[axis]) then -- We were moving in to the centre, but now we've moved past it local past = math.abs(state.pos[axis] - state.nodepos[axis]) -- Trigger a detector? if state.railstatus.detector then local detpos = state.railstatus.detector minetest.add_node(detpos, {name="railcarts:cart_detector_on"}) mesecon:receptor_on(detpos, mesecon.rules.default) dbg.v2("Triggered cart detector at "..minetest.pos_to_string(newpos)) end -- Various control things. These are applied only at the centre -- of the node such that they only occur once. local con = state.railstatus.control if con then if con == "maxspeed" then dbg.v2("Max speed on control rail") state.speed = MAXIMUM_CART_SPEED elseif con == "speedup" then dbg.v2("Accelerating on control rail") state.speed = state.speed + 1 elseif con == "slowdown" then dbg.v2("Decelerating on control rail") state.speed = state.speed - 1 elseif con == "stop" then dbg.v2("Stopping on control rail") state.speed = 0 elseif con == "reverse" then dbg.v2("Reversing on control rail") cartbase.setdirection(self, state, direction_reverse(state.direction)) -- Move back to prevent re-trigger on the way back state.pos[axis] = state.pos[axis] - past elseif string.sub(con, 0, 6) == "speed " then local newspeed = tonumber(string.sub(con, 7)) if newspeed then state.speed = newspeed dbg.v2("Setting speed to "..newspeed.." on control rail") else dbg.v1("Invalid speed from control rail") end elseif string.sub(con, 0, 4) == "tag " then self.tag = string.sub(con, 5) dbg.v2("Cart tagged '"..self.tag.."'") elseif con == "none" then -- doesn't do anything else dbg.v1("Invalid control rail setting") end -- Cap speed appropriately if state.speed < 0 then state.speed = 0 end if state.speed > MAXIMUM_CART_SPEED then state.speed = MAXIMUM_CART_SPEED end end -- Send digiline signal if required if digiline and state.railstatus.digiline then local dpos = state.railstatus.digiline local channel = minetest.get_meta(dpos):get_string("channel") if channel and channel ~= "" then msg = {} msg.tag = self.tag msg.speed = self.speed msg.direction = self.direction if self.linkedplayer then msg.passenger = self.linkedplayer:get_player_name() else msg.passenger = nil end msg.carttype = self.carttype dbg.v2("Sending digiline message on channel "..channel) digiline:receptor_send(dpos, digiline.rules.default, channel, msg) no_more_slices = true end end if state.nextrailstatus.railtype == "inv" then -- End of rails, so stop the cart (or do a custom action) state.pos[axis] = state.nodepos[axis] state.speed = 0 if self.on_end_of_rails then if self.on_end_of_rails(self, state, axis, oaxis, xz) then self.wait = 2 state.speed = 1 -- Back up a bit so we come back here for the next action state.pos[axis] = state.pos[axis] - (0.4 * xz[axis]) end else dbg.v2("Cart reached end of rails and stopped at "..minetest.pos_to_string(state.pos)) end elseif is_curve(state.railstatus.railtype) then state.pos[axis] = state.nodepos[axis] local newdir if state.railstatus.railtype == "x+" then if axis == "x" then newdir = 2 past = -past else newdir = 1 end elseif state.railstatus.railtype == "x-" then if axis == "x" then newdir = 2 past = -past else newdir = 3 past = -past end elseif state.railstatus.railtype == "z+" then if axis == "x" then newdir = 0 else newdir = 1 end else if axis == "x" then newdir = 0 else newdir = 3 past = -past end end cartbase.setdirection(self, state, newdir) state.pos[oaxis] = state.pos[oaxis] + past dbg.v2("Cart turned a corner") end end else --Moving out local newnodepos = vector.round(state.pos) if state.nodepos[axis] ~= newnodepos[axis] then dbg.v3("New nodepos "..newnodepos[axis]..", old "..state.nodepos[axis]) -- We've arrived in the new node if drop then state.pos.y = state.pos.y - 1 elseif state.direction == state.railstatus.slope then state.pos.y = state.pos.y + 1 end end end end -- Set ourselves autonomous if necessary (requires minetest patch) if state.speed > 0 then self.lastmove = 0 end local autonomous = 0 if self.lastmove < 10 then autonomous = 1 end self.object:set_autonomous(autonomous) -- Write things back. Ultimately the position will get 'broken' when core -- turns it from a double to a float. We don't care, because we use our -- own internally stored position as the reference. self.pos = state.pos self.speed = state.speed self.direction = state.direction return no_more_slices end --- Get the status of the rails at the next node --@param state The current state, which will have a next_railstatus -- field added. (And only railstatus, pos, speed and direction are relevant) --@param below True to look below instead of at the same level function cartbase.get_next_railstatus(self, state, below) if state.speed == 0 then state.nextrailstatus = {railtype="inv"} return end local xz, nexty if below then nexty = -1 else if state.railstatus.slope then if state.railstatus.slope == state.direction then nexty = 1 else nexty = 0 end else nexty = 0 end end if is_curve(state.railstatus.railtype) then if state.railstatus.railtype == "x+" then if state.direction == 0 or state.direction == 1 then xz = {x=1,z=0} else xz = {x=0,z=-1} end elseif state.railstatus.railtype == "x-" then if state.direction == 0 or state.direction == 3 then xz = {x=-1,z=0} else xz = {x=0,z=-1} end elseif state.railstatus.railtype == "z+" then if state.direction == 2 or state.direction == 1 then xz = {x=1,z=0} else xz = {x=0,z=1} end else if state.direction == 2 or state.direction == 3 then xz = {x=-1,z=0} else xz = {x=0,z=1} end end else xz = direction_to_xz(state.direction) end nextpos = vector.round(state.pos) nextpos.x = nextpos.x + xz.x nextpos.y = nextpos.y + nexty nextpos.z = nextpos.z + xz.z dbg.v3("Checking nextpos "..minetest.pos_to_string(nextpos)) state.nextrailstatus = get_railstatus(nextpos) end --- Handle cart being punched by a player -- function cartbase.punch_move(self, own_pos, hitterpos) self.wait = 0 local railstatus = get_railstatus(self.pos) -- Only when on rails... if railstatus.railtype == "inv" then return end local xd = own_pos.x - hitterpos.x local zd = own_pos.z - hitterpos.z dbg.v2("Player punched cart with xd="..xd..",zd="..zd) local newdir if railstatus.railtype == "x" then if xd < 0 then newdir = 3 else newdir = 1 end elseif railstatus.railtype == "z" then if zd < 0 then newdir = 2 else newdir = 0 end elseif railstatus.railtype == "x+" then if math.abs(zd) > math.abs(xd) and zd < 0 then newdir = 2 elseif math.abs(xd) > math.abs(zd) and xd > 0 then newdir = 1 end end -- TODO - the rest of the curves! if newdir then -- Punching a moving cart in the same direction speeds it up if self.speed ~= 0 then if newdir == self.direction then self.speed = self.speed + 1 if self.speed > MAXIMUM_CART_SPEED then self.speed = MAXIMUM_CART_SPEED end else -- otherwise it just stops... self.speed = 0 end return end state = {direction=newdir, speed=1, railstatus=railstatus, pos=self.pos} cartbase.get_next_railstatus(self, state, false) if state.nextrailstatus.railtype == "inv" then -- check below, so we can push down a hill cartbase.get_next_railstatus(self, state, true) end if state.nextrailstatus.railtype ~= "inv" and not is_unloaded(state.nextrailstatus.railtype) then cartbase.setdirection(self, nil, newdir) self.speed = 1 end end end --- Handler for on_punch -- @@param hitter The player that hit it function cartbase.on_punch_handler(self, hitter) if hitter:get_player_control().sneak and cartbase.is_empty(self) then hitter:get_inventory():add_item("main", self.getitem) self.object:remove() else local own_pos = self.object:getpos() local hitterpos = hitter:getpos() cartbase.punch_move(self,own_pos,hitterpos) end return true end --- Handler for on_activate -- @param staticdata Saved data if restoring a deactivated cart function cartbase.on_activate_handler(self, staticdata) self.direction = 0 self.speed = 0 self.pos = nil self.tag = nil self.wait = 0 self.loadwait = 0 self.skipnextstep = false self.lastmove = 0 self.dtime_debt = 0 -- These are parameters for the boring cart only... self.dig = false self.safedig = true self.bridge = false self.lay = false self.digslope = 2 self.object:set_armor_groups(self.groups) -- Set up detached inventories if required if self.carttype == "cargo" or self.carttype == "boring" then -- Unique name needed for detached inventory -- TODO - is there a better way of getting this unique ID? local uid for k, v in pairs(minetest.luaentities) do if v == self then uid = k break end end if not uid then dbg.v1("WARNING - unique ID was not generated - cart inventories may be shared") uuid = tostring(math.random()) end self.inventoryname = uid dbg.v3("Creating new cart, inventory name is "..self.inventoryname) self.inventory = minetest.create_detached_inventory(self.inventoryname, nil) self.inventory:set_size("main",12) if self.carttype == "boring" then self.inventory:set_size("materials",12) end end local restored = minetest.deserialize(staticdata) if restored ~= nil then if restored.pos then self.pos = restored.pos else self.pos = self.object:getpos() end if restored.speed then self.speed = restored.speed end if restored.direction then self.direction = restored.direction end if restored.tag then self.tag = restored.tag end if restored.wait then self.wait = restored.wait end if restored.loadwait then self.loadwait = restored.loadwait end if restored.dig then self.dig = restored.dig end if restored.safedig then self.safedig = restored.safedig end if restored.bridge then self.bridge = restored.bridge end if restored.lay then self.lay = restored.lay end if restored.digslope then self.digslope = restored.digslope end local inv_main if restored.stacks then -- TODO legacy support, can remove in a bit inv_main = restored.stacks end inv_main = restored.inv_main if inv_main then for i=1,#inv_main,1 do self.inventory:set_stack("main",i,inv_main[i]) end end local inv_materials = restored.inv_materials if inv_materials then for i=1,#inv_materials,1 do self.inventory:set_stack("materials",i,inv_materials[i]) end end end self.object:set_armor_groups(self.groups) end --- Get static data for saving state on deactivation function cartbase.get_staticdata_handler(self) local tostore = {} tostore.pos = self.pos tostore.speed = self.speed tostore.direction = self.direction tostore.tag = self.tag tostore.wait = self.wait tostore.loadwait = self.loadwait if self.carttype == "cargo" or self.carttype == "boring" then tostore.inv_main = cartbase.get_inv(self, "main") end if self.carttype == "boring" then tostore.inv_materials = cartbase.get_inv(self, "materials") tostore.dig = self.dig tostore.safedig = self.safedig tostore.bridge = self.bridge tostore.lay = self.lay tostore.digslope = self.digslope end return minetest.serialize(tostore) end function cartbase.get_inv(self, name) local st = {} local list = self.inventory:get_list(name) for i=1,#list,1 do table.insert(st,list[i]:to_string()) end return st end function cartbase.place_cart(item, pointed_thing, ent) if pointed_thing.type == "node" then local pos = pointed_thing.above pos.y = pos.y - 1 local railtype = get_railstatus(pos).railtype if railtype ~= "inv" then local obj = minetest.add_entity(pos, ent) if obj then if railtype == "x" then obj:setyaw(0) elseif railtype == "z" then obj:setyaw(0 + math.pi/2) end item:take_item() end end end return item end