2023-09-29 14:42:56 +02:00

720 lines
22 KiB
Lua

local S = minetest.get_translator("advtrains_doc_integration")
local fsescape = minetest.formspec_escape
local worldpath = minetest.get_worldpath() .. DIR_DELIM
advtrains_doc_integration = {}
local function S2(a, b)
return S(a, S(b))
end
local function Ss(x)
if type(x) ~= "string" then
return x
end
return minetest.get_translated_string("en", x)
end
local function htescape(str)
return string.gsub(tostring(str), "([<>])", [[\1]])
end
local function latex_escape(str)
return (string.gsub(str, ".", {
["&"] = [[\&]],
["%"] = [[\%]],
["$"] = [[\$]],
["#"] = [[\#]],
["_"] = [[\_]],
["{"] = [[\{]],
["}"] = [[\}]],
["~"] = [[\textasciitilde]],
["^"] = [[\textasciicircum]],
["\\"] = [[\textbackslash]],
}))
end
local function SL(x)
return latex_escape(Ss(x))
end
local function map(tbl, func)
local t = {}
for k, v in pairs(tbl or {}) do
t[k] = func(v)
end
return t
end
local function htmono(str)
return string.format("<mono>%s</mono>", htescape(str))
end
local function htheader(str)
return string.format("<b>%s</b>", htescape(str))
end
local function describe_conns(conns)
local connsdesc = {[0] = "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}
if type(conns) == "table" then
if conns.c then
if conns.y and conns.y ~= 0 then
return ("%s%+d%%"):format(describe_conns(conns.c), conns.y*100)
else
return describe_conns(conns.c)
end
else
local cst = map(conns, describe_conns)
local cstl = #cst
if cstl == 2 then
return ("%s - %s"):format(unpack(cst))
elseif cstl == 3 then
return ("[%s <-] %s -> %s"):format(cst[3], cst[1], cst[2])
elseif cstl == 4 then
return ("%s - %s; %s - %s"):format(unpack(cst))
elseif cstl == 5 then
return ("[%s,%s <-] %s -> %s"):format(cst[3], cst[4], cst[1], cst[2])
end
end
else
return connsdesc[tonumber(conns)]
end
end
advtrains_doc_integration.describe_conns = describe_conns
if doc then
if doc.sub.items then
local register_factoid = doc.sub.items.register_factoid
local function group_factoid(cat, gr, f)
register_factoid(cat, "groups", function(_, def)
return f(def.groups[gr] or 0) or ""
end)
end
for cat, cinfo in pairs{
nodes = {
not_blocking_trains = S("This block does not block trains."),
save_in_at_nodedb = S("This block is saved in the Advtrains node database."),
},
} do
for group, ginfo in pairs(cinfo) do
local tp = type(ginfo)
if tp == "string" then
group_factoid(cat, group, function(x)
if x > 0 then
return ginfo
end
end)
elseif tp == "function" then
group_factoid(cat, group, ginfo)
end
end
end
register_factoid("nodes", "groups", function(_, ndef)
if ndef.advtrains then
if ndef.advtrains.set_aspect then
return S("This is a signal with a variable aspect.")
elseif ndef.advtrains.get_aspect then
return S("This is a signal with a static aspect.")
end
end
if ndef.at_conns then
return S("This track has the following conns table by default: @1", describe_conns(ndef.at_conns) or "?")
end
return ""
end)
end
doc.add_category("advtrains_wagons", {
name = S("Wagons"),
build_formspec = doc.entry_builders.formspec,
})
end
local function describe_length(x)
local inch = x/0.0254
local infdenom = 32
local infnum = math.floor(inch*infdenom)%infdenom
local ft = math.floor(inch/12)
inch = math.floor(inch)%12
local st = {}
if ft > 0 then
table.insert(st, ft .. "'")
end
local infstr = ""
if infnum > 0 then
while infnum%2 == 0 do
infnum, infdenom = infnum/2, infdenom/2
end
infstr = string.format(" %d/%d", infnum, infdenom)
end
if inch > 0 then
table.insert(st, string.format(' %s%s"', inch, infstr))
elseif infnum > 0 then
table.insert(st, infstr .. '"')
end
if not next(st) then
st = '0"'
end
return string.format("%d mm (%s)", 1000*x, table.concat(st))
end
local function describe_speed(x)
local kmph = x*3.6
local mph = kmph/1.609344
return string.format("%.1f m/s (%.1f km/h; %.1f mph)", x, kmph, mph)
end
local function addlist(lst, tbl, title, fallback1, fallback2, mapf)
if not tbl then
if fallback2 then
table.insert(lst, fallback2)
elseif fallback2 == false and fallback1 then
table.insert(lst, fallback1)
end
elseif next(tbl) ~= nil then
table.insert(lst, title)
for k, v in pairs(tbl) do
if mapf then
k = mapf(k, v)
end
table.insert(lst, "" .. k)
end
elseif fallback1 then
table.insert(lst, fallback1)
end
end
local function get_coupler_name(n)
return advtrains.coupler_types[n] or n
end
local function ht_coupler_name(n)
local s = advtrains.coupler_types[n]
if s then
return htescape(s)
else
return htmono(n)
end
end
local function dlxtrains_livery_information(prototype)
if not dlxtrains then
return nil
end
local old_update_livery = dlxtrains.update_livery
dlxtrains.update_livery = function(_, _, x)
return coroutine.yield(x)
end
local env = {
coroutine = coroutine,
dlxtrains = table.copy(dlxtrains),
}
local function main(G, f)
setfenv(0, G)
f()
return error()
end
local t = {coroutine.resume(coroutine.create(main), env, prototype.custom_may_destroy or function() end)}
dlxtrains.update_livery = old_update_livery
if not t[1] then
return nil
end
return unpack(t, 2)
end
local function advtrains_livery_tools_information(wname)
if not advtrains_livery_database then
return nil
end
local tnames = advtrains_livery_database.get_livery_template_names_for_wagon(wname)
if next(tnames) == nil then
return nil
end
local templates = {}
for _, tname in pairs(tnames) do
templates[tname] = advtrains_livery_database.get_wagon_livery_template(wname, tname)
end
local lnames = advtrains_livery_database.get_predefined_livery_names(wname)
local lnames_t = {}
local liveries = {}
for _, lid in pairs(lnames) do
local lname = lid.livery_name
liveries[lname] = advtrains_livery_database.get_predefined_livery(wname, lname)
table.insert(lnames_t, lname)
end
table.sort(tnames)
table.sort(lnames_t)
return {
templates = templates,
template_names = tnames,
liveries = liveries,
livery_names = lnames_t,
}
end
local function list_itemstring(x)
local item = ItemStack(x)
if item:is_empty() then
return S("Emptyness")
end
return string.format("%s: %d", item:get_short_description(), item:get_count())
end
local function blankline(st)
return table.insert(st, "")
end
local function adjust_wagon_prototype(prototype)
local p = table.copy(prototype)
if p._doc_wagon_longdesc then
p.longdesc = p._long_wagon_longdesc
end
if type(p.horn_sound) == "string" then
p.horn_sound = {name = prototype.horn_sound}
end
if p.horn_sound and p.horn_sound.name == "" then
p.horn_sound = nil
end
local pax, driver = 0, 0
if p.seats and p.seat_groups then
for _, v in pairs(p.seats) do
if p.seat_groups[v.group].driving_ctrl_access then
driver = driver + 1
else
pax = pax + 1
end
p.seat_groups[v.group].count = (p.seat_groups[v.group].count or 0) + 1
end
end
p.max_passengers = pax
p.max_drivers = driver
p.max_seats = pax+driver
return p
end
local function doc_register_wagon(itemname)
local prototype = adjust_wagon_prototype(advtrains.wagon_prototypes[itemname])
local desctext = {}
if prototype._doc_wagon_longdesc then
table.insert(desctext, tostring(prototype._doc_wagon_longdesc))
blankline(desctext)
end
table.insert(desctext, htheader(S("Basic Information")))
table.insert(desctext, S("Itemstring: @1", htmono(itemname)))
addlist(desctext, prototype.drops, S("Drops:"), S("Drops nothing"), false, function(_, v) return list_itemstring(v) end)
addlist(desctext, prototype.drives_on, S("Drives on:"), nil, nil, htmono)
addlist(desctext, prototype.coupler_types_front, S("Compatible front couplers:"), S2("Front coupler: @1", "Absent"), S2("Front coupler: @1", "Universal"), ht_coupler_name)
addlist(desctext, prototype.coupler_types_back, S("Compatible rear couplers:"), S2("Rear coupler: @1", "Absent"), S2("Rear coupler: @1", "Universal"), ht_coupler_name)
table.insert(desctext, S("Wagon span: @1", prototype.wagon_span and describe_length(2*prototype.wagon_span) or S("Undefined")))
table.insert(desctext, S("Maximum speed: @1", prototype.max_speed and describe_speed(prototype.max_speed) or S("Undefined")))
table.insert(desctext, S2("Motive power: @1", prototype.is_locomotive and "Present" or "Absent"))
local hornsound = prototype.horn_sound
table.insert(desctext, S("Horn sound: @1", hornsound and string.format("<action name=playhorn><style color=cyan>%s</style></action>", htmono(hornsound.name)) or S("Undefined")))
blankline(desctext)
table.insert(desctext, htheader(S("Wagon Capacity")))
table.insert(desctext, S("Passenger seats: @1", prototype.max_passengers))
table.insert(desctext, S("Driver seats: @1", prototype.max_drivers))
if prototype.has_inventory then
addlist(desctext, prototype.inventory_list_sizes, S("Cargo inventory size:"), S2("Cargo inventory: @1", "Present"), false, function(k, v)
return string.format("%s: %d", htmono(k), v)
end)
else
table.insert(desctext, S2("Cargo inventory: @1", "Absent"))
end
if techage and prototype.techage_liquid_capacity then
table.insert(desctext, S("Liquid Capacity (Techage): @1", string.format("%d", prototype.techage_liquid_capacity)))
end
blankline(desctext)
table.insert(desctext, htheader(S("Wagon Appearance")))
table.insert(desctext, S("Mesh: @1", prototype.mesh and htmono(prototype.mesh) or "None"))
addlist(desctext, prototype.textures, S("Textures:"), S("No textures"), false, function(_, v) return htmono(v) end)
local bikeliv = S("Unsupported")
local bikelivdesc = nil
if prototype.set_livery then
if prototype.livery_definition then
bikeliv = S("Supported by the multi_component_liveries mod")
bikelivdesc = {}
addlist(bikelivdesc, prototype.livery_definition.components, S("Livery components:"), nil, nil, function(_, v) return htescape(v.description) end)
addlist(bikelivdesc, prototype.livery_definition.presets, S("Livery presets:"), nil, nil, function(_, v) return htescape(v.description) end)
bikelivdesc = table.concat(bikelivdesc, "\n")
else
bikeliv = S("Supported")
end
end
table.insert(desctext, S("Livery system with bike painter: @1", bikeliv))
table.insert(desctext, bikelivdesc)
local dlxlivdef = dlxtrains_livery_information(prototype)
table.insert(desctext, S2("DlxTrains livery system: @1", dlxlivdef and "Supported" or "Unsupported"))
local atlivdef = advtrains_livery_tools_information(itemname)
table.insert(desctext, S2("Advtrains livery tools (Marnack): @1", atlivdef and "Supported" or "Unsupported"))
if atlivdef then
addlist(desctext, atlivdef.template_names, S("Livery templates:"), nil, nil, function(_, v) return htescape(v) end)
addlist(desctext, atlivdef.livery_names, S("Livery presets:"), nil, nil, function(_, v) return htescape(v) end)
end
blankline(desctext)
table.insert(desctext, htheader(S("Implementation Details")))
local attachment_offset_support = S("Unsupported")
if advtrains_attachment_offset_patch then
local t = advtrains_attachment_offset_patch
if prototype.get_on == t.get_on_override and prototype.get_off == t.get_off_override then
attachment_offset_support = S("Supported")
end
end
table.insert(desctext, S("Proper player attachment positioning: @1", attachment_offset_support))
for k, v in pairs {
custom_on_activate = "Custom instantiation callback",
custom_on_step = "Custom step function",
custom_on_velocity_change = "Custom velocity change callback",
} do
table.insert(desctext, S2(v .. ": @1", prototype[k] and "Defined" or "Undefined"))
end
local x0, y0 = doc.FORMSPEC.ENTRY_START_X+0.25, doc.FORMSPEC.ENTRY_START_Y
local x1, y1 = doc.FORMSPEC.ENTRY_END_X+0.75, doc.FORMSPEC.ENTRY_END_Y+0.625
local width, height = x1-x0, y1-y0
local mside = height/2
local mesh = fsescape(prototype.mesh or "")
local textures = table.concat(map(prototype.textures, fsescape), ",")
if dlxlivdef then
textures = fsescape(string.format("%s_%s.png", dlxlivdef.filename_prefix, dlxlivdef[0].code))
end
local fstext = {
string.format("hypertext[%f,%f;%f,%f;entry_body;%s]", x0, y0, width-mside, height+0.875, fsescape(table.concat(desctext, "\n"))),
string.format("item_image[%f,%f;%f,%f;%s]", x1-mside, y0+0.0625, mside, mside, fsescape(itemname)),
string.format("model[%f,%f;%f,%f;%s;%s;%s;%f,%f]",
x1-mside, y1-mside, mside, mside, "wagon_model", mesh, textures, -30, 135),
}
minetest.override_item(itemname, {_doc_items_create_entry = false})
doc.add_entry("advtrains_wagons", itemname, {
name = ItemStack(itemname):get_short_description(),
data = table.concat(fstext),
})
end
if doc then
minetest.register_on_mods_loaded(function()
for k in pairs(advtrains.wagon_prototypes) do
doc_register_wagon(k)
end
end)
end
local function latex_colordesc(cstr)
local color = string.match(cstr,"^#(%x%x%x%x%x%x)$")
cstr = SL(cstr)
if color then
color = SL(string.upper(color))
return string.format([[\tikz \definecolor{c}{HTML}{%s} \draw[fill=c] (0,0) rectangle (1em,1em); \texttt{%s}]], color, cstr)
else
return string.format([[\texttt{%s}]], cstr)
end
end
function advtrains_doc_integration.write_wagon_info_as_latex(itemname)
local filename = string.format("%satdoc_wagon_%s.tex", worldpath, itemname:gsub(":", "_"))
local prototype = adjust_wagon_prototype(advtrains.wagon_prototypes[itemname])
local wname = ItemStack(itemname):get_short_description()
local st = {string.format([[
\documentclass{article}
\usepackage[a4paper,margin=1in,bottom=1.5in]{geometry}
\usepackage[T1]{fontenc}
\usepackage{tikz}
\usepackage{booktabs,multirow,tabularx}
\renewcommand{\arraystretch}{1.5}
\usepackage{hyperref}
\hypersetup{pdftitle={Wagon Datasheet: %s}}
\title{Wagon Datasheet}
\author{%s}
\setlength{\parindent}{0pt}
\begin{document}
\maketitle
]], SL(wname), SL(wname))}
table.insert(st, [[\section{Basic Information}]])
if prototype.longdesc then
table.insert(st, SL(prototype.longdesc) .. "\n")
end
table.insert(st, [[\begin{tabularx}{\textwidth}{l X}]])
table.insert(st, string.format([[Itemstring & \texttt{%s}\\]], SL(itemname)))
if prototype.drives_on then
local i0 = #st+1
local count = 0
for k in pairs(prototype.drives_on) do
table.insert(st, string.format([[& \texttt{%s}\\]], SL(k)))
count = count + 1
end
if count > 0 then
st[i0] = string.format([[Drives on %s]], st[i0])
end
end
if prototype.wagon_span then
table.insert(st, string.format([[Wagon span & %d mm\\]], prototype.wagon_span*2000))
end
if prototype.max_speed then
table.insert(st, string.format([[Maximum speed & %d m/s\\]], prototype.max_speed))
end
table.insert(st, string.format([[Motive power & %s\\]], prototype.is_locomotive and "Present" or "Absent"))
if prototype.horn_sound then
table.insert(st, string.format([[Horn sound & \texttt{%s}\\]], SL(prototype.horn_sound.name)))
else
table.insert(st, [[Horn sound & Undefined\\]])
end
if prototype.mesh then
table.insert(st, string.format([[Mesh & \texttt{%s}\\]], SL(prototype.mesh)))
end
if prototype.textures then
local i0 = #st+1
local count = 0
for _, i in pairs(prototype.textures) do
table.insert(st, string.format([[& \texttt{%s}\\]], SL(i)))
count = count + 1
end
if count > 0 then
st[i0] = string.format([[Textures %s]], st[i0])
end
end
do
local i0 = #st+1
local count = 0
for _, i in ipairs(prototype.drops or {}) do
local item = ItemStack(i)
if not item:is_empty() then
local desc = string.format([[\texttt{%s}]], SL(item:get_name()))
if item:is_known() then
desc = SL(item:get_short_description())
end
table.insert(st, string.format([[& %s: %d\\]], desc, item:get_count()))
count = count + 1
end
end
if count > 0 then
st[i0] = [[Drops ]] .. st[i0]
else
table.insert(st, [[Drops & Nothing \\]])
end
end
table.insert(st, [[\end{tabularx}]])
table.insert(st, [[\section{Coupler Compatibility}]])
do
local fcouplers = prototype.coupler_types_front
local rcouplers = prototype.coupler_types_back
local ccouplers = {}
local lcouplers = {}
local couplerid = {}
local flim, rlim
for k in pairs(fcouplers or {}) do
flim = true
ccouplers[k] = true
end
for k in pairs(rcouplers or {}) do
rlim = true
ccouplers[k] = true
end
for k in pairs(ccouplers) do
local desc = SL(get_coupler_name(k))
table.insert(lcouplers, desc)
couplerid[desc] = k
end
table.sort(lcouplers)
table.insert(st, [[
\begin{tabularx}{\textwidth}{X c c}
\toprule
\multirow[t]{2}{*}{\bfseries Coupler Type} & \multicolumn{2}{c}{\bfseries Compatibility}\\
\cmidrule(lr){2-3}
& {\bfseries Front Coupler} & {\bfseries Rear Coupler}\\\midrule
]])
if not (fcouplers and rcouplers) then
local fd = fcouplers and "" or [[$\bullet$]]
local rd = rcouplers and "" or [[$\bullet$]]
table.insert(st, string.format([[\textit{Universal}&%s&%s\\]], fd, rd))
end
for i = 1, #lcouplers do
local cdesc = lcouplers[i]
local cid = couplerid[cdesc]
local fd, rd = "", ""
if flim then
fd = fcouplers[cid] and [[$\bullet$]] or ""
elseif not fcouplers then
fd = [[$\Uparrow$]]
end
if rlim then
rd = rcouplers[cid] and [[$\bullet$]] or ""
elseif not rcouplers then
rd = [[$\Uparrow$]]
end
table.insert(st, string.format([[%s&%s&%s\\]], cdesc, fd, rd))
end
table.insert(st, [[\bottomrule]])
table.insert(st, [[\end{tabularx}]])
end
local hasinv = prototype.has_inventory
local hasseats = prototype.max_seats>0
local taliquid = prototype.techage_liquid_capacity or 0
if hasinv or hasseats or taliquid>0 then
table.insert(st, [[\section{Wagon Capacity}]])
if hasseats then
table.insert(st, [[
\begin{tabularx}{\textwidth}{X c c}
\toprule
{\bfseries Seat Group} & {\bfseries Driver Stand} & {\bfseries Seat Count}\\\midrule
]])
for _, d in pairs(prototype.seat_groups) do
table.insert(st, string.format([[%s & %s & %d\\]], SL(d.name), d.driving_ctrl_access and [[$\bullet$]] or "", d.count))
end
table.insert(st, [[\bottomrule]])
table.insert(st, [[\end{tabularx}]])
end
if hasinv then
if next(prototype.inventory_list_sizes or {}) ~= nil then
table.insert(st, [[
\begin{tabularx}{\textwidth}{X c}
\toprule
{\bfseries Inventory Name} & {\bfseries Capacity}\\\midrule
]])
for k, v in pairs(prototype.inventory_list_sizes) do
table.insert(st, string.format([[\texttt{%s} & %d\\]], SL(k), v))
end
table.insert(st, [[\bottomrule]])
table.insert(st, [[\end{tabularx}]])
else
table.insert(st, [[This wagon has an inventory of unknown size.]])
end
end
if taliquid > 0 then
table.insert(st, string.format([[
\begin{tabularx}{\textwidth}{X l}
{Liquid Capacity (Techage)} & %d
\end{tabularx}
]], taliquid))
end
end
if prototype.set_livery then
if prototype.livery_definition then
table.insert(st, [[\section{Multi-Component Liveries}]])
local components = prototype.livery_definition.components
local presets = prototype.livery_definition.presets
table.insert(st, [[\subsection*{Components}]])
table.insert(st, [[\begin{itemize}]])
for _, c in ipairs(components) do
table.insert(st, string.format([[\item %s]], SL(c.description)))
end
table.insert(st, [[\end{itemize}]])
for _, p in ipairs(presets) do
table.insert(st, string.format([[\subsection*{Preset: %s}]], SL(p.description)))
table.insert(st, [[
\begin{tabularx}{\textwidth}{X c}
\toprule
{\bfseries Component} & {\bfseries Color} \\\midrule
]])
for _, c in ipairs(p.livery_stack.layers) do
local cdesc = SL(components[c.component].description)
table.insert(st, string.format([[%s & %s\\]], cdesc, latex_colordesc(c.color)))
end
table.insert(st, [[
\bottomrule
\end{tabularx}
]])
end
else
table.insert(st, [[\section{Livery System (Bike Painter)}]])
table.insert(st, [[This wagon can be painted by the bike painter.]])
end
end
local dlxlivdef = dlxtrains_livery_information(prototype)
if dlxlivdef then
table.insert(st, [[
\section{DlxTrains Livery Sytem}
This wagon can be customized with DlxTrains' livery system.
]])
end
local atlivdef = advtrains_livery_tools_information(itemname)
if atlivdef then
table.insert(st, [[\section{Advtrains Livery Tool (Marnack)}]])
for _, tname in ipairs(atlivdef.template_names) do
local tdef = atlivdef.templates[tname]
table.insert(st, string.format([[\subsection*{Template: %s}]], SL(tname)))
table.insert(st, SL(tdef.notes))
table.insert(st, "")
table.insert(st, "This template contains the following components:")
table.insert(st, [[\begin{itemize}]])
for _, overlay in ipairs(tdef.overlays) do
table.insert(st, string.format([[\item %s]], SL(overlay.name)))
end
table.insert(st, [[\end{itemize}]])
end
for _, lname in ipairs(atlivdef.livery_names) do
local ldef = atlivdef.liveries[lname]
local tname = ldef.livery_template_name
table.insert(st, string.format([[\subsection*{Preset: %s}]], SL(lname)))
table.insert(st, string.format([[Template: %s]], SL(tname)))
table.insert(st, "")
table.insert(st, [[
\begin{tabularx}{\textwidth}{X c}
\toprule
{\bfseries Component} & {\bfseries Color}\\\midrule]])
for _, overlay in pairs(ldef.overlays) do
local cname = atlivdef.templates[tname].overlays[overlay.id].name
table.insert(st, string.format([[%s & %s\\]], SL(cname), latex_colordesc(overlay.color)))
end
table.insert(st, [[
\bottomrule
\end{tabularx}
]])
end
end
table.insert(st, [[
\end{document}
]])
st = table.concat(st, "\n")
minetest.safe_file_write(filename, st)
end
function advtrains_doc_integration.write_all_wagons_as_latex()
for k in pairs(advtrains.wagon_prototypes) do
advtrains_doc_integration.write_wagon_info_as_latex(k)
end
end
minetest.register_chatcommand("atdoc_write", {
params = "",
description = S("Export Advtrains-related information"),
privs = {server = true},
func = function()
advtrains_doc_integration.write_all_wagons_as_latex()
end,
})
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "doc:entry" or fields.entry_body ~= "action:playhorn" then
return
end
local pname = player:get_player_name()
local cat, ent = doc.get_selection(pname)
if cat ~= "advtrains_wagons" or ent == nil then
return
end
local sound = (advtrains.wagon_prototypes[ent] or {}).horn_sound
if type(sound) == "table" then
sound = table.copy(sound)
else
sound = {name = sound}
end
if type(sound.name) ~= "string" or sound.name == "" then
return
end
minetest.sound_play(sound, {to_player = pname}, true)
end)