Compare commits

...

5 Commits

Author SHA1 Message Date
gpcf 45e5ad3b37 Fix boardcom train id display, add command to teleport to train by id 2024-08-08 23:53:29 +02:00
erstazi 3526fc2e4a Adding Train ID to Onboard Computer formspec so we know what the Train ID is without LuaATC 2024-08-08 23:42:07 +02:00
gpcf 2458e986e8 Fix minetest server test run 2024-08-08 23:12:19 +02:00
Y. Wang e9aad541cc Fix incorrect speed indicator; include routing info in text HUD 2024-08-08 22:48:23 +02:00
Y. Wang 0bfc7bbe09 Rework graphical train HUD code
- A basic texture manipulation API is added; currently this is only a
  (selected) subset of texture modifiers provided by MT; the goal is to
  avoid writing (potentially incorrect) texture strings by hand;
- The graphical HUD code is cleaned up; in particular, most code used
  for generating texture patterns are moved to texture.lua so that the
  code can be used outside of the HUD;
- Inactive elements are given the darkslategray background.

A basic unittest is added; however, it needs to be expanded for better
coverage.

Reported-by: Lars Müller <appgurulars@gmx.de>
2024-08-08 22:48:23 +02:00
7 changed files with 312 additions and 96 deletions

View File

@ -11,12 +11,6 @@ sources :
tasks: tasks:
- download_mt_server: |
mkdir bin
wget https://lifomaps.de/advtrains-test/builtin.tar.gz
tar xf builtin.tar.gz
curl https://lifomaps.de/advtrains-test/minetestserver -o ~/bin/minetestserver
chmod +x ~/bin/minetestserver
- install_mt_game : | - install_mt_game : |
curl -L https://github.com/minetest/minetest_game/archive/master.zip -o master.zip curl -L https://github.com/minetest/minetest_game/archive/master.zip -o master.zip
mkdir -p .minetest/games/ mkdir -p .minetest/games/
@ -45,4 +39,4 @@ tasks:
git clone https://git.bananach.space/basic_trains.git/ git clone https://git.bananach.space/basic_trains.git/
- run_test_world: | - run_test_world: |
echo "bind_address = 127.0.0.1" > minetest.conf echo "bind_address = 127.0.0.1" > minetest.conf
~/bin/minetestserver --port 31111 --gameid minetest_game --config ~/minetest.conf --world ~/.minetest/worlds/advtrains_testworld minetestserver --port 31111 --gameid minetest_game --config ~/minetest.conf --world ~/.minetest/worlds/advtrains_testworld --logfile ~/minetest.log

View File

