aboutsummaryrefslogtreecommitdiff
path: root/advtrains_interlocking/ars.lua
blob: b3065eefa2f75565e2300c294066f0661bec5bf3 (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
-- ars.lua
-- automatic routesetting

--[[
	The "ARS table" and its effects:
	Every route has (or can have) an associated ARS table. This can either be
	ars = { [n] = {ln="<line>"}/{rc="<routingcode>"}/{c="<a comment>"} }
	a list of rules involving either line or routingcode matchers (or comments, those are ignored)
	The first matching rule determines the route to set.
	- or -
	ars = {default = true}
	this means that all trains that no other rule matches on should use this route
	
	Compound ("and") conjunctions are not supported (--TODO should they?)
	
	For editing, those tables are transformed into lines in a text area:
	{ln=...} -> LN ...
	{rc=...} -> RC ...
	{c=...}  -> #...
	{default=true} -> *
	See also route_ui.lua
]]

local il = advtrains.interlocking

-- The ARS data are saved in a table format, but are entered in text format. Utility functions to transform between both.
function il.ars_to_text(arstab)
	if not arstab then
		return ""
	end
	
	local txt = {}

	for i, arsent in ipairs(arstab) do
		local n = ""
		if arsent.n then
			n = "!"
		end
		if arsent.ln then
			txt[#txt+1] = n.."LN "..arsent.ln
		elseif arsent.rc then
			txt[#txt+1] = n.."RC "..arsent.rc
		elseif arsent.c then
			txt[#txt+1] = "#"..arsent.c
		end
	end
	
	if arstab.default then
		return "*\n" .. table.concat(txt, "\n")
	end
	return table.concat(txt, "\n")
end

function il.text_to_ars(t)
	if t=="" then
		return nil
	elseif t=="*" then
		return {default=true}
	end
	local arstab = {}
	for line in string.gmatch(t, "[^\r\n]+") do
		if line=="*" then
			arstab.default = true
		else
			local c, v = string.match(line, "^(...?)%s(.*)$")
			if c and v then
				local n = nil
				if string.sub(c,1,1) == "!" then
					n = true
					c = string.sub(c,2)
				end
				local tt=string.upper(c)
				if tt=="LN" then
					arstab[#arstab+1] = {ln=v, n=n}
				elseif tt=="RC" then
					arstab[#arstab+1] = {rc=v, n=n}
				end
			else
				local ct = string.match(line, "^#(.*)$")
				if ct then arstab[#arstab+1] = {c = ct} end
			end
		end
	end
	return arstab
end

local function find_rtematch(routes, train)
	local default
	for rteid, route in ipairs(routes) do
		if route.ars then
			if route.ars.default then
				default = rteid
			else
				if il.ars_check_rule_match(route.ars, train) then
					return rteid
				end
			end
		end
	end
	return default
end

-- Checks whether ARS rule explicitly matches. This does not take into account the "default" field, since a wider context is required for this.
-- Returns the rule number that matched, or nil if nothing matched
function il.ars_check_rule_match(ars, train)
		if not ars then
			return nil
		end
		local line = train.line
		local routingcode = train.routingcode
		for arskey, arsent in ipairs(ars) do
			--atdebug(arsent, line, routingcode)
			if arsent.n then
				-- rule is inverse...
				if arsent.ln and (not line or arsent.ln ~= line) then
					return arskey
				elseif arsent.rc and (not routingcode or not string.find(" "..routingcode.." ", " "..arsent.rc.." ", nil, true)) then
					return arskey
				end
				return nil
			end
				
			if arsent.ln and line and arsent.ln == line then
				return arskey
			elseif arsent.rc and routingcode and string.find(" "..routingcode.." ", " "..arsent.rc.." ", nil, true) then
				return arskey
			end
		end
		return nil
end

function advtrains.interlocking.ars_check(signalpos, train, trig_from_dst)
	-- check for distant signal
	-- this whole check must be delayed until after the route setting has taken place, 
	-- because before that the distant signal is yet unknown
	if not trig_from_dst then
		minetest.after(0.5, function()
			-- does signal have dst?
			local _, remote = il.signal.get_aspect(signalpos)
			if remote then
				advtrains.interlocking.ars_check(remote, train, true)
			end
		end)
	end

	local sigd = il.db.get_sigd_for_signal(signalpos)
	local tcbs = sigd and il.db.get_tcbs(sigd)
	-- trigger ARS on this signal
	if tcbs and tcbs.routes then
		
		if tcbs.ars_disabled or tcbs.ars_ignore_next then
			-- No-ARS mode of signal.
			-- ignore...
			-- Note: ars_ignore_next is set by signalling formspec when route is cancelled
			tcbs.ars_ignore_next = nil
			return
		end
		if trig_from_dst and tcbs.no_dst_ars_trig then
			-- signal not to be triggered from distant
			return
		end
		
		if tcbs.routeset then
			-- ARS is not in effect when a route is already set
			-- just "punch" routesetting, just in case callback got lost.
			minetest.after(0, il.route.update_route, sigd, tcbs, nil, nil)
			return
		end
		
		local rteid = find_rtematch(tcbs.routes, train)
		if rteid then
			--delay routesetting, it should not occur inside train step
			-- using after here is OK because that gets called on every path recalculation
			minetest.after(0, il.route.update_route, sigd, tcbs, rteid, nil)
		end
	end
end