built on 14/05/2021 19:19:09

This commit is contained in:
Joachim Stolberg 2021-05-14 19:19:09 +02:00
parent 0d7cb3f591
commit 0434f6b325
54 changed files with 4340 additions and 0 deletions

152
minecart/README.md Normal file
View File

@ -0,0 +1,152 @@
Minecart
========
**Minecart, the lean railway transportation automation system**
Browse on: [GitHub](https://github.com/joe7575/minecart)
Download: [GitHub](https://github.com/joe7575/minecart/archive/master.zip)
![minecart](https://github.com/joe7575/minecart/blob/master/screenshot.png)
Minecart is based on carts, which is
based almost entirely on the mod boost_cart [1], which
itself is based on (and fully compatible with) the carts mod [2].
The model was originally designed by stujones11 [3] (CC-0).
Cart textures are based on original work from PixelBOX by Gambit (permissive
license).
1. https://github.com/SmallJoker/boost_cart/
2. https://github.com/PilzAdam/carts/
3. https://github.com/stujones11/railcart/
Minecart Features
-----------------
The mod Minecart has its own cart (called Minecart) in addition to the standard cart.
Minecarts are used for automated item transport on private and public rail networks.
The mod features are:
- a fast cart for your railway or roller coaster (up to 8 m/s!)
- boost rails and speed limit signs
- rail junction switching with the 'right-left' walking keys
- configurable timetables and routes for Minecarts
- automated loading/unloading of Minecarts by means of a Minecart Hopper
- rail network protection based on protection blocks called Land Marks
- protection of minecarts and cargo
- Minecarts run through unloaded areas (only the stations/hopper have to be loaded)
- Extra Minecart privs for rail workers
- Ingame documentation (German and English), based on the mod "doc"
- API to register carts from other mods
- chat command '/mycart <num>' to output cart state and location
Technical Background
--------------------
The Minecart can "run" through unloaded areas. This is done by means of recorded
and stored routes. If the area is unloaded the cart will simply follow the
predefined route until an area is loaded again. In this case the cart will be
spawned and run as usual.
Introduction
------------
1. Place your rails and build a route with two endpoints. Junctions are allowed
as long as each route has its own start and endpoint.
2. Place a Railway Buffer at both endpoints. (buffers are always needed,
they store the route and timing information)
3. Give both Railway Buffers unique station names, like Oxford and Cambridge
4. Place a Minecart at a buffer and give it a cart number (1..999)
5. Drive from buffer to buffer in both directions using the Minecart(!) to record the
routes (use 'right-left' keys to control the Minecart)
6. Punch the buffers to check the connection data (e.g. "Oxford: connected to Cambridge")
7. Optional: Configure the Minecart waiting time in both buffers. The Minecart
will then start automatically after the configured time
8. Optional: Protect your rail network with the Protection Landmarks (one Landmark
at least every 16 nodes/meters)
9. Place a Minecart in front of the buffer and check whether it starts after the
configured time
10. Check the cart state via the chat command: /mycart <num>
'<num>' is the cart number, or get a list of carts with /mycart
11. Drop items into the Minecart and punch the cart to start it, or "sneak+click" the
Minecart to get cart and items back
Hopper
------
![hopper](https://github.com/joe7575/minecart/blob/master/hopper.png)
The Hopper is used to load/unload Minecarts.
The Hopper can pull and push items into/out off chests and can drop/pick up items
to/from Minecarts. To unload a Minecart place the hopper below the rail.
To load the Minecart, place the hopper right next to the Minecart.
Cart Pusher
-----------
Used to push a cart if the cart does not stop directly at a buffer.
The block has to be placed below the rail.
Cart Speed / Speed Limit Signs
------------------------------
As before, the speed of the carts is also influenced by power rails.
Brake rails are irrelevant, the cart does not brake here.
The maximum speed is 8 m/s. This assumes a ratio of power rails
to normal rails of 1 to 4 on a flat section of rail. A rail section is a
series of rail nodes without a change of direction. After every curve / kink,
the speed for the next section of the route is newly determined,
taking into account the swing of the cart. This means that a cart can
roll over short rail sections without power rails.
In order to additionally brake the cart at certain points
(at switches or in front of a buffer), speed limit signs can be placed
on the track. With these signs the speed can be reduced to 4, 2, or 1 m / s.
The "No speed limit" sign can be used to remove the speed limit.
The speed limit signs must be placed next to the track so that they can
be read from the cart. This allows different speeds in each direction of travel.
Migration to v2
---------------
The way how carts are monitored and the cart speed is calculated has changed.
Therefore, it is necessary that all carts are repositioned and the
recording is repeated.
Rails and buffers are not affected and can be kept unchanged.
History
-------
2019-04-19 v0.01 first commit
2019-04-21 v0.02 functional, with junctions support
2019-04-23 v0.03 bug fixes and improvements
2019-04-25 v0.04 Landmarks and Minecart protection added
2019-05-04 v0.05 Route recording protection added
2019-05-22 v0.06 Pick up items from a cart improved
2019-06-23 v0.07 'doc' mod support and German translation added
2020-01-04 v1.00 Hopper added, buffer improved
2020-02-09 v1.01 cart loading bugfix
2020-02-24 v1.02 Hopper improved
2020-03-05 v1.03 Hopper again improved
2020-03-28 v1.04 cart unloading bugfix
2020-05-14 v1.05 API changed to be able to register carts
2020-06-14 v1.06 API changed and chat command added
2020-06-27 v1.07 Route storage and cart command bugfixes
2020-07-24 V1.08 Adapted to new techage ICTA style
2020-08-14 V1.09 Hopper support for digtron, protector:chest and default:furnace added
2020-11-12 V1.10 Make carts more robust against server lag
2021-04-10 V2.00 Complete revision to make carts robust against server load/lag,
Speed limit signs and cart terminal added

52
minecart/api.lua Normal file
View File

@ -0,0 +1,52 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
--
-- API functions
--
-- 'pos' is the position of the puncher/sensor, the cart
-- position will be determined by means of 'param2' and 'radius'
function minecart.is_cart_available(pos, param2, radius)
local pos2 = minecart.get_nodecart_nearby(pos, param2, radius)
if pos2 then
return true
end
-- The entity check is needed for a cart with driver
local entity = minecart.get_entitycart_nearby(pos, param2, radius)
if entity then
return true
end
end
function minecart.is_nodecart_available(pos, param2, radius)
local pos2 = minecart.get_nodecart_nearby(pos, param2, radius)
if pos2 then
return true
end
end
-- 'pos' is the position of the puncher/sensor, the cart
-- position will be determined by means of 'param2' and 'radius'
function minecart.punch_cart(pos, param2, radius, punch_dir)
local pos2, node = minecart.get_nodecart_nearby(pos, param2, radius)
if pos2 then
minecart.start_nodecart(pos2, node.name, nil, punch_dir)
return true
end
-- The entity check is needed for a cart with driver
local entity = minecart.get_entitycart_nearby(pos, param2, radius)
if entity and entity.driver then
minecart.push_entitycart(entity, punch_dir)
return true
end
end

360
minecart/baselib.lua Normal file
View File

@ -0,0 +1,360 @@
--[[
Minecart
========
Copyright (C) 2019-2020 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 param2_to_dir = {[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}
}
-- Registered carts
minecart.tNodeNames = {} -- [<cart_node_name>] = <cart_entity_name>
minecart.tEntityNames = {} -- [<cart_entity_name>] = true
minecart.lCartNodeNames = {} -- {<cart_node_name>, <cart_node_name>, ...}
minecart.tCartTypes = {}
function minecart.param2_to_dir(param2)
return param2_to_dir[param2 % 6]
end
function minecart.get_node_lvm(pos)
local node = minetest.get_node_or_nil(pos)
if node then
return node
end
local vm = minetest.get_voxel_manip()
local MinEdge, MaxEdge = vm:read_from_map(pos, pos)
local data = vm:get_data()
local param2_data = vm:get_param2_data()
local area = VoxelArea:new({MinEdge = MinEdge, MaxEdge = MaxEdge})
local idx = area:indexp(pos)
if data[idx] and param2_data[idx] then
return {
name = minetest.get_name_from_content_id(data[idx]),
param2 = param2_data[idx]
}
end
return {name="ignore", param2=0}
end
function minecart.find_node_near_lvm(pos, radius, items)
local npos = minetest.find_node_near(pos, radius, items)
if npos then
return npos
end
local tItems = {}
for _,v in ipairs(items) do
tItems[v] = true
end
local pos1 = {x = pos.x - radius, y = pos.y - radius, z = pos.z - radius}
local pos2 = {x = pos.x + radius, y = pos.y + radius, z = pos.z + radius}
local vm = minetest.get_voxel_manip()
local MinEdge, MaxEdge = vm:read_from_map(pos1, pos2)
local data = vm:get_data()
local area = VoxelArea:new({MinEdge = MinEdge, MaxEdge = MaxEdge})
for x = pos1.x, pos2.x do
for y = pos1.y, pos2.y do
for z = pos1.z, pos2.z do
local idx = area:indexp({x = x, y = y, z = z})
if minetest.get_name_from_content_id(data[idx]) then
return {x = x, y = y, z = z}
end
end
end
end
end
-- Marker entities for debugging purposes
function minecart.set_marker(pos, text, size, ttl)
local marker = minetest.add_entity(pos, "minecart:marker_cube")
if marker ~= nil then
marker:set_nametag_attributes({color = "#FFFFFF", text = text})
size = size or 1
marker:set_properties({visual_size = {x = size, y = size}})
if ttl then
minetest.after(ttl, marker.remove, marker)
end
end
end
minetest.register_entity(":minecart:marker_cube", {
initial_properties = {
visual = "cube",
textures = {
"minecart_marker_cube.png",
"minecart_marker_cube.png",
"minecart_marker_cube.png",
"minecart_marker_cube.png",
"minecart_marker_cube.png",
"minecart_marker_cube.png",
},
physical = false,
visual_size = {x = 1, y = 1},
collisionbox = {-0.25,-0.25,-0.25, 0.25,0.25,0.25},
glow = 8,
static_save = false,
},
on_punch = function(self)
self.object:remove()
end,
})
function minecart.is_air_like(name)
local ndef = minetest.registered_nodes[name]
if ndef and ndef.buildable_to then
return true
end
return false
end
function minecart.range(val, min, max)
val = tonumber(val)
if val < min then return min end
if val > max then return max end
return val
end
function minecart.get_next_node(pos, param2)
local pos2 = param2 and vector.add(pos, param2_to_dir[param2]) or pos
local node = minetest.get_node(pos2)
return pos2, node
end
function minecart.get_object_id(object)
for id, entity in pairs(minetest.luaentities) do
if entity.object == object then
return id
end
end
end
function minecart.is_owner(player, owner)
if not player or not player:is_player() or not owner or owner == "" then
return true
end
local name = player:get_player_name()
if minetest.check_player_privs(name, "minecart") then
return true
end
return name == owner
end
function minecart.get_buffer_pos(pos, player_name)
local pos1 = minecart.find_node_near_lvm(pos, 1, {"minecart:buffer"})
if pos1 then
local meta = minetest.get_meta(pos1)
if player_name == nil or player_name == meta:get_string("owner") then
return pos1
end
end
end
function minecart.get_buffer_name(pos)
local pos1 = minecart.find_node_near_lvm(pos, 1, {"minecart:buffer"})
if pos1 then
local name = M(pos1):get_string("name")
if name ~= "" then
return name
end
return P2S(pos1)
end
end
function minecart.manage_attachment(player, entity, get_on)
if not player then
return
end
local player_name = player:get_player_name()
if player_api.player_attached[player_name] == get_on then
return
end
player_api.player_attached[player_name] = get_on
local obj = entity.object
if get_on then
player:set_attach(obj, "", {x=0, y=-4.5, z=-4}, {x=0, y=0, z=0})
player:set_eye_offset({x=0, y=-6, z=0},{x=0, y=-6, z=0})
player:set_properties({visual_size = {x = 2.5, y = 2.5}})
player_api.set_animation(player, "sit")
entity.driver = player:get_player_name()
else
player:set_detach()
player:set_eye_offset({x=0, y=0, z=0},{x=0, y=0, z=0})
player:set_properties({visual_size = {x = 1, y = 1}})
player_api.set_animation(player, "stand")
entity.driver = nil
end
end
function minecart.register_cart_names(node_name, entity_name, cart_type)
minecart.tNodeNames[node_name] = entity_name
minecart.tEntityNames[entity_name] = true
minecart.lCartNodeNames[#minecart.lCartNodeNames+1] = node_name
minecart.add_raillike_nodes(node_name)
minecart.tCartTypes[node_name] = cart_type
end
function minecart.add_nodecart(pos, node_name, param2, cargo, owner, userID)
if pos and node_name and param2 and cargo and owner and userID then
local pos2
if not minecart.is_rail(pos) then
pos2 = minetest.find_node_near(pos, 1, minecart.lRails)
if not pos2 or not minecart.is_rail(pos2) then
pos2 = minetest.find_node_near(pos, 2, minecart.lRails)
if not pos2 or not minecart.is_rail(pos2) then
pos2 = minetest.find_node_near(pos, 2, {"air"})
end
end
else
pos2 = vector.new(pos)
end
if pos2 then
local node = minetest.get_node(pos2)
local ndef = minetest.registered_nodes[node_name]
local rail = node.name
minetest.swap_node(pos2, {name = node_name, param2 = param2})
local meta = M(pos2)
meta:set_string("removed_rail", rail)
meta:set_string("owner", owner)
meta:set_int("userID", userID)
meta:set_string("infotext", owner .. ": " .. userID)
if cargo and ndef.set_cargo then
ndef.set_cargo(pos2, cargo)
end
if ndef.after_place_node then
ndef.after_place_node(pos2)
end
return pos2
else
minetest.add_item(pos, ItemStack({name = node_name}))
end
end
end
function minecart.add_entitycart(pos, node_name, entity_name, vel, cargo, owner, userID)
local obj = minetest.add_entity(pos, entity_name)
local objID = minecart.get_object_id(obj)
if objID then
local entity = obj:get_luaentity()
entity.start_pos = pos
entity.owner = owner
entity.node_name = node_name
entity.userID = userID
entity.objID = objID
entity.cargo = cargo
obj:set_nametag_attributes({color = "#ffff00", text = owner..": "..userID})
obj:set_velocity(vel)
return obj
end
end
function minecart.start_entitycart(self, pos)
local route = {}
self.is_running = true
self.arrival_time = 0
self.start_pos = minecart.get_buffer_pos(pos, self.owner)
if self.start_pos then
-- Read buffer route for the junction info
route = minecart.get_route(self.start_pos) or {}
self.junctions = route and route.junctions
end
-- If set the start waypoint will be deleted
self.no_normal_start = self.start_pos == nil
if self.driver == nil then
minecart.start_monitoring(self.owner, self.userID, pos, self.objID,
route.checkpoints, route.junctions, self.cargo or {})
end
end
function minecart.remove_nodecart(pos)
local node = minetest.get_node(pos)
local ndef = minetest.registered_nodes[node.name]
local meta = M(pos)
local rail = meta:get_string("removed_rail")
if rail == "" then rail = "air" end
local userID = meta:get_int("userID")
local owner = meta:get_string("owner")
meta:set_string("infotext", "")
meta:set_string("formspec", "")
local cargo = ndef.get_cargo and ndef.get_cargo(pos) or {}
minetest.swap_node(pos, {name = rail})
return cargo, owner, userID
end
function minecart.node_to_entity(pos, node_name, entity_name)
-- Remove node
local cargo, owner, userID = minecart.remove_nodecart(pos)
local obj = minecart.add_entitycart(pos, node_name, entity_name,
{x = 0, y = 0, z = 0}, cargo, owner, userID)
if obj then
return obj
else
print("Entity has no ID")
end
end
function minecart.entity_to_node(pos, entity)
-- Stop sound
if entity.sound_handle then
minetest.sound_stop(entity.sound_handle)
entity.sound_handle = nil
end
local rot = entity.object:get_rotation()
local dir = minetest.yaw_to_dir(rot.y)
local facedir = minetest.dir_to_facedir(dir)
minecart.stop_recording(entity, pos)
entity.object:remove()
local pos2 = minecart.add_nodecart(pos, entity.node_name, facedir, entity.cargo, entity.owner, entity.userID)
minecart.stop_monitoring(entity.owner, entity.userID, pos2)
end
function minecart.add_node_to_player_inventory(pos, player, node_name)
local inv = player:get_inventory()
if not (creative and creative.is_enabled_for
and creative.is_enabled_for(player:get_player_name()))
or not inv:contains_item("main", node_name) then
local leftover = inv:add_item("main", node_name)
-- If no room in inventory, drop the cart
if not leftover:is_empty() then
minetest.add_item(pos, leftover)
end
end
end
-- Player removes the node
function minecart.remove_entity(self, pos, player)
-- Stop sound
if self.sound_handle then
minetest.sound_stop(self.sound_handle)
self.sound_handle = nil
end
minecart.add_node_to_player_inventory(pos, player, self.node_name or "minecart:cart")
minecart.stop_monitoring(self.owner, self.userID, pos)
minecart.stop_recording(self, pos)
minecart.monitoring_remove_cart(self.owner, self.userID)
self.object:remove()
end

162
minecart/buffer.lua Normal file
View File

@ -0,0 +1,162 @@
--[[
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 S2P = minetest.string_to_pos
local S = minecart.S
local CYCLE_TIME = 2
local StopTime = {}
local function formspec(pos)
local name = M(pos):get_string("name")
local time = M(pos):get_int("time")
return "size[4,4.2]" ..
"label[0,0;Configuration]" ..
"field[0.5,1.2;3.6,1;name;"..S("Station name")..":;"..name.."]"..
"button_exit[1,3.4;2,1;exit;Save]"..
"field[0.5,2.5;3.6,1;time;"..S("Waiting time/sec")..":;"..time.."]"
end
local function remote_station_name(pos)
local route = minecart.get_route(pos)
if route and route.dest_pos then
return M(route.dest_pos):get_string("name")
end
return "none"
end
local function on_punch(pos, node, puncher)
local name = M(pos):get_string("name")
M(pos):set_string("infotext", name..": "..S("connected to").." "..remote_station_name(pos))
M(pos):set_string("formspec", formspec(pos))
minetest.get_node_timer(pos):start(CYCLE_TIME)
-- Optional Teleport function
if not minecart.teleport_enabled then return end
local route = minecart.get_route(pos)
if route and route.dest_pos and puncher and puncher:is_player() then
-- only teleport if the user is not pressing shift
if not puncher:get_player_control()['sneak'] then
local playername = puncher:get_player_name()
local teleport = function()
-- Make sure the player object still exists
local player = minetest.get_player_by_name(playername)
if player then player:set_pos(route.dest_pos) end
end
minetest.after(0.25, teleport)
end
end
end
minetest.register_node("minecart:buffer", {
description = S("Minecart Railway Buffer"),
tiles = {
'default_junglewood.png',
'default_junglewood.png',
'default_junglewood.png',
'default_junglewood.png',
'default_junglewood.png',
'default_junglewood.png^minecart_buffer.png',
},
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{-8/16, -8/16, -8/16, 8/16, -4/16, 8/16},
{-8/16, -4/16, -8/16, 8/16, 0/16, 4/16},
{-8/16, 0/16, -8/16, 8/16, 4/16, 0/16},
{-8/16, 4/16, -8/16, 8/16, 8/16, -4/16},
},
},
selection_box = {
type = "fixed",
fixed = {-8/16, -8/16, -8/16, 8/16, 8/16, 8/16},
},
after_place_node = function(pos, placer)
M(pos):set_string("owner", placer:get_player_name())
minecart.del_route(pos)
M(pos):set_string("formspec", formspec(pos))
minetest.get_node_timer(pos):start(CYCLE_TIME)
end,
on_timer = function(pos, elapsed)
local time = M(pos):get_int("time")
if time > 0 then
local hash = minetest.hash_node_position(pos)
local param2 = (minetest.get_node(pos).param2 + 2) % 4
if minecart.is_cart_available(pos, param2, 0.5) then
if StopTime[hash] then
if StopTime[hash] < minetest.get_gametime() then
StopTime[hash] = nil
local dir = minetest.facedir_to_dir(param2)
minecart.punch_cart(pos, param2, 0.5, dir)
end
else
StopTime[hash] = minetest.get_gametime() + time
end
else
StopTime[hash] = nil
end
end
return true
end,
after_dig_node = function(pos)
minecart.del_route(pos)
local hash = minetest.hash_node_position(pos)
StopTime[hash] = nil
end,
on_receive_fields = function(pos, formname, fields, player)
if M(pos):get_string("owner") ~= player:get_player_name() then
return
end
if (fields.key_enter == "true" or fields.exit == "Save") and fields.name ~= "" then
M(pos):set_string("name", fields.name)
M(pos):set_int("time", tonumber(fields.time) or 0)
M(pos):set_string("formspec", formspec(pos))
M(pos):set_string("infotext", fields.name.." "..S("connected to").." "..remote_station_name(pos))
minetest.get_node_timer(pos):start(CYCLE_TIME)
end
end,
on_punch = on_punch,
paramtype = "light",
sunlight_propagates = true,
on_rotate = screwdriver.disallow,
paramtype2 = "facedir",
groups = {cracky=2, crumbly=2, choppy=2},
is_ground_content = false,
sounds = default.node_sound_wood_defaults(),
})
minetest.register_craft({
output = "minecart:buffer",
recipe = {
{"dye:red", "", "dye:white"},
{"default:steel_ingot", "default:junglewood", "default:steel_ingot"},
},
})
minetest.register_lbm({
label = "Delete waiting times",
name = "minecart:del_time",
nodenames = {"minecart:buffer"},
run_at_every_load = false,
action = function(pos, node)
-- delete old data
minecart.get_route(pos)
M(pos):set_string("formspec", formspec(pos))
end,
})

4
minecart/depends.txt Normal file
View File

@ -0,0 +1,4 @@
default
carts
screwdriver
doc?

131
minecart/doc.lua Normal file
View File

@ -0,0 +1,131 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
minecart.doc = {}
if not minetest.get_modpath("doc") then
return
end
local S = minecart.S
local summary_doc = table.concat({
S("Summary"),
"------------",
"",
S("1. Place your rails and build a route with two endpoints. Junctions are allowed as long as each route has its own start and endpoint."),
S("2. Place a Railway Buffer at both endpoints (buffers are always needed, they store the route and timing information)."),
S("3. Give both Railway Buffers unique station names, like Oxford and Cambridge."),
S("4. Place a Minecart at a buffer and give it a cart number (1..999)"),
S("5. Drive from buffer to buffer in both directions using the Minecart(!) to record the routes (use 'right-left' keys to control the Minecart)."),
S("6. Punch the buffers to check the connection data (e.g. 'Oxford: connected to Cambridge')."),
S("7. Optional: Configure the Minecart waiting time in both buffers. The Minecart will then start automatically after the configured time."),
S("8. Optional: Protect your rail network with the Protection Landmarks (one Landmark at least every 16 nodes/meters)."),
S("9. Place a Minecart in front of the buffer and check whether it starts after the configured time."),
S("10. Check the cart state via the chat command: /mycart <num>\n '<num>' is the cart number"),
S("11. Drop items into the Minecart and punch the cart to start it."),
S("12. Dig the cart with 'sneak+click' (as usual). The items will be drop down."),
}, "\n")
local cart_doc = S("Primary used to transport items. You can drop items into the Minecart and punch the cart to get started. Sneak+click the cart to get cart and items back")
local buffer_doc = S("Used as buffer on both rail ends. Needed to be able to record the cart routes")
local landmark_doc = S("Protect your rails with the Landmarks (one Landmark at least every 16 blocks near the rail)")
local hopper_doc = S("Used to load/unload Minecart. The Hopper can push/pull items to/from chests and drop/pickup items to/from Minecarts. To unload a Minecart place the hopper below the rail. To load the Minecart, place the hopper right next to the Minecart.")
local pusher_doc = S([[If several carts are running on one route,
it can happen that a buffer position is already occupied and one cart therefore stops earlier.
In this case, the cart pusher is used to push the cart towards the buffer again.
This block must be placed under the rail at a distance of 2 m in front of the buffer.]])
local speed_doc = S([[Limit the cart speed with speed limit signs.
As before, the speed of the carts is also influenced by power rails.
Brake rails are irrelevant, the cart does not brake here.
The maximum speed is 8 m/s. This assumes a ratio of power rails
to normal rails of 1 to 4 on a flat section of rail. A rail section is a
series of rail nodes without a change of direction. After every curve / kink,
the speed for the next section of the route is newly determined,
taking into account the swing of the cart. This means that a cart can
roll over short rail sections without power rails.
In order to additionally brake the cart at certain points
(at switches or in front of a buffer), speed limit signs can be placed
on the track. With these signs the speed can be reduced to 4, 2, or 1 m / s.
The "No speed limit" sign can be used to remove the speed limit.
The speed limit signs must be placed next to the track so that they can
be read from the cart. This allows different speeds in each direction of travel.]])
local function formspec(data)
if data.image then
local image = "image["..(doc.FORMSPEC.ENTRY_WIDTH - 3)..",0;3,2;"..data.image.."]"
local formstring = doc.widgets.text(data.text, doc.FORMSPEC.ENTRY_START_X, doc.FORMSPEC.ENTRY_START_Y+1.6, doc.FORMSPEC.ENTRY_WIDTH, doc.FORMSPEC.ENTRY_HEIGHT - 1.6)
return image..formstring
elseif data.item then
local box = "box["..(doc.FORMSPEC.ENTRY_WIDTH - 1.6)..",0;1,1.1;#BBBBBB]"
local image = "item_image["..(doc.FORMSPEC.ENTRY_WIDTH - 1.5)..",0.1;1,1;"..data.item.."]"
local formstring = doc.widgets.text(data.text, doc.FORMSPEC.ENTRY_START_X, doc.FORMSPEC.ENTRY_START_Y+0.8, doc.FORMSPEC.ENTRY_WIDTH, doc.FORMSPEC.ENTRY_HEIGHT - 0.8)
return box..image..formstring
else
return doc.entry_builders.text(data.text)
end
end
doc.add_category("minecart",
{
name = S("Minecart"),
description = S("Minecart, the lean railway transportation automation system"),
sorting = "custom",
sorting_data = {"summary", "cart"},
build_formspec = formspec,
})
doc.add_entry("minecart", "summary", {
name = S("Summary"),
data = {text=summary_doc, image="minecart_doc_image.png"},
})
doc.add_entry("minecart", "cart", {
name = S("Minecart Cart"),
data = {text=cart_doc, item="minecart:cart"},
})
doc.add_entry("minecart", "buffer", {
name = S("Minecart Railway Buffer"),
data = {text=buffer_doc, item="minecart:buffer"},
})
doc.add_entry("minecart", "landmark", {
name = S("Minecart Landmark"),
data = {text = landmark_doc, item="minecart:landmark"},
})
doc.add_entry("minecart", "speed signs", {
name = S("Minecart Speed Signs"),
data = {text = speed_doc, item="minecart:speed4"},
})
doc.add_entry("minecart", "cart pusher", {
name = S("Cart Pusher"),
data = {text = pusher_doc, item="minecart:cart_pusher"},
})
if minecart.hopper_enabled then
doc.add_entry("minecart", "hopper", {
name = S("Minecart Hopper"),
data = {text=hopper_doc, item="minecart:hopper"},
})
end

318
minecart/entitylib.lua Normal file
View File

@ -0,0 +1,318 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
local P2S = function(pos) if pos then return minetest.pos_to_string(pos) end end
local P2H = minetest.hash_node_position
local H2P = minetest.get_position_from_hash
local MAX_SPEED = minecart.MAX_SPEED
local dot2dir = minecart.dot2dir
local get_waypoint = minecart.get_waypoint
local recording_waypoints = minecart.recording_waypoints
local recording_junctions = minecart.recording_junctions
local set_junctions = minecart.set_junctions
local player_ctrl = minecart.player_ctrl
local tEntityNames = minecart.tEntityNames
local function stop_cart(self, cart_pos)
self.is_running = false
self.arrival_time = 0
if self.driver then
local player = minetest.get_player_by_name(self.driver)
if player then
minecart.stop_recording(self, cart_pos)
minecart.manage_attachment(player, self, false)
end
end
if not minecart.get_buffer_pos(cart_pos, self.owner) then
-- Probably somewhere in the pampas
minecart.delete_cart_waypoint(cart_pos)
end
minecart.entity_to_node(cart_pos, self)
end
local function get_ctrl(self, pos)
-- Use player ctrl or junction data from recorded routes
return (self.driver and self.ctrl) or (self.junctions and self.junctions[P2H(pos)]) or {}
end
local function new_speed(self, new_dir)
self.cart_speed = self.cart_speed or 0
local rail_speed = (self.waypoint.speed or 0) / 10
if rail_speed <= 0 then
rail_speed = math.max(self.cart_speed + rail_speed, 0)
elseif rail_speed <= self.cart_speed then
rail_speed = math.max((self.cart_speed + rail_speed) / 2, 0)
end
-- Speed corrections
if new_dir.y == 1 then
if rail_speed < 1 then rail_speed = 0 end
else
if rail_speed < 0.4 then rail_speed = 0 end
end
self.cart_speed = rail_speed -- store for next cycle
return rail_speed
end
local function running(self)
local rot = self.object:get_rotation()
local dir = minetest.yaw_to_dir(rot.y)
dir.y = math.floor((rot.x / (math.pi/4)) + 0.5)
dir = vector.round(dir)
local facedir = minetest.dir_to_facedir(dir)
local cart_pos, wayp_pos, is_junction
if self.reenter then -- through monitoring
cart_pos = H2P(self.reenter[1])
wayp_pos = cart_pos
is_junction = false
self.waypoint = {pos = H2P(self.reenter[2]), power = 0, dot = self.reenter[4]}
self.cart_speed = self.reenter[3]
self.speed_limit = MAX_SPEED
self.reenter = nil
elseif not self.waypoint then
-- get waypoint
cart_pos = vector.round(self.object:get_pos())
wayp_pos = cart_pos
is_junction = false
self.waypoint = get_waypoint(cart_pos, facedir, get_ctrl(self, cart_pos), true)
if self.no_normal_start then
-- Probably somewhere in the pampas
minecart.delete_waypoint(cart_pos)
self.no_normal_start = nil
end
self.cart_speed = 2 -- push speed
self.speed_limit = MAX_SPEED
else
-- next waypoint
cart_pos = vector.new(self.waypoint.cart_pos or self.waypoint.pos)
wayp_pos = self.waypoint.pos
local vel = self.object:get_velocity()
self.waypoint, is_junction = get_waypoint(wayp_pos, facedir, get_ctrl(self, wayp_pos), self.cart_speed < 0.1)
end
if not self.waypoint then
stop_cart(self, wayp_pos)
return
end
if is_junction then
if self.is_recording then
set_junctions(self, wayp_pos)
end
self.ctrl = nil
end
--print("dist", P2S(cart_pos), P2S(self.waypoint.pos), P2S(self.waypoint.cart_pos), self.waypoint.dot)
local dist = vector.distance(cart_pos, self.waypoint.cart_pos or self.waypoint.pos)
local new_dir = dot2dir(self.waypoint.dot)
local new_speed = new_speed(self, new_dir)
local straight_ahead = vector.equals(new_dir, dir)
-- If straight_ahead, then it's probably a speed limit sign
if straight_ahead then
self.speed_limit = minecart.get_speedlimit(wayp_pos, facedir) or self.speed_limit
end
new_speed = math.min(new_speed, self.speed_limit)
local new_cart_pos, extra_cycle = minecart.get_current_cart_pos_correction(
wayp_pos, facedir, dir.y, self.waypoint.dot) -- TODO: Why has self.waypoint no dot?
if extra_cycle and not vector.equals(cart_pos, new_cart_pos) then
self.waypoint = {pos = wayp_pos, cart_pos = new_cart_pos}
new_dir = vector.direction(cart_pos, new_cart_pos)
dist = vector.distance(cart_pos, new_cart_pos)
--print("extra_cycle", P2S(cart_pos), P2S(wayp_pos), P2S(new_cart_pos), new_speed)
end
-- Slope corrections
--print("Slope corrections", P2S(new_dir), P2S(cart_pos))
if new_dir.y ~= 0 then
cart_pos.y = cart_pos.y + 0.2
end
-- Calc velocity, rotation and arrival_time
local yaw = minetest.dir_to_yaw(new_dir)
local pitch = new_dir.y * math.pi/4
--print("new_speed", new_speed / (new_dir.y ~= 0 and 1.41 or 1))
local vel = vector.multiply(new_dir, new_speed / ((new_dir.y ~= 0) and 1.41 or 1))
self.arrival_time = self.timebase + (dist / new_speed)
-- needed for recording
self.curr_speed = new_speed
self.num_sections = (self.num_sections or 0) + 1
-- Got stuck somewhere
if new_speed < 0.1 or dist < 0 then
print("Got stuck somewhere", new_speed, dist)
stop_cart(self, wayp_pos)
return
end
self.object:set_pos(cart_pos)
self.object:set_rotation({x = pitch, y = yaw, z = 0})
self.object:set_velocity(vel)
return
end
local function play_sound(self)
if self.sound_handle then
local handle = self.sound_handle
self.sound_handle = nil
minetest.after(0.2, minetest.sound_stop, handle)
end
if self.object then
self.sound_handle = minetest.sound_play(
"carts_cart_moving", {
object = self.object,
gain = self.curr_speed / MAX_SPEED,
})
end
end
local function on_step(self, dtime)
self.timebase = (self.timebase or 0) + dtime
if self.is_running then
if self.arrival_time <= self.timebase then
running(self)
end
if (self.sound_ttl or 0) <= self.timebase then
play_sound(self)
self.sound_ttl = self.timebase + 1.0
end
else
if self.sound_handle then
minetest.sound_stop(self.sound_handle)
self.sound_handle = nil
end
end
if self.driver then
if self.is_recording then
if self.rec_time <= self.timebase then
recording_waypoints(self)
self.rec_time = self.rec_time + 2.0
end
recording_junctions(self)
else
player_ctrl(self)
end
end
end
local function on_entitycard_activate(self, staticdata, dtime_s)
self.object:set_armor_groups({immortal=1})
end
-- Start the entity cart (or dig by shift+leftclick)
local function on_entitycard_punch(self, puncher, time_from_last_punch, tool_capabilities, dir)
if minecart.is_owner(puncher, self.owner) then
if puncher:get_player_control().sneak then
if not self.only_dig_if_empty or not next(self.cargo) then
-- drop items
local pos = vector.round(self.object:get_pos())
for _,item in ipairs(self.cargo or {}) do
minetest.add_item(pos, ItemStack(item))
end
-- Dig cart
if self.driver then
-- remove cart as driver
minecart.stop_recording(self, pos)
minecart.monitoring_remove_cart(self.owner, self.userID)
minecart.remove_entity(self, pos, puncher)
minecart.manage_attachment(puncher, self, false)
else
-- remove cart from outside
minecart.monitoring_remove_cart(self.owner, self.userID)
minecart.remove_entity(self, pos, puncher)
end
end
elseif not self.is_running then
-- start the cart
local pos = vector.round(self.object:get_pos())
if puncher then
local yaw = puncher:get_look_horizontal()
self.object:set_rotation({x = 0, y = yaw, z = 0})
end
minecart.start_entitycart(self, pos)
minecart.start_recording(self, pos)
end
end
end
-- Player get on / off
local function on_entitycard_rightclick(self, clicker)
if clicker and clicker:is_player() and self.driver_allowed then
-- Get on / off
if self.driver then
-- get off
local pos = vector.round(self.object:get_pos())
minecart.manage_attachment(clicker, self, false)
minecart.entity_to_node(pos, self)
else
-- get on
local pos = vector.round(self.object:get_pos())
minecart.stop_recording(self, pos)
minecart.manage_attachment(clicker, self, true)
end
end
end
local function on_entitycard_detach_child(self, child)
if child and child:get_player_name() == self.driver then
self.driver = nil
end
end
function minecart.get_entitycart_nearby(pos, param2, radius)
local pos2 = param2 and vector.add(pos, minecart.param2_to_dir(param2)) or pos
for _, object in pairs(minetest.get_objects_inside_radius(pos2, radius or 0.5)) do
local entity = object:get_luaentity()
if entity and entity.name and tEntityNames[entity.name] then
local vel = object:get_velocity()
if vector.equals(vel, {x=0, y=0, z=0}) then -- still standing?
return entity
end
end
end
end
function minecart.push_entitycart(self, punch_dir)
--print("push_entitycart")
local vel = self.object:get_velocity()
punch_dir.y = 0
local yaw = minetest.dir_to_yaw(punch_dir)
self.object:set_rotation({x = 0, y = yaw, z = 0})
self.is_running = true
self.arrival_time = 0
end
function minecart.register_cart_entity(entity_name, node_name, cart_type, entity_def)
entity_def.entity_name = entity_name
entity_def.node_name = node_name
entity_def.on_activate = on_entitycard_activate
entity_def.on_punch = on_entitycard_punch
entity_def.on_step = on_step
entity_def.on_rightclick = on_entitycard_rightclick
entity_def.on_detach_child = on_entitycard_detach_child
entity_def.owner = nil
entity_def.driver = nil
entity_def.cargo = {}
minetest.register_entity(entity_name, entity_def)
-- register node for punching
minecart.register_cart_names(node_name, entity_name, cart_type)
end

172
minecart/hopper.lua Normal file
View File

@ -0,0 +1,172 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
local NUM_ITEMS = 4
-- 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
local function scan_for_objects(pos, inv)
for _, object in pairs(minetest.get_objects_inside_radius(pos, 1)) do
local lua_entity = object:get_luaentity()
if not object:is_player() and lua_entity and lua_entity.name == "__builtin:item" then
if lua_entity.itemstring ~= "" then
local stack = ItemStack(lua_entity.itemstring)
if inv:room_for_item("main", stack) then
inv:add_item("main", stack)
object:remove()
end
end
end
end
end
local function pull_push_item(pos, param2)
local items = minecart.take_items(pos, param2, NUM_ITEMS)
if items then
local leftover = minecart.put_items(pos, param2, items)
if leftover then
-- place item back
minecart.untake_items(pos, param2, leftover)
return false
end
return true
else
items = minecart.take_items({x=pos.x, y=pos.y+1, z=pos.z}, nil, NUM_ITEMS)
if items then
local leftover = minecart.put_items(pos, param2, items)
if leftover then
-- place item back
minecart.untake_items({x=pos.x, y=pos.y+1, z=pos.z}, nil, leftover)
return false
end
return true
end
end
return false
end
local function push_item(pos, inv, param2)
local taken = minecart.inv_take_items(inv, "main", NUM_ITEMS)
if taken then
local leftover = minecart.put_items(pos, param2, taken)
if leftover then
inv:add_item("main", leftover)
end
end
end
local formspec = "size[8,6.5]"..
"list[context;main;3,0;2,2;]"..
"list[current_player;main;0,2.7;8,4;]"..
"listring[context;main]"..
"listring[current_player;main]"
minetest.register_node("minecart:hopper", {
description = S("Minecart Hopper"),
tiles = {
-- up, down, right, left, back, front
"default_cobble.png^minecart_appl_hopper_top.png",
"default_cobble.png^minecart_appl_hopper.png",
"default_cobble.png^minecart_appl_hopper_right.png",
"default_cobble.png^minecart_appl_hopper.png",
"default_cobble.png^minecart_appl_hopper.png",
"default_cobble.png^minecart_appl_hopper.png",
},
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{-8/16, 2/16, -8/16, 8/16, 8/16, -6/16},
{-8/16, 2/16, 6/16, 8/16, 8/16, 8/16},
{-8/16, 2/16, -8/16, -6/16, 8/16, 8/16},
{ 6/16, 2/16, -8/16, 8/16, 8/16, 8/16},
{-6/16, 0/16, -6/16, 6/16, 3/16, 6/16},
{-5/16, -4/16, -5/16, 5/16, 0/16, 5/16},
{ 0/16, -4/16, -3/16, 11/16, 2/16, 3/16},
},
},
selection_box = {
type = "fixed",
fixed = {
{-8/16, 2/16, -8/16, 8/16, 8/16, 8/16},
{-5/16, -4/16, -5/16, 5/16, 0/16, 5/16},
{ 0/16, -4/16, -3/16, 11/16, 2/16, 3/16},
},
},
on_construct = function(pos)
local inv = M(pos):get_inventory()
inv:set_size('main', 4)
end,
after_place_node = function(pos, placer)
local meta = M(pos)
meta:set_string("owner", placer:get_player_name())
meta:set_string("formspec", formspec)
minetest.get_node_timer(pos):start(2)
end,
on_timer = function(pos, elapsed)
local inv = M(pos):get_inventory()
local param2 = minetest.get_node(pos).param2
param2 = (param2 + 1) % 4 -- output is on the right
if not pull_push_item(pos, param2) then
scan_for_objects({x=pos.x, y=pos.y+1, z=pos.z}, inv)
push_item(pos, inv, param2)
end
return true
end,
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
if minetest.is_protected(pos, player:get_player_name()) then
return 0
end
minetest.get_node_timer(pos):start(2)
return stack:get_count()
end,
allow_metadata_inventory_take = function(pos, listname, index, stack, player)
if minetest.is_protected(pos, player:get_player_name()) then
return 0
end
return stack:get_count()
end,
after_dig_node = function(pos, oldnode, oldmetadata, digger)
for _,stack in ipairs(oldmetadata.inventory.main) do
minetest.add_item(pos, stack)
end
end,
paramtype = "light",
sunlight_propagates = true,
paramtype2 = "facedir",
use_texture_alpha = minecart.CLIP,
groups = {choppy=2, cracky=2, crumbly=2},
is_ground_content = false,
sounds = default.node_sound_wood_defaults(),
})
minetest.register_craft({
output = "minecart:hopper",
recipe = {
{"default:stone", "", "default:stone"},
{"default:stone", "default:gold_ingot", "default:stone"},
{"", "default:stone", ""},
},
})

BIN
minecart/hopper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

141
minecart/hopperlib.lua Normal file
View File

@ -0,0 +1,141 @@
--[[
Minecart
========
Copyright (C) 2019-2020 Joachim Stolberg
MIT
See license.txt for more information
]]--
-- for lazy programmers
local M = minetest.get_meta
local RegisteredInventories = {}
-- Take the given number of items from the inv.
-- Returns nil if ItemList is empty.
function minecart.inv_take_items(inv, listname, num)
if inv:is_empty(listname) then
return nil
end
local size = inv:get_size(listname)
for idx = 1, size do
local items = inv:get_stack(listname, idx)
if items:get_count() > 0 then
local taken = items:take_item(num)
inv:set_stack(listname, idx, items)
return taken
end
end
return nil
end
function minecart.take_items(pos, param2, num)
local npos, node
if param2 then
npos, node = minecart.get_next_node(pos, (param2 + 2) % 4)
else
npos, node = pos, minetest.get_node(pos)
end
local def = RegisteredInventories[node.name]
local owner = M(pos):get_string("owner")
local inv = minetest.get_inventory({type="node", pos=npos})
if def and inv and def.take_listname and (not def.allow_take or def.allow_take(npos, nil, owner)) then
return minecart.inv_take_items(inv, def.take_listname, num)
elseif def and def.take_item then
return def.take_item(npos, num, owner)
else
local ndef = minetest.registered_nodes[node.name]
if ndef and ndef.minecart_hopper_takeitem then
return ndef.minecart_hopper_takeitem(npos, num)
end
end
end
function minecart.put_items(pos, param2, stack)
local npos, node = minecart.get_next_node(pos, param2)
local def = RegisteredInventories[node.name]
local owner = M(pos):get_string("owner")
local inv = minetest.get_inventory({type="node", pos=npos})
if def and inv and def.put_listname and (not def.allow_put or def.allow_put(npos, stack, owner)) then
local leftover = inv:add_item(def.put_listname, stack)
if leftover:get_count() > 0 then
return leftover
end
elseif def and def.put_item then
return def.put_item(npos, stack, owner)
elseif minecart.is_air_like(node.name) or minecart.is_nodecart_available(npos) then
minetest.add_item(npos, stack)
else
local ndef = minetest.registered_nodes[node.name]
if ndef and ndef.minecart_hopper_additem then
local leftover = ndef.minecart_hopper_additem(npos, stack)
if leftover:get_count() > 0 then
return leftover
end
else
return stack
end
end
end
function minecart.untake_items(pos, param2, stack)
local npos, node
if param2 then
npos, node = minecart.get_next_node(pos, (param2 + 2) % 4)
else
npos, node = pos, minetest.get_node(pos)
end
local def = RegisteredInventories[node.name]
local inv = minetest.get_inventory({type="node", pos=npos})
if def and inv and def.put_listname then
return inv:add_item(def.put_listname, stack)
elseif def and def.untake_item then
return def.untake_item(npos, stack)
else
local ndef = minetest.registered_nodes[node.name]
if ndef and ndef.minecart_hopper_untakeitem then
return ndef.minecart_hopper_untakeitem(npos, stack)
end
end
end
-- Register inventory node for hopper access
-- (for example, see below)
function minecart.register_inventory(node_names, def)
for _, name in ipairs(node_names) do
RegisteredInventories[name] = {
allow_put = def.put and def.put.allow_inventory_put,
put_listname = def.put and def.put.listname,
allow_take = def.take and def.take.allow_inventory_take,
take_listname = def.take and def.take.listname,
put_item = def.put and def.put.put_item,
take_item = def.take and def.take.take_item,
untake_item = def.take and def.take.untake_item,
}
end
end
-- Allow the hopper the access to itself
minecart.register_inventory({"minecart:hopper"}, {
put = {
allow_inventory_put = function(pos, stack, player_name)
local owner = M(pos):get_string("owner")
return owner == player_name
end,
listname = "main",
},
take = {
allow_inventory_take = function(pos, stack, player_name)
local owner = M(pos):get_string("owner")
return owner == player_name
end,
listname = "main",
},
})

469
minecart/i18n.py Executable file
View File

@ -0,0 +1,469 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to generate the template file and update the translation files.
# Copy the script into the mod or modpack root folder and run it there.
#
# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer
# LGPLv2.1+
#
# See https://github.com/minetest-tools/update_translations for
# potential future updates to this script.
from __future__ import print_function
import os, fnmatch, re, shutil, errno
from sys import argv as _argv
from sys import stderr as _stderr
# Running params
params = {"recursive": False,
"help": False,
"mods": False,
"verbose": False,
"folders": [],
"no-old-file": False,
"break-long-lines": False,
"sort": False,
"print-source": False
}
# Available CLI options
options = {"recursive": ['--recursive', '-r'],
"help": ['--help', '-h'],
"mods": ['--installed-mods', '-m'],
"verbose": ['--verbose', '-v'],
"no-old-file": ['--no-old-file', '-O'],
"break-long-lines": ['--break-long-lines', '-b'],
"sort": ['--sort', '-s'],
"print-source": ['--print-source', '-p']
}
# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 80
def set_params_folders(tab: list):
'''Initialize params["folders"] from CLI arguments.'''
# Discarding argument 0 (tool name)
for param in tab[1:]:
stop_param = False
for option in options:
if param in options[option]:
stop_param = True
break
if not stop_param:
params["folders"].append(os.path.abspath(param))
def set_params(tab: list):
'''Initialize params from CLI arguments.'''
for option in options:
for option_name in options[option]:
if option_name in tab:
params[option] = True
break
def print_help(name):
'''Prints some help message.'''
print(f'''SYNOPSIS
{name} [OPTIONS] [PATHS...]
DESCRIPTION
{', '.join(options["help"])}
prints this help message
{', '.join(options["recursive"])}
run on all subfolders of paths given
{', '.join(options["mods"])}
run on locally installed modules
{', '.join(options["no-old-file"])}
do not create *.old files
{', '.join(options["sort"])}
sort output strings alphabetically
{', '.join(options["break-long-lines"])}
add extra line breaks before and after long strings
{', '.join(options["verbose"])}
add output information
''')
def main():
'''Main function'''
set_params(_argv)
set_params_folders(_argv)
if params["help"]:
print_help(_argv[0])
elif params["recursive"] and params["mods"]:
print("Option --installed-mods is incompatible with --recursive")
else:
# Add recursivity message
print("Running ", end='')
if params["recursive"]:
print("recursively ", end='')
# Running
if params["mods"]:
print(f"on all locally installed modules in {os.path.expanduser('~/.minetest/mods/')}")
run_all_subfolders(os.path.expanduser("~/.minetest/mods"))
elif len(params["folders"]) >= 2:
print("on folder list:", params["folders"])
for f in params["folders"]:
if params["recursive"]:
run_all_subfolders(f)
else:
update_folder(f)
elif len(params["folders"]) == 1:
print("on folder", params["folders"][0])
if params["recursive"]:
run_all_subfolders(params["folders"][0])
else:
update_folder(params["folders"][0])
else:
print("on folder", os.path.abspath("./"))
if params["recursive"]:
run_all_subfolders(os.path.abspath("./"))
else:
update_folder(os.path.abspath("./"))
#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
pattern_lua_s = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
pattern_lua_fs = re.compile(r'[\.=^\t,{\(\s]N?FS\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
pattern_lua_bracketed_s = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)
pattern_lua_bracketed_fs = re.compile(r'[\.=^\t,{\(\s]N?FS\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)
# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)
pattern_tr = re.compile(r'(.*?[^@])=(.*)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')
#attempt to read the mod's name from the mod.conf file or folder name. Returns None on failure
def get_modname(folder):
try:
with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
for line in mod_conf:
match = pattern_name.match(line)
if match:
return match.group(1)
except FileNotFoundError:
if not os.path.isfile(os.path.join(folder, "modpack.txt")):
folder_name = os.path.basename(folder)
# Special case when run in Minetest's builtin directory
if folder_name == "builtin":
return "__builtin"
else:
return folder_name
else:
return None
return None
#If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
out = []
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
if pattern_tr_filename.search(name):
out.append(name)
return out
# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
# The first three items are for unused matches
text = re.sub(r'#~ msgid "', "", text)
text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
text = re.sub(r'"\n#~ msgstr "', "=", text)
# comment lines
text = re.sub(r'#.*\n', "", text)
# converting msg pairs into "=" pairs
text = re.sub(r'msgid "', "", text)
text = re.sub(r'"\nmsgstr ""\n"', "=", text)
text = re.sub(r'"\nmsgstr "', "=", text)
# various line breaks and escape codes
text = re.sub(r'"\n"', "", text)
text = re.sub(r'"\n', "\n", text)
text = re.sub(r'\\"', '"', text)
text = re.sub(r'\\n', '@n', text)
# remove header text
text = re.sub(r'=Project-Id-Version:.*\n', "", text)
# remove double-spaced lines
text = re.sub(r'\n\n', '\n', text)
return text
# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
code_match = pattern_po_language_code.match(name)
if code_match == None:
continue
language_code = code_match.group(1)
tr_name = f'{modname}.{language_code}.tr'
tr_file = os.path.join(root, tr_name)
if os.path.exists(tr_file):
if params["verbose"]:
print(f"{tr_name} already exists, ignoring {name}")
continue
fname = os.path.join(root, name)
with open(fname, "r", encoding='utf-8') as po_file:
if params["verbose"]:
print(f"Importing translations from {name}")
text = process_po_file(po_file.read())
with open(tr_file, "wt", encoding='utf-8') as tr_out:
tr_out.write(text)
# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
# Converts the template dictionary to a text to be written as a file
# dKeyStrings is a dictionary of localized string to source file sets
# dOld is a dictionary of existing translations and comments from
# the previous version of this text
def strings_to_text(dkeyStrings, dOld, mod_name, header_comments):
lOut = [f"# textdomain: {mod_name}"]
if header_comments is not None:
lOut.append(header_comments)
dGroupedBySource = {}
for key in dkeyStrings:
sourceList = list(dkeyStrings[key])
if params["sort"]:
sourceList.sort()
sourceString = "\n".join(sourceList)
listForSource = dGroupedBySource.get(sourceString, [])
listForSource.append(key)
dGroupedBySource[sourceString] = listForSource
lSourceKeys = list(dGroupedBySource.keys())
lSourceKeys.sort()
for source in lSourceKeys:
localizedStrings = dGroupedBySource[source]
if params["sort"]:
localizedStrings.sort()
if params["print-source"]:
if lOut[-1] != "":
lOut.append("")
lOut.append(source)
for localizedString in localizedStrings:
val = dOld.get(localizedString, {})
translation = val.get("translation", "")
comment = val.get("comment")
if params["break-long-lines"] and len(localizedString) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None and comment != "" and not comment.startswith("# textdomain:"):
lOut.append(comment)
lOut.append(f"{localizedString}={translation}")
if params["break-long-lines"] and len(localizedString) > doublespace_threshold:
lOut.append("")
unusedExist = False
for key in dOld:
if key not in dkeyStrings:
val = dOld[key]
translation = val.get("translation")
comment = val.get("comment")
# only keep an unused translation if there was translated
# text or a comment associated with it
if translation != None and (translation != "" or comment):
if not unusedExist:
unusedExist = True
lOut.append("\n\n##### not used anymore #####\n")
if params["break-long-lines"] and len(key) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{key}={translation}")
if params["break-long-lines"] and len(key) > doublespace_threshold:
lOut.append("")
return "\n".join(lOut) + '\n'
# Writes a template.txt file
# dkeyStrings is the dictionary returned by generate_template
def write_template(templ_file, dkeyStrings, mod_name):
# read existing template file to preserve comments
existing_template = import_tr_file(templ_file)
text = strings_to_text(dkeyStrings, existing_template[0], mod_name, existing_template[2])
mkdir_p(os.path.dirname(templ_file))
with open(templ_file, "wt", encoding='utf-8') as template_file:
template_file.write(text)
# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
lOut = []
with open(lua_file, encoding='utf-8') as text_file:
text = text_file.read()
#TODO remove comments here
text = re.sub(pattern_concat, "", text)
strings = []
for s in pattern_lua_s.findall(text):
strings.append(s[1])
for s in pattern_lua_bracketed_s.findall(text):
strings.append(s)
for s in pattern_lua_fs.findall(text):
strings.append(s[1])
for s in pattern_lua_bracketed_fs.findall(text):
strings.append(s)
for s in strings:
s = re.sub(r'"\.\.\s+"', "", s)
s = re.sub("@[^@=0-9]", "@@", s)
s = s.replace('\\"', '"')
s = s.replace("\\'", "'")
s = s.replace("\n", "@n")
s = s.replace("\\n", "@n")
s = s.replace("=", "@=")
lOut.append(s)
return lOut
# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
# Returns also header comments in the third return value.
def import_tr_file(tr_file):
dOut = {}
text = None
header_comment = None
if os.path.exists(tr_file):
with open(tr_file, "r", encoding='utf-8') as existing_file :
# save the full text to allow for comparison
# of the old version with the new output
text = existing_file.read()
existing_file.seek(0)
# a running record of the current comment block
# we're inside, to allow preceeding multi-line comments
# to be retained for a translation line
latest_comment_block = None
for line in existing_file.readlines():
line = line.rstrip('\n')
if line.startswith("###"):
if header_comment is None and not latest_comment_block is None:
# Save header comments
header_comment = latest_comment_block
# Strip textdomain line
tmp_h_c = ""
for l in header_comment.split('\n'):
if not l.startswith("# textdomain:"):
tmp_h_c += l + '\n'
header_comment = tmp_h_c
# Reset comment block if we hit a header
latest_comment_block = None
continue
elif line.startswith("#"):
# Save the comment we're inside
if not latest_comment_block:
latest_comment_block = line
else:
latest_comment_block = latest_comment_block + "\n" + line
continue
match = pattern_tr.match(line)
if match:
# this line is a translated line
outval = {}
outval["translation"] = match.group(2)
if latest_comment_block:
# if there was a comment, record that.
outval["comment"] = latest_comment_block
latest_comment_block = None
dOut[match.group(1)] = outval
return (dOut, text, header_comment)
# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
# Returns a dictionary of localized strings to source file sets
# that can be used with the strings_to_text function.
def generate_template(folder, mod_name):
dOut = {}
for root, dirs, files in os.walk(folder):
for name in files:
if fnmatch.fnmatch(name, "*.lua"):
fname = os.path.join(root, name)
found = read_lua_file_strings(fname)
if params["verbose"]:
print(f"{fname}: {str(len(found))} translatable strings")
for s in found:
sources = dOut.get(s, set())
sources.add(f"### {os.path.basename(fname)} ###")
dOut[s] = sources
if len(dOut) == 0:
return None
templ_file = os.path.join(folder, "locale/template.txt")
write_template(templ_file, dOut, mod_name)
return dOut
# Updates an existing .tr file, copying the old one to a ".old" file
# if any changes have happened
# dNew is the data used to generate the template, it has all the
# currently-existing localized strings
def update_tr_file(dNew, mod_name, tr_file):
if params["verbose"]:
print(f"updating {tr_file}")
tr_import = import_tr_file(tr_file)
dOld = tr_import[0]
textOld = tr_import[1]
textNew = strings_to_text(dNew, dOld, mod_name, tr_import[2])
if textOld and textOld != textNew:
print(f"{tr_file} has changed.")
if not params["no-old-file"]:
shutil.copyfile(tr_file, f"{tr_file}.old")
with open(tr_file, "w", encoding='utf-8') as new_tr_file:
new_tr_file.write(textNew)
# Updates translation files for the mod in the given folder
def update_mod(folder):
modname = get_modname(folder)
if modname is not None:
process_po_files(folder, modname)
print(f"Updating translations for {modname}")
data = generate_template(folder, modname)
if data == None:
print(f"No translatable strings found in {modname}")
else:
for tr_file in get_existing_tr_files(folder):
update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file))
else:
print(f"\033[31mUnable to find modname in folder {folder}.\033[0m", file=_stderr)
exit(1)
# Determines if the folder being pointed to is a mod or a mod pack
# and then runs update_mod accordingly
def update_folder(folder):
is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
if is_modpack:
subfolders = [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')]
for subfolder in subfolders:
update_mod(subfolder)
else:
update_mod(folder)
print("Done.")
def run_all_subfolders(folder):
for modfolder in [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')]:
update_folder(modfolder)
main()

49
minecart/init.lua Normal file
View File

@ -0,0 +1,49 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
minecart = {}
-- Version for compatibility checks, see readme.md/history
minecart.version = 2.00
minecart.hopper_enabled = minetest.settings:get_bool("minecart_hopper_enabled") ~= false
minecart.teleport_enabled = minetest.settings:get_bool("minecart_teleport_enabled") == true
-- Test for MT 5.4 new string mode
minecart.CLIP = minetest.features.use_texture_alpha_string_modes and "clip" or false
minecart.S = minetest.get_translator("minecart")
local MP = minetest.get_modpath("minecart")
dofile(MP .. "/baselib.lua")
dofile(MP .. "/storage.lua")
dofile(MP .. "/rails.lua")
dofile(MP .. "/monitoring.lua")
dofile(MP .. "/recording.lua")
dofile(MP .. "/hopperlib.lua")
dofile(MP .. "/nodelib.lua")
dofile(MP .. "/entitylib.lua")
dofile(MP .. "/api.lua")
dofile(MP .. "/minecart.lua")
dofile(MP .. "/buffer.lua")
dofile(MP .. "/protection.lua")
--dofile(MP .. "/tool.lua") # for debugging only
dofile(MP .. "/signs.lua")
dofile(MP .. "/terminal.lua")
dofile(MP .. "/pusher.lua")
if minecart.hopper_enabled then
dofile(MP .. "/hopper.lua")
dofile(MP .. "/mods_support.lua")
end
dofile(MP .. "/doc.lua")
minetest.log("info", "[MOD] Minecart loaded")

54
minecart/license.txt Normal file
View File

@ -0,0 +1,54 @@
License of source code
----------------------
The MIT License (MIT)
Copyright (C) 2012-2016 PilzAdam
Copyright (C) 2014-2016 SmallJoker
Copyright (C) 2012-2016 Various Minetest developers and contributors
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
For more details:
https://opensource.org/licenses/MIT
Licenses of media
-----------------
CC-0, see: https://creativecommons.org/share-your-work/public-domain/cc0/, except
if other license is mentioned.
Authors
---------
Originally from PixelBOX (Gambit):
carts_cart_side.png
carts_cart_top.png
carts_cart_front.png*
carts_cart.png*
sofar + stujones11:
carts_cart.b3d and carts_cart.blend
hexafraction, modified by sofar
carts_rail_*.png
http://www.freesound.org/people/YleArkisto/sounds/253159/ - YleArkisto - CC-BY-3.0
carts_cart_moving.*.ogg

View File

@ -0,0 +1,58 @@
# textdomain: minecart
Station name=Stationsname
Waiting time/sec=Wartezeit/s
connected to=verbunden mit
Minecart Railway Buffer=Minecart Prellbock
Summary=Zusammenfassung
1. Place your rails and build a route with two endpoints. Junctions are allowed as long as each route has its own start and endpoint.=1. Baue eine Schienenstrecke mit zwei Enden. Kreuzungen sind zulässig, solange jede Route ihre eigenen Start- und Endpunkte hat.
2. Place a Railway Buffer at both endpoints (buffers are always needed, they store the route and timing information).=2. Platziere einen Prellbock an beide Schienenenden (Prellböcke sind zwingend notwendig, sie speichern die Routen- und Zeit-Informationen).
3. Give both Railway Buffers unique station names, like Oxford and Cambridge.=3. Gib beiden Prellböcken eindeutige Stationsnamen wie: Stuttgart und München.
4. Place a Minecart at a buffer and give it a cart number (1..999)=4. Platziere einen Minecart Wagen an einem Prellbock und gib dem Wagen eine Wagennummer (1..999)
5. Drive from buffer to buffer in both directions using the Minecart(!) to record the routes (use 'right-left' keys to control the Minecart).=5. Um eine Route aufzuzeichnen, fahre die Route in beide Richtungen von Prellbock zu Prellbock mit dem Minecart Wagen(!). Nutze 'links-rechts' Tasten zur Steuerung.
6. Punch the buffers to check the connection data (e.g. 'Oxford: connected to Cambridge').=6. Schlage auf die Prellböcke um die Verbindungsdaten zu prüfen (bspw.: 'München: verbunden mit Stuttgart')
7. Optional: Configure the Minecart waiting time in both buffers. The Minecart will then start automatically after the configured time.=7. Optional: Konfiguriere die Wagenwartezeit in beiden Prellböcken. Der Wagen startet dann nach dieser Zeit automatisch.
8. Optional: Protect your rail network with the Protection Landmarks (one Landmark at least every 16 nodes/meters).=8. Optional: Schütze deine Schienen mit Hilfe der Meilensteine (ein Meilenstein mindestens alle 16 Blöcke).
9. Place a Minecart in front of the buffer and check whether it starts after the configured time.=9. Platziere einen Wagen direkt vor einem Prellbock und prüfe, ob er nach der konfigurierten Zeit startet.
10. Check the cart state via the chat command: /mycart <num>@n '<num>' is the cart number=Prüfe den Status des Wagen mit dem Chat Kommando: /mycart <num>@n <num> ist die Wagennummer
11. Drop items into the Minecart and punch the cart to start it.=11: Lege Gegenstände in ein Wagen (Taste Q) und starte dann den Wagen durch Anklicken.
12. Dig the cart with 'sneak+click' (as usual). The items will be drop down.=10. Klicke mit gedrückter Shift-Taste auf den Wagen, um diesen zu entfernen. Die Gegenstände fallen dann zu Boden.
Primary used to transport items. You can drop items into the Minecart and punch the cart to get started. Sneak+click the cart to get cart and items back=Primär für den Transport von Gegenständen genutzt. Du kannst Gegenstände in ein Cart legen (Taste Q) und dann den Wagen durch Anklicken starten. Klicke mit gedrückter Shift-Taste auf den Wagen, um Cart und Gegenstände zurückzuerhalten
Used as buffer on both rail ends. Needed to be able to record the cart routes=Preckblöcke müssen an beiden Schienenenden platziert sein, so dass Aufzeichnungen der Strecke gemacht werden können.
Protect your rails with the Landmarks (one Landmark at least every 16 blocks near the rail)=Schütze deine Schienen mit Hilfe der Meilensteine (ein Meilenstein mindestens alle 16 Blöcke der Strecke entlang)
Used to load/unload Minecart. The Hopper can push/pull items to/from chests and drop/pickup items to/from Minecarts. To unload a Minecart place the hopper below the rail. To load the Minecart, place the hopper right next to the Minecart.=Um Wagen zu be- und entladen. Der Hopper kann Gegenstände aus Kisten Holen und legen, sowie diese in Wagen fallen lassen bzw. aus Wagen entnehmen. Um einen Wagen zu entladen, muss der Hopper unter die Schiene platziert werden. Um einen Wagen zu beladen, muss der Hopper direkt neben die Schiene platziert werden.
Minecart=Minecart
Minecart, the lean railway transportation automation system=Minecart, das schlanke Schienentransport Automatisierungssystem
Minecart Cart=Wagen
Minecart Speed Signs=Geschwindigkeitsbegrenzungszeichen
If several carts are running on one route,@nit can happen that a buffer position is already occupied and one cart therefore stops earlier.@nIn this case, the cart pusher is used to push the cart towards the buffer again.@nThis block must be placed under the rail at a distance of 2 m in front of the buffer.=Wenn mehrere Wagen auf einer Route fahren, kann es vorkommen,@ndass eine Prellbock Position bereits belegt ist und ein Wagen daher früher anhält.@nDer Cart Anschieber dient in diesem Fall dazu, die Wagen wieder in Richtung Prellbock anzuschieben.@nDieser Block muss unter der Schiene mit 2 m Abstand vor dem Prellbock platziert werden.
Limit the cart speed with speed limit signs.@n@nAs before, the speed of the carts is also influenced by power rails.@nBrake rails are irrelevant, the cart does not brake here.@nThe maximum speed is 8 m/s. This assumes a ratio of power rails@nto normal rails of 1 to 4 on a flat section of rail. A rail section is a@nseries of rail nodes without a change of direction. After every curve / kink,@nthe speed for the next section of the route is newly determined,@ntaking into account the swing of the cart. This means that a cart can@nroll over short rail sections without power rails.@n@nIn order to additionally brake the cart at certain points@n(at switches or in front of a buffer), speed limit signs can be placed@non the track. With these signs the speed can be reduced to 4, 2, or 1 m / s.@nThe "No speed limit" sign can be used to remove the speed limit.@n@nThe speed limit signs must be placed next to the track so that they can@nbe read from the cart. This allows different speeds in each direction of travel.=Begrenze die Geschwindigkeit der Wagen mit Geschwindigkeitsbegrenzungszeichen@n@nDie Geschwindigkeit der Carts wird wie bisher auch über "power rails" beeinflusst. "Brake rails" sind ohne Bedeutung, das Cart bremst hier nicht. Die maximale Geschwindigkeit beträgt 8 m/s. Dies setzt eine Verhältnis von "power rails" zu "normal rails" von 1 zu 4 auf einem ebenen Streckenabschnitt voraus. Ein Streckenabschnitt ist dabei ein Reihe von Schienenblöcken ohne Richtungsänderung. Nach jeder Kurve/Knick wird die Geschwindigkeit für den nächsten Streckenabschnitt neu bestimmt, wobei hier der Schwung des Carts mit berücksichtigt wird. So kann ein Cart auch über kurze Streckenabschnitt ohne "power rails" rollen.@n@nUm das Cart zusätzlich an bestimmten Stellen abzubremsen (an Weichen oder vor einen Puffer), können Geschwindigkeitsbegrenzungszeichen an der Strecke platziert werden. Durch diese Zeichen kann die Geschwindigkeit auf 4, 2, oder 1 m/s reduziert werden. Durch das Aufhebungszeichen kann die Geschwindigkeitsbegrenzung wieder aufgehoben werden.@n@nDie Geschwindigkeitsbegrenzungszeichen müssen so neben die Strecke platziert werden, dass sie vom Cart ablesbar sind. Dies erlaubt damit unterschiedliche Geschwindigkeiten pro Fahrtrichtung.
Minecart Hopper=Minecart Hopper
Minecart (Sneak+Click to pick up)=Minecart (Shift+Klick zum Entfernen des Carts)
Output cart state and position, or a list of carts, if no cart number is given.=Gibt Status und Position des Wagens, oder eine Liste aller Wagen aus, wenn keine Wagennummer angegeben ist.
List of carts=Liste aller Wagen
Enter cart number=Gebe Cart Nummer ein
Save=Speichern
[minecart] Area is protected!=[minecart] Bereich ist geschützt!
Allow to dig/place rails in Minecart Landmark areas=Erlaubt dir, Schienen in Meilensteinbereichen zu setzen/zu entfernen
Minecart Landmark=Minecart Meilenstein
Cart Pusher=Wagen Anschieber
left=links
right=rechts
straight=geradeaus
Recording=Aufzeichnung
speed=Tempo
next junction=nächste Weiche
Travel time=Fahrzeit
[minecart] Route stored!=[minecart] Strecke gespeichert
[minecart] Speed @= %u m/s, Time @= %u s, Route length @= %u m=[minecart] Geschw. @= %u m/s, Zeit @= %u s, Routenlänge @= %u m
Speed "1"=Tempo "1"
Speed "2"=Tempo "2"
Speed "4"=Tempo "4"
No speed limit=Keine Geschwindigkeitsbegrenzung
Cart List=Cart Liste
Cart Terminal=Cart Terminal
##### not used anymore #####
Used to push a cart if the cart does not stop directly at a buffer. Block has to be placed below the rail.=Wird verwendet, um einen Wagen anzuschieben, wenn der Wagen nicht direkt an einem Puffer anhält. Der Block muss unter der Schiene platziert werden.

View File

@ -0,0 +1,53 @@
# textdomain: minecart
Station name=
Waiting time/sec=
connected to=
Minecart Railway Buffer=
Summary=
1. Place your rails and build a route with two endpoints. Junctions are allowed as long as each route has its own start and endpoint.=
2. Place a Railway Buffer at both endpoints (buffers are always needed, they store the route and timing information).=
3. Give both Railway Buffers unique station names, like Oxford and Cambridge.=
4. Place a Minecart at a buffer and give it a cart number (1..999)=
5. Drive from buffer to buffer in both directions using the Minecart(!) to record the routes (use 'right-left' keys to control the Minecart).=
6. Punch the buffers to check the connection data (e.g. 'Oxford: connected to Cambridge').=
7. Optional: Configure the Minecart waiting time in both buffers. The Minecart will then start automatically after the configured time.=
8. Optional: Protect your rail network with the Protection Landmarks (one Landmark at least every 16 nodes/meters).=
9. Place a Minecart in front of the buffer and check whether it starts after the configured time.=
10. Check the cart state via the chat command: /mycart <num>@n '<num>' is the cart number=
11. Drop items into the Minecart and punch the cart to start it.=
12. Dig the cart with 'sneak+click' (as usual). The items will be drop down.=
Primary used to transport items. You can drop items into the Minecart and punch the cart to get started. Sneak+click the cart to get cart and items back=
Used as buffer on both rail ends. Needed to be able to record the cart routes=
Protect your rails with the Landmarks (one Landmark at least every 16 blocks near the rail)=
Used to load/unload Minecart. The Hopper can push/pull items to/from chests and drop/pickup items to/from Minecarts. To unload a Minecart place the hopper below the rail. To load the Minecart, place the hopper right next to the Minecart.=
Minecart=
Minecart, the lean railway transportation automation system=
Minecart Cart=
Minecart Speed Signs=
If several carts are running on one route,@nit can happen that a buffer position is already occupied and one cart therefore stops earlier.@nIn this case, the cart pusher is used to push the cart towards the buffer again.@nThis block must be placed under the rail at a distance of 2 m in front of the buffer.=
Limit the cart speed with speed limit signs.@n@nAs before, the speed of the carts is also influenced by power rails.@nBrake rails are irrelevant, the cart does not brake here.@nThe maximum speed is 8 m/s. This assumes a ratio of power rails@nto normal rails of 1 to 4 on a flat section of rail. A rail section is a@nseries of rail nodes without a change of direction. After every curve / kink,@nthe speed for the next section of the route is newly determined,@ntaking into account the swing of the cart. This means that a cart can@nroll over short rail sections without power rails.@n@nIn order to additionally brake the cart at certain points@n(at switches or in front of a buffer), speed limit signs can be placed@non the track. With these signs the speed can be reduced to 4, 2, or 1 m / s.@nThe "No speed limit" sign can be used to remove the speed limit.@n@nThe speed limit signs must be placed next to the track so that they can@nbe read from the cart. This allows different speeds in each direction of travel.=
Minecart Hopper=
Minecart (Sneak+Click to pick up)=
Output cart state and position, or a list of carts, if no cart number is given.=
List of carts=
Enter cart number=
Save=
[minecart] Area is protected!=
Allow to dig/place rails in Minecart Landmark areas=
Minecart Landmark=
Cart Pusher=
left=
right=
straight=
Recording=
speed=
next junction=
Travel time=
[minecart] Route stored!=
[minecart] Speed @= %u m/s, Time @= %u s, Route length @= %u m=
Speed "1"=
Speed "2"=
Speed "4"=
No speed limit=
Cart List=
Cart Terminal=

107
minecart/minecart.lua Normal file
View File

@ -0,0 +1,107 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
local S = minecart.S
local M = minetest.get_meta
minetest.register_node("minecart:cart", {
description = S("Minecart (Sneak+Click to pick up)"),
tiles = {
-- up, down, right, left, back, front
"carts_cart_top.png^minecart_appl_cart_top.png",
"carts_cart_top.png",
"carts_cart_side.png^minecart_logo.png",
"carts_cart_side.png^minecart_logo.png",
"carts_cart_side.png^minecart_logo.png",
"carts_cart_side.png^minecart_logo.png",
},
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{-8/16,-8/16,-8/16, 8/16, 8/16,-7/16},
{-8/16,-8/16, 7/16, 8/16, 8/16, 8/16},
{-8/16,-8/16,-8/16, -7/16, 8/16, 8/16},
{ 7/16,-8/16,-8/16, 8/16, 8/16, 8/16},
{-8/16,-8/16,-8/16, 8/16,-6/16, 8/16},
},
},
-- collision_box = {
-- type = "fixed",
-- fixed = {
-- {-8/16,-8/16,-8/16, 8/16,-4/16, 8/16},
-- },
-- },
paramtype2 = "facedir",
paramtype = "light",
use_texture_alpha = minecart.CLIP,
sunlight_propagates = true,
is_ground_content = false,
groups = {cracky = 2, crumbly = 2, choppy = 2},
node_placement_prediction = "",
diggable = false,
on_place = minecart.on_nodecart_place,
on_punch = minecart.on_nodecart_punch,
on_rightclick = function(pos, node, clicker)
if clicker and clicker:is_player() then
if M(pos):get_int("userID") ~= 0 then
-- enter the cart
local object = minecart.node_to_entity(pos, "minecart:cart", "minecart:cart_entity")
minecart.manage_attachment(clicker, object:get_luaentity(), true)
else
minecart.show_formspec(pos, clicker)
end
end
end,
set_cargo = function(pos, data)
for _,item in ipairs(data or {}) do
minetest.add_item(pos, ItemStack(item))
end
end,
get_cargo = function(pos)
local data = {}
for _, obj in pairs(minetest.get_objects_inside_radius(pos, 1)) do
local entity = obj:get_luaentity()
if not obj:is_player() and entity and entity.name == "__builtin:item" then
obj:remove()
data[#data + 1] = entity.itemstring
end
end
return data
end,
})
minecart.register_cart_entity("minecart:cart_entity", "minecart:cart", "default", {
initial_properties = {
physical = false,
collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
visual = "wielditem",
textures = {"minecart:cart"},
visual_size = {x=0.66, y=0.66, z=0.66},
static_save = false,
},
driver_allowed = true,
})
minetest.register_craft({
output = "minecart:cart",
recipe = {
{"default:steel_ingot", "default:cobble", "default:steel_ingot"},
{"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"},
},
})

4
minecart/mod.conf Normal file
View File

@ -0,0 +1,4 @@
name=minecart
depends = default,carts
optional_depends = doc
description = Minecart, the lean railway transportation automation system

131
minecart/mods_support.lua Normal file
View File

@ -0,0 +1,131 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
Wrapper functions to get hopper support for other mods
]]--
-- for lazy programmers
local M = minetest.get_meta
local CacheForFuelNodeNames = {}
local function is_fuel(stack)
local name = stack:get_name()
if CacheForFuelNodeNames[name] then
return true
end
if minetest.get_craft_result({method="fuel", width=1, items={stack}}).time ~= 0 then
CacheForFuelNodeNames[name] = true
end
return CacheForFuelNodeNames[name]
end
------------------------------------------------------------------------------
-- default
------------------------------------------------------------------------------
minecart.register_inventory({"default:chest", "default:chest_open"}, {
put = {
listname = "main",
},
take = {
listname = "main",
},
})
minecart.register_inventory({"default:chest_locked", "default:chest_locked_open"}, {
put = {
allow_inventory_put = function(pos, stack, player_name)
local owner = M(pos):get_string("owner")
return owner == player_name
end,
listname = "main",
},
take = {
allow_inventory_take = function(pos, stack, player_name)
local owner = M(pos):get_string("owner")
return owner == player_name
end,
listname = "main",
},
})
minecart.register_inventory({"default:furnace", "default:furnace_active"}, {
put = {
-- distinguish between fuel and other items
put_item = function(pos, stack, player_name)
local inv = minetest.get_inventory({type="node", pos=pos})
local listname = is_fuel(stack) and "fuel" or "src"
local leftover = inv:add_item(listname, stack)
minetest.get_node_timer(pos):start(1.0)
if leftover:get_count() > 0 then
return leftover
end
end,
},
take = {
-- fuel can't be taken
listname = "dst",
},
})
------------------------------------------------------------------------------
-- digtron
------------------------------------------------------------------------------
minecart.register_inventory({"digtron:inventory"}, {
put = {
listname = "main",
},
take = {
listname = "main",
},
})
minecart.register_inventory({"digtron:fuelstore"}, {
put = {
listname = "fuel",
},
take = {
listname = "fuel",
},
})
minecart.register_inventory({"digtron:combined_storage"}, {
put = {
-- distinguish between fuel and other items
put_item = function(pos, stack, player_name)
local inv = minetest.get_inventory({type="node", pos=pos})
local listname = is_fuel(stack) and "fuel" or "main"
local leftover = inv:add_item(listname, stack)
if leftover:get_count() > 0 then
return leftover
end
end,
},
take = {
-- fuel can't be taken
listname = "main",
},
})
------------------------------------------------------------------------------
-- protector
------------------------------------------------------------------------------
minecart.register_inventory({"protector:chest"}, {
put = {
listname = "main",
},
take = {
listname = "main",
},
})

356
minecart/monitoring.lua Normal file
View File

@ -0,0 +1,356 @@
--[[
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 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])
-- if M(pos):contains("waypoints") then
-- print("get_checkpoint", P2S(H2P(cp[1])), P2S(H2P(cp[2])))
-- end
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
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
return "unknown", 0, "unknown"
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 monitoring(cycle)
local cart = pop(cycle)
while cart do
-- All running cars
if cart.objID and cart.objID ~= 0 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)
--print("entity card " .. cart.userID .. " at " .. P2S(cart.last_pos))
else
print("entity card 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
print("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)
cart.objID = 0
cart.pos = pos
--print("cart to node", cycle, cart.userID, P2S(pos))
end
elseif cart and 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
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
tCartsOnRail[owner][userID].objID = nil
tCartsOnRail[owner][userID] = nil
minecart.store_carts()
end
end
function minecart.monitoring_valid_cart(owner, userID, pos, node_name)
if tCartsOnRail[owner] and tCartsOnRail[owner][userID] 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 = "<cart-num>",
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
})
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.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)

139
minecart/nodelib.lua Normal file
View File

@ -0,0 +1,139 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
-- for lazy programmers
local M = minetest.get_meta
local S = minecart.S
local P2S = function(pos) if pos then return minetest.pos_to_string(pos) end end
local S2P = minetest.string_to_pos
function minecart.get_nodecart_nearby(pos, param2, radius)
local pos2 = param2 and vector.add(pos, minecart.param2_to_dir(param2)) or pos
local pos3 = minetest.find_node_near(pos2, radius or 0.5, minecart.lCartNodeNames, true)
if pos3 then
return pos3, minetest.get_node(pos3)
end
end
-- Convert node to entity and start cart
function minecart.start_nodecart(pos, node_name, puncher, punch_dir)
local owner = M(pos):get_string("owner")
local userID = M(pos):get_int("userID")
-- check if valid cart
if not minecart.monitoring_valid_cart(owner, userID, pos, node_name) then
--print("invalid cart", owner, userID, P2S(pos), node_name)
M(pos):set_string("infotext",
minetest.get_color_escape_sequence("#FFFF00") .. owner .. ": 0")
return
end
-- Only the owner or a noplayer can start the cart, but owner has to be online
if minecart.is_owner(puncher, owner) and minetest.get_player_by_name(owner) and
userID ~= 0 then
local entity_name = minecart.tNodeNames[node_name]
local obj = minecart.node_to_entity(pos, node_name, entity_name)
if obj then
local entity = obj:get_luaentity()
if puncher then
local yaw = puncher:get_look_horizontal()
entity.object:set_rotation({x = 0, y = yaw, z = 0})
elseif punch_dir then
local yaw = minetest.dir_to_yaw(punch_dir)
entity.object:set_rotation({x = 0, y = yaw, z = 0})
end
minecart.start_entitycart(entity, pos)
end
end
end
function minecart.show_formspec(pos, clicker)
local owner = M(pos):get_string("owner")
if minecart.is_owner(clicker, owner) then
clicker:get_meta():set_string("cart_pos", P2S(pos))
minetest.show_formspec(owner, "minecart:userID_node",
"size[4,3]" ..
"label[0,0;" .. S("Enter cart number") .. ":]" ..
"field[1,1;3,1;userID;;]" ..
"button_exit[1,2;2,1;exit;" .. S("Save") .. "]")
end
end
-- Player places the node
function minecart.on_nodecart_place(itemstack, placer, pointed_thing)
local node_name = itemstack:get_name()
local param2 = minetest.dir_to_facedir(placer:get_look_dir())
local owner = placer:get_player_name()
-- Add node
if minecart.is_rail(pointed_thing.under) then
minecart.add_nodecart(pointed_thing.under, node_name, param2, {}, owner, 0)
placer:get_meta():set_string("cart_pos", P2S(pointed_thing.under))
minecart.show_formspec(pointed_thing.under, placer)
elseif minecart.is_rail(pointed_thing.above) then
minecart.add_nodecart(pointed_thing.above, node_name, param2, {}, owner, 0)
placer:get_meta():set_string("cart_pos", P2S(pointed_thing.above))
minecart.show_formspec(pointed_thing.above, placer)
else
return itemstack
end
minetest.sound_play({name = "default_place_node_metal", gain = 0.5},
{pos = pointed_thing.above})
if not (creative and creative.is_enabled_for
and creative.is_enabled_for(placer:get_player_name())) then
itemstack:take_item()
end
return itemstack
end
-- Start the node cart (or dig by shift+leftclick)
function minecart.on_nodecart_punch(pos, node, puncher, pointed_thing)
--print("on_nodecart_punch")
local owner = M(pos):get_string("owner")
local userID = M(pos):get_int("userID")
if minecart.is_owner(puncher, owner) then
if puncher:get_player_control().sneak then
local ndef = minetest.registered_nodes[node.name]
if not ndef.has_cargo or not ndef.has_cargo(pos) then
minecart.remove_nodecart(pos)
minecart.add_node_to_player_inventory(pos, puncher, node.name)
minecart.monitoring_remove_cart(owner, userID)
end
else
minecart.start_nodecart(pos, node.name, puncher)
end
end
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname == "minecart:userID_node" then
if fields.exit or fields.key_enter == "true" then
local cart_pos = S2P(player:get_meta():get_string("cart_pos"))
local owner = M(cart_pos):get_string("owner")
if minecart.is_owner(player, owner) then
local userID = tonumber(fields.userID) or 0
if minecart.userID_available(owner, userID) then
M(cart_pos):set_int("userID", userID)
M(cart_pos):set_string("infotext",
minetest.get_color_escape_sequence("#FFFF00") ..
player:get_player_name() .. ": " .. userID)
local node = minetest.get_node(cart_pos)
local entity_name = minecart.tNodeNames[node.name]
minecart.monitoring_add_cart(owner, userID, cart_pos, node.name, entity_name)
end
end
end
return true
end
return false
end)

