diff --git a/README.md b/README.md index 09a04f1..fc750ff 100755 --- a/README.md +++ b/README.md @@ -24,7 +24,53 @@ Die Einbindung vom Anlagenscript aus erfolgt mittels `require("kskit")`. Findet EEP die Dateien von Lua nicht, wird im Ereignisfenster eine Liste von Orten ausgegeben, an denen die Dateien gesucht wurden. Die Orte sind in diesem Fall mit dem Installationsort abzugleichen. -## Allgemeine Lua-Kniffe +## Allgemeine Hauptsignale + +Damit KsKit ein Modell als Hauptsignal ansteuern kann, muss das Signalmodell über eine funktionierende Haltstellung verfügen. +Trotz "Ks" im Namen ist KsKit in der Lage, Signalmodelle unabhängig vom Signalsystem anzusteuern. + +Um ein Signal ohne besondere Funktion in KsKit einzutragen, wird die Funktion `Basissignal` verwendet: + +``` +Basissignal(39, 1) +``` + +Der erste Parameter ist die ID-Nummer des Signals, der zweite Parameter ist die Haltstellung. +Als Haltstellung verwenden die meisten Signalmodelle die 1, es gibt allerdings Ausnahmen. + +Das Aufrufen dieser Funktion legt die notwendigen Callbacks und Funktionen für das Signal an. +Daher darf die Funktion nur einmal während der Initialisierung des Lua-Scriptes aufgerufen werden, nicht aus der EEPMain, MainFunktionen oder vergleichbaren heraus. + +![Basissignal mit Anmeldekontakt (gelb) und Signalzugschlussstelle (rot)](img/basissignal.png) + +### Signalzugschlussstalle + +Für jedes Hauptsignal muss eine Signalzugschlussstelle existieren. +Als Signalzugschlussstelle dient ein Signalkontakt (rot), welcher nach der Überfahrt eines Signals dieses wieder auf den Haltbegriff zurücksetzt. +Dieser Kontaktpunkt befindet nach dem Signal am Ende des Durchrutschweges. + +Die Bezeichnung als Zugschlussstelle ist historisch gewachsen, in Epoche V und VI löst die erste Achse des Zuges durch Befahren eines Gleisfreimeldeabschnittes die Rotschaltung aus. +Das Einstellen auf 'Zugschluss' ist daher nicht notwendig. + +An dem Signalkontakt muss keine Lua-Funktion eingetragen werden. +Auch die Wirkrichtung muss nicht beschränkt werden, dies bietet sich aber an, damit man über das Kontakt-Symbol leichter erkennen kann, in welcher Richtung das dazugehörige Signal steht. + +### Anmelde-Kontakt + +Optional zu einem jeden Hauptsignal kann ein Anmeldekontakt verwendet werden. +Ein Anmeldekontakt kann ein Kontakt jeglicher Art sein, an dem die Lua-Funktion "Anmeldung_X" eingetragen wird. +Anstelle "X" wird die ID-Nummer des folgenden Signales eingetragen. + +EEP überprüft bei dem Eintragen der Lua-Funktion, ob diese auch existiert, daher kann dieser Kontakt erst korrekt eingerichtet werden, nachdem EEP die `Basissignal`-Funktion ausgeführt hat, z.B. durch Wechsel in den 3D-Modus. + +Der Anmelde-Kontakt sollte eingerichtet werden, wenn das vorherige Signal keine automatische Anmeldung durch Weiterschaltung des Zugnamens durchgeführt hat. +Dies trifft auch zu, wenn es kein vorheriges Signal gibt, weil es sich um eine Aufgleisstrecke, eine Ausfahrt eines virtuelles Depots oder um einen ungesicherten Anlagenbereich handelt. + +Wird keine Anmeldung durchgeführt und fährt ein Zug unangemeldet an einem logischen Vorsignal vorbei, wird die Anmeldung am jeweiligen Hauptsignal nachgeholt. + +Wer sich nicht daran stört, das Signale erst nach Überfahrt des Vorsignales auf Fahrt schalten, kann auf Anmeldungen generell verzichten. + +## Lua-Kniffe Bei der Entwicklung des Basisscriptes haben sich einige Mechanismen als sehr praktisch erwiesen, um die Komplexität des Codes im Zaum zu halten. diff --git a/grapho b/grapho index 5196199..733ce1b 160000 --- a/grapho +++ b/grapho @@ -1 +1 @@ -Subproject commit 5196199acba962cc6c47f08b8b86428ae88e38d1 +Subproject commit 733ce1b119ed12fc2c755b3e2647c6bcbd4efff4 diff --git a/img/basissignal.png b/img/basissignal.png new file mode 100644 index 0000000..c0826d5 Binary files /dev/null and b/img/basissignal.png differ diff --git a/init.lua b/init.lua index f90598a..146f389 100755 --- a/init.lua +++ b/init.lua @@ -2,6 +2,9 @@ -- Ich weiss jetzt noch nicht, wie viele verschiedene Versionen von dem Script spaeter herumfliegen werden. KsKitVer=0 +-- Uebrige Konstanten +Vmax=400 + -- Tabelle, in der alle Lambda-Funktionen gespeichert werden -- Die Eintragung erfolgt durch die MainFunktion() vom Zielort aus. MainFunktionTabelle={} @@ -37,3 +40,120 @@ function SignalFunktion(Signal, Funktion) -- In unsere eigene Tabelle eintragen table.insert(SignalFunktionen[Signal], Funktion) end + +-- Lua-Serializer und Deserializer laden +serpent=require("kskit/serpent") + +-- Lua-Tabelle aus EEP Slot laden +function ladeTabelle(Slot) + local roh, geparst + ok, roh = EEPLoadData(Slot) + if not ok then return {} end + ok, geparst = serpent.load(roh) + if not ok then return {} end + if type(geparst) ~= "table" then return {} end + return geparst +end + +-- Lua-Tabelle in EEP Slot speichern +function speicherTabelle(Slot, Tabelle) + EEPSaveData(Slot, serpent.line(Tabelle, {comment = false})) +end + +SignalLesenFunktionen={} + +-- Definiere ein Signal +-- Signal: ID-Nummer +-- Haltstellung: Nummer der Haltstellung, meistens 1 +-- SchaltFunk: Callback, wird wiederholt aufgerufen, um Fahrt-Stellung des Signals zu erwirken +-- AbmeldeFunk: Wird nach Durchfahrt am Signal aufgerufen +function Basissignal(Signal, Haltstellung, SchaltFunk, AbmeldeFunk) + if EEPGetSignal(Signal)==0 then + print("Anlagen-Fehler: ID ",Signal," ist kein Signal") + return + end + -- Funktion, damit anderer Programmcode das Signal lesen kann + SignalLesenFunktionen[Signal]=function() + local Stellung = EEPGetSignal(Signal) + if Stellung == 0 or Stellung == Haltstellung then + return false, 0 + end + return true, Vmax + end + -- Funktion fuer Kontaktpunkte: Zug an Signal anmelden + _G["Anmeldung_"..tostring(Signal)] = function(Zug) + local Anmeldung + Anmeldung = ladeTabelle(Signal) + if Anmeldung.train and Anmeldung.train ~= Zug then return end + Anmeldung.train = Zug + speicherTabelle(Signal, Anmeldung) + end + -- Auftragsstatus pollen + MainFunktion(function() + local Anmeldung = ladeTabelle(Signal) + -- Wenn Zug vor Signal steht, Anmeldung ggf. nachholen + if EEPGetSignalTrainsCount(Signal) > 0 then + if Anmeldung.train == nil then + Anmeldung.train = EEPGetSignalTrainName(Signal, 1) + speicherTabelle(Signal, Anmeldung) + end + -- Ankunftszeit merken, sobald Zug am Signal angekommen ist + -- Wird im if von EEPGetSignalTrainsCount gemacht, + -- damit nur Halte an diesem Signal zaehlen + if Anmeldung.arrival == nil then + ok, speed = EEPGetTrainSpeed(Anmeldung.train) + if math.abs(speed) < 5 then + Anmeldung.arrival = EEPTime + speicherTabelle(Signal, Anmeldung) + end + end + end + -- Nichts tun wenn kein Zug da + if Anmeldung.train == nil then return end + -- SchaltFunk + if SchaltFunk and EEPGetSignal(Signal) == Haltstellung then + r=SchaltFunk(Anmeldung.train) + if r and r > 0 and r ~= Haltstellung then + EEPSetSignal(Signal, r, 1) + end + end + end) + -- Reaktion auf Signal-Umstellungen + SignalFunktion(Signal, function(Stellung) + if Stellung == Haltstellung then + -- Wenn Signal auf Halt gestellt wurde, mache eine Abmeldung + if AbmeldeFunk then + local Anmeldung = ladeTabelle(Signal) + AbmeldeFunk(Anmeldung.train) + end + speicherTabelle(Signal, {}) + end + end) +end + +-- Funktion zum Erklaeren, was ein Signal gerade so tut +-- Nimmt die Nummer des Signals als Argument +-- Gibt einen Menschenlesbaren Text zurueck +function SignalBeschreibung(Signal) + local Anmeldung = ladeTabelle(Signal) + local Fahrt, V = SignalLesenFunktionen[Signal]() + Text = "Signal " .. tonumber(Signal) .. ": " + if Fahrt then + if V < Vmax then + Text = Text .. "Fahrt mit " .. tonumber(V) .. " Km/h\n" + else + Text = Text .. "Fahrt\n" + end + else + Text = Text .. "Halt\n" + end + if Anmeldung.train then + Text = Text .. "Zug angemeldet: " .. Anmeldung.train .. "\n" + end + if Anmeldung.arrival then + local Dauer = EEPTime - Anmeldung.arrival + if Dauer < 0 then Dauer = Dauer + 24*60*60 end + Text = Text .. "Ankunft vor " .. Dauer .. " Sekunden\n" + end + return Text +end diff --git a/serpent.lua b/serpent.lua new file mode 100644 index 0000000..a043713 --- /dev/null +++ b/serpent.lua @@ -0,0 +1,140 @@ +local n, v = "serpent", "0.302" -- (C) 2012-18 Paul Kulchenko; MIT License +local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" +local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} +local badtype = {thread = true, userdata = true, cdata = true} +local getmetatable = debug and debug.getmetatable or getmetatable +local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+ +local keyword, globals, G = {}, {}, (_G or _ENV) +for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', + 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', + 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end +for k,v in pairs(G) do globals[v] = k end -- build func to name mapping +for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do + for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end + +local function s(t, opts) + local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum + local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge + local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) + local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring + local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) + local numformat = opts.numformat or "%.17g" + local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 + local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", + -- tostring(val) is needed because __tostring may return a non-string value + function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end + local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s)) + or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 + or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end + local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end + local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal + and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end + local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] + local n = name == nil and '' or name + local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] + local safe = plain and n or '['..safestr(n)..']' + return (path or '')..(plain and path and '.' or '')..safe, safe end + local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding + local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} + local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end + table.sort(k, function(a,b) + -- sort numeric keys first: k[key] is not nil for numerical keys + return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) + < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end + local function val2str(t, name, indent, insref, path, plainindex, level) + local ttype, level, mt = type(t), (level or 0), getmetatable(t) + local spath, sname = safename(path, name) + local tag = plainindex and + ((type(name) == "number") and '' or name..space..'='..space) or + (name ~= nil and sname..space..'='..space or '') + if seen[t] then -- already seen this element + sref[#sref+1] = spath..space..'='..space..seen[t] + return tag..'nil'..comment('ref', level) end + -- protect from those cases where __tostring may fail + if type(mt) == 'table' and metatostring ~= false then + local to, tr = pcall(function() return mt.__tostring(t) end) + local so, sr = pcall(function() return mt.__serialize(t) end) + if (to or so) then -- knows how to serialize itself + seen[t] = insref or spath + t = so and sr or tr + ttype = type(t) + end -- new value falls through to be serialized + end + if ttype == "table" then + if level >= maxl then return tag..'{}'..comment('maxlvl', level) end + seen[t] = insref or spath + if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty + if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end + local maxn, o, out = math.min(#t, maxnum or #t), {}, {} + for key = 1, maxn do o[key] = key end + if not maxnum or #o < maxnum then + local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables + for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end + if maxnum and #o > maxnum then o[maxnum+1] = nil end + if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end + local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) + for n, key in ipairs(o) do + local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse + if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing + or opts.keyallow and not opts.keyallow[key] + or opts.keyignore and opts.keyignore[key] + or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types + or sparse and value == nil then -- skipping nils; do nothing + elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then + if not seen[key] and not globals[key] then + sref[#sref+1] = 'placeholder' + local sname = safename(iname, gensym(key)) -- iname is table for local variables + sref[#sref] = val2str(key,sname,indent,sname,iname,true) end + sref[#sref+1] = 'placeholder' + local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' + sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) + else + out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1) + if maxlen then + maxlen = maxlen - #out[#out] + if maxlen < 0 then break end + end + end + end + local prefix = string.rep(indent or '', level) + local head = indent and '{\n'..prefix..indent or '{' + local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) + local tail = indent and "\n"..prefix..'}' or '}' + return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level) + elseif badtype[ttype] then + seen[t] = insref or spath + return tag..globerr(t, level) + elseif ttype == 'function' then + seen[t] = insref or spath + if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end + local ok, res = pcall(string.dump, t) + local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level) + return tag..(func or globerr(t, level)) + else return tag..safestr(t) end -- handle all other types + end + local sepr = indent and "\n" or ";"..space + local body = val2str(t, name, indent) -- this call also populates sref + local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' + local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' + return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" +end + +local function deserialize(data, opts) + local env = (opts and opts.safe == false) and G + or setmetatable({}, { + __index = function(t,k) return t end, + __call = function(t,...) error("cannot call functions") end + }) + local f, res = (loadstring or load)('return '..data, nil, nil, env) + if not f then f, res = (loadstring or load)(data, nil, nil, env) end + if not f then return f, res end + if setfenv then setfenv(f, env) end + return pcall(f) +end + +local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end +return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, + load = deserialize, + dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, + line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, + block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end }