aboutsummaryrefslogtreecommitdiff
path: root/advtrains
diff options
context:
space:
mode:
Diffstat (limited to 'advtrains')
-rw-r--r--advtrains/api_doc.txt2
-rw-r--r--advtrains/atc.lua52
-rw-r--r--advtrains/copytool.lua29
-rw-r--r--advtrains/couple.lua43
-rw-r--r--advtrains/craft_items.lua11
-rw-r--r--advtrains/crafting.lua17
-rw-r--r--advtrains/debugitems.lua56
-rw-r--r--advtrains/formspec.lua114
-rw-r--r--advtrains/helpers.lua195
-rw-r--r--advtrains/init.lua98
l---------advtrains/locale/README.md1
-rw-r--r--advtrains/locale/advtrains.de.tr77
-rw-r--r--advtrains/locale/advtrains.zh_CN.tr107
-rw-r--r--advtrains/lzb.lua10
-rw-r--r--advtrains/misc_nodes.lua11
-rw-r--r--advtrains/nodedb.lua14
-rw-r--r--advtrains/occupation.lua10
-rw-r--r--advtrains/p_mesecon_iface.lua26
-rw-r--r--advtrains/passive.lua94
-rw-r--r--advtrains/path.lua51
-rw-r--r--advtrains/po/README.md70
-rw-r--r--advtrains/po/advtrains.pot625
-rw-r--r--advtrains/po/de.po925
-rw-r--r--advtrains/po/fr.po1024
-rwxr-xr-xadvtrains/po/update-translations.sh28
-rw-r--r--advtrains/po/zh_CN.po863
-rw-r--r--advtrains/po/zh_TW.po863
-rw-r--r--advtrains/poconvert.lua185
-rw-r--r--advtrains/protection.lua16
-rw-r--r--advtrains/settingtypes.txt7
-rw-r--r--advtrains/signals.lua176
-rw-r--r--advtrains/spec/poconvert_spec.lua70
-rw-r--r--advtrains/spec/texture_spec.lua19
-rw-r--r--advtrains/spec/wagons_spec.lua40
-rw-r--r--advtrains/texture.lua228
-rw-r--r--advtrains/textures/advtrains_wagon_prop_tool.pngbin0 -> 779 bytes
-rw-r--r--advtrains/track_reg_helper.lua751
-rw-r--r--advtrains/trackdb_legacy.lua27
-rw-r--r--advtrains/trackplacer.lua629
-rw-r--r--advtrains/tracks.lua1027
-rw-r--r--advtrains/trainhud.lua200
-rw-r--r--advtrains/trainlogic.lua149
-rw-r--r--advtrains/wagonprop_tool.lua47
-rw-r--r--advtrains/wagons.lua365
44 files changed, 7411 insertions, 1941 deletions
diff --git a/advtrains/api_doc.txt b/advtrains/api_doc.txt
index 5668ba3..6b338a7 100644
--- a/advtrains/api_doc.txt
+++ b/advtrains/api_doc.txt
@@ -18,8 +18,6 @@ advtrains.register_wagon(name, prototype, description, inventory_image)
# Wagon prototype properties
{
... all regular luaentity properties (mesh, textures, collisionbox a.s.o)...
- drives_on = {default=true},
- ^- used to define the tracktypes (see below) that wagon can drive on. The tracktype identifiers are given as keys, similar to privileges)
max_speed = 10,
^- optional, default 10: defines the maximum speed this wagon can drive. The maximum speed of a train is determined by the wagon with the lowest max_speed value.
seats = {
diff --git a/advtrains/atc.lua b/advtrains/atc.lua
index c1ff218..e14e5c1 100644
--- a/advtrains/atc.lua
+++ b/advtrains/atc.lua
@@ -1,6 +1,9 @@
--atc.lua
--registers and controls the ATC system
+-- Get current translator
+local S = advtrains.translate
+
local atc={}
local eval_conditional
@@ -94,6 +97,7 @@ function atc.train_reset_command(train, keep_tarvel)
train.atc_brake_target=nil
train.atc_wait_finish=nil
train.atc_wait_autocouple=nil
+ train.atc_wait_signal=nil
train.atc_arrow=nil
if not keep_tarvel then
train.tarvelocity=nil
@@ -106,7 +110,7 @@ local apn_func=function(pos)
-- FIX for long-persisting ndb bug: there's no node in parameter 2 of this function!
local meta=minetest.get_meta(pos)
if meta then
- meta:set_string("infotext", attrans("ATC controller, unconfigured."))
+ meta:set_string("infotext", S("Unconfigured ATC controller"))
meta:set_string("formspec", atc.get_atc_controller_formspec(pos, meta))
end
end
@@ -128,27 +132,15 @@ advtrains.atc_function = function(def, preset, suffix, rotation)
local meta=minetest.get_meta(pos)
if meta then
if not fields.save then
- --[[--maybe only the dropdown changed
- if fields.mode then
- meta:set_string("mode", idxtrans[fields.mode])
- if fields.mode=="digiline" then
- meta:set_string("infotext", attrans("ATC controller, mode @1\nChannel: @2", fields.mode, meta:get_string("command")) )
- else
- meta:set_string("infotext", attrans("ATC controller, mode @1\nCommand: @2", fields.mode, meta:get_string("command")) )
- end
- meta:set_string("formspec", atc.get_atc_controller_formspec(pos, meta))
- end]]--
return
end
--meta:set_string("mode", idxtrans[fields.mode])
meta:set_string("command", fields.command)
--meta:set_string("command_on", fields.command_on)
meta:set_string("channel", fields.channel)
- --if fields.mode=="digiline" then
- -- meta:set_string("infotext", attrans("ATC controller, mode @1\nChannel: @2", fields.mode, meta:get_string("command")) )
- --else
- meta:set_string("infotext", attrans("ATC controller, mode @1\nCommand: @2", "-", meta:get_string("command")) )
- --end
+
+ meta:set_string("infotext", S("ATC controller, Command: @1", meta:get_string("command")) )
+
meta:set_string("formspec", atc.get_atc_controller_formspec(pos, meta))
local pts=minetest.pos_to_string(pos)
@@ -177,16 +169,11 @@ function atc.get_atc_controller_formspec(pos, meta)
if mode<3 then
formspec=formspec
.."style[command;font=mono]"
- .."field[0.8,1.5;7,1;command;"..attrans("Command")..";"..minetest.formspec_escape(command).."]"
- if tonumber(mode)==2 then
- formspec=formspec
- .."style[command_on;font=mono]"
- .."field[0.8,3;7,1;command_on;"..attrans("Command (on)")..";"..minetest.formspec_escape(command_on).."]"
- end
+ .."field[0.8,1.5;7,1;command;"..S("Command")..";"..minetest.formspec_escape(command).."]"
else
- formspec=formspec.."field[0.8,1.5;7,1;channel;"..attrans("Digiline channel")..";"..minetest.formspec_escape(channel).."]"
+ formspec=formspec.."field[0.8,1.5;7,1;channel;"..S("Digiline channel")..";"..minetest.formspec_escape(channel).."]"
end
- return formspec.."button_exit[0.5,4.5;7,1;save;"..attrans("Save").."]"
+ return formspec.."button_exit[0.5,4.5;7,1;save;"..S("Save").."]"
end
--from trainlogic.lua train step
@@ -233,7 +220,7 @@ local matchptn={
advtrains.train_ensure_init(id, train)
-- no one minds if this failed... this shouldn't even be called without train being initialized...
else
- atwarn(sid(id), attrans("ATC Reverse command warning: didn't reverse train, train moving!"))
+ atwarn(sid(id), S("ATC Reverse command warning: didn't reverse train, train moving."))
end
return 1
end,
@@ -245,11 +232,11 @@ local matchptn={
end,
["K"] = function(id, train)
if train.door_open == 0 then
- atwarn(sid(id), attrans("ATC Kick command warning: Doors closed"))
+ atwarn(sid(id), S("ATC Kick command warning: doors are closed."))
return 1
end
if train.velocity > 0 then
- atwarn(sid(id), attrans("ATC Kick command warning: Train moving"))
+ atwarn(sid(id), S("ATC Kick command warning: train moving."))
return 1
end
local tp = train.trainparts
@@ -278,6 +265,10 @@ local matchptn={
train.atc_wait_autocouple=true
return 3
end,
+ ["G"]=function(id, train)
+ train.atc_wait_signal=true
+ return 1
+ end,
}
eval_conditional = function(command, arrow, speed)
@@ -319,7 +310,7 @@ eval_conditional = function(command, arrow, speed)
local nest, pos, elsepos=0, 1
while nest>=0 do
if pos>#rest then
- atwarn(sid(id), attrans("ATC command syntax error: I statement not closed: @1",command))
+ atwarn(sid(id), S("ATC command syntax error: I statement not closed: @1",command))
return ""
end
local char=string.sub(rest, pos, pos)
@@ -375,14 +366,15 @@ function atc.execute_atc_command(id, train)
train.atc_command=string.sub(command, patlen+1)
if train.atc_delay<=0
and not train.atc_wait_finish
- and not train.atc_wait_autocouple then
+ and not train.atc_wait_autocouple
+ and not train.atc_wait_signal then
--continue (recursive, cmds shouldn't get too long, and it's a end-recursion.)
atc.execute_atc_command(id, train)
end
return
end
end
- atwarn(sid(id), attrans("ATC command parse error: Unknown command: @1", command))
+ atwarn(sid(id), S("ATC command parse error: Unknown command: @1", command))
atc.train_reset_command(train, true)
end
diff --git a/advtrains/copytool.lua b/advtrains/copytool.lua
index 0c1cdfe..cdfbd2e 100644
--- a/advtrains/copytool.lua
+++ b/advtrains/copytool.lua
@@ -4,8 +4,11 @@
-- 4.712389 = 1.5pi; sin(1.5pi) = -1
-- 7.853981 = 2.5pi; sin(2.5pi) = 1
+-- Get current translator
+local S = advtrains.translate
+
minetest.register_tool("advtrains:copytool", {
- description = attrans("Train copy/paste tool\n\nLeft-click: copy train\nRight-click: paste train"),
+ description = S("Train copy/paste tool\n\nLeft-click: copy train\nRight-click: paste train"),
inventory_image = "advtrains_copytool.png",
wield_image = "advtrains_copytool.png",
stack_max = 1,
@@ -21,12 +24,12 @@ minetest.register_tool("advtrains:copytool", {
local node=minetest.get_node_or_nil(pointed_thing.under)
if not node then atprint("[advtrains]Ignore at placer position") return itemstack end
local nodename=node.name
- if(not advtrains.is_track_and_drives_on(nodename, {default=true})) then
+ if(not advtrains.is_track(nodename)) then
atprint("no track here, not placing.")
return itemstack
end
if not minetest.check_player_privs(placer, {train_operator = true }) then
- minetest.chat_send_player(pname, "You don't have the train_operator privilege.")
+ minetest.chat_send_player(pname, S("You do not have the @1 privilege.", "train_operator"))
return itemstack
end
if not minetest.check_player_privs(placer, {train_admin = true }) and minetest.is_protected(pointed_thing.under, placer:get_player_name()) then
@@ -38,23 +41,23 @@ minetest.register_tool("advtrains:copytool", {
local prevpos = advtrains.get_adjacent_rail(pointed_thing.under, tconns, plconnid, {default=true})
if not prevpos then
- minetest.chat_send_player(pname, "The track you are trying to place the wagon on is not long enough!")
+ minetest.chat_send_player(pname, S("The track you are trying to place the wagon on is not long enough."))
return
end
local meta = itemstack:get_meta()
if not meta then
- minetest.chat_send_player(pname, attrans("The clipboard couldn't access the metadata. Paste failed."))
+ minetest.chat_send_player(pname, S("The clipboard couldn't access the metadata. Paste failed."))
return
end
local clipboard = meta:get_string("clipboard")
if (clipboard == "") then
- minetest.chat_send_player(pname, "The clipboard is empty.");
+ minetest.chat_send_player(pname, S("The clipboard is empty."));
return
end
clipboard = minetest.deserialize(clipboard)
if (clipboard.wagons == nil) then
- minetest.chat_send_player(pname, "The clipboard is empty.");
+ minetest.chat_send_player(pname, S("The clipboard is empty."));
return
end
@@ -71,7 +74,7 @@ minetest.register_tool("advtrains:copytool", {
local train = advtrains.trains[id]
train.off_track = train.end_index<train.path_trk_b
if (train.off_track) then
- minetest.chat_send_player(pname, "Back of train would end up off track, cancelling.")
+ minetest.chat_send_player(pname, S("Back of train would end up off track, cancelling."))
advtrains.remove_train(id)
return
end
@@ -89,19 +92,19 @@ minetest.register_tool("advtrains:copytool", {
local le = pointed_thing.ref:get_luaentity()
if (le == nil) then
- minetest.chat_send_player(user:get_player_name(), "No such lua entity!")
+ minetest.chat_send_player(user:get_player_name(), S("No such lua entity."))
return
end
local wagon = advtrains.wagons[le.id]
if (not (le.id and advtrains.wagons[le.id])) then
- minetest.chat_send_player(user:get_player_name(), string.format("No such wagon: %s", le.id))
+ minetest.chat_send_player(user:get_player_name(), S("No such wagon: @1.", le.id))
return
end
local train = advtrains.trains[wagon.train_id]
if (not train) then
- minetest.chat_send_player(user:get_player_name(), string.format("No such train: %s", wagon.train_id))
+ minetest.chat_send_player(user:get_player_name(), S("No such train: @1.", wagon.train_id))
return
end
@@ -173,11 +176,11 @@ minetest.register_tool("advtrains:copytool", {
local meta = itemstack:get_meta()
if not meta then
- minetest.chat_send_player(pname, attrans("The clipboard couldn't access the metadata. Copy failed."))
+ minetest.chat_send_player(pname, S("The clipboard couldn't access the metadata. Copy failed."))
return
end
meta:set_string("clipboard", minetest.serialize(clipboard))
- minetest.chat_send_player(user:get_player_name(), attrans("Train copied!"))
+ minetest.chat_send_player(user:get_player_name(), S("Train copied."))
return itemstack
end
})
diff --git a/advtrains/couple.lua b/advtrains/couple.lua
index b6a445e..cb325a0 100644
--- a/advtrains/couple.lua
+++ b/advtrains/couple.lua
@@ -18,6 +18,9 @@
-- train.couple_* contain references to ObjectRefs of couple objects, which contain all relevant information
-- These objectRefs will delete themselves once the couples no longer match (see below)
+-- Get current translator
+local S = advtrains.translate
+
advtrains.coupler_types = {}
function advtrains.register_coupler_type(code, name)
@@ -25,9 +28,23 @@ function advtrains.register_coupler_type(code, name)
end
-- Register some default couplers
-advtrains.register_coupler_type("chain", attrans("Buffer and Chain Coupler"))
-advtrains.register_coupler_type("scharfenberg", attrans("Scharfenberg Coupler"))
+advtrains.register_coupler_type("chain", S("Buffer and Chain Coupler"))
+advtrains.register_coupler_type("scharfenberg", S("Scharfenberg Coupler"))
+for _, name in pairs {"couple", "decouple"} do
+ local t = {}
+ local function reg(f)
+ table.insert(t, f)
+ end
+ local function cb(...)
+ for _, f in ipairs(t) do
+ f(...)
+ end
+ end
+ advtrains["te_registered_on_" .. name] = t
+ advtrains["te_register_on_" .. name] = reg
+ advtrains["te_run_callbacks_on_" .. name] = cb
+end
local function create_couple_entity(pos, train1, t1_is_front, train2, t2_is_front)
local id1 = train1.id
@@ -184,8 +201,8 @@ end
function advtrains.safe_couple_trains(train1, t1_is_front, train2, t2_is_front, pname)
if pname and not minetest.check_player_privs(pname, "train_operator") then
- minetest.chat_send_player(pname, "Missing train_operator privilege")
- return false
+ minetest.chat_send_player(pname, S("You are not allowed to couple trains without the train_operator privilege."))
+ return false
end
local wck_t1, wck_t2
@@ -231,10 +248,12 @@ function advtrains.couple_trains(init_train, invert_init_train, stat_train, stat
-- sanity check, prevent coupling if train would be longer than 20 after coupling
local tot_len = init_wagoncnt + stat_wagoncnt
if tot_len > advtrains.TRAIN_MAX_WAGONS then
- atwarn("Cannot couple",stat_train.id,"and",init_train.id,"- train would have length",tot_len,"which is above the limit of",advtrains.TRAIN_MAX_WAGONS)
+ atwarn(S("Cannot couple @1 and @2 - train would have length @3 which is above the limit of @4", stat_train.id, init_train.id, tot_len, advtrains.TRAIN_MAX_WAGONS))
return
end
+ advtrains.te_run_callbacks_on_couple(init_train, stat_train)
+
if stat_train_opposite then
-- insert wagons in inverse order and set their wagon_flipped state
for i=1,stat_wagoncnt do
@@ -319,7 +338,7 @@ function advtrains.check_matching_coupler_types(t1, t1_front, t2, t2_front)
--atdebug("CMCT: t1",t1_cplt,"t2",t2_cplt,"")
-- if at least one of the trains has no couplers table, it always couples (fallback behavior and mode for universal shunters)
- if not t1_cplt or not t2_cplt then
+ if minetest.settings:get_bool("advtrains_universal_couplers", false) or not t1_cplt or not t2_cplt then
return true
end
@@ -335,12 +354,12 @@ function advtrains.check_matching_coupler_types(t1, t1_front, t2, t2_front)
for typ,_ in pairs(t1_cplt) do
table.insert(t1_cplhr, advtrains.coupler_types[typ] or typ)
end
- if #t1_cplhr==0 then t1_cplhr[1]=attrans("<none>") end
+ if #t1_cplhr==0 then t1_cplhr[1]=S("<No coupler>") end
for typ,_ in pairs(t2_cplt) do
table.insert(t2_cplhr, advtrains.coupler_types[typ] or typ)
end
- if #t2_cplhr==0 then t2_cplhr[1]=attrans("<none>") end
- return false, attrans("Can not couple: The couplers of the trains do not match (@1 and @2).", table.concat(t1_cplhr, ","), table.concat(t2_cplhr, ","))
+ if #t2_cplhr==0 then t2_cplhr[1]=S("<No coupler>") end
+ return false, S("Can not couple: The couplers of the trains do not match (@1 and @2).", table.concat(t1_cplhr, ","), table.concat(t2_cplhr, ","))
end
-- DECOUPLING --
@@ -437,7 +456,7 @@ minetest.register_entity("advtrains:discouple", {
self.object:remove()
return
end
- --getyaw seems to be a reliable method to check if an object is loaded...if it returns nil, it is not.
+ --get_yaw seems to be a reliable method to check if an object is loaded...if it returns nil, it is not.
if not self.wagon.object:get_yaw() then
self.object:remove()
return
@@ -484,7 +503,7 @@ minetest.register_entity("advtrains:couple", {
self.object:remove()
end,
on_step=function(self, dtime)
- if advtrains.wagon_outside_range(self.object:getpos()) then
+ if advtrains.wagon_outside_range(self.object:get_pos()) then
--atdebug("Couple Removing outside range")
self.object:remove()
return
@@ -523,7 +542,7 @@ minetest.register_entity("advtrains:couple", {
tp2=advtrains.path_get_interpolated(train2, train2.end_index)
end
local pos_median=advtrains.pos_median(tp1, tp2)
- if not vector.equals(pos_median, self.object:getpos()) then
+ if not vector.equals(pos_median, self.object:get_pos()) then
self.object:set_pos(pos_median)
end
self.position_set=true
diff --git a/advtrains/craft_items.lua b/advtrains/craft_items.lua
index 0e693eb..6b1dcef 100644
--- a/advtrains/craft_items.lua
+++ b/advtrains/craft_items.lua
@@ -1,23 +1,26 @@
+-- Get current translator
+local S = advtrains.translate
+
core.register_craftitem("advtrains:boiler", {
- description = attrans("Boiler"),
+ description = S("Boiler"),
inventory_image = "advtrains_boiler.png",
})
core.register_craftitem("advtrains:driver_cab", {
- description = attrans("driver's cab"),
+ description = S("Driver's cab"),
inventory_image = "advtrains_driver_cab.png",
})
core.register_craftitem("advtrains:wheel", {
- description = attrans("Wheel"),
+ description = S("Wheel"),
inventory_image = "advtrains_wheel.png",
})
core.register_craftitem("advtrains:chimney", {
- description = attrans("Chimney"),
+ description = S("Chimney"),
inventory_image = "advtrains_chimney.png",
})
diff --git a/advtrains/crafting.lua b/advtrains/crafting.lua
index 7626d55..9f80456 100644
--- a/advtrains/crafting.lua
+++ b/advtrains/crafting.lua
@@ -22,6 +22,14 @@ minetest.register_craft({
})
--Wallmounted Signal
minetest.register_craft({
+ output = 'advtrains:signal_wall_l_off 2',
+ recipe = {
+ {'default:steel_ingot', 'default:steel_ingot', 'dye:red'},
+ {'', 'default:steel_ingot', ''},
+ {'default:steel_ingot', 'default:steel_ingot', 'dye:dark_green'},
+ },
+})
+minetest.register_craft({
output = 'advtrains:signal_wall_r_off 2',
recipe = {
{'dye:red', 'default:steel_ingot', 'default:steel_ingot'},
@@ -29,6 +37,15 @@ minetest.register_craft({
{'dye:dark_green', 'default:steel_ingot', 'default:steel_ingot'},
},
})
+minetest.register_craft({
+ output = 'advtrains:signal_wall_t_off 2',
+ recipe = {
+ {'default:steel_ingot', '', 'default:steel_ingot'},
+ {'default:steel_ingot', 'default:steel_ingot', 'default:steel_ingot'},
+ {'dye:dark_green', '', 'dye:red'},
+ },
+})
+
--Wallmounted Signals can be converted into every orientation by shapeless crafting
minetest.register_craft({
diff --git a/advtrains/debugitems.lua b/advtrains/debugitems.lua
index e598216..2236cba 100644
--- a/advtrains/debugitems.lua
+++ b/advtrains/debugitems.lua
@@ -81,3 +81,59 @@ minetest.register_tool("advtrains:wagonpos_tester",
end,
}
)
+
+
+local function trackitest(initial_pos, initial_connid)
+ local ti, pos, connid, ok
+ ti = advtrains.get_track_iterator(initial_pos, initial_connid, 500, true)
+ atdebug("Starting at pos:",initial_pos,initial_connid)
+ while ti:has_next_branch() do
+ pos, connid = ti:next_branch() -- in first iteration, this will be the node at initial_pos. In subsequent iterations this will be the switch node from which we are branching off
+ atdebug("Next Branch:",pos, connid)
+ ok = true
+ while ok do
+ ok, pos, connid = ti:next_track()
+ atdebug("Next Track:", ok, pos, connid)
+ end
+ end
+ atdebug("End of traverse. Visited: ",table.concat(ti.visited, ","))
+end
+
+minetest.register_tool("advtrains:trackitest",
+{
+ description = "Track Iterator Tester (leftclick conn 1, rightclick conn 2)",
+ groups = {cracky=1}, -- key=name, value=rating; rating=1..3.
+ inventory_image = "advtrains_track_swlcr_45.png",
+ wield_image = "advtrains_track_swlcr_45.png",
+ stack_max = 1,
+ range = 7.0,
+
+ on_place = function(itemstack, placer, pointed_thing)
+ if pointed_thing.type ~= "node" then return end
+ trackitest(pointed_thing.under, 2)
+ end,
+ on_use = function(itemstack, user, pointed_thing)
+ if pointed_thing.type ~= "node" then return end
+ trackitest(pointed_thing.under, 1)
+ end,
+}
+)
+
+minetest.register_chatcommand("at_trackdef_audit",
+ {
+ params = "",
+ description = "Performs an audit of all track definitions currently loaded and checks for potential problems",
+ func = function(name, param)
+ for name, ndef in pairs(minetest.registered_nodes) do
+ --TODO finish this!
+ if ndef.at_conns then
+ -- check if conn_map is there and if it has enough entries
+ if #ndef.at_conns > 2 then
+ if #ndef.at_conn_map < #ndef.at_conns then
+ atwarn("AUDIT: Node",name,"- Not enough connmap entries! Check ndef:",ndef)
+ end
+ end
+ end
+ end
+ end,
+})
diff --git a/advtrains/formspec.lua b/advtrains/formspec.lua
new file mode 100644
index 0000000..86d8b27
--- /dev/null
+++ b/advtrains/formspec.lua
@@ -0,0 +1,114 @@
+local sformat = string.format
+local fsescape = minetest.formspec_escape
+
+-- Get current translator
+local S = advtrains.translate
+
+local function make_list(entries)
+ local t = {}
+ for k, v in ipairs(entries) do
+ t[k] = fsescape(v)
+ end
+ return table.concat(t, ",")
+end
+
+local function S_wrapper(f, i0)
+ return function(...)
+ local args = {...}
+ args[i0] = S(unpack(args,i0))
+ return f(unpack(args,1,i0))
+ end
+end
+
+local function f_button(x, y, w, id, text)
+ return sformat("button[%f,%f;%f,0.75;%s;%s]", x, y, w, id, text)
+end
+
+local function f_checkbox(x, y, name, selected, label)
+ return sformat("checkbox[%f,%f;%s;%s;%s]", x, y+0.25, name, label, selected and "true" or "false")
+end
+
+local function f_button_exit(x, y, w, id, text)
+ return sformat("button_exit[%f,%f;%f,0.75;%s;%s]", x, y, w, id, text)
+end
+
+local function f_dropdown(x, y, w, id, entries, sel, indexed)
+ return sformat("dropdown[%f,%f;%f,0.75;%s;%s;%d%s]",
+ x, y, w, id, make_list(entries),
+ sel or 1,
+ indexed and ";true" or "")
+end
+
+local function f_image_button(x, y, w, h, texture, id, label, noclip, drawborder, pressed)
+ local st = {string.format("%f,%f;%f,%f;%s;%s;%s", x, y, w, h, fsescape(texture), fsescape(id), fsescape(label))}
+ if pressed then
+ st[#st+1] = tostring(noclip or false)
+ st[#st+1] = tostring(drawborder or false)
+ st[#st+1] = fsescape(pressed)
+ end
+ return sformat("image_button[%s]", table.concat(st, ";"))
+end
+
+local function f_image_button_exit(x, y, w, h, texture, id, label)
+ local st = {string.format("%f,%f;%f,%f;%s;%s;%s", x, y, w, h, fsescape(texture), fsescape(id), fsescape(label))}
+ return sformat("image_button_exit[%s]", table.concat(st, ";"))
+end
+
+local function f_label(x, y, text)
+ return sformat("label[%f,%f;%s]", x, y+0.25, fsescape(text))
+end
+
+local function f_field_aux(x, y, w, id, default)
+ return sformat("field[%f,%f;%f,0.75;%s;;%s]", x, y, w, id, default)
+end
+
+local function f_field(x, y, w, id, label, default)
+ return f_label(x, y-0.5, label) .. f_field_aux(x, y, w, id, default)
+end
+
+local function f_tabheader(x, y, w, h, id, entries, sel, transparent, border)
+ local st = {string.format("%f,%f",x, y)}
+ if h then
+ if w then
+ st[#st+1] = string.format("%f,%f", w, h)
+ else
+ st[#st+1] = tostring(h)
+ end
+ end
+ st[#st+1] = tostring(id)
+ st[#st+1] = make_list(entries)
+ st[#st+1] = tostring(sel)
+ if transparent ~= nil then
+ st[#st+1] = tostring(transparent)
+ if border ~= nil then
+ st[#st+1] = tostring(border)
+ end
+ end
+ return string.format("tabheader[%s]", table.concat(st, ";"))
+end
+
+local function f_textlist(x, y, w, h, id, entries, sel, transparent)
+ local st = {string.format("%f,%f;%f,%f;%s;%s", x, y, w, h, id, make_list(entries))}
+ if sel then
+ st[#st+1] = tostring(sel)
+ st[#st+1] = tostring(transparent or false)
+ end
+ return string.format("textlist[%s]", table.concat(st, ";"))
+end
+
+return {
+ button = f_button,
+ S_button = S_wrapper(f_button, 5),
+ checkbox = f_checkbox,
+ S_checkbox = S_wrapper(f_checkbox, 5),
+ button_exit = f_button_exit,
+ S_button_exit = S_wrapper(f_button_exit, 5),
+ dropdown = f_dropdown,
+ field = f_field,
+ image_button = f_image_button,
+ image_button_exit = f_image_button_exit,
+ label = f_label,
+ S_label = S_wrapper(f_label, 3),
+ tabheader = f_tabheader,
+ textlist = f_textlist,
+}
diff --git a/advtrains/helpers.lua b/advtrains/helpers.lua
index cf890ca..e58625f 100644
--- a/advtrains/helpers.lua
+++ b/advtrains/helpers.lua
@@ -292,18 +292,19 @@ function advtrains.conn_matches_to(conn, other_conns)
end
-- Going from the rail at pos (does not need to be rounded) along connection with id conn_idx, if there is a matching rail, return it and the matching connid
--- returns: <adjacent pos>, <conn index of adjacent>, <my conn index>, <railheight of adjacent>
+-- returns: <adjacent pos>, <conn index of adjacent>, <my conn index>, <railheight of adjacent>, (adjacent conns table), (adjacent connmap table)
-- parameter this_conns_p is connection table of this rail and is optional, is determined by get_rail_info_at if not provided.
-function advtrains.get_adjacent_rail(this_posnr, this_conns_p, conn_idx, drives_on)
+function advtrains.get_adjacent_rail(this_posnr, this_conns_p, conn_idx)
local this_pos = advtrains.round_vector_floor_y(this_posnr)
local this_conns = this_conns_p
+ local _
if not this_conns then
_, this_conns = advtrains.get_rail_info_at(this_pos)
end
if not conn_idx then
for coni, _ in ipairs(this_conns) do
- local adj_pos, adj_conn_idx, _, nry, nco = advtrains.get_adjacent_rail(this_pos, this_conns, coni)
- if adj_pos then return adj_pos,adj_conn_idx,coni,nry, nco end
+ local adj_pos, adj_conn_idx, _, nry, nco, ncm = advtrains.get_adjacent_rail(this_pos, this_conns, coni)
+ if adj_pos then return adj_pos,adj_conn_idx,coni,nry, nco, ncm end
end
return nil
end
@@ -317,34 +318,46 @@ function advtrains.get_adjacent_rail(this_posnr, this_conns_p, conn_idx, drives_
adj_pos.y = adj_pos.y + 1
end
- local nextnode_ok, nextconns, nextrail_y=advtrains.get_rail_info_at(adj_pos, drives_on)
+ local nextnode_ok, nextconns, nextrail_y, nextconnmap=advtrains.get_rail_info_at(adj_pos)
if not nextnode_ok then
adj_pos.y = adj_pos.y - 1
conn_y = conn_y + 1
- nextnode_ok, nextconns, nextrail_y=advtrains.get_rail_info_at(adj_pos, drives_on)
+ nextnode_ok, nextconns, nextrail_y, nextconnmap=advtrains.get_rail_info_at(adj_pos)
if not nextnode_ok then
return nil
end
end
local adj_connid = advtrains.conn_matches_to({c=conn.c, y=conn_y}, nextconns)
if adj_connid then
- return adj_pos, adj_connid, conn_idx, nextrail_y, nextconns
+ return adj_pos, adj_connid, conn_idx, nextrail_y, nextconns, nextconnmap
end
return nil
end
-- when a train enters a rail on connid 'conn', which connid will it go out?
--- nconns: number of connections in connection table:
--- 2 = straight rail; 3 = turnout, 4 = crossing, 5 = three-way turnout (5th entry is a stub)
+-- Since 2.5: This mapping is contained in the conn_map table in the node definition!
-- returns: connid_out
-local connlku={[2]={2,1}, [3]={2,1,1}, [4]={2,1,4,3}, [5]={2,1,1,1}}
-function advtrains.get_matching_conn(conn, nconns)
- return connlku[nconns][conn]
+function advtrains.get_matching_conn(conn, conn_map)
+ if tonumber(conn_map) then
+ error("Legacy call to get_matching_conn! Instead of nconns, conn_map needs to be provided!")
+ end
+ if not conn_map then
+ --OK for two-conn rails, just return the other
+ if conn==1 then return 2 end
+ if conn==2 then return 1 end
+ error("get_matching_conn: For connid >=3, conn_map must not be nil!")
+ end
+ local cout = conn_map[conn]
+ if not cout then
+ error("get_matching_conn: Connid "..conn.." not found in conn_map which is "..atdump(conn_map))
+ end
+ return cout
end
-function advtrains.random_id()
+function advtrains.random_id(lenp)
local idst=""
- for i=0,5 do
+ local len = lenp or 6
+ for i=1,len do
idst=idst..(math.random(0,9))
end
return idst
@@ -461,7 +474,13 @@ end
local active_node_range = tonumber(minetest.settings:get("active_block_range"))*16 + 16
-- Function to check whether node at position(pos) is "loaded"/"active"
-- That is, whether it is within the active_block_range to a player
-if minetest.is_block_active then -- define function differently whether minetest.is_block_active is available or not
+if core.compare_block_status then
+ -- latest API
+ function advtrains.is_node_loaded(pos)
+ return core.compare_block_status(pos, "active")
+ end
+elseif minetest.is_block_active then -- define function differently whether minetest.is_block_active is available or not
+ -- API added by my PR but later superseded by the above and now removed
advtrains.is_node_loaded = minetest.is_block_active
else
function advtrains.is_node_loaded(pos)
@@ -470,3 +489,149 @@ else
end
end
end
+
+
+-- TrackIterator interface --
+
+-- Metatable:
+local trackiter_mt = {
+ -- Internal State:
+ -- branches: A list of {pos, connid, limit} for where to restart
+ -- pos: The *next* position that the track iterator will return
+ -- bconnid: The connid of the connection of the rail at pos that points backward
+ -- tconns: The connections of the rail at pos
+ -- limit: the current limit
+ -- visited: a key-boolean table of already visited rails
+
+ -- get whether there are still unprocessed branches
+ has_next_branch = function(self)
+ return #self.branches > 0
+ end,
+ -- go to the next unprocessed branch
+ -- returns track_pos, track_connid of the switch/crossing node where the track branches off
+ next_branch = function(self)
+ local br = table.remove(self.branches, 1)
+ -- Advance internal state
+ local adj_pos, adj_connid, _, _, adj_conns, adj_connmap = advtrains.get_adjacent_rail(br.pos, nil, br.connid)
+ self.pos = adj_pos
+ self.bconnid = adj_connid
+ self.tconns = adj_conns
+ self.tconnmap = adj_connmap
+ self.limit = br.limit - 1
+ self.visited[advtrains.encode_pos(br.pos)] = true
+ self.last_track_already_visited = false
+ return br.pos, br.connid
+ end,
+ -- get the next track along the current branch,
+ -- potentially adding branching tracks to the unprocessed branches list
+ -- returns track_pos, track_connid, track_backwards_connid
+ -- On error, returns nil, reason; reason is one of "track_end", "limit_hit", "already_visited"
+ next_track = function(self)
+ if self.last_track_already_visited then
+ -- see comment below
+ return nil, "already_visited"
+ end
+ local pos = self.pos
+ if not pos then
+ -- last run found track end. Return false
+ return false, "track_end"
+ end
+ -- if limit hit, return nil to signal this
+ if self.limit <= 0 then
+ return nil, "limit_hit"
+ end
+ -- select next conn (main conn to follow is the associated connection)
+ local old_bconnid = self.bconnid
+ local mconnid = advtrains.get_matching_conn(self.bconnid, self.tconnmap)
+ if self.visited[advtrains.encode_pos(pos)] then
+ -- node was already seen
+ -- Due to special requirements for the track section updater, return this first already visited track once
+ -- but do not process any further rails on this branch
+ -- The next call will then throw already_visited error
+ self.last_track_already_visited = true
+ return pos, mconnid, old_bconnid
+ end
+ -- If there are more connections, add these to branches
+ for nconnid,_ in ipairs(self.tconns) do
+ if nconnid~=mconnid and nconnid~=self.bconnid then
+ table.insert(self.branches, {pos = self.pos, connid = nconnid, limit=self.limit})
+ end
+ end
+ -- Advance internal state
+ local adj_pos, adj_connid, _, _, adj_conns, adj_connmap = advtrains.get_adjacent_rail(pos, self.tconns, mconnid)
+ self.pos = adj_pos
+ self.bconnid = adj_connid
+ self.tconns = adj_conns
+ self.tconnmap = adj_connmap
+ self.limit = self.limit - 1
+ self.visited[advtrains.encode_pos(pos)] = true
+ self.last_track_already_visited = false
+ return pos, mconnid, old_bconnid
+ end,
+
+ add_branch = function(self, pos, connid)
+ table.insert(self.branches, {pos = pos, connid = connid, limit=self.limit})
+ end,
+
+ is_visited = function(self, pos)
+ return self.visited[advtrains.encode_pos(pos)]
+ end,
+}
+
+-- Returns a new TrackIterator object
+
+-- Parameters:
+-- initial_pos: the initial track position of the track iterator
+-- initial_connid: the connection index in which to traverse. If nil, adds a "branch" for every connection of the track (traverse in all directions)
+-- limit: maximum distance from the start point after which the traverser stops
+-- follow_all: NOT IMPLEMENTED (supposed: if true, follows all branches at multi-connection tracks, even the ones pointing backwards or the crossing track on crossings. If false, follows only switches in driving direction.)
+
+-- Functions of the returned TrackIterator can be called via the Lua : notation, such as ti:next_track()
+-- If only the main track needs to be followed, use only the ti:next_track() function and do not call ti:next_branch().
+function advtrains.get_track_iterator(initial_pos, initial_connid, limit, follow_all)
+ local ti = {
+ visited = {}
+ }
+ if initial_connid then
+ ti.branches = { {pos = initial_pos, connid = initial_connid, limit=limit} }
+ else
+ -- get track info here
+ local node_ok, conns, rail_y=advtrains.get_rail_info_at(initial_pos)
+ assert(node_ok, "get_track_iterator called with non-track node!")
+ ti.branches = {}
+ for coni, _ in pairs(conns) do
+ table.insert(ti.branches, {pos = initial_pos, connid = coni, limit=limit})
+ end
+ end
+ ti.limit = limit -- safeguard if someone adds a branch before calling anything
+ setmetatable(ti, {__index=trackiter_mt})
+ return ti
+end
+
+--[[
+Example TrackIterator usage structure:
+
+local ti, pos, connid, ok
+ti = advtrains.get_track_iterator(initial_pos, initial_connid, 500, true)
+while ti:has_next_branch() do
+ pos, connid = ti:next_branch() -- in first iteration, this will be the node at initial_pos. In subsequent iterations this will be the switch node from which we are branching off
+ repeat
+ <do something with the track>
+ if <track satisfies an abort condition> then break end --for example, when traversing should stop at TCBs this can check if there is a tcb here
+ pos, connid = ti:next_track()
+ until not pos -- this stops the loop when either the track end is reached or the limit is hit
+ -- while loop continues with the next branch ( diverging branch of one of the switches/crossings) until no more are left
+end
+
+Example for walking only a single track (without branching):
+
+local ti, pos, connid, ok
+ti = advtrains.get_track_iterator(initial_pos, initial_connid, 500, true)
+
+pos, connid = ti:next_branch() -- this always needs to be done at least one time, and gets the track at initial_pos
+repeat
+ <do something with the track>
+ if <track satisfies an abort condition> then break end --for example, when traversing should stop at TCBs this can check if there is a tcb here
+ ok, pos, connid = ti:next_track()
+until not ok -- this stops the loop when either the track end is reached or the limit is hit
+]]
diff --git a/advtrains/init.lua b/advtrains/init.lua
index cc8f8d1..cd63104 100644
--- a/advtrains/init.lua
+++ b/advtrains/init.lua
@@ -22,9 +22,6 @@ Copyright (C) 2016-2020 Moritz Blei (orwell96) and contributors
local lot = os.clock()
minetest.log("action", "[advtrains] Loading...")
--- There is no need to support 0.4.x anymore given that the compatitability with it is already broken by 1bb1d825f46af3562554c12fba35a31b9f7973ff
-attrans = minetest.get_translator ("advtrains")
-
--advtrains
advtrains = {trains={}, player_to_train_mapping={}}
@@ -53,6 +50,17 @@ advtrains.TRAIN_MAX_WAGONS = 20
-- ==========================================================================
+advtrains.modpath = minetest.get_modpath("advtrains")
+
+-- Initialize internationalization (using ywang's poconvert)
+advtrains.poconvert = dofile(advtrains.modpath.."/poconvert.lua")
+advtrains.poconvert.from_flat("advtrains")
+-- ask engine for translator instance, this will load the translation files
+advtrains.translate = core.get_translator("advtrains")
+
+-- Get current translator
+local S = advtrains.translate
+
-- Use a global slowdown factor to slow down train movements. Now a setting
advtrains.DTIME_LIMIT = tonumber(minetest.settings:get("advtrains_dtime_limit")) or 0.2
advtrains.SAVE_INTERVAL = tonumber(minetest.settings:get("advtrains_save_interval")) or 60
@@ -70,7 +78,7 @@ end
local no_action=false
local function reload_saves()
- atwarn("Restoring saved state in 1 second...")
+ atwarn(S("Restoring saved state in 1 second..."))
no_action=true
advtrains.lock_path_inval = false
--read last save state and continue, as if server was restarted
@@ -81,13 +89,11 @@ local function reload_saves()
end
minetest.after(1, function()
advtrains.load()
- atwarn("Reload successful!")
+ atwarn(S("Reload successful!"))
advtrains.ndb.restore_all()
end)
end
-advtrains.modpath = minetest.get_modpath("advtrains")
-
--Advtrains dump (special treatment of pos and sigd)
function atdump(t, intend)
local str
@@ -181,7 +187,7 @@ function assertt(var, typ)
end
end
-dofile(advtrains.modpath.."/helpers.lua");
+dofile(advtrains.modpath.."/helpers.lua")
--dofile(advtrains.modpath.."/debugitems.lua");
advtrains.meseconrules =
@@ -202,19 +208,22 @@ advtrains.meseconrules =
advtrains.fpath=minetest.get_worldpath().."/advtrains"
advtrains.speed = dofile(advtrains.modpath.."/speed.lua")
+advtrains.formspec = dofile(advtrains.modpath.."/formspec.lua")
+advtrains.texture = dofile(advtrains.modpath.."/texture.lua")
dofile(advtrains.modpath.."/path.lua")
dofile(advtrains.modpath.."/trainlogic.lua")
dofile(advtrains.modpath.."/trainhud.lua")
dofile(advtrains.modpath.."/trackplacer.lua")
dofile(advtrains.modpath.."/copytool.lua")
+dofile(advtrains.modpath.."/wagonprop_tool.lua")
dofile(advtrains.modpath.."/tracks.lua")
+dofile(advtrains.modpath.."/track_reg_helper.lua")
dofile(advtrains.modpath.."/occupation.lua")
dofile(advtrains.modpath.."/atc.lua")
dofile(advtrains.modpath.."/wagons.lua")
dofile(advtrains.modpath.."/protection.lua")
-dofile(advtrains.modpath.."/trackdb_legacy.lua")
dofile(advtrains.modpath.."/nodedb.lua")
dofile(advtrains.modpath.."/couple.lua")
@@ -232,6 +241,9 @@ end
dofile(advtrains.modpath.."/lzb.lua")
+if minetest.settings:get_bool("advtrains_register_debugitems") then
+ dofile(advtrains.modpath.."/debugitems.lua")
+end
--load/save
@@ -336,7 +348,7 @@ function advtrains.avt_load()
end
end
for wid, _ in pairs(todel) do
- atwarn("Removing unused wagon", wid, "from wagon_save table.")
+ atwarn(S("Removing unused wagon"), wid, S("from wagon_save table."))
advtrains.wagon_save[wid]=nil
end
else
@@ -401,7 +413,7 @@ function advtrains.load_version_4()
end
end
for wid, _ in pairs(todel) do
- atwarn("Removing unused wagon", wid, "from wagon_save table.")
+ atwarn(S("Removing unused wagon"), wid, S("from wagon_save table."))
advtrains.wagon_save[wid]=nil
end
end
@@ -414,6 +426,16 @@ function advtrains.load_version_4()
if il_save then
advtrains.interlocking.db.load(il_save)
end
+
+ -- TODO 2.5.0 backwards compatibility fallback: Store the pre-v2.5.0 save file so that it can be reverted to if needed
+ local fallback_file = advtrains.fpath.."_interlocking.ls.pre250"
+ local file = io.open(fallback_file, "rb")
+ if file then
+ io.close(file)
+ else
+ atwarn("Backing up pre-2.5.0 version of Interlocking save file to",fallback_file," for potential downgrade to older versions")
+ os.rename(advtrains.fpath.."_interlocking.ls", fallback_file)
+ end
end
--== load lines ==
@@ -473,12 +495,13 @@ advtrains.avt_save = function(remove_players_from_wagons)
"atc_brake_target", "atc_wait_finish", "atc_command", "atc_delay", "door_open",
"text_outside", "text_inside", "line", "routingcode",
"il_sections", "speed_restriction", "speed_restrictions_t", "is_shunt",
- "points_split", "autocouple", "atc_wait_autocouple", "ars_disable",
+ "path_ori_cp", "autocouple", "atc_wait_autocouple", "ars_disable",
+ "staticdata",
})
--then save it
tmp_trains[id]=v
else
- atwarn("Train",id,"had no wagons left because of some bug. It is being deleted. Wave it goodbye!")
+ atwarn(S("Train @1 had no wagons left because of some bug. It is being deleted. Wave it goodbye!", id))
advtrains.remove_train(id)
end
end
@@ -559,7 +582,7 @@ advtrains.avt_save = function(remove_players_from_wagons)
local succ, err = serialize_lib.save_atomic_multiple(parts_table, advtrains.fpath.."_", callbacks_table)
if not succ then
- atwarn("Saving failed: "..err)
+ atwarn(S("Saving failed: @1", err))
else
-- store version
advtrains.save_component(4, "version")
@@ -662,7 +685,7 @@ end
function advtrains.save(remove_players_from_wagons)
if not init_load then
--wait... we haven't loaded yet?!
- atwarn("Instructed to save() but load() was never called!")
+ atwarn(S("Instructed to save() but load() was never called!"))
return
end
@@ -691,7 +714,7 @@ function advtrains.save(remove_players_from_wagons)
end
minetest.register_on_shutdown(function()
if within_mainstep then
- atwarn("Crash during advtrains main step - skipping the shutdown save operation to not save inconsistent data!")
+ atwarn(S("Crash during advtrains main step - skipping the shutdown save operation to not save inconsistent data!"))
else
advtrains.save()
end
@@ -703,10 +726,10 @@ end)
minetest.register_chatcommand("at_empty_seats",
{
params = "", -- Short parameter description
- description = "Detach all players, especially the offline ones, from all trains. Use only when no one serious is on a train.", -- Full description
+ description = S("Detach all players, especially the offline ones, from all trains. Use only when no one serious is on a train."), -- Full description
privs = {train_operator=true, server=true}, -- Require the "privs" privilege to run
func = function(name, param)
- atwarn("Data is being saved. While saving, advtrains will remove the players from trains. Save files will be reloaded afterwards!")
+ atwarn(S("Data is being saved. While saving, advtrains will remove the players from trains. Save files will be reloaded afterwards!"))
advtrains.save(true)
reload_saves()
end,
@@ -715,45 +738,60 @@ minetest.register_chatcommand("at_empty_seats",
minetest.register_chatcommand("at_reroute",
{
params = "",
- description = "Delete all train routes, force them to recalculate",
+ description = S("Delete all train routes, force them to recalculate"),
privs = {train_operator=true}, -- Only train operator is required, since this is relatively safe.
func = function(name, param)
advtrains.invalidate_all_paths()
- return true, "Successfully invalidated train routes"
+ return true, S("Successfully invalidated train routes")
end,
})
minetest.register_chatcommand("at_whereis",
{
params = "<train id>",
- description = "Returns the position of the train with the given id",
+ description = S("Returns the position of the train with the given id"),
privs = {train_operator = true},
func = function(name,param)
local train = advtrains.trains[param]
if not train or not train.last_pos then
- return false, "Train "..param.." does not exist or is invalid"
+ return false, S("Train @1 does not exist or is invalid", param)
+ else
+ return true, S("Train @1 is at @2", param, minetest.pos_to_string(train.last_pos))
+ end
+ end,
+})
+minetest.register_chatcommand("at_tp",
+ {
+ params = "<train id>",
+ description = S("Teleports you to the position of the train with the given id"),
+ privs = {train_operator = true, teleport = true},
+ func = function(name,param)
+ local train = advtrains.trains[param]
+ if not train or not train.last_pos then
+ return false, S("Train @1 does not exist or is invalid", param)
else
- return true, "Train "..param.." is at "..minetest.pos_to_string(train.last_pos)
+ minetest.get_player_by_name(name):set_pos(train.last_pos)
+ return true, S("Teleporting to train @1", param)
end
end,
})
minetest.register_chatcommand("at_disable_step",
{
params = "<yes/no>",
- description = "Disable the advtrains globalstep temporarily",
+ description = S("Disable the advtrains globalstep temporarily"),
privs = {server=true},
func = function(name, param)
if minetest.is_yes(param) then
-- disable everything, and turn off saving
no_action = true;
- atwarn("The advtrains globalstep has been disabled. Trains are not moving, and no data is saved! Run '/at_disable_step no' to enable again!")
- return true, "Disabled advtrains successfully"
+ atwarn(S("The advtrains globalstep has been disabled. Trains are not moving, and no data is saved! Run '/at_disable_step no' to enable again!"))
+ return true, S("Disabled advtrains successfully")
elseif no_action then
- atwarn("Re-enabling advtrains globalstep...")
+ atwarn(S("Re-enabling advtrains globalstep..."))
reload_saves()
return true
else
- return false, "Advtrains is already running normally!"
+ return false, S("Advtrains is already running normally!")
end
end,
})
@@ -761,10 +799,10 @@ minetest.register_chatcommand("at_disable_step",
minetest.register_chatcommand("at_status",
{
params = "",
- description = "Print advtrains status info",
+ description = S("Print advtrains status info"),
privs = {train_operator = true},
func = function(name, param)
- return true, advtrains.print_concat_table({"Advtrains Status: no_action",no_action,"slowdown",advtrains.global_slowdown,"(log",math.log(advtrains.global_slowdown),")"})
+ return true, S("Advtrains Status: no_action @1 slowdown @2 (log @3)", no_action, advtrains.global_slowdown, math.log(advtrains.global_slowdown))
end,
})
diff --git a/advtrains/locale/README.md b/advtrains/locale/README.md
new file mode 120000
index 0000000..61e473c
--- /dev/null
+++ b/advtrains/locale/README.md
@@ -0,0 +1 @@
+../po/README.md \ No newline at end of file
diff --git a/advtrains/locale/advtrains.de.tr b/advtrains/locale/advtrains.de.tr
deleted file mode 100644
index 6abbc12..0000000
--- a/advtrains/locale/advtrains.de.tr
+++ /dev/null
@@ -1,77 +0,0 @@
-# textdomain: advtrains
-This wagon is owned by @1, you can't destroy it.=Dieser Waggon gehört @1, du kannst ihn nicht abbauen.
-Warning: If you destroy this wagon, you only get some steel back! If you are sure, hold Sneak and left-click the wagon.=Warnung: Du erhältst nur etwas Stahl zurück. Wenn du sicher bist, dass du den Waggon zerstören willst, halte 'Schleichen' und klicke links.
-Show Inventory=Zeige Inventar
-Select seat:=Wähle einen Sitzplatz aus:
-ATC controller, unconfigured.=Zugbeeinflussungsschiene, nicht konfiguiert.
-ATC controller=Zugbeeinflussungsschiene
-ATC controller, mode @1@nChannel: @2=Zugbeeinflussungsschiene in Betriebsart "@1"@nKanal: @2
-ATC controller, mode @1@nCommand: @2=Zugbeeinflussungsschiene in Betriebsart "@1"@nBefehl: @2
-Command=Befehl
-Command (on)=Befehl (wenn ein)
-Digiline channel=Digiline-Kanal
-Save=Speichern
-ATC Reverse command warning: didn't reverse train, train moving!=Zugbeeinflussung - Warnung: Befehl 'R' nicht ausgeführt, Zug in Bewegung!
-ATC command syntax error: I statement not closed: @1=Zugbeeinflussung - Syntaxfehler: I-Anweisung nicht geschlossen: @1
-ATC command parse error: Unknown command: @1=Zugbeeinflussung - Fehler: Unbekannter Befehl: @1
-This position is protected!=Diese Position ist geschützt!
-You need to own at least one neighboring wagon to destroy this couple.=Du musst Besitzer eines angrenzenden Waggons sein, um hier abzukuppeln.
-@1 Platform (low)=Niedriger @1-Bahnsteig
-@1 Platform (high)=Hoher @1-Bahnsteig
-off=aus
-on=ein
-Lampless Signal (@1)=Mechanisches Signal (@1)
-Signal (@1)=Lichtsignal (@1)
-Track Worker Tool@n@nLeft-click: change rail type (straight/curve/switch)@nRight-click: rotate rail/bumper/signal/etc.=Schienenwerkzeug@n@nLinksklick: Schienentyp ändern, Rechtsklick: Objekt drehen.
-This node can't be rotated using the trackworker!=Kann diesen Block nicht mit dem Schienenwerkzeug drehen.
-This node can't be changed using the trackworker!=Kann diesen Block nicht mit dem Schienenwerkzeug bearbeiten.
-Can't place: not pointing at node=Kann nicht platzieren: Du zeigst nicht auf einen Block.
-Can't place: space occupied!=Kann nicht platzieren: Platz besetzt.
-Can't place: protected position!=Kann nicht platzieren: Position geschützt.
-Can't place: Not enough slope items left (@1 required)=Kann nicht platzieren: nicht genug Steigungsblöcke, es werden insgesamt @1 benötigt.
-Can't place: There's no slope of length @1=Kann nicht platzieren: Keine Steigung der Länge @1 definiert.
-Can't place: no supporting node at upper end.=Kann nicht platzieren: kein unterstützender Block am Ende der Steigung.
-Deprecated Track=ausrangierte Schiene, nicht verwenden.
-Track=Schiene
-Bumper=Prellbock
-Detector Rail=Detektorschiene
-Speed:=Geschw.:
-Target:=Zielges.:
-@1 Slope=@1 Steigung
-Can't get on: wagon full or doors closed!=Kann nicht einsteigen: Waggon voll oder Türen geschlossen.
-Use Sneak+rightclick to bypass closed doors!=Nutze Sneak+Rechtsklick, um die Türnotöffnung zu aktivieren und trotzdem einzusteigen.
-Lock couples=Kupplungen sperren
-Save wagon properties=Waggon-Einstellungen speichern
-Doors are closed! Use Sneak+rightclick to ignore the closed doors and get off!=Türen sind geschlossen! Sneak+Rechtsklick, um die Türnotöffnung zu aktivieren und trotzdem auszusteigen.
-Wagon properties=Waggon-Einstellungen
-Get off=Aussteigen
-Get off (forced)=Aussteigen (erzwingen)
-(Doors closed)=(Türen geschlossen)
-Access to @1=Zugang zu @1
-Default Seat=Standardsitzplatz
-Default Seat (driver stand)=Standardsitzplatz (Führerstand)
-Driver Stand=Führerstand
-Driver Stand (left)=Führerstand Links
-Driver Stand (right)=Führerstand Rechts
-Industrial Train Engine=Industrielle Lokomotive
-Industrial tank wagon=Tankwaggon
-Industrial wood wagon=Holztransportwaggon
-Japanese Train Engine=Japanische Personenzug-Lokomotive
-Japanese Train Wagon=Japanischer Personenzug-Passagierwaggon
-Steam Engine=Dampflokomotive
-Detailed Steam Engine=detaillierte Dampflokomotive
-Passenger Wagon=Passagierwaggon
-Box wagon=Güterwaggon
-Subway Passenger Wagon=U-Bahn-Waggon
-The wagon's inventory is not empty!=Das Inventar dieses Waggons ist nicht leer!
-This track can not be changed!=Diese Schiene kann nicht geändert werden!
-This track can not be rotated!=Diese Schiene kann nicht gedreht werden!
-This track can not be removed!=Diese Schiene kann nicht entfernt werden!
-Position is occupied by a train.=Ein Zug steht an dieser Position.
-There's a Track Circuit Break here.=Hier ist eine Gleisabschnittsgrenze (TCB).
-There's a Signal Influence Point here.=Hier ist ein Signal-Beeinflussungspunkt.
-Buffer and Chain Coupler=Schraubenkupplung
-Scharfenberg Coupler=Scharfenbergkupplung
-Japanese Train Inter-Wagon Connection=Waggonzwischenverbindung Japanischer Personenzug
-Can not couple: The couplers of the trains do not match (@1 and @2).=Kann nicht ankuppeln: Die Kupplungen der Züge passen nicht zueinander (@1 und @2)
-<none>=<keine>
diff --git a/advtrains/locale/advtrains.zh_CN.tr b/advtrains/locale/advtrains.zh_CN.tr
deleted file mode 100644
index ef9c99b..0000000
--- a/advtrains/locale/advtrains.zh_CN.tr
+++ /dev/null
@@ -1,107 +0,0 @@
-# textdomain: advtrains
-
-# Advtrains Core (unorganized)
-This wagon is owned by @1, you can't destroy it.=这是@1的车厢, 你不能摧毁它.
-Warning: If you destroy this wagon, you only get some steel back! If you are sure, hold Sneak and left-click the wagon.=警告: 如果你摧毁此车厢, 你只能拿到一些钢方块. 如果你确定要摧毁这个车厢,请按潜行键并左键单击此车厢.
-ATC controller, unconfigured.=ATC控制器 (未配置)
-ATC controller=ATC控制器
-ATC controller, mode @1@nChannel: @2=ATC控制器@n模式: @1@n频道: @2
-ATC controller, mode @1@nCommand: @2=ATC控制器@n模式: @1@n命令: @2
-Command=命令
-Command (on)=命令(激活时)
-Digiline channel=Digiline 频道
-ATC Reverse command warning: didn't reverse train, train moving!=ATC警告:未执行“R”命令, 火车在移动
-ATC command syntax error: I statement not closed: @1=ATC语法错误: "I"命令不完整: @1
-ATC command parse error: Unknown command: @1=ATC语法错误: 未知命令: @1
-This position is protected!=这里已被保护.
-You need to own at least one neighboring wagon to destroy this couple.=你必须至少拥有其中一个车厢才能解耦这两个车厢.
-This node can't be rotated using the trackworker!=你不能使用铁路调整工具旋转这个方块.
-This node can't be changed using the trackworker!=你不能使用铁路调整工具调整这个方块.
-Can't place: not pointing at node=无法放置: 你没有选择任何方块.
-Can't place: space occupied!=无法放置: 此区域已被占用.
-Can't place: protected position!=无法放置: 此区域已被保护.
-Can't place: Not enough slope items left (@1 required)=无法放置: 你没有足够的铁路斜坡放置工具 (你需要@1个)
-Can't place: There's no slope of length @1=无法放置: advtrains不支持长度为@1m的斜坡.
-Can't place: no supporting node at upper end.=无法放置: 较高端没有支撑方块.
-Deprecated Track=请不要使用
-Can't get on: wagon full or doors closed!=无法上车: 车门已关闭或车厢已满
-Use Sneak+rightclick to bypass closed doors!=请使用潜行+右键上车
-Lock couples=锁定连接处
-Doors are closed! Use Sneak+rightclick to ignore the closed doors and get off!=车门已关闭, 请使用潜行+右键单击下车
-Access to @1=可前往@1
-The clipboard couldn't access the metadata. Paste failed.=无法粘贴: 剪贴板无法访问元数据
-The clipboard couldn't access the metadata. Copy failed.=无法复制: 剪贴板无法访问元数据
-
-# Train HUD/Formspecs
-Speed:=速度:
-Target:=目标速度:
-Show Inventory=显示物品栏
-Select seat:=请选择座位
-Wagon properties=车厢属性
-Save wagon properties=保存车厢属性
-Text displayed outside on train=车厢外部显示
-Text displayed inside train=车厢内部显示
-Line=火车线路
-Routingcode=路由码
-Get off=下车
-Get off (forced)=强制下车
-(Doors closed)=(车门已关闭)
-
-# General
-Save=保存
-# "off" and "on" can be translated differently depending on the context and are therefore not translated.
-off=off
-on=on
-
-# Line automation
-Station Code=车站代码
-Station Name=车站名称
-Door Delay=车门关闭时间
-Departure Speed=出发速度
-Stop Time=停站时间
-
-# Items
-Track Worker Tool@n@nLeft-click: change rail type (straight/curve/switch)@nRight-click: rotate rail/bumper/signal/etc.=铁路调整工具@n@n左键单击: 切换轨道类型@n右键单击: 旋转方块
-Passive Component Naming Tool@n@nRight-click to name a passive component.=被动元件命名工具@n@n右键单击命名所选元件.
-Train copy/paste tool@n@nLeft-click: copy train@nRight-click: paste train=火车复制工具@n@n左键单击: 复制@n右键单击: 粘帖
-Track=铁轨
-Perpendicular Diamond Crossing Track=垂直交叉铁轨
-45/90 Degree Diamond Crossing Track=45度交叉铁轨
-Unloading Track=卸货铁轨
-Loading Track=装货铁轨
-Bumper=保险杠
-Detector Rail=探测铁轨
-@1 Slope=@1斜坡
-@1 Platform (low)=50cm高的@1站台
-@1 Platform (high)=1m高的@1站台
-@1 Platform (45 degree)=1m高的@1站台 (45度)
-Lampless Signal (@1)=臂板信号机 (@1)
-Signal (@1)=信号灯 (@1)
-Wallmounted Signal (l)=壁挂式信号灯 (左侧)
-Wallmounted Signal (r)=壁挂式信号灯 (右侧)
-Wallmounted Signal (t)=悬挂式信号灯
-Andrew's Cross=铁路道口信号灯
-Boiler=锅炉
-driver's cab=驾驶室
-Wheel=车轮
-Chimney=烟囱
-
-# Seats
-Default Seat=默认座位
-Default Seat (driver stand)=默认座位 (司机座位)
-Driver Stand=司机座位
-Driver Stand (left)=左侧司机座位
-Driver Stand (right)=右侧司机座位
-
-# Wagon/engine types
-Industrial Train Engine=工业用火车头
-Big Industrial Train Engine=大型工业用火车头
-Industrial tank wagon=液体运输车厢
-Industrial wood wagon=木材运输车厢
-Japanese Train Engine=高速列车车头
-Japanese Train Wagon=高速列车车厢
-Steam Engine=蒸汽机车
-Detailed Steam Engine=精细的蒸汽机车
-Passenger Wagon=客车
-Box Wagon=货运车厢
-Subway Passenger Wagon=地铁车厢
diff --git a/advtrains/lzb.lua b/advtrains/lzb.lua
index 64e4553..116777c 100644
--- a/advtrains/lzb.lua
+++ b/advtrains/lzb.lua
@@ -29,8 +29,9 @@ The LZB subsystem keeps track of "checkpoints" the train will pass in the future
To perform 2, it populates the train.path_speed table which is handled along with the path subsystem.
This table is used in trainlogic.lua/train_step_b() and applied to the velocity calculations.
-Note: in contrast to node enter callbacks, which are called when the train passes the .5 index mark, LZB callbacks are executed on passing the .0 index mark!
-If an LZB checkpoint has speed 0, the train will still enter the node (the enter callback will be called), but will stop at the 0.9 index mark (for details, see SLOW_APPROACH in trainlogic.lua)
+Note: As of 2024-11-25, node enter callbacks as well as LZB callbacks are executed on passing the .0 index mark!
+If an LZB checkpoint has speed 0, it will stop at the 0.9 index mark (for details, see SLOW_APPROACH in trainlogic.lua) and will not enter the node.
+(previous behavior: node enter callback was called at .5 index mark)
The start point for the LZB traverser (and thus the first node that will receive an approach callback) is floor(train.index) + 1. This means, once the LZB checkpoint callback has fired,
this path node will not receive any further approach callbacks for the same approach situation
@@ -48,7 +49,7 @@ local params = {
ZONE_HOLD = 5, -- added on top of ZONE_ROLL
ZONE_VSLOW = 3, -- When speed is <2, still allow accelerating
- DST_FACTOR = 1.5,
+ DST_FACTOR = 3,--1.5,
SHUNT_SPEED_MAX = advtrains.SHUNT_SPEED_MAX,
}
@@ -264,7 +265,8 @@ end
advtrains.te_register_on_new_path(function(id, train)
advtrains.lzb_invalidate(train)
-- Taken care of in pre-move hook (see train_step_b)
- --look_ahead(id, train)
+ -- 2025-01-28 - do anyway, there seems to be an issue
+ look_ahead(id, train)
end)
advtrains.te_register_on_invalidate_ahead(function(id, train, start_idx)
diff --git a/advtrains/misc_nodes.lua b/advtrains/misc_nodes.lua
index bcf7329..8720272 100644
--- a/advtrains/misc_nodes.lua
+++ b/advtrains/misc_nodes.lua
@@ -1,5 +1,8 @@
--all nodes that do not fit in any other category
+-- Get current translator
+local S = advtrains.translate
+
function advtrains.register_platform(modprefix, preset)
local ndef=minetest.registered_nodes[preset]
if not ndef then
@@ -13,7 +16,7 @@ function advtrains.register_platform(modprefix, preset)
local desc=ndef.description or ""
local nodename=string.match(preset, ":(.+)$")
minetest.register_node(modprefix .. ":platform_low_"..nodename, {
- description = attrans("@1 Platform (low)", desc),
+ description = S("@1 Platform (low)", desc),
tiles = {btex.."^advtrains_platform.png", btex, btex, btex, btex, btex},
groups = {cracky = 1, not_blocking_trains = 1, platform=1},
sounds = ndef.sounds,
@@ -30,7 +33,7 @@ function advtrains.register_platform(modprefix, preset)
sunlight_propagates = true,
})
minetest.register_node(modprefix .. ":platform_high_"..nodename, {
- description = attrans("@1 Platform (high)", desc),
+ description = S("@1 Platform (high)", desc),
tiles = {btex.."^advtrains_platform.png", btex, btex, btex, btex, btex},
groups = {cracky = 1, not_blocking_trains = 1, platform=2},
sounds = ndef.sounds,
@@ -56,7 +59,7 @@ function advtrains.register_platform(modprefix, preset)
}
}
minetest.register_node(modprefix..":platform_45_"..nodename, {
- description = attrans("@1 Platform (45 degree)", desc),
+ description = S("@1 Platform (45 degree)", desc),
groups = {cracky = 1, not_blocking_trains = 1, platform=2},
sounds = ndef.sounds,
drawtype = "mesh",
@@ -78,7 +81,7 @@ function advtrains.register_platform(modprefix, preset)
}
}
minetest.register_node(modprefix..":platform_45_low_"..nodename, {
- description = attrans("@1 Platform (low, 45 degree)", desc),
+ description = S("@1 Platform (low, 45 degree)", desc),
groups = {cracky = 1, not_blocking_trains = 1, platform=2},
sounds = ndef.sounds,
drawtype = "mesh",
diff --git a/advtrains/nodedb.lua b/advtrains/nodedb.lua
index 41ac089..62405ce 100644
--- a/advtrains/nodedb.lua
+++ b/advtrains/nodedb.lua
@@ -212,6 +212,10 @@ function ndb.get_node(pos)
end
return n
end
+function ndb.get_ndef(pos)
+ local n=ndb.get_node_or_nil(pos)
+ return n and minetest.registered_nodes[n.name]
+end
function ndb.get_node_raw(pos)
local cid=ndbget(pos.x, pos.y, pos.z)
if cid then
@@ -264,23 +268,23 @@ end
--get_node with pseudoload. now we only need track data, so we can use the trackdb as second fallback
--nothing new will be saved inside the trackdb.
--returns:
---true, conn1, conn2, rely1, rely2, railheight in case everything's right.
+--true, conns, railheight, connmap in case everything's right.
--false if it's not a rail or the train does not drive on this rail, but it is loaded or
--nil if the node is neither loaded nor in trackdb
--the distraction between false and nil will be needed only in special cases.(train initpos)
-function advtrains.get_rail_info_at(pos, drives_on)
+function advtrains.get_rail_info_at(pos)
local rdp=advtrains.round_vector_floor_y(pos)
local node=ndb.get_node_or_nil(rdp)
if not node then return end
local nodename=node.name
- if(not advtrains.is_track_and_drives_on(nodename, drives_on)) then
+ if(not advtrains.is_track(nodename)) then
return false
end
- local conns, railheight, tracktype=advtrains.get_track_connections(node.name, node.param2)
+ local conns, railheight, connmap = advtrains.get_track_connections(node.name, node.param2)
- return true, conns, railheight
+ return true, conns, railheight, connmap
end
local IGNORE_WORLD = advtrains.IGNORE_WORLD
diff --git a/advtrains/occupation.lua b/advtrains/occupation.lua
index 7fce312..20a986e 100644
--- a/advtrains/occupation.lua
+++ b/advtrains/occupation.lua
@@ -89,8 +89,8 @@ function o.set_item(train_id, pos, idx)
assert(idx)
local i = 1
while t[i] do
- if t[i]==train_id and t[i+1]==index then
- break
+ if t[i]==train_id and t[i+1]==idx then
+ return
end
i = i + 2
end
@@ -159,10 +159,8 @@ function o.reverse_lookup(ppos)
local r = {}
local i = 1
while t[i] do
- if t[i]~=train_id then
- if not r[t[i]] then r[t[i]] = {} end
- table.insert(r[t[i]], t[i+1])
- end
+ if not r[t[i]] then r[t[i]] = {} end
+ table.insert(r[t[i]], t[i+1])
i = i + 2
end
return r
diff --git a/advtrains/p_mesecon_iface.lua b/advtrains/p_mesecon_iface.lua
index 33fcecd..0426e2b 100644
--- a/advtrains/p_mesecon_iface.lua
+++ b/advtrains/p_mesecon_iface.lua
@@ -14,13 +14,11 @@ minetest.override_item("mesecons_switch:mesecon_switch_off", {
minetest.sound_play("mesecons_switch", {pos=pos})
end,
advtrains = {
- getstate = "off",
- setstate = function(pos, node, newstate)
- if newstate=="on" then
- advtrains.ndb.swap_node(pos, {name="mesecons_switch:mesecon_switch_on", param2=node.param2})
- if advtrains.is_node_loaded(pos) then
- mesecon.receptor_on(pos)
- end
+ node_state = "off",
+ node_state_map = { on = "mesecons_switch:mesecon_switch_on", off = "mesecons_switch:mesecon_switch_off" },
+ node_on_switch_state = function(pos, new_node, old_state, new_state)
+ if advtrains.is_node_loaded(pos) then
+ mesecon.receptor_on(pos)
end
end,
on_updated_from_nodedb = function(pos, node)
@@ -41,16 +39,14 @@ minetest.override_item("mesecons_switch:mesecon_switch_on", {
minetest.sound_play("mesecons_switch", {pos=pos})
end,
advtrains = {
- getstate = "on",
- setstate = function(pos, node, newstate)
- if newstate=="off" then
- advtrains.ndb.swap_node(pos, {name="mesecons_switch:mesecon_switch_off", param2=node.param2})
- if advtrains.is_node_loaded(pos) then
- mesecon.receptor_off(pos)
- end
+ node_state = "on",
+ node_state_map = { on = "mesecons_switch:mesecon_switch_on", off = "mesecons_switch:mesecon_switch_off" },
+ node_on_switch_state = function(pos, new_node, old_state, new_state)
+ if advtrains.is_node_loaded(pos) then
+ mesecon.receptor_off(pos)
end
end,
- fallback_state = "off",
+ node_fallback_state = "off",
on_updated_from_nodedb = function(pos, node)
mesecon.receptor_on(pos)
end,
diff --git a/advtrains/passive.lua b/advtrains/passive.lua
index fe4790c..37b79e4 100644
--- a/advtrains/passive.lua
+++ b/advtrains/passive.lua
@@ -1,9 +1,5 @@
-- passive.lua
--- API to passive components, as described in passive_api.txt of advtrains_luaautomation
--- This has been moved to the advtrains core in turn with the interlocking system,
--- to prevent a dependency on luaautomation.
-
-local deprecation_warned = {}
+-- Rework for advtrains 2.5: The passive API now uses the reworked node_state system. Please see the comment in tracks.lua
function advtrains.getstate(parpos, pnode)
local pos
@@ -19,20 +15,8 @@ function advtrains.getstate(parpos, pnode)
local node=pnode or advtrains.ndb.get_node(pos)
local ndef=minetest.registered_nodes[node.name]
local st
- if ndef and ndef.advtrains and ndef.advtrains.getstate then
- st=ndef.advtrains.getstate
- elseif ndef and ndef.luaautomation and ndef.luaautomation.getstate then
- if not deprecation_warned[node.name] then
- minetest.log("warning", node.name.." uses deprecated definition of ATLATC functions in the 'luaautomation' field. Please move them to the 'advtrains' field!")
- end
- st=ndef.luaautomation.getstate
- else
- return nil
- end
- if type(st)=="function" then
- return st(pos, node)
- else
- return st
+ if ndef and ndef.advtrains then
+ return ndef.advtrains.node_state
end
end
@@ -45,31 +29,48 @@ function advtrains.setstate(parpos, newstate, pnode)
end
if type(pos)~="table" or (not pos.x or not pos.y or not pos.z) then
debug.sethook()
- error("Invalid position supplied to getstate")
+ error("Invalid position supplied to setstate")
end
local node=pnode or advtrains.ndb.get_node(pos)
local ndef=minetest.registered_nodes[node.name]
- local st
- if ndef and ndef.advtrains and ndef.advtrains.setstate then
- st=ndef.advtrains.setstate
- elseif ndef and ndef.luaautomation and ndef.luaautomation.setstate then
- if not deprecation_warned[node.name] then
- minetest.log("warning", node.name.." uses deprecated definition of ATLATC functions in the 'luaautomation' field. Please move them to the 'advtrains' field!")
- end
- st=ndef.luaautomation.setstate
- else
- return nil
+
+ if not ndef or not ndef.advtrains then
+ return false, "missing_node_def"
+ end
+ local old_state = ndef.advtrains.node_state
+
+ if old_state == newstate then
+ -- nothing needs to be done
+ return true
end
+ if not ndef.advtrains.node_state_map then
+ return false, "missing_node_state_map"
+ end
+ local new_node_name = ndef.advtrains.node_state_map[newstate]
+ if not new_node_name then
+ return false, "no_such_state"
+ end
+
+ -- prevent state switching when route lock or train is present
if advtrains.get_train_at_pos(pos) then
- return false
+ return false, "train_here"
end
- if advtrains.interlocking and advtrains.interlocking.route.has_route_lock(minetest.pos_to_string(pos)) then
- return false
+ if advtrains.interlocking and advtrains.interlocking.route.has_route_lock(advtrains.encode_pos(pos)) then
+ return false, "route_lock_here"
end
- st(pos, node, newstate)
+ -- perform the switch
+ local new_node = {name = new_node_name, param2 = node.param2}
+ advtrains.ndb.swap_node(pos, new_node)
+ -- if callback is present, call it
+ if ndef.advtrains.node_on_switch_state then
+ ndef.advtrains.node_on_switch_state(pos, new_node, old_state, newstate)
+ end
+ -- invalidate paths (only relevant if this is a track)
+ advtrains.invalidate_all_paths(pos)
+
return true
end
@@ -86,12 +87,7 @@ function advtrains.is_passive(parpos, pnode)
end
local node=pnode or advtrains.ndb.get_node(pos)
local ndef=minetest.registered_nodes[node.name]
- if ndef and ndef.advtrains and ndef.advtrains.getstate then
- return true
- elseif ndef and ndef.luaautomation and ndef.luaautomation.getstate then
- if not deprecation_warned[node.name] then
- minetest.log("warning", node.name.." uses deprecated definition of ATLATC functions in the 'luaautomation' field. Please move them to the 'advtrains' field!")
- end
+ if ndef and ndef.advtrains and ndef.advtrains.node_state_map then
return true
else
return false
@@ -102,20 +98,10 @@ end
function advtrains.set_fallback_state(pos, pnode)
local node=pnode or advtrains.ndb.get_node(pos)
local ndef=minetest.registered_nodes[node.name]
- local st
- if ndef and ndef.advtrains and ndef.advtrains.setstate
- and ndef.advtrains.fallback_state then
- if advtrains.get_train_at_pos(pos) then
- return false
- end
-
- if advtrains.interlocking and advtrains.interlocking.route.has_route_lock(minetest.pos_to_string(pos)) then
- return false
- end
-
- ndef.advtrains.setstate(pos, node, ndef.advtrains.fallback_state)
- return true
- end
+ if not ndef or not ndef.advtrains or not ndef.advtrains.node_fallback_state then
+ return false, "no_fallback_state"
+ end
+ return advtrains.setstate(pos, ndef.advtrains.node_fallback_state, node)
end
diff --git a/advtrains/path.lua b/advtrains/path.lua
index 7676947..57829ad 100644
--- a/advtrains/path.lua
+++ b/advtrains/path.lua
@@ -33,17 +33,16 @@
-- If you need to proceed along the path by a specific actual distance, it does NOT work to simply add it to the index. You should use the path_get_index_by_offset() function.
-- creates the path data structure, reconstructing the train from a position and a connid
--- Important! train.drives_on must exist while calling this method
-- returns: true - successful
-- nil - node not yet available/unloaded, please wait
-- false - node definitely gone, remove train
function advtrains.path_create(train, pos, connid, rel_index)
local posr = advtrains.round_vector_floor_y(pos)
- local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, train.drives_on)
+ local node_ok, conns, rhe, connmap = advtrains.get_rail_info_at(pos)
if not node_ok then
return node_ok
end
- local mconnid = advtrains.get_matching_conn(connid, #conns)
+ local mconnid = advtrains.get_matching_conn(connid, connmap)
train.index = rel_index
train.path = { [0] = { x=posr.x, y=posr.y+rhe, z=posr.z } }
train.path_cn = { [0] = connid }
@@ -113,14 +112,21 @@ end
-- before returning from the calling function.
function advtrains.path_invalidate(train, ignore_lock)
if advtrains.lock_path_inval and not ignore_lock then
- atwarn("Train ",train.train_id,": Illegal path invalidation has occured during train step:")
+ atwarn("Train ",train.id,": Illegal path invalidation has occured during train step:")
atwarn(debug.traceback())
end
-
if train.path then
+ --atdebug("path_invalidate for",train.id)
+ local _cnt = 0
for i,p in pairs(train.path) do
+ _cnt = _cnt + 1
+ if _cnt > 10000 then
+ atdebug(train)
+ error("Loop trap in advtrains.path_invalidate was triggered!")
+ end
advtrains.occ.clear_all_items(train.id, advtrains.round_vector_floor_y(p))
end
+ --atdebug("occ cleared")
end
train.path = nil
train.path_dist = nil
@@ -207,23 +213,19 @@ function advtrains.path_get(train, index)
while index > pef do
local pos = train.path[pef]
local connid = train.path_cn[pef]
- local node_ok, this_conns, adj_pos, adj_connid, conn_idx, nextrail_y, next_conns
+ local node_ok, this_conns, adj_pos, adj_connid, conn_idx, nextrail_y, next_conns, next_connmap
if pef == train.path_trk_f then
node_ok, this_conns = advtrains.get_rail_info_at(pos)
if not node_ok then error("For train "..train.id..": Path item "..pef.." on-track but not a valid node!") end
- adj_pos, adj_connid, conn_idx, nextrail_y, next_conns = advtrains.get_adjacent_rail(pos, this_conns, connid, train.drives_on)
+ adj_pos, adj_connid, conn_idx, nextrail_y, next_conns, next_connmap = advtrains.get_adjacent_rail(pos, this_conns, connid)
end
pef = pef + 1
if adj_pos then
advtrains.occ.set_item(train.id, adj_pos, pef)
-
- -- If we have split points, notify accordingly
- local mconnid = advtrains.get_matching_conn(adj_connid, #next_conns)
- if #next_conns==3 and adj_connid==1 and train.points_split and train.points_split[advtrains.encode_pos(adj_pos)] then
- --atdebug(id,"has split points restored at",adj_pos)
- mconnid = 3
- end
-
+
+ local mconnid = advtrains.get_matching_conn(adj_connid, next_connmap)
+ -- NO split points handling here. It is only required for backwards path calculation
+
adj_pos.y = adj_pos.y + nextrail_y
train.path_cp[pef] = adj_connid
train.path_cn[pef] = mconnid
@@ -246,23 +248,26 @@ function advtrains.path_get(train, index)
while index < peb do
local pos = train.path[peb]
local connid = train.path_cp[peb]
- local node_ok, this_conns, adj_pos, adj_connid, conn_idx, nextrail_y, next_conns
+ local node_ok, this_conns, adj_pos, adj_connid, conn_idx, nextrail_y, next_conns, next_connmap
if peb == train.path_trk_b then
node_ok, this_conns = advtrains.get_rail_info_at(pos)
if not node_ok then error("For train "..train.id..": Path item "..peb.." on-track but not a valid node!") end
- adj_pos, adj_connid, conn_idx, nextrail_y, next_conns = advtrains.get_adjacent_rail(pos, this_conns, connid, train.drives_on)
+ adj_pos, adj_connid, conn_idx, nextrail_y, next_conns, next_connmap = advtrains.get_adjacent_rail(pos, this_conns, connid)
end
peb = peb - 1
if adj_pos then
advtrains.occ.set_item(train.id, adj_pos, peb)
- -- If we have split points, notify accordingly
- local mconnid = advtrains.get_matching_conn(adj_connid, #next_conns)
- if #next_conns==3 and adj_connid==1 and train.points_split and train.points_split[advtrains.encode_pos(adj_pos)] then
- -- atdebug(id,"has split points restored at",adj_pos)
- mconnid = 3
+ local mconnid = advtrains.get_matching_conn(adj_connid, next_connmap)
+ -- If, for this position, we have remembered the origin conn, apply it here
+ if next_connmap then -- only needs to be done when this track is a turnout (>2 conns)
+ local origin_conn = train.path_ori_cp[advtrains.encode_pos(adj_pos)]
+ if origin_conn then
+ --atdebug("Train",train.id,"at",adj_pos,"restoring turnout origin CP",origin_conn,"for path item",index)
+ mconnid = origin_conn
+ end
end
-
+
adj_pos.y = adj_pos.y + nextrail_y
train.path_cn[peb] = adj_connid
train.path_cp[peb] = mconnid
diff --git a/advtrains/po/README.md b/advtrains/po/README.md
new file mode 100644
index 0000000..3e94682
--- /dev/null
+++ b/advtrains/po/README.md
@@ -0,0 +1,70 @@
+# Translations
+Please read this document before working on any translations.
+
+Unlike many other mods, Advtrains uses `.po` files for localization,
+which are then automatically converted to `.tr` files when the mod is
+loaded. Therefore, please submit patches that edit the `.po` files.
+
+## Getting Started
+The translation files can be edited like any other `.po` file.
+
+If the translation file for your language does not exist, create it by
+copying `template.txt` to `advtrains.XX.tr`, where `XX` is replaced by
+the language code.
+
+Feel free to use the [discussion mailing list][srht-discuss] if you
+have any questions regarding localization.
+
+You can share your `.po` file directly or [as a patch][gsm] to the [dev
+mailing list][srht-devel]. The latter is encouraged, but, unlike code
+changes, translation files sent directly are also accepted.
+
+[tr-format]: https://minetest.gitlab.io/minetest/translations/#translation-file-format
+[srht-discuss]: https://lists.sr.ht/~gpcf/advtrains-discuss
+[srht-devel]: https://lists.sr.ht/~gpcf/advtrains-devel
+[gsm]: https://git-send-email.io
+
+## Translation Notes
+* Translations should be consistent. You can use other entries or the
+translations in Minetest as a reference.
+* Translations do not have to fully correspond to the original text -
+they only need to provide the same information. In particular,
+translations do not need to have the same linguistical structure as the
+original text.
+* Replacement sequences (`@1`, `@2`, etc) should not be translated.
+* Certain abbreviations or names, such as "Ks" or "Zs 3", should
+generally not be translated.
+
+### (de) German
+* Verwenden Sie die neue Rechtschreibung und die Sie-Form.
+* Mit der deutschen Tastaturbelegung unter Linux können die
+Anführungszeichen „“ mit AltGr-V bzw. AltGr-B eingegeben werden.
+
+### (zh) Chinese
+(This section is written in English to avoid writing the note twice or
+using only one of the variants, as most of this section applies to both
+the traditional and simplified variants.)
+
+* Please use the 「」 quotation marks for Traditional Chinese and “”
+for Simplified Chinese.
+* Please use the fullwidth variants of: , 、 。 ? ! : ;
+* Please use the halfwidth variants of: ( ) [ ] / \ |
+* Please do not leave any space between Han characters (including
+fullwidth punctuation marks).
+* Please leave a space between Han characters (excluding fullwidth
+punctuation marks) and characters from other scripts (including
+halfwidth punctuation marks). However, do not leave any space between
+Han characters and Arabic numerals.
+
+## Notes for developers
+* The `update-translations.sh` script can be used to update the
+translation files. However, please make sure to install the
+`basic_trains` mod before running the script.
+* Please make sure that the first argument to `S` (or `attrans`) _only_
+includes string literals without formatting or concatenation. This is
+unfortunately a limitation of the `xgettext` utility.
+* Avoid word-by-word translations.
+* Avoid manipulating translated strings (except for concatenation). Use
+server-side translations if you have to modify the text sent to users.
+* Avoid truncating strings unless multibyte characters are handled
+properly.
diff --git a/advtrains/po/advtrains.pot b/advtrains/po/advtrains.pot
new file mode 100644
index 0000000..db19fc9
--- /dev/null
+++ b/advtrains/po/advtrains.pot
@@ -0,0 +1,625 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the advtrains package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: advtrains\n"
+"Report-Msgid-Bugs-To: advtrains-discuss@lists.sr.ht\n"
+"POT-Creation-Date: 2025-06-10 21:40+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: atc.lua
+msgid "Unconfigured ATC controller"
+msgstr ""
+
+#: atc.lua
+msgid "ATC controller, Command: @1"
+msgstr ""
+
+#: atc.lua
+msgid "Command"
+msgstr ""
+
+#: atc.lua
+msgid "Digiline channel"
+msgstr ""
+
+#: atc.lua wagons.lua
+msgid "Save"
+msgstr ""
+
+#: atc.lua
+msgid "ATC Reverse command warning: didn't reverse train, train moving."
+msgstr ""
+
+#: atc.lua
+msgid "ATC Kick command warning: doors are closed."
+msgstr ""
+
+#: atc.lua
+msgid "ATC Kick command warning: train moving."
+msgstr ""
+
+#: atc.lua
+msgid "ATC command syntax error: I statement not closed: @1"
+msgstr ""
+
+#: atc.lua
+msgid "ATC command parse error: Unknown command: @1"
+msgstr ""
+
+#: copytool.lua
+msgid ""
+"Train copy/paste tool\n"
+"\n"
+"Left-click: copy train\n"
+"Right-click: paste train"
+msgstr ""
+
+#: copytool.lua
+msgid "You do not have the @1 privilege."
+msgstr ""
+
+#: copytool.lua
+msgid "The track you are trying to place the wagon on is not long enough."
+msgstr ""
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Paste failed."
+msgstr ""
+
+#: copytool.lua
+msgid "The clipboard is empty."
+msgstr ""
+
+#: copytool.lua
+msgid "Back of train would end up off track, cancelling."
+msgstr ""
+
+#: copytool.lua
+msgid "No such lua entity."
+msgstr ""
+
+#: copytool.lua
+msgid "No such wagon: @1."
+msgstr ""
+
+#: copytool.lua
+msgid "No such train: @1."
+msgstr ""
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Copy failed."
+msgstr ""
+
+#: copytool.lua
+msgid "Train copied."
+msgstr ""
+
+#: couple.lua
+msgid "Buffer and Chain Coupler"
+msgstr ""
+
+#: couple.lua
+msgid "Scharfenberg Coupler"
+msgstr ""
+
+#: couple.lua
+msgid ""
+"You are not allowed to couple trains without the train_operator privilege."
+msgstr ""
+
+#: couple.lua
+msgid ""
+"Cannot couple @1 and @2 - train would have length @3 which is above the "
+"limit of @4"
+msgstr ""
+
+#: couple.lua
+msgid "<No coupler>"
+msgstr ""
+
+#: couple.lua
+msgid "Can not couple: The couplers of the trains do not match (@1 and @2)."
+msgstr ""
+
+#: craft_items.lua
+msgid "Boiler"
+msgstr ""
+
+#: craft_items.lua
+msgid "Driver's cab"
+msgstr ""
+
+#: craft_items.lua
+msgid "Wheel"
+msgstr ""
+
+#: craft_items.lua
+msgid "Chimney"
+msgstr ""
+
+#: init.lua
+msgid "Restoring saved state in 1 second..."
+msgstr ""
+
+#: init.lua
+msgid "Reload successful!"
+msgstr ""
+
+#: init.lua
+msgid "Removing unused wagon"
+msgstr ""
+
+#: init.lua
+msgid "from wagon_save table."
+msgstr ""
+
+#: init.lua
+msgid ""
+"Train @1 had no wagons left because of some bug. It is being deleted. Wave "
+"it goodbye!"
+msgstr ""
+
+#: init.lua
+msgid "Saving failed: @1"
+msgstr ""
+
+#: init.lua
+msgid "Instructed to save() but load() was never called!"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Crash during advtrains main step - skipping the shutdown save operation to "
+"not save inconsistent data!"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Detach all players, especially the offline ones, from all trains. Use only "
+"when no one serious is on a train."
+msgstr ""
+
+#: init.lua
+msgid ""
+"Data is being saved. While saving, advtrains will remove the players from "
+"trains. Save files will be reloaded afterwards!"
+msgstr ""
+
+#: init.lua
+msgid "Delete all train routes, force them to recalculate"
+msgstr ""
+
+#: init.lua
+msgid "Successfully invalidated train routes"
+msgstr ""
+
+#: init.lua
+msgid "Returns the position of the train with the given id"
+msgstr ""
+
+#: init.lua
+msgid "Train @1 does not exist or is invalid"
+msgstr ""
+
+#: init.lua
+msgid "Train @1 is at @2"
+msgstr ""
+
+#: init.lua
+msgid "Teleports you to the position of the train with the given id"
+msgstr ""
+
+#: init.lua
+msgid "Teleporting to train @1"
+msgstr ""
+
+#: init.lua
+msgid "Disable the advtrains globalstep temporarily"
+msgstr ""
+
+#: init.lua
+msgid ""
+"The advtrains globalstep has been disabled. Trains are not moving, and no "
+"data is saved! Run '/at_disable_step no' to enable again!"
+msgstr ""
+
+#: init.lua
+msgid "Disabled advtrains successfully"
+msgstr ""
+
+#: init.lua
+msgid "Re-enabling advtrains globalstep..."
+msgstr ""
+
+#: init.lua
+msgid "Advtrains is already running normally!"
+msgstr ""
+
+#: init.lua
+msgid "Print advtrains status info"
+msgstr ""
+
+#: init.lua
+msgid "Advtrains Status: no_action @1 slowdown @2 (log @3)"
+msgstr ""
+
+#: misc_nodes.lua
+msgid "@1 Platform (low)"
+msgstr ""
+
+#: misc_nodes.lua
+msgid "@1 Platform (high)"
+msgstr ""
+
+#: misc_nodes.lua
+msgid "@1 Platform (45 degree)"
+msgstr ""
+
+#: misc_nodes.lua
+msgid "@1 Platform (low, 45 degree)"
+msgstr ""
+
+#: protection.lua
+msgid "Can place, remove and operate trains"
+msgstr ""
+
+#: protection.lua
+msgid ""
+"Can place, remove and operate any train, regardless of owner, whitelist, or "
+"protection"
+msgstr ""
+
+#: protection.lua
+msgid "Can place and dig tracks in unprotected areas"
+msgstr ""
+
+#: protection.lua
+msgid "Can operate turnouts and signals in unprotected areas"
+msgstr ""
+
+#: protection.lua
+msgid ""
+"You are not allowed to build near tracks without the track_builder privilege."
+msgstr ""
+
+#: protection.lua
+msgid ""
+"You are not allowed to build tracks without the track_builder privilege."
+msgstr ""
+
+#: protection.lua
+msgid "You are not allowed to build near tracks at this protected position."
+msgstr ""
+
+#: protection.lua
+msgid "You are not allowed to build tracks at this protected position."
+msgstr ""
+
+#: protection.lua
+msgid ""
+"You are not allowed to operate turnouts and signals without the "
+"railway_operator privilege."
+msgstr ""
+
+#: signals.lua
+msgid "Lampless Signal (deprecated!)"
+msgstr ""
+
+#: signals.lua
+msgid "Signal (deprecated!)"
+msgstr ""
+
+#: signals.lua
+msgid "Wallmounted Signal (left) (deprecated!)"
+msgstr ""
+
+#: signals.lua
+msgid "Wallmounted Signal (right) (deprecated!)"
+msgstr ""
+
+#: signals.lua
+msgid "Wallmounted Signal (top) (deprecated!)"
+msgstr ""
+
+#: signals.lua
+msgid "Andrew's Cross"
+msgstr ""
+
+#: track_reg_helper.lua tracks.lua
+msgid "This track can not be removed!"
+msgstr ""
+
+#: track_reg_helper.lua
+msgid "@1 Slope"
+msgstr ""
+
+#: track_reg_helper.lua
+msgid "Can't place: not pointing at node"
+msgstr ""
+
+#: track_reg_helper.lua
+msgid "Can't place: space occupied!"
+msgstr ""
+
+#: track_reg_helper.lua
+msgid "Can't place: Not enough slope items left (@1 required)"
+msgstr ""
+
+#: track_reg_helper.lua
+msgid "Can't place: There's no slope of length @1"
+msgstr ""
+
+#: track_reg_helper.lua
+msgid "Can't place: no supporting node at upper end."
+msgstr ""
+
+#: trackplacer.lua
+msgid ""
+"Track Worker Tool\n"
+"\n"
+"Left-click: change rail type (straight/curve/switch)\n"
+"Right-click: rotate object"
+msgstr ""
+
+#: trackplacer.lua
+msgid "This node can't be rotated using the trackworker!"
+msgstr ""
+
+#: trackplacer.lua
+msgid "This track can not be rotated!"
+msgstr ""
+
+#: trackplacer.lua
+msgid "This node can't be changed using the trackworker!"
+msgstr ""
+
+#: tracks.lua
+msgid "Position is occupied by a train."
+msgstr ""
+
+#: tracks.lua
+msgid "There's a Track Circuit Break here."
+msgstr ""
+
+#: tracks.lua
+msgid "There's a Signal Influence Point here."
+msgstr ""
+
+#: trainhud.lua
+msgid "OVERRUN RED SIGNAL! Examine situation and reverse train to move again."
+msgstr ""
+
+#: wagonprop_tool.lua
+msgid ""
+"Wagon Properties Tool\n"
+"Punch a wagon to view and edit the Wagon Properties"
+msgstr ""
+
+#: wagonprop_tool.lua
+msgid "Wagon Properties Tool"
+msgstr ""
+
+#: wagonprop_tool.lua
+msgid "Insufficient privileges to use this!"
+msgstr ""
+
+#: wagons.lua
+msgid "Uninitialized, removing"
+msgstr ""
+
+#: wagons.lua
+msgid "This wagon is owned by @1, you can't destroy it."
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Destroying wagon with inventory, but inventory is not found? Shouldn't "
+"happen!"
+msgstr ""
+
+#: wagons.lua
+msgid "The wagon's inventory is not empty."
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon needs to be decoupled from other wagons in order to destroy it."
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Warning: If you destroy this wagon, you only get some steel back! If you are "
+"sure, hold Sneak and left-click the wagon."
+msgstr ""
+
+#: wagons.lua
+msgid "!!! Train off track !!!"
+msgstr ""
+
+#: wagons.lua
+msgid " units"
+msgstr ""
+
+#: wagons.lua
+msgid "Liquid: "
+msgstr ""
+
+#: wagons.lua
+msgid "Liquid: empty"
+msgstr ""
+
+#: wagons.lua
+msgid "Show Inventory"
+msgstr ""
+
+#: wagons.lua
+msgid "Onboard Computer"
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon properties"
+msgstr ""
+
+#: wagons.lua
+msgid "Get off"
+msgstr ""
+
+#: wagons.lua
+msgid "Get off (forced)"
+msgstr ""
+
+#: wagons.lua
+msgid "(Doors closed)"
+msgstr ""
+
+#: wagons.lua
+msgid "This wagon has no seats."
+msgstr ""
+
+#: wagons.lua
+msgid "This wagon is full."
+msgstr ""
+
+#: wagons.lua
+msgid "Doors are closed! (Try holding sneak key!)"
+msgstr ""
+
+#: wagons.lua
+msgid "You can't get on this wagon."
+msgstr ""
+
+#: wagons.lua
+msgid "Select seat:"
+msgstr ""
+
+#: wagons.lua
+msgid "This Wagon ID"
+msgstr ""
+
+#: wagons.lua
+msgid "Allow these players to access your wagon:"
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon road number:"
+msgstr ""
+
+#: wagons.lua
+msgid "Freight Code:"
+msgstr ""
+
+#: wagons.lua
+msgid "Prev FC"
+msgstr ""
+
+#: wagons.lua
+msgid "Current FC: "
+msgstr ""
+
+#: wagons.lua
+msgid "Next FC:"
+msgstr ""
+
+#: wagons.lua
+msgid "Save wagon properties"
+msgstr ""
+
+#: wagons.lua
+msgid "Train ID"
+msgstr ""
+
+#: wagons.lua
+msgid "Text displayed outside on train"
+msgstr ""
+
+#: wagons.lua
+msgid "Text displayed inside train"
+msgstr ""
+
+#: wagons.lua
+msgid "Line"
+msgstr ""
+
+#: wagons.lua
+msgid "Routingcode"
+msgstr ""
+
+#: wagons.lua
+msgid "Train overview /coupling control:"
+msgstr ""
+
+#: wagons.lua
+msgid "Train overview / coupling control is only shown when the train stands."
+msgstr ""
+
+#: wagons.lua
+msgid "Remote Routesetting"
+msgstr ""
+
+#: wagons.lua
+msgid "Clear 'Disable ARS' flag"
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Doors are closed. Use Sneak+rightclick to ignore the closed doors and get "
+"off."
+msgstr ""
+
+#: wagons.lua
+msgid "You are not allowed to access the driver stand."
+msgstr ""
+
+#: wagons.lua
+msgid "Missing train_operator privilege"
+msgstr ""
+
+#: wagons.lua
+msgid "Not allowed to do this."
+msgstr ""
+
+#: wagons.lua
+msgid "You don't have the train_operator privilege."
+msgstr ""
+
+#: wagons.lua
+msgid "The track you are trying to place the wagon on is not long enough!"
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon placeholder"
+msgstr ""
+
+#: wagons.lua
+msgid "Please specify a player name to transfer ownership to."
+msgstr ""
+
+#: wagons.lua
+msgid "That player does not exist!"
+msgstr ""
+
+#: wagons.lua
+msgid "Not a valid wagon id."
+msgstr ""
+
+#: wagons.lua
+msgid "That wagon does not exist!"
+msgstr ""
+
+#: wagons.lua
+msgid "You have been given ownership of wagon @1"
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon @1 ownership changed from @2 to @3"
+msgstr ""
diff --git a/advtrains/po/de.po b/advtrains/po/de.po
new file mode 100644
index 0000000..f83b760
--- /dev/null
+++ b/advtrains/po/de.po
@@ -0,0 +1,925 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: advtrains\n"
+"Report-Msgid-Bugs-To: advtrains-discuss@lists.sr.ht\n"
+"POT-Creation-Date: 2025-06-10 21:40+0200\n"
+"PO-Revision-Date: 2025-06-11 22:55+0200\n"
+"Last-Translator: Y. Wang <yw05@forksworld.de>\n"
+"Language-Team: German\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.2.2\n"
+
+#: atc.lua
+msgid "ATC Kick command warning: doors are closed."
+msgstr ""
+"Zugbeeinflussung: Wegen geschlossener Türen werden Fahrgäste nicht zum "
+"Ausstieg gezwungen."
+
+#: atc.lua
+msgid "ATC Kick command warning: train moving."
+msgstr ""
+"Zugbeeinflussung: Der Zug befindet sich in Bewegung, Fahrgäste werden nicht "
+"zum Ausstieg gezwungen."
+
+#: atc.lua
+msgid "ATC Reverse command warning: didn't reverse train, train moving."
+msgstr ""
+"Zugbeeinflussung: Der Zug befindet sich in Bewegung und kann nicht umgekehrt "
+"werden."
+
+#: atc.lua
+msgid "ATC command parse error: Unknown command: @1"
+msgstr "Zugbeeinflussung: Unbekannter Befehl: @1"
+
+#: atc.lua
+msgid "ATC command syntax error: I statement not closed: @1"
+msgstr "Zugbeeinflussung: Unvollständiger I-Befehl: @1"
+
+#: atc.lua
+msgid "ATC controller, Command: @1"
+msgstr "Zugbeeinflussungsgleis, Befehl: @1"
+
+#: atc.lua
+msgid "Command"
+msgstr "Befehl"
+
+#: atc.lua
+msgid "Digiline channel"
+msgstr "Digiline-Kanal"
+
+#: atc.lua wagons.lua
+msgid "Save"
+msgstr "Speichern"
+
+#: atc.lua
+msgid "Unconfigured ATC controller"
+msgstr "Nicht konfiguiertes Zugbeeinflussungsgleis"
+
+#: copytool.lua
+msgid "Back of train would end up off track, cancelling."
+msgstr "Der hinterer Teil dez Zuges wäre nicht auf dem Gleis."
+
+#: copytool.lua
+msgid "No such lua entity."
+msgstr ""
+"Sie zeigen nicht auf einem Objekt, das mit diesem Werkzeug kopiert werden "
+"kann."
+
+#: copytool.lua
+msgid "No such train: @1."
+msgstr "Es gibt keinen mit „@1“ identifizierbaren Zug."
+
+#: copytool.lua
+msgid "No such wagon: @1."
+msgstr "Es gibt keinen mit „@1“ identifizierbaren Waggon."
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Copy failed."
+msgstr ""
+"Wegen des fehlgeschlagenen Zugriffs auf die Metadaten konnte der Zug nicht "
+"kopiert werden."
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Paste failed."
+msgstr ""
+"Wegen des fehlgeschlagenen Zugriffs auf die Metadaten konnte eine Kopie des "
+"Zuges nicht eingefügt werden."
+
+#: copytool.lua
+msgid "The clipboard is empty."
+msgstr "Das Clipboard ist leer."
+
+#: copytool.lua
+msgid "The track you are trying to place the wagon on is not long enough."
+msgstr "Das Gleis, auf dem der Waggon platziert werden woll, ist zu kurz."
+
+#: copytool.lua
+msgid "Train copied."
+msgstr "Der Zug wurde kopiert."
+
+#: copytool.lua
+msgid ""
+"Train copy/paste tool\n"
+"\n"
+"Left-click: copy train\n"
+"Right-click: paste train"
+msgstr ""
+"Werkzeug zur Erstellung von Zugkopien\n"
+"\n"
+"Linksklick: Zug ins Clipboard kopieren\n"
+"Right-click: Kopierten Zug einfügen"
+
+#: copytool.lua
+msgid "You do not have the @1 privilege."
+msgstr "Ihnen fehlt das „@1“-Privileg."
+
+#: couple.lua
+msgid "<No coupler>"
+msgstr "<Keine Kupplung vorhanden>"
+
+#: couple.lua
+msgid "Buffer and Chain Coupler"
+msgstr "Schraubenkupplung"
+
+#: couple.lua
+msgid "Can not couple: The couplers of the trains do not match (@1 and @2)."
+msgstr "Die Kupplungen der Züge passen nicht zueinander (@1 und @2)."
+
+#: couple.lua
+msgid ""
+"Cannot couple @1 and @2 - train would have length @3 which is above the "
+"limit of @4"
+msgstr ""
+"Züge @1 und @2 können nicht gekuppelt werden - die resultierende Zuglänge @3 "
+"wäre länger als das Maximum von @4"
+
+#: couple.lua
+msgid "Scharfenberg Coupler"
+msgstr "Scharfenbergkupplung"
+
+#: couple.lua
+msgid ""
+"You are not allowed to couple trains without the train_operator privilege."
+msgstr "Sie dürfen ohne das „train_operator“-Privileg keine Züge ankuppeln."
+
+#: craft_items.lua
+msgid "Boiler"
+msgstr "Kessel"
+
+#: craft_items.lua
+msgid "Chimney"
+msgstr "Schornstein"
+
+#: craft_items.lua
+msgid "Driver's cab"
+msgstr "Führerstand"
+
+#: craft_items.lua
+msgid "Wheel"
+msgstr "Rad"
+
+#: init.lua
+msgid "Advtrains Status: no_action @1 slowdown @2 (log @3)"
+msgstr ""
+"Advtrains Status: keine Aktion: @1 Verlangsamungsfaktor: @2 (logarithmisch "
+"@3)"
+
+#: init.lua
+msgid "Advtrains is already running normally!"
+msgstr "Advtrains läuft bereits normal!"
+
+#: init.lua
+msgid ""
+"Crash during advtrains main step - skipping the shutdown save operation to "
+"not save inconsistent data!"
+msgstr ""
+"Advtrains ist während der Ausführung des Main Step abgestürzt - Daten werden "
+"nicht gespeichert um Inkonsistenzen zu verhindern!"
+
+#: init.lua
+msgid ""
+"Data is being saved. While saving, advtrains will remove the players from "
+"trains. Save files will be reloaded afterwards!"
+msgstr ""
+"Daten werden gespeichert. Beim Speichern werden alle Spieler aus Waggons "
+"entfernt. Anschließend werden die Speicherdateien neu geladen!"
+
+#: init.lua
+msgid "Delete all train routes, force them to recalculate"
+msgstr "Erzwingt die Neuberechnung der Bewegungspfade aller Züge"
+
+#: init.lua
+msgid ""
+"Detach all players, especially the offline ones, from all trains. Use only "
+"when no one serious is on a train."
+msgstr ""
+"Bewirkt, dass alle Spieler (insbesondere Offline-Spieler) aus Waggons "
+"entfernt werden. Nur benutzen, wenn niemand ernsthaft in einem Zug sitzt."
+
+#: init.lua
+msgid "Disable the advtrains globalstep temporarily"
+msgstr "Advtrains-Schrittverarbeitung temporär deaktivieren"
+
+#: init.lua
+msgid "Disabled advtrains successfully"
+msgstr "Advtrains erfolgreich deaktiviert"
+
+#: init.lua
+msgid "Instructed to save() but load() was never called!"
+msgstr "Speichern angefordert, aber Speicherdateien wurden nie geladen!"
+
+#: init.lua
+msgid "Print advtrains status info"
+msgstr "Advtrains-Statusinformationen ausgeben"
+
+#: init.lua
+msgid "Re-enabling advtrains globalstep..."
+msgstr "Advtrains-Schrittverarbeitung wird wieder aktiviert..."
+
+#: init.lua
+msgid "Reload successful!"
+msgstr "Neu Laden Erfolgreich!"
+
+#: init.lua
+msgid "Removing unused wagon"
+msgstr "Ungenutzer Waggon wird entfernt"
+
+#: init.lua
+msgid "Restoring saved state in 1 second..."
+msgstr "Speicherstand wird in 1 Sekunde wiederhergestellt..."
+
+#: init.lua
+msgid "Returns the position of the train with the given id"
+msgstr "Gibt die Position des Zugs mit der gegebenen ID aus"
+
+#: init.lua
+msgid "Saving failed: @1"
+msgstr "Speichern ist fehlgeschlagen: @1"
+
+#: init.lua
+msgid "Successfully invalidated train routes"
+msgstr "Bewegungspfade der Züge erfolgreich invalidiert"
+
+#: init.lua
+msgid "Teleporting to train @1"
+msgstr "Teleportiere zu Zug @1"
+
+#: init.lua
+msgid "Teleports you to the position of the train with the given id"
+msgstr "Teleportiere dich zu dem Zug mit der gegebenen ID"
+
+#: init.lua
+msgid ""
+"The advtrains globalstep has been disabled. Trains are not moving, and no "
+"data is saved! Run '/at_disable_step no' to enable again!"
+msgstr ""
+"Advtrains-Schrittverarbeitung ist deaktiviert. Züge bewegen sich nicht und "
+"Änderungen werden nicht gespeichert. Führe '/at_disable_step no' aus um sie "
+"wieder zu aktivieren!"
+
+#: init.lua
+msgid "Train @1 does not exist or is invalid"
+msgstr "Zug @1 existiert nicht oder ist ungültig"
+
+#: init.lua
+msgid ""
+"Train @1 had no wagons left because of some bug. It is being deleted. Wave "
+"it goodbye!"
+msgstr ""
+"Zug @1 hat keine Wagen mehr und sollte nicht mehr existieren. Er wird "
+"gelöscht. Auf Wiedersehen!"
+
+#: init.lua
+msgid "Train @1 is at @2"
+msgstr "Zug @1 ist bei @2"
+
+#: init.lua
+msgid "from wagon_save table."
+msgstr "aus der wagon_save table"
+
+#: misc_nodes.lua
+msgid "@1 Platform (45 degree)"
+msgstr "Hoher @1-Bahnsteig (45°)"
+
+#: misc_nodes.lua
+msgid "@1 Platform (high)"
+msgstr "Hoher @1-Bahnsteig"
+
+#: misc_nodes.lua
+msgid "@1 Platform (low)"
+msgstr "Niedriger @1-Bahnsteig"
+
+#: misc_nodes.lua
+msgid "@1 Platform (low, 45 degree)"
+msgstr "Niedriger @1-Bahnsteig (45°)"
+
+#: protection.lua
+msgid "Can operate turnouts and signals in unprotected areas"
+msgstr "Darf Weichen und Signale in ungeschütztem Gebiet stellen"
+
+#: protection.lua
+msgid "Can place and dig tracks in unprotected areas"
+msgstr "Darf Gleise in ungeschütztem Gebiet verlegen und entfernen"
+
+#: protection.lua
+msgid ""
+"Can place, remove and operate any train, regardless of owner, whitelist, or "
+"protection"
+msgstr "Darf, unabhängig von Besitzer oder Schutz, sämtliche Züge steuern"
+
+#: protection.lua
+msgid "Can place, remove and operate trains"
+msgstr "Darf Züge platzieren, entfernen und steuern"
+
+#: protection.lua
+msgid "You are not allowed to build near tracks at this protected position."
+msgstr "Sie dürfen an geschützten Stellen nicht in der Nähe von Gleisen bauen."
+
+#: protection.lua
+msgid ""
+"You are not allowed to build near tracks without the track_builder privilege."
+msgstr ""
+"Sie dürfen ohne das „track_builder“-Privileg nicht in der Nähe von Gleisen "
+"bauen."
+
+#: protection.lua
+msgid "You are not allowed to build tracks at this protected position."
+msgstr "Sie dürfen an geschützten Stellen kein Gleis bauen."
+
+#: protection.lua
+msgid ""
+"You are not allowed to build tracks without the track_builder privilege."
+msgstr "Sie dürfen ohne das „track_builder“-Privileg kein Gleis bauen."
+
+#: protection.lua
+msgid ""
+"You are not allowed to operate turnouts and signals without the "
+"railway_operator privilege."
+msgstr ""
+"Sie dürfen ohne das „railway_operator“-Privileg keine Bahnanlage operieren."
+
+#: signals.lua
+msgid "Andrew's Cross"
+msgstr "Andreaskreuz"
+
+#: signals.lua
+msgid "Lampless Signal (deprecated!)"
+msgstr "Mechanisches Signal (veraltet!)"
+
+#: signals.lua
+msgid "Signal (deprecated!)"
+msgstr "Signal (veraltet!)"
+
+#: signals.lua
+msgid "Wallmounted Signal (left) (deprecated!)"
+msgstr "An der linken Seite montiertes Signal (veraltet!)"
+
+#: signals.lua
+msgid "Wallmounted Signal (right) (deprecated!)"
+msgstr "An der rechten Seite montiertes Signal (veraltet!)"
+
+#: signals.lua
+msgid "Wallmounted Signal (top) (deprecated!)"
+msgstr "An der Decke montiertes Signal (veraltet!)"
+
+#: track_reg_helper.lua
+msgid "@1 Slope"
+msgstr "@1 Steigung"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: Not enough slope items left (@1 required)"
+msgstr ""
+"Es kann nicht platziert werden: Sie haben nicht genug Steigungsblöcke, es "
+"werden insgesamt @1 benötigt."
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: There's no slope of length @1"
+msgstr ""
+"Es kann nicht platziert werden: die Steigung der Länge @1 ist nicht "
+"definiert."
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: no supporting node at upper end."
+msgstr ""
+"Es kann nicht platziert werden: es gibt keinen unterstützenden Block am Ende "
+"der Steigung."
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: not pointing at node"
+msgstr "Es kann nicht platziert werden: Sie zeigen nicht auf einem Block."
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: space occupied!"
+msgstr "Es kann nicht platziert werden: Diese Position ist besetzt."
+
+#: track_reg_helper.lua tracks.lua
+#, fuzzy
+msgid "This track can not be removed!"
+msgstr "Dieses Gleis kann nicht entfernt werden."
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This node can't be changed using the trackworker!"
+msgstr "Dieser Block kann nicht mit dem Gleiswerkzeug bearbeitet werden."
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This node can't be rotated using the trackworker!"
+msgstr "Dieser Block kann nicht mit dem Gleiswerkzeug gedreht werden."
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This track can not be rotated!"
+msgstr "Dieses Gleis kann nicht gedreht werden."
+
+#: trackplacer.lua
+msgid ""
+"Track Worker Tool\n"
+"\n"
+"Left-click: change rail type (straight/curve/switch)\n"
+"Right-click: rotate object"
+msgstr ""
+"Gleiswerkzeug\n"
+"\n"
+"Linksklick: Gleistyp ändern\n"
+"Rechtsklick: Objekt drehen"
+
+#: tracks.lua
+msgid "Position is occupied by a train."
+msgstr "Ein Zug steht an dieser Position."
+
+#: tracks.lua
+msgid "There's a Signal Influence Point here."
+msgstr "Hier ist ein Signal-Beeinflussungspunkt."
+
+#: tracks.lua
+msgid "There's a Track Circuit Break here."
+msgstr "Hier ist eine Gleisabschnittsgrenze (TCB)."
+
+#: trainhud.lua
+msgid "OVERRUN RED SIGNAL! Examine situation and reverse train to move again."
+msgstr ""
+"ROTES SIGNAL ÜBERFAHREN! Situation überprüfen und Fahrtrichtung wechseln, um "
+"Bewegungssperre aufzuheben."
+
+#: wagonprop_tool.lua
+msgid "Insufficient privileges to use this!"
+msgstr "Unzureichende Privilegien, um dies zu benutzen!"
+
+#: wagonprop_tool.lua
+#, fuzzy
+msgid "Wagon Properties Tool"
+msgstr "Waggon-Einstellungen"
+
+#: wagonprop_tool.lua
+msgid ""
+"Wagon Properties Tool\n"
+"Punch a wagon to view and edit the Wagon Properties"
+msgstr ""
+"Waggon-Einstellungstool\n"
+"Auf Waggon anwenden um Waggon-Einstellungen aufzurufen"
+
+#: wagons.lua
+msgid " units"
+msgstr " Einheiten"
+
+#: wagons.lua
+msgid "!!! Train off track !!!"
+msgstr "!!! Abseits vom Gleis !!!"
+
+#: wagons.lua
+msgid "(Doors closed)"
+msgstr "(Türen geschlossen)"
+
+#: wagons.lua
+msgid "Allow these players to access your wagon:"
+msgstr "Diese Spieler dürfen auf den Waggon zugreifen:"
+
+#: wagons.lua
+msgid "Clear 'Disable ARS' flag"
+msgstr "ARS-Sperre aufheben"
+
+#: wagons.lua
+msgid "Current FC: "
+msgstr "Aktueller FC:"
+
+#: wagons.lua
+msgid ""
+"Destroying wagon with inventory, but inventory is not found? Shouldn't "
+"happen!"
+msgstr ""
+"Waggon mit Inventar zerstört, aber Inventar nicht gefunden? Sollte nicht "
+"passieren!"
+
+#: wagons.lua
+msgid "Doors are closed! (Try holding sneak key!)"
+msgstr "Die Türen sind geschlossen! (Probiere Schleichen-Taste!)"
+
+#: wagons.lua
+msgid ""
+"Doors are closed. Use Sneak+rightclick to ignore the closed doors and get "
+"off."
+msgstr ""
+"Die Türen sind geschlossen. Nutzen Sie Schleichen+Rechtsklick, um trotz "
+"geschlossener Türen auszusteigen."
+
+#: wagons.lua
+msgid "Freight Code:"
+msgstr "Frachtcode:"
+
+#: wagons.lua
+msgid "Get off"
+msgstr "Aussteigen"
+
+#: wagons.lua
+msgid "Get off (forced)"
+msgstr "Ausstieg zwingen"
+
+#: wagons.lua
+msgid "Line"
+msgstr "Linie"
+
+#: wagons.lua
+msgid "Liquid: "
+msgstr "Flüssigkeit:"
+
+#: wagons.lua
+msgid "Liquid: empty"
+msgstr "Flüssigkeit: leer"
+
+#: wagons.lua
+msgid "Missing train_operator privilege"
+msgstr "Fehlendes train_operator Privileg"
+
+#: wagons.lua
+msgid "Next FC:"
+msgstr "Nächster FC:"
+
+#: wagons.lua
+msgid "Not a valid wagon id."
+msgstr "Keine gültige Waggon-ID"
+
+#: wagons.lua
+msgid "Not allowed to do this."
+msgstr "Sie dürfen dies nicht tun."
+
+#: wagons.lua
+msgid "Onboard Computer"
+msgstr "Bordcomputer"
+
+#: wagons.lua
+msgid "Please specify a player name to transfer ownership to."
+msgstr ""
+"Geben Sie einen Spielernamen an, an den der Besitz übertragen werden soll."
+
+#: wagons.lua
+msgid "Prev FC"
+msgstr "Vorheriger FC"
+
+#: wagons.lua
+msgid "Remote Routesetting"
+msgstr "Fahrstraße entfernt stellen"
+
+#: wagons.lua
+msgid "Routingcode"
+msgstr "Routingcode"
+
+#: wagons.lua
+msgid "Save wagon properties"
+msgstr "Waggon-Einstellungen speichern"
+
+#: wagons.lua
+msgid "Select seat:"
+msgstr "Wählen Sie einen Sitzplatz aus:"
+
+#: wagons.lua
+msgid "Show Inventory"
+msgstr "Inventar Zeigen"
+
+#: wagons.lua
+msgid "Text displayed inside train"
+msgstr "Innere Anzeige"
+
+#: wagons.lua
+msgid "Text displayed outside on train"
+msgstr "Äußere Anzeige"
+
+#: wagons.lua
+msgid "That player does not exist!"
+msgstr "Dieser Spieler existiert nicht!"
+
+#: wagons.lua
+msgid "That wagon does not exist!"
+msgstr "Dieser Wagen existiert nicht!"
+
+#: wagons.lua
+msgid "The track you are trying to place the wagon on is not long enough!"
+msgstr "Das Gleis, auf dem der Waggon platziert werden woll, ist zu kurz!"
+
+#: wagons.lua
+msgid "The wagon's inventory is not empty."
+msgstr "Das Inventar dieses Waggons ist nicht leer."
+
+#: wagons.lua
+msgid "This Wagon ID"
+msgstr "Dieser Wagen ID"
+
+#: wagons.lua
+msgid "This wagon has no seats."
+msgstr "In diesem Waggon ist kein Sitzplatz vorhanden."
+
+#: wagons.lua
+msgid "This wagon is full."
+msgstr "Der Waggon ist voll."
+
+#: wagons.lua
+msgid "This wagon is owned by @1, you can't destroy it."
+msgstr "Dieser Waggon gehört @1, Sie dürfen ihn nicht abbauen."
+
+#: wagons.lua
+msgid "Train ID"
+msgstr "Zug-ID"
+
+#: wagons.lua
+msgid "Train overview / coupling control is only shown when the train stands."
+msgstr "Zugübersicht / Kupplungssteuerung nur im Stillstand verfügbar."
+
+#: wagons.lua
+msgid "Train overview /coupling control:"
+msgstr "Zugübersicht / Kupplungssteuerung:"
+
+#: wagons.lua
+msgid "Uninitialized, removing"
+msgstr "Nicht initialisiert, entferne"
+
+#: wagons.lua
+msgid "Wagon @1 ownership changed from @2 to @3"
+msgstr "Besitzer von Waggon @1 geändert von @2 auf @3"
+
+#: wagons.lua
+msgid "Wagon needs to be decoupled from other wagons in order to destroy it."
+msgstr "Der Waggon muss abgekoppelt sein, damit Sie ihn abbauen können."
+
+#: wagons.lua
+msgid "Wagon placeholder"
+msgstr "Waggon-Platzhalter"
+
+#: wagons.lua
+msgid "Wagon properties"
+msgstr "Waggon-Einstellungen"
+
+#: wagons.lua
+msgid "Wagon road number:"
+msgstr "Wagennummer:"
+
+#: wagons.lua
+msgid ""
+"Warning: If you destroy this wagon, you only get some steel back! If you are "
+"sure, hold Sneak and left-click the wagon."
+msgstr ""
+"Warnung: Durch den Abbau des Waggons erhalten Sie nur etwas Stahl zurück. "
+"Nutzen Sie Schleichen+Linksklick, um dem Waggon abzubauen."
+
+#: wagons.lua
+msgid "You are not allowed to access the driver stand."
+msgstr "Sie haben keinen Zugang zum Führerstand."
+
+#: wagons.lua
+msgid "You can't get on this wagon."
+msgstr "Sie können nicht in diesen Waggon einsteigen."
+
+#: wagons.lua
+#, fuzzy
+msgid "You don't have the train_operator privilege."
+msgstr "Ihnen fehlt das „@1“-Privileg."
+
+#: wagons.lua
+msgid "You have been given ownership of wagon @1"
+msgstr "Sie sind nun der Besitzer von Waggon @1"
+
+#~ msgid "3-way turnout"
+#~ msgstr "Dreiwegweiche"
+
+#~ msgid "90+Angle Diamond Crossing Track"
+#~ msgstr "Kreuzung mit einem achsenparallelen Gleis"
+
+#~ msgid "ATC controller"
+#~ msgstr "Zugbeeinflussungsgleis"
+
+#~ msgid ""
+#~ "ATC controller, mode @1\n"
+#~ "Channel: @2"
+#~ msgstr ""
+#~ "Zugbeeinflussungsgleis in Betriebsart „@1“\n"
+#~ "Kanal: @2"
+
+#~ msgid "Access to @1"
+#~ msgstr "Zugang zu @1"
+
+#~ msgid "Big Industrial Train Engine"
+#~ msgstr "Große Industrielle Lokomotive"
+
+#~ msgid "Box Wagon"
+#~ msgstr "Güterwaggon"
+
+#~ msgid "Bumper"
+#~ msgstr "Prellbock"
+
+#~ msgid ""
+#~ "Can place and configure LuaATC components, including execute potentially "
+#~ "harmful Lua code"
+#~ msgstr ""
+#~ "Kann LuaATC-Bauteile platzieren und konfigurieren (auch evtl. schädliche "
+#~ "Programme ausführen)"
+
+#~ msgid "Can't get on: wagon full or doors closed!"
+#~ msgstr ""
+#~ "Sie können nicht einsteigen: der Waggon ist voll oder die Türen sind "
+#~ "geschlossen."
+
+#~ msgid "Can't place: protected position!"
+#~ msgstr "Es kann nicht platziert werden: diese Position ist geschützt."
+
+#~ msgid "Command (on)"
+#~ msgstr "Befehl (wenn aktiviert)"
+
+#~ msgid "Default Seat"
+#~ msgstr "Standardsitzplatz"
+
+#~ msgid "Default Seat (driver stand)"
+#~ msgstr "Standardsitzplatz (Führerstand)"
+
+#~ msgid "Dep. Speed"
+#~ msgstr "Zielgeschwindigkeit bei Abfahrt"
+
+#~ msgid "Deprecated Track"
+#~ msgstr "ausrangiertes Gleis, nicht verwenden."
+
+#~ msgid "Detailed Steam Engine"
+#~ msgstr "Detaillierte Dampflokomotive"
+
+#~ msgid "Detector Rail"
+#~ msgstr "Detektorgleis"
+
+#~ msgid "Diagonal Diamond Crossing Track"
+#~ msgstr "Diagonale Gleiskreuzung"
+
+#~ msgid "Door Delay"
+#~ msgstr "Zeit für die Türschließung"
+
+#~ msgid "Door Side"
+#~ msgstr "Türseite"
+
+#, fuzzy
+#~ msgid "Driver Stand"
+#~ msgstr "Führerstand"
+
+#~ msgid "Driver Stand (left)"
+#~ msgstr "Führerstand Links"
+
+#~ msgid "Driver Stand (right)"
+#~ msgstr "Führerstand Rechts"
+
+#~ msgid "Driver stand"
+#~ msgstr "Führerstand"
+
+#~ msgid "Industrial Train Engine"
+#~ msgstr "Industrielle Lokomotive"
+
+#~ msgid "Industrial tank wagon"
+#~ msgstr "Tankwaggon"
+
+#~ msgid "Industrial wood wagon"
+#~ msgstr "Holztransportwaggon"
+
+#~ msgid "Japanese Train Engine"
+#~ msgstr "Japanische Personenzug-Lokomotive"
+
+#~ msgid "Japanese Train Inter-Wagon Connection"
+#~ msgstr "Waggonzwischenverbindung Japanischer Personenzüge"
+
+#~ msgid "Japanese Train Wagon"
+#~ msgstr "Japanischer Personenzug-Passagierwaggon"
+
+#, fuzzy
+#~ msgid "Japanese signal pole"
+#~ msgstr "Japanischer Personenzug-Passagierwaggon"
+
+#~ msgid "Kick out passengers"
+#~ msgstr "Fahrgäste zum Ausstieg zwingen"
+
+#~ msgid "Loading Track"
+#~ msgstr "Beladungsgleis"
+
+#~ msgid "Lock couples"
+#~ msgstr "Kupplungen sperren"
+
+#~ msgid "LuaATC component with error: @1"
+#~ msgstr "LuaATC-Bauteil mit Fehlermeldung: @1"
+
+#~ msgid "Passenger Wagon"
+#~ msgstr "Passagierwaggon"
+
+#, fuzzy
+#~ msgid "Passenger area"
+#~ msgstr "Passagierwaggon"
+
+#~ msgid ""
+#~ "Passive Component Naming Tool\n"
+#~ "\n"
+#~ "Right-click to name a passive component."
+#~ msgstr ""
+#~ "PC-Benennungswerkzeug\n"
+#~ "\n"
+#~ "Rechtsklick zur Benennung der passiven Komponente."
+
+#~ msgid "Perpendicular Diamond Crossing Track"
+#~ msgstr "Kreuzung mit zueinander orthogonalen Gleisen"
+
+#~ msgid "Point Speed Restriction Track"
+#~ msgstr "Geschwindigkeitskontrollgleis"
+
+#~ msgid "Point speed restriction: @1"
+#~ msgstr "Geschwindigkeitskontrolle: @1"
+
+#~ msgid "Reverse train"
+#~ msgstr "Zug Umkehren"
+
+#~ msgid "Signal"
+#~ msgstr "Lichtsignal"
+
+#~ msgid "Speed:"
+#~ msgstr "Geschw.:"
+
+#~ msgid "Station Code"
+#~ msgstr "Kennzeichen der Haltestelle"
+
+#~ msgid "Station Name"
+#~ msgstr "Name der Haltestelle"
+
+#~ msgid "Station code \"@1\" already exists and is owned by @2."
+#~ msgstr ""
+#~ "Die Haltestelle mit dem Kennzeichen „@1“ ist bereits vorhanden und wird "
+#~ "von @2 verwaltet."
+
+#~ msgid "Station/Stop Track"
+#~ msgstr "Gleis zur Kennzeichnung einer Haltestelle"
+
+#~ msgid "Steam Engine"
+#~ msgstr "Dampflokomotive"
+
+#~ msgid "Stop Time"
+#~ msgstr "Wartezeit"
+
+#~ msgid "Subway Passenger Wagon"
+#~ msgstr "U-Bahn-Waggon"
+
+#~ msgid "Target:"
+#~ msgstr "Zielges.:"
+
+#~ msgid "This position is protected!"
+#~ msgstr "Diese Position ist geschützt!"
+
+#~ msgid "This station is owned by @1. You are not allowed to edit its name."
+#~ msgstr ""
+#~ "Diese Haltestelle wird von @1 verwaltet. Sie dürfen sie nicht umbenennen."
+
+#~ msgid "This track can not be changed."
+#~ msgstr "Dieses Gleis kann nicht geändert werden."
+
+#~ msgid "Track"
+#~ msgstr "Gleis"
+
+#, fuzzy
+#~ msgid "Train "
+#~ msgstr "Der Zug wurde Kopiert."
+
+#~ msgid "Unconfigured LuaATC component"
+#~ msgstr "Nicht konfiguierter LuaATC-Bauteil"
+
+#~ msgid "Unloading Track"
+#~ msgstr "Abladungsgleis"
+
+#~ msgid "Use Sneak+rightclick to bypass closed doors!"
+#~ msgstr ""
+#~ "Nutzen Sie Schleichen+Rechtsklick, um trotz geschlossener Türen "
+#~ "einzusteigen."
+
+#~ msgid "Y-turnout"
+#~ msgstr "Y-Weiche"
+
+#~ msgid ""
+#~ "You are not allowed to configure this LuaATC component without the @1 "
+#~ "privilege."
+#~ msgstr ""
+#~ "Sie dürfen ohne das „@1“-Privileg diesen LuaATC-Bauteil nicht "
+#~ "konfigurieren."
+
+#~ msgid ""
+#~ "You are not allowed to configure this track without the @1 privilege."
+#~ msgstr "Sie dürfen ohne das „@1“-Privileg dieses Gleis nicht konfigurieren."
+
+#~ msgid "You are not allowed to configure this track."
+#~ msgstr "Sie dürfen dieses Gleis nicht konfigurieren."
+
+#, fuzzy
+#~ msgid "You are not allowed to modify this protected track."
+#~ msgstr "Sie dürfen an geschützten Stellen kein Gleis bauen."
+
+#~ msgid ""
+#~ "You are not allowed to name LuaATC passive components without the @1 "
+#~ "privilege."
+#~ msgstr "Sie dürfen ohne das „@1“ keinen passiven LuaATC-Bauteil benennen."
+
+#~ msgid ""
+#~ "You need to own at least one neighboring wagon to destroy this couple."
+#~ msgstr ""
+#~ "Sie müssen Besitzer eines angrenzenden Waggons sein, um hier abzukuppeln."
diff --git a/advtrains/po/fr.po b/advtrains/po/fr.po
new file mode 100644
index 0000000..b2a54da
--- /dev/null
+++ b/advtrains/po/fr.po
@@ -0,0 +1,1024 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: advtrains\n"
+"Report-Msgid-Bugs-To: advtrains-discuss@lists.sr.ht\n"
+"POT-Creation-Date: 2025-06-10 21:40+0200\n"
+"PO-Revision-Date: 2025-03-25 15:06+0100\n"
+"Last-Translator: Tanavit <tanavit@posto.ovh>\n"
+"Language-Team: French\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.4.2\n"
+
+#: atc.lua
+msgid "ATC Kick command warning: doors are closed."
+msgstr "Avertissement commande ATC Éjecter : portes closes."
+
+#: atc.lua
+msgid "ATC Kick command warning: train moving."
+msgstr "Avertissement commande ATC Éjecter : train en mouvement."
+
+#: atc.lua
+msgid "ATC Reverse command warning: didn't reverse train, train moving."
+msgstr ""
+"Attention : Commande ATC de renversement impossible car le train se déplace."
+
+#: atc.lua
+msgid "ATC command parse error: Unknown command: @1"
+msgstr "Erreur d'analyse de commande ATC : Commande inconnue : @1"
+
+#: atc.lua
+msgid "ATC command syntax error: I statement not closed: @1"
+msgstr "Erreur de syntaxe de commande ATC : instruction \"I\" incomplète : @1"
+
+#: atc.lua
+#, fuzzy
+msgid "ATC controller, Command: @1"
+msgstr ""
+"Controlleur ATC, mode @1\n"
+"Commande : @2"
+
+#: atc.lua
+msgid "Command"
+msgstr "Commande"
+
+#: atc.lua
+msgid "Digiline channel"
+msgstr "Canal Digiline"
+
+#: atc.lua wagons.lua
+msgid "Save"
+msgstr "Sauvegarder"
+
+#: atc.lua
+msgid "Unconfigured ATC controller"
+msgstr "Controlleur ATC, non-configuré"
+
+#: copytool.lua
+msgid "Back of train would end up off track, cancelling."
+msgstr "La fin du train serait hors voie : annulation."
+
+#: copytool.lua
+msgid "No such lua entity."
+msgstr "Pas de telle entité lua."
+
+#: copytool.lua
+msgid "No such train: @1."
+msgstr "Pas de tel train : @1."
+
+#: copytool.lua
+msgid "No such wagon: @1."
+msgstr "Pas de tel wagon : @1."
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Copy failed."
+msgstr "Le presse-papier ne peut accéder aux métadonnées. Échec de la copie."
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Paste failed."
+msgstr "Le presse-papier ne peut accéder aux métadonnées. Échec du collage."
+
+#: copytool.lua
+msgid "The clipboard is empty."
+msgstr "Le presse-papier est vide."
+
+#: copytool.lua
+msgid "The track you are trying to place the wagon on is not long enough."
+msgstr "La voie sur laquelle vous tentez de placer le wagon est trop courte."
+
+#: copytool.lua
+msgid "Train copied."
+msgstr "Train copié."
+
+#: copytool.lua
+msgid ""
+"Train copy/paste tool\n"
+"\n"
+"Left-click: copy train\n"
+"Right-click: paste train"
+msgstr ""
+"Outil de copie/collage de train\n"
+"\n"
+"Clic-Gauche : copie\n"
+"\n"
+"Clic-Droit : collage"
+
+#: copytool.lua
+msgid "You do not have the @1 privilege."
+msgstr "Vous ne possédez pas le privilège \"@1\"."
+
+#: couple.lua
+msgid "<No coupler>"
+msgstr "<Pas de coupleur>"
+
+#: couple.lua
+msgid "Buffer and Chain Coupler"
+msgstr "Attelage à tampon et vis"
+
+#: couple.lua
+msgid "Can not couple: The couplers of the trains do not match (@1 and @2)."
+msgstr ""
+"Accouplement impossible: les attelages des trains ne concordent pas (@1 et "
+"@2)."
+
+#: couple.lua
+msgid ""
+"Cannot couple @1 and @2 - train would have length @3 which is above the "
+"limit of @4"
+msgstr ""
+
+#: couple.lua
+msgid "Scharfenberg Coupler"
+msgstr "Attelage Scharfenberg"
+
+#: couple.lua
+msgid ""
+"You are not allowed to couple trains without the train_operator privilege."
+msgstr ""
+"Vous n'êtes pas autorisé à coupler des trains sans le privilège "
+"\"train_operator\"."
+
+#: craft_items.lua
+msgid "Boiler"
+msgstr "Chaudière à vapeur"
+
+#: craft_items.lua
+msgid "Chimney"
+msgstr "Cheminée"
+
+#: craft_items.lua
+msgid "Driver's cab"
+msgstr "Cabine de pilotage"
+
+#: craft_items.lua
+msgid "Wheel"
+msgstr "Roue"
+
+#: init.lua
+#, fuzzy
+msgid "Advtrains Status: no_action @1 slowdown @2 (log @3)"
+msgstr "État d'advtrains : aucune action"
+
+#: init.lua
+msgid "Advtrains is already running normally!"
+msgstr "Advtrains fonctionne déjà correctement !"
+
+#: init.lua
+msgid ""
+"Crash during advtrains main step - skipping the shutdown save operation to "
+"not save inconsistent data!"
+msgstr ""
+"Crash durant le pas principal d'advtrains - saut de l'opération de "
+"sauvegarde de terminaison pour éviter l'enregistrement de données "
+"corrompues !"
+
+#: init.lua
+msgid ""
+"Data is being saved. While saving, advtrains will remove the players from "
+"trains. Save files will be reloaded afterwards!"
+msgstr ""
+"Données en cours de sauvegarde. Durant cette phase, advtrains débarquera les "
+"joueurs des trains. Les fichiers de sauvegarde seront ultérieurement "
+"rechargés !"
+
+# Routage est il le bon terme ?
+#: init.lua
+msgid "Delete all train routes, force them to recalculate"
+msgstr "Suppression et recalcul de tous les routages"
+
+#: init.lua
+msgid ""
+"Detach all players, especially the offline ones, from all trains. Use only "
+"when no one serious is on a train."
+msgstr ""
+"Débarque tous les joueurs, en particulier ceux déconnectés, de tous les "
+"trains. À n'utiliser que quand aucun joueur sérieux n'a embarqué."
+
+#: init.lua
+msgid "Disable the advtrains globalstep temporarily"
+msgstr "Désactive temporairement le pas global d'advtrains"
+
+#: init.lua
+msgid "Disabled advtrains successfully"
+msgstr "Succès de la désactivation d'advtrains"
+
+#: init.lua
+msgid "Instructed to save() but load() was never called!"
+msgstr "Appel de save() requis sans appel préalable de load() !"
+
+#: init.lua
+msgid "Print advtrains status info"
+msgstr "Affiche les informations d'état d'advtrains"
+
+#: init.lua
+msgid "Re-enabling advtrains globalstep..."
+msgstr "Réacivation du pas global d'advtrains..."
+
+#: init.lua
+msgid "Reload successful!"
+msgstr "Succès du rechargement !"
+
+#: init.lua
+msgid "Removing unused wagon"
+msgstr "Suppression d'un wagon inutilisé"
+
+#: init.lua
+msgid "Restoring saved state in 1 second..."
+msgstr "Restauration du l'état sauvegardé dans une seconde..."
+
+#: init.lua
+msgid "Returns the position of the train with the given id"
+msgstr "Affiche la position du train identifié"
+
+#: init.lua
+#, fuzzy
+msgid "Saving failed: @1"
+msgstr "Échec de sauvegarde : "
+
+# Routage est il le bon terme ?
+#: init.lua
+msgid "Successfully invalidated train routes"
+msgstr "Succès d'invalidation des routages des trains"
+
+#: init.lua
+#, fuzzy
+msgid "Teleporting to train @1"
+msgstr "Téléportation au train "
+
+#: init.lua
+msgid "Teleports you to the position of the train with the given id"
+msgstr "Vous téléporte à la position du train identifié"
+
+#: init.lua
+msgid ""
+"The advtrains globalstep has been disabled. Trains are not moving, and no "
+"data is saved! Run '/at_disable_step no' to enable again!"
+msgstr ""
+"Le pas global d'advtrains est désactivé. Les trains sont immobiles et aucune "
+"donnée n'est sauvegardée. Exécutez '/at_disable_step no ' pour le réactiver !"
+
+#: init.lua
+#, fuzzy
+msgid "Train @1 does not exist or is invalid"
+msgstr " n'existe pas ou est invalide"
+
+#: init.lua
+#, fuzzy
+msgid ""
+"Train @1 had no wagons left because of some bug. It is being deleted. Wave "
+"it goodbye!"
+msgstr ""
+"n'a plus de wagon à cause d'un bug quelconque. Il est détruit. Faites lui "
+"coucou !"
+
+#: init.lua
+msgid "Train @1 is at @2"
+msgstr ""
+
+#: init.lua
+msgid "from wagon_save table."
+msgstr "de la table wagon_save."
+
+#: misc_nodes.lua
+msgid "@1 Platform (45 degree)"
+msgstr "Quai @1 (haut, 45°)"
+
+#: misc_nodes.lua
+msgid "@1 Platform (high)"
+msgstr "Quai @1 (haut)"
+
+#: misc_nodes.lua
+msgid "@1 Platform (low)"
+msgstr "Quai @1 (bas)"
+
+#: misc_nodes.lua
+msgid "@1 Platform (low, 45 degree)"
+msgstr "Quai @1 (bas, 45°)"
+
+#: protection.lua
+msgid "Can operate turnouts and signals in unprotected areas"
+msgstr ""
+"Possibilité d'opérer des embranchements et signaux dans les zones non "
+"protégées"
+
+#: protection.lua
+msgid "Can place and dig tracks in unprotected areas"
+msgstr "Possibilité de poser ou retirer des voies dans les zones non protégées"
+
+#: protection.lua
+msgid ""
+"Can place, remove and operate any train, regardless of owner, whitelist, or "
+"protection"
+msgstr ""
+"Possibilité de poser, retirer ou opérer un quelconque train, indépendamment "
+"du propriétaire, de la liste blanche ou de protection"
+
+#: protection.lua
+msgid "Can place, remove and operate trains"
+msgstr "Possibilité de poser, retirer ou opérer les trains"
+
+#: protection.lua
+msgid "You are not allowed to build near tracks at this protected position."
+msgstr ""
+"Vous ne pouvez pas construire à proximité d'une voie à cet emplacement "
+"protégé."
+
+#: protection.lua
+msgid ""
+"You are not allowed to build near tracks without the track_builder privilege."
+msgstr ""
+"Vous ne pouvez pas construire à proximité d'une voie sans le privilège "
+"\"track_builder\" (?)"
+
+#: protection.lua
+msgid "You are not allowed to build tracks at this protected position."
+msgstr "Vous ne pouvez pas construire une voie à cet emplacement protégé."
+
+#: protection.lua
+msgid ""
+"You are not allowed to build tracks without the track_builder privilege."
+msgstr ""
+"Vous ne pouvez pas construire une voie sans le privilège \"track_builder\"."
+
+#: protection.lua
+msgid ""
+"You are not allowed to operate turnouts and signals without the "
+"railway_operator privilege."
+msgstr ""
+"Vous ne pouvez pas actionner les aiguillages ou les signaux (privilège "
+"\"railway_operator\" manquant)"
+
+#: signals.lua
+msgid "Andrew's Cross"
+msgstr "Croix de Saint André"
+
+#: signals.lua
+#, fuzzy
+msgid "Lampless Signal (deprecated!)"
+msgstr "Sémaphore"
+
+#: signals.lua
+msgid "Signal (deprecated!)"
+msgstr ""
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (left) (deprecated!)"
+msgstr "Signal mural (gauche)"
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (right) (deprecated!)"
+msgstr "Signal mural (droit)"
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (top) (deprecated!)"
+msgstr "Signal mural (plafond)"
+
+#: track_reg_helper.lua
+msgid "@1 Slope"
+msgstr "Pente @1"
+
+#: track_reg_helper.lua
+msgid "Can't place: Not enough slope items left (@1 required)"
+msgstr ""
+"Placement impossible : quantité insuffisante de voie pentue (@1 manquant)"
+
+#: track_reg_helper.lua
+msgid "Can't place: There's no slope of length @1"
+msgstr "Placement impossible : il n'y a pas de voie pentue de longueur @1"
+
+#: track_reg_helper.lua
+msgid "Can't place: no supporting node at upper end."
+msgstr "Placement impossible : pas de nœud d'appui à l'extrémité supérieure."
+
+#: track_reg_helper.lua
+msgid "Can't place: not pointing at node"
+msgstr "Placement impossible : ne pointe pas un nœud"
+
+#: track_reg_helper.lua
+msgid "Can't place: space occupied!"
+msgstr "Placement impossible : espace occupé !"
+
+#: track_reg_helper.lua tracks.lua
+msgid "This track can not be removed!"
+msgstr "Cette voie ne peut pas être enlevée !"
+
+#: trackplacer.lua
+msgid "This node can't be changed using the trackworker!"
+msgstr "Ce nœud ne peut être modifié avec l'outil \"Trackworker\" !"
+
+#: trackplacer.lua
+msgid "This node can't be rotated using the trackworker!"
+msgstr "Ce nœud ne peut être tourné avec l'outil \"Trackworker\" !"
+
+#: trackplacer.lua
+msgid "This track can not be rotated!"
+msgstr "Cette voie ne peut pas être tournée !"
+
+#: trackplacer.lua
+msgid ""
+"Track Worker Tool\n"
+"\n"
+"Left-click: change rail type (straight/curve/switch)\n"
+"Right-click: rotate object"
+msgstr ""
+"Outil \"Trackworker\"\n"
+"\n"
+"Clic-Gauche : change le type de rail (droit/courbé/aiguillage)\n"
+"\n"
+"Clic-Droit : tourne l'objet"
+
+#: tracks.lua
+msgid "Position is occupied by a train."
+msgstr "Cet emplacement est occupé par un train."
+
+#: tracks.lua
+msgid "There's a Signal Influence Point here."
+msgstr "Il y a un \"Signal Influence Point\" ici."
+
+#: tracks.lua
+msgid "There's a Track Circuit Break here."
+msgstr "Il y a un \"Track Circuit Break\" ici."
+
+#: trainhud.lua
+msgid "OVERRUN RED SIGNAL! Examine situation and reverse train to move again."
+msgstr ""
+"Franchissement de signal rouge : examinez la situation et inversez le sens "
+"de marche du train."
+
+#: wagonprop_tool.lua
+msgid "Insufficient privileges to use this!"
+msgstr "Privilèges insuffisants pour utiliser ceci !"
+
+#: wagonprop_tool.lua
+msgid "Wagon Properties Tool"
+msgstr "Outil de propriété du wagon"
+
+#: wagonprop_tool.lua
+msgid ""
+"Wagon Properties Tool\n"
+"Punch a wagon to view and edit the Wagon Properties"
+msgstr ""
+"Outil de propriété du wagon\n"
+"Frappez un wagon pour voir et modifier ses propriétés"
+
+#: wagons.lua
+msgid " units"
+msgstr " Unités"
+
+#: wagons.lua
+msgid "!!! Train off track !!!"
+msgstr "!!! Train hors voie !!!"
+
+#: wagons.lua
+msgid "(Doors closed)"
+msgstr "(Portes closes)"
+
+#: wagons.lua
+msgid "Allow these players to access your wagon:"
+msgstr "Autoriser ces joueurs à embarquer :"
+
+#: wagons.lua
+msgid "Clear 'Disable ARS' flag"
+msgstr "Effacer le drapeau \"Désactiver l'ARS\""
+
+#: wagons.lua
+msgid "Current FC: "
+msgstr "Code de fret courant: "
+
+#: wagons.lua
+msgid ""
+"Destroying wagon with inventory, but inventory is not found? Shouldn't "
+"happen!"
+msgstr "Desctruction d'un wagon avec inventaire introuvable ? Anomalie !"
+
+#: wagons.lua
+msgid "Doors are closed! (Try holding sneak key!)"
+msgstr "Portes closes : (Essayez la \"sneak key\"!\")"
+
+#: wagons.lua
+msgid ""
+"Doors are closed. Use Sneak+rightclick to ignore the closed doors and get "
+"off."
+msgstr ""
+"Portes closes ! Utilisez \"Marcher lentement (Sneak)\" et Clic-Droit pour "
+"franchir les portes et débarquer."
+
+#: wagons.lua
+msgid "Freight Code:"
+msgstr "Code de frêt :"
+
+#: wagons.lua
+msgid "Get off"
+msgstr "Débarquer"
+
+#: wagons.lua
+msgid "Get off (forced)"
+msgstr "Débarquer (de force)"
+
+#: wagons.lua
+msgid "Line"
+msgstr "Ligne"
+
+#: wagons.lua
+msgid "Liquid: "
+msgstr "Liquide : "
+
+#: wagons.lua
+msgid "Liquid: empty"
+msgstr "Liquide : vide"
+
+#: wagons.lua
+msgid "Missing train_operator privilege"
+msgstr "Privilège \"train_operator\" manquant"
+
+#: wagons.lua
+msgid "Next FC:"
+msgstr "Code de fret suivant :"
+
+#: wagons.lua
+msgid "Not a valid wagon id."
+msgstr "Identificateur de wagon invalide."
+
+#: wagons.lua
+msgid "Not allowed to do this."
+msgstr "Vous n'êtes pas autorisé effectuer ceci."
+
+#: wagons.lua
+msgid "Onboard Computer"
+msgstr "Ordinateur embarqué"
+
+#: wagons.lua
+msgid "Please specify a player name to transfer ownership to."
+msgstr ""
+"Spécifiez le nom du joueur à qui la propriété doit être transférée, SVP."
+
+#: wagons.lua
+msgid "Prev FC"
+msgstr "Code de fret précédent"
+
+#: wagons.lua
+msgid "Remote Routesetting"
+msgstr "Routage à distance"
+
+#: wagons.lua
+msgid "Routingcode"
+msgstr "Code de routage"
+
+#: wagons.lua
+msgid "Save wagon properties"
+msgstr "Sauvegarder les propriétés du wagon"
+
+#: wagons.lua
+msgid "Select seat:"
+msgstr "Choisir le siège :"
+
+#: wagons.lua
+msgid "Show Inventory"
+msgstr "Montrer le stock"
+
+#: wagons.lua
+msgid "Text displayed inside train"
+msgstr "Texte affiché à l'intérieur du train"
+
+#: wagons.lua
+msgid "Text displayed outside on train"
+msgstr "Texte affiché à l'extérieur du train"
+
+#: wagons.lua
+msgid "That player does not exist!"
+msgstr "Ce joueur n'existe pas !"
+
+#: wagons.lua
+msgid "That wagon does not exist!"
+msgstr "Ce wagon n'a pas de siège !"
+
+#: wagons.lua
+msgid "The track you are trying to place the wagon on is not long enough!"
+msgstr "La voie sur laquelle vous tentez de placer le wagon est trop courte !"
+
+#: wagons.lua
+msgid "The wagon's inventory is not empty."
+msgstr "Le stock de ce wagon n'est pas vide."
+
+#: wagons.lua
+msgid "This Wagon ID"
+msgstr "Identificateur du wagon"
+
+#: wagons.lua
+msgid "This wagon has no seats."
+msgstr "Ce wagon n'a pas de siège."
+
+#: wagons.lua
+msgid "This wagon is full."
+msgstr "Ce wagon est plein."
+
+#: wagons.lua
+msgid "This wagon is owned by @1, you can't destroy it."
+msgstr "Ce wagon est la propriété de @1, vous ne pouvez pas le détruire."
+
+#: wagons.lua
+msgid "Train ID"
+msgstr "Identificateur du train"
+
+#: wagons.lua
+msgid "Train overview / coupling control is only shown when the train stands."
+msgstr ""
+"Aperçu du train / commande d'accouplement montré uniquement à l'arrêt du "
+"train."
+
+#: wagons.lua
+msgid "Train overview /coupling control:"
+msgstr "Aperçu du train / commande d'accouplement :"
+
+#: wagons.lua
+msgid "Uninitialized, removing"
+msgstr "Non initialisé, retiré"
+
+#: wagons.lua
+msgid "Wagon @1 ownership changed from @2 to @3"
+msgstr "La propriété du wagon @1 a été transférée de @2 à @3"
+
+#: wagons.lua
+msgid "Wagon needs to be decoupled from other wagons in order to destroy it."
+msgstr ""
+"Les wagons doivent être désaccouplés des autres pour pouvoir être détruits."
+
+#: wagons.lua
+#, fuzzy
+msgid "Wagon placeholder"
+msgstr ", dans un espace réservé"
+
+#: wagons.lua
+msgid "Wagon properties"
+msgstr "Propriétés du wagon"
+
+#: wagons.lua
+msgid "Wagon road number:"
+msgstr "Immatriculation du wagon :"
+
+#: wagons.lua
+msgid ""
+"Warning: If you destroy this wagon, you only get some steel back! If you are "
+"sure, hold Sneak and left-click the wagon."
+msgstr ""
+"Attention: Si vous détruisez ce wagon, vous ne récupérerez que de la "
+"ferraille ! Si vous êtes sûr de vous, appuyez la touche \"Marcher lentement "
+"(Sneak)\" et Clic-Gauche."
+
+#: wagons.lua
+msgid "You are not allowed to access the driver stand."
+msgstr "Accès interdit au poste de pilotage."
+
+#: wagons.lua
+msgid "You can't get on this wagon."
+msgstr "Montée impossible dans ce wagon."
+
+#: wagons.lua
+msgid "You don't have the train_operator privilege."
+msgstr "Vous ne possédez pas le privilège \"train_operator\"."
+
+#: wagons.lua
+msgid "You have been given ownership of wagon @1"
+msgstr "La propriété du wagon @1 vous a été transférée"
+
+#~ msgid " is at "
+#~ msgstr " est à la position "
+
+#~ msgid " wagon:destroy(): data is not set!"
+#~ msgstr " Appel de wagon:destroy() : données non définies !"
+
+#~ msgid "(log"
+#~ msgstr "(log"
+
+#~ msgid "3-way turnout"
+#~ msgstr "Embranchement triple"
+
+#~ msgid "90+Angle Diamond Crossing Track"
+#~ msgstr "Croisement perpendiculo-diagonal"
+
+#~ msgid "ATC controller"
+#~ msgstr "Controlleur ATC"
+
+#~ msgid ""
+#~ "ATC controller, mode @1\n"
+#~ "Channel: @2"
+#~ msgstr ""
+#~ "Controlleur ATC, mode @1\n"
+#~ "Canal : @2"
+
+#~ msgid "Access to @1"
+#~ msgstr "Accès à @1"
+
+#~ msgid "Big Industrial Train Engine"
+#~ msgstr "Grosse locomotive industrielle"
+
+#~ msgid "Box Wagon"
+#~ msgstr "Wagon de frêt"
+
+#~ msgid "Bumper"
+#~ msgstr "Heurtoir"
+
+#~ msgid ""
+#~ "Can place and configure LuaATC components, including execute potentially "
+#~ "harmful Lua code"
+#~ msgstr ""
+#~ "Permet le placement et la configuration de composants LuaATC avec risque "
+#~ "d'exécution de code Lua dangereux"
+
+#~ msgid "Can't get on: wagon full or doors closed!"
+#~ msgstr ""
+#~ "Embarquement impossible : le wagon est plein ou ses portes sont closes !"
+
+#~ msgid "Can't place: protected position!"
+#~ msgstr "Placement impossible : emplacement protégé"
+
+#~ msgid "Caution"
+#~ msgstr "Attention"
+
+#~ msgid "Clear (proceed)"
+#~ msgstr "Autorisation (procédez)"
+
+#~ msgid "Clear Local Environment"
+#~ msgstr "Effacer l'environnement LuaATC"
+
+#~ msgid "Closed"
+#~ msgstr "Fermé"
+
+#~ msgid "Code"
+#~ msgstr "Code"
+
+#~ msgid "Command (on)"
+#~ msgstr "Commande (marche)"
+
+#~ msgid "Danger (halt)"
+#~ msgstr "Danger (stop)"
+
+#~ msgid "Default Seat"
+#~ msgstr "Siège par défaut"
+
+#~ msgid "Default Seat (driver stand)"
+#~ msgstr "Siège par défaut (poste de pilotage)"
+
+#~ msgid "Dep. Speed"
+#~ msgstr "Vit. de départ"
+
+#~ msgid "Deprecated Track"
+#~ msgstr "Voie déconseillée"
+
+#~ msgid "Detailed Steam Engine"
+#~ msgstr "Locomotive à vapeur complexe"
+
+#~ msgid "Detector Rail"
+#~ msgstr "Voie détectrice"
+
+#~ msgid "Diagonal Diamond Crossing Track"
+#~ msgstr "Croisement diagonal"
+
+#~ msgid "Door Delay"
+#~ msgstr "Durée d'ouverture des portes"
+
+#~ msgid "Door Side"
+#~ msgstr "Ouv. des portes coté"
+
+#~ msgid "Driver Stand"
+#~ msgstr "Poste de pilotage"
+
+#~ msgid "Driver Stand (left)"
+#~ msgstr "Poste de pilotage (gauche)"
+
+#~ msgid "Driver Stand (right)"
+#~ msgstr "Poste de pilotage (droit)"
+
+#~ msgid "Driver stand"
+#~ msgstr "Poste de pilotage"
+
+#~ msgid "Industrial Train Engine"
+#~ msgstr "Locomotive industrielle"
+
+#~ msgid "Industrial tank wagon"
+#~ msgstr "Wagon-citerne industriel"
+
+#~ msgid "Industrial wood wagon"
+#~ msgstr "Wagon grumier industriel"
+
+#~ msgid "Japanese Train Engine"
+#~ msgstr "Motrice Japonaise"
+
+#~ msgid "Japanese Train Inter-Wagon Connection"
+#~ msgstr "Passage inter-voiture de train Japonais"
+
+#~ msgid "Japanese Train Wagon"
+#~ msgstr "Voiture Japonaise"
+
+#~ msgid "Japanese signal pole"
+#~ msgstr "Voiture Japonaise"
+
+#~ msgid "Kick out passengers"
+#~ msgstr "Éjecter les passagers"
+
+#~ msgid "Left"
+#~ msgstr "Gauche"
+
+#~ msgid "Left,Right,Closed;"
+#~ msgstr "Gauche,Droit,Fermé;"
+
+#~ msgid "Loading Track"
+#~ msgstr "Voie de Chargement"
+
+#~ msgid "Lock couples"
+#~ msgstr "Verrouiller l'accouplement"
+
+#~ msgid "LuaATC Environment"
+#~ msgstr "Environnement LuaATC"
+
+#~ msgid "LuaATC Mesecon Controller"
+#~ msgstr "Commande Mesecon de LuaATC"
+
+#~ msgid "LuaATC Operation Panel"
+#~ msgstr "Panneau de commande de LuaATC"
+
+#~ msgid "LuaATC component assigned to an invalid environment"
+#~ msgstr "Composant LuaATC assigné à un environnement invalide"
+
+#~ msgid "LuaATC component assigned to environment '@1'"
+#~ msgstr "Composant LuaATC assigné à l'environnement '@1'"
+
+#~ msgid "LuaATC component with error: @1"
+#~ msgstr "Erreur @1 du composant LuaATC"
+
+#~ msgid "Munich U-Bahn Distant Signal ("
+#~ msgstr "Signal distant métro de Munich ("
+
+#~ msgid "Munich U-Bahn Main Signal ("
+#~ msgstr "Signal principal métro de Munich ("
+
+#~ msgid "Next Stop:\n"
+#~ msgstr "Prochain arrêt :\n"
+
+#~ msgid "No callback to handle schedule"
+#~ msgstr "Absence de fonction de gestion de planning"
+
+#~ msgid "Passenger Wagon"
+#~ msgstr "Voiture passager"
+
+#~ msgid "Passenger area"
+#~ msgstr "Voiture Passager"
+
+#~ msgid ""
+#~ "Passive Component Naming Tool\n"
+#~ "\n"
+#~ "Right-click to name a passive component."
+#~ msgstr ""
+#~ "Outil de nommage de composant passif\n"
+#~ "\n"
+#~ "Clic-Droit pour nommer un composant passif."
+
+#~ msgid "Perpendicular Diamond Crossing Track"
+#~ msgstr "Croisement perpendiculaire"
+
+#~ msgid "Point Speed Restriction Track"
+#~ msgstr "Voie de point de limitation de vitesse"
+
+#~ msgid "Point speed restriction: @1"
+#~ msgstr "Point de limitation de vitesse : @1"
+
+#~ msgid "Reduced speed"
+#~ msgstr "Vitesse réduite"
+
+#~ msgid "Restricted speed"
+#~ msgstr "Vitesse limitée"
+
+#~ msgid "Reverse train"
+#~ msgstr "Inversion du sens de marche"
+
+#~ msgid "Right"
+#~ msgstr "Droit"
+
+#~ msgid "Route state changed."
+#~ msgstr "Changement d'état de l'itinéraire."
+
+#~ msgid "Set name of component (empty to clear)"
+#~ msgstr "Nommer le composant (chaîne vide pour effacer)"
+
+#~ msgid "Set point speed restriction:"
+#~ msgstr "Placez un point de limitation de vitesse :"
+
+#~ msgid "Signal"
+#~ msgstr "Signal"
+
+#~ msgid "Speed:"
+#~ msgstr "Vitesse : "
+
+#~ msgid "Station Code"
+#~ msgstr "Code de Station"
+
+#~ msgid "Station Name"
+#~ msgstr "Nom de Station"
+
+#~ msgid "Station code \"@1\" already exists and is owned by @2."
+#~ msgstr "Le code de station \"@1\" existe et est possédé par @2."
+
+#~ msgid "Station/Stop Track"
+#~ msgstr "Voie d'arrêt en station"
+
+#~ msgid "Steam Engine"
+#~ msgstr "Locomotive à vapeur"
+
+#~ msgid "Stop Time"
+#~ msgstr "Durée d'arrêt"
+
+#~ msgid "Subway Passenger Wagon"
+#~ msgstr "Voiture de Métropolitain"
+
+#~ msgid "Target:"
+#~ msgstr "Destination : "
+
+#, fuzzy
+#~ msgid "This node can't be rotated using the trackworker,"
+#~ msgstr "Ce nœud ne peut être tourné avec l'outil \"Trackworker\" !"
+
+#~ msgid "This position is protected!"
+#~ msgstr "Cet emplacement est protégé !"
+
+#~ msgid "This station is owned by @1. You are not allowed to edit its name."
+#~ msgstr ""
+#~ "Cette station est la propriété de @1. Vous n'êtes pas autorisé à modifier "
+#~ "son nom."
+
+#~ msgid "This track can not be changed."
+#~ msgstr "Cette voie ne peut pas être modifiée."
+
+#~ msgid "Track"
+#~ msgstr "Voie"
+
+#~ msgid "Train"
+#~ msgstr "Identificateur du train"
+
+#~ msgid "Train "
+#~ msgstr "Identificateur du train "
+
+#~ msgid "Trains stopping here (ARS rules)"
+#~ msgstr "Trains marquant l'arrêt (règles ARS)"
+
+#~ msgid "Unable to load wagon type"
+#~ msgstr "Impossible de charger le type du wagon"
+
+#~ msgid "Unconfigured LuaATC component"
+#~ msgstr "Composant LuaATC non configuré"
+
+#~ msgid "Uninitialized init="
+#~ msgstr "Variable init non initialisée"
+
+#~ msgid "Unknown Station"
+#~ msgstr "Gare inconnue"
+
+#~ msgid "Unloading Track"
+#~ msgstr "Voie de Déchargement"
+
+#~ msgid "Use Sneak+rightclick to bypass closed doors!"
+#~ msgstr ""
+#~ "Utilisez \"Marcher lentement (Sneak)\" et Clic-Droit pour franchir les "
+#~ "portes closes !"
+
+#~ msgid "Wait for signal to clear"
+#~ msgstr "En attente de signal d'autorisation"
+
+#~ msgid "Y-turnout"
+#~ msgstr "Embranchement en Y"
+
+#~ msgid ""
+#~ "You are not allowed to configure this LuaATC component without the @1 "
+#~ "privilege."
+#~ msgstr "Vous ne pouvez configurer ce composant LuaATC sans le privilege @1."
+
+#~ msgid ""
+#~ "You are not allowed to configure this track without the @1 privilege."
+#~ msgstr ""
+#~ "Vous n'êtes pas autorisé à configurer cette voie sans le privilège @1."
+
+#~ msgid "You are not allowed to configure this track."
+#~ msgstr "Vous n'êtes pas autorisé à configurer cette voie."
+
+#, fuzzy
+#~ msgid "You are not allowed to modify this protected track."
+#~ msgstr "Vous ne pouvez pas construire une voie à cet emplacement protégé"
+
+#~ msgid ""
+#~ "You are not allowed to name LuaATC passive components without the @1 "
+#~ "privilege."
+#~ msgstr ""
+#~ "Vous ne pouvez nommer un composant LuaATC passif sans le privilege @1."
+
+#~ msgid ""
+#~ "You need to own at least one neighboring wagon to destroy this couple."
+#~ msgstr ""
+#~ "Vous devez être propriétaire d'au moins un wagon voisin pour supprimer "
+#~ "cet attelage."
+
+#~ msgid "slowdown"
+#~ msgstr "ralentissement"
diff --git a/advtrains/po/update-translations.sh b/advtrains/po/update-translations.sh
new file mode 100755
index 0000000..3228037
--- /dev/null
+++ b/advtrains/po/update-translations.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+MODNAME="advtrains"
+MSGID_BUGS_ADDR='advtrains-discuss@lists.sr.ht'
+
+PODIR=`dirname "$0"`
+ATDIR="$PODIR/.."
+POTFILE="$PODIR/$MODNAME.pot"
+
+xgettext \
+ -D "$ATDIR" \
+ -d "$MODNAME" \
+ -o "$POTFILE" \
+ -p . \
+ -L lua \
+ --add-location=file \
+ --from-code=UTF-8 \
+ --sort-by-file \
+ --keyword='S' \
+ --package-name="$MODNAME" \
+ --msgid-bugs-address="$MSGID_BUGS_ADDR" \
+ `find $ATDIR $BTDIR -name '*.lua' -printf '%P\n'` \
+ &&
+for i in "$PODIR"/*.po; do
+ msgmerge -U \
+ --sort-by-file \
+ $i "$POTFILE"
+done
diff --git a/advtrains/po/zh_CN.po b/advtrains/po/zh_CN.po
new file mode 100644
index 0000000..4fa4502
--- /dev/null
+++ b/advtrains/po/zh_CN.po
@@ -0,0 +1,863 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: advtrains\n"
+"Report-Msgid-Bugs-To: advtrains-discuss@lists.sr.ht\n"
+"POT-Creation-Date: 2025-06-10 21:40+0200\n"
+"PO-Revision-Date: 2023-10-09 11:24+0200\n"
+"Last-Translator: Y. Wang <yw05@forksworld.de>\n"
+"Language-Team: Chinese (Simplified)\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.3.2\n"
+
+#: atc.lua
+msgid "ATC Kick command warning: doors are closed."
+msgstr "ATC 警告:车门已关闭,无法踢出乘客。"
+
+#: atc.lua
+msgid "ATC Kick command warning: train moving."
+msgstr "ATC 警告:火车正在移动,无法踢出乘客。"
+
+#: atc.lua
+msgid "ATC Reverse command warning: didn't reverse train, train moving."
+msgstr "ATC 警告:火车正在移动,无法改变行车方向。"
+
+#: atc.lua
+msgid "ATC command parse error: Unknown command: @1"
+msgstr "ATC 语法错误:未知命令:@1"
+
+#: atc.lua
+msgid "ATC command syntax error: I statement not closed: @1"
+msgstr "ATC 语法错误:“I”命令不完整:@1"
+
+#: atc.lua
+#, fuzzy
+msgid "ATC controller, Command: @1"
+msgstr ""
+"ATC 控制器\n"
+"模式:@1\n"
+"命令:@2"
+
+#: atc.lua
+msgid "Command"
+msgstr "命令"
+
+#: atc.lua
+msgid "Digiline channel"
+msgstr "Digiline 频道"
+
+#: atc.lua wagons.lua
+msgid "Save"
+msgstr "保存"
+
+#: atc.lua
+msgid "Unconfigured ATC controller"
+msgstr "ATC 控制器 (未配置)"
+
+#: copytool.lua
+msgid "Back of train would end up off track, cancelling."
+msgstr "火车后部不在轨道上。"
+
+#: copytool.lua
+msgid "No such lua entity."
+msgstr "您没有指向一个可以用火车复制工具复制的物体。"
+
+#: copytool.lua
+msgid "No such train: @1."
+msgstr "ID 为“@1”的列车不存在。"
+
+#: copytool.lua
+msgid "No such wagon: @1."
+msgstr "ID 为“@1”的车厢不存在。"
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Copy failed."
+msgstr "无法复制:剪贴板无法访问元数据。"
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Paste failed."
+msgstr "无法粘贴:剪贴板无法访问元数据。"
+
+#: copytool.lua
+msgid "The clipboard is empty."
+msgstr "剪贴板是空的。"
+
+#: copytool.lua
+msgid "The track you are trying to place the wagon on is not long enough."
+msgstr "轨道太短。"
+
+#: copytool.lua
+msgid "Train copied."
+msgstr "已复制列车。"
+
+#: copytool.lua
+msgid ""
+"Train copy/paste tool\n"
+"\n"
+"Left-click: copy train\n"
+"Right-click: paste train"
+msgstr ""
+"火车复制工具\n"
+"\n"
+"左键单击:复制\n"
+"右键单击:粘帖"
+
+#: copytool.lua
+msgid "You do not have the @1 privilege."
+msgstr "您没有“@1”权限。"
+
+#: couple.lua
+msgid "<No coupler>"
+msgstr "<没有车钩>"
+
+#: couple.lua
+msgid "Buffer and Chain Coupler"
+msgstr "链式车钩"
+
+#: couple.lua
+msgid "Can not couple: The couplers of the trains do not match (@1 and @2)."
+msgstr "您无法连接这两节车厢:这两节车厢使用不同的车钩 (@1和@2)。"
+
+#: couple.lua
+msgid ""
+"Cannot couple @1 and @2 - train would have length @3 which is above the "
+"limit of @4"
+msgstr ""
+
+#: couple.lua
+msgid "Scharfenberg Coupler"
+msgstr "Scharfenberg 式车钩"
+
+#: couple.lua
+msgid ""
+"You are not allowed to couple trains without the train_operator privilege."
+msgstr "您没有“train_operator”权限,不能连接这两节车厢。"
+
+#: craft_items.lua
+msgid "Boiler"
+msgstr "锅炉"
+
+#: craft_items.lua
+msgid "Chimney"
+msgstr "烟囱"
+
+#: craft_items.lua
+msgid "Driver's cab"
+msgstr "驾驶室"
+
+#: craft_items.lua
+msgid "Wheel"
+msgstr "车轮"
+
+#: init.lua
+msgid "Advtrains Status: no_action @1 slowdown @2 (log @3)"
+msgstr ""
+
+#: init.lua
+msgid "Advtrains is already running normally!"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Crash during advtrains main step - skipping the shutdown save operation to "
+"not save inconsistent data!"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Data is being saved. While saving, advtrains will remove the players from "
+"trains. Save files will be reloaded afterwards!"
+msgstr ""
+
+#: init.lua
+msgid "Delete all train routes, force them to recalculate"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Detach all players, especially the offline ones, from all trains. Use only "
+"when no one serious is on a train."
+msgstr ""
+
+#: init.lua
+msgid "Disable the advtrains globalstep temporarily"
+msgstr ""
+
+#: init.lua
+msgid "Disabled advtrains successfully"
+msgstr ""
+
+#: init.lua
+msgid "Instructed to save() but load() was never called!"
+msgstr ""
+
+#: init.lua
+msgid "Print advtrains status info"
+msgstr ""
+
+#: init.lua
+msgid "Re-enabling advtrains globalstep..."
+msgstr ""
+
+#: init.lua
+msgid "Reload successful!"
+msgstr ""
+
+#: init.lua
+msgid "Removing unused wagon"
+msgstr ""
+
+#: init.lua
+msgid "Restoring saved state in 1 second..."
+msgstr ""
+
+#: init.lua
+msgid "Returns the position of the train with the given id"
+msgstr ""
+
+#: init.lua
+msgid "Saving failed: @1"
+msgstr ""
+
+#: init.lua
+msgid "Successfully invalidated train routes"
+msgstr ""
+
+#: init.lua
+msgid "Teleporting to train @1"
+msgstr ""
+
+#: init.lua
+msgid "Teleports you to the position of the train with the given id"
+msgstr ""
+
+#: init.lua
+msgid ""
+"The advtrains globalstep has been disabled. Trains are not moving, and no "
+"data is saved! Run '/at_disable_step no' to enable again!"
+msgstr ""
+
+#: init.lua
+msgid "Train @1 does not exist or is invalid"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Train @1 had no wagons left because of some bug. It is being deleted. Wave "
+"it goodbye!"
+msgstr ""
+
+#: init.lua
+msgid "Train @1 is at @2"
+msgstr ""
+
+#: init.lua
+msgid "from wagon_save table."
+msgstr ""
+
+#: misc_nodes.lua
+msgid "@1 Platform (45 degree)"
+msgstr "较高的@1站台 (45°)"
+
+#: misc_nodes.lua
+msgid "@1 Platform (high)"
+msgstr "较高的@1站台"
+
+#: misc_nodes.lua
+msgid "@1 Platform (low)"
+msgstr "较低的@1站台"
+
+#: misc_nodes.lua
+msgid "@1 Platform (low, 45 degree)"
+msgstr "较低的@1站台 (45°)"
+
+#: protection.lua
+msgid "Can operate turnouts and signals in unprotected areas"
+msgstr ""
+
+#: protection.lua
+msgid "Can place and dig tracks in unprotected areas"
+msgstr ""
+
+#: protection.lua
+msgid ""
+"Can place, remove and operate any train, regardless of owner, whitelist, or "
+"protection"
+msgstr ""
+
+#: protection.lua
+msgid "Can place, remove and operate trains"
+msgstr ""
+
+#: protection.lua
+msgid "You are not allowed to build near tracks at this protected position."
+msgstr "这里已被保护,您不能在这里的铁路附近建任何东西。"
+
+#: protection.lua
+msgid ""
+"You are not allowed to build near tracks without the track_builder privilege."
+msgstr "您没有“train_operator”权限,不能在铁路附近建任何东西。"
+
+#: protection.lua
+msgid "You are not allowed to build tracks at this protected position."
+msgstr "这里已被保护,您不能在这里建造铁路。"
+
+#: protection.lua
+msgid ""
+"You are not allowed to build tracks without the track_builder privilege."
+msgstr "您没有“train_operator”权限,不能在这里建造铁路。"
+
+#: protection.lua
+msgid ""
+"You are not allowed to operate turnouts and signals without the "
+"railway_operator privilege."
+msgstr "您没有“railway_operator”权限,不能控制铁路设施。"
+
+#: signals.lua
+msgid "Andrew's Cross"
+msgstr "铁路道口信号灯"
+
+#: signals.lua
+#, fuzzy
+msgid "Lampless Signal (deprecated!)"
+msgstr "臂板信号机"
+
+#: signals.lua
+msgid "Signal (deprecated!)"
+msgstr ""
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (left) (deprecated!)"
+msgstr "壁挂式信号灯 (左侧)"
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (right) (deprecated!)"
+msgstr "壁挂式信号灯 (右侧)"
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (top) (deprecated!)"
+msgstr "悬挂式信号灯"
+
+#: track_reg_helper.lua
+msgid "@1 Slope"
+msgstr "@1斜坡"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: Not enough slope items left (@1 required)"
+msgstr "无法放置斜坡:您没有足够的铁路斜坡放置工具 (您总共需要@1个)"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: There's no slope of length @1"
+msgstr "无法放置斜坡:advtrains 不支持长度为@1米的斜坡。"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: no supporting node at upper end."
+msgstr "无法放置斜坡:较高端没有支撑方块。"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: not pointing at node"
+msgstr "无法放置斜坡:您没有选择任何方块。"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: space occupied!"
+msgstr "无法放置斜坡:此区域已被占用。"
+
+#: track_reg_helper.lua tracks.lua
+#, fuzzy
+msgid "This track can not be removed!"
+msgstr "您不能移除这段轨道。"
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This node can't be changed using the trackworker!"
+msgstr "您不能使用铁路调整工具调整这个方块。"
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This node can't be rotated using the trackworker!"
+msgstr "您不能使用铁路调整工具旋转这个方块。"
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This track can not be rotated!"
+msgstr "您不能旋转这段轨道。"
+
+#: trackplacer.lua
+msgid ""
+"Track Worker Tool\n"
+"\n"
+"Left-click: change rail type (straight/curve/switch)\n"
+"Right-click: rotate object"
+msgstr ""
+"铁路调整工具\n"
+"\n"
+"左键单击:切换轨道类型\n"
+"右键单击:旋转方块"
+
+#: tracks.lua
+msgid "Position is occupied by a train."
+msgstr ""
+
+#: tracks.lua
+msgid "There's a Signal Influence Point here."
+msgstr ""
+
+#: tracks.lua
+msgid "There's a Track Circuit Break here."
+msgstr ""
+
+#: trainhud.lua
+msgid "OVERRUN RED SIGNAL! Examine situation and reverse train to move again."
+msgstr ""
+
+#: wagonprop_tool.lua
+msgid "Insufficient privileges to use this!"
+msgstr ""
+
+#: wagonprop_tool.lua
+#, fuzzy
+msgid "Wagon Properties Tool"
+msgstr "车厢属性"
+
+#: wagonprop_tool.lua
+msgid ""
+"Wagon Properties Tool\n"
+"Punch a wagon to view and edit the Wagon Properties"
+msgstr ""
+
+#: wagons.lua
+msgid " units"
+msgstr ""
+
+#: wagons.lua
+msgid "!!! Train off track !!!"
+msgstr ""
+
+#: wagons.lua
+msgid "(Doors closed)"
+msgstr "(车门已关闭)"
+
+#: wagons.lua
+msgid "Allow these players to access your wagon:"
+msgstr ""
+
+#: wagons.lua
+msgid "Clear 'Disable ARS' flag"
+msgstr ""
+
+#: wagons.lua
+msgid "Current FC: "
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Destroying wagon with inventory, but inventory is not found? Shouldn't "
+"happen!"
+msgstr ""
+
+#: wagons.lua
+msgid "Doors are closed! (Try holding sneak key!)"
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Doors are closed. Use Sneak+rightclick to ignore the closed doors and get "
+"off."
+msgstr "车门已关闭,请使用潜行+右键单击下车。"
+
+#: wagons.lua
+msgid "Freight Code:"
+msgstr ""
+
+#: wagons.lua
+msgid "Get off"
+msgstr "下车"
+
+#: wagons.lua
+msgid "Get off (forced)"
+msgstr "强制下车"
+
+#: wagons.lua
+msgid "Line"
+msgstr "火车线路"
+
+#: wagons.lua
+msgid "Liquid: "
+msgstr ""
+
+#: wagons.lua
+msgid "Liquid: empty"
+msgstr ""
+
+#: wagons.lua
+msgid "Missing train_operator privilege"
+msgstr ""
+
+#: wagons.lua
+msgid "Next FC:"
+msgstr ""
+
+#: wagons.lua
+msgid "Not a valid wagon id."
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "Not allowed to do this."
+msgstr "您不能调整这段轨道。"
+
+#: wagons.lua
+msgid "Onboard Computer"
+msgstr ""
+
+#: wagons.lua
+msgid "Please specify a player name to transfer ownership to."
+msgstr ""
+
+#: wagons.lua
+msgid "Prev FC"
+msgstr ""
+
+#: wagons.lua
+msgid "Remote Routesetting"
+msgstr ""
+
+#: wagons.lua
+msgid "Routingcode"
+msgstr "路由码"
+
+#: wagons.lua
+msgid "Save wagon properties"
+msgstr "保存车厢属性"
+
+#: wagons.lua
+msgid "Select seat:"
+msgstr "请选择座位:"
+
+#: wagons.lua
+msgid "Show Inventory"
+msgstr "显示物品栏"
+
+#: wagons.lua
+msgid "Text displayed inside train"
+msgstr "车厢内部显示"
+
+#: wagons.lua
+msgid "Text displayed outside on train"
+msgstr "车厢外部显示"
+
+#: wagons.lua
+msgid "That player does not exist!"
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "That wagon does not exist!"
+msgstr "这节车厢没有座位。"
+
+#: wagons.lua
+#, fuzzy
+msgid "The track you are trying to place the wagon on is not long enough!"
+msgstr "轨道太短。"
+
+#: wagons.lua
+msgid "The wagon's inventory is not empty."
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "This Wagon ID"
+msgstr "车厢已满。"
+
+#: wagons.lua
+msgid "This wagon has no seats."
+msgstr "这节车厢没有座位。"
+
+#: wagons.lua
+msgid "This wagon is full."
+msgstr "车厢已满。"
+
+#: wagons.lua
+msgid "This wagon is owned by @1, you can't destroy it."
+msgstr "这是 @1 的车厢,您不能摧毁它。"
+
+#: wagons.lua
+msgid "Train ID"
+msgstr ""
+
+#: wagons.lua
+msgid "Train overview / coupling control is only shown when the train stands."
+msgstr ""
+
+#: wagons.lua
+msgid "Train overview /coupling control:"
+msgstr ""
+
+#: wagons.lua
+msgid "Uninitialized, removing"
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon @1 ownership changed from @2 to @3"
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon needs to be decoupled from other wagons in order to destroy it."
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "Wagon placeholder"
+msgstr "车厢属性"
+
+#: wagons.lua
+msgid "Wagon properties"
+msgstr "车厢属性"
+
+#: wagons.lua
+msgid "Wagon road number:"
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Warning: If you destroy this wagon, you only get some steel back! If you are "
+"sure, hold Sneak and left-click the wagon."
+msgstr ""
+"警告:如果您摧毁此车厢,您只能拿到一些钢方块。如果您确定要摧毁这节车厢,请按"
+"潜行键并左键单击此车厢。"
+
+#: wagons.lua
+msgid "You are not allowed to access the driver stand."
+msgstr ""
+
+#: wagons.lua
+msgid "You can't get on this wagon."
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "You don't have the train_operator privilege."
+msgstr "您没有“@1”权限。"
+
+#: wagons.lua
+msgid "You have been given ownership of wagon @1"
+msgstr ""
+
+#~ msgid "3-way turnout"
+#~ msgstr "三开道岔"
+
+#~ msgid "90+Angle Diamond Crossing Track"
+#~ msgstr "交叉轨道 (其中一条轨道与坐标轴平行)"
+
+#~ msgid "ATC controller"
+#~ msgstr "ATC 控制器"
+
+#~ msgid ""
+#~ "ATC controller, mode @1\n"
+#~ "Channel: @2"
+#~ msgstr ""
+#~ "ATC 控制器\n"
+#~ "模式:@1\n"
+#~ "频道:@2"
+
+#~ msgid "Access to @1"
+#~ msgstr "可前往@1"
+
+#~ msgid "Big Industrial Train Engine"
+#~ msgstr "大型工业用火车头"
+
+#~ msgid "Box Wagon"
+#~ msgstr "货运车厢"
+
+#~ msgid "Bumper"
+#~ msgstr "保险杠"
+
+#~ msgid "Can't get on: wagon full or doors closed!"
+#~ msgstr "无法上车:车门已关闭或车厢已满。"
+
+#~ msgid "Can't place: protected position!"
+#~ msgstr "无法放置:此区域已被保护。"
+
+#~ msgid "Command (on)"
+#~ msgstr "命令 (激活时)"
+
+#~ msgid "Default Seat"
+#~ msgstr "默认座位"
+
+#~ msgid "Default Seat (driver stand)"
+#~ msgstr "默认座位 (司机座位)"
+
+#~ msgid "Dep. Speed"
+#~ msgstr "出发速度"
+
+#~ msgid "Deprecated Track"
+#~ msgstr "请不要使用"
+
+#~ msgid "Detailed Steam Engine"
+#~ msgstr "精细的蒸汽机车"
+
+#~ msgid "Detector Rail"
+#~ msgstr "探测轨道"
+
+#~ msgid "Diagonal Diamond Crossing Track"
+#~ msgstr "交叉轨道"
+
+#~ msgid "Door Delay"
+#~ msgstr "车门关闭时间"
+
+#, fuzzy
+#~ msgid "Driver Stand"
+#~ msgstr "司机座位"
+
+#~ msgid "Driver Stand (left)"
+#~ msgstr "左侧司机座位"
+
+#~ msgid "Driver Stand (right)"
+#~ msgstr "右侧司机座位"
+
+#~ msgid "Driver stand"
+#~ msgstr "司机座位"
+
+#~ msgid "Industrial Train Engine"
+#~ msgstr "工业用火车头"
+
+#~ msgid "Industrial tank wagon"
+#~ msgstr "液体运输车厢"
+
+#~ msgid "Industrial wood wagon"
+#~ msgstr "木材运输车厢"
+
+#~ msgid "Japanese Train Engine"
+#~ msgstr "高速列车车头"
+
+#~ msgid "Japanese Train Inter-Wagon Connection"
+#~ msgstr "日本火车车钩"
+
+#~ msgid "Japanese Train Wagon"
+#~ msgstr "高速列车车厢"
+
+#, fuzzy
+#~ msgid "Japanese signal pole"
+#~ msgstr "高速列车车厢"
+
+#~ msgid "Kick out passengers"
+#~ msgstr "踢出乘客"
+
+#~ msgid "Loading Track"
+#~ msgstr "装货轨道"
+
+#~ msgid "Lock couples"
+#~ msgstr "锁定连接处"
+
+#~ msgid "Passenger Wagon"
+#~ msgstr "客车"
+
+#, fuzzy
+#~ msgid "Passenger area"
+#~ msgstr "客车"
+
+#~ msgid ""
+#~ "Passive Component Naming Tool\n"
+#~ "\n"
+#~ "Right-click to name a passive component."
+#~ msgstr ""
+#~ "被动元件命名工具\n"
+#~ "\n"
+#~ "右键单击命名所选元件。"
+
+#~ msgid "Perpendicular Diamond Crossing Track"
+#~ msgstr "垂直交叉轨道"
+
+#~ msgid "Reverse train"
+#~ msgstr "改变行车方向"
+
+#~ msgid "Signal"
+#~ msgstr "信号灯"
+
+#~ msgid "Speed:"
+#~ msgstr "速度"
+
+#~ msgid "Station Code"
+#~ msgstr "车站代码"
+
+#~ msgid "Station Name"
+#~ msgstr "车站名称"
+
+#~ msgid "Station/Stop Track"
+#~ msgstr "车站轨道"
+
+#~ msgid "Steam Engine"
+#~ msgstr "蒸汽机车"
+
+#~ msgid "Stop Time"
+#~ msgstr "停站时间"
+
+#~ msgid "Subway Passenger Wagon"
+#~ msgstr "地铁车厢"
+
+#~ msgid "Target:"
+#~ msgstr "目标速度"
+
+#, fuzzy
+#~ msgid "This node can't be rotated using the trackworker,"
+#~ msgstr "您不能使用铁路调整工具旋转这个方块。"
+
+#~ msgid "This position is protected!"
+#~ msgstr "这里已被保护。"
+
+#~ msgid "This track can not be changed."
+#~ msgstr "您不能调整这段轨道。"
+
+#~ msgid "Track"
+#~ msgstr "轨道"
+
+#, fuzzy
+#~ msgid "Train "
+#~ msgstr "已复制列车。"
+
+#~ msgid "Unconfigured LuaATC component"
+#~ msgstr "LuaATC 部件 (未配置)"
+
+#~ msgid "Unloading Track"
+#~ msgstr "卸货轨道"
+
+#~ msgid "Use Sneak+rightclick to bypass closed doors!"
+#~ msgstr "请使用潜行+右键上车。"
+
+#~ msgid "Y-turnout"
+#~ msgstr "对称道岔"
+
+#~ msgid ""
+#~ "You are not allowed to configure this LuaATC component without the @1 "
+#~ "privilege."
+#~ msgstr "您没有“@1”权限,不能配置这个 LuaATC 部件。"
+
+#~ msgid ""
+#~ "You are not allowed to configure this track without the @1 privilege."
+#~ msgstr "您没有“@1”权限,不能调整这段轨道。"
+
+#~ msgid "You are not allowed to configure this track."
+#~ msgstr "您不能调整这段轨道。"
+
+#, fuzzy
+#~ msgid "You are not allowed to modify this protected track."
+#~ msgstr "这里已被保护,您不能在这里建造铁路。"
+
+#~ msgid ""
+#~ "You are not allowed to name LuaATC passive components without the @1 "
+#~ "privilege."
+#~ msgstr "您没有“@1”权限,不能命名被动元件。"
+
+#~ msgid ""
+#~ "You need to own at least one neighboring wagon to destroy this couple."
+#~ msgstr "您必须至少拥有其中一节车厢才能分开这两节车厢。"
diff --git a/advtrains/po/zh_TW.po b/advtrains/po/zh_TW.po
new file mode 100644
index 0000000..b46de64
--- /dev/null
+++ b/advtrains/po/zh_TW.po
@@ -0,0 +1,863 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: advtrains\n"
+"Report-Msgid-Bugs-To: advtrains-discuss@lists.sr.ht\n"
+"POT-Creation-Date: 2025-06-10 21:40+0200\n"
+"PO-Revision-Date: 2023-10-09 11:31+0200\n"
+"Last-Translator: Y. Wang <yw05@forksworld.de>\n"
+"Language-Team: Chinese (Traditional)\n"
+"Language: zh_TW\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.3.2\n"
+
+#: atc.lua
+msgid "ATC Kick command warning: doors are closed."
+msgstr "ATC 警告:車門已關閉,無法踢出乘客。"
+
+#: atc.lua
+msgid "ATC Kick command warning: train moving."
+msgstr "ATC 警告:火車正在移動,無法踢出乘客。"
+
+#: atc.lua
+msgid "ATC Reverse command warning: didn't reverse train, train moving."
+msgstr "ATC 警告:火車正在移動,無法改變行車方向。"
+
+#: atc.lua
+msgid "ATC command parse error: Unknown command: @1"
+msgstr "ATC 語法錯誤:未知命令:@1"
+
+#: atc.lua
+msgid "ATC command syntax error: I statement not closed: @1"
+msgstr "ATC 語法錯誤:「I」命令不完整:@1"
+
+#: atc.lua
+#, fuzzy
+msgid "ATC controller, Command: @1"
+msgstr ""
+"ATC 控制器\n"
+"模式:@1\n"
+"命令:@2"
+
+#: atc.lua
+msgid "Command"
+msgstr "命令"
+
+#: atc.lua
+msgid "Digiline channel"
+msgstr "Digiline 頻道"
+
+#: atc.lua wagons.lua
+msgid "Save"
+msgstr "儲存"
+
+#: atc.lua
+msgid "Unconfigured ATC controller"
+msgstr "ATC 控制器 (未配置)"
+
+#: copytool.lua
+msgid "Back of train would end up off track, cancelling."
+msgstr "火車後部不在軌道上。"
+
+#: copytool.lua
+msgid "No such lua entity."
+msgstr "您沒有指向一個可以用火車複製工具複製的物體。"
+
+#: copytool.lua
+msgid "No such train: @1."
+msgstr "ID 為「@1」的列車不存在。"
+
+#: copytool.lua
+msgid "No such wagon: @1."
+msgstr "ID 為「@1」的車廂不存在。"
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Copy failed."
+msgstr "無法複製:剪貼簿無法訪問元資料。"
+
+#: copytool.lua
+msgid "The clipboard couldn't access the metadata. Paste failed."
+msgstr "無法貼上:剪貼簿無法訪問元資料。"
+
+#: copytool.lua
+msgid "The clipboard is empty."
+msgstr "剪貼簿是空的。"
+
+#: copytool.lua
+msgid "The track you are trying to place the wagon on is not long enough."
+msgstr "軌道太短。"
+
+#: copytool.lua
+msgid "Train copied."
+msgstr "已複製火車。"
+
+#: copytool.lua
+msgid ""
+"Train copy/paste tool\n"
+"\n"
+"Left-click: copy train\n"
+"Right-click: paste train"
+msgstr ""
+"火車複製工具\n"
+"\n"
+"左鍵單擊:複製\n"
+"右鍵單擊:粘帖"
+
+#: copytool.lua
+msgid "You do not have the @1 privilege."
+msgstr "您沒有「@1」許可權。"
+
+#: couple.lua
+msgid "<No coupler>"
+msgstr "<無連結器>"
+
+#: couple.lua
+msgid "Buffer and Chain Coupler"
+msgstr "鏈式連結器"
+
+#: couple.lua
+msgid "Can not couple: The couplers of the trains do not match (@1 and @2)."
+msgstr "您無法連結這兩節車廂:這兩節車廂使用不同的連結器 (@1和@2)。"
+
+#: couple.lua
+msgid ""
+"Cannot couple @1 and @2 - train would have length @3 which is above the "
+"limit of @4"
+msgstr ""
+
+#: couple.lua
+msgid "Scharfenberg Coupler"
+msgstr "Scharfenberg 式連結器"
+
+#: couple.lua
+msgid ""
+"You are not allowed to couple trains without the train_operator privilege."
+msgstr "您沒有「train_operator」許可權,不能連結這兩節車廂。"
+
+#: craft_items.lua
+msgid "Boiler"
+msgstr "鍋爐"
+
+#: craft_items.lua
+msgid "Chimney"
+msgstr "煙囪"
+
+#: craft_items.lua
+msgid "Driver's cab"
+msgstr "駕駛室"
+
+#: craft_items.lua
+msgid "Wheel"
+msgstr "車輪"
+
+#: init.lua
+msgid "Advtrains Status: no_action @1 slowdown @2 (log @3)"
+msgstr ""
+
+#: init.lua
+msgid "Advtrains is already running normally!"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Crash during advtrains main step - skipping the shutdown save operation to "
+"not save inconsistent data!"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Data is being saved. While saving, advtrains will remove the players from "
+"trains. Save files will be reloaded afterwards!"
+msgstr ""
+
+#: init.lua
+msgid "Delete all train routes, force them to recalculate"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Detach all players, especially the offline ones, from all trains. Use only "
+"when no one serious is on a train."
+msgstr ""
+
+#: init.lua
+msgid "Disable the advtrains globalstep temporarily"
+msgstr ""
+
+#: init.lua
+msgid "Disabled advtrains successfully"
+msgstr ""
+
+#: init.lua
+msgid "Instructed to save() but load() was never called!"
+msgstr ""
+
+#: init.lua
+msgid "Print advtrains status info"
+msgstr ""
+
+#: init.lua
+msgid "Re-enabling advtrains globalstep..."
+msgstr ""
+
+#: init.lua
+msgid "Reload successful!"
+msgstr ""
+
+#: init.lua
+msgid "Removing unused wagon"
+msgstr ""
+
+#: init.lua
+msgid "Restoring saved state in 1 second..."
+msgstr ""
+
+#: init.lua
+msgid "Returns the position of the train with the given id"
+msgstr ""
+
+#: init.lua
+msgid "Saving failed: @1"
+msgstr ""
+
+#: init.lua
+msgid "Successfully invalidated train routes"
+msgstr ""
+
+#: init.lua
+msgid "Teleporting to train @1"
+msgstr ""
+
+#: init.lua
+msgid "Teleports you to the position of the train with the given id"
+msgstr ""
+
+#: init.lua
+msgid ""
+"The advtrains globalstep has been disabled. Trains are not moving, and no "
+"data is saved! Run '/at_disable_step no' to enable again!"
+msgstr ""
+
+#: init.lua
+msgid "Train @1 does not exist or is invalid"
+msgstr ""
+
+#: init.lua
+msgid ""
+"Train @1 had no wagons left because of some bug. It is being deleted. Wave "
+"it goodbye!"
+msgstr ""
+
+#: init.lua
+msgid "Train @1 is at @2"
+msgstr ""
+
+#: init.lua
+msgid "from wagon_save table."
+msgstr ""
+
+#: misc_nodes.lua
+msgid "@1 Platform (45 degree)"
+msgstr "較高的@1月臺 (45°)"
+
+#: misc_nodes.lua
+msgid "@1 Platform (high)"
+msgstr "較高的@1月臺"
+
+#: misc_nodes.lua
+msgid "@1 Platform (low)"
+msgstr "較低的@1月臺"
+
+#: misc_nodes.lua
+msgid "@1 Platform (low, 45 degree)"
+msgstr "較低的@1月臺 (45°)"
+
+#: protection.lua
+msgid "Can operate turnouts and signals in unprotected areas"
+msgstr ""
+
+#: protection.lua
+msgid "Can place and dig tracks in unprotected areas"
+msgstr ""
+
+#: protection.lua
+msgid ""
+"Can place, remove and operate any train, regardless of owner, whitelist, or "
+"protection"
+msgstr ""
+
+#: protection.lua
+msgid "Can place, remove and operate trains"
+msgstr ""
+
+#: protection.lua
+msgid "You are not allowed to build near tracks at this protected position."
+msgstr "這裡已被保護,您不能在這裡的鐵路附近建任何東西。"
+
+#: protection.lua
+msgid ""
+"You are not allowed to build near tracks without the track_builder privilege."
+msgstr "您沒有「train_operator」許可權,不能在鐵路附近建任何東西。"
+
+#: protection.lua
+msgid "You are not allowed to build tracks at this protected position."
+msgstr "這裡已被保護,您不能在這裡建造鐵路。"
+
+#: protection.lua
+msgid ""
+"You are not allowed to build tracks without the track_builder privilege."
+msgstr "您沒有「train_operator」許可權,不能在這裡建造鐵路。"
+
+#: protection.lua
+msgid ""
+"You are not allowed to operate turnouts and signals without the "
+"railway_operator privilege."
+msgstr "您沒有「railway_operator」許可權,不能控制鐵路設施。"
+
+#: signals.lua
+msgid "Andrew's Cross"
+msgstr "平交道號誌燈"
+
+#: signals.lua
+#, fuzzy
+msgid "Lampless Signal (deprecated!)"
+msgstr "臂木式號誌機"
+
+#: signals.lua
+msgid "Signal (deprecated!)"
+msgstr ""
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (left) (deprecated!)"
+msgstr "壁掛式色燈號誌機 (左側)"
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (right) (deprecated!)"
+msgstr "壁掛式色燈號誌機 (右側)"
+
+#: signals.lua
+#, fuzzy
+msgid "Wallmounted Signal (top) (deprecated!)"
+msgstr "懸掛式色燈號誌機"
+
+#: track_reg_helper.lua
+msgid "@1 Slope"
+msgstr "@1斜坡"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: Not enough slope items left (@1 required)"
+msgstr "無法放置斜坡:您沒有足夠的鐵路斜坡放置工具 (您總共需要@1個)"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: There's no slope of length @1"
+msgstr "無法放置斜坡:advtrains 不支援長度為@1米的斜坡。"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: no supporting node at upper end."
+msgstr "無法放置斜坡:較高階沒有支撐方塊。"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: not pointing at node"
+msgstr "無法放置斜坡:您沒有選擇任何方塊。"
+
+#: track_reg_helper.lua
+#, fuzzy
+msgid "Can't place: space occupied!"
+msgstr "無法放置斜坡:此區域已被佔用。"
+
+#: track_reg_helper.lua tracks.lua
+#, fuzzy
+msgid "This track can not be removed!"
+msgstr "您不能移除這段軌道。"
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This node can't be changed using the trackworker!"
+msgstr "您不能使用鐵路調整工具調整這個方塊。"
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This node can't be rotated using the trackworker!"
+msgstr "您不能使用鐵路調整工具旋轉這個方塊。"
+
+#: trackplacer.lua
+#, fuzzy
+msgid "This track can not be rotated!"
+msgstr "您不能旋轉這段軌道。"
+
+#: trackplacer.lua
+msgid ""
+"Track Worker Tool\n"
+"\n"
+"Left-click: change rail type (straight/curve/switch)\n"
+"Right-click: rotate object"
+msgstr ""
+"鐵路調整工具\n"
+"\n"
+"左鍵單擊:切換軌道型別\n"
+"右鍵單擊:旋轉方塊"
+
+#: tracks.lua
+msgid "Position is occupied by a train."
+msgstr ""
+
+#: tracks.lua
+msgid "There's a Signal Influence Point here."
+msgstr ""
+
+#: tracks.lua
+msgid "There's a Track Circuit Break here."
+msgstr ""
+
+#: trainhud.lua
+msgid "OVERRUN RED SIGNAL! Examine situation and reverse train to move again."
+msgstr ""
+
+#: wagonprop_tool.lua
+msgid "Insufficient privileges to use this!"
+msgstr ""
+
+#: wagonprop_tool.lua
+#, fuzzy
+msgid "Wagon Properties Tool"
+msgstr "車廂屬性"
+
+#: wagonprop_tool.lua
+msgid ""
+"Wagon Properties Tool\n"
+"Punch a wagon to view and edit the Wagon Properties"
+msgstr ""
+
+#: wagons.lua
+msgid " units"
+msgstr ""
+
+#: wagons.lua
+msgid "!!! Train off track !!!"
+msgstr ""
+
+#: wagons.lua
+msgid "(Doors closed)"
+msgstr "(車門已關閉)"
+
+#: wagons.lua
+msgid "Allow these players to access your wagon:"
+msgstr ""
+
+#: wagons.lua
+msgid "Clear 'Disable ARS' flag"
+msgstr ""
+
+#: wagons.lua
+msgid "Current FC: "
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Destroying wagon with inventory, but inventory is not found? Shouldn't "
+"happen!"
+msgstr ""
+
+#: wagons.lua
+msgid "Doors are closed! (Try holding sneak key!)"
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Doors are closed. Use Sneak+rightclick to ignore the closed doors and get "
+"off."
+msgstr "車門已關閉,請使用潛行+右鍵單擊下車。"
+
+#: wagons.lua
+msgid "Freight Code:"
+msgstr ""
+
+#: wagons.lua
+msgid "Get off"
+msgstr "下車"
+
+#: wagons.lua
+msgid "Get off (forced)"
+msgstr "強制下車"
+
+#: wagons.lua
+msgid "Line"
+msgstr "火車線路"
+
+#: wagons.lua
+msgid "Liquid: "
+msgstr ""
+
+#: wagons.lua
+msgid "Liquid: empty"
+msgstr ""
+
+#: wagons.lua
+msgid "Missing train_operator privilege"
+msgstr ""
+
+#: wagons.lua
+msgid "Next FC:"
+msgstr ""
+
+#: wagons.lua
+msgid "Not a valid wagon id."
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "Not allowed to do this."
+msgstr "您不能調整這段軌道。"
+
+#: wagons.lua
+msgid "Onboard Computer"
+msgstr ""
+
+#: wagons.lua
+msgid "Please specify a player name to transfer ownership to."
+msgstr ""
+
+#: wagons.lua
+msgid "Prev FC"
+msgstr ""
+
+#: wagons.lua
+msgid "Remote Routesetting"
+msgstr ""
+
+#: wagons.lua
+msgid "Routingcode"
+msgstr "路由碼"
+
+#: wagons.lua
+msgid "Save wagon properties"
+msgstr "儲存車廂屬性"
+
+#: wagons.lua
+msgid "Select seat:"
+msgstr "請選擇座位:"
+
+#: wagons.lua
+msgid "Show Inventory"
+msgstr "顯示物品欄"
+
+#: wagons.lua
+msgid "Text displayed inside train"
+msgstr "車廂內部顯示"
+
+#: wagons.lua
+msgid "Text displayed outside on train"
+msgstr "車廂外部顯示"
+
+#: wagons.lua
+msgid "That player does not exist!"
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "That wagon does not exist!"
+msgstr "這節車廂沒有座位。"
+
+#: wagons.lua
+#, fuzzy
+msgid "The track you are trying to place the wagon on is not long enough!"
+msgstr "軌道太短。"
+
+#: wagons.lua
+msgid "The wagon's inventory is not empty."
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "This Wagon ID"
+msgstr "車廂已滿。"
+
+#: wagons.lua
+msgid "This wagon has no seats."
+msgstr "這節車廂沒有座位。"
+
+#: wagons.lua
+msgid "This wagon is full."
+msgstr "車廂已滿。"
+
+#: wagons.lua
+msgid "This wagon is owned by @1, you can't destroy it."
+msgstr "這是 @1 的車廂,您不能摧毀它。"
+
+#: wagons.lua
+msgid "Train ID"
+msgstr ""
+
+#: wagons.lua
+msgid "Train overview / coupling control is only shown when the train stands."
+msgstr ""
+
+#: wagons.lua
+msgid "Train overview /coupling control:"
+msgstr ""
+
+#: wagons.lua
+msgid "Uninitialized, removing"
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon @1 ownership changed from @2 to @3"
+msgstr ""
+
+#: wagons.lua
+msgid "Wagon needs to be decoupled from other wagons in order to destroy it."
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "Wagon placeholder"
+msgstr "車廂屬性"
+
+#: wagons.lua
+msgid "Wagon properties"
+msgstr "車廂屬性"
+
+#: wagons.lua
+msgid "Wagon road number:"
+msgstr ""
+
+#: wagons.lua
+msgid ""
+"Warning: If you destroy this wagon, you only get some steel back! If you are "
+"sure, hold Sneak and left-click the wagon."
+msgstr ""
+"警告:如果您摧毀此車廂,您只能拿到一些鋼方塊。如果您確定要摧毀這節車廂,請按"
+"潛行鍵並左鍵單擊此車廂。"
+
+#: wagons.lua
+msgid "You are not allowed to access the driver stand."
+msgstr ""
+
+#: wagons.lua
+msgid "You can't get on this wagon."
+msgstr ""
+
+#: wagons.lua
+#, fuzzy
+msgid "You don't have the train_operator privilege."
+msgstr "您沒有「@1」許可權。"
+
+#: wagons.lua
+msgid "You have been given ownership of wagon @1"
+msgstr ""
+
+#~ msgid "3-way turnout"
+#~ msgstr "三開道岔"
+
+#~ msgid "90+Angle Diamond Crossing Track"
+#~ msgstr "交叉軌道 (其中一條軌道與座標軸平行)"
+
+#~ msgid "ATC controller"
+#~ msgstr "ATC 控制器"
+
+#~ msgid ""
+#~ "ATC controller, mode @1\n"
+#~ "Channel: @2"
+#~ msgstr ""
+#~ "ATC 控制器\n"
+#~ "模式:@1\n"
+#~ "頻道:@2"
+
+#~ msgid "Access to @1"
+#~ msgstr "可前往@1"
+
+#~ msgid "Big Industrial Train Engine"
+#~ msgstr "大型工業用火車頭"
+
+#~ msgid "Box Wagon"
+#~ msgstr "貨運車廂"
+
+#~ msgid "Bumper"
+#~ msgstr "保險槓"
+
+#~ msgid "Can't get on: wagon full or doors closed!"
+#~ msgstr "無法上車:車門已關閉或車廂已滿。"
+
+#~ msgid "Can't place: protected position!"
+#~ msgstr "無法放置:此區域已被保護。"
+
+#~ msgid "Command (on)"
+#~ msgstr "命令 (啟用時)"
+
+#~ msgid "Default Seat"
+#~ msgstr "預設座位"
+
+#~ msgid "Default Seat (driver stand)"
+#~ msgstr "預設座位 (司機座位)"
+
+#~ msgid "Dep. Speed"
+#~ msgstr "出發速度"
+
+#~ msgid "Deprecated Track"
+#~ msgstr "請不要使用"
+
+#~ msgid "Detailed Steam Engine"
+#~ msgstr "精細的蒸汽機車"
+
+#~ msgid "Detector Rail"
+#~ msgstr "探測軌道"
+
+#~ msgid "Diagonal Diamond Crossing Track"
+#~ msgstr "交叉軌道"
+
+#~ msgid "Door Delay"
+#~ msgstr "車門關閉時間"
+
+#, fuzzy
+#~ msgid "Driver Stand"
+#~ msgstr "司機座位"
+
+#~ msgid "Driver Stand (left)"
+#~ msgstr "左側司機座位"
+
+#~ msgid "Driver Stand (right)"
+#~ msgstr "右側司機座位"
+
+#~ msgid "Driver stand"
+#~ msgstr "司機座位"
+
+#~ msgid "Industrial Train Engine"
+#~ msgstr "工業用火車頭"
+
+#~ msgid "Industrial tank wagon"
+#~ msgstr "液體運輸車廂"
+
+#~ msgid "Industrial wood wagon"
+#~ msgstr "木材運輸車廂"
+
+#~ msgid "Japanese Train Engine"
+#~ msgstr "高速列車車頭"
+
+#~ msgid "Japanese Train Inter-Wagon Connection"
+#~ msgstr "日本火車連結器"
+
+#~ msgid "Japanese Train Wagon"
+#~ msgstr "高速列車車廂"
+
+#, fuzzy
+#~ msgid "Japanese signal pole"
+#~ msgstr "高速列車車廂"
+
+#~ msgid "Kick out passengers"
+#~ msgstr "踢出乘客"
+
+#~ msgid "Loading Track"
+#~ msgstr "裝貨軌道"
+
+#~ msgid "Lock couples"
+#~ msgstr "鎖定連結處"
+
+#~ msgid "Passenger Wagon"
+#~ msgstr "客車"
+
+#, fuzzy
+#~ msgid "Passenger area"
+#~ msgstr "客車"
+
+#~ msgid ""
+#~ "Passive Component Naming Tool\n"
+#~ "\n"
+#~ "Right-click to name a passive component."
+#~ msgstr ""
+#~ "被動元件命名工具\n"
+#~ "\n"
+#~ "右鍵單擊命名所選元件。"
+
+#~ msgid "Perpendicular Diamond Crossing Track"
+#~ msgstr "垂直交叉軌道"
+
+#~ msgid "Reverse train"
+#~ msgstr "改變行車方向"
+
+#~ msgid "Signal"
+#~ msgstr "色燈號誌機"
+
+#~ msgid "Speed:"
+#~ msgstr "速度"
+
+#~ msgid "Station Code"
+#~ msgstr "車站碼"
+
+#~ msgid "Station Name"
+#~ msgstr "車站名稱"
+
+#~ msgid "Station/Stop Track"
+#~ msgstr "車站軌道"
+
+#~ msgid "Steam Engine"
+#~ msgstr "蒸汽機車"
+
+#~ msgid "Stop Time"
+#~ msgstr "停站時間"
+
+#~ msgid "Subway Passenger Wagon"
+#~ msgstr "地鐵車廂"
+
+#~ msgid "Target:"
+#~ msgstr "目標速度"
+
+#, fuzzy
+#~ msgid "This node can't be rotated using the trackworker,"
+#~ msgstr "您不能使用鐵路調整工具旋轉這個方塊。"
+
+#~ msgid "This position is protected!"
+#~ msgstr "這裡已被保護。"
+
+#~ msgid "This track can not be changed."
+#~ msgstr "您不能調整這段軌道。"
+
+#~ msgid "Track"
+#~ msgstr "軌道"
+
+#, fuzzy
+#~ msgid "Train "
+#~ msgstr "已複製火車。"
+
+#~ msgid "Unconfigured LuaATC component"
+#~ msgstr "LuaATC 元件 (未配置)"
+
+#~ msgid "Unloading Track"
+#~ msgstr "卸貨軌道"
+
+#~ msgid "Use Sneak+rightclick to bypass closed doors!"
+#~ msgstr "請使用潛行+右鍵上車。"
+
+#~ msgid "Y-turnout"
+#~ msgstr "對稱道岔"
+
+#~ msgid ""
+#~ "You are not allowed to configure this LuaATC component without the @1 "
+#~ "privilege."
+#~ msgstr "您沒有「@1」許可權,不能配置這個 LuaATC 元件。"
+
+#~ msgid ""
+#~ "You are not allowed to configure this track without the @1 privilege."
+#~ msgstr "您沒有「@1」許可權,不能調整這段軌道。"
+
+#~ msgid "You are not allowed to configure this track."
+#~ msgstr "您不能調整這段軌道。"
+
+#, fuzzy
+#~ msgid "You are not allowed to modify this protected track."
+#~ msgstr "這裡已被保護,您不能在這裡建造鐵路。"
+
+#~ msgid ""
+#~ "You are not allowed to name LuaATC passive components without the @1 "
+#~ "privilege."
+#~ msgstr "您沒有「@1」許可權,不能命名這個元件。"
+
+#~ msgid ""
+#~ "You need to own at least one neighboring wagon to destroy this couple."
+#~ msgstr "您必須至少擁有其中一節車廂才能分開這兩節車廂。"
diff --git a/advtrains/poconvert.lua b/advtrains/poconvert.lua
new file mode 100644
index 0000000..74f962e
--- /dev/null
+++ b/advtrains/poconvert.lua
@@ -0,0 +1,185 @@
+local unescape_string
+do
+ local schartbl = { -- https://en.wikipedia.org/wiki/Escape_sequences_in_C
+ a = "\a",
+ b = "\b",
+ e = string.char(0x1b),
+ f = "\f",
+ n = "\n",
+ r = "\r",
+ t = "\t",
+ v = "\v",
+ }
+ local function replace_single(pfx, c)
+ local pl = #pfx
+ if pl % 2 == 0 then
+ return string.sub(pfx, 1, pl/2) .. c
+ end
+ return string.sub(pfx, 1, math.floor(pl/2)) .. (schartbl[c] or c)
+ end
+ unescape_string = function(str)
+ return string.gsub(str, [[(\+)([abefnrtv'"?])]], replace_single)
+ end
+end
+
+local function readstring_aux(str, pos)
+ local _, spos = string.find(str, [[^%s*"]], pos)
+ if not spos then
+ return nil
+ end
+ local ipos = spos
+ while true do
+ local _, epos, m = string.find(str, [[(\*)"]], ipos+1)
+ if not epos then
+ return error("String extends beyond the end of input")
+ end
+ ipos = epos
+ if #m % 2 == 0 then
+ return unescape_string(string.sub(str, spos+1, epos-1)), epos+1
+ end
+ end
+end
+
+local function readstring(str, pos)
+ local st = {}
+ local nxt = pos
+ while true do
+ local s, npos = readstring_aux(str, nxt)
+ if not s then
+ if not st[1] then
+ return nil, nxt
+ else
+ return table.concat(st), nxt
+ end
+ end
+ nxt = npos
+ table.insert(st, s)
+ end
+end
+
+local function readtoken(str, pos)
+ local _, epos, tok = string.find(str, [[^%s*(%S+)]], pos)
+ if epos == nil then
+ return nil, pos
+ end
+ return tok, epos+1
+end
+
+local function readcomment_add_flags(flags, s)
+ for flag in string.gmatch(s, ",%s*([^,]+)") do
+ flags[flag] = true
+ end
+end
+
+local function readcomment_aux(str, pos)
+ local _, epos, sval = string.find(str, "^\n*#([^\n]*)", pos)
+ if not epos then
+ return nil
+ end
+ return sval, epos+1
+end
+
+local function readcomment(str, pos)
+ local st = {}
+ local nxt = pos
+ local flags = {}
+ while true do
+ local s, npos = readcomment_aux(str, nxt)
+ if not npos then
+ local t = {
+ comment = table.concat(st, "\n"),
+ flags = flags,
+ }
+ return t, nxt
+ end
+ if string.sub(s, 1, 1) == "," then
+ readcomment_add_flags(flags, s)
+ end
+ table.insert(st, s)
+ nxt = npos
+ end
+end
+
+local function readpo(str)
+ local st = {}
+ local pos = 1
+ while true do
+ local entry, nxt = readcomment(str, pos)
+ local msglines = 0
+ while true do
+ local tok, npos = readtoken(str, nxt)
+ if tok == nil or string.sub(tok, 1, 1) == "#" then
+ break
+ elseif string.sub(tok, 1, 3) ~= "msg" then
+ return error("Invalid token: " .. tok)
+ elseif entry[tok] ~= nil then
+ break
+ else
+ local value, npos = readstring(str, npos)
+ assert(value ~= nil, "No string provided for " .. tok)
+ entry[tok] = value
+ nxt = npos
+ msglines = msglines+1
+ end
+ end
+ if msglines == 0 then
+ return st
+ elseif entry.msgid ~= "" then
+ assert(entry.msgid ~= nil, "Missing untranslated string")
+ assert(entry.msgstr ~= nil, "Missing translated string")
+ table.insert(st, entry)
+ end
+ pos = nxt
+ end
+end
+
+local escape_lookup = {
+ ["="] = "@=",
+ ["\n"] = "@n"
+}
+local function escape_string(st)
+ return (string.gsub(st, "[=\n]", escape_lookup))
+end
+
+local function convert_po_string(textdomain, str)
+ local st = {string.format("# textdomain: %s", textdomain)}
+ for _, entry in ipairs(readpo(str)) do
+ local line = ("%s=%s"):format(escape_string(entry.msgid), escape_string(entry.msgstr))
+ if entry.flags.fuzzy then
+ line = "#" .. line
+ end
+ table.insert(st, line)
+ end
+ return table.concat(st, "\n")
+end
+
+local function convert_po_file(textdomain, inpath, outpath)
+ local f, err = io.open(inpath, "rb")
+ assert(f, err)
+ local str = convert_po_string(textdomain, f:read("*a"))
+ f:close()
+ minetest.safe_file_write(outpath, str)
+ return str
+end
+
+local function convert_flat_po_directory(textdomain, modpath)
+ assert(textdomain, "No textdomain specified for po file conversion")
+ local mp = modpath or minetest.get_modpath(textdomain)
+ assert(mp ~= nil, "No path to write for " .. textdomain)
+ local popath = mp .. "/po"
+ local trpath = mp .. "/locale"
+ for _, infile in pairs(minetest.get_dir_list(popath, false)) do
+ local lang = string.match(infile, [[^([^%.]+)%.po$]])
+ if lang then
+ local inpath = popath .. "/" .. infile
+ local outpath = ("%s/%s.%s.tr"):format(trpath, textdomain, lang)
+ convert_po_file(textdomain, inpath, outpath)
+ end
+ end
+end
+
+return {
+ from_string = convert_po_string,
+ from_file = convert_po_file,
+ from_flat = convert_flat_po_directory,
+}
diff --git a/advtrains/protection.lua b/advtrains/protection.lua
index 7474977..6bb1ccb 100644
--- a/advtrains/protection.lua
+++ b/advtrains/protection.lua
@@ -1,27 +1,29 @@
-- advtrains
-- protection.lua: privileges and rail protection, and some helpers
+-- Get current translator
+local S = advtrains.translate
-- Privileges to control TRAIN DRIVING/COUPLING
minetest.register_privilege("train_operator", {
- description = "Without this privilege, a player can't do anything about trains, neither place or remove them nor drive or couple them (but he can build tracks if he has track_builder)",
+ description = S("Can place, remove and operate trains"),
give_to_singleplayer= true,
});
minetest.register_privilege("train_admin", {
- description = "Player may drive, place or remove any trains from/to anywhere, regardless of owner, whitelist or protection",
+ description = S("Can place, remove and operate any train, regardless of owner, whitelist, or protection"),
give_to_singleplayer= true,
});
-- Privileges to control TRACK BUILDING
minetest.register_privilege("track_builder", {
- description = "Player can place and/or dig rails not protected from him. If he also has protection_bypass, he can place/dig any rails",
+ description = S("Can place and dig tracks in unprotected areas"),
give_to_singleplayer= true,
});
-- Privileges to control OPERATING TURNOUTS/SIGNALS
minetest.register_privilege("railway_operator", {
- description = "Player can operate turnouts and signals not protected from him. If he also has protection_bypass, he can operate any turnouts/signals",
+ description = S("Can operate turnouts and signals in unprotected areas"),
give_to_singleplayer= true,
});
@@ -145,12 +147,12 @@ function advtrains.check_track_protection(pos, pname, near, prot_p)
--atdebug("CTP: ",pos,pname,near,prot_p,"priv=",priv,"prot=",prot,"dprot=",dprot)
if not priv and (not boo or prot or not dprot) then
- minetest.chat_send_player(pname, "You are not allowed to build "..nears.."tracks without track_builder privilege")
+ minetest.chat_send_player(pname, near and S("You are not allowed to build near tracks without the track_builder privilege.") or S("You are not allowed to build tracks without the track_builder privilege."))
minetest.log("action", pname.." tried to modify terrain "..nears.."track at "..minetest.pos_to_string(apos).." but is not permitted to (no privilege)")
return false
end
if prot then
- minetest.chat_send_player(pname, "You are not allowed to build "..nears.."tracks at protected position!")
+ minetest.chat_send_player(pname, near and S("You are not allowed to build near tracks at this protected position.") or S("You are not allowed to build tracks at this protected position."))
minetest.record_protection_violation(pos, pname)
minetest.log("action", pname.." tried to modify "..nears.."track at "..minetest.pos_to_string(apos).." but position is protected!")
return false
@@ -181,7 +183,7 @@ function advtrains.check_turnout_signal_protection(pos, pname)
nocheck=false
return true
else
- minetest.chat_send_player(pname, "You are not allowed to operate turnouts and signals (missing railway_operator privilege)")
+ minetest.chat_send_player(pname, S("You are not allowed to operate turnouts and signals without the railway_operator privilege."))
minetest.log("action", pname.." tried to operate turnout/signal at "..minetest.pos_to_string(pos).." but does not have railway_operator")
nocheck=false
return false
diff --git a/advtrains/settingtypes.txt b/advtrains/settingtypes.txt
index 2b627cb..a09da71 100644
--- a/advtrains/settingtypes.txt
+++ b/advtrains/settingtypes.txt
@@ -7,6 +7,10 @@ advtrains_show_ids (Show ID's in infotext) bool false
# You probably want to leave this setting set to false.
advtrains_enable_debugging (Enable debugging) bool false
+# Register certain debug items, for example the tunnelborer
+# Do not use on productive servers!
+advtrains_register_debugitems (Register Debug Items) bool false
+
# Enable the logging of certain events related to advtrains
# Logs are saved in the world directory as advtrains.log
# This setting is useful for multiplayer servers
@@ -61,3 +65,6 @@ advtrains_save_interval (Save Interval) int 60 20 3600
# If enabled, trains only collide with nodes with "normal" drawtype.
advtrains_forgiving_collision (Forgiving Collision mode) bool false
+# Enable universal couplers for wagons
+# If enabled, wagons will bypass the checks that compare the coupler types when coupling.
+advtrains_universal_couplers (Universal Couplers) bool false \ No newline at end of file
diff --git a/advtrains/signals.lua b/advtrains/signals.lua
index b26c950..198507a 100644
--- a/advtrains/signals.lua
+++ b/advtrains/signals.lua
@@ -1,17 +1,20 @@
--advtrains by orwell96
--signals.lua
+-- Get current translator
+local S = advtrains.translate
+
local mrules_wallsignal = advtrains.meseconrules
-local function can_dig_func(pos)
+local function can_dig_func(pos, player)
if advtrains.interlocking then
- return advtrains.interlocking.signal_can_dig(pos)
+ return advtrains.interlocking.signal.can_dig(pos, player)
end
return true
end
-local function after_dig_func(pos)
+local function after_dig_func(pos, oldnode, oldmetadata, digger)
if advtrains.interlocking then
- return advtrains.interlocking.signal_after_dig(pos)
+ return advtrains.interlocking.signal.after_dig(pos, oldnode, oldmetadata, digger)
end
return true
end
@@ -26,22 +29,21 @@ return {
}
end
-local suppasp = {
- main = {0, -1},
- dst = {false},
- shunt = nil,
- proceed_as_main = true,
- info = {
- call_on = false,
- dead_end = false,
- w_speed = nil,
- }
+local main_aspects = {
+ { name = "free", description = "Free" }
}
-for r,f in pairs({on={as="off", ls="green", als="red"}, off={as="on", ls="red", als="green"}}) do
+local function simple_apply_aspect(offname, onname)
+ return function(pos, node, main_aspect, rem_aspect, rem_aspinfo)
+ if main_aspect.halt then
+ advtrains.ndb.swap_node(pos, {name = offname, param2 = node.param2})
+ else
+ advtrains.ndb.swap_node(pos, {name = onname, param2 = node.param2})
+ end
+ end
+end
- advtrains.trackplacer.register_tracktype("advtrains:retrosignal", "")
- advtrains.trackplacer.register_tracktype("advtrains:signal", "")
+for r,f in pairs({on={as="off", ls="green", als="red"}, off={as="on", ls="red", als="green"}}) do
for rotid, rotation in ipairs({"", "_30", "_45", "_60"}) do
local crea=1
@@ -60,7 +62,7 @@ for r,f in pairs({on={as="off", ls="green", als="red"}, off={as="on", ls="red",
tiles = {"advtrains_retrosignal.png"},
inventory_image="advtrains_retrosignal_inv.png",
drop="advtrains:retrosignal_off",
- description=attrans("Lampless Signal (@1)", attrans(r..rotation)),
+ description=S("Lampless Signal (deprecated!)"),
sunlight_propagates=true,
groups = {
cracky=3,
@@ -74,42 +76,41 @@ for r,f in pairs({on={as="off", ls="green", als="red"}, off={as="on", ls="red",
["action_"..f.as] = function (pos, node)
advtrains.ndb.swap_node(pos, {name = "advtrains:retrosignal_"..f.as..rotation, param2 = node.param2}, true)
if advtrains.interlocking then
- advtrains.interlocking.signal_on_aspect_changed(pos)
+ -- forcefully clears any set aspect, so that aspect system doesnt override it again
+ advtrains.interlocking.signal.unregister_aspect(pos)
+ -- notify trains
+ advtrains.interlocking.signal.notify_trains(pos)
end
end
}},
on_rightclick=function(pos, node, player)
local pname = player:get_player_name()
local sigd = advtrains.interlocking and advtrains.interlocking.db.get_sigd_for_signal(pos)
- if sigd then
+ if sigd and not player:get_player_control().aux1 then
advtrains.interlocking.show_signalling_form(sigd, pname)
elseif advtrains.interlocking and player:get_player_control().aux1 then
advtrains.interlocking.show_ip_form(pos, pname)
elseif advtrains.check_turnout_signal_protection(pos, player:get_player_name()) then
advtrains.ndb.swap_node(pos, {name = "advtrains:retrosignal_"..f.as..rotation, param2 = node.param2}, true)
if advtrains.interlocking then
- advtrains.interlocking.signal_on_aspect_changed(pos)
+ -- forcefully clears any set aspect, so that aspect system doesnt override it again
+ advtrains.interlocking.signal.unregister_aspect(pos)
+ -- notify trains
+ advtrains.interlocking.signal.notify_trains(pos)
end
end
end,
- -- new signal API
+ -- very new signal API
advtrains = {
- set_aspect = function(pos, node, asp)
- if asp.main ~= 0 then
- advtrains.ndb.swap_node(pos, {name = "advtrains:retrosignal_on"..rotation, param2 = node.param2}, true)
- else
- advtrains.ndb.swap_node(pos, {name = "advtrains:retrosignal_off"..rotation, param2 = node.param2}, true)
- end
- end,
- get_aspect = function(pos, node)
- return aspect(r=="on")
- end,
- supported_aspects = suppasp,
+ main_aspects = main_aspects,
+ apply_aspect = simple_apply_aspect("advtrains:retrosignal_off"..rotation, "advtrains:retrosignal_on"..rotation),
+ get_aspect_info = function() return aspect(r=="on") end,
+ route_role = "main",
},
can_dig = can_dig_func,
after_dig_node = after_dig_func,
+ --TODO add rotation using trackworker
})
- advtrains.trackplacer.add_worked("advtrains:retrosignal", r, rotation, nil)
minetest.register_node("advtrains:signal_"..r..rotation, {
drawtype = "mesh",
@@ -124,7 +125,7 @@ for r,f in pairs({on={as="off", ls="green", als="red"}, off={as="on", ls="red",
tiles = {"advtrains_signal_"..r..".png"},
inventory_image="advtrains_signal_inv.png",
drop="advtrains:signal_off",
- description=attrans("Signal (@1)", attrans(r..rotation)),
+ description=S("Signal (deprecated!)"),
groups = {
cracky=3,
not_blocking_trains=1,
@@ -138,55 +139,51 @@ for r,f in pairs({on={as="off", ls="green", als="red"}, off={as="on", ls="red",
rules=advtrains.meseconrules,
["action_"..f.as] = function (pos, node)
advtrains.setstate(pos, f.als, node)
- if advtrains.interlocking then
- advtrains.interlocking.signal_on_aspect_changed(pos)
- end
end
}},
on_rightclick=function(pos, node, player)
local pname = player:get_player_name()
local sigd = advtrains.interlocking and advtrains.interlocking.db.get_sigd_for_signal(pos)
- if sigd then
+ if sigd and not player:get_player_control().aux1 then
advtrains.interlocking.show_signalling_form(sigd, pname)
elseif advtrains.interlocking and player:get_player_control().aux1 then
advtrains.interlocking.show_ip_form(pos, pname)
elseif advtrains.check_turnout_signal_protection(pos, player:get_player_name()) then
advtrains.setstate(pos, f.als, node)
- if advtrains.interlocking then
- advtrains.interlocking.signal_on_aspect_changed(pos)
- end
end
end,
- -- new signal API
+ -- very new signal API
advtrains = {
- set_aspect = function(pos, node, asp)
- if asp.main ~= 0 then
- advtrains.ndb.swap_node(pos, {name = "advtrains:signal_on"..rotation, param2 = node.param2}, true)
- else
- advtrains.ndb.swap_node(pos, {name = "advtrains:signal_off"..rotation, param2 = node.param2}, true)
- end
- end,
- get_aspect = function(pos, node)
- return aspect(r=="on")
- end,
- supported_aspects = suppasp,
- getstate = f.ls,
- setstate = function(pos, node, newstate)
- if newstate == f.als then
- advtrains.ndb.swap_node(pos, {name = "advtrains:signal_"..f.as..rotation, param2 = node.param2}, true)
+ main_aspects = main_aspects,
+ apply_aspect = simple_apply_aspect("advtrains:signal_off"..rotation, "advtrains:signal_on"..rotation),
+ get_aspect_info = function() return aspect(r=="on") end,
+ route_role = "main",
+ node_state = f.ls,
+ node_state_map = { red = "advtrains:signal_off"..rotation, green = "advtrains:signal_on"..rotation},
+ node_on_switch_state = function(pos, new_node, old_state, new_state)
+ if advtrains.interlocking then
+ -- forcefully clears any set aspect, so that aspect system doesnt override it again
+ advtrains.interlocking.signal.unregister_aspect(pos)
+ -- notify trains
+ advtrains.interlocking.signal.notify_trains(pos)
end
end,
},
can_dig = can_dig_func,
after_dig_node = after_dig_func,
+ --TODO add rotation using trackworker
})
- advtrains.trackplacer.add_worked("advtrains:signal", r, rotation, nil)
end
local crea=1
if r=="off" then crea=0 end
--tunnel signals. no rotations.
+ local swdesc = { -- needed for xgettext
+ l = S("Wallmounted Signal (left) (deprecated!)"),
+ r = S("Wallmounted Signal (right) (deprecated!)"),
+ t = S("Wallmounted Signal (top) (deprecated!)"),
+ }
for loc, sbox in pairs({l={-1/2, -1/2, -1/4, 0, 1/2, 1/4}, r={0, -1/2, -1/4, 1/2, 1/2, 1/4}, t={-1/2, 0, -1/4, 1/2, 1/2, 1/4}}) do
minetest.register_node("advtrains:signal_wall_"..loc.."_"..r, {
drawtype = "mesh",
@@ -200,7 +197,7 @@ for r,f in pairs({on={as="off", ls="green", als="red"}, off={as="on", ls="red",
mesh = "advtrains_signal_wall_"..loc..".b3d",
tiles = {"advtrains_signal_wall_"..r..".png"},
drop="advtrains:signal_wall_"..loc.."_off",
- description=attrans("Wallmounted Signal ("..loc..")"),
+ description=swdesc[loc],
groups = {
cracky=3,
not_blocking_trains=1,
@@ -214,42 +211,33 @@ for r,f in pairs({on={as="off", ls="green", als="red"}, off={as="on", ls="red",
rules = mrules_wallsignal,
["action_"..f.as] = function (pos, node)
advtrains.setstate(pos, f.als, node)
- if advtrains.interlocking then
- advtrains.interlocking.signal_on_aspect_changed(pos)
- end
end
}},
on_rightclick=function(pos, node, player)
local pname = player:get_player_name()
local sigd = advtrains.interlocking and advtrains.interlocking.db.get_sigd_for_signal(pos)
- if sigd then
+ if sigd and not player:get_player_control().aux1 then
advtrains.interlocking.show_signalling_form(sigd, pname)
elseif advtrains.interlocking and player:get_player_control().aux1 then
advtrains.interlocking.show_ip_form(pos, pname)
elseif advtrains.check_turnout_signal_protection(pos, player:get_player_name()) then
advtrains.setstate(pos, f.als, node)
- if advtrains.interlocking then
- advtrains.interlocking.signal_on_aspect_changed(pos)
- end
end
end,
- -- new signal API
+ -- very new signal API
advtrains = {
- set_aspect = function(pos, node, asp)
- if asp.main ~= 0 then
- advtrains.ndb.swap_node(pos, {name = "advtrains:signal_wall_"..loc.."_on", param2 = node.param2}, true)
- else
- advtrains.ndb.swap_node(pos, {name = "advtrains:signal_wall_"..loc.."_off", param2 = node.param2}, true)
- end
- end,
- get_aspect = function(pos, node)
- return aspect(r=="on")
- end,
- supported_aspects = suppasp,
- getstate = f.ls,
- setstate = function(pos, node, newstate)
- if newstate == f.als then
- advtrains.ndb.swap_node(pos, {name = "advtrains:signal_wall_"..loc.."_"..f.as, param2 = node.param2}, true)
+ main_aspects = main_aspects,
+ apply_aspect = simple_apply_aspect("advtrains:signal_wall_"..loc.."_off", "advtrains:signal_wall_"..loc.."_on"),
+ get_aspect_info = function() return aspect(r=="on") end,
+ route_role = "main",
+ node_state = f.ls,
+ node_state_map = { red = "advtrains:signal_wall_"..loc.."_off", green = "advtrains:signal_wall_"..loc.."_on" },
+ node_on_switch_state = function(pos, new_node, old_state, new_state)
+ if advtrains.interlocking then
+ -- forcefully clears any set aspect, so that aspect system doesnt override it again
+ advtrains.interlocking.signal.unregister_aspect(pos)
+ -- notify trains
+ advtrains.interlocking.signal.notify_trains(pos)
end
end,
},
@@ -273,7 +261,7 @@ minetest.register_node("advtrains:across_off", {
mesh = "advtrains_across.obj",
tiles = {"advtrains_across.png"},
drop="advtrains:across_off",
- description=attrans("Andrew's Cross"),
+ description=S("Andrew's Cross"),
groups = {
cracky=3,
not_blocking_trains=1,
@@ -289,12 +277,8 @@ minetest.register_node("advtrains:across_off", {
end
}},
advtrains = {
- getstate = "off",
- setstate = function(pos, node, newstate)
- if newstate == "on" then
- advtrains.ndb.swap_node(pos, {name = "advtrains:across_on", param2 = node.param2}, true)
- end
- end,
+ node_state = "off",
+ node_state_map = { on = "advtrains:across_on", off = "advtrains:across_off" },
},
on_rightclick=function(pos, node, player)
if advtrains.check_turnout_signal_protection(pos, player:get_player_name()) then
@@ -314,7 +298,7 @@ minetest.register_node("advtrains:across_on", {
mesh = "advtrains_across.obj",
tiles = {{name="advtrains_across_anim.png", animation={type="vertical_frames", aspect_w=64, aspect_h=64, length=1.0}}},
drop="advtrains:across_off",
- description=attrans("Andrew's Cross (on) (you hacker you)"),
+ description=S("Andrew's Cross"),
groups = {
cracky=3,
not_blocking_trains=1,
@@ -330,13 +314,9 @@ minetest.register_node("advtrains:across_on", {
end
}},
advtrains = {
- getstate = "on",
- setstate = function(pos, node, newstate)
- if newstate == "off" then
- advtrains.ndb.swap_node(pos, {name = "advtrains:across_off", param2 = node.param2}, true)
- end
- end,
- fallback_state = "off",
+ node_state = "on",
+ node_state_map = { on = "advtrains:across_on", off = "advtrains:across_off" },
+ node_fallback_state = "off",
},
on_rightclick=function(pos, node, player)
if advtrains.check_turnout_signal_protection(pos, player:get_player_name()) then
diff --git a/advtrains/spec/poconvert_spec.lua b/advtrains/spec/poconvert_spec.lua
new file mode 100644
index 0000000..51f33e7
--- /dev/null
+++ b/advtrains/spec/poconvert_spec.lua
@@ -0,0 +1,70 @@
+package.path = "../?.lua;" .. package.path
+advtrains = {}
+_G.advtrains = advtrains
+local poconvert = require("poconvert")
+
+describe("PO file converter", function()
+ it("should convert PO files", function()
+ assert.equals([[
+# textdomain: foo
+foo=bar
+baz=
+#@=wh\at\\@n=@=w\as\\@n
+multiline@nstrings=multiline@nresult
+with context?=oder doch nicht]], poconvert.from_string("foo", [[
+msgid ""
+msgstr "whatever metadata"
+
+msgid "foo"
+msgstr "bar"
+
+msgid "baz"
+msgstr ""
+
+#, fuzzy
+msgid "=wh\\at\\\\\n"
+msgstr "=w\\as\\\\\n"
+
+msgid "multi"
+"line\n"
+"strings"
+msgstr "multi"
+"line\n"
+"result"
+
+msgctxt "i18n context"
+msgid "with context?"
+msgstr "oder doch nicht"]]))
+ end)
+ it("should reject invalid tokens", function()
+ assert.has.errors(function()
+ poconvert.from_string("", [[
+foo ""
+bar ""]])
+ end, "Invalid token: foo")
+ end)
+ it("should reject entries without a msgstr", function()
+ assert.has.errors(function()
+ poconvert.from_string("", [[msgid "foo"]])
+ end, "Missing translated string")
+ end)
+ it("should reject entries without a msgid", function()
+ assert.has.errors(function()
+ poconvert.from_string("", [[msgstr "foo"]])
+ end, "Missing untranslated string")
+ end)
+ it("should reject entries with improperly enclosed strings", function()
+ assert.has.errors(function()
+ poconvert.from_string("", [[
+msgid "foo"
+msgstr "bar \]])
+ end, "String extends beyond the end of input")
+ end)
+ it("should reject incomplete input", function()
+ assert.has.errors(function()
+ poconvert.from_string("", [[
+msgid "foo"
+msgstr]])
+ end, "No string provided for msgstr")
+ end)
+end)
diff --git a/advtrains/spec/texture_spec.lua b/advtrains/spec/texture_spec.lua
new file mode 100644
index 0000000..2e3bd5d
--- /dev/null
+++ b/advtrains/spec/texture_spec.lua
@@ -0,0 +1,19 @@
+package.path = "../?.lua;" .. package.path
+local T = require "texture"
+
+describe("Texture creation", function()
+ it("works", function()
+ assert.same("^.png", tostring(T.raw"^.png"))
+ assert.same("foo\\:bar.png", tostring(T"foo:bar.png"))
+ end)
+end)
+
+describe("Texture modifiers", function()
+ it("work", function()
+ assert.same("x^[colorize:c", tostring(T"x":colorize"c"))
+ assert.same("x^[colorize:c:alpha", tostring(T"x":colorize("c", "alpha")))
+ assert.same("x^[multiply:c", tostring(T"x":multiply"c"))
+ assert.same("x^[resize:2x3", tostring(T"x":resize(2, 3)))
+ assert.same("x^[transformI", tostring(T"x":transform"I"))
+ end)
+end)
diff --git a/advtrains/spec/wagons_spec.lua b/advtrains/spec/wagons_spec.lua
new file mode 100644
index 0000000..df0687b
--- /dev/null
+++ b/advtrains/spec/wagons_spec.lua
@@ -0,0 +1,40 @@
+require "mineunit"
+mineunit "core"
+
+_G.advtrains = {
+ wagon_load_range = 32
+}
+sourcefile "wagons"
+
+local myproto = {_test = true}
+advtrains.register_wagon(":mywagon", myproto, "My wagon", "", false)
+advtrains.register_wagon_alias(":myalias", ":mywagon")
+advtrains.register_wagon_alias(":myotheralias", ":myalias")
+
+local myotherproto = {_other = true}
+advtrains.register_wagon(":noalias", myotherproto, "Not aliased wagon", "", false)
+advtrains.register_wagon_alias(":noalias", ":mywagon")
+
+advtrains.register_wagon_alias(":nilalias", ":nil")
+
+advtrains.register_wagon_alias(":R1", ":R2")
+advtrains.register_wagon_alias(":R2", ":R3")
+advtrains.register_wagon_alias(":R3", ":R1")
+
+describe("wagon alias system", function()
+ it("should work", function()
+ assert.same({":mywagon", myproto}, {advtrains.resolve_wagon_alias(":myalias")})
+ assert.equal(myproto, advtrains.wagon_prototypes[":myalias"])
+ assert.same({":mywagon", myproto}, {advtrains.resolve_wagon_alias(":myotheralias")})
+ end)
+ it("should respect wagon registration", function()
+ assert.same({":noalias", myotherproto}, {advtrains.resolve_wagon_alias(":noalias")})
+ end)
+ it("should handle recursive loops", function()
+ assert.same({}, {advtrains.resolve_wagon_alias(":R1")})
+ end)
+ it("should return nil for missing entries", function()
+ assert.same({}, {advtrains.resolve_wagon_alias(":what")})
+ assert.same({}, {advtrains.resolve_wagon_alias(":nilalias")})
+ end)
+end)
diff --git a/advtrains/texture.lua b/advtrains/texture.lua
new file mode 100644
index 0000000..e6d83b0
--- /dev/null
+++ b/advtrains/texture.lua
@@ -0,0 +1,228 @@
+local tx = {}
+setmetatable(tx, {__call = function(_, ...) return tx.base(...) end})
+
+function tx.escape(str)
+ return (string.gsub(tostring(str), [[([%^:\])]], [[\%1]]))
+end
+
+local function getargs(...)
+ return select("#", ...), {...}
+end
+
+local function curry(f, x)
+ return function(...)
+ return f(x, ...)
+ end
+end
+
+local function xmkmodifier(func)
+ return function(self, ...)
+ table.insert(self, (func(...)))
+ return self
+ end
+end
+
+local function mkmodifier(fmt, spec)
+ return xmkmodifier(function(...)
+ local count = select("#", ...)
+ local args = {...}
+ for k, f in pairs(spec) do
+ args[k] = f(args[k])
+ end
+ return string.format(fmt, unpack(args, 1, count))
+ end)
+end
+
+-- Texture object
+local tx_lib = {}
+local tx_mt = {
+ __index = tx_lib,
+ __tostring = function(self)
+ return table.concat(self, "^")
+ end,
+ __concat = function(a, b)
+ return tx.raw(("%s^%s"):format(tostring(a), tostring(b)))
+ end,
+}
+
+function tx.raw(str)
+ return setmetatable({str}, tx_mt)
+end
+function tx.base(str)
+ return tx.raw(tx.escape(str))
+end
+-- TODO: use [fill when 5.8.0 becomes widely used client-side
+function tx.fill(w, h, color)
+ return tx"advtrains_hud_bg.png":resize(w, h):colorize(color)
+end
+
+-- Most texture modifiers
+tx_lib.colorize = xmkmodifier(function(c, a)
+ local str = ("[colorize:%s"):format(tx.escape(c))
+ if a then
+ str = str .. ":" .. a
+ end
+ return str
+end)
+tx_lib.multiply = mkmodifier("[multiply:%s", {tx.escape})
+tx_lib.resize = mkmodifier("[resize:%dx%d", {})
+tx_lib.transform = mkmodifier("[transform%s", {tx.escape})
+
+-- [combine
+
+local combine = {}
+
+function combine:add(x, y, ent)
+ table.insert(self.st, ([[%d,%d=%s]]):format(x, y, tx.escape(tostring(ent))))
+ return self
+end
+
+local combine_mt = {
+ __index = combine,
+ __tostring = function(self)
+ return table.concat(self.st, ":")
+ end,
+}
+
+function tx.combine(w, h, bg)
+ local base = ("[combine:%dx%d"):format(w, h)
+ local obj = setmetatable({width = w, height = h, st = {base}}, combine_mt)
+ if bg then
+ obj:add_fill(0, 0, w, h, bg)
+ end
+ return obj
+end
+
+function combine:add_fill(x, y, ...)
+ return self:add(x, y, tx.fill(...))
+end
+
+local function add_multicolor_fill(n, self, x, y, w, h, ...)
+ local argc, argv = getargs(...)
+ local t = 0
+ for k = 1, argc, 2 do
+ t = t + argv[k]
+ end
+ local newargs = {x, y, w, h}
+ local sk, wk = n, n+2
+ local s = newargs[wk]/t
+ for k = 1, argc, 2 do
+ local v = argv[k] * s
+ newargs[wk] = v
+ newargs[5] = argv[k+1]
+ self:add_fill(unpack(newargs))
+ newargs[sk] = newargs[sk] + v
+ end
+ return self
+end
+combine.add_multicolor_fill_topdown = curry(add_multicolor_fill, 2)
+combine.add_multicolor_fill_leftright = curry(add_multicolor_fill, 1)
+
+local function add_segmentbar(n, self, x, y, w, h, m, c, ...)
+ local argc, argv = getargs(...)
+ local baseargs = {x, y, w, h}
+ local ss = (baseargs[n+2]+m)/c
+ local bs = ss - m
+ for k = 1, argc, 3 do
+ local lower, upper, fill = argv[k], argv[k+1], argv[k+2]
+ lower = math.max(0, math.floor(lower))+1
+ upper = math.min(c, math.floor(upper))
+ if lower <= upper then
+ local args = {x, y, w, h, fill}
+ args[n+2] = bs
+ args[n] = args[n] + ss*(lower-1)
+ for i = lower, upper do
+ self:add_fill(unpack(args))
+ args[n] = args[n] + ss
+ end
+ end
+ end
+ return self
+end
+combine.add_segmentbar_topdown = curry(add_segmentbar, 2)
+combine.add_segmentbar_leftright = curry(add_segmentbar, 1)
+
+local function add_lever(n, self, x, y, w, h, hs, ss, val, hf, sf)
+ local baseargs = {x, y, w, h}
+ local sargs = {x, y, w, h, sf}
+ sargs[5-n] = ss
+ sargs[n+2] = baseargs[n+2] + ss - hs
+ for k = 1, 2 do
+ sargs[k] = baseargs[k] + (baseargs[k+2] - sargs[k+2])/2
+ end
+ self:add_fill(unpack(sargs))
+ local hargs = {x, y, w, h, hf}
+ hargs[n+2] = hs
+ hargs[n] = baseargs[n] + (baseargs[n+2]-hs)*val
+ self:add_fill(unpack(hargs))
+ return self
+end
+combine.add_lever_topdown = curry(add_lever, 2)
+combine.add_lever_leftright = curry(add_lever, 1)
+
+--[[ Seven-segment display
+ -1-
+6 2
+ -7-
+5 3
+ -4-
+--]]
+local sevenseg_digits = {
+ ["0"] = {1, 2, 3, 4, 5, 6},
+ ["1"] = {2, 3},
+ ["2"] = {1, 2, 4, 5, 7},
+ ["3"] = {1, 2, 3, 4, 7},
+ ["4"] = {2, 3, 6, 7},
+ ["5"] = {1, 3, 4, 6, 7},
+ ["6"] = {1, 3, 4, 5, 6, 7},
+ ["7"] = {1, 2, 3},
+ ["8"] = {1, 2, 3, 4, 5, 6, 7},
+ ["9"] = {1, 2, 3, 4, 6, 7},
+}
+
+function combine:add_str7seg(x0, y0, tw, th, str, fill)
+ --[[ w and h (as width/height of individual (horizontal) segments) have the following properties:
+ tw = n(w+3h)-h
+ th = 2w+3h
+ --]]
+ local len = #str
+ local h = (2*tw-len*th)/(3*len-2)
+ local w = (th-3*h)/2
+ local ws = w+3*h
+ local segs = {
+ {h, 0, w, h},
+ {w+h, h, h, w},
+ {w+h, w+2*h, h, w},
+ {h, 2*(w+h), w, h},
+ {0, w+2*h, h, w},
+ {0, h, h, w},
+ {h, w+h, w, h},
+ }
+ for i = 1, len do
+ for _, k in pairs(sevenseg_digits[string.sub(str, i, i)] or {}) do
+ local s = segs[k]
+ self:add_fill(s[1]+x0, s[2]+y0, s[3], s[4], fill)
+ end
+ x0 = x0 + ws
+ end
+ return self
+end
+
+function combine:add_n7seg(x, y, w, h, n, prec, ...)
+ if not (type(n) == "number" and type(prec) == "number") then
+ error("passed non-numeric value or precision to numeric display")
+ elseif prec < 0 then
+ error("negative length")
+ end
+ local pfx = ""
+ if n >= 0 then
+ n = math.min(10^prec-1, n)
+ else
+ n = math.min(10^(prec-1)-1, -n)
+ pfx = "-"
+ end
+ local str = ("%d"):format(n)
+ return self:add_str7seg(x, y, w, h, pfx .. ("0"):rep(prec-#str-#pfx) .. str, ...)
+end
+
+return tx
diff --git a/advtrains/textures/advtrains_wagon_prop_tool.png b/advtrains/textures/advtrains_wagon_prop_tool.png
new file mode 100644
index 0000000..6352c55
--- /dev/null
+++ b/advtrains/textures/advtrains_wagon_prop_tool.png
Binary files differ
diff --git a/advtrains/track_reg_helper.lua b/advtrains/track_reg_helper.lua
new file mode 100644
index 0000000..fe1d11d
--- /dev/null
+++ b/advtrains/track_reg_helper.lua
@@ -0,0 +1,751 @@
+-- New track registration helper
+-- Retains the old table-template-based definition format, but adapts it to the new (advtrains 2.5)
+-- track definition system
+-- Note to future: This is actually just a work-saver, avoiding me to port over all the crossing nodes as well as the linetrack tracks.
+-- Future track mods should please directly use the appropriate advtrains.register_node_4rot() API and not rely on this!
+
+-- Get current translator
+local S = advtrains.translate
+
+--definition preparation
+local function conns(c1, c2, r1, r2) return {{c=c1, y=r1}, {c=c2, y=r2}} end
+local function conns3(c1, c2, c3, r1, r2, r3) return {{c=c1, y=r1}, {c=c2, y=r2}, {c=c3, y=r3}} end
+
+advtrains.ap={}
+advtrains.ap.t_30deg_flat={
+ v25_format = true,
+ regstep=1,
+ variant={
+ st={
+ conns = conns(0,8),
+ desc = "straight",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "cr",
+ },
+ cr={
+ conns = conns(0,7),
+ desc = "curve",
+ tpdouble = true,
+ trackworker = "swlst",
+ },
+ swlst={
+ conns = conns3(0,8,7),
+ desc = "left switch (straight)",
+ trackworker = "swrst",
+ switchalt = "cr",
+ switchmc = "on",
+ switchst = "st",
+ switchprefix = "swl",
+ conn_map = {2,1,1},
+ stmref = "swl",
+ },
+ swlcr={
+ conns = conns3(0,8,7),
+ desc = "left switch (curve)",
+ trackworker = "swrcr",
+ switchalt = "st",
+ switchmc = "off",
+ switchst = "cr",
+ switchprefix = "swl",
+ conn_map = {3,1,1},
+ stmref = "swl",
+ },
+ swrst={
+ conns = conns3(0,8,9),
+ desc = "right switch (straight)",
+ trackworker = "st",
+ switchalt = "cr",
+ switchmc = "on",
+ switchst = "st",
+ switchprefix = "swr",
+ conn_map = {2,1,1},
+ stmref = "swr",
+ },
+ swrcr={
+ conns = conns3(0,8,9),
+ desc = "right switch (curve)",
+ trackworker = "st",
+ switchalt = "st",
+ switchmc = "off",
+ switchst = "cr",
+ switchprefix = "swr",
+ conn_map = {3,1,1},
+ stmref = "swr",
+ },
+ },
+ regtp=true,
+ tpdefault="st",
+ trackworker={
+ ["swrcr"]="st",
+ ["swrst"]="st",
+ ["cr"]="swlst",
+ ["swlcr"]="swrcr",
+ ["swlst"]="swrst",
+ },
+ rotation={"", "_30", "_45", "_60"},
+ statemaps = {
+ swl = { st = "swlst", cr = "swlcr"},
+ swr = { st = "swrst", cr = "swrcr"}
+ }
+}
+advtrains.ap.t_yturnout={
+ v25_format = true,
+ regstep=1,
+ variant={
+ l={
+ conns = conns3(0,7,9),
+ desc = "Y-turnout (left)",
+ switchalt = "r",
+ switchmc = "off",
+ switchst = "l",
+ switchprefix = "",
+ conn_map = {2,1,1},
+ stmref = "sw",
+ tpsingle = true,
+ },
+ r={
+ conns = conns3(0,7,9),
+ desc = "Y-turnout (right)",
+ switchalt = "l",
+ switchmc = "on",
+ switchst = "r",
+ switchprefix = "",
+ conn_map = {3,1,1},
+ stmref = "sw",
+ }
+ },
+ regtp=true,
+ tpdefault="l",
+ rotation={"", "_30", "_45", "_60"},
+ statemaps = {
+ sw = { l = "l", r = "r"}
+ }
+}
+advtrains.ap.t_s3way={
+ v25_format = true,
+ regstep=1,
+ variant={
+ l={
+ conns = { {c=0}, {c=7}, {c=8}, {c=9}},
+ desc = "3-way turnout (left)",
+ switchalt = "s",
+ switchst="l",
+ switchprefix = "",
+ conn_map = {2,1,1,1},
+ stmref = "sw",
+ },
+ s={
+ conns = { {c=0}, {c=7}, {c=8}, {c=9}},
+ desc = "3-way turnout (straight)",
+ switchalt ="r",
+ switchst = "s",
+ switchprefix = "",
+ conn_map = {3,1,1,1},
+ stmref = "sw",
+ tpsingle = true,
+ },
+ r={
+ conns = { {c=0}, {c=7}, {c=8}, {c=9}},
+ desc = "3-way turnout (right)",
+ switchalt = "l",
+ switchst="r",
+ switchprefix = "",
+ conn_map = {4,1,1,1},
+ stmref = "sw",
+ }
+ },
+ regtp=true,
+ tpdefault="l",
+ rotation={"", "_30", "_45", "_60"},
+ statemaps = {
+ sw = { l = "l", s = "s", r = "r"}
+ }
+}
+advtrains.ap.t_30deg_slope={
+ v25_format = true,
+ regstep=1,
+ variant={
+ vst1={conns = conns(8,0,0,0.5), rail_y = 0.25, desc = "steep uphill 1/2", slope=true},
+ vst2={conns = conns(8,0,0.5,1), rail_y = 0.75, desc = "steep uphill 2/2", slope=true},
+ vst31={conns = conns(8,0,0,0.33), rail_y = 0.16, desc = "uphill 1/3", slope=true},
+ vst32={conns = conns(8,0,0.33,0.66), rail_y = 0.5, desc = "uphill 2/3", slope=true},
+ vst33={conns = conns(8,0,0.66,1), rail_y = 0.83, desc = "uphill 3/3", slope=true},
+ },
+ regsp=true,
+ slopeplacer={
+ [2]={"vst1", "vst2"},
+ [3]={"vst31", "vst32", "vst33"},
+ max=3,--highest entry
+ },
+ slopeplacer_45={
+ [2]={"vst1_45", "vst2_45"},
+ max=2,
+ },
+ rotation={"", "_30", "_45", "_60"},
+ trackworker={},
+ increativeinv={},
+}
+advtrains.ap.t_30deg_straightonly={
+ v25_format = true,
+ regstep=1,
+ variant={
+ st={
+ conns = conns(0,8),
+ desc = "straight",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "st",
+ },
+ },
+ regtp=true,
+ tpdefault="st",
+ rotation={"", "_30", "_45", "_60"},
+}
+advtrains.ap.t_30deg_straightonly_noplacer={
+ v25_format = true,
+ regstep=1,
+ variant={
+ st={
+ conns = conns(0,8),
+ desc = "straight",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "st",
+ },
+ },
+ tpdefault="st",
+ rotation={"", "_30", "_45", "_60"},
+}
+advtrains.ap.t_45deg={
+ v25_format = true,
+ regstep=2,
+ variant={
+ st={
+ conns = conns(0,8),
+ desc = "straight",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "cr",
+ },
+ cr={
+ conns = conns(0,6),
+ desc = "curve",
+ tpdouble = true,
+ trackworker = "swlst",
+ },
+ swlst={
+ conns = conns3(0,8,6),
+ desc = "left switch (straight)",
+ trackworker = "swrst",
+ switchalt = "cr",
+ switchmc = "on",
+ switchst = "st",
+ },
+ swlcr={
+ conns = conns3(0,6,8),
+ desc = "left switch (curve)",
+ trackworker = "swrcr",
+ switchalt = "st",
+ switchmc = "off",
+ switchst = "cr",
+ },
+ swrst={
+ conns = conns3(0,8,10),
+ desc = "right switch (straight)",
+ trackworker = "st",
+ switchalt = "cr",
+ switchmc = "on",
+ switchst = "st",
+ },
+ swrcr={
+ conns = conns3(0,10,8),
+ desc = "right switch (curve)",
+ trackworker = "st",
+ switchalt = "st",
+ switchmc = "off",
+ switchst = "cr",
+ },
+ },
+ regtp=true,
+ tpdefault="st",
+ trackworker={
+ ["swrcr"]="st",
+ ["swrst"]="st",
+ ["cr"]="swlst",
+ ["swlcr"]="swrcr",
+ ["swlst"]="swrst",
+ },
+ rotation={"", "_30", "_45", "_60"},
+}
+advtrains.ap.t_perpcrossing={
+ v25_format = true,
+ regstep = 1,
+ variant={
+ st={
+ conns = { {c=0}, {c=8}, {c=4}, {c=12} },
+ desc = "perpendicular crossing",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "st",
+ conn_map = {2,1,4,3},
+ },
+ },
+ regtp=true,
+ tpdefault="st",
+ rotation={"", "_30", "_45", "_60"},
+}
+advtrains.ap.t_90plusx_crossing={
+ v25_format = true,
+ regstep = 1,
+ variant={
+ ["30l"]={
+ conns = { {c=0}, {c=8}, {c=1}, {c=9} },
+ desc = "30/90 degree crossing (left)",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "45l",
+ conn_map = {2,1,4,3},
+ },
+ ["45l"]={
+ conns = { {c=0}, {c=8}, {c=2}, {c=10} },
+ desc = "45/90 degree crossing (left)",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "60l",
+ conn_map = {2,1,4,3},
+ },
+ ["60l"]={
+ conns = { {c=0}, {c=8}, {c=3}, {c=11}},
+ desc = "60/90 degree crossing (left)",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "60r",
+ conn_map = {2,1,4,3},
+ },
+ ["60r"]={
+ conns = { {c=0}, {c=8}, {c=5}, {c=13} },
+ desc = "60/90 degree crossing (right)",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "45r",
+ conn_map = {2,1,4,3},
+ },
+ ["45r"]={
+ conns = { {c=0}, {c=8}, {c=6}, {c=14} },
+ desc = "45/90 degree crossing (right)",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "30r",
+ conn_map = {2,1,4,3},
+ },
+ ["30r"]={
+ conns = { {c=0}, {c=8}, {c=7}, {c=15}},
+ desc = "30/90 degree crossing (right)",
+ tpdouble = true,
+ tpsingle = true,
+ trackworker = "30l",
+ conn_map = {2,1,4,3},
+ },
+ },
+ regtp=true,
+ tpdefault="30l",
+ rotation={""},
+ trackworker = {
+ ["30l"] = "45l",
+ ["45l"] = "60l",
+ ["60l"] = "60r",
+ ["60r"] = "45r",
+ ["45r"] = "30r",
+ ["30r"] = "30l",
+ }
+}
+
+advtrains.ap.t_diagonalcrossing = {
+ v25_format = true,
+ regstep=1,
+ variant={
+ ["30l45r"]={
+ conns = {{c=1}, {c=9}, {c=6}, {c=14}},
+ desc = "30left-45right diagonal crossing",
+ tpdouble=true,
+ tpsingle=true,
+ trackworker="60l30l",
+ conn_map = {2,1,4,3},
+ },
+ ["60l30l"]={
+ conns = {{c=3}, {c=11}, {c=1}, {c=9}},
+ desc = "30left-60right diagonal crossing",
+ tpdouble=true,
+ tpsingle=true,
+ trackworker="60l45r",
+ conn_map = {2,1,4,3},
+ },
+ ["60l45r"]={
+ conns = {{c=3}, {c=11}, {c=6}, {c=14}},
+ desc = "60left-45right diagonal crossing",
+ tpdouble=true,
+ tpsingle=true,
+ trackworker="60l60r",
+ conn_map = {2,1,4,3},
+ },
+ ["60l60r"]={
+ conns = {{c=3}, {c=11}, {c=5}, {c=13}},
+ desc = "60left-60right diagonal crossing",
+ tpdouble=true,
+ tpsingle=true,
+ trackworker="60r45l",
+ conn_map = {2,1,4,3},
+ },
+ --If 60l60r had a mirror image, it would be here, but it's symmetric.
+ -- 60l60r is also equivalent to 30l30r but rotated 90 degrees.
+ ["60r45l"]={
+ conns = {{c=5}, {c=13}, {c=2}, {c=10}},
+ desc = "60right-45left diagonal crossing",
+ tpdouble=true,
+ tpsingle=true,
+ trackworker="60r30r",
+ conn_map = {2,1,4,3},
+ },
+ ["60r30r"]={
+ conns = {{c=5}, {c=13}, {c=7}, {c=15}},
+ desc = "60right-30right diagonal crossing",
+ tpdouble=true,
+ tpsingle=true,
+ trackworker="30r45l",
+ conn_map = {2,1,4,3},
+ },
+ ["30r45l"]={
+ conns = {{c=7}, {c=15}, {c=2}, {c=10}},
+ desc = "30right-45left diagonal crossing",
+ tpdouble=true,
+ tpsingle=true,
+ trackworker="30l45r",
+ conn_map = {2,1,4,3},
+ },
+
+ },
+ regtp=true,
+ tpdefault="30l45r",
+ rotation={""},
+ trackworker = {
+ ["30l45r"] = "60l30l",
+ ["60l30l"] = "60l45r",
+ ["60l45r"] = "60l60r",
+ ["60l60r"] = "60r45l",
+ ["60r45l"] = "60r30r",
+ ["60r30r"] = "30r45l",
+ ["30r45l"] = "30l45r",
+ }
+}
+
+advtrains.trackpresets = advtrains.ap
+
+--definition format: ([] optional)
+--[[{
+ nodename_prefix
+ texture_prefix
+ [shared_texture]
+ models_prefix
+ models_suffix (with dot)
+ [shared_model]
+ formats={
+ st,cr,swlst,swlcr,swrst,swrcr,vst1,vst2
+ (each a table with indices 0-3, for if to register a rail with this 'rotation' table entry. nil is assumed as 'all', set {} to not register at all)
+ }
+ common={} change something on common rail appearance
+}
+[18.12.17] Note on new connection system:
+In order to support real rail crossing nodes and finally make the trackplacer respect switches, I changed the connection system.
+There can be a variable number of connections available. These are specified as tuples {c=<connection>, y=<rely>}
+The table "at_conns" consists of {<conn1>, <conn2>...}
+the "at_rail_y" property holds the value that was previously called "railheight"
+Depending on the number of connections:
+2 conns: regular rail
+3 conns: switch:
+ - when train passes in at conn1, will move out of conn2
+ - when train passes in at conn2 or conn3, will move out of conn1
+4 conns: cross (or cross switch, depending on arrangement of conns):
+ - conn1 <> conn2
+ - conn3 <> conn4
+]]
+
+-- Notify the user if digging the rail is not allowed
+local function can_dig_callback(pos, player)
+ local ok, reason = advtrains.can_dig_or_modify_track(pos)
+ if not ok and player then
+ minetest.chat_send_player(player:get_player_name(), S("This track can not be removed!") .. " " .. reason)
+ end
+ return ok
+end
+
+local function append_statemap_suffix(state_map, nnpref, rot)
+ local t = {}
+ for state, nn in pairs(state_map) do
+ t[state] = nnpref .. "_" .. nn .. rot
+ end
+ return t
+end
+
+function advtrains.default_suitable_substrate(upos)
+ return core.registered_nodes[core.get_node(upos).name]
+ and core.registered_nodes[core.get_node(upos).name].walkable
+end
+
+function advtrains.register_tracks(tracktype, def, preset)
+ if not preset.v25_format then
+ error("advtrains.register_tracks(): A track preset for pre-v2.5 is used with advtrains 2.5+. Mod probably defines own track preset instead of using it from the advtrains.ap table! Please check track mod compatibility!")
+ end
+
+ if preset.regtp then
+ local nnprefix = def.nodename_prefix
+ minetest.register_craftitem(":"..nnprefix.."_placer", {
+ description = def.description,
+ inventory_image = def.texture_prefix.."_placer.png",
+ wield_image = def.texture_prefix.."_placer.png",
+ groups={advtrains_trackplacer=1, digtron_on_place=1},
+ liquids_pointable = false,
+ on_place = function(itemstack, placer, pointed_thing)
+ local name = placer:get_player_name()
+ if not name then
+ return itemstack, false
+ end
+ if pointed_thing.type=="node" then
+ local pos=pointed_thing.above
+ local upos=vector.subtract(pointed_thing.above, {x=0, y=1, z=0})
+ if not advtrains.check_track_protection(pos, name) then
+ return itemstack, false
+ end
+ if minetest.registered_nodes[minetest.get_node(pos).name] and minetest.registered_nodes[minetest.get_node(pos).name].buildable_to then
+ if (def.suitable_substrate and def.suitable_substrate or advtrains.default_suitable_substrate)(upos) then
+ -- minetest.chat_send_all(nnprefix)
+ local yaw = placer:get_look_horizontal()
+ advtrains.trackplacer.place_track(pos, nnprefix, name, yaw)
+ if not advtrains.is_creative(name) then
+ itemstack:take_item()
+ end
+ end
+ end
+ end
+ return itemstack, true
+ end,
+ })
+
+ advtrains.trackplacer.set_default_place_candidate(def.nodename_prefix, def.nodename_prefix.."_"..preset.tpdefault)
+ end
+ if preset.regsp then
+ advtrains.slope.register_placer(def, preset)
+ end
+
+ for suffix, var in pairs(preset.variant) do
+ for rotid, rotation in ipairs(preset.rotation) do
+ if not def.formats[suffix] or def.formats[suffix][rotid] then
+ local img_suffix = suffix..rotation
+ local ndef = advtrains.merge_tables({
+ description=def.description.."("..(var.desc or "any")..rotation..")",
+ drawtype = "mesh",
+ paramtype="light",
+ paramtype2="facedir",
+ use_texture_alpha = "clip",
+ walkable = false,
+ selection_box = {
+ type = "fixed",
+ fixed = {-1/2-1/16, -1/2, -1/2, 1/2+1/16, -1/2+2/16, 1/2},
+ },
+
+ mesh = def.shared_model or (def.models_prefix.."_"..img_suffix..def.models_suffix),
+ tiles = {def.shared_texture or (def.texture_prefix.."_"..img_suffix..".png"), def.second_texture},
+
+ groups = {
+ attached_node = advtrains.IGNORE_WORLD and 0 or 1,
+ advtrains_track=1,
+ ["advtrains_track_"..tracktype]=1,
+ save_in_at_nodedb=1,
+ dig_immediate=2,
+ not_in_creative_inventory=1,
+ not_blocking_trains=1,
+ },
+
+ can_dig = advtrains.track_can_dig_callback,
+ after_dig_node = advtrains.track_update_callback,
+ after_place_node = advtrains.track_update_callback,
+
+ at_rail_y = var.rail_y
+ }, def.common or {})
+
+ if preset.regtp then
+ ndef.drop = def.nodename_prefix.."_placer"
+ end
+ if preset.regsp and var.slope then
+ ndef.drop = def.nodename_prefix.."_slopeplacer"
+ end
+
+ --connections
+ ndef.at_conns = advtrains.rotate_conn_by(var.conns, (rotid-1)*preset.regstep)
+ -- NEW since 2.5
+ ndef.at_conn_map = var.conn_map
+
+ local ndef_avt_table = {}
+
+ if var.switchalt and var.switchst then
+ -- NEW since 2.5
+ ndef.on_rightclick = advtrains.state_node_on_rightclick_callback
+ ndef_avt_table.node_state = var.switchst
+ ndef_avt_table.node_next_state = var.switchalt
+ -- obtain and build statemap
+ local state_map = preset.statemaps[var.stmref]
+ if not state_map then error("On registering "..def.nodename_prefix.."_"..suffix..rotation..", stmref of variant doesn't reference entry in preset.statemaps") end
+ ndef_avt_table.node_state_map = append_statemap_suffix(state_map, def.nodename_prefix, rotation)
+
+ if var.switchmc then
+ local vswitchalt = var.switchalt
+ ndef.mesecons = {effector = {
+ ["action_"..var.switchmc] = function(pos, node)
+ advtrains.setstate(pos, vswitchalt, node)
+ end,
+ rules=advtrains.meseconrules
+ }}
+ end
+ end
+
+ local adef={}
+ if def.get_additional_definiton then
+ adef=def.get_additional_definiton(def, preset, suffix, rotation)
+ end
+ ndef = advtrains.merge_tables(ndef, adef)
+
+ -- insert getstate/setstate functions after merging the additional definitions
+ if ndef_avt_table then
+ ndef.advtrains = advtrains.merge_tables(ndef.advtrains or {}, ndef_avt_table)
+ end
+
+ -- NEW since 2.5: add appropriate fields for trackworker rotation
+ -- get the next rotation step
+ local num_rots = #preset.rotation
+ if rotid >= num_rots then
+ ndef.advtrains.trackworker_next_rot = def.nodename_prefix.."_"..suffix..preset.rotation[1]
+ ndef.advtrains.trackworker_rot_incr_param2 = true
+ else
+ ndef.advtrains.trackworker_next_rot = def.nodename_prefix.."_"..suffix..preset.rotation[rotid+1]
+ end
+ if var.trackworker then
+ ndef.advtrains.trackworker_next_var = def.nodename_prefix.."_"..var.trackworker..rotation
+ end
+
+ local the_node_name = def.nodename_prefix.."_"..suffix..rotation
+
+ --trackplacer
+ if preset.regtp and (var.tpsingle or var.tpdouble) then
+ advtrains.trackplacer.register_candidate(
+ def.nodename_prefix,
+ the_node_name,
+ ndef,
+ var.tpsingle,
+ var.tpdouble,
+ true)
+ end
+
+ -- All set, go ahead and register node!
+ --atdebug("track_reg_helper: Registering ",the_node_name," as",ndef)
+ minetest.register_node(":"..the_node_name, ndef)
+
+ end
+ end
+ end
+end
+
+-- slope placer. Defined in register_tracks.
+--crafted with rail and gravel
+local sl={}
+function sl.register_placer(def, preset)
+ minetest.register_craftitem(":"..def.nodename_prefix.."_slopeplacer",{
+ description = S("@1 Slope", def.description),
+ inventory_image = def.texture_prefix.."_slopeplacer.png",
+ wield_image = def.texture_prefix.."_slopeplacer.png",
+ groups={},
+ on_place = sl.create_slopeplacer_on_place(def, preset)
+ })
+end
+--(itemstack, placer, pointed_thing)
+function sl.create_slopeplacer_on_place(def, preset)
+ return function(istack, player, pt)
+ if not pt.type=="node" then
+ minetest.chat_send_player(player:get_player_name(), S("Can't place: not pointing at node"))
+ return istack
+ end
+ local pos=pt.above
+ if not pos then
+ minetest.chat_send_player(player:get_player_name(), S("Can't place: not pointing at node"))
+ return istack
+ end
+ local node=minetest.get_node(pos)
+ if not minetest.registered_nodes[node.name] or not minetest.registered_nodes[node.name].buildable_to then
+ minetest.chat_send_player(player:get_player_name(), S("Can't place: space occupied!"))
+ return istack
+ end
+ if not advtrains.check_track_protection(pos, player:get_player_name()) then
+ minetest.record_protection_violation(pos, player:get_player_name())
+ return istack
+ end
+ --determine player orientation (only horizontal component)
+ --get_look_horizontal may not be available
+ local yaw=player.get_look_horizontal and player:get_look_horizontal() or (player:get_look_yaw() - math.pi/2)
+
+ --rounding unit vectors is a nice way for selecting 1 of 8 directions since sin(30°) is 0.5.
+ local dirvec={x=math.floor(math.sin(-yaw)+0.5), y=0, z=math.floor(math.cos(-yaw)+0.5)}
+ --translate to direction to look up inside the preset table
+ local param2, rot45=({
+ [-1]={
+ [-1]=2,
+ [0]=3,
+ [1]=3,
+ },
+ [0]={
+ [-1]=2,
+ [1]=0,
+ },
+ [1]={
+ [-1]=1,
+ [0]=1,
+ [1]=0,
+ },
+ })[dirvec.x][dirvec.z], dirvec.x~=0 and dirvec.z~=0
+ local lookup=preset.slopeplacer
+ if rot45 then lookup=preset.slopeplacer_45 end
+
+ --go unitvector forward and look how far the next node is
+ local step=1
+ while step<=lookup.max do
+ local node=minetest.get_node(vector.add(pos, dirvec))
+ --next node solid?
+ if not minetest.registered_nodes[node.name] or not minetest.registered_nodes[node.name].buildable_to or advtrains.is_protected(pos, player:get_player_name()) then
+ --do slopes of this distance exist?
+ if lookup[step] then
+ if minetest.settings:get_bool("creative_mode") or istack:get_count()>=step then
+ --start placing
+ local placenodes=lookup[step]
+ while step>0 do
+ minetest.set_node(pos, {name=def.nodename_prefix.."_"..placenodes[step], param2=param2})
+ if not minetest.settings:get_bool("creative_mode") then
+ istack:take_item()
+ end
+ step=step-1
+ pos=vector.subtract(pos, dirvec)
+ end
+ else
+ minetest.chat_send_player(player:get_player_name(), S("Can't place: Not enough slope items left (@1 required)", step))
+ end
+ else
+ minetest.chat_send_player(player:get_player_name(), S("Can't place: There's no slope of length @1",step))
+ end
+ return istack
+ end
+ step=step+1
+ pos=vector.add(pos, dirvec)
+ end
+ minetest.chat_send_player(player:get_player_name(), S("Can't place: no supporting node at upper end."))
+ return itemstack
+ end
+end
+
+advtrains.slope=sl
diff --git a/advtrains/trackdb_legacy.lua b/advtrains/trackdb_legacy.lua
deleted file mode 100644
index 99349e8..0000000
--- a/advtrains/trackdb_legacy.lua
+++ /dev/null
@@ -1,27 +0,0 @@
---trackdb_legacy.lua
---loads the (old) track database. the only use for this is to provide data for rails that haven't been written into the ndb database.
---nothing will be saved.
---if the user thinks that he has loaded every track in his world at least once, he can delete the track database.
-
---trackdb[[y][x][z]={conn1, conn2, rely1, rely2, railheight}
-
-
---trackdb keeps its own save file.
-advtrains.fpath_tdb=minetest.get_worldpath().."/advtrains_trackdb2"
-local file, err = io.open(advtrains.fpath_tdb, "r")
-if not file then
- atprint("Not loading a trackdb file.")
-else
- local tbl = minetest.deserialize(file:read("*a"))
- if type(tbl) == "table" then
- advtrains.trackdb=tbl
- atprint("Loaded trackdb file.")
- end
- file:close()
-end
-
-
-
-
-
-
diff --git a/advtrains/trackplacer.lua b/advtrains/trackplacer.lua
index fe76290..0bcde77 100644
--- a/advtrains/trackplacer.lua
+++ b/advtrains/trackplacer.lua
@@ -1,431 +1,338 @@
--trackplacer.lua
--holds code for the track-placing system. the default 'track' item will be a craftitem that places rails as needed. this will neither place or change switches nor place vertical rails.
+-- Get current translator
+local S = advtrains.translate
+
--all new trackplacer code
local tp={
- tracks={}
+ groups={}
}
-function tp.register_tracktype(nnprefix, n_suffix)
- if tp.tracks[nnprefix] then return end--due to the separate registration of slopes and flats for the same nnpref, definition would be overridden here. just don't.
- tp.tracks[nnprefix]={
- default=n_suffix,
- single_conn={},
- single_conn_1={},
- single_conn_2={},
- double_conn={},
- double_conn_1={},
- double_conn_2={},
- --keys:conn1_conn2 (example:1_4)
- --values:{name=x, param2=x}
- twcycle={},
- twrotate={},--indexed by suffix, list, tells order of rotations
- modify={},
- }
-end
-function tp.add_double_conn(nnprefix, suffix, rotation, conns)
- local nodename=nnprefix.."_"..suffix..rotation
- for i=0,3 do
- tp.tracks[nnprefix].double_conn[((conns.conn1+4*i)%16).."_"..((conns.conn2+4*i)%16)]={name=nodename, param2=i}
- tp.tracks[nnprefix].double_conn[((conns.conn2+4*i)%16).."_"..((conns.conn1+4*i)%16)]={name=nodename, param2=i}
- tp.tracks[nnprefix].double_conn_1[((conns.conn1+4*i)%16).."_"..((conns.conn2+4*i)%16)]={name=nodename, param2=i}
- tp.tracks[nnprefix].double_conn_2[((conns.conn2+4*i)%16).."_"..((conns.conn1+4*i)%16)]={name=nodename, param2=i}
- end
- tp.tracks[nnprefix].modify[nodename]=true
-end
-function tp.add_single_conn(nnprefix, suffix, rotation, conns)
- local nodename=nnprefix.."_"..suffix..rotation
- for i=0,3 do
- tp.tracks[nnprefix].single_conn[((conns.conn1+4*i)%16)]={name=nodename, param2=i}
- tp.tracks[nnprefix].single_conn[((conns.conn2+4*i)%16)]={name=nodename, param2=i}
- tp.tracks[nnprefix].single_conn_1[((conns.conn1+4*i)%16)]={name=nodename, param2=i}
- tp.tracks[nnprefix].single_conn_2[((conns.conn2+4*i)%16)]={name=nodename, param2=i}
- end
- tp.tracks[nnprefix].modify[nodename]=true
-end
+--[[ New in version 2.5:
+The track placer no longer uses hacky nodename pattern matching.
+The base criterion for rotating or matching tracks is the common "ndef.advtrains.track_place_group" property.
+Only rails where this field is set are considered for replacement. Other rails can still be considered for connection.
+Replacement ("bending") of rails can only happen within their respective track place group. Only two-conn rails are allowed in the trackplacer.
-function tp.add_worked(nnprefix, suffix, rotation, cycle_follows)
- tp.tracks[nnprefix].twcycle[suffix]=cycle_follows
- if not tp.tracks[nnprefix].twrotate[suffix] then tp.tracks[nnprefix].twrotate[suffix]={} end
- table.insert(tp.tracks[nnprefix].twrotate[suffix], rotation)
-end
+The track registration functions register the candidates for any given track_place_group in two separate collections:
+- double: tracks that can be used to connect both ends of the rail
+- single: tracks that will be used to connect conn1 when only a single end is to be connected
+When track placing is requested, the calling code just supplies the track_place_group to be placed.
---[[
- rewrite algorithm.
- selection criteria: these will never be changed or even selected:
- - tracks being already connected on both sides
- - tracks that are already connected on one side but are not bendable to the desired position
- the following situations can occur:
- 1. there are two more than two rails around
- 1.1 there is one or more subset(s) that can be directly connected
- -> choose the first possibility
- 2.2 not
- -> choose the first one and orient straight
- 2. there's exactly 1 rail around
- -> choose and orient straight
- 3. there's no rail around
- -> set straight
-]]
+]]--
-local function istrackandbc(pos_p, conn)
- local tpos = pos_p
- local cnode=minetest.get_node(advtrains.dirCoordSet(tpos, conn.c))
- if advtrains.is_track_and_drives_on(cnode.name, advtrains.all_tracktypes) then
- local cconns=advtrains.get_track_connections(cnode.name, cnode.param2)
- return advtrains.conn_matches_to(conn, cconns)
- end
- --try the same 1 node below
- tpos = {x=tpos.x, y=tpos.y-1, z=tpos.z}
- cnode=minetest.get_node(advtrains.dirCoordSet(tpos, conn.c))
- if advtrains.is_track_and_drives_on(cnode.name, advtrains.all_tracktypes) then
- local cconns=advtrains.get_track_connections(cnode.name, cnode.param2)
- return advtrains.conn_matches_to(conn, cconns)
- end
- return false
+local function rotate(conn, rot)
+ return (conn + rot) % 16
end
-function tp.find_already_connected(pos)
- local dnode=minetest.get_node(pos)
- local dconns=advtrains.get_track_connections(dnode.name, dnode.param2)
- local found_conn
- for connid, conn in ipairs(dconns) do
- if istrackandbc(pos, conn) then
- if found_conn then --we found one in previous iteration
- return true, true --signal that it's connected
- else
- found_conn = conn.c
+-- Register a track node as candidate
+-- tpg: the track place group to register the candidates for
+-- NOTE: This value is automatically added to the node definition (ndef) in field ndef.advtrains.track_place_group!
+-- name, ndef: the node name and node definition table to register
+-- as_single: whether the rail should be considered as candidate for one-endpoint connection
+-- Typically only set for the straight rail variants
+-- as_double: whether the rail should be considered as candidate for two-endpoint connection
+-- Typically set for straights and curves
+-- ignore_2conn_for_legacy_xing: skips the 2-connection assertion - ONLY for compatibility with the legacy crossing nodes, DO NOT USE!
+function tp.register_candidate(tpg, name, ndef, as_single, as_double, ignore_2conn_for_legacy_xing)
+ --atdebug("TP Register candidate:",tpg, name, as_single, as_double)
+ --get or create TP group
+ if not tp.groups[tpg] then
+ tp.groups[tpg] = {double = {}, single1 = {}, single2 = {}, default = {name = name, param2 = 0} }
+ -- note: this causes the first candidate to ever be registered to be the default (which is typically what you want)
+ -- But it can be overwritten using tp.set_default_place_candidate
+ end
+ local g = tp.groups[tpg]
+
+ -- get conns
+ if not ignore_2conn_for_legacy_xing then
+ assert(#ndef.at_conns == 2)
+ end
+ local c1, c2 = ndef.at_conns[1].c, ndef.at_conns[2].c
+ local is_symmetrical = (rotate(c1, 8) == c2)
+
+ -- store all possible rotations (param2 values)
+ for i=0,3 do
+ if as_double then
+ g.double[rotate(c1,i*4).."_"..rotate(c2,i*4)] = {name=name, param2=i}
+ if not is_symmetrical then
+ g.double[rotate(c2,i*4).."_"..rotate(c1,i*4)] = {name=name, param2=i}
+ -- if the track is unsymmetric (e.g. a curve), we may require the "wrong" orientation to fill a gap.
end
end
+ if as_single then
+ g.single1[rotate(c1,i*4)] = {name=name, param2=i}
+ g.single2[rotate(c2,i*4)] = {name=name, param2=i}
+ end
+ end
+
+ -- Set track place group on the node
+ if not ndef.advtrains then
+ ndef.advtrains = {}
end
- return found_conn
+ ndef.advtrains.track_place_group = tpg
end
-function tp.rail_and_can_be_bent(originpos, conn)
- local pos=advtrains.dirCoordSet(originpos, conn)
- local newdir=(conn+8)%16
- local node=minetest.get_node(pos)
- if not advtrains.is_track_and_drives_on(node.name, advtrains.all_tracktypes) then
+
+-- Sets the node that is placed by the track placer when there is no track nearby. param2 defaults to 0
+function tp.set_default_place_candidate(tpg, name, param2)
+ if not tp.groups[tpg] then
+ tp.groups[tpg] = {double = {}, single1 = {}, single2 = {}, default = {name = name, param2 = param2 or 0} }
+ else
+ tp.groups[tpg].default = {name = name, param2 = param2 or 0}
+ end
+end
+
+local function check_or_bend_rail(origin, dir, pname, commit)
+ local pos = advtrains.pos_add_dir(origin, dir)
+ local back_dir = advtrains.oppd(dir);
+
+ local node_ok, conns = advtrains.get_rail_info_at(pos)
+ if not node_ok then
+ -- try the node one level below
+ pos.y = pos.y - 1
+ node_ok, conns = advtrains.get_rail_info_at(pos)
+ end
+ if not node_ok then
return false
end
- local ndef=minetest.registered_nodes[node.name]
- local nnpref = ndef and ndef.at_nnpref
- if not nnpref then return false end
- local tr=tp.tracks[nnpref]
- if not tr then return false end
- if not tr.modify[node.name] then
- --we actually can use this rail, but only if it already points to the desired direction.
- if advtrains.is_track_and_drives_on(node.name, advtrains.all_tracktypes) then
- local cconns=advtrains.get_track_connections(node.name, node.param2)
- return advtrains.conn_matches_to(conn, cconns)
+ -- if one conn of the node here already points towards us, nothing to do
+ for connid, conn in ipairs(conns) do
+ if back_dir == conn.c then
+ return true
end
end
- -- If the rail is not allowed to be modified, also only use if already in desired direction
+ -- can we bend the node here?
+ local node = advtrains.ndb.get_node(pos)
+ local ndef = minetest.registered_nodes[node.name]
+ if not ndef or not ndef.advtrains or not ndef.advtrains.track_place_group then
+ return false
+ end
+ -- now the track must be two-conn, else it wouldn't be allowed to have track_place_group set.
+ --assert(#conns == 2) -- cannot check here, because of legacy crossing hack
+ -- Is player and game allowed to do this?
if not advtrains.can_dig_or_modify_track(pos) then
- local cconns=advtrains.get_track_connections(node.name, node.param2)
- return advtrains.conn_matches_to(conn, cconns)
+ return false
end
- --rail at other end?
- local adj1, adj2=tp.find_already_connected(pos)
- if adj1 and adj2 then
- return false--dont destroy existing track
- elseif adj1 and not adj2 then
- if tr.double_conn[adj1.."_"..newdir] then
- return true--if exists, connect new rail and old end
- end
+ if not advtrains.check_track_protection(pos, pname) then
return false
- else
- if tr.single_conn[newdir] then--just rotate old rail to right orientation
- return true
+ end
+ -- we confirmed that track can be modified. Does there exist a suitable connection candidate?
+ -- check if there are any unbound ends
+ local bound_connids = {}
+ for connid, conn in ipairs(conns) do
+ local adj_pos, adj_connid = advtrains.get_adjacent_rail(pos, conns, connid)
+ if adj_pos then
+ bound_connids[#bound_connids+1] = connid
end
- return false
end
-end
-function tp.bend_rail(originpos, conn)
- local pos=advtrains.dirCoordSet(originpos, conn)
- local newdir=advtrains.oppd(conn)
- local node=minetest.get_node(pos)
- local ndef=minetest.registered_nodes[node.name]
- local nnpref = ndef and ndef.at_nnpref
- if not nnpref then return false end
- local tr=tp.tracks[nnpref]
- if not tr then return false end
- --is rail already connected? no need to bend.
- local conns=advtrains.get_track_connections(node.name, node.param2)
- if advtrains.conn_matches_to(conn, conns) then
- return
+ -- depending on the nummber of ends, decide
+ if #bound_connids == 2 then
+ -- rail is within a fixed track, do not break up
+ return false
end
- --rail at other end?
- local adj1, adj2=tp.find_already_connected(pos)
- if adj1 and adj2 then
- return false--dont destroy existing track
- elseif adj1 and not adj2 then
- if tr.double_conn[adj1.."_"..newdir] then
- advtrains.ndb.swap_node(pos, tr.double_conn[adj1.."_"..newdir])
- return true--if exists, connect new rail and old end
+ -- obtain the group table
+ local g = tp.groups[ndef.advtrains.track_place_group]
+ if #bound_connids == 1 then
+ -- we can attempt double
+ local bound_dir = conns[bound_connids[1]].c
+ if g.double[back_dir.."_"..bound_dir] then
+ if commit then
+ advtrains.ndb.swap_node(pos, g.double[back_dir.."_"..bound_dir])
+ end
+ return true
end
- return false
else
- if tr.single_conn[newdir] then--just rotate old rail to right orientation
- advtrains.ndb.swap_node(pos, tr.single_conn[newdir])
+ -- rail is entirely unbound, we can attempt single1
+ if g.single1[back_dir] then
+ if commit then
+ advtrains.ndb.swap_node(pos, g.single1[back_dir])
+ end
return true
end
- return false
end
end
-function tp.placetrack(pos, nnpref, placer, itemstack, pointed_thing, yaw)
- --1. find all rails that are likely to be connected
- local tr=tp.tracks[nnpref]
- local p_rails={}
- local p_railpos={}
+
+local function track_place_node(pos, node, ndef_p, pname)
+ --atdebug("track_place_node: ",pos, node)
+ advtrains.ndb.swap_node(pos, node)
+ local ndef = ndef_p or minetest.registered_nodes[node.name]
+ if ndef and ndef.after_place_node then
+ -- resolve player again
+ local player = pname and core.get_player_by_name(pname) or nil
+ ndef.after_place_node(pos, player) -- note: itemstack and pointed_thing are NOT available here anymore (crap!)
+ end
+end
+
+
+-- Main API function to place a track. Replaces the older "placetrack"
+-- This function will attempt to place a track of the specified track placing group at the specified position, connecting it
+-- with neighboring rails. Neighboring rails can themselves be replaced ("bent") within their own track place group,
+-- if the player is permitted to do this.
+-- Order of preference is:
+-- Connect two track ends if possible
+-- Connect one track end if any rail is near
+-- Place the default track if no tracks are near
+-- The function returns true on success.
+function tp.place_track(pos, tpg, pname, yaw)
+ -- 1. collect neighboring tracks and whether they can be connected
+ --atdebug("tp.place_track(",pos, tpg, pname, yaw,")")
+ local cand = {}
for i=0,15 do
- if tp.rail_and_can_be_bent(pos, i, nnpref) then
- p_rails[#p_rails+1]=i
- p_railpos[i] = pos
- else
- local upos = {x=pos.x, y=pos.y-1, z=pos.z}
- if tp.rail_and_can_be_bent(upos, i, nnpref) then
- p_rails[#p_rails+1]=i
- p_railpos[i] = upos
- end
+ if check_or_bend_rail(pos, i, pname) then
+ cand[#cand+1] = i
end
end
-
- -- try double_conn
- if #p_rails > 1 then
- --iterate subsets
- for k1, conn1 in ipairs(p_rails) do
- for k2, conn2 in ipairs(p_rails) do
- if k1~=k2 then
- local dconn1 = tr.double_conn_1
- local dconn2 = tr.double_conn_2
- if not (advtrains.yawToDirection(yaw, conn1, conn2) == conn1) then
- dconn1 = tr.double_conn_2
- dconn2 = tr.double_conn_1
- end
- -- Checks are made this way round so that dconn1 has priority (this will make arrows of atc rails
- -- point in the right direction)
- local using
- if (dconn2[conn1.."_"..conn2]) then
- using = dconn2[conn1.."_"..conn2]
- end
- if (dconn1[conn1.."_"..conn2]) then
- using = dconn1[conn1.."_"..conn2]
- end
- if using then
- -- has found a fitting rail in either direction
- -- if not, continue loop
- tp.bend_rail(p_railpos[conn1], conn1, nnpref)
- tp.bend_rail(p_railpos[conn2], conn2, nnpref)
- advtrains.ndb.swap_node(pos, using)
- local nname=using.name
- if minetest.registered_nodes[nname] and minetest.registered_nodes[nname].after_place_node then
- minetest.registered_nodes[nname].after_place_node(pos, placer, itemstack, pointed_thing)
- end
- return
+ --atdebug("Candidates: ",cand)
+ -- obtain the group table
+ local g = tp.groups[tpg]
+ if not g then
+ error("tp.place_track: for tpg="..tpg.." couldn't find the group table")
+ end
+ --atdebug("Group table:",g)
+ -- 2. try all possible two-endpoint connections
+ for k1, conn1 in ipairs(cand) do
+ for k2, conn2 in ipairs(cand) do
+ if k1~=k2 then
+ -- order of conn1/conn2: prefer conn1 being in the direction of the player facing.
+ -- the combination the other way round will be run through in a later loop iteration
+ if advtrains.yawToDirection(yaw, conn1, conn2) == conn1 then
+ -- does there exist a suitable double-connection rail?
+ --atdebug("Try double conn: ",conn1, conn2)
+ local node = g.double[conn1.."_"..conn2]
+ if node then
+ check_or_bend_rail(pos, conn1, pname, true)
+ check_or_bend_rail(pos, conn2, pname, true)
+ track_place_node(pos, node, nil, pname) -- calls after_place_node implicitly
+ return true
end
end
end
end
end
- -- try single_conn
- if #p_rails > 0 then
- for ix, p_rail in ipairs(p_rails) do
- local sconn1 = tr.single_conn_1
- local sconn2 = tr.single_conn_2
- if not (advtrains.yawToDirection(yaw, p_rail, (p_rail+8)%16) == p_rail) then
- sconn1 = tr.single_conn_2
- sconn2 = tr.single_conn_1
- end
- if sconn1[p_rail] then
- local using = sconn1[p_rail]
- tp.bend_rail(p_railpos[p_rail], p_rail, nnpref)
- advtrains.ndb.swap_node(pos, using)
- local nname=using.name
- if minetest.registered_nodes[nname] and minetest.registered_nodes[nname].after_place_node then
- minetest.registered_nodes[nname].after_place_node(pos, placer, itemstack, pointed_thing)
- end
- return
- end
- if sconn2[p_rail] then
- local using = sconn2[p_rail]
- tp.bend_rail(p_railpos[p_rail], p_rail, nnpref)
- advtrains.ndb.swap_node(pos, using)
- local nname=using.name
- if minetest.registered_nodes[nname] and minetest.registered_nodes[nname].after_place_node then
- minetest.registered_nodes[nname].after_place_node(pos, placer, itemstack, pointed_thing)
- end
- return
- end
+ -- 3. try all possible one_endpoint connections
+ for k1, conn1 in ipairs(cand) do
+ -- select single1 or single2? depending on yaw
+ local single
+ if advtrains.yawToDirection(yaw, conn1, advtrains.oppd(conn1)) == conn1 then
+ single = g.single1
+ else
+ single = g.single2
+ end
+ --atdebug("Try single conn: ",conn1)
+ local node = single[conn1]
+ if node then
+ check_or_bend_rail(pos, conn1, pname, true)
+ track_place_node(pos, node, nil, pname) -- calls after_place_node implicitly
+ return true
end
end
- --use default
- minetest.set_node(pos, {name=nnpref.."_"..tr.default})
- if minetest.registered_nodes[nnpref.."_"..tr.default] and minetest.registered_nodes[nnpref.."_"..tr.default].after_place_node then
- minetest.registered_nodes[nnpref.."_"..tr.default].after_place_node(pos, placer, itemstack, pointed_thing)
- end
+ -- 4. if nothing worked, set the default
+ local node = g.default
+ track_place_node(pos, node, nil, pname) -- calls after_place_node implicitly
+ return true
end
-function tp.register_track_placer(nnprefix, imgprefix, dispname, def)
- minetest.register_craftitem(":"..nnprefix.."_placer",{
- description = dispname,
- inventory_image = imgprefix.."_placer.png",
- wield_image = imgprefix.."_placer.png",
- groups={advtrains_trackplacer=1, digtron_on_place=1},
- liquids_pointable = def.liquids_pointable,
- on_place = function(itemstack, placer, pointed_thing)
- local name = placer:get_player_name()
- if not name then
- return itemstack, false
- end
- if pointed_thing.type=="node" then
- local pos=pointed_thing.above
- local upos=vector.subtract(pointed_thing.above, {x=0, y=1, z=0})
- if not advtrains.check_track_protection(pos, name) then
- return itemstack, false
- end
- if minetest.registered_nodes[minetest.get_node(pos).name] and minetest.registered_nodes[minetest.get_node(pos).name].buildable_to then
- local s
- if def.suitable_substrate then
- s = def.suitable_substrate(upos)
- else
- s = minetest.registered_nodes[minetest.get_node(upos).name] and minetest.registered_nodes[minetest.get_node(upos).name].walkable
- end
- if s then
--- minetest.chat_send_all(nnprefix)
- local yaw = placer:get_look_horizontal()
- tp.placetrack(pos, nnprefix, placer, itemstack, pointed_thing, yaw)
- if not advtrains.is_creative(name) then
- itemstack:take_item()
- end
- end
- end
- end
- return itemstack, true
- end,
- })
-end
-
+-- TRACK WORKER --
minetest.register_craftitem("advtrains:trackworker",{
- description = attrans("Track Worker Tool\n\nLeft-click: change rail type (straight/curve/switch)\nRight-click: rotate rail/bumper/signal/etc."),
+ description = S("Track Worker Tool\n\nLeft-click: change rail type (straight/curve/switch)\nRight-click: rotate object"),
groups = {cracky=1}, -- key=name, value=rating; rating=1..3.
inventory_image = "advtrains_trackworker.png",
wield_image = "advtrains_trackworker.png",
stack_max = 1,
on_place = function(itemstack, placer, pointed_thing)
- local name = placer:get_player_name()
- if not name then
+ local name = placer:get_player_name()
+ if not name then
+ return
+ end
+ local has_aux1_down = placer:get_player_control().aux1
+ if pointed_thing.type=="node" then
+ local pos=pointed_thing.under
+ if not advtrains.check_track_protection(pos, name) then
return
end
- local has_aux1_down = placer:get_player_control().aux1
- if pointed_thing.type=="node" then
- local pos=pointed_thing.under
- if not advtrains.check_track_protection(pos, name) then
- return
- end
- local node=minetest.get_node(pos)
+ local node=minetest.get_node(pos)
- --if not advtrains.is_track_and_drives_on(minetest.get_node(pos).name, advtrains.all_tracktypes) then return end
-
- local nnprefix, suffix, rotation=string.match(node.name, "^(.+)_([^_]+)(_[^_]+)$")
- --atdebug(node.name.."\npattern recognizes:"..nnprefix.." / "..suffix.." / "..rotation)
- --atdebug("nntab: ",tp.tracks[nnprefix])
- if not tp.tracks[nnprefix] or not tp.tracks[nnprefix].twrotate[suffix] then
- nnprefix, suffix=string.match(node.name, "^(.+)_([^_]+)$")
- rotation = ""
- if not tp.tracks[nnprefix] or not tp.tracks[nnprefix].twrotate[suffix] then
- minetest.chat_send_player(placer:get_player_name(), attrans("This node can't be rotated using the trackworker!"))
- return
- end
- end
-
- -- check if the node is modify-protected
- if advtrains.is_track_and_drives_on(minetest.get_node(pos).name, advtrains.all_tracktypes) then
- -- is a track, we can query
- local can_modify, reason = advtrains.can_dig_or_modify_track(pos)
- if not can_modify then
- local str = attrans("This track can not be rotated!")
- if reason then
- str = str .. " " .. reason
- end
- minetest.chat_send_player(placer:get_player_name(), str)
- return
+ -- New since 2.5: only the fields in the node definition are considered, no more hacky pattern matching on the nodename
+
+ local ndef = minetest.registered_nodes[node.name]
+
+ if not ndef.advtrains or not ndef.advtrains.trackworker_next_rot then
+ minetest.chat_send_player(placer:get_player_name(), S("This node can't be rotated using the trackworker!"))
+ return
+ end
+
+ -- check if the node is modify-protected
+ if advtrains.is_track(node.name) then
+ -- is a track, we can query
+ local can_modify, reason = advtrains.can_dig_or_modify_track(pos)
+ if not can_modify then
+ local str = S("This track can not be rotated!")
+ if reason then
+ str = str .. " " .. reason
end
- end
-
- if has_aux1_down then
- --feature: flip the node by 180°
- --i've always wanted this!
- advtrains.ndb.swap_node(pos, {name=node.name, param2=(node.param2+2)%4})
- return
- end
-
- local modext=tp.tracks[nnprefix].twrotate[suffix]
-
- if rotation==modext[#modext] then --increase param2
- advtrains.ndb.swap_node(pos, {name=nnprefix.."_"..suffix..modext[1], param2=(node.param2+1)%4})
+ minetest.chat_send_player(placer:get_player_name(), str)
return
- else
- local modpos
- for k,v in pairs(modext) do
- if v==rotation then modpos=k end
- end
- if not modpos then
- minetest.chat_send_player(placer:get_player_name(), attrans("This node can't be rotated using the trackworker!"))
- return
- end
- advtrains.ndb.swap_node(pos, {name=nnprefix.."_"..suffix..modext[modpos+1], param2=node.param2})
end
end
+
+ if has_aux1_down then
+ --feature: flip the node by 180°
+ --i've always wanted this!
+ advtrains.ndb.swap_node(pos, {name=node.name, param2=(node.param2+2)%4})
+ return
+ end
+
+ local new_node = {name = ndef.advtrains.trackworker_next_rot, param2 = node.param2}
+ if ndef.advtrains.trackworker_rot_incr_param2 then
+ new_node.param2 = ((node.param2 + 1) % 4)
+ end
+ advtrains.ndb.swap_node(pos, new_node)
+ end
end,
- on_use=function(itemstack, user, pointed_thing)
- local name = user:get_player_name()
- if not name then
- return
+ on_use=function(itemstack, player, pointed_thing)
+ local name = player:get_player_name()
+ if not name then
+ return
+ end
+ if pointed_thing.type=="node" then
+ local pos=pointed_thing.under
+ local node=minetest.get_node(pos)
+ if not advtrains.check_track_protection(pos, name) then
+ return
end
- if pointed_thing.type=="node" then
- local pos=pointed_thing.under
- local node=minetest.get_node(pos)
- if not advtrains.check_track_protection(pos, name) then
- return
- end
-
- --if not advtrains.is_track_and_drives_on(minetest.get_node(pos).name, advtrains.all_tracktypes) then return end
- if advtrains.get_train_at_pos(pos) then return end
- local nnprefix, suffix, rotation=string.match(node.name, "^(.+)_([^_]+)(_[^_]+)$")
- --atdebug(node.name.."\npattern recognizes:"..nodeprefix.." / "..railtype.." / "..rotation)
- if not tp.tracks[nnprefix] or not tp.tracks[nnprefix].twcycle[suffix] then
- nnprefix, suffix=string.match(node.name, "^(.+)_([^_]+)$")
- rotation = ""
- if not tp.tracks[nnprefix] or not tp.tracks[nnprefix].twcycle[suffix] then
- minetest.chat_send_player(user:get_player_name(), attrans("This node can't be changed using the trackworker!"))
- return
- end
- end
-
- -- check if the node is modify-protected
- if advtrains.is_track_and_drives_on(minetest.get_node(pos).name, advtrains.all_tracktypes) then
- -- is a track, we can query
- local can_modify, reason = advtrains.can_dig_or_modify_track(pos)
- if not can_modify then
- local str = attrans("This track can not be changed!")
- if reason then
- str = str .. " " .. reason
- end
- minetest.chat_send_player(user:get_player_name(), str)
- return
+
+ -- New since 2.5: only the fields in the node definition are considered, no more hacky pattern matching on the nodename
+
+ local ndef = minetest.registered_nodes[node.name]
+
+ if not ndef.advtrains or not ndef.advtrains.trackworker_next_var then
+ minetest.chat_send_player(name, S("This node can't be changed using the trackworker!"))
+ return
+ end
+
+ -- check if the node is modify-protected
+ if advtrains.is_track(node.name) then
+ -- is a track, we can query
+ local can_modify, reason = advtrains.can_dig_or_modify_track(pos)
+ if not can_modify then
+ local str = S("This track can not be rotated!")
+ if reason then
+ str = str .. " " .. reason
end
+ minetest.chat_send_player(name, str)
+ return
end
-
- local nextsuffix=tp.tracks[nnprefix].twcycle[suffix]
- advtrains.ndb.swap_node(pos, {name=nnprefix.."_"..nextsuffix..rotation, param2=node.param2})
-
- else
- atprint(name, dump(tp.tracks))
end
+
+ local new_node = {name = ndef.advtrains.trackworker_next_var, param2 = node.param2}
+ advtrains.ndb.swap_node(pos, new_node)
+ end
end,
})
diff --git a/advtrains/tracks.lua b/advtrains/tracks.lua
index 3959232..e6d08a7 100644
--- a/advtrains/tracks.lua
+++ b/advtrains/tracks.lua
@@ -1,752 +1,275 @@
---advtrains by orwell96, see readme.txt
-
---dev-time settings:
---EDIT HERE
---If the old non-model rails on straight tracks should be replaced by the new...
---false: no
---true: yes
-advtrains.register_replacement_lbms=false
-
---[[TracksDefinition
-nodename_prefix
-texture_prefix
-description
-common={}
-straight={}
-straight45={}
-curve={}
-curve45={}
-lswitchst={}
-lswitchst45={}
-rswitchst={}
-rswitchst45={}
-lswitchcr={}
-lswitchcr45={}
-rswitchcr={}
-rswitchcr45={}
-vert1={
- --you'll probably want to override mesh here
-}
-vert2={
- --you'll probably want to override mesh here
-}
-]]--
-advtrains.all_tracktypes={}
-
---definition preparation
-local function conns(c1, c2, r1, r2) return {{c=c1, y=r1}, {c=c2, y=r2}} end
-local function conns3(c1, c2, c3, r1, r2, r3) return {{c=c1, y=r1}, {c=c2, y=r2}, {c=c3, y=r3}} end
-
-advtrains.ap={}
-advtrains.ap.t_30deg_flat={
- regstep=1,
- variant={
- st={
- conns = conns(0,8),
- desc = "straight",
- tpdouble = true,
- tpsingle = true,
- trackworker = "cr",
- },
- cr={
- conns = conns(0,7),
- desc = "curve",
- tpdouble = true,
- trackworker = "swlst",
- },
- swlst={
- conns = conns3(0,8,7),
- desc = "left switch (straight)",
- trackworker = "swrst",
- switchalt = "cr",
- switchmc = "on",
- switchst = "st",
- switchprefix = "swl",
- },
- swlcr={
- conns = conns3(0,7,8),
- desc = "left switch (curve)",
- trackworker = "swrcr",
- switchalt = "st",
- switchmc = "off",
- switchst = "cr",
- switchprefix = "swl",
- },
- swrst={
- conns = conns3(0,8,9),
- desc = "right switch (straight)",
- trackworker = "st",
- switchalt = "cr",
- switchmc = "on",
- switchst = "st",
- switchprefix = "swr",
- },
- swrcr={
- conns = conns3(0,9,8),
- desc = "right switch (curve)",
- trackworker = "st",
- switchalt = "st",
- switchmc = "off",
- switchst = "cr",
- switchprefix = "swr",
- },
- },
- regtp=true,
- tpdefault="st",
- trackworker={
- ["swrcr"]="st",
- ["swrst"]="st",
- ["cr"]="swlst",
- ["swlcr"]="swrcr",
- ["swlst"]="swrst",
- },
- rotation={"", "_30", "_45", "_60"},
-}
-advtrains.ap.t_yturnout={
- regstep=1,
- variant={
- l={
- conns = conns3(0,7,9),
- desc = "Y-turnout (left)",
- switchalt = "r",
- switchmc = "off",
- switchst = "l",
- switchprefix = "",
- },
- r={
- conns = conns3(0,9,7),
- desc = "Y-turnout (right)",
- switchalt = "l",
- switchmc = "on",
- switchst = "r",
- switchprefix = "",
- }
- },
- regtp=true,
- tpdefault="l",
- rotation={"", "_30", "_45", "_60"},
-}
-advtrains.ap.t_s3way={
- regstep=1,
- variant={
- l={
- conns = { {c=0}, {c=7}, {c=8}, {c=9}, {c=0} },
- desc = "3-way turnout (left)",
- switchalt = "s",
- switchst="l",
- switchprefix = "",
- },
- s={
- conns = { {c=0}, {c=8}, {c=7}, {c=9}, {c=0} },
- desc = "3-way turnout (straight)",
- switchalt ="r",
- switchst = "s",
- switchprefix = "",
- },
- r={
- conns = { {c=0}, {c=9}, {c=8}, {c=7}, {c=0} },
- desc = "3-way turnout (right)",
- switchalt = "l",
- switchst="r",
- switchprefix = "",
- }
- },
- regtp=true,
- tpdefault="l",
- rotation={"", "_30", "_45", "_60"},
-}
-advtrains.ap.t_30deg_slope={
- regstep=1,
- variant={
- vst1={conns = conns(8,0,0,0.5), rail_y = 0.25, desc = "steep uphill 1/2", slope=true},
- vst2={conns = conns(8,0,0.5,1), rail_y = 0.75, desc = "steep uphill 2/2", slope=true},
- vst31={conns = conns(8,0,0,0.33), rail_y = 0.16, desc = "uphill 1/3", slope=true},
- vst32={conns = conns(8,0,0.33,0.66), rail_y = 0.5, desc = "uphill 2/3", slope=true},
- vst33={conns = conns(8,0,0.66,1), rail_y = 0.83, desc = "uphill 3/3", slope=true},
- },
- regsp=true,
- slopeplacer={
- [2]={"vst1", "vst2"},
- [3]={"vst31", "vst32", "vst33"},
- max=3,--highest entry
- },
- slopeplacer_45={
- [2]={"vst1_45", "vst2_45"},
- max=2,
- },
- rotation={"", "_30", "_45", "_60"},
- trackworker={},
- increativeinv={},
-}
-advtrains.ap.t_30deg_straightonly={
- regstep=1,
- variant={
- st={
- conns = conns(0,8),
- desc = "straight",
- tpdouble = true,
- tpsingle = true,
- trackworker = "st",
- },
- },
- regtp=true,
- tpdefault="st",
- rotation={"", "_30", "_45", "_60"},
-}
-advtrains.ap.t_30deg_straightonly_noplacer={
- regstep=1,
- variant={
- st={
- conns = conns(0,8),
- desc = "straight",
- tpdouble = true,
- tpsingle = true,
- trackworker = "st",
- },
- },
- tpdefault="st",
- rotation={"", "_30", "_45", "_60"},
-}
-advtrains.ap.t_45deg={
- regstep=2,
- variant={
- st={
- conns = conns(0,8),
- desc = "straight",
- tpdouble = true,
- tpsingle = true,
- trackworker = "cr",
- },
- cr={
- conns = conns(0,6),
- desc = "curve",
- tpdouble = true,
- trackworker = "swlst",
- },
- swlst={
- conns = conns3(0,8,6),
- desc = "left switch (straight)",
- trackworker = "swrst",
- switchalt = "cr",
- switchmc = "on",
- switchst = "st",
- },
- swlcr={
- conns = conns3(0,6,8),
- desc = "left switch (curve)",
- trackworker = "swrcr",
- switchalt = "st",
- switchmc = "off",
- switchst = "cr",
- },
- swrst={
- conns = conns3(0,8,10),
- desc = "right switch (straight)",
- trackworker = "st",
- switchalt = "cr",
- switchmc = "on",
- switchst = "st",
- },
- swrcr={
- conns = conns3(0,10,8),
- desc = "right switch (curve)",
- trackworker = "st",
- switchalt = "st",
- switchmc = "off",
- switchst = "cr",
- },
- },
- regtp=true,
- tpdefault="st",
- trackworker={
- ["swrcr"]="st",
- ["swrst"]="st",
- ["cr"]="swlst",
- ["swlcr"]="swrcr",
- ["swlst"]="swrst",
- },
- rotation={"", "_30", "_45", "_60"},
-}
-advtrains.ap.t_perpcrossing={
- regstep = 1,
- variant={
- st={
- conns = { {c=0}, {c=8}, {c=4}, {c=12} },
- desc = "perpendicular crossing",
- tpdouble = true,
- tpsingle = true,
- trackworker = "st",
- },
- },
- regtp=true,
- tpdefault="st",
- rotation={"", "_30", "_45", "_60"},
-}
-advtrains.ap.t_90plusx_crossing={
- regstep = 1,
- variant={
- ["30l"]={
- conns = { {c=0}, {c=8}, {c=1}, {c=9} },
- desc = "30/90 degree crossing (left)",
- tpdouble = true,
- tpsingle = true,
- trackworker = "45l"
- },
- ["45l"]={
- conns = { {c=0}, {c=8}, {c=2}, {c=10} },
- desc = "45/90 degree crossing (left)",
- tpdouble = true,
- tpsingle = true,
- trackworker = "60l",
- },
- ["60l"]={
- conns = { {c=0}, {c=8}, {c=3}, {c=11}},
- desc = "60/90 degree crossing (left)",
- tpdouble = true,
- tpsingle = true,
- trackworker = "60r",
- },
- ["60r"]={
- conns = { {c=0}, {c=8}, {c=5}, {c=13} },
- desc = "60/90 degree crossing (right)",
- tpdouble = true,
- tpsingle = true,
- trackworker = "45r"
- },
- ["45r"]={
- conns = { {c=0}, {c=8}, {c=6}, {c=14} },
- desc = "45/90 degree crossing (right)",
- tpdouble = true,
- tpsingle = true,
- trackworker = "30r",
- },
- ["30r"]={
- conns = { {c=0}, {c=8}, {c=7}, {c=15}},
- desc = "30/90 degree crossing (right)",
- tpdouble = true,
- tpsingle = true,
- trackworker = "30l",
- },
- },
- regtp=true,
- tpdefault="30l",
- rotation={""},
- trackworker = {
- ["30l"] = "45l",
- ["45l"] = "60l",
- ["60l"] = "60r",
- ["60r"] = "45r",
- ["45r"] = "30r",
- ["30r"] = "30l",
- }
-}
-
-advtrains.ap.t_diagonalcrossing = {
- regstep=1,
- variant={
- ["30l45r"]={
- conns = {{c=1}, {c=9}, {c=6}, {c=14}},
- desc = "30left-45right diagonal crossing",
- tpdouble=true,
- tpsingle=true,
- trackworker="60l30l",
- },
- ["60l30l"]={
- conns = {{c=3}, {c=11}, {c=1}, {c=9}},
- desc = "30left-60right diagonal crossing",
- tpdouble=true,
- tpsingle=true,
- trackworker="60l45r"
- },
- ["60l45r"]={
- conns = {{c=3}, {c=11}, {c=6}, {c=14}},
- desc = "60left-45right diagonal crossing",
- tpdouble=true,
- tpsingle=true,
- trackworker="60l60r"
- },
- ["60l60r"]={
- conns = {{c=3}, {c=11}, {c=5}, {c=13}},
- desc = "60left-60right diagonal crossing",
- tpdouble=true,
- tpsingle=true,
- trackworker="60r45l",
- },
- --If 60l60r had a mirror image, it would be here, but it's symmetric.
- -- 60l60r is also equivalent to 30l30r but rotated 90 degrees.
- ["60r45l"]={
- conns = {{c=5}, {c=13}, {c=2}, {c=10}},
- desc = "60right-45left diagonal crossing",
- tpdouble=true,
- tpsingle=true,
- trackworker="60r30r",
- },
- ["60r30r"]={
- conns = {{c=5}, {c=13}, {c=7}, {c=15}},
- desc = "60right-30right diagonal crossing",
- tpdouble=true,
- tpsingle=true,
- trackworker="30r45l",
- },
- ["30r45l"]={
- conns = {{c=7}, {c=15}, {c=2}, {c=10}},
- desc = "30right-45left diagonal crossing",
- tpdouble=true,
- tpsingle=true,
- trackworker="30l45r",
- },
-
- },
- regtp=true,
- tpdefault="30l45r",
- rotation={""},
- trackworker = {
- ["30l45r"] = "60l30l",
- ["60l30l"] = "60l45r",
- ["60l45r"] = "60l60r",
- ["60l60r"] = "60r45l",
- ["60r45l"] = "60r30r",
- ["60r30r"] = "30r45l",
- ["30r45l"] = "30l45r",
- }
-}
-
-advtrains.trackpresets = advtrains.ap
-
---definition format: ([] optional)
---[[{
- nodename_prefix
- texture_prefix
- [shared_texture]
- models_prefix
- models_suffix (with dot)
- [shared_model]
- formats={
- st,cr,swlst,swlcr,swrst,swrcr,vst1,vst2
- (each a table with indices 0-3, for if to register a rail with this 'rotation' table entry. nil is assumed as 'all', set {} to not register at all)
- }
- common={} change something on common rail appearance
-}
-[18.12.17] Note on new connection system:
-In order to support real rail crossing nodes and finally make the trackplacer respect switches, I changed the connection system.
-There can be a variable number of connections available. These are specified as tuples {c=<connection>, y=<rely>}
-The table "at_conns" consists of {<conn1>, <conn2>...}
-the "at_rail_y" property holds the value that was previously called "railheight"
-Depending on the number of connections:
-2 conns: regular rail
-3 conns: switch:
- - when train passes in at conn1, will move out of conn2
- - when train passes in at conn2 or conn3, will move out of conn1
-4 conns: cross (or cross switch, depending on arrangement of conns):
- - conn1 <> conn2
- - conn3 <> conn4
-]]
-
--- Notify the user if digging the rail is not allowed
-local function can_dig_callback(pos, player)
- local ok, reason = advtrains.can_dig_or_modify_track(pos)
- if not ok and player then
- minetest.chat_send_player(player:get_player_name(), attrans("This track can not be removed!") .. " " .. reason)
- end
- return ok
-end
-
-function advtrains.register_tracks(tracktype, def, preset)
- advtrains.trackplacer.register_tracktype(def.nodename_prefix, preset.tpdefault)
- if preset.regtp then
- advtrains.trackplacer.register_track_placer(def.nodename_prefix, def.texture_prefix, def.description, def)
- end
- if preset.regsp then
- advtrains.slope.register_placer(def, preset)
- end
- for suffix, var in pairs(preset.variant) do
- for rotid, rotation in ipairs(preset.rotation) do
- if not def.formats[suffix] or def.formats[suffix][rotid] then
- local img_suffix = suffix..rotation
- local ndef = advtrains.merge_tables({
- description=def.description.."("..(var.desc or "any")..rotation..")",
- drawtype = "mesh",
- paramtype="light",
- paramtype2="facedir",
- use_texture_alpha = "blend",
- walkable = false,
- selection_box = {
- type = "fixed",
- fixed = {-1/2-1/16, -1/2, -1/2, 1/2+1/16, -1/2+2/16, 1/2},
- },
-
- mesh = def.shared_model or (def.models_prefix.."_"..img_suffix..def.models_suffix),
- tiles = {def.shared_texture or (def.texture_prefix.."_"..img_suffix..".png"), def.second_texture},
-
- groups = {
- attached_node = advtrains.IGNORE_WORLD and 0 or 1,
- advtrains_track=1,
- ["advtrains_track_"..tracktype]=1,
- save_in_at_nodedb=1,
- dig_immediate=2,
- not_in_creative_inventory=1,
- not_blocking_trains=1,
- },
-
- can_dig = can_dig_callback,
- after_dig_node=function(pos)
- advtrains.ndb.update(pos)
- end,
- after_place_node=function(pos)
- advtrains.ndb.update(pos)
- end,
- at_nnpref = def.nodename_prefix,
- at_suffix = suffix,
- at_rotation = rotation,
- at_rail_y = var.rail_y
- }, def.common or {})
-
- if preset.regtp then
- ndef.drop = def.nodename_prefix.."_placer"
- end
- if preset.regsp and var.slope then
- ndef.drop = def.nodename_prefix.."_slopeplacer"
- end
-
- --connections
- ndef.at_conns = advtrains.rotate_conn_by(var.conns, (rotid-1)*preset.regstep)
-
- local ndef_avt_table
-
- if var.switchalt and var.switchst then
- local switchfunc=function(pos, node, newstate)
- newstate = newstate or var.switchalt -- support for 3 (or more) state switches
- -- this code is only called from the internal setstate function, which
- -- ensures that it is safe to switch the turnout
- if newstate~=var.switchst then
- advtrains.ndb.swap_node(pos, {name=def.nodename_prefix.."_"..(var.switchprefix or "")..newstate..rotation, param2=node.param2})
- advtrains.invalidate_all_paths(pos)
- end
- end
- ndef.on_rightclick = function(pos, node, player)
- if advtrains.check_turnout_signal_protection(pos, player:get_player_name()) then
- advtrains.setstate(pos, nil, node)
- advtrains.log("Switch", player:get_player_name(), pos)
- end
- end
- if var.switchmc then
- ndef.mesecons = {effector = {
- ["action_"..var.switchmc] = function(pos, node)
- advtrains.setstate(pos, nil, node)
- end,
- rules=advtrains.meseconrules
- }}
- end
- ndef_avt_table = {
- getstate = var.switchst,
- setstate = switchfunc,
- }
- end
-
- local adef={}
- if def.get_additional_definiton then
- adef=def.get_additional_definiton(def, preset, suffix, rotation)
- end
- ndef = advtrains.merge_tables(ndef, adef)
-
- -- insert getstate/setstate functions after merging the additional definitions
- if ndef_avt_table then
- ndef.advtrains = advtrains.merge_tables(ndef.advtrains or {}, ndef_avt_table)
- end
-
- minetest.register_node(":"..def.nodename_prefix.."_"..suffix..rotation, ndef)
- --trackplacer
- if preset.regtp then
- local tpconns = {conn1=ndef.at_conns[1].c, conn2=ndef.at_conns[2].c}
- if var.tpdouble then
- advtrains.trackplacer.add_double_conn(def.nodename_prefix, suffix, rotation, tpconns)
- end
- if var.tpsingle then
- advtrains.trackplacer.add_single_conn(def.nodename_prefix, suffix, rotation, tpconns)
- end
- end
- advtrains.trackplacer.add_worked(def.nodename_prefix, suffix, rotation, var.trackworker)
- end
- end
- end
- advtrains.all_tracktypes[tracktype]=true
-end
-
-function advtrains.is_track_and_drives_on(nodename, drives_on_p)
- local drives_on = drives_on_p
- if not drives_on then drives_on = advtrains.all_tracktypes end
- local hasentry = false
- for _,_ in pairs(drives_on) do
- hasentry=true
- end
- if not hasentry then drives_on = advtrains.all_tracktypes end
-
- if not minetest.registered_nodes[nodename] then
- return false
- end
- local nodedef=minetest.registered_nodes[nodename]
- for k,v in pairs(drives_on) do
- if nodedef.groups["advtrains_track_"..k] then
- return true
- end
- end
- return false
-end
-
-function advtrains.get_track_connections(name, param2)
- local nodedef=minetest.registered_nodes[name]
- if not nodedef then atprint(" get_track_connections couldn't find nodedef for nodename "..(name or "nil")) return nil end
- local noderot=param2
- if not param2 then noderot=0 end
- if noderot > 3 then atprint(" get_track_connections: rail has invaild param2 of "..noderot) noderot=0 end
-
- local tracktype
- for k,_ in pairs(nodedef.groups) do
- local tt=string.match(k, "^advtrains_track_(.+)$")
- if tt then
- tracktype=tt
- end
- end
- return advtrains.rotate_conn_by(nodedef.at_conns, noderot*AT_CMAX/4), (nodedef.at_rail_y or 0), tracktype
-end
-
--- Function called when a track is about to be dug or modified by the trackworker
--- Returns either true (ok) or false,"translated string describing reason why it isn't allowed"
-function advtrains.can_dig_or_modify_track(pos)
- if advtrains.get_train_at_pos(pos) then
- return false, attrans("Position is occupied by a train.")
- end
- -- interlocking: tcb, signal IP a.s.o.
- if advtrains.interlocking then
- -- TCB?
- if advtrains.interlocking.db.get_tcb(pos) then
- return false, attrans("There's a Track Circuit Break here.")
- end
- -- signal ip?
- if advtrains.interlocking.db.is_ip_at(pos, true) then -- is_ip_at with purge parameter
- return false, attrans("There's a Signal Influence Point here.")
- end
- end
- return true
-end
-
--- slope placer. Defined in register_tracks.
---crafted with rail and gravel
-local sl={}
-function sl.register_placer(def, preset)
- minetest.register_craftitem(":"..def.nodename_prefix.."_slopeplacer",{
- description = attrans("@1 Slope", def.description),
- inventory_image = def.texture_prefix.."_slopeplacer.png",
- wield_image = def.texture_prefix.."_slopeplacer.png",
- groups={},
- on_place = sl.create_slopeplacer_on_place(def, preset)
- })
-end
---(itemstack, placer, pointed_thing)
-function sl.create_slopeplacer_on_place(def, preset)
- return function(istack, player, pt)
- if not pt.type=="node" then
- minetest.chat_send_player(player:get_player_name(), attrans("Can't place: not pointing at node"))
- return istack
- end
- local pos=pt.above
- if not pos then
- minetest.chat_send_player(player:get_player_name(), attrans("Can't place: not pointing at node"))
- return istack
- end
- local node=minetest.get_node(pos)
- if not minetest.registered_nodes[node.name] or not minetest.registered_nodes[node.name].buildable_to then
- minetest.chat_send_player(player:get_player_name(), attrans("Can't place: space occupied!"))
- return istack
- end
- if not advtrains.check_track_protection(pos, player:get_player_name()) then
- minetest.record_protection_violation(pos, player:get_player_name())
- return istack
- end
- --determine player orientation (only horizontal component)
- --get_look_horizontal may not be available
- local yaw=player.get_look_horizontal and player:get_look_horizontal() or (player:get_look_yaw() - math.pi/2)
-
- --rounding unit vectors is a nice way for selecting 1 of 8 directions since sin(30°) is 0.5.
- local dirvec={x=math.floor(math.sin(-yaw)+0.5), y=0, z=math.floor(math.cos(-yaw)+0.5)}
- --translate to direction to look up inside the preset table
- local param2, rot45=({
- [-1]={
- [-1]=2,
- [0]=3,
- [1]=3,
- },
- [0]={
- [-1]=2,
- [1]=0,
- },
- [1]={
- [-1]=1,
- [0]=1,
- [1]=0,
- },
- })[dirvec.x][dirvec.z], dirvec.x~=0 and dirvec.z~=0
- local lookup=preset.slopeplacer
- if rot45 then lookup=preset.slopeplacer_45 end
-
- --go unitvector forward and look how far the next node is
- local step=1
- while step<=lookup.max do
- local node=minetest.get_node(vector.add(pos, dirvec))
- --next node solid?
- if not minetest.registered_nodes[node.name] or not minetest.registered_nodes[node.name].buildable_to or advtrains.is_protected(pos, player:get_player_name()) then
- --do slopes of this distance exist?
- if lookup[step] then
- if minetest.settings:get_bool("creative_mode") or istack:get_count()>=step then
- --start placing
- local placenodes=lookup[step]
- while step>0 do
- minetest.set_node(pos, {name=def.nodename_prefix.."_"..placenodes[step], param2=param2})
- if not minetest.settings:get_bool("creative_mode") then
- istack:take_item()
- end
- step=step-1
- pos=vector.subtract(pos, dirvec)
- end
- else
- minetest.chat_send_player(player:get_player_name(), attrans("Can't place: Not enough slope items left (@1 required)", step))
- end
- else
- minetest.chat_send_player(player:get_player_name(), attrans("Can't place: There's no slope of length @1",step))
- end
- return istack
- end
- step=step+1
- pos=vector.add(pos, dirvec)
- end
- minetest.chat_send_player(player:get_player_name(), attrans("Can't place: no supporting node at upper end."))
- return itemstack
- end
-end
-
-advtrains.slope=sl
-
---END code, BEGIN definition
---definition format: ([] optional)
---[[{
- nodename_prefix
- texture_prefix
- [shared_texture]
- models_prefix
- models_suffix (with dot)
- [shared_model]
- formats={
- st,cr,swlst,swlcr,swrst,swrcr,vst1,vst2
- (each a table with indices 0-3, for if to register a rail with this 'rotation' table entry. nil is assumed as 'all', set {} to not register at all)
- }
- common={} change something on common rail appearance
-}]]
-
-
-
-
-
-
-
-
-
+-- tracks.lua
+-- rewritten with advtrains 2.5 according to new track registration system
+
+-- Get current translator
+local S = advtrains.translate
+
+--[[
+
+Tracks in advtrains are defined by the node definition. They must have at least 2 connections, but can have any number.
+Switchable nodes (turnouts, single/double-slip switches) are implemented by having a separate node (node name) for each of the possible states.
+
+ minetest.register_node(nodename, {
+ ... usual node definition ...
+ groups = {
+ advtrains_track = 1,
+ advtrains_track_<tracktype>=1
+ ^- these groups tell that the node is a track
+ not_blocking_trains=1,
+ ^- this group tells that the node should not block trains although it's walkable.
+ },
+
+ at_rail_y = 0,
+ ^- Height of this rail node (the y position of a wagon that stands centered on this rail)
+ at_conns = {
+ [1] = { c=0..15, y=0..1 },
+ [2] = { c=0..15, y=0..1 },
+ ( [3] = { c=0..15, y=0..1 }, )
+ ( [4] = { c=0..15, y=0..1 }, )
+ ( ... )
+ }
+ ^- Connections of this rail. There are two general cases:
+ a) SIMPLE TRACK - the track has exactly 2 connections, and does not feature a turnout, crossing or other contraption
+ For simple tracks, except for the at_conns table no further setup needs to be specified. A train entering on conn 1 will go out at conn 2 and vice versa.
+ A track with only one connection defined is not permitted.
+ b) COMPOUND TRACK - the track has more than 2 connections
+ This will be used for turnouts and crossings. Tracks with more than 2 conns MUST define 'at_conn_map'.
+ Switchable nodes, whose state can be changed (e.g. turnouts) MUST define a 'state_map' within the advtrains table as well.
+ This differs from the behavior up until 2.4.2, where the conn mapping was fixed.
+ ^- Connection definition:
+ - c is the direction of the connection (0-16). For the mapping to world directions see helpers.lua.
+ - Connections will be auto-rotated with param2 of the node (horizontal, param2 values 0-3 only)
+ - y is the height of the connection (rail will only connect when this matches)
+ ^- The index of a connection inside the conns table (1, 2, 3, ...) is referred throughout advtrains code as 'connid'
+ ^- IMPORTANT: For switchable nodes (any kind of turnout), it is crucial that for all of the node's variants the at_conns table stays the same. See below.
+
+ at_conn_map = {
+ [1] = 2,
+ [2] = 1,
+ [3] = 1,
+ }
+ ^- Connection map of this rail. It specifies when a train enters the track on connid X, on which connid it will leave
+ This field MUST be specified when the number of connections in at_conns is greater than 2
+ This field may, and obviously will, vary between nodes for switchable nodes.
+
+ can_dig = advtrains.track_can_dig_callback
+ after_dig_node = advtrains.track_update_callback
+ after_place_node = advtrains.track_update_callback
+ ^- the code in these 3 default minetest API functions is required for advtrains to work, however you can add your own code
+
+ on_rightclick = advtrains.state_node_on_rightclick_callback
+ ^- Must be added if the node is a turnout and if it should be switched by right-click. It will cause the turnout to be switched to next_state.
+
+ advtrains = {
+ on_train_enter=function(pos, train_id, train, index) end
+ ^- called when a train enters the rail
+ on_train_leave=function(pos, train_id, train, index) end
+ ^- called when a train leaves the rail
+
+ -- The following function is only in effect when interlocking is enabled:
+ on_train_approach = function(pos, train_id, train, index, has_entered, lzbdata)
+ ^- called when a train is approaching this position, called exactly once for every path recalculation (which can happen at any time)
+ ^- This is called so that if the train would start braking now, it would come to halt about(wide approx) 5 nodes before the rail.
+ ^- has_entered: when true, the train is already standing on this node with its front tip, and the enter callback has already been called.
+ Possibly, some actions need not to be taken in this case. Only set if it's the very first node the train is standing on.
+ ^- lzbdata should be ignored and nothing should be assigned to it
+
+ -- The following information is required if the node is a turnout (e.g. can be switched into different positions)
+ node_state = "st"
+ ^- The name of the state this node represents
+ ^- Conventions for this field are as follows:
+ - Two-way straight/turn switches: 'st'=straight branch, 'cr'=diverting/turn branch
+ - 3-way turnouts, Y-turnouts: 'l'=left branch, 's'=straight branch, 'r'=right branch
+
+ node_next_state = "cr"
+ ^- The name of the state that the turnout should be switched to when it is right-clicked
+
+ node_fallback_state = "st"
+ ^- The name of the state that the turnout should "fall back" to when it is released
+ Only used by the interlocking system, when a route on the node is released it is switched back to this state.
+
+ node_state_map = {
+ ["st"] = "<node name of the st variant>",
+ ["cr"] = "<node name of the cr variant>",
+ ... etc ...
+ }
+ ^- Map of state name to the appropriate node name that should be set by advtrains when a switch is requested
+ Note that for all of those nodes, the at_conns table must be identical (however the conn_map will vary)
+
+ node_on_switch_state = function(pos, node, oldstate, newstate)
+ ^- Called when the node state is switched by advtrains, after the node replacement has commenced.
+
+ Turnout switching can happen programmatically via advtrains.setstate(pos, state), via user right_click or via the interlocking system.
+ In no other situation is it permissible to exchange track nodes in-place, unless both at_conns and at_conn_map stay identical.
+
+ Note that the fields node_state, node_next_state and node_state_map completely replace the getstate/setstate functions.
+ There must be a one-to-one mapping between states and node names and no function can be defined for state switching.
+ This principle enables the seamless working of the interlocking autorouter and reduces failure points.
+ The node_state_* system can also be used as drop-in replacement for the passive-API-enabled nodes (andrews-cross, mesecon_switch etc.)
+ The advtrains API functions advtrains.getstate() and advtrains.setstate() remain the programmatic access points, but will now utilize the new state system.
+
+
+ trackworker_next_rot = <nodename of next rotation step>,
+ ^- if set, right-click with trackworker will set this node
+ trackworker_rot_incr_param2 = true
+ ^- if set, trackworker will increase node param2 on rightclick
+
+ trackworker_next_var = <nodename of next variant>
+ ^- if set, left-click with trackworker will set this node
+ }
+ })
+
+]]--
+
+-- This file provides some utilities to register tracks, but tries to not get into the way too much
+
+
+function advtrains.track_can_dig_callback(pos, player)
+ local ok, reason = advtrains.can_dig_or_modify_track(pos)
+ if not ok and player then
+ minetest.chat_send_player(player:get_player_name(), S("This track can not be removed!") .. " " .. reason)
+ end
+ return ok
+end
+
+function advtrains.track_update_callback(pos)
+ advtrains.ndb.update(pos)
+end
+
+function advtrains.state_node_on_rightclick_callback(pos, node, player)
+ if advtrains.check_turnout_signal_protection(pos, player:get_player_name()) then
+ local ndef = minetest.registered_nodes[node.name]
+ if ndef and ndef.advtrains and ndef.advtrains.node_next_state then
+ advtrains.setstate(pos, ndef.advtrains.node_next_state, node)
+ advtrains.log("Switch", player:get_player_name(), pos)
+ end
+ end
+end
+
+-- advtrains.register_node_4rot(name, nodedef)
+-- Registers four rotations for the node defined by nodedef (0°, 30°, 45° and 60°; the 4 90°-steps are already handled by the param2, resulting in 16 directions total).
+-- You must provide the definition for the base node, and certain fields are altered automatically for the 3 additional rotations:
+-- name: appends the suffix "_30", "_45" or "_60"
+-- description: appends the rotation (human-readable) in parenthesis
+-- tiles_prefix: if defined, "tiles" field will be set as prefix..rotationExtension..".png"
+-- mesh_prefix, mesh_suffix: if defined, "mesh" field will be set as prefix..rotationExtension..suffix
+-- at_conns: are rotated according to the node rotation
+-- node_state_map, trackworker_next_var: appends the suffix appropriately.
+-- groups: applies save_in_at_nodedb and not_blocking_trains groups if not already present
+-- The nodes are registered in the trackworker to be rotated with right-click.
+-- definition_mangling_function is an optional parameter. For each of the 4 rotations, it gets passed the modified node definition and may perform final modifications to it.
+-- signature: function definition_mangling_function(name, nodedef, rotationIndex, rotationSuffix)
+-- Example usage: define the setstate function of turnouts (if that is not done via the "automatic" way of state_node_map)
+local rotations = {
+ {i = 0, s = "", h = " (0)", n = "_30"},
+ {i = 1, s = "_30", h = " (30)", n = "_45"},
+ {i = 2, s = "_45", h = " (45)", n = "_60"},
+ {i = 3, s = "_60", h = " (60)", n = ""},
+}
+function advtrains.register_node_4rot(ori_name, ori_ndef, definition_mangling_function)
+ for _, rot in ipairs(rotations) do
+ local ndef = table.copy(ori_ndef)
+ if ori_ndef.advtrains then
+ -- make sure advtrains table is deep-copied because we may need to replace node_state_map
+ ndef.advtrains = table.copy(ori_ndef.advtrains)
+ else
+ ndef.advtrains = {} -- we need the table later for trackworker
+ end
+ -- Perform the name mangling
+ local suffix = rot.s
+ local name = ori_name..suffix
+ ndef.description = ori_ndef.description .. rot.h
+ if ori_ndef.tiles_prefix then
+ ndef.tiles = { ori_ndef.tiles_prefix .. suffix .. ".png" }
+ end
+ if ori_ndef.mesh_prefix then
+ ndef.mesh = ori_ndef.mesh_prefix .. suffix .. ori_ndef.mesh_suffix
+ end
+ -- rotate connections
+ if ori_ndef.at_conns then
+ ndef.at_conns = advtrains.rotate_conn_by(ori_ndef.at_conns, rot.i)
+ end
+ -- update node state map if present
+ if ori_ndef.advtrains then
+ if ori_ndef.advtrains.node_state_map then
+ local new_nsm = {}
+ for state, nname in pairs(ori_ndef.advtrains.node_state_map) do
+ new_nsm[state] = nname .. suffix
+ end
+ ndef.advtrains.node_state_map = new_nsm
+ end
+ if ori_ndef.advtrains.trackworker_next_var then
+ ndef.advtrains.trackworker_next_var = ori_ndef.advtrains.trackworker_next_var .. suffix
+ end
+ -- apply trackworker rot field
+ ndef.advtrains.trackworker_next_rot = ori_name .. rot.n
+ ndef.advtrains.trackworker_rot_incr_param2 = (rot.n=="")
+ end
+ -- apply groups
+ ndef.groups.save_in_at_nodedb = 1
+ ndef.groups.not_blocking_trains = 1
+
+ -- give the definition mangling function an option to do some adjustments
+ if definition_mangling_function then
+ definition_mangling_function(name, ndef, rot.i, suffix)
+ end
+
+ -- register node
+ minetest.register_node(":"..name, ndef)
+
+ -- if this has the track_place_group set, register as a candidate for the track_place_group
+ if ndef.advtrains.track_place_group then
+ advtrains.trackplacer.register_candidate(ndef.advtrains.track_place_group, name, ndef, ndef.advtrains.track_place_single, true)
+ end
+ end
+end
+
+-- track-related helper functions
+
+function advtrains.is_track(nodename)
+ if not minetest.registered_nodes[nodename] then
+ return false
+ end
+ local nodedef=minetest.registered_nodes[nodename]
+ if nodedef and nodedef.groups.advtrains_track then
+ return true
+ end
+ return false
+end
+
+-- returns the connection tables of the track with given node details
+-- returns: conns table, railheight, conn_map table
+function advtrains.get_track_connections(name, param2)
+ local nodedef=minetest.registered_nodes[name]
+ if not nodedef then atprint(" get_track_connections couldn't find nodedef for nodename "..(name or "nil")) return nil end
+ local noderot=param2
+ if not param2 then noderot=0 end
+ if noderot > 3 then atprint(" get_track_connections: rail has invaild param2 of "..noderot) noderot=0 end
+
+ if not nodedef.at_conns then
+ return nil
+ end
+ --atdebug("Track connections of ",name,param2,":",nodedef.at_conns)
+ return advtrains.rotate_conn_by(nodedef.at_conns, noderot*AT_CMAX/4), (nodedef.at_rail_y or 0), nodedef.at_conn_map
+end
+
+-- Function called when a track is about to be dug or modified by the trackworker
+-- Returns either true (ok) or false,"translated string describing reason why it isn't allowed"
+-- Impl Note: possibly duplicate code in "self contained TCB" - see interlocking/tcb_ts_ui.lua!
+function advtrains.can_dig_or_modify_track(pos)
+ if advtrains.get_train_at_pos(pos) then
+ return false, S("Position is occupied by a train.")
+ end
+ -- interlocking: tcb, signal IP a.s.o.
+ if advtrains.interlocking then
+ -- TCB?
+ if advtrains.interlocking.db.get_tcb(pos) then
+ return false, S("There's a Track Circuit Break here.")
+ end
+ -- signal ip?
+ if advtrains.interlocking.db.is_ip_at(pos, true) then -- is_ip_at with purge parameter
+ return false, S("There's a Signal Influence Point here.")
+ end
+ end
+ return true
+end
diff --git a/advtrains/trainhud.lua b/advtrains/trainhud.lua
index 22aa6cf..eeebff0 100644
--- a/advtrains/trainhud.lua
+++ b/advtrains/trainhud.lua
@@ -1,5 +1,10 @@
--trainhud.lua: holds all the code for train controlling
+-- Get current translator
+local S = advtrains.translate
+
+local T = advtrains.texture
+
advtrains.hud = {}
advtrains.hhud = {}
@@ -8,6 +13,8 @@ advtrains.hud[player:get_player_name()] = nil
advtrains.hhud[player:get_player_name()] = nil
end)
+local hud_type_key = minetest.features.hud_def_type_field and "type" or "hud_elem_type"
+
local mletter={[1]="F", [-1]="R", [0]="N"}
function advtrains.on_control_change(pc, train, flip)
@@ -46,6 +53,10 @@ function advtrains.on_control_change(pc, train, flip)
train.ctrl_user = 1
act=true
end
+ if train.ars_disable and pc.up then
+ -- up clears ars disable flag in any situation
+ train.ars_disable = nil
+ end
-- If atc command set, only "Jump" key can clear command. To prevent accidental control.
if train.tarvelocity or train.atc_command then
return
@@ -101,19 +112,21 @@ function advtrains.set_trainhud(name, text, driver)
if not player then
return
end
+ local drivertext = driver or ""
local driverhud = {
- hud_elem_type = "image",
+ [hud_type_key] = "image",
name = "ADVTRAINS_DRIVER",
position = {x=0.5, y=1},
offset = {x=0,y=-170},
- text = driver or "",
+ text = drivertext,
alignment = {x=0,y=-1},
- scale = {x=1,y=1},}
+ scale = {x=1,y=1},
+ }
if not hud then
- hud = {["driver"]={}}
+ hud = {}
advtrains.hud[name] = hud
hud.id = player:hud_add({
- hud_elem_type = "text",
+ [hud_type_key] = "text",
name = "ADVTRAINS",
number = 0xFFFFFF,
position = {x=0.5, y=1},
@@ -122,17 +135,22 @@ function advtrains.set_trainhud(name, text, driver)
scale = {x=200, y=60},
alignment = {x=0, y=-1},
})
- hud.oldText=text
hud.driver = player:hud_add(driverhud)
+ hud.oldText = text
+ hud.oldDriver = drivertext
else
if hud.oldText ~= text then
player:hud_change(hud.id, "text", text)
hud.oldText=text
end
if hud.driver then
- player:hud_change(hud.driver, "text", driver or "")
+ if hud.oldDriver ~= drivertext then
+ player:hud_change(hud.driver, "text", drivertext)
+ hud.oldDriver = drivertext
+ end
elseif driver then
hud.driver = player:hud_add(driverhud)
+ hud.oldDriver = drivertext
end
end
end
@@ -147,7 +165,7 @@ function advtrains.set_help_hud(name, text)
hud = {}
advtrains.hhud[name] = hud
hud.id = player:hud_add({
- hud_elem_type = "text",
+ [hud_type_key] = "text",
name = "ADVTRAINS_HELP",
number = 0xFFFFFF,
position = {x=1, y=0.3},
@@ -184,138 +202,108 @@ function advtrains.hud_train_format(train, flip)
local vel = advtrains.abs_ceil(train.velocity)
local vel_kmh=advtrains.abs_ceil(advtrains.ms_to_kmh(train.velocity))
- local tlev=train.lever or 1
+ local tlev=train.lever or 3
if train.velocity==0 and not train.active_control then tlev=1 end
if train.hud_lzb_effect_tmr then
tlev=1
end
- local ht = {"[combine:440x110:0,0=(advtrains_hud_bg.png^[resize\\:440x110)"}
+ local hud = T.combine(440, 110, "black")
local st = {}
if train.debug then st = {train.debug} end
- -- seven-segment display
- local function sevenseg(digit, x, y, w, h, m)
- --[[
- -1-
- 2 3
- -4-
- 5 6
- -7-
- ]]
- local segs = {
- {h, 0, w, h},
- {0, h, h, w},
- {w+h, h, h, w},
- {h, w+h, w, h},
- {0, w+2*h, h, w},
- {w+h, w+2*h, h, w},
- {h, 2*(w+h), w, h}}
- local trans = {
- [0] = {true, true, true, false, true, true, true},
- [1] = {false, false, true, false, false, true, false},
- [2] = {true, false, true, true, true, false, true},
- [3] = {true, false, true, true, false, true, true},
- [4] = {false, true, true, true, false, true, false},
- [5] = {true, true, false, true, false, true, true},
- [6] = {true, true, false, true, true, true, true},
- [7] = {true, false, true, false, false, true, false},
- [8] = {true, true, true, true, true, true, true},
- [9] = {true, true, true, true, false, true, true}}
- local ent = trans[digit or 10]
- if not ent then return end
- for i = 1, 7, 1 do
- if ent[i] then
- local s = segs[i]
- ht[#ht+1] = sformat("%d,%d=(advtrains_hud_bg.png^[resize\\:%dx%d^%s)",x+s[1], y+s[2], s[3], s[4], m)
- end
- end
- end
-
-- lever
- ht[#ht+1] = "275,10=(advtrains_hud_bg.png^[colorize\\:cyan^[resize\\:5x18)"
- ht[#ht+1] = "275,28=(advtrains_hud_bg.png^[colorize\\:white^[resize\\:5x18)"
- ht[#ht+1] = "275,46=(advtrains_hud_bg.png^[colorize\\:orange^[resize\\:5x36)"
- ht[#ht+1] = "275,82=(advtrains_hud_bg.png^[colorize\\:red^[resize\\:5x18)"
- ht[#ht+1] = "292,16=(advtrains_hud_bg.png^[colorize\\:darkslategray^[resize\\:6x78)"
- ht[#ht+1] = sformat("280,%s=(advtrains_hud_bg.png^[colorize\\:gray^[resize\\:30x18)",18*(4-tlev)+10)
+ hud:add_multicolor_fill_topdown(275, 10, 5, 90, 1, "cyan", 1, "white", 2, "orange", 1, "red")
+ hud:add_lever_topdown(280, 10, 30, 90, 18, 6, (4-tlev)/4, "gray", "darkslategray")
-- reverser
- ht[#ht+1] = sformat("245,10=(advtrains_hud_arrow.png^[transformFY%s)", flip and "" or "^[multiply\\:cyan")
- ht[#ht+1] = sformat("245,85=(advtrains_hud_arrow.png%s)", flip and "^[multiply\\:orange" or "")
- ht[#ht+1] = "250,35=(advtrains_hud_bg.png^[colorize\\:darkslategray^[resize\\:5x40)"
- ht[#ht+1] = sformat("240,%s=(advtrains_hud_bg.png^[resize\\:25x15^[colorize\\:gray)", flip and 65 or 30)
+ hud:add(245, 10, T"advtrains_hud_arrow.png":transform"FY":multiply(flip and "gray" or "cyan"))
+ hud:add(245, 85, T"advtrains_hud_arrow.png":multiply(flip and "orange" or "gray"))
+ hud:add_lever_topdown(240, 30, 25, 50, 15, 5, flip and 1 or 0, "gray", "darkslategray")
-- train control/safety indication
- if train.tarvelocity or train.atc_command then
- ht[#ht+1] = "10,10=(advtrains_hud_atc.png^[resize\\:30x30^[multiply\\:cyan)"
- end
- if train.hud_lzb_effect_tmr then
- ht[#ht+1] = "50,10=(advtrains_hud_lzb.png^[resize\\:30x30^[multiply\\:red)"
- end
- if train.is_shunt then
- ht[#ht+1] = "90,10=(advtrains_hud_shunt.png^[resize\\:30x30^[multiply\\:orange)"
- end
+ hud:add(10, 10, T"advtrains_hud_atc.png":resize(30, 30):multiply((train.tarvelocity or train.atc_command) and "cyan" or "darkslategray"))
+ hud:add(50, 10, T"advtrains_hud_lzb.png":resize(30, 30):multiply(train.hud_lzb_effect_tmr and "red" or "darkslategray"))
+ hud:add(90, 10, T"advtrains_hud_shunt.png":resize(30, 30):multiply(train.is_shunt and "orange" or "darkslategray"))
-- door
- ht[#ht+1] = "187,10=(advtrains_hud_bg.png^[resize\\:26x30^[colorize\\:white)"
- ht[#ht+1] = "189,12=(advtrains_hud_bg.png^[resize\\:22x11)"
- ht[#ht+1] = sformat("170,10=(advtrains_hud_bg.png^[resize\\:15x30^[colorize\\:%s)", train.door_open==-1 and "white" or "darkslategray")
- ht[#ht+1] = "172,12=(advtrains_hud_bg.png^[resize\\:11x11)"
- ht[#ht+1] = sformat("215,10=(advtrains_hud_bg.png^[resize\\:15x30^[colorize\\:%s)", train.door_open==1 and "white" or "darkslategray")
- ht[#ht+1] = "217,12=(advtrains_hud_bg.png^[resize\\:11x11)"
+ hud:add_fill(187, 10, 26, 30, "white"):add_fill(189, 12, 22, 11, "black")
+ hud:add_fill(170, 10, 15, 30, train.door_open==-1 and "white" or "darkslategray"):add_fill(172, 12, 11, 11, "black")
+ hud:add_fill(215, 10, 15, 30, train.door_open==1 and "white" or "darkslategray"):add_fill(217, 12, 11, 11, "black")
-- speed indication(s)
- sevenseg(math.floor(vel/10), 320, 10, 30, 10, "[colorize\\:red\\:255")
- sevenseg(vel%10, 380, 10, 30, 10, "[colorize\\:red\\:255")
- for i = 1, vel, 1 do
- ht[#ht+1] = sformat("%d,65=(advtrains_hud_bg.png^[resize\\:8x20^[colorize\\:white)", i*11-1)
- end
- for i = max+1, 20, 1 do
- ht[#ht+1] = sformat("%d,65=(advtrains_hud_bg.png^[resize\\:8x20^[colorize\\:darkslategray)", i*11-1)
- end
+ hud:add_n7seg(320, 10, 110, 90, vel, 2, "red")
+ hud:add_segmentbar_leftright(10, 65, 217, 20, 3, 20, max, 20, "darkslategray", 0, vel, "white")
if res and res > 0 then
- ht[#ht+1] = sformat("%d,60=(advtrains_hud_bg.png^[resize\\:3x30^[colorize\\:red\\:255)", 7+res*11)
+ hud:add_fill(7+res*11, 60, 3, 30, "red")
end
if train.tarvelocity then
- ht[#ht+1] = sformat("%d,85=(advtrains_hud_arrow.png^[multiply\\:cyan^[transformFY^[makealpha\\:#000000)", 1+train.tarvelocity*11)
+ hud:add(1+train.tarvelocity*11, 85, T"advtrains_hud_arrow.png":transform"FY":multiply"cyan")
end
local lzb = train.lzb
- if lzb and lzb.checkpoints then
- local oc = lzb.checkpoints
- for i = 1, #oc do
- local spd = oc[i].speed
- spd = advtrains.speed.min(spd, train.speed_restriction)
- if spd == -1 then spd = nil end
- local c = not spd and "lime" or (type(spd) == "number" and (spd == 0) and "red" or "orange") or nil
- if c then
- ht[#ht+1] = sformat("130,10=(advtrains_hud_bg.png^[resize\\:30x5^[colorize\\:%s)",c)
- ht[#ht+1] = sformat("130,35=(advtrains_hud_bg.png^[resize\\:30x5^[colorize\\:%s)",c)
- if spd and spd~=0 then
- ht[#ht+1] = sformat("%d,50=(advtrains_hud_arrow.png^[multiply\\:red^[makealpha\\:#000000)", 1+spd*11)
- end
- local floor = math.floor
- local dist = floor(((oc[i].index or train.index)-train.index))
- dist = math.max(0, math.min(999, dist))
- for j = 1, 3, 1 do
- sevenseg(floor((dist/10^(3-j))%10), 119+j*11, 18, 4, 2, "[colorize\\:"..c)
+ local lzbdisp = {c = "darkslategray", d = 888}
+ if lzb and lzb.checkpoints and lzb.checkpoints[1] then
+ local cp = lzb.checkpoints[1]
+ if advtrains.interlocking and cp.udata and cp.udata.signal_pos then
+ local sigd = advtrains.interlocking.db.get_sigd_for_signal(cp.udata.signal_pos)
+ if sigd then
+ local tcbs = advtrains.interlocking.db.get_tcbs(sigd) or {}
+ if tcbs.route_rsn then
+ table.insert(st, ("%s: %s"):format(minetest.pos_to_string(sigd.p), tcbs.route_rsn))
end
- break
end
end
+ local spd = cp.speed
+ local c, arrow
+ if not spd or spd == -1 then
+ c = "lime"
+ elseif spd == 0 then
+ c = "red"
+ elseif not res or spd <= res then
+ c = "orange"
+ arrow = true
+ elseif spd <= max then
+ c = "yellow"
+ arrow = true
+ else
+ c = "cyan"
+ arrow = true
+ end
+ if arrow then
+ hud:add(1+spd*11, 50, T"advtrains_hud_arrow.png":multiply"red")
+ end
+ local dist = math.floor(((cp.index or train.index)-train.index))
+ dist = math.max(0, math.min(999, dist))
+ lzbdisp = {c = c, d = dist}
end
+ hud:add_fill(130, 10, 30, 5, lzbdisp.c)
+ hud:add_fill(130, 35, 30, 5, lzbdisp.c)
+ hud:add_n7seg(131, 18, 28, 14, lzbdisp.d, 3, lzbdisp.c)
if res and res == 0 then
- st[#st+1] = attrans("OVERRUN RED SIGNAL! Examine situation and reverse train to move again.")
+ table.insert(st, S("OVERRUN RED SIGNAL! Examine situation and reverse train to move again."))
end
if train.atc_command then
- st[#st+1] = sformat("ATC: %s%s", train.atc_delay and advtrains.abs_ceil(train.atc_delay).."s " or "", train.atc_command or "")
+ local delay_str = ""
+ if train.atc_delay and train.atc_delay >= 0 then
+ delay_str = advtrains.abs_ceil(train.atc_delay).."s "
+ end
+ if train.atc_wait_finish then
+ delay_str = delay_str.."[W] "
+ end
+ if train.atc_wait_autocouple then
+ delay_str = delay_str.."[Cpl] "
+ end
+ if train.atc_wait_signal then
+ delay_str = delay_str.."[G] "
+ end
+ table.insert(st, ("ATC: %s%s"):format(delay_str, train.atc_command or ""))
end
- return table.concat(st,"\n"), table.concat(ht,":")
+ return table.concat(st,"\n"), tostring(hud)
end
local _, texture = advtrains.hud_train_format { -- dummy train object to demonstrate the train hud
max_speed = 15, speed_restriction = 15, velocity = 15, tarvelocity = 12,
active_control = true, lever = 3, ctrl = {lzb = true}, is_shunt = true,
- door_open = 1, lzb = {oncoming = {{spd=6, idx=125.7}}}, index = 0,
+ door_open = 1, lzb = {checkpoints = {{speed=6, index=125.7}}}, index = 100,
}
minetest.register_node("advtrains:hud_demo",{
diff --git a/advtrains/trainlogic.lua b/advtrains/trainlogic.lua
index f136577..c0ac179 100644
--- a/advtrains/trainlogic.lua
+++ b/advtrains/trainlogic.lua
@@ -1,6 +1,9 @@
--trainlogic.lua
--controls train entities stuff about connecting/disconnecting/colliding trains and other things
+-- Get current translator
+local S = advtrains.translate
+
local setting_overrun_mode = minetest.settings:get("advtrains_overrun_mode")
local benchmark=false
@@ -142,13 +145,8 @@ minetest.register_on_joinplayer(function(player)
local pname = player:get_player_name()
local id=advtrains.player_to_train_mapping[pname]
if id then
- for _,wagon in pairs(minetest.luaentities) do
- if wagon.is_wagon and wagon.initialized and wagon.id then
- local wdata = advtrains.wagons[wagon.id]
- if wdata and wdata.train_id == id then
- wagon:reattach_all()
- end
- end
+ for _, wagon in advtrains.wagon_entity_pairs_in_train(id) do
+ wagon:reattach_all()
end
end
end)
@@ -160,12 +158,10 @@ minetest.register_on_dieplayer(function(player)
if id then
local train=advtrains.trains[id]
if not train then advtrains.player_to_train_mapping[pname]=nil return end
- for _,wagon in pairs(minetest.luaentities) do
- if wagon.is_wagon and wagon.initialized and wagon.train_id==id then
- --when player dies, detach him from the train
- --call get_off_plr on every wagon since we don't know which one he's on.
- wagon:get_off_plr(pname)
- end
+ for _, wagon in advtrains.wagon_entity_pairs_in_train(id) do
+ --when player dies, detach him from the train
+ --call get_off_plr on every wagon since we don't know which one he's on.
+ wagon:get_off_plr(pname)
end
-- just in case no wagon felt responsible for this player: clear train mapping
advtrains.player_to_train_mapping[pname] = nil
@@ -270,6 +266,10 @@ function advtrains.train_ensure_init(id, train)
atwarn(debug.traceback())
return nil
end
+
+ if not train.staticdata then
+ train.staticdata = {}
+ end
train.dirty = true
if train.no_step then
@@ -280,10 +280,12 @@ function advtrains.train_ensure_init(id, train)
assertdef(train, "velocity", 0)
--assertdef(train, "tarvelocity", 0)
assertdef(train, "acceleration", 0)
- assertdef(train, "id", id)
-
+ if train.id ~= id then
+ train.id = id
+ end
+ assertdef(train, "path_ori_cp", {})
- if not train.drives_on or not train.max_speed then
+ if not train.max_speed then
--atprint("in ensure_init: missing properties, updating!")
advtrains.update_trainpart_properties(id)
end
@@ -428,7 +430,8 @@ function advtrains.train_step_b(id, train, dtime)
if train.atc_command then
if (not train.atc_delay or train.atc_delay<=0)
and not train.atc_wait_finish
- and not train.atc_wait_autocouple then
+ and not train.atc_wait_autocouple
+ and not train.atc_wait_signal then
advtrains.atc.execute_atc_command(id, train)
elseif train.atc_delay and train.atc_delay > 0 then
train.atc_delay=train.atc_delay-dtime
@@ -457,6 +460,15 @@ function advtrains.train_step_b(id, train, dtime)
train.atc_wait_finish=nil
end
end
+ -- clear atc_wait_signal immediately when the next LZB checkpoint is not a "stop"
+ -- (but make sure lzb is initialized, otherwise wait for it)
+ if train.atc_wait_signal and train.lzb then
+ local first_ckp = train.lzb.checkpoints and train.lzb.checkpoints[1]
+ -- no checkpoint exists, or it has a speed of either nil or >0
+ if not first_ckp or first_ckp.speed ~= 0 then
+ train.atc_wait_signal = nil
+ end
+ end
if train.tarvelocity and train.tarvelocity>v0 then
--atprint("in train_step_b: applying ATC ACCEL", train.tarvelocity)
@@ -631,7 +643,7 @@ function advtrains.train_step_b(id, train, dtime)
local ocn = otrn.path_cn[ob_idx]
local ocp = otrn.path_cp[ob_idx]
- local target_is_inside, ref_index, facing
+ local target_is_inside, ref_index, facing, same_dir
if base_cn == ocn then
-- same direction
@@ -803,9 +815,12 @@ function advtrains.train_step_c(id, train, dtime)
if is_loaded_area then
local objs = minetest.get_objects_inside_radius(rcollpos, 2)
for _,obj in ipairs(objs) do
- if not obj:is_player() and obj:get_armor_groups().fleshy and obj:get_armor_groups().fleshy > 0
- and obj:get_luaentity() and obj:get_luaentity().name~="signs_lib:text" then
- obj:punch(obj, 1, { full_punch_interval = 1.0, damage_groups = {fleshy = 1000}, }, nil)
+ if not obj:is_player() then
+ local armor = obj:get_armor_groups()
+ local luaentity = obj:get_luaentity()
+ if armor.fleshy and armor.fleshy > 0 and luaentity and luaentity.name ~= "signs_lib:text" then
+ obj:punch(obj, 1, { full_punch_interval = 1.0, damage_groups = {fleshy = 1000}, }, nil)
+ end
end
end
end
@@ -842,7 +857,7 @@ local callbacks_leave_node, run_callbacks_leave_node = mknodecallback("leave")
-- Node callback for approaching
-- Might be called multiple times, whenever path is recalculated. Also called for the first node the train is standing on, then has_entered is true.
-- signature is function(pos, id, train, index, has_entered, lzbdata)
--- has_entered: true if the "enter" callback has already been executed for this train in this location
+-- has_entered: Provided for legacy reasons. Always false. (used to signify that the enter callback has already been called, at a time where enter was still called at .5 index)
-- lzbdata: arbitrary data (shared between all callbacks), deleted when LZB is restarted.
-- These callbacks are called in order of distance as train progresses along tracks, so lzbdata can be used to
-- keep track of a train's state once it passes this point
@@ -861,13 +876,10 @@ local function tnc_call_enter_callback(pos, train_id, train, index)
run_callbacks_enter_node(pos, train_id, train, index)
-- check for split points
- if mregnode and mregnode.at_conns and #mregnode.at_conns == 3 and train.path_cp[index] == 3 then
- -- train came from connection 3 of a switch, so it split points.
- if not train.points_split then
- train.points_split = {}
- end
- train.points_split[advtrains.encode_pos(pos)] = true
- --atdebug(train_id,"split points at",pos)
+ if mregnode and mregnode.at_conn_map then
+ -- If this node has >2 conns (and a connmap), remember the connection where we came from to handle split points
+ --atdebug("Train",train_id,"at",pos,"saving turnout origin CP",train.path_cp[index],"for path item",index)
+ train.path_ori_cp[advtrains.encode_pos(pos)] = train.path_cp[index]
end
end
local function tnc_call_leave_callback(pos, train_id, train, index)
@@ -882,23 +894,16 @@ local function tnc_call_leave_callback(pos, train_id, train, index)
run_callbacks_leave_node(pos, train_id, train, index)
-- split points do not matter anymore. clear them
- if train.points_split then
- if train.points_split[advtrains.encode_pos(pos)] then
- train.points_split[advtrains.encode_pos(pos)] = nil
- --atdebug(train_id,"has passed split points at",pos)
- end
- -- any entries left?
- for _,_ in pairs(train.points_split) do
- return
- end
- train.points_split = nil
+ if mregnode and mregnode.at_conn_map then
+ -- If this node has >2 conns (and a connmap), remember the connection where we came from to handle split points
+ --atdebug("Train",train_id,"at",pos,"removing turnout origin CP for path item",index," because train has left it")
+ train.path_ori_cp[advtrains.encode_pos(pos)] = nil
end
- -- WARNING possibly unreachable place!
end
function advtrains.tnc_call_approach_callback(pos, train_id, train, index, lzbdata)
--atdebug("tnc approach",pos,train_id, lzbdata)
- local has_entered = atround(train.index) == index
+ local has_entered = false -- 2024-11-25: has_entered is now always false, because enter point = LZB hit point!
local node = advtrains.ndb.get_node(pos) --this spares the check if node is nil, it has a name in any case
local mregnode=minetest.registered_nodes[node.name]
@@ -911,18 +916,21 @@ function advtrains.tnc_call_approach_callback(pos, train_id, train, index, lzbda
end
-- === te callback definition for tnc node callbacks ===
+-- Change 2024-11-25: Enter node happens when index surpasses whole number (i.e. at center of rail)
+-- Instead of atround (prev behavior) nouw use floor and ceil (note this also fixes issues where index was exactly on .0)
+local atceil = math.ceil
advtrains.te_register_on_new_path(function(id, train)
train.tnc = {
- old_index = atround(train.index),
- old_end_index = atround(train.end_index),
+ old_index = atfloor(train.index),
+ old_end_index = atceil(train.end_index),
}
--atdebug(id,"tnc init",train.index,train.end_index)
end)
advtrains.te_register_on_update(function(id, train)
- local new_index = atround(train.index)
- local new_end_index = atround(train.end_index)
+ local new_index = atfloor(train.index)
+ local new_end_index = atceil(train.end_index)
local old_index = train.tnc.old_index
local old_end_index = train.tnc.old_end_index
while old_index < new_index do
@@ -940,8 +948,8 @@ advtrains.te_register_on_update(function(id, train)
end)
advtrains.te_register_on_create(function(id, train)
- local index = atround(train.index)
- local end_index = atround(train.end_index)
+ local index = atfloor(train.index)
+ local end_index = atceil(train.end_index)
while end_index <= index do
local pos = advtrains.round_vector_floor_y(advtrains.path_get(train,end_index))
tnc_call_enter_callback(pos, id, train, end_index)
@@ -951,8 +959,8 @@ advtrains.te_register_on_create(function(id, train)
end)
advtrains.te_register_on_remove(function(id, train)
- local index = atround(train.index)
- local end_index = atround(train.end_index)
+ local index = atfloor(train.index)
+ local end_index = atceil(train.end_index)
while end_index <= index do
local pos = advtrains.round_vector_floor_y(advtrains.path_get(train,end_index))
tnc_call_leave_callback(pos, id, train, end_index)
@@ -1034,10 +1042,9 @@ end
-- Note: safe_decouple_wagon() has been moved to wagons.lua
--- this function sets wagon's pos_in_train(parts) properties and train's max_speed and drives_on (and more)
+-- this function sets wagon's pos_in_train(parts) properties and train's max_speed (and more)
function advtrains.update_trainpart_properties(train_id, invert_flipstate)
local train=advtrains.trains[train_id]
- train.drives_on=advtrains.merge_tables(advtrains.all_tracktypes)
--FIX: deep-copy the table!!!
train.max_speed=20
train.extent_h = 0;
@@ -1055,7 +1062,7 @@ function advtrains.update_trainpart_properties(train_id, invert_flipstate)
if not wagon then
local ent = advtrains.wagon_objects[w_id]
local pdesc
- if ent then
+ if ent and ent:get_pos() then
pdesc = "at " .. minetest.pos_to_string(ent:get_pos())
elseif train.last_pos then
pdesc = "near " .. minetest.pos_to_string(train.last_pos)
@@ -1079,13 +1086,6 @@ function advtrains.update_trainpart_properties(train_id, invert_flipstate)
end
rel_pos=rel_pos+wagon.wagon_span
- if wagon.drives_on then
- for k,_ in pairs(train.drives_on) do
- if not wagon.drives_on[k] then
- train.drives_on[k]=nil
- end
- end
- end
train.max_speed=math.min(train.max_speed, wagon.max_speed)
train.extent_h = math.max(train.extent_h, wagon.extent_h or 1);
end
@@ -1117,8 +1117,17 @@ function advtrains.spawn_wagons(train_id)
if advtrains.position_in_range(pos, ablkrng) then
--atdebug("wagon",w_id,"spawning")
local wt = advtrains.get_wagon_prototype(data)
- local wagon = minetest.add_entity(pos, wt):get_luaentity()
- wagon:set_id(w_id)
+ local wobj = minetest.add_entity(pos, wt)
+ if not wobj then
+ atwarn("Failed to spawn wagon", w_id, "of type", wt)
+ else
+ local wagon = wobj:get_luaentity()
+ if not wagon then
+ atwarn("Wagon", w_id, "of type", wt, "spawned with nil luaentity")
+ else
+ wagon:set_id(w_id)
+ end
+ end
end
end
else
@@ -1185,6 +1194,8 @@ function advtrains.split_train_at_index(train, index)
newtrain.points_split = advtrains.merge_tables(train.points_split)
newtrain.autocouple = train.autocouple
+ advtrains.te_run_callbacks_on_decouple(train, newtrain, index)
+
return newtrain_id -- return new train ID, so new train can be manipulated
end
@@ -1197,8 +1208,24 @@ function advtrains.invert_train(train_id)
return
end
+ -- Before flipping the train, we must check if there are any points on the path
+ -- which will become split on rotating, and store their cn (which will become the cp)
+ local ori_cp_after_flip = {}
+ for index = atround(train.end_index),atround(train.index) do
+ local pos = advtrains.path_get(train, index)
+ local ok, conns, railheight, connmap = advtrains.get_rail_info_at(pos)
+ if ok and connmap then
+ --atdebug("Reversing Train",train.id," ori_cp Checks: at",pos,"saving turnout origin CP",train.path_cn[index],"for path item",index)
+ ori_cp_after_flip[advtrains.encode_pos(pos)] = train.path_cn[index]
+ end
+ end
+
+ -- Actual rotation happens here! This sets the path restore position to the end of the train, inverting the connid.
advtrains.path_setrestore(train, true)
+ -- clear the origin cp list because it is now invalid, and replace it by what we built prior.
+ train.path_ori_cp = ori_cp_after_flip
+
-- rotate some other stuff
if train.door_open then
train.door_open = - train.door_open
diff --git a/advtrains/wagonprop_tool.lua b/advtrains/wagonprop_tool.lua
new file mode 100644
index 0000000..f32dd03
--- /dev/null
+++ b/advtrains/wagonprop_tool.lua
@@ -0,0 +1,47 @@
+
+-- Get current translator
+local S = advtrains.translate
+
+minetest.register_craftitem("advtrains:wagon_prop_tool",{ --craftitem because it does nothing on its own
+ description = S("Wagon Properties Tool\nPunch a wagon to view and edit the Wagon Properties"),
+ short_description = S("Wagon Properties Tool"),
+ groups = {},
+ inventory_image = "advtrains_wagon_prop_tool.png",
+ wield_image = "advtrains_wagon_prop_tool.png",
+ stack_max = 1,
+ on_use = function(itemstack, user, pointed_thing)
+ local pname = user:get_player_name()
+ if not pname or pname == "" then
+ return
+ end
+
+ --sanity checks in case of clicking the wrong entity/node/nothing
+ if pointed_thing.type ~= "object" then return end --not an entity
+ local object = pointed_thing.ref:get_luaentity()
+ if not object.id then return end --entity doesn't have an id field
+
+ local wagon = advtrains.wagons[object.id] --check if wagon exists in advtrains
+ if not wagon then --not a wagon
+ return
+ end --end sanity checks
+
+ --whitelist protection check
+ if not advtrains.check_driving_couple_protection(pname,wagon.owner,wagon.whitelist) then
+ minetest.chat_send_player(pname, S("Insufficient privileges to use this!"))
+ return
+ end
+ object:show_wagon_properties(pname)
+ return itemstack
+ end,
+})
+
+if minetest.get_modpath("default") then --register recipe
+ minetest.register_craft({
+ output = "advtrains:wagon_prop_tool",
+ recipe = {
+ {"advtrains:dtrack_placer","dye:black","default:paper"},
+ {"screwdriver:screwdriver","default:paper","default:paper"},
+ {"","","group:wood"},
+ }
+ })
+end
diff --git a/advtrains/wagons.lua b/advtrains/wagons.lua
index 62e65af..52ce577 100644
--- a/advtrains/wagons.lua
+++ b/advtrains/wagons.lua
@@ -7,13 +7,22 @@
-- An entity is ONLY spawned by update_trainpart_properties when it finds it useful.
-- Only data that are only important to the entity itself are stored in the luaentity
+-- Get current translator
+local S = advtrains.translate
+
-- TP delay when getting off wagon
local GETOFF_TP_DELAY = 0.5
local IGNORE_WORLD = advtrains.IGNORE_WORLD
advtrains.wagons = {}
-advtrains.wagon_prototypes = {}
+advtrains.wagon_alias = {}
+advtrains.wagon_prototypes = setmetatable({}, {
+ __index = function(t, k)
+ local _, proto = advtrains.resolve_wagon_alias(k)
+ return proto
+ end
+})
advtrains.wagon_objects = {}
local unload_wgn_range = advtrains.wagon_load_range + 32
@@ -148,12 +157,12 @@ function wagon:ensure_init()
end
end
if not self.noninitticks then
- atwarn("wagon",self.id,"uninitialized init=",self.initialized)
+ atwarn("Wagon",self.id,"Uninitialized init=",self.initialized)
self.noninitticks=0
end
self.noninitticks=self.noninitticks+1
if self.noninitticks>20 then
- atwarn("wagon",self.id,"uninitialized, removing")
+ atwarn("Wagon",self.id,S("Uninitialized, removing"))
self:destroy()
else
self.object:set_velocity({x=0,y=0,z=0})
@@ -176,7 +185,7 @@ function wagon:on_punch(puncher, time_from_last_punch, tool_capabilities, direct
return
end
if data.owner and puncher:get_player_name()~=data.owner and (not minetest.check_player_privs(puncher, {train_admin = true })) then
- minetest.chat_send_player(puncher:get_player_name(), attrans("This wagon is owned by @1, you can't destroy it.", data.owner));
+ minetest.chat_send_player(puncher:get_player_name(), S("This wagon is owned by @1, you can't destroy it.", data.owner));
return
end
@@ -195,25 +204,25 @@ function wagon:on_punch(puncher, time_from_last_punch, tool_capabilities, direct
if self.has_inventory then
local inv=minetest.get_inventory({type="detached", name="advtrains_wgn_"..self.id})
if not inv then -- inventory is not initialized when wagon was never loaded - should never happen
- atwarn("Destroying wagon with inventory, but inventory is not found? Shouldn't happen!")
+ atwarn(S("Destroying wagon with inventory, but inventory is not found? Shouldn't happen!"))
return
end
for listname, _ in pairs(inv:get_lists()) do
if not inv:is_empty(listname) then
- minetest.chat_send_player(puncher:get_player_name(), attrans("The wagon's inventory is not empty!"));
+ minetest.chat_send_player(puncher:get_player_name(), S("The wagon's inventory is not empty."));
return
end
end
end
if #(self:train().trainparts)>1 then
- minetest.chat_send_player(puncher:get_player_name(), attrans("Wagon needs to be decoupled from other wagons in order to destroy it."));
+ minetest.chat_send_player(puncher:get_player_name(), S("Wagon needs to be decoupled from other wagons in order to destroy it."));
return
end
local pc=puncher:get_player_control()
if not pc.sneak then
- minetest.chat_send_player(puncher:get_player_name(), attrans("Warning: If you destroy this wagon, you only get some steel back! If you are sure, hold Sneak and left-click the wagon."))
+ minetest.chat_send_player(puncher:get_player_name(), S("Warning: If you destroy this wagon, you only get some steel back! If you are sure, hold Sneak and left-click the wagon."))
return
end
@@ -234,7 +243,7 @@ function wagon:destroy()
if self.id then
local data = advtrains.wagons[self.id]
if not data then
- atwarn("wagon:destroy(): data is not set!")
+ atwarn(" wagon:destroy(): data is not set!")
return
end
@@ -280,7 +289,7 @@ function wagon:on_step(dtime)
local data = advtrains.wagons[self.id]
if not pos then
- --atdebug("["..self.id.."][fatal] missing position (object:getpos() returned nil)")
+ --atdebug("["..self.id.."][fatal] missing position (object:get_pos() returned nil)")
return
end
@@ -361,15 +370,15 @@ function wagon:on_step(dtime)
--show off-track information in outside text instead of notifying the whole server about this
if train.off_track then
- outside = outside .."\n!!! Train off track !!!"
+ outside = outside .."\n"..S("!!! Train off track !!!")
end
-- liquid container: display liquid contents in infotext
if self.techage_liquid_capacity then
if data.techage_liquid and data.techage_liquid.name then
- outside = outside .."\nLiquid: "..data.techage_liquid.name..", "..data.techage_liquid.amount.." units"
+ outside = outside .."\n"..S("Liquid: ")..data.techage_liquid.name..", "..data.techage_liquid.amount..S(" units")
else
- outside = outside .."\nLiquid: empty"
+ outside = outside .."\n"..S("Liquid: empty")
end
end
@@ -455,26 +464,34 @@ function wagon:on_step(dtime)
--needs to know index and path
if train.velocity==0 and self.door_entry and train.door_open and train.door_open~=0 then
--using the mapping created by the trainlogic globalstep
+ local platform_offset = math.floor(self.wagon_width / 2)
for i, ino in ipairs(self.door_entry) do
--fct is the flipstate flag from door animation above
local aci = advtrains.path_get_index_by_offset(train, index, ino*fct)
local ix1, ix2 = advtrains.path_get_adjacent(train, aci)
-- the two wanted positions are ix1 and ix2 + (2nd-1st rotated by 90deg)
-- (x z) rotated by 90deg is (-z x) (http://stackoverflow.com/a/4780141)
- local add = { x = (ix2.z-ix1.z)*train.door_open, y = 0, z = (ix1.x-ix2.x)*train.door_open }
- local pts1=vector.round(vector.add(ix1, add))
- local pts2=vector.round(vector.add(ix2, add))
- if minetest.get_item_group(minetest.get_node(pts1).name, "platform")>0 then
- local ckpts={
- pts1,
- pts2,
- vector.add(pts1, {x=0, y=1, z=0}),
- vector.add(pts2, {x=0, y=1, z=0}),
- }
- for _,ckpos in ipairs(ckpts) do
- local cpp=minetest.pos_to_string(ckpos)
- if advtrains.playersbypts[cpp] then
- self:on_rightclick(advtrains.playersbypts[cpp])
+ local add = {
+ x = atround((ix2.z-ix1.z)*train.door_open),
+ y = 0,
+ z = atround((ix1.x-ix2.x)*train.door_open)
+ }
+ for offset = (platform_offset == 0 and 0 or 1), platform_offset do
+ local scaled_add = vector.multiply(add, offset)
+ local pts1=vector.add(ix1, scaled_add)
+ local pts2=vector.add(ix2, scaled_add)
+ if minetest.get_item_group(minetest.get_node(pts1).name, "platform")>0 then
+ local ckpts={
+ pts1,
+ pts2,
+ vector.add(pts1, {x=0, y=1, z=0}),
+ vector.add(pts2, {x=0, y=1, z=0}),
+ }
+ for _,ckpos in ipairs(ckpts) do
+ local cpp=minetest.pos_to_string(ckpos)
+ if advtrains.playersbypts[cpp] then
+ self:on_rightclick(advtrains.playersbypts[cpp])
+ end
end
end
end
@@ -655,21 +672,21 @@ function wagon:on_rightclick(clicker)
end
end
if self.has_inventory and self.get_inventory_formspec and advtrains.check_driving_couple_protection(pname, data.owner, data.whitelist) then
- poss[#poss+1]={name=attrans("Show Inventory"), key="inv"}
+ poss[#poss+1]={name=S("Show Inventory"), key="inv"}
end
if self.seat_groups[sgr].driving_ctrl_access and advtrains.check_driving_couple_protection(pname, data.owner, data.whitelist) then
- poss[#poss+1]={name=attrans("Onboard Computer"), key="bordcom"}
+ poss[#poss+1]={name=S("Onboard Computer"), key="bordcom"}
end
if data.owner==pname then
- poss[#poss+1]={name=attrans("Wagon properties"), key="prop"}
+ poss[#poss+1]={name=S("Wagon properties"), key="prop"}
end
if not self.seat_groups[sgr].require_doors_open or self:train().door_open~=0 then
- poss[#poss+1]={name=attrans("Get off"), key="off"}
+ poss[#poss+1]={name=S("Get off"), key="off"}
else
if clicker:get_player_control().sneak then
- poss[#poss+1]={name=attrans("Get off (forced)"), key="off"}
+ poss[#poss+1]={name=S("Get off (forced)"), key="off"}
else
- poss[#poss+1]={name=attrans("(Doors closed)"), key="dcwarn"}
+ poss[#poss+1]={name=S("(Doors closed)"), key="dcwarn"}
end
end
if #poss==0 then
@@ -679,7 +696,7 @@ function wagon:on_rightclick(clicker)
else
local form = "size[5,"..1+(#poss).."]"
for pos,ent in ipairs(poss) do
- form = form .. "button_exit[0.5,"..(pos-0.5)..";4,1;"..ent.key..";"..ent.name.."]"
+ form = form.. "button_exit[0.5,"..(pos-0.5)..";4,1;"..ent.key..";"..ent.name.."]"
end
minetest.show_formspec(pname, "advtrains_seating_"..self.id, form)
end
@@ -698,7 +715,7 @@ function wagon:on_rightclick(clicker)
end
local doors_open = self:train().door_open~=0 or clicker:get_player_control().sneak
- local allow, rsn=false, "Wagon has no seats!"
+ local allow, rsn=false, S("This wagon has no seats.")
for _,sgr in ipairs(self.assign_to_seat_group) do
allow, rsn = self:check_seat_group_access(pname, sgr)
if allow then
@@ -709,16 +726,16 @@ function wagon:on_rightclick(clicker)
self:get_on(clicker, seatid)
return
else
- rsn="Wagon is full."
+ rsn=S("This wagon is full.")
end
else
- rsn="Doors are closed! (try holding sneak key!)"
+ rsn=S("Doors are closed! (Try holding sneak key!)")
end
end
end
end
end
- minetest.chat_send_player(pname, attrans("Can't get on: "..rsn))
+ minetest.chat_send_player(pname, rsn or S("You can't get on this wagon."))
else
self:show_get_on_form(pname)
end
@@ -814,8 +831,8 @@ function wagon:get_off(seatno)
end
--if not door_entry, or paths missing, fall back to old method
--atdebug("using fallback")
- local objpos=advtrains.round_vector_floor_y(self.object:getpos())
- local yaw=self.object:getyaw()
+ local objpos=advtrains.round_vector_floor_y(self.object:get_pos())
+ local yaw=self.object:get_yaw()
local isx=(yaw < math.pi/4) or (yaw > 3*math.pi/4 and yaw < 5*math.pi/4) or (yaw > 7*math.pi/4)
local offp
--abuse helper function
@@ -844,7 +861,7 @@ function wagon:show_get_on_form(pname)
end
return
end
- local form, comma="size[5,8]label[0.5,0.5;"..attrans("Select seat:").."]textlist[0.5,1;4,6;seat;", ""
+ local form, comma="size[5,8]label[0.5,0.5;"..S("Select seat:").."]textlist[0.5,1;4,6;seat;", ""
for seatno, seattbl in ipairs(self.seats) do
local addtext, colorcode="", ""
if data.seatp and data.seatp[seatno] then
@@ -856,7 +873,7 @@ function wagon:show_get_on_form(pname)
end
form=form..";0,false]"
if self.has_inventory and self.get_inventory_formspec then
- form=form.."button_exit[1,7;3,1;inv;"..attrans("Show Inventory").."]"
+ form=form.."button_exit[1,7;3,1;inv;"..S("Show Inventory").."]"
end
minetest.show_formspec(pname, "advtrains_geton_"..self.id, form)
end
@@ -868,31 +885,32 @@ function wagon:show_wagon_properties(pname)
]]
local data = advtrains.wagons[self.id]
local form="size[5,5]"
- form = form .. "field[0.5,1;4.5,1;whitelist;Allow these players to access your wagon:;"..minetest.formspec_escape(data.whitelist or "").."]"
- form = form .. "field[0.5,2;4.5,1;roadnumber;Wagon road number:;"..minetest.formspec_escape(data.roadnumber or "").."]"
+ form=form.."label[0.2,0;"..S("This Wagon ID")..": "..self.id.." ("..data.owner..")]"
+ form = form.."field[0.5,1;4.5,1;whitelist;"..S("Allow these players to access your wagon:")..";"..minetest.formspec_escape(data.whitelist or "").."]"
+ form = form.."field[0.5,2;4.5,1;roadnumber;"..S("Wagon road number:")..";"..minetest.formspec_escape(data.roadnumber or "").."]"
local fc = ""
if data.fc then
fc = table.concat(data.fc, "!")
end
- form = form .. "field[0.5,3;4.5,1;fc;Freight Code:;"..fc.."]"
+ form = form.. "field[0.5,3;4.5,1;fc;"..S("Freight Code:")..";"..fc.."]"
if data.fc then
if not data.fcind then data.fcind = 1 end
if data.fcind > 1 then
- form=form.."button[0.5,3.5;1,1;fcp;prev FC]"
+ form=form.."button[0.5,3.5;1,1;fcp;"..S("Prev FC").."]"
end
- form=form.."label[1.5,3.5;Current FC:]"
+ form=form.."label[1.5,3.5;"..S("Current FC: ").."]"
local cur = data.fc[data.fcind] or ""
form=form.."label[1.5,3.75;"..minetest.formspec_escape(cur).."]"
- form=form.."button[3.5,3.5;1,1;fcn;next FC]"
+ form=form.."button[3.5,3.5;1,1;fcn;"..S("Next FC:").."]"
end
- form=form.."button_exit[0.5,4.5;4,1;save;"..attrans("Save wagon properties").."]"
+ form=form.."button_exit[0.5,4.5;4,1;save;"..S("Save wagon properties").."]"
minetest.show_formspec(pname, "advtrains_prop_"..self.id, form)
end
--BordCom
local function checkcouple(ent)
- if not ent or not ent:getyaw() then
+ if not ent or not ent:get_yaw() then
return nil
end
local le = ent:get_luaentity()
@@ -971,29 +989,30 @@ function wagon:show_bordcom(pname)
local linhei
local form = "size[11,9]label[0.5,0;AdvTrains Boardcom v0.1]"
- form=form.."textarea[0.5,1.5;7,1;text_outside;"..attrans("Text displayed outside on train")..";"..(minetest.formspec_escape(train.text_outside or "")).."]"
- form=form.."textarea[0.5,3;7,1;text_inside;"..attrans("Text displayed inside train")..";"..(minetest.formspec_escape(train.text_inside or "")).."]"
- form=form.."field[7.5,1.75;3,1;line;"..attrans("Line")..";"..(minetest.formspec_escape(train.line or "")).."]"
- form=form.."field[7.5,3.25;3,1;routingcode;"..attrans("Routingcode")..";"..(minetest.formspec_escape(train.routingcode or "")).."]"
+ form=form.."textarea[7.5,0.05;10,1;;"..S("Train ID")..": "..(minetest.formspec_escape(train.id or ""))..";]"
+ form=form.."textarea[0.5,1.5;7,1;text_outside;"..S("Text displayed outside on train")..";"..(minetest.formspec_escape(train.text_outside or "")).."]"
+ form=form.."textarea[0.5,3;7,1;text_inside;"..S("Text displayed inside train")..";"..(minetest.formspec_escape(train.text_inside or "")).."]"
+ form=form.."field[7.5,1.75;3,1;line;"..S("Line")..";"..(minetest.formspec_escape(train.line or "")).."]"
+ form=form.."field[7.5,3.25;3,1;routingcode;"..S("Routingcode")..";"..(minetest.formspec_escape(train.routingcode or "")).."]"
--row 5 : train overview and autocoupling
if train.velocity==0 then
- form=form.."label[0.5,4;Train overview /coupling control:]"
+ form=form.."label[0.5,4;"..S("Train overview /coupling control:").."])"
linhei=5
local pre_own, pre_wl, owns_any = nil, nil, minetest.check_player_privs(pname, "train_admin")
for i, tpid in ipairs(train.trainparts) do
local ent = advtrains.wagons[tpid]
if ent then
local roadnumber = ent.roadnumber or ""
- form = form .. string.format("button[%d,%d;%d,%d;%s;%s]", i, linhei, 1, 0.2, "wgprp"..i, roadnumber)
+ form = form.. string.format("button[%d,%d;%d,%d;%s;%s]", i, linhei, 1, 0.2, "wgprp"..i, roadnumber)
local ename = ent.type
- form = form .. "item_image["..i..","..(linhei+0.5)..";1,1;"..ename.."]"
+ form = form.. "item_image["..i..","..(linhei+0.5)..";1,1;"..ename.."]"
if i~=1 then
if checklock(pname, ent.owner, pre_own, ent.whitelist, pre_wl) then
- form = form .. "image_button["..(i-0.5)..","..(linhei+1.5)..";1,1;advtrains_discouple.png;dcpl_"..i..";]"
+ form = form.. "image_button["..(i-0.5)..","..(linhei+1.5)..";1,1;advtrains_discouple.png;dcpl_"..i..";]"
end
end
if i == data.pos_in_trainparts then
- form = form .. "box["..(i-0.1)..","..(linhei+0.4)..";1,1;green]"
+ form = form.. "box["..(i-0.1)..","..(linhei+0.4)..";1,1;green]"
end
pre_own = ent.owner
pre_wl = ent.whitelist
@@ -1002,24 +1021,24 @@ function wagon:show_bordcom(pname)
end
if train.movedir==1 then
- form = form .. "label["..(#train.trainparts+1)..","..(linhei)..";-->]"
+ form = form.. "label["..(#train.trainparts+1)..","..(linhei)..";-->]"
else
- form = form .. "label[0.5,"..(linhei)..";<--]"
+ form = form.. "label[0.5,"..(linhei)..";<--]"
end
--check cpl_eid_front and _back of train
local couple_front = checkcouple(train.cpl_front)
local couple_back = checkcouple(train.cpl_back)
if couple_front then
- form = form .. "image_button[0.5,"..(linhei+1)..";1,1;advtrains_couple.png;cpl_f;]"
+ form = form.. "image_button[0.5,"..(linhei+1)..";1,1;advtrains_couple.png;cpl_f;]"
end
if couple_back then
- form = form .. "image_button["..(#train.trainparts+0.5)..","..(linhei+1)..";1,1;advtrains_couple.png;cpl_b;]"
+ form = form.. "image_button["..(#train.trainparts+0.5)..","..(linhei+1)..";1,1;advtrains_couple.png;cpl_b;]"
end
else
- form=form.."label[0.5,4.5;Train overview / coupling control is only shown when the train stands.]"
+ form=form.."label[0.5,4.5;"..S("Train overview / coupling control is only shown when the train stands.").."]"
end
- form = form .. "button[0.5,8;3,1;save;Save]"
+ form = form.. "button[0.5,8;3,1;save;"..S("Save").."]"
-- Interlocking functionality: If the interlocking module is loaded, you can set the signal aspect
-- from inside the train
@@ -1029,14 +1048,14 @@ function wagon:show_bordcom(pname)
local oci = train.lzb.checkpoints[i]
if oci.udata and oci.udata.signal_pos then
if advtrains.interlocking.db.get_sigd_for_signal(oci.udata.signal_pos) then
- form = form .. "button[4.5,8;5,1;ilrs;Remote Routesetting]"
+ form = form.. "button[4.5,8;5,1;ilrs;"..S("Remote Routesetting").."]"
break
end
end
i=i+1
end
if train.ars_disable then
- form = form .. "button[4.5,7;5,1;ilarsenable;Clear 'Disable ARS' flag]"
+ form = form.. "button[4.5,7;5,1;ilarsenable;"..S("Clear 'Disable ARS' flag").."]"
end
end
@@ -1088,12 +1107,11 @@ function wagon:handle_bordcom_fields(pname, formname, fields)
for i, tpid in ipairs(train.trainparts) do
if fields["dcpl_"..i] then
advtrains.safe_decouple_wagon(tpid, pname)
- elseif fields["wgprp"..i] then
- for _,wagon in pairs(minetest.luaentities) do
- if wagon.is_wagon and wagon.initialized and wagon.id==tpid and data.owner==pname then
- wagon:show_wagon_properties(pname)
- return
- end
+ elseif fields["wgprp"..i] and data.owner==pname then
+ local wagon = advtrains.get_wagon_entity(tpid)
+ if wagon then
+ wagon:show_wagon_properties(pname)
+ return
end
end
end
@@ -1139,44 +1157,48 @@ end
minetest.register_on_player_receive_fields(function(player, formname, fields)
local uid=string.match(formname, "^advtrains_geton_(.+)$")
if uid then
- for _,wagon in pairs(minetest.luaentities) do
- if wagon.is_wagon and wagon.initialized and wagon.id==uid then
- local data = advtrains.wagons[wagon.id]
- if fields.inv then
- if wagon.has_inventory and wagon.get_inventory_formspec then
- minetest.show_formspec(player:get_player_name(), "advtrains_inv_"..uid, wagon:get_inventory_formspec(player:get_player_name(), make_inv_name(uid)))
- end
- elseif fields.seat then
- local val=minetest.explode_textlist_event(fields.seat)
- if val and val.type~="INV" and not data.seatp[player:get_player_name()] then
- --get on
- wagon:get_on(player, val.index)
- --will work with the new close_formspec functionality. close exactly this formspec.
- minetest.show_formspec(player:get_player_name(), formname, "")
- end
+ local wagon = advtrains.get_wagon_entity(uid)
+ if wagon then
+ local data = advtrains.wagons[wagon.id]
+ if fields.inv then
+ if wagon.has_inventory and wagon.get_inventory_formspec then
+ minetest.show_formspec(player:get_player_name(), "advtrains_inv_"..uid, wagon:get_inventory_formspec(player:get_player_name(), make_inv_name(uid)))
+ end
+ elseif fields.seat then
+ local val=minetest.explode_textlist_event(fields.seat)
+ if val and val.type~="INV" and not data.seatp[player:get_player_name()] then
+ --get on
+ wagon:get_on(player, val.index)
+ --will work with the new close_formspec functionality. close exactly this formspec.
+ minetest.show_formspec(player:get_player_name(), formname, "")
end
end
end
+ return true
end
+
uid=string.match(formname, "^advtrains_seating_(.+)$")
if uid then
- for _,wagon in pairs(minetest.luaentities) do
- if wagon.is_wagon and wagon.initialized and wagon.id==uid then
- local pname=player:get_player_name()
- local no=wagon:get_seatno(pname)
- if no then
- if wagon.seat_groups then
- wagon:seating_from_key_helper(pname, fields, no)
- end
+ local wagon = advtrains.get_wagon_entity(uid)
+ if wagon then
+ local pname=player:get_player_name()
+ local no=wagon:get_seatno(pname)
+ if no then
+ if wagon.seat_groups then
+ wagon:seating_from_key_helper(pname, fields, no)
end
end
end
+ return true
end
+
uid=string.match(formname, "^advtrains_prop_(.+)$")
if uid then
local pname=player:get_player_name()
local data = advtrains.wagons[uid]
- if pname~=data.owner and not minetest.check_player_privs(pname, {train_admin = true}) then
+ if not data then
+ return true
+ elseif pname~=data.owner and not minetest.check_player_privs(pname, {train_admin = true}) then
return true
end
if fields.save or not fields.quit then
@@ -1198,29 +1220,32 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
wagon.show_wagon_properties({id=uid}, pname)
end
end
+ return true
end
uid=string.match(formname, "^advtrains_bordcom_(.+)$")
if uid then
- for _,wagon in pairs(minetest.luaentities) do
- if wagon.is_wagon and wagon.initialized and wagon.id==uid then
- wagon:handle_bordcom_fields(player:get_player_name(), formname, fields)
- end
+ local wagon = advtrains.get_wagon_entity(uid)
+ if wagon then
+ wagon:handle_bordcom_fields(player:get_player_name(), formname, fields)
end
+ return true
end
+
uid=string.match(formname, "^advtrains_inv_(.+)$")
if uid then
local pname=player:get_player_name()
local data = advtrains.wagons[uid]
if fields.prop and data.owner==pname then
- for _,wagon in pairs(minetest.luaentities) do
- if wagon.is_wagon and wagon.initialized and wagon.id==uid and data.owner==pname then
- wagon:show_wagon_properties(pname)
- --wagon:handle_bordcom_fields(player:get_player_name(), formname, fields)
- end
+ local wagon = advtrains.get_wagon_entity(uid)
+ if wagon then
+ wagon:show_wagon_properties(pname)
+ --wagon:handle_bordcom_fields(player:get_player_name(), formname, fields)
end
end
+ return true
end
end)
+
function wagon:seating_from_key_helper(pname, fields, no)
local data = advtrains.wagons[self.id]
local sgr=self.seats[no].group
@@ -1247,7 +1272,7 @@ function wagon:seating_from_key_helper(pname, fields, no)
self:show_bordcom(pname)
end
if fields.dcwarn then
- minetest.chat_send_player(pname, attrans("Doors are closed! Use Sneak+rightclick to ignore the closed doors and get off!"))
+ minetest.chat_send_player(pname, S("Doors are closed. Use Sneak+rightclick to ignore the closed doors and get off."))
end
if fields.off then
self:get_off(no)
@@ -1256,10 +1281,10 @@ end
function wagon:check_seat_group_access(pname, sgr)
local data = advtrains.wagons[self.id]
if self.seat_groups[sgr].driving_ctrl_access and not (advtrains.check_driving_couple_protection(pname, data.owner, data.whitelist)) then
- return false, "Not allowed to access a driver stand!"
+ return false, S("You are not allowed to access the driver stand.")
end
if self.seat_groups[sgr].driving_ctrl_access then
- advtrains.log("Drive", pname, self.object:getpos(), self:train().text_outside)
+ advtrains.log("Drive", pname, self.object:get_pos(), self:train().text_outside)
end
return true
end
@@ -1276,7 +1301,7 @@ end
function advtrains.safe_decouple_wagon(w_id, pname, try_run)
if not minetest.check_player_privs(pname, "train_operator") then
- minetest.chat_send_player(pname, "Missing train_operator privilege")
+ minetest.chat_send_player(pname, S("Missing train_operator privilege"))
return false
end
local data = advtrains.wagons[w_id]
@@ -1294,7 +1319,7 @@ function advtrains.safe_decouple_wagon(w_id, pname, try_run)
end
if not checklock(pname, data.owner, owdata.owner, data.whitelist, owdata.whitelist) then
- minetest.chat_send_player(pname, "Not allowed to do this.")
+ minetest.chat_send_player(pname, S("Not allowed to do this."))
return false
end
@@ -1317,11 +1342,33 @@ function advtrains.get_wagon_prototype(data)
data.type = data.entity_name
data.entity_name = nil
end
- if not wt or not advtrains.wagon_prototypes[wt] then
- atwarn("Unable to load wagon type",wt,", using placeholder")
- wt="advtrains:wagon_placeholder"
+ local rt, proto = advtrains.resolve_wagon_alias(wt)
+ if not rt then
+ --atwarn(S("Unable to load wagon type"),wt,S(", using placeholder"))
+ rt = "advtrains:wagon_placeholder"
+ proto = advtrains.wagon_prototypes[rt]
end
- return wt, advtrains.wagon_prototypes[wt]
+ return rt, proto
+end
+
+function advtrains.register_wagon_alias(src, dst)
+ advtrains.wagon_alias[src] = dst
+end
+
+local function recursive_resolve_alias(name, seen)
+ local prototype = rawget(advtrains.wagon_prototypes, name)
+ if prototype then
+ return name, prototype
+ end
+ local resolved = advtrains.wagon_alias[name]
+ if resolved and not seen[resolved] then
+ seen[name] = true
+ return recursive_resolve_alias(resolved, seen)
+ end
+end
+
+function advtrains.resolve_wagon_alias(name)
+ return recursive_resolve_alias(name, {})
end
function advtrains.standard_inventory_formspec(self, pname, invname)
@@ -1334,7 +1381,7 @@ function advtrains.standard_inventory_formspec(self, pname, invname)
local r = "size[8,11]"..
"list["..invname..";box;0,0;8,3;]"
if data.owner==pname then
- r = r .. "button_exit[0,9;4,1;prop;"..attrans("Wagon properties").."]"
+ r = r .. "button_exit[0,9;4,1;prop;"..S("Wagon properties").."]"
end
r = r .. "list[current_player;main;0,5;8,4;]"..
"listring[]"
@@ -1368,38 +1415,48 @@ function advtrains.register_wagon(sysname_p, prototype, desc, inv_img, nincreati
groups = wagon_groups,
on_place = function(itemstack, placer, pointed_thing)
- if not pointed_thing.type == "node" then
+ if pointed_thing.type ~= "node" then
return
end
+
+ local pos = pointed_thing.under
+ local node = minetest.get_node(pos)
+ local pointed_def = minetest.registered_nodes[node.name]
+ if pointed_def and pointed_def.on_rightclick then
+ local controls = placer:get_player_control()
+ if not controls.sneak then
+ return pointed_def.on_rightclick(pos, node, placer, itemstack, pointed_thing)
+ end
+ end
+
local pname = placer:get_player_name()
- local node=minetest.get_node_or_nil(pointed_thing.under)
- if not node then atprint("[advtrains]Ignore at placer position") return itemstack end
+ if node.name == "ignore" then atprint("[advtrains]Ignore at placer position") return itemstack end
local nodename=node.name
- if(not advtrains.is_track_and_drives_on(nodename, prototype.drives_on)) then
+ if(not advtrains.is_track(nodename)) then
atprint("no track here, not placing.")
return itemstack
end
if not minetest.check_player_privs(placer, {train_operator = true }) then
- minetest.chat_send_player(pname, "You don't have the train_operator privilege.")
+ minetest.chat_send_player(pname, S("You don't have the train_operator privilege."))
return itemstack
end
- if not minetest.check_player_privs(placer, {train_admin = true }) and minetest.is_protected(pointed_thing.under, placer:get_player_name()) then
+ if not minetest.check_player_privs(placer, {train_admin = true }) and minetest.is_protected(pos, placer:get_player_name()) then
return itemstack
end
local tconns=advtrains.get_track_connections(node.name, node.param2)
local yaw = placer:get_look_horizontal()
local plconnid = advtrains.yawToClosestConn(yaw, tconns)
- local prevpos = advtrains.get_adjacent_rail(pointed_thing.under, tconns, plconnid, prototype.drives_on)
+ local prevpos = advtrains.get_adjacent_rail(pos, tconns, plconnid)
if not prevpos then
- minetest.chat_send_player(pname, "The track you are trying to place the wagon on is not long enough!")
+ minetest.chat_send_player(pname, S("The track you are trying to place the wagon on is not long enough!"))
return
end
local wid = advtrains.create_wagon(sysname, pname)
- local id=advtrains.create_new_train_at(pointed_thing.under, plconnid, 0, {wid})
+ local id=advtrains.create_new_train_at(pos, plconnid, 0, {wid})
if not advtrains.is_creative(pname) then
itemstack:take_item()
@@ -1416,7 +1473,6 @@ advtrains.register_wagon("advtrains:wagon_placeholder", {
collisionbox = {-0.3,-0.3,-0.3, 0.3,0.3,0.3},
visual_size = {x=0.7, y=0.7},
initial_sprite_basepos = {x=0, y=0},
- drives_on = advtrains.all_tracktypes,
max_speed = 5,
seats = {
},
@@ -1425,7 +1481,7 @@ advtrains.register_wagon("advtrains:wagon_placeholder", {
assign_to_seat_group = {},
wagon_span=1,
drops={},
-}, "Wagon placeholder", "advtrains_wagon_placeholder.png", true)
+}, S("Wagon placeholder"), "advtrains_wagon_placeholder.png", true)
@@ -1487,4 +1543,57 @@ function advtrains.get_wagon_at_index(train_id, w_index)
end
-- nothing found, dist must be further back
return nil
-end \ No newline at end of file
+end
+
+function advtrains.get_wagon_entity(wagon_id)
+ if not advtrains.wagons[wagon_id] then return end
+ local object = advtrains.wagon_objects[wagon_id]
+ if object then
+ return object:get_luaentity()
+ end
+end
+
+function advtrains.next_wagon_entity_in_train(train, i)
+ local wagon_id = train.trainparts[i + 1]
+ if wagon_id then
+ local wagon = advtrains.get_wagon_entity(wagon_id)
+ if wagon then
+ return i + 1, wagon
+ end
+ end
+end
+
+function advtrains.wagon_entity_pairs_in_train(train_id)
+ local train = advtrains.trains[train_id]
+ if not train then return function() end end
+ return advtrains.next_wagon_entity_in_train, train, 0
+end
+
+minetest.register_chatcommand("at_chown", {
+ params = "<wagon_id> <player_name>",
+ description = "Change the owner of an advtrains wagon",
+ privs = {train_admin=true},
+ func = function(name, param)
+ local params = string.split(param," ")
+ local wid = params[1]
+ local new_owner = params[2]
+ if not wid then return false end --no params added
+ --player name checks
+ if not new_owner then return false, S("Please specify a player name to transfer ownership to.") end --no player name argument
+ if not core.player_exists(new_owner) then return false, S("That player does not exist!") end --is a valid player
+ --wagon id checks
+ if not wid:match("%d%d%d%d%d%d") then return false, S("Not a valid wagon id.") end -- invalid wagon id
+ local w_data = advtrains.wagons[wid]
+ if not w_data then return false, S("That wagon does not exist!") end
+ -- actually chown the wagon
+ local curr_owner = w_data.owner
+ w_data.owner = new_owner
+ advtrains.wagons[wid] = w_data
+ advtrains.log("Chown", name, core.get_player_by_name(name):get_pos(), "wid="..wid..", from="..curr_owner..", to="..new_owner)
+
+ if name ~= new_owner then
+ core.chat_send_player(new_owner, S("You have been given ownership of wagon @1", wid))
+ end
+ return true, S("Wagon @1 ownership changed from @2 to @3", wid, curr_owner, new_owner)
+ end
+})