aboutsummaryrefslogtreecommitdiff
path: root/advtrains_luaautomation/atc_rail.lua
blob: aac11f07e710b37fba03d40ed3ffa87bf4e8ac75 (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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
-- atc_rail.lua
-- registers and handles the ATC rail. Active component.
-- This is the only component that can interface with trains, so train interface goes here too.

--Using subtable
local r={}

-- Note on appr_internal:
-- The Approach callback is a special corner case: the train is not on the node, and it is executed synchronized 
-- (in the train step right during LZB traversal). We therefore need access to the train id and the lzbdata table
function r.fire_event(pos, evtdata, appr_internal)
	
	local ph=minetest.pos_to_string(pos)
	local railtbl = atlatc.active.nodes[ph]
	
	if not railtbl then
		atwarn("LuaATC interface rail at",ph,": Data not in memory! Please visit position and click 'Save'!")
		return
	end
	
	--prepare ingame API for ATC. Regenerate each time since pos needs to be known
	--If no train, then return false.
	
	-- try to get the train from the event data
	-- This workaround is required because the callback is one step delayed, and a fast train may have already left the node.
	-- Also used for approach callback
	local train_id = evtdata._train_id
	local atc_arrow = evtdata._train_arrow
	local train, tvel
	
	if train_id then
		train=advtrains.trains[train_id]
		-- speed
		tvel=train.velocity
	-- if still no train_id available, try to get the train at my position
	else
		train_id=advtrains.get_train_at_pos(pos)
		if train_id then
			train=advtrains.trains[train_id]
			advtrains.train_ensure_init(train_id, train)
			-- look up atc_arrow
			local index = advtrains.path_lookup(train, pos)
			atc_arrow = (train.path_cn[index] == 1)
			-- speed
			tvel=train.velocity
		end
	end
	
	local customfct={
		atc_send = function(cmd)
			if not train_id then return false end
			assertt(cmd, "string")
			advtrains.atc.train_set_command(train, cmd, atc_arrow)
			return true
		end,
		split_at_index = function(index, cmd)
			if not train_id then return false end
			assertt(cmd, "string")
			if type(index) ~= "number" or index < 2 then
				return false
			end
			local new_id = advtrains.split_train_at_index(train, index)
			if new_id then
				minetest.after(1,advtrains.atc.train_set_command,advtrains.trains[new_id], cmd, atc_arrow)
				return true
			end
			return false
		end,
		split_at_fc = function(cmd, len)
			assertt(cmd, "string")
			if not train_id then return false end
			local new_id, fc = advtrains.split_train_at_fc(train, false, len)
			if new_id then
				minetest.after(1,advtrains.atc.train_set_command,advtrains.trains[new_id], cmd, atc_arrow)
			end
			return fc or ""
		end,
		split_off_locomotive = function(cmd, len)
			assertt(cmd, "string")
			if not train_id then return false end
			local new_id, fc = advtrains.split_train_at_fc(train, true, len)
			if new_id then
				minetest.after(1,advtrains.atc.train_set_command,advtrains.trains[new_id], cmd, atc_arrow)
			end						
		end,
		train_length = function ()
			if not train_id then return false end
			return #train.trainparts
		end,
		step_fc = function()
			if not train_id then return false end
			advtrains.train_step_fc(train)
		end,
		get_fc = function()
			if not train_id then return end
			local fc_list = {}
			for index,wagon_id in ipairs(train.trainparts) do
				fc_list[index] = table.concat(advtrains.wagons[wagon_id].fc,"!") or ""
			end
			return fc_list
		end,
		set_fc = function(fc_list)
			assertt(fc_list, "table")
			if not train_id then return false end
			-- safety type-check for entered values
			for _,v in ipairs(fc_list) do
				if v and type(v) ~= "string" then
					error("FC entries must be a string")
					return
				end
			end
			for index,wagon_id in ipairs(train.trainparts) do
				if fc_list[index] then -- has FC to enter to this wagon
					local data = advtrains.wagons[wagon_id]
					if data then -- wagon actually exists
						for _,wagon in pairs(minetest.luaentities) do -- find wagon entity
							if wagon.is_wagon and wagon.initialized and wagon.id==wagon_id then
								wagon.set_fc(data,fc_list[index]) -- overwrite to new FC
								break -- no point cycling through every other entity. we found our wagon
							end
						end
					end
				end
			end
		end,
		set_shunt = function()
			-- enable shunting mode
			if not train_id then return false end
			train.is_shunt = true
		end,
		unset_shunt = function()
			if not train_id then return false end
			train.is_shunt = nil
		end,
		set_autocouple = function ()
			if not train_id then return false end
			train.autocouple = true			
		end,
		unset_autocouple = function ()
			if not train_id then return false end
			train.autocouple = nil
 		end,
		set_line = function(line)
			if type(line)~="string" and type(line)~="number" then
				return false
			end
			train.line = line .. ""
			minetest.after(0, advtrains.invalidate_path, train_id)
			return true
		end,
		get_line = function()
			return train.line
		end,
		set_rc = function(rc)
			if type(rc)~="string"then
				return false
			end
			train.routingcode = rc
			minetest.after(0, advtrains.invalidate_path, train_id)
			return true
		end,
		get_rc = function()
			return train.routingcode
		end,
		atc_reset = function()
			if not train_id then return false end
			advtrains.atc.train_reset_command(train)
			return true
		end,
		atc_arrow = atc_arrow,
		atc_id = train_id,
		atc_speed = tvel,
		atc_set_text_outside = function(text)
			if not train_id then return false end
			if text then assertt(text, "string") end
			advtrains.trains[train_id].text_outside=text
			return true
		end,
		atc_set_text_inside = function(text)
			if not train_id then return false end
			if text then assertt(text, "string") end
			advtrains.trains[train_id].text_inside=text
			return true
		end,
		atc_get_text_outside = function()
			if not train_id then return false end
			return advtrains.trains[train_id].text_outside
		end,
		atc_get_text_inside = function(text)
			if not train_id then return false end
			return advtrains.trains[train_id].text_inside
		end,
		atc_set_lzb_tsr = function(speed)
			if not appr_internal then
				error("atc_set_lzb_tsr() can only be used during 'approach' events!")
			end
			assert(tonumber(speed), "Number expected!")
			
			local index = appr_internal.index
			advtrains.lzb_add_checkpoint(train, index, speed, nil)
			
			return true
		end,
	}
	-- interlocking specific
	if advtrains.interlocking then
		customfct.atc_set_ars_disable = function(value)
			advtrains.interlocking.ars_set_disable(train, value)
		end
	end
	
	atlatc.active.run_in_env(pos, evtdata, customfct)
	
end

advtrains.register_tracks("default", {
	nodename_prefix="advtrains_luaautomation:dtrack",
	texture_prefix="advtrains_dtrack_atc",
	models_prefix="advtrains_dtrack",
	models_suffix=".b3d",
	shared_texture="advtrains_dtrack_shared_atc.png",
	description=atltrans("LuaATC Rail"),
	formats={},
	get_additional_definiton = function(def, preset, suffix, rotation)
		return {
			after_place_node = atlatc.active.after_place_node,
			after_dig_node = atlatc.active.after_dig_node,

			on_receive_fields = function(pos, ...)
				atlatc.active.on_receive_fields(pos, ...)
				
				--set arrowconn (for ATC)
				local ph=minetest.pos_to_string(pos)
				local _, conns=advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
				local nodeent = atlatc.active.nodes[ph]
				if nodeent then
					nodeent.arrowconn=conns[1].c
				end
			end,

			advtrains = {
				on_train_enter = function(pos, train_id, train, index)
					--do async. Event is fired in train steps
					atlatc.interrupt.add(0, pos, {type="train", train=true, id=train_id,
							_train_id = train_id, _train_arrow = (train.path_cn[index] == 1)})
				end,
				on_train_approach = function(pos, train_id, train, index, has_entered, lzbdata)
					-- Insert an event only if the rail indicated that it supports approach callbacks
					local ph=minetest.pos_to_string(pos)
					local railtbl = atlatc.active.nodes[ph]
					-- uses a "magic variable" in the local environment of the node
					-- This hack is necessary because code might not be prepared to get approach events...
					if railtbl and railtbl.data and railtbl.data.__approach_callback_mode then
						local acm = railtbl.data.__approach_callback_mode
						local in_arrow = (train.path_cn[index] == 1)
						if acm==2 or (acm==1 and in_arrow) then
							local evtdata = {type="approach", approach=true, id=train_id, has_entered = has_entered,
									_train_id = train_id, _train_arrow = in_arrow} -- reuses code from train_enter
							-- This event is *required* to run synchronously, because it might set the ars_disable flag on the train and add LZB checkpoints,
							-- although this is generally discouraged because this happens right in a train step
							-- At this moment, I am not aware whether this may cause side effects, and I must encourage users not to do expensive calculations here.
							r.fire_event(pos, evtdata, {train_id = train_id, train = train, index = index, lzbdata = lzbdata})
						end
					end
				end,
			},
			luaautomation = {
				fire_event=r.fire_event
			},
			digiline = {
				receptor = {},
				effector = {
					action = atlatc.active.on_digiline_receive
				},
			},
		}
	end,
}, advtrains.trackpresets.t_30deg_straightonly)


atlatc.rail = r
span class="hl opt">.." Terminal "..i) end end end end -- display locks set by player for pts, state in pairs(tmp_lcks) do local pos = minetest.string_to_pos(pts) routesprite(context, pos, "fixp"..pts, "at_il_route_lock_edit.png", "Fixed in state '"..state.."' by route "..route.name.." (punch to unfix)", function() clear_lock(tmp_lcks, pname, pts) end) end end end local player_rte_prog = {} function advtrains.interlocking.init_route_prog(pname, sigd) if not minetest.check_player_privs(pname, "interlocking") then minetest.chat_send_player(pname, "Insufficient privileges to use this!") return end player_rte_prog[pname] = { origin = sigd, route = { name = "PROG["..pname.."]", }, tmp_lcks = {}, } advtrains.interlocking.visualize_route(sigd, player_rte_prog[pname].route, "prog_"..pname, player_rte_prog[pname].tmp_lcks, pname) minetest.chat_send_player(pname, "Route programming mode active. Punch TCBs to add route segments, punch turnouts to lock them.") end local function get_last_route_item(origin, route) if #route == 0 then return origin end return route[#route].next end local function do_advance_route(pname, rp, sigd, tsname) table.insert(rp.route, {next = sigd, locks = rp.tmp_lcks}) rp.tmp_lcks = {} chat(pname, "Added track section '"..tsname.."' to the route.") end local function finishrpform(pname) local rp = player_rte_prog[pname] if not rp then return end local form = "size[7,6]label[0.5,0.5;Finish programming route]" local terminal = get_last_route_item(rp.origin, rp.route) if terminal then local term_tcbs = advtrains.interlocking.db.get_tcbs(terminal) if term_tcbs.signal then form = form .. "label[0.5,1.5;Route ends at signal:]" form = form .. "label[0.5,2 ;"..term_tcbs.signal_name.."]" else form = form .. "label[0.5,1.5;WARNING: Route does not end at a signal.]" form = form .. "label[0.5,2 ;Routes should in most cases end at signals.]" form = form .. "label[0.5,2.5;Cancel if you are unsure!]" end else form = form .. "label[0.5,1.5;Route leads into]" form = form .. "label[0.5,2 ;non-interlocked area]" end form = form.."field[0.8,3.5;5.2,1;name;Enter Route Name;]" form = form.."button_exit[0.5,4.5; 5,1;save;Save Route]" minetest.show_formspec(pname, "at_il_routepf", form) end local function check_advance_valid(tcbpos, rp) -- track circuit break, try to advance route over it local lri = get_last_route_item(rp.origin, rp.route) if not lri then return false, false end local is_endpoint = false local this_sigd, this_ts, adv_side if vector.equals(lri.p, tcbpos) then -- If the player just punched the last TCB again, it's of course possible to -- finish the route here (although it can't be advanced by here. -- Fun fact: you can now program routes that end exactly where they begin :) is_endpoint = true this_sigd = lri else -- else, we need to check whether this TS actually borders local start_tcbs = advtrains.interlocking.db.get_tcbs(lri) if not start_tcbs.ts_id then return false, false end this_ts = advtrains.interlocking.db.get_ts(start_tcbs.ts_id) for _,sigd in ipairs(this_ts.tc_breaks) do if vector.equals(sigd.p, tcbpos) then adv_side = otherside(sigd.s) end end if not adv_side then -- this TCB is not bordering to the section return false, false end this_sigd = {p=tcbpos, s=adv_side} end -- check whether the ts at the other end is capable of "end over" local adv_tcbs = advtrains.interlocking.db.get_tcbs(this_sigd) local next_tsid = adv_tcbs.ts_id local can_over, over_ts, next_tc_bs = false, nil, nil local cannotover_rsn = "Next section is diverging (>2 TCBs)" if next_tsid then -- you may not advance over EOI. While this is technically possible, -- in practise this just enters an unnecessary extra empty route item. over_ts = advtrains.interlocking.db.get_ts(adv_tcbs.ts_id) next_tc_bs = over_ts.tc_breaks can_over = #next_tc_bs <= 2 else cannotover_rsn = "End of interlocking" end local over_sigd = nil if can_over then if next_tc_bs and #next_tc_bs == 2 then local sdt if vector.equals(next_tc_bs[1].p, tcbpos) then sdt = next_tc_bs[2] end if vector.equals(next_tc_bs[2].p, tcbpos) then sdt = next_tc_bs[1] end if not sdt then error("Inconsistency: "..dump(next_ts)) end -- swap TCB direction over_sigd = {p = sdt.p, s = otherside(sdt.s) } end end return is_endpoint, true, this_sigd, this_ts, can_over, over_ts, over_sigd, cannotover_rsn end local function show_routing_form(pname, tcbpos, message) local rp = player_rte_prog[pname] if not rp then return end local is_endpoint, advance_valid, this_sigd, this_ts, can_over, over_ts, over_sigd, cannotover_rsn = check_advance_valid(tcbpos, rp) -- at this place, advance_valid shows whether the current route can be advanced -- over this TCB. -- If it can: -- Advance over (continue programming) -- End here -- Advance and end (only <=2 TCBs, terminal signal needs to be known) -- if not: -- show nothing at all -- In all cases, Discard and Backtrack buttons needed. local form = "size[7,9.5]label[0.5,0.5;Advance/Complete Route]" if message then form = form .. "label[0.5,1;"..message.."]" end if advance_valid and not is_endpoint then form = form.. "label[0.5,1.8;Advance to next route section]" form = form.."image_button[0.5,2.2; 5,1;at_il_routep_advance.png;advance;]" form = form.. "label[0.5,3.5;-------------------------]" else form = form.. "label[0.5,2.3;This TCB is not suitable as]" form = form.. "label[0.5,2.8;route continuation.]" end if advance_valid or is_endpoint then form = form.. "label[0.5,3.8;Finish route HERE]" form = form.."image_button[0.5, 4.2; 5,1;at_il_routep_end_here.png;endhere;]" if can_over then form = form.. "label[0.5,5.3;Finish route at end of NEXT section]" form = form.."image_button[0.5,5.7; 5,1;at_il_routep_end_over.png;endover;]" else form = form.. "label[0.5,5.3;Advancing over next section is]" form = form.. "label[0.5,5.8;impossible at this place.]" if cannotover_rsn then form = form.. "label[0.5,6.3;"..cannotover_rsn.."]" end end end form = form.. "label[0.5,7;-------------------------]" if #rp.route > 0 then form = form.."button[0.5,7.4; 5,1;retract;Step back one section]" end form = form.."button[0.5,8.4; 5,1;cancel;Cancel route programming]" minetest.show_formspec(pname, "at_il_rprog_"..minetest.pos_to_string(tcbpos), form) end minetest.register_on_player_receive_fields(function(player, formname, fields) local pname = player:get_player_name() local tcbpts = string.match(formname, "^at_il_rprog_([^_]+)$") local tcbpos if tcbpts then tcbpos = minetest.string_to_pos(tcbpts) end if tcbpos then -- RPROG form local rp = player_rte_prog[pname] if not rp then minetest.close_formspec(pname, formname) return end local is_endpoint, advance_valid, this_sigd, this_ts, can_over, over_ts, over_sigd = check_advance_valid(tcbpos, rp) if advance_valid then if fields.advance then -- advance route if not is_endpoint then do_advance_route(pname, rp, this_sigd, this_ts.name) end end if fields.endhere then if not is_endpoint then do_advance_route(pname, rp, this_sigd, this_ts.name) end finishrpform(pname) end if can_over and fields.endover then if not is_endpoint then do_advance_route(pname, rp, this_sigd, this_ts.name) end do_advance_route(pname, rp, over_sigd, over_ts and over_ts.name or "--EOI--") finishrpform(pname) end end if fields.retract then if #rp.route <= 0 then minetest.close_formspec(pname, formname) return end rp.tmp_locks = rp.route[#rp.route].locks rp.route[#rp.route] = nil chat(pname, "Route section "..(#rp.route+1).." removed.") end if fields.cancel then player_rte_prog[pname] = nil advtrains.interlocking.clear_visu_context("prog_"..pname) chat(pname, "Route discarded.") minetest.close_formspec(pname, formname) return end advtrains.interlocking.visualize_route(rp.origin, rp.route, "prog_"..pname, rp.tmp_lcks, pname) minetest.close_formspec(pname, formname) return end if formname == "at_il_routepf" then if not fields.save or not fields.name then return end if fields.name == "" then -- show form again finishrpform(pname) return end local rp = player_rte_prog[pname] if rp then if #rp.route <= 0 then chat(pname, "Cannot program route without a target") return end local tcbs = advtrains.interlocking.db.get_tcbs(rp.origin) if not tcbs then chat(pname, "The origin TCB has become unknown during programming. Try again.") return end local terminal = get_last_route_item(rp.origin, rp.route) rp.route.terminal = terminal rp.route.name = fields.name table.insert(tcbs.routes, rp.route) advtrains.interlocking.clear_visu_context("prog_"..pname) player_rte_prog[pname] = nil chat(pname, "Successfully programmed route.") advtrains.interlocking.show_route_edit_form(pname, rp.origin, #tcbs.routes) return end end end) -- Central route programming punch callback minetest.register_on_punchnode(function(pos, node, player, pointed_thing) local pname = player:get_player_name() if not minetest.check_player_privs(pname, "interlocking") then return end local rp = player_rte_prog[pname] if rp then -- determine what the punched node is if minetest.get_item_group(node.name, "at_il_track_circuit_break") >= 1 then -- get position of the assigned tcb local meta = minetest.get_meta(pos) local tcbpts = meta:get_string("tcb_pos") if tcbpts == "" then chat(pname, "This TCB is unconfigured, you first need to assign it to a rail") return end local tcbpos = minetest.string_to_pos(tcbpts) -- show formspec show_routing_form(pname, tcbpos) advtrains.interlocking.visualize_route(rp.origin, rp.route, "prog_"..pname, rp.tmp_lcks, pname) return end if advtrains.is_passive(pos) then local pts = advtrains.roundfloorpts(pos) if rp.tmp_lcks[pts] then clear_lock(rp.tmp_lcks, pname, pts) else local state = advtrains.getstate(pos) rp.tmp_lcks[pts] = state chat(pname, pts.." is held in "..state.." position when this route is set and freed ") end advtrains.interlocking.visualize_route(rp.origin, rp.route, "prog_"..pname, rp.tmp_lcks, pname) return end end end) --TODO on route setting -- routes should end at signals. complete route setting by punching a signal, and command as exceptional route completion -- Create simpler way to advance a route to the next tcb/signal on simple sections without turnouts