diff --git a/README.md b/README.md
index 59f3c8a..c760b55 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,15 @@ This mod adds the "Wagons" category that contains information related
to the wagons. It also adds adds certain Advtrains-related properties
to certain nodes.
+This mod also provides the `atdoc_write` command, which can be used to
+export wagon datasheets.
+
**Please keep in mind that, while this mod attempts to provide accurate
information, its accuracy, especially for information related to
liveries, is limited by various factors. Refer the source code of the
relevant mods for authoritative information.**
-## API
+## Documentation strings
Wagon mods can add a description to the `_doc_wagon_longdesc` field of
the wagon prototype; adding `advtrains_doc_integration` as an optional
@@ -26,30 +29,10 @@ dependency is _not_ needed.
## CI
-If you use [Mineunit][mineunit], you can copy (or add a symlink to)
-this directory into the fixtures directory of your mod and then using
-(for exmaple) the code below to generate wagon datasheets.
-
-```
-require "mineunit"
-mineunit "core"
-fixture "advtrains_doc_integration/ci/init"
-sourcefile "init"
-```
+This mod also be used with [Mineunit][mineunit] in addition to a
+regular Minetest environment, such as to generate wagon datasheets.
+However, please note that, due to various limitations, the datasheet
+generated in such an environment does not contain certain information
+that would otherwise be available from Minetest.
[mineunit]: https://github.com/S-S-X/mineunit
-
-If you do not use Mineunit, the following functions are available in
-the `advtrains_doc_integration` table:
-
-* `write_wagon_info_as_latex`, which exports the datasheet of a single
-type of wagon as LaTeX, and
-* `write_all_wagons_as_latex`, which exports all wagon datasheets as
-LaTeX.
-
-It is also possible to export wagon datasheets using the `atdoc_write`
-command in Minetest.
-
-The datasheets are exported to the world directory (or, for Mineunit,
-the fixtures directory). Note that the result of generating datasheets
-from Minetest and from CI may differ for various reasons.
diff --git a/config.ld b/config.ld
index 7e7ac05..4463d85 100644
--- a/config.ld
+++ b/config.ld
@@ -5,4 +5,12 @@ project = "Documentation System Integration for Advtrains"
title = "Manual for advtrains_doc_integration"
package = "advtrains_doc_integration"
readme = "README.md"
--- vim: syntax=lua
+
+custom_see_handler("^advtrains%.[%w%._]+$", function(name)
+ return name, "https://git.bananach.space/advtrains.git/tree/advtrains/api_doc.txt"
+end)
+custom_see_handler("^SimpleSoundSpec$", function()
+ return "SimpleSoundSpec", "https://api.minetest.net/sounds/#simplesoundspec"
+end)
+
+-- vim: ft=lua
diff --git a/describe.lua b/describe.lua
index ec8be95..c38a16e 100644
--- a/describe.lua
+++ b/describe.lua
@@ -7,7 +7,7 @@ local utils = advtrains_doc_integration.utils
local D = {}
--- Describe a conns object
--- @tparam integer|table conns The conns object to describe.
+-- @tparam advtrains.conns|{advtrains.conns,number,...} conns The conns object to describe.
-- @treturn string|nil The description of the conns object.
function D.conns(conns)
local connsdesc = {[0] = "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}
diff --git a/doc_ui.lua b/doc_ui.lua
new file mode 100644
index 0000000..23e76ab
--- /dev/null
+++ b/doc_ui.lua
@@ -0,0 +1,324 @@
+local S = minetest.get_translator "advtrains_doc_integration"
+local D = advtrains_doc_integration.describe
+local H = advtrains_doc_integration.hypertext
+local utils = advtrains_doc_integration.utils
+local fsescape = minetest.formspec_escape
+
+local function S2(a, b)
+ return S(a, S(b))
+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, H.listitem(k, true))
+ end
+ elseif fallback1 then
+ table.insert(lst, fallback1)
+ end
+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 wlivprev = {}
+
+local function get_livery_preview_selection(pname, itemname)
+ return (wlivprev[pname] or {})[itemname] or 0
+end
+
+local function get_livery_preview(itemname, id)
+ local tx = (advtrains_doc_integration.prototypes[itemname] or {}).livery_textures
+ return tx[id] or tx[0]
+end
+
+local function render_livery_textures(pname, itemname)
+ local str = table.concat(utils.map(get_livery_preview(itemname, get_livery_preview_selection(pname, itemname)), fsescape), ",")
+ return str
+end
+
+local function set_livery_preview_selection(pname, itemname, id)
+ local t = wlivprev[pname]
+ if not t then
+ t = {}
+ wlivprev[pname] = t
+ end
+ t[itemname] = id
+end
+
+local function doc_render_wagon_information(prototype, pname)
+ local desctext = {}
+ if prototype._doc_wagon_longdesc then
+ table.insert(desctext, tostring(prototype._doc_wagon_longdesc))
+ blankline(desctext)
+ end
+ table.insert(desctext, H.header(S("Basic Information")))
+ table.insert(desctext, S("Itemstring: @1", H.mono(prototype.name)))
+ 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, H.mono)
+ addlist(desctext, prototype.coupler_types_front, S("Compatible front couplers:"), S2("Front coupler: @1", "Absent"), S2("Front coupler: @1", "Universal"), utils.get_coupler_name)
+ addlist(desctext, prototype.coupler_types_back, S("Compatible rear couplers:"), S2("Rear coupler: @1", "Absent"), S2("Rear coupler: @1", "Universal"), utils.get_couple_name)
+ table.insert(desctext, S("Wagon span: @1", prototype.wagon_span and D.length(2*prototype.wagon_span) or S("Undefined")))
+ table.insert(desctext, S("Maximum speed: @1", prototype.max_speed and D.speed(prototype.max_speed) or S("Undefined")))
+ table.insert(desctext, S2("Motive power: @1", prototype.is_locomotive and "Present" or "Absent"))
+ table.insert(desctext, S("Horn sound: @1", H.describe_sound("playhorn", prototype.horn_sound)))
+ if prototype.doors.open.sound or prototype.doors.close.sound then
+ table.insert(desctext, S("Door sound: @1 (when opening), @2 (when closing)",
+ H.describe_sound("playdooropen", prototype.doors.open.sound),
+ H.describe_sound("playdoorclose", prototype.doors.close.sound)))
+ else
+ table.insert(desctext, S2("Door sound: @1", "Undefined"))
+ end
+
+ blankline(desctext)
+ table.insert(desctext, H.header(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", H.mono(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, H.header(S("Wagon Appearance")))
+ table.insert(desctext, S("Mesh: @1", prototype.mesh and H.mono(prototype.mesh) or "None"))
+ addlist(desctext, prototype.textures, S("Textures:"), S("No textures"), false, function(_, v) return H.mono(v) end)
+
+ local livsel = get_livery_preview_selection(pname, prototype.name)
+ local livrst = (livsel ~= 0 and not prototype.dlxtrains_livery) and " " .. H.action("preview_0", S("[Reset Preview]")) or ""
+ local livids = 0
+ local function livprev(desc)
+ livids = livids+1
+ local label = H.plain(desc)
+ if livids == livsel then
+ label = H.bold(desc)
+ end
+ return H.action(string.format("preview_%d", livids), label, true)
+ end
+
+ local dlxlivdef = prototype.dlxtrains_livery
+ table.insert(desctext, S2("DlxTrains livery system: @1", dlxlivdef and "Supported" or "Unsupported"))
+ if dlxlivdef then
+ livids = -1
+ table.insert(desctext, "Livery presets:")
+ for k = 0, dlxlivdef.count-1 do
+ table.insert(desctext, H.listitem(string.format("%s (%s, %s)", H.mono(dlxlivdef[k].code), livprev(S("default")), livprev(S("weathered"))), true))
+ end
+ 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 H.plain(v.description) end)
+ addlist(bikelivdesc, prototype.livery_definition.presets, S("Livery presets:") .. livrst, nil, nil, function(_, v) return livprev(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 atlivdef = prototype.advtrains_livery_tools
+ 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 H.plain(v) end)
+ addlist(desctext, atlivdef.livery_names, S("Livery presets:") .. livrst, nil, nil, function(_, v) return livprev(v) end)
+ end
+
+ blankline(desctext)
+ table.insert(desctext, H.header(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 = render_livery_textures(pname, prototype.name)
+ 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(prototype.name)),
+ string.format("model[%f,%f;%f,%f;%s;%s;%s;%f,%f]",
+ x1-mside, y1-mside, mside, mside, "wagon_model", mesh, textures, -30, 135),
+ }
+ return table.concat(fstext, "\n")
+end
+
+if doc then
+ advtrains_doc_integration.register_on_prototype_loaded(function(itemname, prototype)
+ minetest.override_item(itemname, {_doc_items_create_entry = false})
+ doc.add_entry("advtrains_wagons", itemname, {
+ name = ItemStack(itemname):get_short_description(),
+ data = prototype,
+ })
+ if doc.sub.identifier then
+ doc.sub.identifier.register_object(itemname, "advtrains_wagons", itemname)
+ end
+ end)
+
+ if doc.sub.items then
+ local register_factoid = doc.sub.items.register_factoid
+ local function ndef_field_factoid(cat, ftype, f, ...)
+ local ftp = type(f)
+ if ftp == "string" then
+ local desc = f
+ f = function(x)
+ if x ~= nil then
+ return desc
+ end
+ end
+ end
+ local keys = {...}
+ local idx = function(t)
+ for _, k in ipairs(keys) do
+ if type(t) ~= "table" then
+ return nil
+ end
+ t = t[k]
+ end
+ return t
+ end
+ local function fgen(_, def)
+ return f(idx(def)) or ""
+ end
+ return register_factoid(cat, ftype, fgen)
+ end
+ local function group_factoid(cat, gr, f)
+ local function func(x)
+ return f(x or 0)
+ end
+ return ndef_field_factoid(cat, "groups", func, "groups", gr)
+ 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
+ for fname, t in pairs {
+ advtrains = {
+ on_train_enter = S("This track reacts to passing trains."),
+ on_train_approach = S("This track reacts to approaching trains."),
+ },
+ luaautomation = {
+ fire_event = S("This block handles LuaATC events."),
+ },
+ } do
+ for subfname, val in pairs(t) do
+ local function f(x)
+ if x ~= nil then
+ return val
+ end
+ end
+ ndef_field_factoid("nodes", "groups", f, fname, subfname)
+ end
+ end
+ register_factoid("nodes", "groups", function(_, ndef)
+ if ndef.advtrains then
+ local atdef = ndef.advtrains
+ if atdef.set_aspect then
+ return S("This is a signal with a variable aspect.")
+ elseif atdef.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", D.conns(ndef.at_conns) or "?")
+ end
+ return ""
+ end)
+ end
+
+ doc.add_category("advtrains_wagons", {
+ name = S("Wagons"),
+ build_formspec = doc_render_wagon_information,
+ })
+end
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ if formname ~= "doc:entry" 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 act = fields.entry_body
+ local prototype = advtrains_doc_integration.prototypes[ent]
+ local sounds = {
+ ["action:playhorn"] = prototype.horn_sound,
+ ["action:playdooropen"] = prototype.doors.open.sound,
+ ["action:playdoorclose"] = prototype.doors.close.sound,
+ }
+ if not act then
+ return
+ elseif sounds[act] then
+ minetest.sound_play(sounds[act], {to_player = pname}, true)
+ else
+ local txid = string.match(act, [[^action:preview_(%d+)$]])
+ txid = tonumber(txid)
+ if txid then
+ set_livery_preview_selection(pname, ent, txid)
+ doc.show_entry(pname, cat, ent)
+ end
+ end
+end)
diff --git a/hypertext.lua b/hypertext.lua
new file mode 100644
index 0000000..83db3a0
--- /dev/null
+++ b/hypertext.lua
@@ -0,0 +1,92 @@
+--- Utilities for hypertext used in Minetest formspecs.
+-- @module advtrains_doc_integration.hypertext
+-- @alias H
+local H = {}
+local S = minetest.get_translator("advtrains_doc_integration")
+
+--- Create a plain text hypertext element.
+-- @tparam string str The content of the element.
+-- @tparam[opt] boolean noescape Whether to avoid escaping the content.
+-- @treturn string The hypertext element containing the text.
+function H.plain(str, noescape)
+ str = tostring(str)
+ if noescape then
+ return str
+ end
+ return (string.gsub(str, "([<>])", [[\%1]]))
+end
+
+local function addtag(tag, attrs, str, noescape)
+ local attrlist = {}
+ for k, v in pairs(attrs) do
+ table.insert(attrlist, (" %s=%s"):format(k, v))
+ end
+ return ("<%s%s>%s%s>"):format(tag, table.concat(attrlist), H.plain(str, noescape), tag)
+end
+
+--- Create a hypertext element with monospace text.
+-- @tparam string str The content of the element.
+-- @tparam[opt] boolean noescape Whether to avoid escaping the content.
+-- @treturn string The hypertext element containing the text.
+function H.mono(str, noescape)
+ return addtag("mono", {}, str, noescape)
+end
+
+--- Create a hypertext element with bold text.
+-- @tparam string str The content of the element.
+-- @tparam[opt] boolean noescape Whether to avoid escaping the content.
+-- @treturn string The hypertext element containing the text.
+function H.bold(str, noescape)
+ return addtag("b", {}, str, noescape)
+end
+
+--- Create a hypertext element with italic text.
+-- @tparam string str The content of the element.
+-- @tparam[opt] boolean noescape Whether to avoid escaping the content.
+-- @treturn string The hypertext element containing the text.
+function H.italic(str, noescape)
+ return addtag("i", {}, str, noescape)
+end
+
+--- Create a hypertext element with text formatted as header.
+-- @tparam string str The content of the element.
+-- @tparam[opt] boolean noescape Whether to avoid escaping the content.
+-- @treturn string The hypertext element containing the text.
+function H.header(str, noescape)
+ return H.bold(str, noescape)
+end
+
+--- Create a hypertext element with text formatted as an item in a list.
+-- @tparam string str The content of the element.
+-- @tparam[opt] boolean noescape Whether to avoid escaping the content.
+-- @treturn string The hypertext element containing the text.
+function H.listitem(str, noescape)
+ return "• " .. H.plain(str, noescape)
+end
+
+--- Create a hypertext element with an action.
+-- @tparam string action The name of the action.
+-- @tparam string str The content of the element.
+-- @tparam[opt] boolean noescape Whether to avoid escaping the content.
+-- @treturn string The hypertext element containing the text.
+function H.action(action, str, noescape)
+ return addtag("action", {name = action},
+ addtag("style", {color = "cyan"}, str, noescape), true)
+end
+
+--- Describe a soundspec.
+-- @tparam[opt] string action The name of the action.
+-- @tparam SimpleSoundSpec soundspec The soundspec to describe.
+-- @treturn string The hypertext element describing the soundspec.
+function H.describe_sound(action, soundspec)
+ if not soundspec then
+ return S("Undefined")
+ end
+ local sounddesc = H.mono(soundspec.name)
+ if action then
+ sounddesc = H.action(action, sounddesc, true)
+ end
+ return sounddesc
+end
+
+return H
diff --git a/init.lua b/init.lua
index 7acd4d4..73ffc3d 100644
--- a/init.lua
+++ b/init.lua
@@ -1,147 +1,16 @@
--- Entry point.
-- @module advtrains_doc_integration
-local worldpath = minetest.get_worldpath() .. "/"
+local worldpath = minetest.get_worldpath()
local modpath = minetest.get_modpath("advtrains_doc_integration")
local S = minetest.get_translator("advtrains_doc_integration")
-local fsescape = minetest.formspec_escape
advtrains_doc_integration = {}
-for _, m in ipairs{"mathutils", "describe", "utils"} do
+for _, m in ipairs{
+ "mathutils", "utils", "describe", "hypertext", "latex",
+} do
advtrains_doc_integration[m] = dofile(("%s/%s.lua"):format(modpath, m))
end
-local D = advtrains_doc_integration.describe
-
-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 txescape(str)
- return (string.gsub(tostring(str), "[:^\\]", [[\%1]]))
-end
-
-local function htescape(str)
- return (string.gsub(tostring(str), "([<>])", [[\%1]])) -- clip to one result
-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 spairs(tbl, sort)
- local keys = {}
- local kn = {}
- for k in pairs(tbl or {}) do
- table.insert(keys, k)
- end
- table.sort(keys, sort)
- for i = 2, #keys do
- kn[keys[i-1]] = keys[i]
- end
- return function(t, n)
- local k = kn[n]
- if n == nil then
- k = keys[1]
- end
- return k, t[k]
- end, tbl, nil
-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("%s", htescape(str))
-end
-
-local function htbold(str)
- return string.format("%s", htescape(str))
-end
-
-local function htheader(str)
- return htbold(str)
-end
-
-local function htitem(str)
- return "• " .. str
-end
-
-local function htaction(action, str, noesc)
- if not noesc then
- str = htescape(str)
- end
- return string.format("", action, str)
-end
-
-local function htsound(action, soundspec)
- if not soundspec then
- return S("Undefined")
- end
- local sounddesc = htmono(soundspec.name)
- if action then
- sounddesc = htaction(action, sounddesc, true)
- end
- return sounddesc
-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, htitem(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 utils = advtrains_doc_integration.utils
local function dlxtrains_livery_information(prototype)
if not dlxtrains then
@@ -183,16 +52,16 @@ do -- helper for Marnack's Advtrains livery tools
return nil
end
local odef = template.overlays
- local textures = map(template.base_textures, function(str) return {str} end)
- for _, overlay in spairs(design.overlays) do
+ local textures = utils.map(template.base_textures, function(str) return {str} end)
+ for _, overlay in utils.spairs(design.overlays) do
local o = odef[overlay.id]
local t = textures[(o or {}).slot_idx]
if t and o then
local alpha = math.min(255, math.max(0, o.alpha or 255))
- table.insert(t, string.format("(%s^[colorize:%s:%d)", txescape(o.texture), txescape(overlay.color), alpha))
+ table.insert(t, string.format("(%s^[colorize:%s:%d)", utils.texture_escape(o.texture), utils.texture_escape(overlay.color), alpha))
end
end
- return map(textures, function(st) return table.concat(st, "^") end)
+ return utils.map(textures, function(st) return table.concat(st, "^") end)
end
local mt = {
__index = atliv,
@@ -229,31 +98,9 @@ do -- helper for Marnack's Advtrains livery tools
end
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 prototype_cache = {}
advtrains_doc_integration.prototypes = prototype_cache -- ONLY FOR DEBUGGING
-local function adjust_soundspec(spec)
- if type(spec) == "string" then
- spec = {name = spec}
- end
- if type(spec) == "table" and spec.name and spec.name ~= "" then
- return spec
- end
- return nil
-end
-
local function adjust_wagon_prototype(itemname, prototype)
local p = prototype_cache[itemname]
if p then
@@ -264,7 +111,7 @@ local function adjust_wagon_prototype(itemname, prototype)
if p._doc_wagon_longdesc then
p.longdesc = p._long_wagon_longdesc
end
- p.horn_sound = adjust_soundspec(p.horn_sound)
+ p.horn_sound = utils.adjust_soundspec(p.horn_sound)
local pax, driver = 0, 0
if p.seats and p.seat_groups then
for _, v in pairs(p.seats) do
@@ -284,7 +131,7 @@ local function adjust_wagon_prototype(itemname, prototype)
for _, state in pairs {"open", "close"} do
local st = (prototype.doors or {})[state] or {}
local state_data = {
- sound = adjust_soundspec(st.sound),
+ sound = utils.adjust_soundspec(st.sound),
}
p.doors[state] = state_data
end
@@ -316,7 +163,7 @@ local function adjust_wagon_prototype(itemname, prototype)
local tx = table.copy(txbase)
local st = {basefile}
for _, l in ipairs(pdef.livery_stack.layers) do
- table.insert(st, string.format("(%s^[colorize:%s)", txescape(components[l.component].texture_file), txescape(l.color)))
+ table.insert(st, string.format("(%s^[colorize:%s)", utils.texture_escape(components[l.component].texture_file), utils.texture_escape(l.color)))
end
tx[slot] = table.concat(st, "^")
table.insert(p.livery_textures, tx)
@@ -333,545 +180,39 @@ local function adjust_wagon_prototype(itemname, prototype)
return p
end
-local function doc_register_wagon(itemname)
- local prototype = adjust_wagon_prototype(itemname, advtrains.wagon_prototypes[itemname])
- prototype.name = itemname
- minetest.override_item(itemname, {_doc_items_create_entry = false})
- doc.add_entry("advtrains_wagons", itemname, {
- name = ItemStack(itemname):get_short_description(),
- data = prototype,
- })
- if doc.sub.identifier then
- doc.sub.identifier.register_object(itemname, "advtrains_wagons", itemname)
- end
+local registered_on_prototype_loaded = {}
+
+--- Register a callback that will be run when a prototype becomes available.
+-- @tparam function callback The callback to run when the prototype
+-- becomes available. The callback function is passed the itemname and the
+-- prototype of the wagon.
+function advtrains_doc_integration.register_on_prototype_loaded(callback)
+ table.insert(registered_on_prototype_loaded, callback)
end
-local wlivprev = {}
-
-local function get_livery_preview_selection(pname, itemname)
- return (wlivprev[pname] or {})[itemname] or 0
-end
-
-local function get_livery_preview(itemname, id)
- local tx = (prototype_cache[itemname] or {}).livery_textures
- return tx[id] or tx[0]
-end
-
-local function render_livery_textures(pname, itemname)
- local str = table.concat(map(get_livery_preview(itemname, get_livery_preview_selection(pname, itemname)), fsescape), ",")
- return str
-end
-
-local function set_livery_preview_selection(pname, itemname, id)
- local t = wlivprev[pname]
- if not t then
- t = {}
- wlivprev[pname] = t
- end
- t[itemname] = id
-end
-
-local function doc_render_wagon_information(prototype, pname)
- 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(prototype.name)))
- 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 D.length(2*prototype.wagon_span) or S("Undefined")))
- table.insert(desctext, S("Maximum speed: @1", prototype.max_speed and D.speed(prototype.max_speed) or S("Undefined")))
- table.insert(desctext, S2("Motive power: @1", prototype.is_locomotive and "Present" or "Absent"))
- table.insert(desctext, S("Horn sound: @1", htsound("playhorn", prototype.horn_sound)))
- if prototype.doors.open.sound or prototype.doors.close.sound then
- table.insert(desctext, S("Door sound: @1 (when opening), @2 (when closing)",
- htsound("playdooropen", prototype.doors.open.sound),
- htsound("playdoorclose", prototype.doors.close.sound)))
- else
- table.insert(desctext, S2("Door sound: @1", "Undefined"))
- end
-
- 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 livsel = get_livery_preview_selection(pname, prototype.name)
- local livrst = (livsel ~= 0 and not prototype.dlxtrains_livery) and " " .. htaction("preview_0", S("[Reset Preview]")) or ""
- local livids = 0
- local function livprev(desc)
- livids = livids+1
- local label = htescape(desc)
- if livids == livsel then
- label = htbold(desc)
- end
- return htaction(string.format("preview_%d", livids), label, true)
- end
-
- local dlxlivdef = prototype.dlxtrains_livery
- table.insert(desctext, S2("DlxTrains livery system: @1", dlxlivdef and "Supported" or "Unsupported"))
- if dlxlivdef then
- livids = -1
- table.insert(desctext, "Livery presets:")
- for k = 0, dlxlivdef.count-1 do
- table.insert(desctext, htitem(string.format("%s (%s, %s)", htmono(dlxlivdef[k].code), livprev(S("default")), livprev(S("weathered")))))
+minetest.register_on_mods_loaded(function()
+ for itemname, prototype in pairs(advtrains.wagon_prototypes) do
+ prototype = adjust_wagon_prototype(itemname, prototype)
+ prototype.name = itemname
+ for _, cb in ipairs(registered_on_prototype_loaded) do
+ cb(itemname, prototype)
end
end
+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:") .. livrst, nil, nil, function(_, v) return livprev(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 atlivdef = prototype.advtrains_livery_tools
- 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:") .. livrst, nil, nil, function(_, v) return livprev(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 = render_livery_textures(pname, prototype.name)
- 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(prototype.name)),
- string.format("model[%f,%f;%f,%f;%s;%s;%s;%f,%f]",
- x1-mside, y1-mside, mside, mside, "wagon_model", mesh, textures, -30, 135),
- }
- return table.concat(fstext, "\n")
-end
-
-if doc then
- minetest.register_on_mods_loaded(function()
- for k in pairs(advtrains.wagon_prototypes) do
- doc_register_wagon(k)
- end
- end)
-
- if doc.sub.items then
- local register_factoid = doc.sub.items.register_factoid
- local function ndef_field_factoid(cat, ftype, f, ...)
- local ftp = type(f)
- if ftp == "string" then
- local desc = f
- f = function(x)
- if x ~= nil then
- return desc
- end
- end
- end
- local keys = {...}
- local idx = function(t)
- for _, k in ipairs(keys) do
- if type(t) ~= "table" then
- return nil
- end
- t = t[k]
- end
- return t
- end
- local function fgen(_, def)
- return f(idx(def)) or ""
- end
- return register_factoid(cat, ftype, fgen)
- end
- local function group_factoid(cat, gr, f)
- local function func(x)
- return f(x or 0)
- end
- return ndef_field_factoid(cat, "groups", func, "groups", gr)
- 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
- for fname, t in pairs {
- advtrains = {
- on_train_enter = S("This track reacts to passing trains."),
- on_train_approach = S("This track reacts to approaching trains."),
- },
- luaautomation = {
- fire_event = S("This block handles LuaATC events."),
- },
- } do
- for subfname, val in pairs(t) do
- local function f(x)
- if x ~= nil then
- return val
- end
- end
- ndef_field_factoid("nodes", "groups", f, fname, subfname)
- end
- end
- register_factoid("nodes", "groups", function(_, ndef)
- if ndef.advtrains then
- local atdef = ndef.advtrains
- if atdef.set_aspect then
- return S("This is a signal with a variable aspect.")
- elseif atdef.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", D.conns(ndef.at_conns) or "?")
- end
- return ""
- end)
- end
-
- doc.add_category("advtrains_wagons", {
- name = S("Wagons"),
- build_formspec = doc_render_wagon_information,
- })
-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
-
+--- Write a wagon datasheet to a LaTeX file in the world path.
+-- @tparam string itemname The item name of the wagon prototype.
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(itemname, 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")
+ local filename = string.format("%s/atdoc_wagon_%s.tex", worldpath, itemname:gsub(":", "_"))
+ local description = advtrains_doc_integration.latex.describe_wagon_prototype(itemname)
+ if description then
+ minetest.safe_file_write(filename, description)
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
+--- Write a wagon datasheet for each wagon using @{write_wagon_info_as_latex}.
function advtrains_doc_integration.write_all_wagons_as_latex()
- for k in pairs(advtrains.wagon_prototypes) do
+ for k in pairs(prototype_cache) do
advtrains_doc_integration.write_wagon_info_as_latex(k)
end
end
@@ -885,50 +226,7 @@ minetest.register_chatcommand("atdoc_write", {
end,
})
-minetest.register_on_player_receive_fields(function(player, formname, fields)
- if formname ~= "doc:entry" 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 act = fields.entry_body
- local prototype = prototype_cache[ent]
- local sounds = {
- ["action:playhorn"] = prototype.horn_sound,
- ["action:playdooropen"] = prototype.doors.open.sound,
- ["action:playdoorclose"] = prototype.doors.close.sound,
- }
- if not act then
- return
- elseif sounds[act] then
- minetest.sound_play(sounds[act], {to_player = pname}, true)
- else
- local txid = string.match(act, [[^action:preview_(%d+)$]])
- txid = tonumber(txid)
- if txid then
- set_livery_preview_selection(pname, ent, txid)
- doc.show_entry(pname, cat, ent)
- end
- end
-end)
-
+dofile(modpath .. "/doc_ui.lua")
if minetest.get_modpath("mtt") then
- if doc then
- mtt.register("wagon information formspec", function(cb)
- local player = mtt.join_player("singleplayer")
- for k in pairs(prototype_cache) do
- print("Found wagon prototype: " .. k)
- doc.show_entry("singleplayer", "advtrains_wagons", k, true)
- end
- player:leave()
- cb()
- end)
- end
- mtt.register("wagon datasheet generation", function(cb)
- advtrains_doc_integration.write_all_wagons_as_latex()
- cb()
- end)
+ dofile(modpath .. "/mtt.lua")
end
diff --git a/latex.lua b/latex.lua
new file mode 100644
index 0000000..4e1521f
--- /dev/null
+++ b/latex.lua
@@ -0,0 +1,316 @@
+--- Code related to LaTeX generation.
+-- @module advtrains_doc_integration.latex
+local latex = {}
+local utils = advtrains_doc_integration.utils
+
+--- Escape string in LaTeX.
+-- @tparam string str The string to escape.
+-- @treturn string The escaped string.
+function latex.escape(str)
+ return (string.gsub(str, ".", {
+ ["&"] = [[\&]],
+ ["%"] = [[\%]],
+ ["$"] = [[\$]],
+ ["#"] = [[\#]],
+ ["_"] = [[\_]],
+ ["{"] = [[\{]],
+ ["}"] = [[\}]],
+ ["~"] = [[\textasciitilde]],
+ ["^"] = [[\textasciicircum]],
+ ["\\"] = [[\textbackslash]],
+ }))
+end
+
+--- Escape a translated string in LaTeX.
+-- @tparam string lang The language to use for server-side translation
+-- @tparam string str The string to escape.
+-- @treturn The escaped string.
+function latex.escape_translated(lang, str)
+ return latex.escape(minetest.get_translated_string(lang, str))
+end
+
+local function SL(str)
+ return latex.escape_translated("en", str)
+end
+
+--- Describe a color in LaTeX.
+-- @tparam string cstr The color string.
+-- @treturn string The string describing the color.
+function latex.describe_color(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
+
+--- Describe a wagon prototype in LaTeX.
+-- @tparam string itemname The item name of the wagon prototype.
+-- @treturn string The description of the prototype.
+function latex.describe_wagon_prototype(itemname)
+ local prototype = advtrains_doc_integration.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(utils.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.describe_color(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 = prototype.dlxtrains_livery
+ if dlxlivdef then
+ table.insert(st, [[
+\section{DlxTrains Livery Sytem}
+This wagon can be customized with DlxTrains' livery system.
+]])
+ end
+
+ local atlivdef = prototype.advtrains_livery_tools
+ 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.describe_color(overlay.color)))
+ end
+ table.insert(st, [[
+\bottomrule
+\end{tabularx}
+]])
+ end
+ end
+
+ table.insert(st, [[
+\end{document}
+]])
+ return table.concat(st, "\n")
+end
+
+return latex
diff --git a/mathutils.lua b/mathutils.lua
index c7377ff..ee4658a 100644
--- a/mathutils.lua
+++ b/mathutils.lua
@@ -15,6 +15,8 @@ local function gcdmain(x, y, ...)
end
--- Compute the greatest common divider.
+-- Note that the behavior of this function is undefined if the arguments
+-- include infinity or NaN.
-- @tparam integer ... The numbers used for the computation.
-- @treturn integer The GCD of the given numbers.
function M.gcd(...)
diff --git a/mtt.lua b/mtt.lua
new file mode 100644
index 0000000..035a7de
--- /dev/null
+++ b/mtt.lua
@@ -0,0 +1,15 @@
+if doc then
+ mtt.register("wagon information formspec", function(cb)
+ local player = mtt.join_player("singleplayer")
+ for k in pairs(advtrains_doc_integration.prototypes) do
+ print("Found wagon prototype: " .. k)
+ doc.show_entry("singleplayer", "advtrains_wagons", k, true)
+ end
+ player:leave()
+ cb()
+ end)
+end
+mtt.register("wagon datasheet generation", function(cb)
+ advtrains_doc_integration.write_all_wagons_as_latex()
+ cb()
+end)
diff --git a/utils.lua b/utils.lua
index 077148d..629c162 100644
--- a/utils.lua
+++ b/utils.lua
@@ -1,13 +1,13 @@
--- Utility functions.
-- @module advtrains_doc_integration.utils
--- @alias M
-local M = {}
+-- @alias utils
+local utils = {}
--- Create a table by applying a function to each element.
-- @tparam table tbl The table to map from.
-- @tparam function func The function to apply.
-- @treturn table The resulting table.
-function M.map(tbl, func)
+function utils.map(tbl, func)
local t = {}
for k, v in pairs(tbl or {}) do
t[k] = func(v)
@@ -15,4 +15,58 @@ function M.map(tbl, func)
return t
end
-return M
+--- Create an iterator that iterates through the table in the order of
+-- the keys sorted in a certain order.
+-- Note that the behavior is undefined if a key is added during the iteration.
+-- @tparam table tbl The table to iterate
+-- @tparam[opt] function sort The function passed to @{table.sort} for
+-- sorting the keys. The default sorting order is used if the function
+-- is not provided.
+-- @return An iterator suitable for use with Lua's `for` loop.
+function utils.spairs(tbl, sort)
+ local keys = {}
+ local kn = {}
+ for k in pairs(tbl or {}) do
+ table.insert(keys, k)
+ end
+ table.sort(keys, sort)
+ for i = 2, #keys do
+ kn[keys[i-1]] = keys[i]
+ end
+ return function(t, n)
+ local k = kn[n]
+ if n == nil then
+ k = keys[1]
+ end
+ return k, t[k]
+ end, tbl, nil
+end
+
+--- Gets the name of the coupler
+-- @tparam string str The technical name of the coupler
+-- @treturn string The name of the coupler
+function utils.get_coupler_name(str)
+ return advtrains.coupler_types[str] or str
+end
+
+--- Adjust the soundspec to table form.
+-- @tparam SimpleSoundSpec spec The soundspec to adjust.
+-- @treturn SimpleSoundSpec The adjusted soundspec.
+function utils.adjust_soundspec(spec)
+ if type(spec) == "string" then
+ spec = {name = spec}
+ end
+ if type(spec) == "table" and spec.name and spec.name ~= "" then
+ return spec
+ end
+ return nil
+end
+
+--- Escape the texture string.
+-- @tparam string str The texture string to escape.
+-- @treturn string The escaped texture string.
+function utils.texture_escape(str)
+ return (string.gsub(tostring(str), "[:^\\]", [[\%1]]))
+end
+
+return utils