205
minecart/protection.lua Normal file
View File

@ -0,0 +1,205 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
local S = minecart.S
local RANGE = 8
local IsNodeUnderObservation = {}
-- Register all nodes, which should be protected by the "minecart:landmark"
function minecart.register_protected_node(name)
IsNodeUnderObservation[name] = true
end
local function landmark_found(pos, name, range)
local pos1 = {x=pos.x-range, y=pos.y-range, z=pos.z-range}
local pos2 = {x=pos.x+range, y=pos.y+range, z=pos.z+range}
for _,npos in ipairs(minetest.find_nodes_in_area(pos1, pos2, {"minecart:landmark"})) do
if minetest.get_meta(npos):get_string("owner") ~= name then
return true
end
end
return false
end
local function is_protected(pos, name, range)
if minetest.check_player_privs(name, "minecart")
or not landmark_found(pos, name, range) then
return false
end
return true
end
local old_is_protected = minetest.is_protected
function minetest.is_protected(pos, name)
if pos and name then
local node = minetest.get_node(pos)
if IsNodeUnderObservation[node.name] and is_protected(pos, name, RANGE) then
return true
end
end
return old_is_protected(pos, name)
end
minetest.register_node("minecart:landmark", {
description = S("Minecart Landmark"),
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{-3/16, -8/16, -3/16, 3/16, 4/16, 3/16},
{-2/16, 4/16, -3/16, 2/16, 5/16, 3/16},
},
},
tiles = {
'default_mossycobble.png',
'default_mossycobble.png',
'default_mossycobble.png',
'default_mossycobble.png',
'default_mossycobble.png^minecart_protect.png',
'default_mossycobble.png^minecart_protect.png',
},
after_place_node = function(pos, placer, itemstack, pointed_thing)
local meta = minetest.get_meta(pos)
meta:set_string("owner", placer:get_player_name())
if is_protected(pos, placer:get_player_name(), RANGE+3) then
minetest.remove_node(pos)
return true
end
end,
can_dig = function(pos, digger)
local meta = minetest.get_meta(pos)
if meta:get_string("owner") == digger:get_player_name() then
return true
end
if minetest.check_player_privs(digger:get_player_name(), "minecart") then
return true
end
minetest.chat_send_player(digger:get_player_name(),
S("[minecart] Area is protected!").." (owner: "..meta:get_string("owner")..")")
return false
end,
paramtype2 = "facedir",
sunlight_propagates = true,
groups = {cracky = 3, stone = 1},
is_ground_content = false,
sounds = default.node_sound_stone_defaults(),
})
minetest.register_craft({
output = "minecart:landmark 6",
recipe = {
{"", "default:mossycobble", ""},
{"", "default:mossycobble", ""},
{"", "default:mossycobble", ""},
},
})
minetest.register_node("minecart:ballast", {
description = "Minecart Ballast",
tiles = {"minecart_ballast.png"},
groups = {crumbly = 1, cracky = 3},
sounds = default.node_sound_stone_defaults(),
})
minetest.register_node("minecart:ballast_slope", {
description = "Minecart Ballast Slope",
tiles = {"minecart_ballast.png"},
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{-8/16, -8/16, -8/16, 8/16, -4/16, 8/16},
{-8/16, -4/16, -4/16, 8/16, 0/16, 8/16},
{-8/16, 0/16, 0/16, 8/16, 4/16, 8/16},
{-8/16, 4/16, 4/16, 8/16, 8/16, 8/16},
},
},
selection_box = {
type = "fixed",
fixed = {-8/16, -8/16, -8/16, 8/16, 8/16, 8/16},
},
paramtype2 = "facedir",
groups = {crumbly = 1, cracky = 3},
sounds = default.node_sound_stone_defaults(),
})
minetest.register_node("minecart:ballast_ramp", {
description = "Minecart Ballast Ramp",
tiles = {"minecart_ballast.png"},
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{-8/16, -8/16, -8/16, 8/16, 8/16, 8/16},
{-8/16, -4/16, -4/16, 8/16, 12/16, 8/16},
{-8/16, 0/16, 0/16, 8/16, 16/16, 8/16},
{-8/16, 4/16, 4/16, 8/16, 20/16, 8/16},
},
},
selection_box = {
type = "fixed",
fixed = {-8/16, -8/16, -8/16, 8/16, 8/16, 8/16},
},
paramtype2 = "facedir",
groups = {crumbly = 1, cracky = 3},
sounds = default.node_sound_stone_defaults(),
})
minetest.register_craft({
output = "minecart:ballast 6",
recipe = {
{"", "", ""},
{"default:cobble", "default:stone", "default:cobble"},
{"default:cobble", "default:stone", "default:cobble"},
},
})
minetest.register_craft({
output = "minecart:ballast_slope 6",
recipe = {
{"", "", "default:cobble"},
{"", "default:stone", "default:cobble"},
{"default:cobble", "default:stone", "default:cobble"},
},
})
minetest.register_craft({
output = "minecart:ballast_ramp 2",
recipe = {
{"", "", ""},
{"minecart:ballast_slope", "", ""},
{"minecart:ballast", "", ""},
},
})
minetest.register_privilege("minecart", {
description = S("Allow to dig/place rails in Minecart Landmark areas"),
give_to_singleplayer = false,
give_to_admin = true,
})
minecart.register_protected_node("carts:rail")
minecart.register_protected_node("carts:powerrail")
minecart.register_protected_node("carts:brakerail")
minecart.register_protected_node("minecart:buffer")
minecart.register_protected_node("minecart:ballast")
minecart.register_protected_node("minecart:ballast_slope")
minecart.register_protected_node("minecart:ballast_ramp")
minecart.register_protected_node("minecart:speed1")
minecart.register_protected_node("minecart:speed2")
minecart.register_protected_node("minecart:speed4")
minecart.register_protected_node("minecart:speed8")

