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>
This commit is contained in:
Y. Wang 2023-06-02 13:30:54 +02:00 committed by gpcf
parent 4cfd07e992
commit 0bfc7bbe09
4 changed files with 281 additions and 87 deletions

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

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
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,98 +186,41 @@ 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
@ -285,37 +230,38 @@ function advtrains.hud_train_format(train, flip)
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",{