aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--advtrains/atcjit.lua53
-rw-r--r--advtrains/couple.lua448
-rw-r--r--advtrains/init.lua16
-rw-r--r--advtrains/path.lua39
-rw-r--r--advtrains/settingtypes.txt5
-rw-r--r--advtrains/spec/atcjit_spec.lua (renamed from advtrains/tests/atcjit_spec.lua)39
-rw-r--r--advtrains/trainlogic.lua428
-rw-r--r--advtrains/wagons.lua64
8 files changed, 651 insertions, 441 deletions
diff --git a/advtrains/atcjit.lua b/advtrains/atcjit.lua
index 708d035..366a442 100644
--- a/advtrains/atcjit.lua
+++ b/advtrains/atcjit.lua
@@ -1,7 +1,26 @@
local aj_cache = {}
local aj_strout = {}
-local aj_tostring
+local aj_tostring, aj_execute
+
+local rwt, sched
+
+minetest.register_on_mods_loaded(function()
+ if not advtrains.lines then return end
+ rwt = advtrains.lines.rwt
+ if not rwt then return end
+ sched = advtrains.lines.sched
+ if not sched then return end
+ sched.register_callback("atcjit", function(d)
+ local id, cmd = d.trainid, d.cmd
+ if not (id and cmd) then return end
+ local train = advtrains.trains[id]
+ if not train then return end
+ train.atc_arrow = d.arrow or false
+ train.atc_command = cmd
+ aj_execute(id, train)
+ end)
+end)
--[[ Notes on the pattern matching functions:
- Patterns can have multiple captures (e.g. coordinates). These captures
@@ -42,6 +61,36 @@ local matchptn = {
return
end]], match, cont), true
end,
+ ["Ds([0-9]+)%+([0-9]+)"] = function(cont, int, off)
+ if sched then
+ return string.format([[do
+ local rwt = advtrains.lines.rwt
+ local tnext = rwt.next_rpt(rwt.now(),%s,%s)
+ local edata = {trainid = train.id, cmd = %q, arrow = train.atc_arrow}
+ advtrains.lines.sched.enqueue(tnext,"atcjit",edata,"atcjit-"..(train.id),1)
+ end]], int, off, cont), true
+ else
+ return string.format([[do
+ train.atc_delay = %s-(os.time()-%s)%%%s
+ train.atc_command = %q
+ return
+ end]], int, off, int, cont), true
+ end
+ end,
+ ["Ds%+([0-9]+)"] = function(cont, delta)
+ if sched then
+ return string.format([[do
+ local edata = {trainid = train.id, cmd = %q, arrow = train.atc_arrow}
+ advtrains.lines.sched.enqueue_in(%s,"atcjit",edata,"atcjit-"..(train.id),1)
+ end]], cont, delta), true
+ else
+ return string.format([[do
+ train.atc_delay = %s
+ train.atc_command = %q
+ return
+ end]], delta, cont), true
+ end
+ end,
["(%bI;)"] = function(cont, match)
local i = 2
local l = #match
@@ -214,7 +263,7 @@ local function aj_compile(cmd)
end
end
-local function aj_execute(id,train)
+aj_execute = function(id,train)
if not train.atc_command then return end
local func, err = aj_compile(train.atc_command)
if func then return func(id,train) end
diff --git a/advtrains/couple.lua b/advtrains/couple.lua
index 3dc336f..336a6d4 100644
--- a/advtrains/couple.lua
+++ b/advtrains/couple.lua
@@ -1,14 +1,290 @@
--couple.lua
---defines couple entities.
+--Handles coupling and discoupling of trains, and defines the coupling entities
+--Rework June 2021 - some functions from trainlogic.lua have been moved here
---advtrains:discouple
---set into existing trains to split them when punched.
---they are attached to the wagons.
---[[fields
-wagon
+-- COUPLING --
+-- During coupling rework, the behavior of coupling was changed to make automation easier. It is now as follows:
+-- Coupling is only ever initiated when a train is standing somewhere (not moving) and another train drives onto one of its ends
+-- with a speed greater than 0
+-- "stationary" train is the one standing there - in old code called "train2"
+-- "initiating" train is the one that approached it and bumped into it - typically an engine - in old code called "train1"
+-- When the initiating train has autocouple set, trains are immediately coupled
+-- When not, a couple entity is spawned and coupling commences on click
+-- Coupling MUST preserve the train ID of the initiating train, so it is done like this:
+ -- initiating train is reversed
+ -- stationary train is reversed if required, so that it points towards the initiating train
+ -- do_connect_trains(initiating, stationary)
+-- As a result, the coupled train is reversed in direction. Alternative way of doing things (might be considered later):
+ -- stationary train is reversed if required, so that it points away from the initiating train
+ -- index of initiating train is set so that it matches the front pos of stationary train
+ -- wagons of stationary train are inserted at the beginning of initiating train
+ -- remove stationary train
-wagons keep their couple entity minetest-internal id inside the field discouple_id. if it refers to nowhere, they will spawn a new one if player is near
-]]
+-- train.couple_* contain references to ObjectRefs of couple objects, which contain all relevant information
+-- These objectRefs will delete themselves once the couples no longer match (see below)
+local function create_couple_entity(pos, train1, t1_is_front, train2, t2_is_front)
+ local id1 = train1.id
+ local id2 = train2.id
+
+ -- delete previous couple entities
+ if t1_is_front then
+ if train1.cpl_front then train1.cpl_front:remove() end
+ else
+ if train1.cpl_back then train1.cpl_back:remove() end
+ end
+ if t2_is_front then
+ if train2.cpl_front then train2.cpl_front:remove() end
+ else
+ if train2.cpl_back then train2.cpl_back:remove() end
+ end
+
+ local obj=minetest.add_entity(pos, "advtrains:couple")
+ if not obj then error("Failed creating couple object!") return end
+ local le=obj:get_luaentity()
+ le.train_id_1=id1
+ le.t1_is_front=t1_is_front
+ le.train_id_2=id2
+ le.t2_is_front=t2_is_front
+ --atdebug("created couple between",train1.id,train2.id,t2_is_front)
+
+ if t1_is_front then
+ train1.cpl_front = obj
+ else
+ train2.cpl_back = obj
+ end
+ if t2_is_front then
+ train2.cpl_front = obj
+ else
+ train2.cpl_back = obj
+ end
+end
+
+-- Old static couple checking. Never used for autocouple, only used for standing trains if train did not approach
+local CPL_CHK_DST = -1
+local CPL_ZONE = 2
+function advtrains.train_check_couples(train)
+ --atdebug("rechecking couples")
+ if train.cpl_front then
+ if not train.cpl_front:get_yaw() then
+ -- objectref is no longer valid. reset.
+ train.cpl_front = nil
+ end
+ end
+ if not train.cpl_front then
+ -- recheck front couple
+ local front_trains, pos = advtrains.occ.get_occupations(train, atround(train.index) + CPL_CHK_DST)
+ if advtrains.is_node_loaded(pos) then -- if the position is loaded...
+ for tid, idx in pairs(front_trains) do
+ local other_train = advtrains.trains[tid]
+ if not advtrains.train_ensure_init(tid, other_train) then
+ atwarn("Train",tid,"is not initialized! Couldn't check couples!")
+ return
+ end
+ --atdebug(train.id,"front: ",idx,"on",tid,atround(other_train.index),atround(other_train.end_index))
+ if other_train.velocity == 0 then
+ if idx>=other_train.index and idx<=other_train.index + CPL_ZONE then
+ create_couple_entity(pos, train, true, other_train, true)
+ break
+ end
+ if idx<=other_train.end_index and idx>=other_train.end_index - CPL_ZONE then
+ create_couple_entity(pos, train, true, other_train, false)
+ break
+ end
+ end
+ end
+ end
+ end
+ if train.cpl_back then
+ if not train.cpl_back:get_yaw() then
+ -- objectref is no longer valid. reset.
+ train.cpl_back = nil
+ end
+ end
+ if not train.cpl_back then
+ -- recheck back couple
+ local back_trains, pos = advtrains.occ.get_occupations(train, atround(train.end_index) - CPL_CHK_DST)
+ if advtrains.is_node_loaded(pos) then -- if the position is loaded...
+ for tid, idx in pairs(back_trains) do
+ local other_train = advtrains.trains[tid]
+ if not advtrains.train_ensure_init(tid, other_train) then
+ atwarn("Train",tid,"is not initialized! Couldn't check couples!")
+ return
+ end
+ --atdebug(train.id,"back: ",idx,"on",tid,atround(other_train.index),atround(other_train.end_index))
+ if other_train.velocity == 0 then
+ if idx>=other_train.index and idx<=other_train.index + CPL_ZONE then
+ create_couple_entity(pos, train, false, other_train, true)
+ break
+ end
+ if idx<=other_train.end_index and idx>=other_train.end_index - CPL_ZONE then
+ create_couple_entity(pos, train, false, other_train, false)
+ break
+ end
+ end
+ end
+ end
+ end
+end
+
+-- Deletes couple entities from the train
+function advtrains.couple_invalidate(train)
+ if train.cpl_back then
+ train.cpl_back:remove()
+ train.cpl_back = nil
+ end
+ if train.cpl_front then
+ train.cpl_front:remove()
+ train.cpl_front = nil
+ end
+ train.couples_up_to_date = nil
+end
+
+-- Called from train_step_b() when the current train (init_train) just stopped at one of the end indices of another train (stat_train)
+-- Depending on autocouple, either couples immediately or spawns a couple entity
+function advtrains.couple_initiate_with(init_train, stat_train, stat_is_front)
+ --atdebug("Initiating couplign between init=",init_train.id,"stat=",stat_train.id,"backside=",stat_is_backside)
+ if init_train.autocouple then
+ advtrains.couple_trains(init_train, true, stat_train, stat_is_front)
+ else
+ local pos = advtrains.path_get_interpolated(init_train, init_train.index)
+ create_couple_entity(pos, init_train, true, stat_train, stat_is_front)
+ end
+
+end
+
+-- check if the player has permission for the first/last wagon of the train
+local function check_twagon_owner(train, b_first, pname)
+ local wtp = b_first and 1 or #train.trainparts
+ local wid = train.trainparts[wtp]
+ local wdata = advtrains.wagons[wid]
+ if wdata then
+ return advtrains.check_driving_couple_protection(pname, wdata.owner, wdata.whitelist)
+ end
+ return false
+end
+
+-- Perform coupling, but check if the player is authorized to couple
+function advtrains.safe_couple_trains(train1, t1_is_front, train2, t2_is_front, pname)
+
+ if pname and not minetest.check_player_privs(pname, "train_operator") then
+ minetest.chat_send_player(pname, "Missing train_operator privilege")
+ return false
+ end
+
+ local wck_t1, wck_t2
+ if pname then
+ wck_t1 = check_twagon_owner(train1, t1_is_front, pname)
+ wck_t2 = check_twagon_owner(train2, t2_is_front, pname)
+ end
+ if (wck_t1 or wck_t2) or not pname then
+ advtrains.couple_trains(train1, t1_is_front, train2, t2_is_front)
+ end
+end
+
+-- Actually performs the train coupling. Always retains train ID of train1
+function advtrains.couple_trains(train1, t1_is_front, train2, t2_is_front)
+ --atdebug("Couple trains init=",init_train.id,"stat=",stat_train.id,"statreverse=",stat_must_reverse)
+ -- see comment on top of file
+ if t1_is_front then
+ advtrains.invert_train(train1.id)
+ end
+ if not t2_is_front then
+ advtrains.invert_train(train2.id)
+ end
+
+ advtrains.do_connect_trains(train1, train2)
+end
+
+-- Adds the wagons of first to second and deletes second_id afterwards
+-- Assumes that second_id stands right behind first_id and both trains point to the same direction
+function advtrains.do_connect_trains(first, second)
+
+ if not advtrains.train_ensure_init(first.id, first) then
+ atwarn("Coupling: first train",first.id,"is not initialized! Operation aborted!")
+ return
+ end
+ if not advtrains.train_ensure_init(second.id, second) then
+ atwarn("Coupling: second train",second.id,"is not initialized! Operation aborted!")
+ return
+ end
+
+ local first_wagoncnt=#first.trainparts
+ local second_wagoncnt=#second.trainparts
+
+ for _,v in ipairs(second.trainparts) do
+ table.insert(first.trainparts, v)
+ end
+
+ advtrains.remove_train(second.id)
+
+ first.velocity = 0
+
+ advtrains.update_trainpart_properties(first.id)
+ advtrains.couple_invalidate(first)
+ return true
+end
+
+
+
+-- DECOUPLING --
+function advtrains.split_train_at_fc(train, count_empty, length_limit)
+ -- splits train at first different current FC by convention,
+ -- locomotives have empty FC so are ignored
+ -- count_empty is used to split off locomotives
+ -- length_limit limits the length of the first train to length_limit wagons
+ local train_id = train.id
+ local fc = false
+ local ind = 0
+ for i = 1, #train.trainparts do
+ local w_id = train.trainparts[i]
+ local data = advtrains.wagons[w_id]
+ if length_limit and i > length_limit then
+ ind = i
+ break
+ end
+ if data then
+ local wfc = advtrains.get_cur_fc(data)
+ if wfc ~= "" or count_empty then
+ if fc then
+ if fc ~= wfc then
+ ind = i
+ break
+ end
+ else
+ fc = wfc
+ end
+ end
+ end
+ end
+ if ind > 0 then
+ return advtrains.split_train_at_index(train, ind), fc
+ end
+ if fc then
+ return nil, fc
+ end
+end
+
+function advtrains.train_step_fc(train)
+ for i=1,#train.trainparts do
+ local w_id = train.trainparts[i]
+ local data = advtrains.wagons[w_id]
+ if data then
+ advtrains.step_fc(data)
+ end
+ end
+end
+
+
+-- split_train_at_index() is in trainlogic.lua because it needs access to two local functions
+
+function advtrains.split_train_at_wagon(wagon_id)
+ --get train
+ local data = advtrains.wagons[wagon_id]
+ advtrains.split_train_at_index(advtrains.trains[data.train_id], data.pos_in_trainparts)
+end
+
+
+-- COUPLE ENTITIES --
local couple_max_dist=3
@@ -36,8 +312,6 @@ minetest.register_entity("advtrains:discouple", {
if pname and pname~="" and self.wagon then
if advtrains.safe_decouple_wagon(self.wagon.id, pname) then
self.object:remove()
- else
- minetest.add_entity(self.object:getpos(), "advtrains:lockmarker")
end
end
end,
@@ -60,10 +334,6 @@ minetest.register_entity("advtrains:discouple", {
-- advtrains:couple
-- Couple entity
-local function lockmarker(obj)
- minetest.add_entity(obj:get_pos(), "advtrains:lockmarker")
- obj:remove()
-end
minetest.register_entity("advtrains:couple", {
visual="sprite",
@@ -75,107 +345,71 @@ minetest.register_entity("advtrains:couple", {
is_couple=true,
static_save = false,
on_activate=function(self, staticdata)
- if staticdata=="COUPLE" then
- --couple entities have no right to exist further...
- atprint("Couple loaded from staticdata, destroying")
- self.object:remove()
- return
- end
- self.object:set_armor_groups({immmortal=1})
+ if staticdata=="COUPLE" then
+ --couple entities have no right to exist further...
+ --atdebug("Couple loaded from staticdata, destroying")
+ self.object:remove()
+ return
+ end
+ self.object:set_armor_groups({immmortal=1})
end,
get_staticdata=function(self) return "COUPLE" end,
on_rightclick=function(self, clicker)
- if not self.train_id_1 or not self.train_id_2 then return end
-
- local pname=clicker
- if type(clicker)~="string" then pname=clicker:get_player_name() end
-
- if advtrains.safe_couple_trains(self.train_id_1, self.train_id_2, self.t1_is_front, self.t2_is_front, pname) then
- self.object:remove()
- else
- lockmarker(self.object)
- end
+ if not self.train_id_1 or not self.train_id_2 then return end
+
+ local pname=clicker
+ if type(clicker)~="string" then pname=clicker:get_player_name() end
+
+ local train1=advtrains.trains[self.train_id_1]
+ local train2=advtrains.trains[self.train_id_2]
+
+ advtrains.safe_couple_trains(train1, self.t1_is_front, train2, self.t2_is_front, pname)
+ self.object:remove()
end,
on_step=function(self, dtime)
- if advtrains.wagon_outside_range(self.object:getpos()) then
- self.object:remove()
- return
- end
+ if advtrains.wagon_outside_range(self.object:getpos()) then
+ --atdebug("Couple Removing outside range")
+ self.object:remove()
+ return
+ end
- if not self.train_id_1 or not self.train_id_2 then atprint("Couple: train ids not set!") self.object:remove() return end
- local train1=advtrains.trains[self.train_id_1]
- local train2=advtrains.trains[self.train_id_2]
- if not train1 or not train2 then
- atprint("Couple: trains missing, destroying")
- self.object:remove()
- return
- end
-
- --shh, silence here, this is an on-step callback!
- if not advtrains.train_ensure_init(self.train_id_1, train1) then
- --atwarn("Train",self.train_id_1,"is not initialized! Operation aborted!")
- return
- end
- if not advtrains.train_ensure_init(self.train_id_2, train2) then
- --atwarn("Train",self.train_id_2,"is not initialized! Operation aborted!")
- return
- end
-
- if train1.velocity>0 or train2.velocity>0 then
- if not self.position_set then --ensures that train stands a single time before check fires. Using flag below
- return
- end
- atprint("Couple: train is moving, destroying")
- self.object:remove()
- return
+ if not self.train_id_1 or not self.train_id_2 then
+ --atdebug("Couple Removing ids missing")
+ self.object:remove()
+ return
+ end
+ local train1=advtrains.trains[self.train_id_1]
+ local train2=advtrains.trains[self.train_id_2]
+ if not train1 or not train2 then
+ --atdebug("Couple Removing trains missing")
+ self.object:remove()
+ return
+ end
+
+ if self.position_set and train1.velocity>0 or train2.velocity>0 then
+ --atdebug("Couple: train is moving, destroying")
+ self.object:remove()
+ return
+ end
+
+ if not self.position_set then
+ local tp1
+ if self.t1_is_front then
+ tp1=advtrains.path_get_interpolated(train1, train1.index)
+ else
+ tp1=advtrains.path_get_interpolated(train1, train1.end_index)
end
-
- if not self.position_set then
- local tp1
- if self.t1_is_front then
- tp1=advtrains.path_get_interpolated(train1, train1.index)
- else
- tp1=advtrains.path_get_interpolated(train1, train1.end_index)
- end
- local tp2
- if self.t2_is_front then
- tp2=advtrains.path_get_interpolated(train2, train2.index)
- else
- tp2=advtrains.path_get_interpolated(train2, train2.end_index)
- end
- local pos_median=advtrains.pos_median(tp1, tp2)
- if not vector.equals(pos_median, self.object:getpos()) then
- self.object:set_pos(pos_median)
- end
- self.position_set=true
+ local tp2
+ if self.t2_is_front then
+ tp2=advtrains.path_get_interpolated(train2, train2.index)
+ else
+ tp2=advtrains.path_get_interpolated(train2, train2.end_index)
end
- advtrains.atprint_context_tid=nil
- end,
-})
-minetest.register_entity("advtrains:lockmarker", {
- visual="sprite",
- textures = {"advtrains_cpl_lock.png"},
- collisionbox = {-0.3,-0.3,-0.3, 0.3,0.3,0.3},
- visual_size = {x=0.7, y=0.7},
- initial_sprite_basepos = {x=0, y=0},
-
- is_lockmarker=true,
- static_save = false,
- on_activate=function(self, staticdata)
- if staticdata=="COUPLE" then
- --couple entities have no right to exist further...
- atprint("Couple loaded from staticdata, destroying")
- self.object:remove()
- return
+ local pos_median=advtrains.pos_median(tp1, tp2)
+ if not vector.equals(pos_median, self.object:getpos()) then
+ self.object:set_pos(pos_median)
end
- self.object:set_armor_groups({immmortal=1})
- self.life=5
- end,
- get_staticdata=function(self) return "COUPLE" end,
- on_step=function(self, dtime)
- self.life=(self.life or 5)-dtime
- if self.life<0 then
- self.object:remove()
+ self.position_set=true
end
end,
-})
+})
diff --git a/advtrains/init.lua b/advtrains/init.lua
index b7ba08f..927d9e8 100644
--- a/advtrains/init.lua
+++ b/advtrains/init.lua
@@ -571,11 +571,13 @@ advtrains.mainloop_runcnt=0
advtrains.global_slowdown = 1
local t = 0
+local within_mainstep = false
minetest.register_globalstep(function(dtime_mt)
if no_action then
-- the advtrains globalstep is skipped by command. Return immediately
return
end
+ within_mainstep = true
advtrains.mainloop_runcnt=advtrains.mainloop_runcnt+1
--atprint("Running the main loop, runcnt",advtrains.mainloop_runcnt)
@@ -588,6 +590,7 @@ minetest.register_globalstep(function(dtime_mt)
if GENERATE_ATRICIFIAL_LAG then
dtime = HOW_MANY_LAG
if os.clock()<t then
+ within_mainstep = false
return
end
@@ -618,7 +621,7 @@ minetest.register_globalstep(function(dtime_mt)
if advtrains.lines then
advtrains.lines.step(dtime)
end
-
+
--trigger a save when necessary
save_timer=save_timer-dtime
if save_timer<=0 then
@@ -628,6 +631,9 @@ minetest.register_globalstep(function(dtime_mt)
save_timer = advtrains.SAVE_INTERVAL
atprintbm("saving", t)
end
+
+ within_mainstep = false
+
end)
--if something goes wrong in these functions, there is no help. no pcall here.
@@ -680,7 +686,13 @@ function advtrains.save(remove_players_from_wagons)
--TODO very simple yet hacky workaround for the "green signals" bug
advtrains.invalidate_all_paths()
end
-minetest.register_on_shutdown(advtrains.save)
+minetest.register_on_shutdown(function()
+ if within_mainstep then
+ atwarn("Crash during advtrains main step - skipping the shutdown save operation to not save inconsistent data!")
+ else
+ advtrains.save()
+ end
+end)
-- This chat command provides a solution to the problem known on the LinuxWorks server
-- There are many players that joined a single time, got on a train and then left forever
diff --git a/advtrains/path.lua b/advtrains/path.lua
index 714781a..f2b8a13 100644
--- a/advtrains/path.lua
+++ b/advtrains/path.lua
@@ -417,3 +417,42 @@ function advtrains.path_lookup(train, pos)
end
return nil
end
+
+-- Projects the path of "train" onto the path of "onto_train_id", and returns the index on onto_train's path
+-- that corresponds to "index" on "train"'s path, as well as whether both trains face each other
+-- index may be fractional
+-- returns: res_index, trains_facing
+-- returns nil when path can not be projected, either because trains are on different tracks or
+-- node at "index" happens to be on a turnout and it's the wrong direction
+-- Note - duplicate with similar functionality is in train_step_b() - that code combines train detection with projecting
+function advtrains.path_project(train, index, onto_train_id)
+ local base_idx = atfloor(index)
+ local frac_part = index - base_idx
+ local base_pos = advtrains.path_get(train, base_idx)
+ local base_cn = train.path_cn[base_idx]
+ local otrn = advtrains.trains[onto_train_id]
+ -- query occupation
+ local occ = advtrains.occ.get_trains_over(base_pos)
+ -- is wanted train id contained?
+ local ob_idx = occ[onto_train_id]
+ if not ob_idx then
+ return nil
+ end
+
+ -- retrieve other train's cn and cp
+ local ocn = otrn.path_cn[ob_idx]
+ local ocp = otrn.path_cp[ob_idx]
+
+ if base_cn == ocn then
+ -- same direction
+ return ob_idx + frac_part, false
+ elseif base_cn == ocp then
+ -- facing trains - subtract index frac
+ return ob_idx - frac_part, true
+ else
+ -- same path item but no common connections - deny
+ return nil
+ end
+end
+
+
diff --git a/advtrains/settingtypes.txt b/advtrains/settingtypes.txt
index 6acff80..2b627cb 100644
--- a/advtrains/settingtypes.txt
+++ b/advtrains/settingtypes.txt
@@ -56,3 +56,8 @@ advtrains_dtime_limit (DTime Limit for slow-down) float 0.2 0 5
# Time interval in seconds in which advtrains stores its save data to disk
# Nevertheless, advtrains saves all data when shutting down the server.
advtrains_save_interval (Save Interval) int 60 20 3600
+
+# Enable forgiving collision mode
+# If enabled, trains only collide with nodes with "normal" drawtype.
+advtrains_forgiving_collision (Forgiving Collision mode) bool false
+
diff --git a/advtrains/tests/atcjit_spec.lua b/advtrains/spec/atcjit_spec.lua
index 638e5c8..62896e4 100644
--- a/advtrains/tests/atcjit_spec.lua
+++ b/advtrains/spec/atcjit_spec.lua
@@ -1,10 +1,17 @@
package.path = "../?.lua;" .. package.path
advtrains = {}
+minetest = {}
_G.advtrains = advtrains
+_G.minetest = minetest
function _G.attrans(...) return ... end
function advtrains.invert_train() end
function advtrains.train_ensure_init() end
+local on_mods_loaded = function() end
+function minetest.register_on_mods_loaded(f)
+ on_mods_loaded = f
+end
+
local atcjit = require("atcjit")
local function assert_atc(train, warn, err, res)
@@ -176,4 +183,36 @@ describe("ATC track that sets ARS modes", function()
thisatc("should enable ARS on the train with A1", t, {}, nil, {atc_wait_finish=true, ars_disable=false, atc_command="AFWAT"})
thisatc("should disable ARS on the train with AF", t, {}, nil, {atc_wait_finish=true, ars_disable=true, atc_command="AT"})
thisatc("should enable ARS on the train with AT", t, {}, nil, {atc_wait_finish=true, ars_disable=false,})
+end)
+
+insulate("ATC scheduling commands without line automation", function()
+ _G.os.time = function() return 12 end
+ local t = {atc_command = "Ds+5Ds20+10W"}
+ thisatc("should do the same as D5", t, {}, nil, {atc_delay=5, atc_command="Ds20+10W"})
+ thisatc("should do the same as D15", t, {}, nil, {atc_delay=18, atc_command="W"})
+end)
+
+insulate("ATC scheduling commands with line automation", function()
+ advtrains.lines = {
+ rwt = mock{
+ now = function() return 12 end,
+ next_rpt = function(n, i, o) return n+i-(n-o)%i end,
+ },
+ sched = mock{
+ enqueue = function() end,
+ enqueue_in = function() end,
+ register_callback = function() end,
+ },
+ }
+ local rwt, sched = advtrains.lines.rwt, advtrains.lines.sched
+ on_mods_loaded()
+ it("should have line automation modules loaded", function() assert.stub(sched.register_callback).was.called() end)
+ it("should schedule the train in 0;05", function()
+ assert_atc({id="foo", atc_command="Ds+5W", atc_arrow = true}, {}, nil, {id="foo", atc_arrow=true})
+ assert.stub(sched.enqueue_in).was.called_with(5, "atcjit", {trainid="foo", cmd="W", arrow=true}, "atcjit-foo", 1)
+ end)
+ it("should schedule the train at 0;25", function()
+ assert_atc({id="bar", atc_command="Ds20+5W", atc_arrow = true}, {}, nil, {id="bar", atc_arrow=true})
+ assert.stub(sched.enqueue).was.called_with(25, "atcjit", {trainid="bar", cmd="W", arrow=true}, "atcjit-bar", 1)
+ end)
end) \ No newline at end of file
diff --git a/advtrains/trainlogic.lua b/advtrains/trainlogic.lua
index 187e5ba..00c04bf 100644
--- a/advtrains/trainlogic.lua
+++ b/advtrains/trainlogic.lua
@@ -139,6 +139,7 @@ minetest.register_on_joinplayer(function(player)
advtrains.hhud[player:get_player_name()] = nil
--independent of this, cause all wagons of the train which are loaded to reattach their players
--needed because already loaded wagons won't call reattach_all()
+ local pname = player:get_player_name()
local id=advtrains.player_to_train_mapping[pname]
if id then
for _,wagon in pairs(minetest.luaentities) do
@@ -394,7 +395,7 @@ function advtrains.train_step_b(id, train, dtime)
local back_off_track=train.end_index<train.path_trk_b
train.off_track = front_off_track or back_off_track
- if back_off_track and (not v_cap or v_cap > 1) then
+ if back_off_track and (not sit_v_cap or sit_v_cap > 1) then
--atprint("in train_step_b: applying back_off_track")
sit_v_cap = 1
elseif front_off_track then
@@ -587,12 +588,76 @@ function advtrains.train_step_b(id, train, dtime)
else
--atprint("in train_step_b: movement calculation reusing from LZB newindex=",new_index_curr_tv)
end
-
+
-- if the zeroappr mechanism has hit, go no further than zeroappr index
if lzb_next_zero_barrier and new_index_curr_tv > lzb_next_zero_barrier then
--atprint("in train_step_b: Zero barrier hit, clipping to newidx_tv=",new_index_curr_tv, "zb_idx=",lzb_next_zero_barrier)
new_index_curr_tv = lzb_next_zero_barrier
end
+
+ -- New same-track collision system - check for any other trains within the range we're going to move
+ -- do the checks if we either are moving or about to start moving
+ if new_index_curr_tv > train.index or accelerating then -- only if train is actually advancing
+ -- Note: duplicate code from path_project() because of subtle differences: no frac processing and scanning all occupations
+ --[[train.debug = ""
+ local atdebug = function(t, ...)
+ local text=advtrains.print_concat_table({t, ...})
+ train.debug = train.debug..text.."\n"
+ end]]
+ local base_idx = atfloor(new_index_curr_tv + 1)
+ local base_pos = advtrains.path_get(train, base_idx)
+ local base_cn = train.path_cn[base_idx]
+ --atdebug(id,"Begin Checking for on-track collisions new_idx=",new_index_curr_tv,"base_idx=",base_idx,"base_pos=",base_pos,"base_cn=",base_cn)
+ -- query occupation
+ local occ = advtrains.occ.get_trains_over(base_pos)
+ -- iterate other trains
+ for otid, ob_idx in pairs(occ) do
+ if otid ~= id then
+ --atdebug(id,"Found other train",otid," with matching index ",ob_idx)
+ -- Phase 1 - determine if trains are facing and which is the relefant stpo index
+ local otrn = advtrains.trains[otid]
+
+ -- retrieve other train's cn and cp
+ local ocn = otrn.path_cn[ob_idx]
+ local ocp = otrn.path_cp[ob_idx]
+
+ local target_is_inside, ref_index, facing
+
+ if base_cn == ocn then
+ -- same direction
+ ref_index = otrn.end_index
+ same_dir = true
+ target_is_inside = (ob_idx >= ref_index)
+ --atdebug("Same direction: ref_index",ref_index,"inside=",target_is_inside)
+ elseif base_cn == ocp then
+ -- facing trains - subtract index frac
+ ref_index = otrn.index
+ same_dir = false
+ target_is_inside = (ob_idx <= ref_index)
+ --atdebug("Facing direction: ref_index",ref_index,"inside=",target_is_inside)
+ end
+
+ -- Phase 2 - project ref_index back onto our path and check again (necessary because there might be a turnout on the way and we are driving into the flank
+ if target_is_inside then
+ local our_index = advtrains.path_project(otrn, ref_index, id)
+ --atdebug("Backprojected our_index",our_index)
+ if our_index and our_index <= new_index_curr_tv then
+ -- ON_TRACK COLLISION IS HAPPENING
+ -- the actual collision is handled in train_step_c, so set appropriate signal variables
+ train.ontrack_collision_info = {
+ otid = otid,
+ same_dir = same_dir,
+ }
+ -- clip newindex
+ --atdebug("-- Collision detected!")
+ new_index_curr_tv = our_index
+ end
+ end
+ end
+ end
+ end
+
+ -- ## Movement happens here ##
train.index = new_index_curr_tv
recalc_end_index(train)
@@ -638,21 +703,42 @@ function advtrains.train_step_c(id, train, dtime)
advtrains.spawn_wagons(id)
train.check_trainpartload=2
end
-
- --- 8. check for collisions with other trains and damage players ---
-
+
local train_moves=(train.velocity~=0)
-
- --- Check whether this train can be coupled to another, and set couple entities accordingly
- if not train.was_standing and not train_moves then
- advtrains.train_check_couples(train)
+ local very_short_train = train.trainlen < 3
+
+ --- On-track collision handling - detected in train_step_b, but handled here so all other train movements have already happened.
+ if train.ontrack_collision_info then
+ train.velocity = 0
+ train.acceleration = 0
+ advtrains.atc.train_reset_command(train)
+
+ local otrn = advtrains.trains[train.ontrack_collision_info.otid]
+
+ if otrn.velocity == 0 then -- other train must be standing, else don't initiate coupling
+ advtrains.couple_initiate_with(train, otrn, not train.ontrack_collision_info.same_dir)
+ end
+
+ train.ontrack_collision_info = nil
+ train.couples_up_to_date = true
end
- train.was_standing = not train_moves
-
+
+ -- handle couples if on_track collision handling did not fire
if train_moves then
-
+ train.couples_up_to_date = nil
+ elseif not train.couples_up_to_date then
+ if not very_short_train then -- old coupling system is buggy for short trains
+ advtrains.train_check_couples(train) -- no guarantee for train order here
+ end
+ train.couples_up_to_date = true
+ end
+
+ --- 8. check for collisions with other trains and damage players ---
+ if train_moves then
+ -- Note: this code handles collisions with trains that are not on the same path as the current train
+ -- The same-track collisions and coupling handling is found in couple.lua and handled from train_step_b() and code 2 blocks above.
local collided = false
- local coll_grace=1
+ local coll_grace=2
local collindex = advtrains.path_get_index_by_offset(train, train.index, -coll_grace)
local collpos = advtrains.path_get(train, atround(collindex))
if collpos then
@@ -663,13 +749,14 @@ function advtrains.train_step_c(id, train, dtime)
local testpos=vector.add(rcollpos, {x=x, y=0, z=z})
--- 8a Check collision ---
if not collided then
-
- local col_tr = advtrains.occ.check_collision(testpos, id)
- if col_tr then
- advtrains.train_check_couples(train)
- train.velocity = 0
- advtrains.atc.train_reset_command(train)
- collided = true
+ if not very_short_train then -- position collision system is buggy for short trains
+ local col_tr = advtrains.occ.check_collision(testpos, id)
+ if col_tr then
+ train.velocity = 0
+ train.acceleration = 0
+ advtrains.atc.train_reset_command(train)
+ collided = true
+ end
end
--- 8b damage players ---
@@ -703,7 +790,7 @@ function advtrains.train_step_c(id, train, dtime)
local objs = minetest.get_objects_inside_radius(rcollpos, 2)
for _,obj in ipairs(objs) do
if not obj:is_player() and obj:get_armor_groups().fleshy and obj:get_armor_groups().fleshy > 0
- and obj:get_luaentity() and obj:get_luaentity().name~="signs:text" then
+ and obj:get_luaentity() and obj:get_luaentity().name~="signs_lib:text" then
obj:punch(obj, 1, { full_punch_interval = 1.0, damage_groups = {fleshy = 1000}, }, nil)
end
end
@@ -899,7 +986,7 @@ function advtrains.remove_train(id)
run_callbacks_remove(id, train)
- advtrains.path_invalidate(train)
+ advtrains.path_invalidate(train, true)
advtrains.couple_invalidate(train)
local tp = train.trainparts
@@ -1019,53 +1106,6 @@ function advtrains.spawn_wagons(train_id)
end
end
-function advtrains.split_train_at_fc(train, count_empty, length_limit)
- -- splits train at first different current FC by convention,
- -- locomotives have empty FC so are ignored
- -- count_empty is used to split off locomotives
- -- length_limit limits the length of the first train to length_limit wagons
- local train_id = train.id
- local fc = false
- local ind = 0
- for i = 1, #train.trainparts do
- local w_id = train.trainparts[i]
- local data = advtrains.wagons[w_id]
- if length_limit and i > length_limit then
- ind = i
- break
- end
- if data then
- local wfc = advtrains.get_cur_fc(data)
- if wfc ~= "" or count_empty then
- if fc then
- if fc ~= wfc then
- ind = i
- break
- end
- else
- fc = wfc
- end
- end
- end
- end
- if ind > 0 then
- return advtrains.split_train_at_index(train, ind), fc
- end
- if fc then
- return nil, fc
- end
-end
-
-function advtrains.train_step_fc(train)
- for i=1,#train.trainparts do
- local w_id = train.trainparts[i]
- local data = advtrains.wagons[w_id]
- if data then
- advtrains.step_fc(data)
- end
- end
-end
-
function advtrains.split_train_at_index(train, index)
-- this function splits a train at index, creating a new train from the back part of the train.
@@ -1121,167 +1161,6 @@ function advtrains.split_train_at_index(train, index)
end
-function advtrains.split_train_at_wagon(wagon_id)
- --get train
- local data = advtrains.wagons[wagon_id]
- advtrains.split_train_at_index(advtrains.trains[data.train_id], data.pos_in_trainparts)
-end
-
--- coupling
-local CPL_CHK_DST = -1
-local CPL_ZONE = 2
-
--- train.couple_* contain references to ObjectRefs of couple objects, which contain all relevant information
--- These objectRefs will delete themselves once the couples no longer match
-local function createcouple(pos, train1, t1_is_front, train2, t2_is_front)
- local id1 = train1.id
- local id2 = train2.id
- if train1.autocouple or train2.autocouple then
- -- couple trains
- train1.autocouple = nil
- train2.autocouple = nil
- minetest.after(0, advtrains.safe_couple_trains, id1, id2, t1_is_front, t2_is_front, false, false, train1.velocity, train2.velocity)
- return
- end
-
- local obj=minetest.add_entity(pos, "advtrains:couple")
- if not obj then error("Failed creating couple object!") return end
- local le=obj:get_luaentity()
- le.train_id_1=id1
- le.train_id_2=id2
- le.t1_is_front=t1_is_front
- le.t2_is_front=t2_is_front
- --atdebug("created couple between",train1.id,t1_is_front,train2.id,t2_is_front)
- if t1_is_front then
- train1.cpl_front = obj
- else
- train1.cpl_back = obj
- end
- if t2_is_front then
- train2.cpl_front = obj
- else
- train2.cpl_back = obj
- end
-
-end
-
-function advtrains.train_check_couples(train)
- --atdebug("rechecking couples")
- if train.cpl_front then
- if not train.cpl_front:get_yaw() then
- -- objectref is no longer valid. reset.
- train.cpl_front = nil
- end
- end
- if not train.cpl_front then
- -- recheck front couple
- local front_trains, pos = advtrains.occ.get_occupations(train, atround(train.index) + CPL_CHK_DST)
- if advtrains.is_node_loaded(pos) then -- if the position is loaded...
- for tid, idx in pairs(front_trains) do
- local other_train = advtrains.trains[tid]
- if not advtrains.train_ensure_init(tid, other_train) then
- atwarn("Train",tid,"is not initialized! Couldn't check couples!")
- return
- end
- --atdebug(train.id,"front: ",idx,"on",tid,atround(other_train.index),atround(other_train.end_index))
- if other_train.velocity == 0 then
- if idx>=other_train.index and idx<=other_train.index + CPL_ZONE then
- createcouple(pos, train, true, other_train, true)
- break
- end
- if idx<=other_train.end_index and idx>=other_train.end_index - CPL_ZONE then
- createcouple(pos, train, true, other_train, false)
- break
- end
- end
- end
- end
- end
- if train.cpl_back then
- if not train.cpl_back:get_yaw() then
- -- objectref is no longer valid. reset.
- train.cpl_back = nil
- end
- end
- if not train.cpl_back then
- -- recheck back couple
- local back_trains, pos = advtrains.occ.get_occupations(train, atround(train.end_index) - CPL_CHK_DST)
- if advtrains.is_node_loaded(pos) then -- if the position is loaded...
- for tid, idx in pairs(back_trains) do
- local other_train = advtrains.trains[tid]
- if not advtrains.train_ensure_init(tid, other_train) then
- atwarn("Train",tid,"is not initialized! Couldn't check couples!")
- return
- end
- if other_train.velocity == 0 then
- if idx>=other_train.index and idx<=other_train.index + CPL_ZONE then
- createcouple(pos, train, false, other_train, true)
- break
- end
- if idx<=other_train.end_index and idx>=other_train.end_index - CPL_ZONE then
- createcouple(pos, train, false, other_train, false)
- break
- end
- end
- end
- end
- end
-end
-
-function advtrains.couple_invalidate(train)
- if train.cpl_back then
- train.cpl_back:remove()
- train.cpl_back = nil
- end
- if train.cpl_front then
- train.cpl_front:remove()
- train.cpl_front = nil
- end
- train.was_standing = nil
-end
-
--- relevant code for this comment is in couple.lua
-
---there are 4 cases:
---1/2. F<->R F<->R regular, put second train behind first
---->frontpos of first train will match backpos of second
---3. F<->R R<->F flip one of these trains, take the other as new train
---->backpos's will match
---4. R<->F F<->R flip one of these trains and take it as new parent
---->frontpos's will match
-
-
-function advtrains.do_connect_trains(first_id, second_id, vel)
- local first, second=advtrains.trains[first_id], advtrains.trains[second_id]
-
- if not advtrains.train_ensure_init(first_id, first) then
- atwarn("Train",first_id,"is not initialized! Operation aborted!")
- return
- end
- if not advtrains.train_ensure_init(second_id, second) then
- atwarn("Train",second_id,"is not initialized! Operation aborted!")
- return
- end
-
- local first_wagoncnt=#first.trainparts
- local second_wagoncnt=#second.trainparts
-
- for _,v in ipairs(second.trainparts) do
- table.insert(first.trainparts, v)
- end
-
- advtrains.remove_train(second_id)
- if vel < 0 then
- advtrains.invert_train(first_id)
- vel = -vel
- end
- first.velocity= vel or 0
-
- advtrains.update_trainpart_properties(first_id)
- advtrains.couple_invalidate(first)
- return true
-end
-
function advtrains.invert_train(train_id)
local train=advtrains.trains[train_id]
@@ -1368,44 +1247,61 @@ function advtrains.invalidate_path(id)
end
--not blocking trains group
-function advtrains.train_collides(node)
- if node and minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].walkable then
+
+if minetest.settings:get_bool("advtrains_forgiving_collision") then
+ function advtrains.train_collides(node)
+ if node and minetest.registered_nodes[node.name] then
+ local ndef = minetest.registered_nodes[node.name]
+ -- if the node is drawtype normal (that is a full cube) then it does collide
+ if ndef.drawtype == "normal" then
+ -- except if it is not_blocking_trains
+ if ndef.groups.not_blocking_trains and ndef.groups.not_blocking_trains ~= 0 then
+ return false
+ end
+ return true
+ end
+ end
+ return false
+ end
+else
+ function advtrains.train_collides(node)
+ if node and minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].walkable then
if not minetest.registered_nodes[node.name].groups.not_blocking_trains then
return true
end
- end
- return false
-end
-
-local nonblocknodes={
- "default:fence_wood",
- "default:fence_acacia_wood",
- "default:fence_aspen_wood",
- "default:fence_pine_wood",
- "default:fence_junglewood",
- "default:torch",
- "bones:bones",
-
- "default:sign_wall",
- "signs:sign_wall",
- "signs:sign_wall_blue",
- "signs:sign_wall_brown",
- "signs:sign_wall_orange",
- "signs:sign_wall_green",
- "signs:sign_yard",
- "signs:sign_wall_white_black",
- "signs:sign_wall_red",
- "signs:sign_wall_white_red",
- "signs:sign_wall_yellow",
- "signs:sign_post",
- "signs:sign_hanging",
-
-
-}
-minetest.after(0, function()
- for _,name in ipairs(nonblocknodes) do
- if minetest.registered_nodes[name] then
- minetest.registered_nodes[name].groups.not_blocking_trains=1
end
+ return false
end
-end)
+
+ local nonblocknodes={
+ "default:fence_wood",
+ "default:fence_acacia_wood",
+ "default:fence_aspen_wood",
+ "default:fence_pine_wood",
+ "default:fence_junglewood",
+ "default:torch",
+ "bones:bones",
+
+ "default:sign_wall",
+ "signs:sign_wall",
+ "signs:sign_wall_blue",
+ "signs:sign_wall_brown",
+ "signs:sign_wall_orange",
+ "signs:sign_wall_green",
+ "signs:sign_yard",
+ "signs:sign_wall_white_black",
+ "signs:sign_wall_red",
+ "signs:sign_wall_white_red",
+ "signs:sign_wall_yellow",
+ "signs:sign_post",
+ "signs:sign_hanging",
+
+ }
+ minetest.after(0, function()
+ for _,name in ipairs(nonblocknodes) do
+ if minetest.registered_nodes[name] then
+ minetest.registered_nodes[name].groups.not_blocking_trains=1
+ end
+ end
+ end)
+end
diff --git a/advtrains/wagons.lua b/advtrains/wagons.lua
index e9b6d7a..4093f06 100644
--- a/advtrains/wagons.lua
+++ b/advtrains/wagons.lua
@@ -1240,70 +1240,6 @@ function wagon:reattach_all()
end
end
-local function check_twagon_owner(train, b_first, pname)
- local wtp = b_first and 1 or #train.trainparts
- local wid = train.trainparts[wtp]
- local wdata = advtrains.wagons[wid]
- if wdata then
- return advtrains.check_driving_couple_protection(pname, wdata.owner, wdata.whitelist)
- end
- return false
-end
-
-function advtrains.safe_couple_trains(id1, id2, t1f, t2f, pname, try_run,v1,v2)
-
- if pname and not minetest.check_player_privs(pname, "train_operator") then
- minetest.chat_send_player(pname, "Missing train_operator privilege")
- return false
- end
-
- local train1=advtrains.trains[id1]
- local train2=advtrains.trains[id2]
-
- if not advtrains.train_ensure_init(id1, train1)
- or not advtrains.train_ensure_init(id2, train2) then
- return false
- end
- local wck_t1, wck_t2
- if pname then
- wck_t1 = check_twagon_owner(train1, t1f, pname)
- wck_t2 = check_twagon_owner(train2, t2f, pname)
- end
- if (wck_t1 or wck_t2) or not pname then
- if not v1 then
- v1 = 0
- end
- if not v2 then
- v2 = 0
- end
- if try_run then
- return true
- end
- if t1f then
- if t2f then
- v1 = -v1
- advtrains.invert_train(id1)
- advtrains.do_connect_trains(id1, id2, v1+v2)
- else
- advtrains.do_connect_trains(id2, id1, v1+v2)
- end
- else
- if t2f then
- advtrains.do_connect_trains(id1, id2, v1+v2)
- else
- v2 = -v2
- advtrains.invert_train(id2)
- advtrains.do_connect_trains(id1, id2, v1+v2)
- end
- end
- return true
- else
- minetest.chat_send_player(pname, "You must be authorized for at least one wagon.")
- return false
- end
-end
-
-
function advtrains.safe_decouple_wagon(w_id, pname, try_run)
if not minetest.check_player_privs(pname, "train_operator") then
minetest.chat_send_player(pname, "Missing train_operator privilege")