67
minecart/pusher.lua Normal file
View File

@ -0,0 +1,67 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
-- for lazy programmers
local M = minetest.get_meta
local S = minecart.S
local CYCLE_TIME = 4
local function node_timer(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
minecart.punch_cart({x = pos.x, y = pos.y + 1, z = pos.z}, nil, 1, dir)
return true
end
local function after_dig_node(pos, oldnode, oldmetadata, digger)
techage.remove_node(pos, oldnode, oldmetadata)
end
minetest.register_node("minecart:cart_pusher", {
description = S("Cart Pusher"),
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{-8/16,-8/16,-8/16, 8/16, 8/16, 8/16},
{-1/16, 8/16,-4/16, 1/16, 10/16, 4/16},
},
},
tiles = {
-- up, down, right, left, back, front
"default_steel_block.png^minecart_pusher_top.png",
"default_steel_block.png",
"default_steel_block.png^minecart_pusher.png",
"default_steel_block.png^minecart_pusher.png",
"default_steel_block.png^minecart_pusher.png",
"default_steel_block.png^minecart_pusher.png",
},
after_place_node = function(pos)
minetest.get_node_timer(pos):start(CYCLE_TIME)
end,
on_timer = node_timer,
paramtype2 = "facedir",
groups = {choppy=2, cracky=2, crumbly=2},
is_ground_content = true,
sounds = default.node_sound_metal_defaults(),
})
minetest.register_craft({
output = "minecart:cart_pusher",
recipe = {
{"dye:black", "default:steel_ingot", "dye:yellow"},
{"default:steel_ingot", "default:mese_crystal", "default:steel_ingot"},
{"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"},
},
})

