aboutsummaryrefslogtreecommitdiff
path: root/advtrains_interlocking/smartroute.lua
blob: f03ece02d2d4d1e3d630e927954aaff16852ef4f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
-- smartroute.lua
-- Implementation of the advtrains auto-route search

local atil = advtrains.interlocking
local ildb = atil.db
local sr = {}


-- Start the SmartRoute process. This searches for routes and tries to match them with existing routes, showing them in a form
function sr.start(pname, sigd)
	-- is start signal a shunt signal? This becomes default setting for searching_shunt
	local is_startsignal_shunt = false
	local tcbs = ildb.get_tcbs(sigd)
	if tcbs.signal then
		local ndef = advtrains.ndb.get_ndef(tcbs.signal)
		if ndef and ndef.advtrains then
			if ndef.advtrains.route_role == "shunt" then
				is_startsignal_shunt = true
			end
		end
	end
	sr.propose_next(pname, sigd, 0, is_startsignal_shunt)
end


local function otherside(s)
	if s==1 then return 2 else return 1 end
end

--route search implementation
-- new 2025-01-06: rely on the already present info from rscache to traverse sections
-- this allows to implement a breadth first search
-- format of foundroute:
-- { name = "the name", tcbseq = { list of sigds in sequence, not containing the start sigd }}

