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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
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
|