548
minecart/rails.lua Normal file
View File

@ -0,0 +1,548 @@
--[[
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
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
--minetest.register_lbm({
-- label = "Delete waypoints",
-- name = "minecart:del_meta",
-- nodenames = {"carts:brakerail"},
-- run_at_every_load = true,
-- action = function(pos, node)
-- del_metadata(pos)
-- end,
--})

185
minecart/recording.lua Normal file
View File

@ -0,0 +1,185 @@
--[[
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 S2P = minetest.string_to_pos
local P2H = minetest.hash_node_position
local H2P = minetest.get_position_from_hash
local S = minecart.S
local function dashboard_destroy(self)
if self.driver and self.hud_id then
local player = minetest.get_player_by_name(self.driver)
if player then
player:hud_remove(self.hud_id)
self.hud_id = nil
end
end
end
local function dashboard_create(self)
if self.driver then
local player = minetest.get_player_by_name(self.driver)
if player then
dashboard_destroy(self)
self.hud_id = player:hud_add({
name = "minecart",
hud_elem_type = "text",
position = {x = 0.4, y = 0.25},
scale = {x=100, y=100},
text = "Recording:",
number = 0xFFFFFF,
size = {x = 1},
})
end
end
end
local function dashboard_update(self)
if self.driver and self.hud_id then
local player = minetest.get_player_by_name(self.driver)
if player then
local time = self.runtime or 0
local dir = (self.ctrl and self.ctrl.left and S("left")) or
(self.ctrl and self.ctrl.right and S("right")) or S("straight")
local speed = math.floor((self.curr_speed or 0) + 0.5)
local s = string.format(S("Recording") ..
" | " .. S("speed") ..
": %.1f | " .. S("next junction") ..
": %-8s | " .. S("Travel time") .. ": %.1f s",
speed, dir, time)
player:hud_change(self.hud_id, "text", s)
end
end
end
local function check_waypoint(self, pos)
-- If next waypoint already reached but not handled
-- determine next waypoint
if vector.equals(pos, self.waypoint.pos) then
local rot = self.object:get_rotation()
local dir = minetest.yaw_to_dir(rot.y)
dir.y = math.floor((rot.x / (math.pi/4)) + 0.5)
local facedir = minetest.dir_to_facedir(dir)
local waypoint = minecart.get_waypoint(pos, facedir, self.ctrl or {}, false)
if waypoint then
return waypoint.pos
end
end
return self.waypoint.pos
end
--
-- Route recording
--
function minecart.start_recording(self, pos)
--print("start_recording")
if self.driver then
self.start_pos = minecart.get_buffer_pos(pos, self.driver)
if self.start_pos then
self.checkpoints = {} -- {cart_pos, next_waypoint_pos, speed, dot}
self.junctions = {}
self.is_recording = true
self.rec_time = self.timebase
self.hud_time = self.timebase
self.runtime = 0
self.num_sections = 0
self.sum_speed = 0
self.ctrl = {}
dashboard_create(self)
dashboard_update(self, 0)
end
end
end
function minecart.stop_recording(self, pos)
--print("stop_recording")
if self.driver and self.is_recording then
local dest_pos = minecart.get_buffer_pos(pos, self.driver)
local player = minetest.get_player_by_name(self.driver)
if dest_pos and player and #self.checkpoints > 3 then
-- Remove last checkpoint, because it is potentially too close to the dest_pos
table.remove(self.checkpoints)
if self.start_pos then
local route = {
dest_pos = dest_pos,
checkpoints = self.checkpoints,
junctions = self.junctions,
}
minecart.store_route(self.start_pos, route)
minetest.chat_send_player(self.driver, S("[minecart] Route stored!"))
local speed = self.sum_speed / #self.checkpoints
local length = speed * self.runtime
local fmt = S("[minecart] Speed = %u m/s, Time = %u s, Route length = %u m")
minetest.chat_send_player(self.driver, string.format(fmt, speed, self.runtime, length))
end
end
dashboard_destroy(self)
end
self.is_recording = false
self.checkpoints = nil
self.waypoints = nil
self.junctions = nil
end
function minecart.recording_waypoints(self)
local pos = vector.round(self.object:get_pos())
-- hier müsste überprüfung dest_pos rein
self.sum_speed = self.sum_speed + self.curr_speed
local wp_pos = check_waypoint(self, pos)
self.checkpoints[#self.checkpoints+1] = {
-- cart_pos, next_waypoint_pos, speed, dot
P2H(pos),
P2H(wp_pos),
math.floor(self.curr_speed + 0.5),
self.waypoint.dot
}
end
function minecart.recording_junctions(self)
local player = minetest.get_player_by_name(self.driver)
if player then
local ctrl = player:get_player_control()
if ctrl.left then
self.ctrl = {left = true}
elseif ctrl.right then
self.ctrl = {right = true}
elseif ctrl.up or ctrl.down then
self.ctrl = nil
end
end
if self.hud_time <= self.timebase then
dashboard_update(self)
self.hud_time = self.timebase + 0.5
self.runtime = self.runtime + 0.5
end
end
function minecart.set_junctions(self, wayp_pos)
if self.ctrl then
self.junctions[P2H(wayp_pos)] = self.ctrl
end
end
function minecart.player_ctrl(self)
local player = minetest.get_player_by_name(self.driver)
if player then
local ctrl = player:get_player_control()
if ctrl.left then
self.ctrl = {left = true}
elseif ctrl.right then
self.ctrl = {right = true}
end
end
end

BIN
minecart/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@ -0,0 +1,3 @@
# If enabled, allows the complete automation of Minecarts by means of Hopper and station stop times.
minecart_hopper_enabled (Hopper enabled) bool true
minecart_teleport_enabled (Teleport enabled) bool false

107
minecart/signs.lua Normal file
View File

@ -0,0 +1,107 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg 4
MIT
See license.txt for more information
]]--
local S = minecart.S
local function register_sign(def)
minetest.register_node("minecart:"..def.name, {
description = def.description,
inventory_image = def.image,
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{ -1/16, -8/16, -1/16, 1/16,-2/16, 1/16},
{ -5/16, -2/16, -1/32, 5/16, 8/16, 1/32},
},
},
paramtype2 = "facedir",
tiles = {
"default_steel_block.png",
"default_steel_block.png",
"default_steel_block.png",
"default_steel_block.png",
"default_steel_block.png",
"default_steel_block.png^"..def.image,
},
after_place_node = minecart.delete_signs_waypoint,
preserve_metadata = minecart.delete_signs_waypoint,
on_rotate = screwdriver.disallow,
paramtype = "light",
use_texture_alpha = minecart.CLIP,
sunlight_propagates = true,
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 2, flammable = 2, minecart_sign = 1},
sounds = default.node_sound_wood_defaults(),
})
end
register_sign({
name = "speed1",
description = S('Speed "1"'),
image = "minecart_sign1.png",
})
register_sign({
name = "speed2",
description = S('Speed "2"'),
image = "minecart_sign2.png",
})
register_sign({
name = "speed4",
description = S('Speed "4"'),
image = "minecart_sign4.png",
})
register_sign({
name = "speed8",
description = S('No speed limit'),
image = "minecart_sign8.png",
})
minetest.register_craft({
output = "minecart:speed8 8",
recipe = {
{"default:tin_ingot", "dye:red", "default:tin_ingot"},
{"", "default:steel_ingot", ""},
{"", "default:steel_ingot", ""}
}
})
minetest.register_craft({
type = "shapeless",
output = "minecart:speed4",
recipe = {"minecart:speed8"}
})
minetest.register_craft({
type = "shapeless",
output = "minecart:speed2",
recipe = {"minecart:speed4"}
})
minetest.register_craft({
type = "shapeless",
output = "minecart:speed1",
recipe = {"minecart:speed2"}
})
minetest.register_craft({
type = "shapeless",
output = "minecart:speed8",
recipe = {"minecart:speed1"}
})