local function build_route_from_foundroute(froute, name)
	local route = {
		name = froute.name,
		use_rscache = true,
		smartroute_generated = true,
	}
	for _, sigd in ipairs(froute.tcbseq) do
		route[#route+1] = { next = sigd, locks = {} }
	end
	return route
end

-- Maximum num of sections for routes to be found
local RTE_MAX_SECS = 16

-- scan for possible routes from the start tcb in a bread-first-search manner
-- find_more_than: search is aborted only if more than the specified number of routes are found
function sr.rescan(pname, sigd, tcbs, find_more_than, searching_shunt, pname)
	local found_routes = {}
	local restart_tcbs = { {sigd = sigd, tcbseq = {} } }
	local last_len = 0
	while true do
		-- take first entry out of restart_tcbs (due to the way it is inserted the first entry will always be the one with the lowest length
		local cur_restart
		for idx, rst in ipairs(restart_tcbs) do
			cur_restart = rst
			table.remove(restart_tcbs, idx)
			break
		end
		if not cur_restart then
			-- we have no candidates left. Give up and return what we have
			--atdebug("(SR) No Candidates left, end rescan")
			return found_routes
		end
		-- check if we need to stop due to having found enough routes
		local cur_len = #cur_restart.tcbseq
		if cur_len > last_len then
			-- one level is finished, check if enoufh routes are found
			if #found_routes > find_more_than then
				--atdebug("(SR) Layer finished and enough routes found, end rescan")
				return found_routes
			end
			last_len = cur_len
		end
		-- our current restart point is nouw in cur_restart
		local c_sigd = cur_restart.sigd
		--atdebug("(SR) Search continues at",c_sigd,"seqlen",#cur_restart.tcbseq)
		-- do a TS repair, this also updates the RS cache should it be out of date
		local c_ts_id = ildb.check_and_repair_ts_at_pos(c_sigd.p, c_sigd.s, pname, false)
		if c_ts_id then
			local c_ts = ildb.get_ts(c_ts_id)
			local bgn_pts = advtrains.encode_pos(c_sigd.p)
			local rsout = c_ts.rs_cache[bgn_pts]
			if rsout then
				for _, end_sigd in ipairs(c_ts.tc_breaks) do
					end_pkey = advtrains.encode_pos(end_sigd.p)
					if rsout[end_pkey] then
						--atdebug("(SR) Section",c_ts_id,c_ts.name,"has way",c_sigd,"->",end_sigd)
						local nsigd = {p=end_sigd.p, s = end_sigd.s==1 and 2 or 1} -- invert to other side
						-- record nsigd in the tcbseq
						local ntcbseq = table.copy(cur_restart.tcbseq)
						ntcbseq[#ntcbseq+1] = nsigd
						local shall_continue = true
						-- check if that sigd is a route target
						local tcbs = ildb.get_tcbs(nsigd)
                        if tcbs.signal then
							local ndef = advtrains.ndb.get_ndef(tcbs.signal)
							if ndef and ndef.advtrains then
								if ndef.advtrains.route_role == "main" or ndef.advtrains.route_role == "main_distant"
											or ndef.advtrains.route_role == "end" or ndef.advtrains.route_role == "shunt" then
									-- signal is suitable target
									local is_mainsignal = ndef.advtrains.route_role ~= "shunt"
									--atdebug("(SR) Suitable end signal at",nsigd,", recording route!")
									-- record the found route in the results
									found_routes[#found_routes+1] = {
											tcbseq = ntcbseq,
											shunt_route = not is_mainsignal,
											name = tcbs.signal_name or atil.sigd_to_string(nsigd)
									}
									-- if this is a main signal and/or we are only searching shunt routes, stop the search here
									if is_mainsignal or searching_shunt then
											--atdebug("(SR) Not continuing this branch!")
											shall_continue = false
									end
								end
							end
                        end
						-- unless overridden, insert the next restart point
						if shall_continue then
							restart_tcbs[#restart_tcbs+1] =  {sigd = nsigd, tcbseq = ntcbseq } 
						end
					end
				end
			else
				--atdebug("(SR) Section",c_ts_id,c_ts.name,"found no rscache entry for start ",bgn_pts)
			end
		else
			--atdebug("(SR) Stop at",c_sigd,"because no sec ahead")
		end
	end
end

local players_smartroute_actions = {}
-- Propose to pname the smartroute actions in a form, with the current settings as passed to this function
function sr.propose_next(pname, sigd, find_more_than, searching_shunt)
	local tcbs = ildb.get_tcbs(sigd)
	if not tcbs or not tcbs.routes then
		minetest.chat_send_player(pname, "Smartroute: TCBS or routes don't exist here!")
		return
	elseif not tcbs.ts_id then
		minetest.chat_send_player(pname, "Smartroute: No track section directly ahead!")
		return
	end
	-- Step 1: search for routes using the current settings
	local found_routes = sr.rescan(pname, sigd, tcbs, find_more_than, searching_shunt, pname)
	-- Step 2: store in actions table
	players_smartroute_actions[pname] = {
		sigd = sigd,
		searching_shunt = searching_shunt,
		found_routes = found_routes
	}
	-- step 3: build form
	local form = "size[5,5]label[0,0;Route search: "..#found_routes.." found]"
	local tab = {}
	for idx, froute in ipairs(found_routes) do
		tab[idx] = minetest.formspec_escape(froute.name.." (Len="..#froute.tcbseq..")")
	end
	form=form.."textlist[0.5,1;4,3;rtelist;"..table.concat(tab, ",").."]"
	form=form.."button[0.5,4;2,1;continue;Search further]"
	form=form.."button[2.5,4;2,1;apply;Apply]"
	
	minetest.show_formspec(pname, "at_il_smartroute_propose", form)
end

minetest.register_on_player_receive_fields(function(player, formname, fields)
	local pname = player:get_player_name()
	if not minetest.check_player_privs(pname, "interlocking") then
		return
	end
	if formname ~= "at_il_smartroute_propose" then
		return
	end
	-- retrieve from the storage the current search result
	local srtab = players_smartroute_actions[pname]
	if not srtab then
		return
	end
	local sigd = srtab.sigd
	local found_routes = srtab.found_routes
	
	if fields.continue then
		-- search on, but find at least one route more
		sr.propose_next(pname, sigd, #found_routes, srtab.searching_shunt)
		return
	end
	
	if fields.apply then
		-- user is happy with the found routes. Proceed to save them in the signal
		local tcbs = ildb.get_tcbs(sigd)
		if not tcbs then return end
		-- remove routes for endpoints for which routes already exist
		local ex_endpts = {} -- key = sigd_to_string
		for rtid, route in ipairs(tcbs.routes) do
			local valid = advtrains.interlocking.check_route_valid(route, sigd)
			local endpoint = route[#route].next -- 'next' field of the last route segment (the segment with index==len)
			if valid and endpoint then
				local endstr = advtrains.interlocking.sigd_to_string(endpoint)
				--atdebug("(Smartroute) Find existing endpoint:",route.name,"ends at",endstr)
				ex_endpts[endstr] = route.name
			else
				--atdebug("(Smartroute) Find existing endpoint:",route.name," not considered, endpoint",endpoint,"valid",valid)
			end
		end
		local new_frte = {}
		for _,froute in ipairs(found_routes) do
			local endpoint = froute.tcbseq[#froute.tcbseq]
			local endstr = advtrains.interlocking.sigd_to_string(endpoint)
			if not ex_endpts[endstr] then
				new_frte[#new_frte+1] = froute
			else
				--atdebug("(Smartroute) Throwing away",froute.name,"because endpoint",endstr,"already reached by route",ex_endpts[endstr])
			end
		end
		
		-- All remaining routes will be applied to the signal
		local sel_rte = #tcbs.routes+1
		for idx, froute in ipairs(new_frte) do
			tcbs.routes[#tcbs.routes+1] = build_route_from_foundroute(froute)
		end
		-- if only one route present and it is newly created (there was no route before, thus sel_rte==1), make default
		if sel_rte == 1 and #tcbs.routes == 1 then
			local route1 = tcbs.routes[1]
			route1.ars = {default=true}
			-- if that only route furthermore is a suitable block signal route (1 section with no locks), set it into block signal mode
			if #route1 == 1 then
				local ts = tcbs.ts_id and advtrains.interlocking.db.get_ts(tcbs.ts_id)
				if ts and #ts.tc_breaks == 2 then
					-- check for presence of any locks
					local epos1 = advtrains.encode_pos(ts.tc_breaks[1].p)
					local epos2 = advtrains.encode_pos(ts.tc_breaks[2].p)
					local haslocks =
							(route1[1].locks and next(route1[1].locks)) -- the route itself has no locks
							or (ts.fixed_locks and next(ts.fixed_locks)) -- the section has no fixedlocks
							or (ts.rs_cache and ts.rs_cache[epos1] and ts.rs_cache[epos1][epos2] and next(ts.rs_cache[epos1][epos2])) -- the section has no locks in rscache
					if not haslocks then
						-- yeah, blocksignal!
						route1.default_autoworking = true
					end
				end
			end
		end
		--atdebug("Smartroute done!")
		advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte)
		players_smartroute_actions[pname] = nil
	end
	if fields.quit then
		players_smartroute_actions[pname] = nil
	end
end)


advtrains.interlocking.smartroute = sr