@ -202,6 +202,7 @@ advtrains.meseconrules =
advtrains.fpath=minetest.get_worldpath().."/advtrains" advtrains.fpath=minetest.get_worldpath().."/advtrains"
advtrains.speed = dofile(advtrains.modpath.."/speed.lua") advtrains.speed = dofile(advtrains.modpath.."/speed.lua")
advtrains.texture = dofile(advtrains.modpath.."/texture.lua")
dofile(advtrains.modpath.."/path.lua") dofile(advtrains.modpath.."/path.lua")
dofile(advtrains.modpath.."/trainlogic.lua") dofile(advtrains.modpath.."/trainlogic.lua")
@ -737,6 +738,21 @@ minetest.register_chatcommand("at_whereis",
end end
end, end,
}) })
minetest.register_chatcommand("at_tp",
{
params = "<train id>",
description = "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, "Train "..param.." does not exist or is invalid"
else
minetest.get_player_by_name(name):set_pos(train.last_pos)
return true, "Teleporting to train "..param
end
end,
})
minetest.register_chatcommand("at_disable_step", minetest.register_chatcommand("at_disable_step",
{ {
params = "<yes/no>", params = "<yes/no>",

View File

@ -74,4 +74,5 @@ Buffer and Chain Coupler=Schraubenkupplung
Scharfenberg Coupler=Scharfenbergkupplung Scharfenberg Coupler=Scharfenbergkupplung
Japanese Train Inter-Wagon Connection=Waggonzwischenverbindung Japanischer Personenzug 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) 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)
Train ID=Zugnummer
<none>=<keine> <none>=<keine>

View File

@ -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)

228
advtrains/texture.lua Normal file
View File

@ -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

View File

@ -1,5 +1,7 @@
--trainhud.lua: holds all the code for train controlling --trainhud.lua: holds all the code for train controlling
local T = advtrains.texture
advtrains.hud = {} advtrains.hud = {}
advtrains.hhud = {} advtrains.hhud = {}
@ -184,138 +186,94 @@ function advtrains.hud_train_format(train, flip)
local vel = advtrains.abs_ceil(train.velocity) local vel = advtrains.abs_ceil(train.velocity)
local vel_kmh=advtrains.abs_ceil(advtrains.ms_to_kmh(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.velocity==0 and not train.active_control then tlev=1 end
if train.hud_lzb_effect_tmr then if train.hud_lzb_effect_tmr then
tlev=1 tlev=1
end end
local ht = {"[combine:440x110:0,0=(advtrains_hud_bg.png^[resize\\:440x110)"} local hud = T.combine(440, 110, "black")
local st = {} local st = {}
if train.debug then st = {train.debug} end 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 -- lever
ht[#ht+1] = "275,10=(advtrains_hud_bg.png^[colorize\\:cyan^[resize\\:5x18)" hud:add_multicolor_fill_topdown(275, 10, 5, 90, 1, "cyan", 1, "white", 2, "orange", 1, "red")
ht[#ht+1] = "275,28=(advtrains_hud_bg.png^[colorize\\:white^[resize\\:5x18)" hud:add_lever_topdown(280, 10, 30, 90, 18, 6, (4-tlev)/4, "gray", "darkslategray")
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)
-- reverser -- reverser
ht[#ht+1] = sformat("245,10=(advtrains_hud_arrow.png^[transformFY%s)", flip and "" or "^[multiply\\:cyan") hud:add(245, 10, T"advtrains_hud_arrow.png":transform"FY":multiply(flip and "gray" or "cyan"))
ht[#ht+1] = sformat("245,85=(advtrains_hud_arrow.png%s)", flip and "^[multiply\\:orange" or "") hud:add(245, 85, T"advtrains_hud_arrow.png":multiply(flip and "orange" or "gray"))
ht[#ht+1] = "250,35=(advtrains_hud_bg.png^[colorize\\:darkslategray^[resize\\:5x40)" hud:add_lever_topdown(240, 30, 25, 50, 15, 5, flip and 1 or 0, "gray", "darkslategray")
ht[#ht+1] = sformat("240,%s=(advtrains_hud_bg.png^[resize\\:25x15^[colorize\\:gray)", flip and 65 or 30)
-- train control/safety indication -- train control/safety indication
if train.tarvelocity or train.atc_command then hud:add(10, 10, T"advtrains_hud_atc.png":resize(30, 30):multiply((train.tarvelocity or train.atc_command) and "cyan" or "darkslategray"))
ht[#ht+1] = "10,10=(advtrains_hud_atc.png^[resize\\:30x30^[multiply\\:cyan)" hud:add(50, 10, T"advtrains_hud_lzb.png":resize(30, 30):multiply(train.hud_lzb_effect_tmr and "red" or "darkslategray"))
end hud:add(90, 10, T"advtrains_hud_shunt.png":resize(30, 30):multiply(train.is_shunt and "orange" or "darkslategray"))
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
-- door -- door
ht[#ht+1] = "187,10=(advtrains_hud_bg.png^[resize\\:26x30^[colorize\\:white)" hud:add_fill(187, 10, 26, 30, "white"):add_fill(189, 12, 22, 11, "black")
ht[#ht+1] = "189,12=(advtrains_hud_bg.png^[resize\\:22x11)" hud:add_fill(170, 10, 15, 30, train.door_open==-1 and "white" or "darkslategray"):add_fill(172, 12, 11, 11, "black")
ht[#ht+1] = sformat("170,10=(advtrains_hud_bg.png^[resize\\:15x30^[colorize\\:%s)", train.door_open==-1 and "white" or "darkslategray") hud:add_fill(215, 10, 15, 30, train.door_open==1 and "white" or "darkslategray"):add_fill(217, 12, 11, 11, "black")
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)"
-- speed indication(s) -- speed indication(s)
sevenseg(math.floor(vel/10), 320, 10, 30, 10, "[colorize\\:red\\:255") hud:add_n7seg(320, 10, 110, 90, vel, 2, "red")
sevenseg(vel%10, 380, 10, 30, 10, "[colorize\\:red\\:255") hud:add_segmentbar_leftright(10, 65, 217, 20, 3, 20, max, 20, "darkslategray", 0, vel, "white")
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
if res and res > 0 then 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 end
if train.tarvelocity then 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 end
local lzbdisp
local lzb = train.lzb local lzb = train.lzb
if lzb and lzb.checkpoints then if lzb and lzb.checkpoints then
local oc = lzb.checkpoints local oc = lzb.checkpoints
for i = 1, #oc do for i = 1, #oc do
if advtrains.interlocking then
local udata = oc[i].udata
if udata and udata.signal_pos then
local sigd = advtrains.interlocking.db.get_sigd_for_signal(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
end
end
end
local spd = oc[i].speed local spd = oc[i].speed
spd = advtrains.speed.min(spd, train.speed_restriction) spd = advtrains.speed.min(spd, train.speed_restriction)
if spd == -1 then spd = nil end 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 local c = not spd and "lime" or (type(spd) == "number" and (spd == 0) and "red" or "orange") or nil
if c then 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 if spd and spd~=0 then
ht[#ht+1] = sformat("%d,50=(advtrains_hud_arrow.png^[multiply\\:red^[makealpha\\:#000000)", 1+spd*11) hud:add(1+spd*11, 50, T"advtrains_hud_arrow.png":multiply"red")
end end
local floor = math.floor local dist = math.floor(((oc[i].index or train.index)-train.index))
local dist = floor(((oc[i].index or train.index)-train.index))
dist = math.max(0, math.min(999, dist)) dist = math.max(0, math.min(999, dist))
for j = 1, 3, 1 do lzbdisp = {c = c, d = dist}
sevenseg(floor((dist/10^(3-j))%10), 119+j*11, 18, 4, 2, "[colorize\\:"..c)
end
break break
end end
end end
end end
if not lzbdisp then
lzbdisp = {c = "darkslategray", d = 888}
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 if res and res == 0 then
st[#st+1] = attrans("OVERRUN RED SIGNAL! Examine situation and reverse train to move again.") table.insert(st, attrans("OVERRUN RED SIGNAL! Examine situation and reverse train to move again."))
end end
if train.atc_command then 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 "") table.insert(st, ("ATC: %s%s"):format(train.atc_delay and advtrains.abs_ceil(train.atc_delay).."s " or "", train.atc_command or ""))
end end
return table.concat(st,"\n"), table.concat(ht,":") return table.concat(st,"\n"), tostring(hud)
end end
local _, texture = advtrains.hud_train_format { -- dummy train object to demonstrate the train hud local _, texture = advtrains.hud_train_format { -- dummy train object to demonstrate the train hud
max_speed = 15, speed_restriction = 15, velocity = 15, tarvelocity = 12, max_speed = 15, speed_restriction = 15, velocity = 15, tarvelocity = 12,
active_control = true, lever = 3, ctrl = {lzb = true}, is_shunt = true, 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",{ minetest.register_node("advtrains:hud_demo",{

View File

@ -970,7 +970,7 @@ function wagon:show_bordcom(pname)
local data = advtrains.wagons[self.id] local data = advtrains.wagons[self.id]
local linhei local linhei
local form = "size[11,9]label[0.5,0;AdvTrains Boardcom v0.1]" local form = "size[11,9]label[0.5,0;AdvTrains Boardcom v0.1 | "..attrans("Train ID")..": "..(minetest.formspec_escape(train.id or "")).."]"
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,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.."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,1.75;3,1;line;"..attrans("Line")..";"..(minetest.formspec_escape(train.line or "")).."]"