120
minecart/storage.lua Normal file
View File

@ -0,0 +1,120 @@
--[[
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 S2P = minetest.string_to_pos
local P2H = minetest.hash_node_position
local H2P = minetest.get_position_from_hash
local S = minecart.S
local storage = minetest.get_mod_storage()
local function place_carts(t)
local Carts = {
["minecart:cart"] = "minecart:cart",
["techage:tank_cart_entity"] = "techage:tank_cart",
["techage:chest_cart_entity"] = "techage:chest_cart",
}
for id, item in pairs(t) do
local pos = vector.round((item.start_pos or item.last_pos))
local name = Carts[item.entity_name] or "minecart:cart"
--print(P2S(pos), name, item.owner, item.userID)
if minetest.registered_nodes[name] then
minecart.add_nodecart(pos, name, 0, {}, item.owner or "", item.userID or 0)
end
end
end
-------------------------------------------------------------------------------
-- Store data of running carts
-------------------------------------------------------------------------------
minecart.CartsOnRail = {}
minetest.register_on_mods_loaded(function()
local version = storage:get_int("version")
if version < 2 then
local t = minetest.deserialize(storage:get_string("CartsOnRail")) or {}
minetest.after(5, place_carts, t)
storage:set_int("version", 2)
else
local t = minetest.deserialize(storage:get_string("CartsOnRail")) or {}
for owner, carts in pairs(t) do
minecart.CartsOnRail[owner] = {}
for userID, cart in pairs(carts) do
print("reload cart", owner, userID, cart.objID)
minecart.CartsOnRail[owner][userID] = cart
-- mark all entity carts as zombified
if cart.objID and cart.objID ~= 0 then
cart.objID = -1
minecart.push(1, cart)
end
end
end
end
end)
minetest.after(10, function()
for owner, carts in pairs(minecart.CartsOnRail) do
for userID, cart in pairs(carts) do
-- Remove node carts that are not available anymore
if cart.objID == 0 or not cart.objID then
local node = minecart.get_node_lvm(cart.pos)
if not minecart.tNodeNames[node.name] then
-- Mark as "to be deleted"
print("Node cart deleted", owner, userID)
minecart.CartsOnRail[owner][userID] = nil
end
end
end
end
end)
minetest.register_on_shutdown(function()
storage:set_string("CartsOnRail", minetest.serialize(minecart.CartsOnRail))
print("minecart shutdown finished!!!")
end)
function minecart.store_carts()
storage:set_string("CartsOnRail", minetest.serialize(minecart.CartsOnRail))
end
-------------------------------------------------------------------------------
-- Store routes (in buffers)
-------------------------------------------------------------------------------
function minecart.store_route(pos, route)
if pos and route then
M(pos):set_string("route", minetest.serialize(route))
return true
end
return false
end
function minecart.get_route(pos)
if pos then
local s = M(pos):get_string("route")
if s ~= "" then
local route = minetest.deserialize(s)
if route.waypoints then
M(pos):set_string("route", "")
M(pos):set_int("time", 0)
return
end
return minetest.deserialize(s)
end
end
end
function minecart.del_route(pos)
M(pos):set_string("route", "")
end

90
minecart/terminal.lua Normal file
View File

@ -0,0 +1,90 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
-- for lazy programmers
local M = minetest.get_meta
local S = minecart.S
local function is_player_nearby(pos)
for _, object in pairs(minetest.get_objects_inside_radius(pos, 6)) do
if object:is_player() then
return true
end
end
end
local function formspec(pos, text)
text = minetest.formspec_escape(text)
text = text:gsub("\n", ",")
return "size[11,9]"..
default.gui_bg..
default.gui_bg_img..
default.gui_slots..
"box[0,-0.1;10.8,0.5;#c6e8ff]"..
"label[4.5,-0.1;"..minetest.colorize( "#000000", S("Cart List")).."]"..
"style_type[table,field;font=mono]"..
"table[0,0.5;10.8,8.6;output;"..text..";200]"
end
minetest.register_node("minecart:terminal", {
description = S("Cart Terminal"),
inventory_image = "minecart_terminal_front.png",
tiles = {
"minecart_terminal_top.png",
"minecart_terminal_top.png",
"minecart_terminal_side.png",
"minecart_terminal_side.png",
"minecart_terminal_back.png",
"minecart_terminal_front.png",
},
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{ -8/16, -8/16, 0/16, 8/16, 8/16, 8/16},
},
},
after_place_node = function(pos, placer)
local meta = M(pos)
meta:set_string("owner", placer:get_player_name())
meta:set_string("formspec", formspec(pos, ""))
minetest.get_node_timer(pos):start(2)
end,
on_timer = function(pos, elapsed)
if is_player_nearby(pos) then
local text = minecart.get_cart_list(pos, M(pos):get_string("owner"))
M(pos):set_string("formspec", formspec(pos, text))
end
return true
end,
paramtype2 = "facedir",
paramtype = "light",
use_texture_alpha = minecart.CLIP,
on_rotate = screwdriver.disallow,
sunlight_propagates = true,
is_ground_content = false,
groups = {cracky = 2, level = 2},
sounds = default.node_sound_metal_defaults(),
})
minetest.register_craft({
output = "minecart:terminal",
recipe = {
{"", "default:obsidian_glass", "default:steel_ingot"},
{"", "default:obsidian_glass", "default:copper_ingot"},
{"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"},
},
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

2
minecart/textures/shrink.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
pngquant --skip-if-larger --quality=80 --strip *.png --ext .png --force

101
minecart/tool.lua Normal file
View File

@ -0,0 +1,101 @@
--[[
Minecart
========
Copyright (C) 2019-2021 Joachim Stolberg
MIT
See license.txt for more information
]]--
-- for lazy programmers
local M = minetest.get_meta
local S = minecart.S
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 sDir = {[0] = "north", "east", "south", "west"}
local function DOTS(dots)
if dots then
return table.concat(dots, ", ")
else
return ""
end
end
local old_pos
local function test_get_route(pos, node, player)
local yaw = player:get_look_horizontal()
local dir = minetest.yaw_to_dir(yaw)
local facedir = minetest.dir_to_facedir(dir)
local route = minecart.get_waypoint(pos, facedir, {})
if route then
-- print(dump(route))
minecart.set_marker(route.pos, "pos", 0.3, 10)
if route.cart_pos then
minecart.set_marker(route.cart_pos, "cart", 0.3, 10)
end
-- determine some kind of current y
old_pos = old_pos or pos
local curr_y = pos.y > old_pos.y and 1 or pos.y < old_pos.y and -1 or 0
local cart_pos, extra_cycle = minecart.get_current_cart_pos_correction(pos, facedir, curr_y, route.dot)
minecart.set_marker(cart_pos, "curr", 0.3, 10)
old_pos = pos
print(string.format("Route: dist = %u, dot = %u, speed = %d, extra cycle = %s",
vector.distance(pos, route.pos), route.dot, route.speed or 0, extra_cycle))
end
end
local function test_get_connections(pos, node, player, ctrl)
local wp = minecart.get_waypoints(pos)
for i = 0,3 do
if wp[i] then
local dir = minecart.Dot2Dir[ wp[i].dot]
print(sDir[i], vector.distance(pos, wp[i].pos), dir.y)
end
end
print(dump(M(pos):to_table()))
end
local function click_left(itemstack, placer, pointed_thing)
if pointed_thing.type == "node" then
local pos = pointed_thing.under
local node = minetest.get_node(pos)
if node.name == "carts:rail" or node.name == "carts:powerrail" then
test_get_route(pos, node, placer)
end
end
end
local function click_right(itemstack, placer, pointed_thing)
if pointed_thing.type == "node" then
local pos = pointed_thing.under
local node = minetest.get_node(pos)
if node.name == "carts:rail" or node.name == "carts:powerrail" then
test_get_connections(pos, node, placer)
elseif node.name == "minecart:buffer" then
local route = minecart.get_route(P2S(pos))
print(dump(route))
end
end
end
minetest.register_node("minecart:tool", {
description = "Tool",
inventory_image = "minecart_tool.png",
wield_image = "minecart_tool.png",
liquids_pointable = true,
use_texture_alpha = true,
groups = {cracky=1, book=1},
on_use = click_left,
on_place = click_right,
node_placement_prediction = "",
stack_max = 1,
})