diff options
Diffstat (limited to 'advtrains/lzb.lua')
-rw-r--r-- | advtrains/lzb.lua | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/advtrains/lzb.lua b/advtrains/lzb.lua new file mode 100644 index 0000000..cbdc422 --- /dev/null +++ b/advtrains/lzb.lua @@ -0,0 +1,276 @@ +-- lzb.lua +-- Enforced and/or automatic train override control, providing the on_train_approach callback + +--[[ +Documentation of train.lzb table +train.lzb = { + trav_index = Current index that the traverser has advanced so far + checkpoints = table containing oncoming signals, in order of index + { + pos = position of the point + index = where this is on the path + speed = speed allowed to pass. nil = no effect + callback = function(pos, id, train, index, speed, lzbdata) + -- Function that determines what to do on the train in the moment it drives over that point. + -- When spd==0, called instead when train has stopped in front + -- nil = no effect + lzbdata = {} + -- Table of custom data filled in by approach callbacks + -- Whenever an approach callback inserts an LZB checkpoint with changed lzbdata, + -- all consecutive approach callbacks will see these passed as lzbdata table. + + udata = arbitrary user data, no official way to retrieve (do not use) + } + trav_lzbdata = currently active lzbdata table at traverser index +} +The LZB subsystem keeps track of "checkpoints" the train will pass in the future, and has two main tasks: +1. run approach callbacks, and run callbacks when passing LZB checkpoints +2. keep track of the permitted speed at checkpoints, and make sure that the train brakes accordingly +To perform 2, it populates the train.path_speed table which is handled along with the path subsystem. +This table is used in trainlogic.lua/train_step_b() and applied to the velocity calculations. + +Note: in contrast to node enter callbacks, which are called when the train passes the .5 index mark, LZB callbacks are executed on passing the .0 index mark! +If an LZB checkpoint has speed 0, the train will still enter the node (the enter callback will be called), but will stop at the 0.9 index mark (for details, see SLOW_APPROACH in trainlogic.lua) + +The start point for the LZB traverser (and thus the first node that will receive an approach callback) is floor(train.index) + 1. This means, once the LZB checkpoint callback has fired, +this path node will not receive any further approach callbacks for the same approach situation +]] + + +local params = { + BRAKE_SPACE = 10, + AWARE_ZONE = 50, + + ADD_STAND = 2.5, + ADD_SLOW = 1.5, + ADD_FAST = 7, + ZONE_ROLL = 2, + ZONE_HOLD = 5, -- added on top of ZONE_ROLL + ZONE_VSLOW = 3, -- When speed is <2, still allow accelerating + + DST_FACTOR = 1.5, + + SHUNT_SPEED_MAX = advtrains.SHUNT_SPEED_MAX, +} + +function advtrains.set_lzb_param(par, val) + if params[par] and tonumber(val) then + params[par] = tonumber(val) + else + error("Inexistant param or not a number") + end +end + +local function resolve_latest_lzbdata(ckp, index) + local i = #ckp + local ckpi + while i>0 do + ckpi = ckp[i] + if ckpi.index <= index and ckpi.lzbdata then + return ckpi.lzbdata + end + i=i-1 + end + return {} +end + +local function look_ahead(id, train) + local lzb = train.lzb + if lzb.zero_checkpoint then + -- if the checkpoints list contains a zero checkpoint, don't look ahead + -- in order to not trigger approach callbacks on the wrong path + return + end + + local acc = advtrains.get_acceleration(train, 1) + -- worst-case: the starting point is maximum speed + local vel = train.max_speed or train.velocity + local brakedst = ( -(vel*vel) / (2*acc) ) * params.DST_FACTOR + + --local brake_i = advtrains.path_get_index_by_offset(train, train.index, brakedst + params.BRAKE_SPACE) + -- worst case (don't use index_by_offset) + local brake_i = atfloor(train.index + brakedst + params.BRAKE_SPACE) + atprint("LZB: looking ahead up to ", brake_i) + + --local aware_i = advtrains.path_get_index_by_offset(train, brake_i, AWARE_ZONE) + + local trav = lzb.trav_index + -- retrieve latest lzbdata + if not lzb.trav_lzbdata then + lzb.trav_lzbdata = resolve_latest_lzbdata(lzb.checkpoints, trav) + end + + if lzb.trav_lzbdata.off_track then + --previous position was off track, do not scan any further + end + + while trav <= brake_i and not lzb.zero_checkpoint do + local pos = advtrains.path_get(train, trav) + -- check offtrack + if trav - 1 == train.path_trk_f then + lzb.trav_lzbdata.off_track = true + advtrains.lzb_add_checkpoint(train, trav - 1, 0, nil, lzb.trav_lzbdata) + else + -- run callbacks + -- Note: those callbacks are defined in trainlogic.lua for consistency with the other node callbacks + advtrains.tnc_call_approach_callback(pos, id, train, trav, lzb.trav_lzbdata) + + end + trav = trav + 1 + + end + + lzb.trav_index = trav + +end +advtrains.lzb_look_ahead = look_ahead + + +local function call_runover_callbacks(id, train) + if not train.lzb then return end + + local i = 1 + local idx = atfloor(train.index) + local ckp = train.lzb.checkpoints + while ckp[i] do + if ckp[i].index <= idx then + atprint("LZB: checkpoint run over: i=",ckp[i].index,"s=",ckp[i].speed) + -- call callback + local it = ckp[i] + if it.callback then + it.callback(it.pos, id, train, it.index, it.speed, train.lzb.lzbdata) + end + -- note: lzbdata is always defined as look_ahead was called before + table.remove(ckp, i) + else + i = i + 1 + end + end +end + +-- Flood-fills train.path_speed, based on this checkpoint +local function apply_checkpoint_to_path(train, checkpoint) + if not checkpoint.speed then + return + end + atprint("LZB: applying checkpoint: i=",checkpoint.index,"s=",checkpoint.speed) + + if checkpoint.speed == 0 then + train.lzb.zero_checkpoint = true + end + + -- make sure path exists until checkpoint + local pos = advtrains.path_get(train, checkpoint.index) + + local brake_accel = advtrains.get_acceleration(train, 11) + + -- start with the checkpoint index at specified speed + local index = checkpoint.index + local p_speed -- speed in path_speed + local c_speed = checkpoint.speed -- calculated speed at current index + while true do + p_speed = train.path_speed[index] + if (p_speed and p_speed <= c_speed) or index < train.index then + --we're done. train already slower than wanted at this position + return + end + -- insert calculated target speed + train.path_speed[index] = c_speed + -- calculate c_speed at previous index + advtrains.path_get(train, index-1) + local eldist = train.path_dist[index] - train.path_dist[index-1] + -- Calculate the start velocity the train would have if it had a end velocity of c_speed and accelerating with brake_accel, after a distance of eldist: + -- v0² = v1² - 2*a*s + c_speed = math.sqrt( (c_speed * c_speed) - (2 * brake_accel * eldist) ) + index = index - 1 + end +end + +--[[ +Distance needed to accelerate from v0 to v1 with constant acceleration a: + + v1 - v0 a / v1 - v0 \ 2 v1^2 - v0^2 +s = v0 * ------- + - * | ------- | = ----------- + a 2 \ a / 2*a +]] + +-- Removes all LZB checkpoints and restarts the traverser at the current train index +function advtrains.lzb_invalidate(train) + train.lzb = { + trav_index = atfloor(train.index) + 1, + checkpoints = {}, + } +end + +-- LZB part of path_invalidate_ahead. Clears all checkpoints that are ahead of start_idx +-- in contrast to path_inv_ahead, doesn't complain if start_idx is behind train.index, clears everything then +function advtrains.lzb_invalidate_ahead(train, start_idx) + if train.lzb then + local idx = atfloor(start_idx) + local i = 1 + while train.lzb.checkpoints[i] do + if train.lzb.checkpoints[i].index >= idx then + table.remove(train.lzb.checkpoints, i) + else + i=i+1 + end + end + train.lzb.trav_index = idx + -- FIX reset trav_lzbdata (look_ahead fetches these when required) + train.lzb.trav_lzbdata = nil + -- re-apply all checkpoints to path_speed + train.path_speed = {} + train.lzb.zero_checkpoint = false + for _,ckp in ipairs(train.lzb.checkpoints) do + apply_checkpoint_to_path(train, ckp) + end + end +end + +-- Add LZB control point +-- lzbdata: If you modify lzbdata in an approach callback, you MUST add a checkpoint AND pass the (modified) lzbdata into it. +-- If you DON'T modify lzbdata, you MUST pass nil as lzbdata. Always modify the lzbdata table in place, never overwrite it! +-- udata: user-defined data, do not use externally +function advtrains.lzb_add_checkpoint(train, index, speed, callback, lzbdata, udata) + local lzb = train.lzb + local pos = advtrains.path_get(train, index) + local lzbdata_c = nil + if lzbdata then + -- make a shallow copy of lzbdata + lzbdata_c = {} + for k,v in pairs(lzbdata) do lzbdata_c[k] = v end + end + local ckp = { + pos = pos, + index = index, + speed = speed, + callback = callback, + lzbdata = lzbdata_c, + udata = udata, + } + table.insert(lzb.checkpoints, ckp) + + apply_checkpoint_to_path(train, ckp) +end + + +advtrains.te_register_on_new_path(function(id, train) + advtrains.lzb_invalidate(train) + -- Taken care of in pre-move hook (see train_step_b) + --look_ahead(id, train) +end) + +advtrains.te_register_on_invalidate_ahead(function(id, train, start_idx) + advtrains.lzb_invalidate_ahead(train, start_idx) +end) + +advtrains.te_register_on_update(function(id, train) + if not train.path or not train.lzb then + atprint("LZB run: no path on train, skip step") + return + end + -- Note: look_ahead called from train_step_b before applying movement + -- TODO: if more pre-move hooks are added, make a separate callback hook + --look_ahead(id, train) + call_runover_callbacks(id, train) +end, true) |