From 30e98520e7e6fa17beaf3f1c328fd58e2b19cebc Mon Sep 17 00:00:00 2001 From: orwell96 Date: Mon, 23 Apr 2018 15:51:50 +0200 Subject: Occupation System, new train steps, still incomplete --- advtrains/atc.lua | 52 ++-- advtrains/occupation.lua | 279 +++++++++++++++++++++ advtrains/path.lua | 38 +++ advtrains/trainhud.lua | 6 +- advtrains/trainlogic.lua | 626 +++++++++++++++++++++-------------------------- advtrains/wagons.lua | 24 +- 6 files changed, 642 insertions(+), 383 deletions(-) create mode 100644 advtrains/occupation.lua diff --git a/advtrains/atc.lua b/advtrains/atc.lua index 10321d9..940cb08 100644 --- a/advtrains/atc.lua +++ b/advtrains/atc.lua @@ -20,34 +20,39 @@ end --contents: {command="...", arrowconn=0-15 where arrow points} --general +function atc.train_set_command(train_id, command, arrow) + atc.train_reset_command(train_id) + advtrains.trains[train_id].atc_arrow = arrow + advtrains.trains[train_id].atc_command = command +end function atc.send_command(pos, par_tid) local pts=minetest.pos_to_string(pos) if atc.controllers[pts] then --atprint("Called send_command at "..pts) - local train_id = par_tid or advtrains.detector.get(pos) + local train_id = par_tid or advtrains.detector.get(pos) -- TODO: succesively replace those detector calls! if train_id then if advtrains.trains[train_id] then --atprint("send_command inside if: "..sid(train_id)) - atc.train_reset_command(train_id) - local arrowconn=atc.controllers[pts].arrowconn - local train=advtrains.trains[train_id] - for index, ppos in pairs(train.path) do - if vector.equals(advtrains.round_vector_floor_y(ppos), pos) then - advtrains.trains[train_id].atc_arrow = - vector.equals( - advtrains.dirCoordSet(pos, arrowconn), - advtrains.round_vector_floor_y(train.path[index+train.movedir]) - ) - advtrains.trains[train_id].atc_command=atc.controllers[pts].command - atprint("Sending ATC Command: ", atc.controllers[pts].command) - return true - end + if atc.controllers[pts].arrowconn then + atlog("ATC controller at",pts,": This controller had an arrowconn of", atc.controllers[pts].arrowconn, "set. Since this field is now deprecated, it was removed.") + atc.controllers[pts].arrowconn = nil end - atwarn("ATC rail at", pos, ": Rail not on train's path! Can't determine arrow direction. Assuming +!") - advtrains.trains[train_id].atc_arrow=true - advtrains.trains[train_id].atc_command=atc.controllers[pts].command - atprint("Sending ATC Command: ", atc.controllers[pts].command) + + local train = advtrains.trains[train_id] + local index = advtrains.path_lookup(train, pos) + + local iconnid = 1 + if index then + iconnid = train.path_cn[index] + else + atwarn("ATC rail at", pos, ": Rail not on train's path! Can't determine arrow direction. Assuming +!") + end + + atc.train_set_command(train_id, atc.controllers[pts].command, iconnid==1) + atprint("Sending ATC Command to", train_id, ":", atc.controllers[pts].command, "iconnid=",iconnid) + return true + else atwarn("ATC rail at", pos, ": Sending command failed: The train",train_id,"does not exist. This seems to be a bug.") end @@ -125,7 +130,7 @@ advtrains.atc_function = function(def, preset, suffix, rotation) local pts=minetest.pos_to_string(pos) local _, conns=advtrains.get_rail_info_at(pos, advtrains.all_tracktypes) - atc.controllers[pts]={command=fields.command, arrowconn=conns[1].c} + atc.controllers[pts]={command=fields.command} if advtrains.detector.get(pos) then atc.send_command(pos) end @@ -168,8 +173,11 @@ local matchptn={ train.tarvelocity=tonumber(match) return #match+1 end, - ["B([0-9]+)"]=function(id, train, match) - if train.velocity>tonumber(match) then + ["B([0-9B]+)"]=function(id, train, match) + if match=="B" then + train.atc_brake_target = -1 -- this means emergency brake. TODO don't forget to implement in train step! + train.tarvelocity = 0 + elseif train.velocity>tonumber(match) then train.atc_brake_target=tonumber(match) if train.tarvelocity>train.atc_brake_target then train.tarvelocity=train.atc_brake_target diff --git a/advtrains/occupation.lua b/advtrains/occupation.lua new file mode 100644 index 0000000..f6fa6fc --- /dev/null +++ b/advtrains/occupation.lua @@ -0,0 +1,279 @@ +-- occupation.lua +--[[ +Collects and manages positions where trains occupy and/or reserve/require space + +Zone diagram of a train: + |___| |___| --> Direction of travel + oo oo+oo oo +=|=======|===|===========|===|=======|===================|========|=== + |SafetyB|CpB| Train |CpF|SafetyF| Brake |Aware | +[1] [2] [3] [4] [5] [6] [7] [8] + +ID|Name |Desc + 0 Free Zone that was occupied before, which has now been left + 1 Train Zone where the train actually is. + 2 SafetyB Safety zone behind the train. extends 4m + 3 SafetyF Safety zone in front of the train. extends 4m + If a train is about to enter this zone, immediately brake it down to 2 + 4 CpB Backside coupling zone. If the coupling zones of 2 trains overlap, they can be coupled + 5 CpF Frontside coupling zone + 6 Brake Brake distance of the train. Extends to the point ~5 nodes in front + of the point where the train would stop if it would regularily brake now. + 7 Aware Awareness zone. Extends 10-20 nodes beyond the Brake zone + Whenever any of the non-aware zones of other trains are detected here, the train will start to brake. + +Table format: +occ[y][x][z] = { + [1] = train 1 id + [2] = train 1 ZoneID +// [3] = entry seqnum* + ... + [2n-1] = train n id + [2n ] = train n ZoneID +// [3n-2] = train n id +// [3n-1] = train n ZoneID +// [3n ] = entry seqnum* +} +occ_chg[n] = { + pos = vector, + train_id, + old_val, (0 when entry did not exist before) + new_val, (0 when entry was deleted) +} + +*Sequence number: +Sequence number system reserved for possible future use, but unused. +The train will (and has to) memorize it's zone path indexes ("windows"), and do all actions that in any way modify these zone lengths +in the movement phase (after restore, but before reporting occupations) +(( +The sequence number is used to determine out-of-date entries to the occupation list +The current sequence number (seqnum) is increased each step, until it rolls over MAX_SEQNUM, which is when a complete reset is triggered +Inside a step, when a train updates an occupation, the sequence number is set to the currently active sequence number +Whenever checking an entry for other occupations (e.g. in the aware zone), all entries that have a seqnum different from the current seqnum +are considered not existant, and are cleared. +Note that those outdated entries are only cleared on-demand, so there will be a large memory overhead over time. This is why in certain time intervals +complete resets are required (however, this method should be much more performant than resetting the whole occ table each step, to spare continuous memory allocations) +This complex behavior is required because there is no way to reliably determine which positions are _no longer_ occupied... +)) + +Composition of a step: + +1. (only when needed) restore step - write all current occupations into the table +2. trains move +3. trains pass new occupations to here. We keep track of which entries have changed +4. we iterate our change lists and determine what to do + +]]-- +local o + +o.restore_required = true + +local MAX_SEQNUM = 65500 +local seqnum = 0 + +local occ = {} +local occ_chg = {} + + +local function occget(p) + local t = occ[p.y] + if not t then + occ[p.y] = {} + t = occ[p.y] + end + t = t[p.x] + if not t then + t[p.x] = {} + t = t[p.x] + end + return t[p.z] +end +local function occgetcreate(p) + local t = occ[p.y] + if not t then + occ[p.y] = {} + t = occ[p.y] + end + t = t[p.x] + if not t then + t[p.x] = {} + t = t[p.x] + end + t = t[p.z] + if not t then + t[p.z] = {} + t = t[p.z] + end + return t +end + +-- Resets the occupation memory, and sets the o.restore_required flag that instructs trains to report their occupations before moving +function o.reset() + o.restore_required = true + occ = {} + occ_chg = {} + seqnum = 0 +end + +-- set occupation inside the restore step +function o.init_occupation(train_id, pos, oid) + local t = occgetcreate(pos) + local i = 1 + while t[i] + if t[i]==train_id then + break + end + i = i + 2 + end + t[i] = train_id + t[i+1] = oid +end + +function o.set_occupation(train_id, pos, oid) + local t = occgetcreate(pos) + local i = 1 + while t[i] + if t[i]==train_id then + break + end + i = i + 2 + end + local oldoid = t[i+1] or 0 + if oldoid ~= oid then + addchg(pos, train_id, oldoid, oid) + end + t[i] = train_id + t[i+1] = oid +end + + +function o.clear_occupation(train_id, pos) + local t = occget(pos) + if not t then return end + local i = 1 + local moving = false + while t[i] do + if t[i]==train_id then + if moving then + -- if, for some occasion, there should be a duplicate entry, erase this one too + atwarn("Duplicate occupation entry at",pos,"for train",train_id,":",t) + i = i - 2 + end + local oldoid = t[i+1] or 0 + addchg(pos, train_id, oldoid, 0) + moving = true + end + if moving then + t[i] = t[i+2] + t[i+1] = t[i+3] + end + i = i + 2 + end +end + +local function addchg(pos, train_id, old, new) + occ_chg[#occ_chg + 1] = { + pos = pos, + train_id = train_id, + old_val = old, + new_val = new, + } +end + +-- Called after all occupations have been fed in +-- This function is doing the interesting work... +function o.end_step() + count_chg = false + + for _,chg in ipairs(occ_chg) do + local t = occget(chg.pos) + if not t then + atwarn("o.end_step() change entry but there's no entry in occ table!",chg) + end + handle_chg(t, chg.pos, chg.train_id, chg.old_val, chg.new_val) + end + + seqnum = seqnum + 1 +end + +local function handle_chg(t, pos, train_id, old, new) + -- Handling the actual "change" is only necessary on_train_enter (change to 1) and on_train_leave (change from 1) + if new==1 then + o.call_enter_callback(pos, train_id) + elseif old==1 then + o.call_leave_callback(pos, train_id) + end + + --all other cases check the simultaneous presence of 2 or more occupations + if #t<=2 then + return + end + local blocking = {} + local aware = {} + local i = 1 + while t[i] do + if t[i+1] ~= 7 then --anything not aware zone: + blocking[#blocking+1] = i + else + aware[#aware+1] = i + end + i = i + 2 + end + if #blocking > 0 then + -- the aware trains should brake + for _, ix in ipairs(aware) do + atc.train_set_command(t[ix], "B2") + end + if #blocking > 1 then + -- not good, 2 trains interfered with their blocking zones + -- make them brake too + local txt = {} + for _, ix in ipairs(blocking) do + atc.train_set_command(t[ix], "B2") + txt[#txt+1] = t[ix] + end + atwarn("Trains",table.concat(txt, ","), "interfered with their blocking zones, braking...") + -- TODO: different behavior for automatic trains! they need to be notified of those brake events and handle them! + -- To drive in safety zone is ok when train is controlled by hand + end + end + +end + +function o.call_enter_callback(pos, train_id) + --atprint("instructed to call enter calback") + + local node = advtrains.ndb.get_node(pos) --this spares the check if node is nil, it has a name in any case + local mregnode=minetest.registered_nodes[node.name] + if mregnode and mregnode.advtrains and mregnode.advtrains.on_train_enter then + mregnode.advtrains.on_train_enter(pos, train_id) + end +end +function o.call_leave_callback(pos, train_id) + --atprint("instructed to call leave calback") + + local node = advtrains.ndb.get_node(pos) --this spares the check if node is nil, it has a name in any case + local mregnode=minetest.registered_nodes[node.name] + if mregnode and mregnode.advtrains and mregnode.advtrains.on_train_leave then + mregnode.advtrains.on_train_leave(pos, train_id) + end +end + +-- Checks whether some other train (apart from train_id) has it's 0 zone here +function o.check_collision(pos, train_id) + local npos = advtrains.round_vector_floor_y(pos) + local t = occget(npos) + if not t then return end + local i = 1 + while t[i] do + if t[i]~=train_id then + if t[i+1] ~= 7 then + return true + end + end + i = i + 2 + end + return false +end + +advtrains.occ = o diff --git a/advtrains/path.lua b/advtrains/path.lua index 43add95..a98a1b5 100644 --- a/advtrains/path.lua +++ b/advtrains/path.lua @@ -183,6 +183,13 @@ function advtrains.path_get(train, index) train.path_dist[train.path_ext_b] = vector.distance(pos, adj_pos) end + if index < train.path_req_b then + train.path_req_b = index + end + if index > train.path_req_f then + train.path_req_f = index + end + return train.path[index], (index<=train.path_trk_f and index>=train.path_trk_b) end @@ -233,3 +240,34 @@ function advtrains.path_get_index_by_offset(train, index, offset) end return index end + +local PATH_CLEAR_KEEP = 2 + +function advtrains.path_clear_unused(train) + for i = train.path_ext_b, train.path_req_b - PATH_CLEAR_KEEP do + train.path[i] = nil + train.path_dist[i] = nil + train.path_cp[i] = nil + train.path_cn[i] = nil + train.path_dir[i] = nil + end + for i = train.path_req_f + PATH_CLEAR_KEEP, train.path_ext_f do + train.path[i] = nil + train.path_dist[i-1] = nil + train.path_cp[i] = nil + train.path_cn[i] = nil + train.path_dir[i+1] = nil + end + train.path_req_f = math.ceil(train.index) + train.path_req_b = math.floor(train.end_index or train.index) +end + +function advtrains.path_lookup(train, pos) + local cp = advtrains.round_vector_floor_y(pos) + for i = train.path_ext_b, train.path_ext_f do + if vector.equals(advtrains.round_vector_floor_y(train.path[i]), cp) then + return i + end + end + return nil +end diff --git a/advtrains/trainhud.lua b/advtrains/trainhud.lua index 9b7b9e8..e48e3e4 100644 --- a/advtrains/trainhud.lua +++ b/advtrains/trainhud.lua @@ -67,14 +67,14 @@ function advtrains.on_control_change(pc, train, flip) if train.door_open ~= 0 then train.door_open = 0 else - train.door_open = -train.movedir + train.door_open = -1 end end if pc.right then if train.door_open ~= 0 then train.door_open = 0 else - train.door_open = train.movedir + train.door_open = 1 end end train.active_control = act @@ -176,7 +176,7 @@ function advtrains.hud_train_format(train, flip) local topLine, firstLine, secondLine - topLine=" ["..mletter[fct*train.movedir].."] {"..levers.."} "..doorstr[(train.door_open or 0) * train.movedir] + topLine=" ["..mletter[fct].."] {"..levers.."} "..doorstr[(train.door_open or 0) * fct] firstLine=attrans("Speed:").." |"..string.rep("+", vel)..string.rep("_", max-vel).."> "..vel_kmh.." km/h" secondLine=attrans("Target:").." |"..string.rep("+", tvel)..string.rep("_", max-tvel).."> "..tvel_kmh.." km/h" diff --git a/advtrains/trainlogic.lua b/advtrains/trainlogic.lua index 412d229..110d3f6 100644 --- a/advtrains/trainlogic.lua +++ b/advtrains/trainlogic.lua @@ -1,6 +1,7 @@ --trainlogic.lua --controls train entities stuff about connecting/disconnecting/colliding trains and other things +-- TODO: what should happen when a train has no trainparts anymore? local benchmark=false local bm={} @@ -57,21 +58,31 @@ advtrains.mainloop_trainlogic=function(dtime) end end --regular train step - -- do in two steps: - -- a: predict path and add all nodes to the advtrains.detector.on_node table - -- b: check for collisions based on these data - -- (and more) + --[[ structure: + 1. make trains calculate their occupation windows when needed (a) + 2. when occupation tells us so, restore the occupation tables (a) + 4. make trains move and update their new occupation windows and write changes + to occupation tables (b) + 5. make trains do other stuff (c) + ]]-- local t=os.clock() - advtrains.detector.on_node={} + for k,v in pairs(advtrains.trains) do advtrains.atprint_context_tid=sid(k) advtrains.atprint_context_tid_full=k - advtrains.train_step_a(k, v, dtime) + train_ensure_clean(k, v, dtime) end + for k,v in pairs(advtrains.trains) do advtrains.atprint_context_tid=sid(k) advtrains.atprint_context_tid_full=k - advtrains.train_step_b(k, v, dtime) + train_step_b(k, v, dtime) + end + + for k,v in pairs(advtrains.trains) do + advtrains.atprint_context_tid=sid(k) + advtrains.atprint_context_tid_full=k + train_step_c(k, v, dtime) end advtrains.atprint_context_tid=nil @@ -120,118 +131,176 @@ minetest.register_on_dieplayer(function(player) end end) end) + --[[ -train step structure: +The occupation window system. + +Each train occupies certain nodes as certain occupation types. See occupation.lua for a graphic and an ID listing. +There's an occwindows table in the train table. This is clearable, such as the path, and therefore needs to be exactly reconstructible. +During runtime, the extents (in meters) of the zones are determined. the occwindows table holds the assigned fractional path indices. +After the train moves, the occupation windows are re-calculated, and all differences are written to the occupation tables. + +Zone diagram of a train (copy from occupation.lua!): + |___| |___| --> Direction of travel + oo oo+oo oo +=|=======|===|===========|===|=======|===================|========|=== + |SafetyB|CpB| Train |CpF|SafetyF| Brake |Aware | +[1] [2] [3] [4] [5] [6] [7] [8] +This mapping from indices in occwindows to zone ids is contained in WINDOW_ZONE_IDS + +occwindows = { +[n] = (index of the position determined in the graphic above, + where floor(i) belongs to the left zone and floor(i+1) belongs to the right. +} +]]-- +-- unless otherwise stated, in meters. +local SAFETY_ZONE = 10 +local COUPLE_ZONE = 2 --value in index positions! +local BRAKE_SPACE = 10 +local AWARE_ZONE = 10 +local WINDOW_ZONE_IDS = { + 2, -- 1 - SafetyB + 4, -- 2 - CpB + 1, -- 3 - Train + 5, -- 4 - CpF + 3, -- 5 - SafetyF + 6, -- 6 - Brake + 7, -- 7 - Aware +} -- legacy stuff -- preparing the initial path and creating index -- setting node coverage old indices -- handle velocity influences: - - off-track - - atc - - player controls - - environment collision -- update index = move -- create path -- call stay_node on all old positions to register train there, for collision system -- do less important stuff such as checking trainpartload or removing --- break -- -- Call enter_node and leave_node callbacks (required here because stay_node needs to be called on all trains first) -- handle train collisions +-- If a variable does not exist in the table, it is assigned the default value +local function assertdef(tbl, var, def) + if not tbl[var] then + tbl[var] = def + end +end -]] -function advtrains.train_step_a(id, train, dtime) - --atprint("--- runcnt ",advtrains.mainloop_runcnt,": index",train.index,"end_index", train.end_index,"| max_iot", train.max_index_on_track, "min_iot", train.min_index_on_track, "<> pe_min", train.path_extent_min,"pe_max", train.path_extent_max) - if train.min_index_on_track then - assert(math.floor(train.min_index_on_track)==train.min_index_on_track) +-- Calculates the indices where the window borders of the occupation windows are. +local function calc_occwindows(id, train) + local end_index = advtrains.path_get_index_by_offset(train, train.index, -train.trainlen) + train.end_index = end_index + local cpl_b = end_index - COUPLE_ZONE + local safety_b = advtrains.path_get_index_by_offset(train, cpl_b, -SAFETY_ZONE) + local cpl_f = end_index + COUPLE_ZONE + local safety_f = advtrains.path_get_index_by_offset(train, cpl_f, SAFETY_ZONE) + + -- calculate brake distance + local acc_all = t_accel_all[1] + local acc_eng = t_accel_eng[1] + local nwagons = #train.trainparts + local acc = acc_all + (acc_eng*train.locomotives_in_train)/nwagons + local vel = train.velocity + local brakedst = (vel*vel) / (2*acc) + + local brake_i = math.max(advtrains.path_get_index_by_offset(train, train.index, brakedst + BRAKE_SPACE), safety_f) + local aware_i = advtrains.path_get_index_by_offset(train, brake_i, AWARE_ZONE) + + return { + safety_b, + cpl_b, + end_index, + train.index, + cpl_f, + safety_f, + brake_i, + aware_i, + } +end + +-- this function either inits (no write_mode), sets(1) or clears(2) the occupations for train +local function write_occupation(win, train_id, train, write_mode) + local n_window = 2 + local c_index = math.ceil(win[1]) + while win[n_window] do + local winix = win[n_window] + local oid = WINDOW_ZONE_IDS[n_windows - 1] + while winix > c_index do + local pos = advtrains.path_get(train, c_index) + if write_mode == 1 then + advtrains.occ.set_occupation(train_id, pos, oid) + elseif write_mode == 2 then + advtrains.occ.clear_occupation(train_id, pos) + else + advtrains.occ.init_occupation(train_id, pos, oid) + end + c_index = c_index + 1 + end + c_index = math.ceil(winix) + n_window = n_window + 1 end - --- 1. not exactly legacy. required now because of saving --- + +end +local function apply_occupation_changes(old, new, train_id, train) + -- TODO +end + + +-- train_ensure_clean: responsible for creating a state that we can work on, after one of the following events has happened: +-- - the train's path got cleared +-- - the occupation table got cleared +-- Additionally, this gets called outside the step cycle to initialize and/or remove a train, then occ_write_mode is set. +local function train_ensure_clean(id, train, dtime, report_occupations, occ_write_mode) + train.dirty = true + if train.no_step then return end + + assertdef(train, "velocity", 0) + assertdef(train, "tarvelocity", 0) + assertdef(train, "acceleration", 0) + + if not train.drives_on or not train.max_speed then advtrains.update_trainpart_properties(id) end - --TODO check for all vars to be present - if not train.velocity then - train.velocity=0 - end - if not train.movedir or (train.movedir~=1 and train.movedir~=-1) then - train.movedir=1 - end - --- 2. prepare initial path and index if needed --- - if not train.index then train.index=0 end + + --restore path if not train.path then if not train.last_pos then - --no chance to recover - atwarn("Unable to restore train ",id,": missing last_pos") - advtrains.trains[id]=nil - return false + atwarn("Train",id": Restoring path failed, no last_pos set! Train will be disabled. You can try to fix the issue in the save file.") + train.no_step = true + return end - - local node_ok=advtrains.get_rail_info_at(advtrains.round_vector_floor_y(train.last_pos), train.drives_on) - - if node_ok==nil then - --block not loaded, do nothing - atprint("last_pos", advtrains.round_vector_floor_y(train.last_pos), "not loaded and not in ndb, waiting") - return nil - elseif node_ok==false then - atprint("Unable to restore train ",id,": No rail at train's position") - return false + if not train.last_connid then + atwarn("Train",id": Restoring path failed, no last_connid set! Will assume 1") end - if not train.last_pos_prev then - --no chance to recover - atwarn("Unable to restore train ",id,": missing last_pos_prev") - advtrains.trains[id]=nil - return false - end + local result = advtrains.path_create(train, train.last_pos, train.last_connid, train.last_frac or 0) - local prevnode_ok=advtrains.get_rail_info_at(advtrains.round_vector_floor_y(train.last_pos_prev), train.drives_on) - - if prevnode_ok==nil then - --block not loaded, do nothing - atprint("last_pos_prev", advtrains.round_vector_floor_y(train.last_pos_prev), "not loaded and not in ndb, waiting") - return nil - elseif prevnode_ok==false then - atprint("Unable to restore train ",id,": No rail at train's position") - return false + if result==false then + atwarn("Train",id": Restoring path failed, node at",train.last_pos,"is gone! Train will be disabled. You can try to fix the issue in the save file.") + train.no_step = true + return + elseif result==nil then + if not train.wait_for_path then + atwarn("Train",id": Can't initialize: Waiting for the (yet unloaded) node at",train.last_pos," to be loaded.") + end + train.wait_for_path = true end - - train.index=(train.restore_add_index or 0)+(train.savedpos_off_track_index_offset or 0) - --restore_add_index is set by save() to prevent trains hopping to next round index. should be between -0.5 and 0.5 - --savedpos_off_track_index_offset is set if train went off track. see below. - train.path={} - train.path_dist={} - train.path[0]=train.last_pos - train.path[-1]=train.last_pos_prev - train.path_dist[-1]=vector.distance(train.last_pos, train.last_pos_prev) - train.path_extent_min=-1 - train.path_extent_max=0 - train.min_index_on_track=-1 - train.max_index_on_track=0 - - --[[ - Bugfix for trains randomly ignoring ATC rails: - - Paths have been invalidated. 1 gets executed and ensures an initial path - - 2a sets train end index. The problem is that path_dist is not known for the whole path, so train end index will be nearly trainlen - - Since the detector indices are also unknown, they get set to the new (wrong) train_end_index. Enter_node calls are not executed for the nodes that lie in between real end_index and trainlen. - - The next step, mistake is recognized, train leaves some positions. From there, everything works again. - To overcome this, we will generate the full required path here so that path_dist is available for get_train_end_index(). - ]] - advtrains.pathpredict(id, train, 3, -train.trainlen-3) + -- by now, we should have a working initial path + train.occwindows = nil + advtrains.update_trainpart_properties(id) + -- TODO recoverposition?! end - --- 2a. set train.end_index which is required in different places, IF IT IS NOT SET YET by STMT afterwards. --- - --- table entry to avoid triple recalculation --- - if not train.end_index then - train.end_index=advtrains.get_train_end_index(train) + --restore occupation windows + if not train.occwindows then + train.occwindows = calc_occwindows(id, train) + end + if report_occupations then + write_occupation(train.occwindows, train, occ_write_mode) end - --- 2b. set node coverage old indices --- + train.dirty = false -- TODO einbauen! +end + +local function train_step_b(id, train, dtime) + if train.no_step or train.wait_for_path then return end - train.detector_old_index = atround(train.index) - train.detector_old_end_index = atround(train.end_index) + -- in this code, we check variables such as path_trk_? and path_dist. We need to ensure that the path is known for the whole 'Train' zone + advtrains.path_get(train, train.index + 1) + advtrains.path_get(train, train.end_index - 1) --- 3. handle velocity influences --- local train_moves=(train.velocity~=0) @@ -249,33 +318,22 @@ function advtrains.train_step_a(id, train, dtime) end --- 3a. this can be useful for debugs/warnings and is used for check_trainpartload --- - local t_info, train_pos=sid(id), train.path[atround(train.index)] + local t_info, train_pos=sid(id), advtrains.path_get(atfloor(train.index)) if train_pos then t_info=t_info.." @"..minetest.pos_to_string(train_pos) --atprint("train_pos:",train_pos) end --apply off-track handling: - local front_off_track=train.max_index_on_track and train.index>train.max_index_on_track - local back_off_track=train.min_index_on_track and train.end_indextrain.path_trk_f + local back_off_track=train.end_index mspeed then vdiff = mspeed - trainvelocity end - train.last_accel=(vdiff*train.movedir) + train.acceleration=vdiff train.velocity=train.velocity+vdiff if train.active_control then train.tarvelocity = train.velocity @@ -354,218 +412,60 @@ function advtrains.train_step_a(id, train, dtime) --- 4. move train --- - train.index=train.index and train.index+(((train.velocity*train.movedir)/(train.path_dist[math.floor(train.index)] or 1))*dtime) or 0 - - --- 4a. update train.end_index to the new position --- - train.end_index=advtrains.get_train_end_index(train) - - --- 4b calculate how far a path is required --- - local path_req_dd = 10 -- path required in driving direction - local path_req_ndd = 4 -- path required against driving direction - - -- when using easyBSS (block signalling), we need to make sure that the whole brake distance is known - if advtrains_easybss then - local acc_all = t_accel_all[1] - local acc_eng = t_accel_eng[1] - local nwagons = #train.trainparts - local acc = acc_all + (acc_eng*train.locomotives_in_train)/nwagons - local vel = train.velocity - local brakedst = (vel*vel) / (2*acc) - path_req_dd = math.ceil(brakedst+10) - end - - - local idx_front=math.max(train.index, train.detector_old_index) - local idx_back=math.min(train.end_index, train.detector_old_end_index) - local path_req_front, path_req_back - if train.movedir == 1 then - path_req_front = atround(idx_front + path_req_dd) - path_req_back = atround(idx_back - path_req_ndd) - else - path_req_front = atround(idx_front + path_req_ndd) - path_req_back = atround(idx_back - path_req_dd) - end - - --- 5. extend path as necessary --- - --why this is an extra function, see under 3. - advtrains.pathpredict(id, train, path_req_front, path_req_back) - - --- 5a. make pos/yaw available for possible recover calls --- - if train.max_index_on_tracktrain.index then --whoops, train went even more far. same behavior - train.savedpos_off_track_index_offset=atround(train.index)-train.min_index_on_track - train.last_pos=train.path[train.min_index_on_track+1] - train.last_pos_prev=train.path[train.min_index_on_track] - atprint("train is off-track (back), last positions kept at", train.last_pos, "/", train.last_pos_prev) - else --regular case - train.savedpos_off_track_index_offset=nil - train.last_pos=train.path[math.floor(train.index+1)] - train.last_pos_prev=train.path[math.floor(train.index)] - end - - --- 5b. Remove path items that are no longer used --- - -- Necessary since path items are no longer invalidated in save steps - local del_keep=8 - local offtrack_keep=4 - - local delete_min=math.min(train.max_index_on_track - offtrack_keep, path_req_back - del_keep) - local delete_max=math.max(train.min_index_on_track + offtrack_keep, path_req_front + del_keep) - - if train.path_extent_mindelete_max then - --atprint(sid(id),"clearing path max ",train.path_extent_max," to ",delete_max) - train.path_dist[delete_max]=nil - for i=delete_max+1,train.path_extent_max do - train.path[i]=nil - train.path_dist[i]=nil - end - train.path_extent_max=delete_max - train.max_index_on_track=math.min(train.max_index_on_track, delete_max) - end + train.index=train.index and train.index+((train.velocity/(train.path_dist[math.floor(train.index)] or 1))*dtime) or 0 + +end + +local function train_recalc_occupation() + local new_occwindows = calc_occwindows(id, train) + apply_occupation_changes(train.occwindows, new_occwindows, id) + train.occwindows = new_occwindows +end + +local function train_step_c(id, train, dtime) +if train.no_step or train.wait_for_path then return end - --- 6b. call stay_node to register trains in the location table - actual enter_node stuff is done in step b --- + -- all location/extent-critical actions have been done. + -- calculate the new occupation window + train_recalc_occupation(id, train) - local ifo, ibo = train.detector_old_index, train.detector_old_end_index - local path=train.path + advtrains.path_clear_unused(train) - for i=ibo, ifo do - if path[i] then - advtrains.detector.stay_node(path[i], id) - end - end + -- Set our path restoration position + local fli = atfloor(train.index) + train.last_pos = advtrains.path_get(fli) + train.last_connid = train.path_cn[fli] + train.last_frac = train.index - fli - --- 7. do less important stuff --- - --check for any trainpart entities if they have been unloaded. do this only if train is near a player, to not spawn entities into unloaded areas + -- less important stuff train.check_trainpartload=(train.check_trainpartload or 0)-dtime - local node_range=(math.max((minetest.settings:get("active_block_range") or 0),1)*16) if train.check_trainpartload<=0 then - local ori_pos=train_pos --see 3a. - --atprint("[train "..id.."] at "..minetest.pos_to_string(vector.round(ori_pos))) - - local should_check=false - for _,p in ipairs(minetest.get_connected_players()) do - should_check=should_check or (ori_pos and ((vector.distance(ori_pos, p:getpos())", ifn, "<==>", ibo,">", ibn) - - local path=train.path - - if train.enter_node_all then - --field set by create_new_train_at. - --ensures that new train calls enter_node on all nodes - for i=ibn, ifn do - if path[i] then - advtrains.detector.enter_node(path[i], id) - end - end - train.enter_node_all=nil - else - if ifn>ifo then - for i=ifo+1, ifn do - if path[i] then - if advtrains.detector.occupied(path[i], id) then - --if another train has signed up for this position first, it won't be recognized in train_step_b. So do collision here. - atprint("Collision detected in enter_node callbacks (front) @",path[i],"with",sid(advtrains.detector.get(path[i], id))) - advtrains.collide_and_spawn_couple(id, path[i], advtrains.detector.get(path[i], id), false) - end - atprint("enter_node (front) @index",i,"@",path[i],"on_node",sid(advtrains.detector.get(path[i], id))) - advtrains.detector.enter_node(path[i], id) - end - end - elseif ifnibo then - for i=ibo, ibn-1 do - if path[i] then - advtrains.detector.leave_node(path[i], id) - end - end - end - end - --- 8. check for collisions with other trains and damage players --- local train_moves=(train.velocity~=0) if train_moves then - - --TO BE REMOVED: - if not train.extent_h then advtrains.update_trainpart_properties(id) end local collpos local coll_grace=1 - if train.movedir==1 then - collpos=advtrains.get_real_index_position(train.path, train.index-coll_grace) - else - collpos=advtrains.get_real_index_position(train.path, train.end_index+coll_grace) - end + collpos=advtrains.path_get_index_by_offset(train, train.index-coll_grace) if collpos then local rcollpos=advtrains.round_vector_floor_y(collpos) for x=-train.extent_h,train.extent_h do for z=-train.extent_h,train.extent_h do local testpos=vector.add(rcollpos, {x=x, y=0, z=z}) --- 8a Check collision --- - if advtrains.detector.occupied(testpos, id) then + if advtrains.occ.check_collision(testpos, id) then --collides - advtrains.collide_and_spawn_couple(id, testpos, advtrains.detector.get(testpos, id), train.movedir==-1) + --advtrains.collide_and_spawn_couple(id, testpos, advtrains.detector.get(testpos, id), train.movedir==-1) + train.velocity = 0 + train.tarvelocity = 0 + atwarn("Train",id,"collided!") end --- 8b damage players --- if not minetest.settings:get_bool("creative_mode") then @@ -601,37 +501,64 @@ function advtrains.train_step_b(id, train, dtime) end end +--TODO: Collisions! + + --returns new id -function advtrains.create_new_train_at(pos, pos_prev) - local newtrain_id=advtrains.random_id() - while advtrains.trains[newtrain_id] do newtrain_id=advtrains.random_id() end--ensure uniqueness +function advtrains.create_new_train_at(pos, connid, ioff, trainparts) + local new_id=advtrains.random_id() + while advtrains.trains[new_id] do new_id=advtrains.random_id() end--ensure uniqueness + + t={} + t.id = newtrain_id - advtrains.trains[newtrain_id]={} - advtrains.trains[newtrain_id].id = newtrain_id - advtrains.trains[newtrain_id].last_pos=pos - advtrains.trains[newtrain_id].last_pos_prev=pos_prev - advtrains.trains[newtrain_id].tarvelocity=0 - advtrains.trains[newtrain_id].velocity=0 - advtrains.trains[newtrain_id].trainparts={} + t.last_pos=pos + t.last_connid=connid + t.last_frac=ioff - advtrains.trains[newtrain_id].enter_node_all=true + t.tarvelocity=0 + t.velocity=0 + t.trainparts=trainparts + + + advtrains.trains[new_id] = t + + advtrains.update_trainpart_properties(new_id) + + train_ensure_clean(new_id, advtrains.trains[new_id], 0, true, 1) return newtrain_id end - -function advtrains.get_train_end_index(train) - return advtrains.get_real_path_index(train, train.trainlen or 2)--this function can be found inside wagons.lua since it's more related to wagons. we just set trainlen as pos_in_train +function advtrains.remove_train(id) + local train = advtrains.trains[id] + + advtrains.update_trainpart_properties(id) + + train_ensure_clean(id, train, 0, true, 2) + + local tp = train.trainparts + + advtrains.trains[id] = nil + + return tp + end + function advtrains.add_wagon_to_train(wagon_id, train_id, index) local train=advtrains.trains[train_id] + + train_ensure_clean(train_id, train) + if index then table.insert(train.trainparts, index, wagon_id) else table.insert(train.trainparts, wagon_id) end + advtrains.update_trainpart_properties(train_id) + train_recalc_occupation(train_id, train) end -- this function sets wagon's pos_in_train(parts) properties and train's max_speed and drives_on (and more) @@ -685,7 +612,7 @@ end -- This function checks whether entities need to be spawned for certain wagons, and spawns them. function advtrains.spawn_wagons(train_id) - local train=advtrains.trains[train_id] + local train = advtrains.trains[train_id] for i, w_id in ipairs(train.trainparts) do local data = advtrains.wagons[w_id] @@ -696,11 +623,7 @@ function advtrains.spawn_wagons(train_id) local pos = advtrains.path_get(train, atfloor(index)) if minetest.get_node_or_nil(pos) then - local wt = data.type - if not minetest.registered_luaentities[wt] then - atprint("Unable to load",w_id,"of type",wt,", using placeholder") - wt="advtrains:wagon_placeholder" - end + local wt = advtrains.get_wagon_prototype(data) wagon=minetest.add_entity(pos, wt):get_luaentity() wagon:set_id(w_id) end @@ -712,54 +635,55 @@ function advtrains.spawn_wagons(train_id) end -function advtrains.split_train_at_wagon(wagon) +function advtrains.split_train_at_wagon(wagon_id) --get train - local train=advtrains.trains[wagon.train_id] - if not train.path then return end + local data = advtrains.wagons[wagon_id] + local train=advtrains.trains[data.train_id] + local _, wagon = advtrains.get_wagon_prototype(data) + + train_ensure_clean(data.train_id, train) - local real_pos_in_train=advtrains.get_real_path_index(train, wagon.pos_in_train) - local pos_for_new_train=train.path[math.floor(real_pos_in_train+wagon.wagon_span)] - local pos_for_new_train_prev=train.path[math.floor(real_pos_in_train+wagon.wagon_span-1)] + local index=advtrains.path_get_index_by_offset(train, train.index, -(data.pos_in_train + wagon.wagon_span)) + if index < train.path_trk_b or index > train.path_trk_f then + atprint("split_train: pos_for_new_train is off-track") -- TODO function for finding initial positions from a path + return false + end + + local pos, _, frac = advtrains.path_get_adjacent(train, index) + local nconn = train.path_cn[atfloor(index)] --before doing anything, check if both are rails. else do not allow - if not pos_for_new_train then + if not pos then atprint("split_train: pos_for_new_train not set") return false end - local node_ok=advtrains.get_rail_info_at(advtrains.round_vector_floor_y(pos_for_new_train), train.drives_on) + local npos = advtrains.round_vector_floor_y(pos) + local node_ok=advtrains.get_rail_info_at(npos, train.drives_on) if not node_ok then atprint("split_train: pos_for_new_train ",advtrains.round_vector_floor_y(pos_for_new_train_prev)," not loaded or is not a rail") return false end - if not train.last_pos_prev then - atprint("split_train: pos_for_new_train_prev not set") - return false - end - - local prevnode_ok=advtrains.get_rail_info_at(advtrains.round_vector_floor_y(pos_for_new_train), train.drives_on) - if not prevnode_ok then - atprint("split_train: pos_for_new_train_prev ", advtrains.round_vector_floor_y(pos_for_new_train_prev), " not loaded or is not a rail") - return false - end - - --create subtrain - local newtrain_id=advtrains.create_new_train_at(pos_for_new_train, pos_for_new_train_prev) - local newtrain=advtrains.trains[newtrain_id] - --insert all wagons to new train + -- build trainparts table, passing it directly to the train constructor + local tp = {} for k,v in ipairs(train.trainparts) do if k>=wagon.pos_in_trainparts then - table.insert(newtrain.trainparts, v) + table.insert(tp, v) train.trainparts[k]=nil end end + + --create subtrain + local newtrain_id=advtrains.create_new_train_at(npos, nconn, frac, tp) + local newtrain=advtrains.trains[newtrain_id] + --update train parts - advtrains.update_trainpart_properties(wagon.train_id)--atm it still is the desierd id. - advtrains.update_trainpart_properties(newtrain_id) + advtrains.update_trainpart_properties(data.train_id)--atm it still is the desierd id. + train.tarvelocity=0 newtrain.velocity=train.velocity newtrain.tarvelocity=0 - newtrain.enter_node_all=true + newtrain.couple_lck_back=train.couple_lck_back newtrain.couple_lck_front=false train.couple_lck_back=false @@ -777,6 +701,7 @@ end --true when trains are facing each other. needed on colliding. -- check done by iterating paths and checking their direction --returns nil when not on the same track at all OR when required path items are not generated. this distinction may not always be needed. +-- TODO do we need to change this behavior, since direct path accesses are now discouraged? function advtrains.trains_facing(train1, train2) local sr_pos=train1.path[atround(train1.index)] local sr_pos_p=train1.path[atround(train1.index)-1] @@ -922,6 +847,7 @@ function advtrains.do_connect_trains(first_id, second_id, player) return true end +-- TODO function advtrains.invert_train(train_id) local train=advtrains.trains[train_id] diff --git a/advtrains/wagons.lua b/advtrains/wagons.lua index 6abd9b7..83f919e 100644 --- a/advtrains/wagons.lua +++ b/advtrains/wagons.lua @@ -10,7 +10,7 @@ advtrains.wagons = {} -- -function advtrains.create_wagon(type, train_id, owner) +function advtrains.create_wagon(type, owner) local new_id=advtrains.random_id() while advtrains.wagons[new_id] do new_id=advtrains.random_id() end local wgn = {} @@ -18,7 +18,7 @@ function advtrains.create_wagon(type, train_id, owner) wgn.seatp = {} wgn.owner = owner wgn.id = new_id - wgn.train_id = train_id + ---wgn.train_id = train_id --- will get this via update_trainpart_properties advtrains.wagons[new_id] = wgn return new_id end @@ -39,7 +39,8 @@ local wagon={ function wagon:train() - return advtrains.trains[self.train_id] + local data = advtrains.wagons[self.id] + return advtrains.trains[data.train_id] end @@ -272,7 +273,7 @@ function wagon:on_step(dtime) local train=self:train() --show off-track information in outside text instead of notifying the whole server about this - if train.index < train.path_trk_b or train.index > train.path_trk_f then + if not train.dirty and train.end_index < train.path_trk_b or train.index > train.path_trk_f then outside = outside .."\n!!! Train off track !!!" end @@ -1021,10 +1022,18 @@ function wagon:safe_decouple(pname) end atprint("wagon:discouple() Splitting train", self.train_id) advtrains.log("Discouple", pname, self.object:getpos(), self:train().text_outside) - advtrains.split_train_at_wagon(self)--found in trainlogic.lua + advtrains.split_train_at_wagon(self.id)--found in trainlogic.lua return true end +function advtrains.get_wagon_prototype(data) + local wt = data.type + if not minetest.registered_luaentities[wt] then + atprint("Unable to load wagon type",wt,", using placeholder") + wt="advtrains:wagon_placeholder" + end + return wt, minetest.registered_luaentities[wt] +end function advtrains.register_wagon(sysname_p, prototype, desc, inv_img, nincreative) local sysname = sysname_p @@ -1072,11 +1081,10 @@ function advtrains.register_wagon(sysname_p, prototype, desc, inv_img, nincreati minetest.chat_send_player(pname, "The track you are trying to place the wagon on is not long enough!") return end - local id=advtrains.create_new_train_at(pointed_thing.under, plconnid) - local wid = advtrains.create_wagon(type, id, pname) + local wid = advtrains.create_wagon(type, pname) - advtrains.add_wagon_to_train(wid, id) + local id=advtrains.create_new_train_at(pointed_thing.under, plconnid, 0, {wid}) if not advtrains.is_creative(pname) then itemstack:take_item() -- cgit v1.2.3