aboutsummaryrefslogtreecommitdiff
path: root/advtrains_interlocking
diff options
context:
space:
mode:
authororwell <orwell@bleipb.de>2025-01-07 00:46:08 +0100
committerorwell <orwell@bleipb.de>2025-01-07 00:46:08 +0100
commit2e1681930c15954bead9c1b0ef9f4296508f60ee (patch)
tree81b55eb543cb1768b0d4587d35a33e02876f4db2 /advtrains_interlocking
parent7d5f840579b74374698704f256479520bde25091 (diff)
Smartroute: rework to use result of rs_cache instead of duplicating, use bread-first-search and incremental search further with formspec
Diffstat (limited to 'advtrains_interlocking')
-rw-r--r--advtrains_interlocking/database.lua1
-rw-r--r--advtrains_interlocking/smartroute.lua295
-rwxr-xr-xadvtrains_interlocking/tcb_ts_ui.lua10
3 files changed, 184 insertions, 122 deletions
diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua
index f84f60b..38b1bc8 100644
--- a/advtrains_interlocking/database.lua
+++ b/advtrains_interlocking/database.lua
@@ -718,6 +718,7 @@ local function recursively_find_routes(s_pos, s_connid, locks_found, result_tabl
return
end
local out_conns = ildb.get_possible_out_connids(node.name, bconnid)
+ -- note 2025-01-06: get_possible_out_connids contains some logic so that the correct switch state is selected even if the turnout is entered from the branch side (see "have_back_conn")
for oconnid, state in pairs(out_conns) do
--atdebug("Going in direction",oconnid,"state",state)
locks_found[pts] = state
diff --git a/advtrains_interlocking/smartroute.lua b/advtrains_interlocking/smartroute.lua
index a26f2d1..f697d7b 100644
--- a/advtrains_interlocking/smartroute.lua
+++ b/advtrains_interlocking/smartroute.lua
@@ -19,7 +19,7 @@ function sr.start(pname, sigd)
end
end
end
- sr.propose_next(pname, sigd, 10, is_startsignal_shunt) -- TODO set tscnt_limit to 2 initially and then increase it. Do this when form is implemented
+ sr.propose_next(pname, sigd, 0, is_startsignal_shunt)
end
@@ -28,88 +28,10 @@ local function otherside(s)
end
--route search implementation
--- Note this is similar to recursively_find_routes in database.lua, there used for the rscache
-
-local function recursively_find_routes(s_pos, s_connid, searching_shunt, tcbseq, mark_pos, result_table, scan_limit, tscnt_limit, cur_ts_id, notify_pname)
- atdebug("(SmartRoute) Recursively restarting at ",s_pos, s_connid, "limit left", scan_limit,"tscnt",tscnt_limit)
- local ti = advtrains.get_track_iterator(s_pos, s_connid, scan_limit, false)
- local pos, connid, bconnid = ti:next_branch()
- pos, connid, bconnid = ti:next_track()-- step once to get ahead of previous turnout
- local last_pos
- repeat
- -- record position in mark_pos
- local pts = advtrains.encode_pos(pos)
- mark_pos[pts] = true
-
- local node = advtrains.ndb.get_node_or_nil(pos)
- --atdebug("(SmartRoute) Walk ",pos, "nodename", node.name, "entering at conn",bconnid)
- local ndef = minetest.registered_nodes[node.name]
- if ndef.advtrains and ndef.advtrains.node_state_map then
- -- Stop, this is a switchable node. Find out which conns we can go at
- atdebug("(SmartRoute) Found turnout ",pos, "nodename", node.name, "entering at conn",bconnid)
- local out_conns = ildb.get_possible_out_connids(node.name, bconnid)
- for oconnid, state in pairs(out_conns) do
- --atdebug("Going in direction",oconnid,"state",state)
- recursively_find_routes(pos, oconnid, searching_shunt,
- table.copy(tcbseq), table.copy(mark_pos),
- result_table, ti.limit, tscnt_limit, cur_ts_id, notify_pname)
- end
- return
- end
- --otherwise, this might be a tcb
- local tcb = ildb.get_tcb(pos)
- if tcb then
- local fsigd = { p = pos, s = connid }
- atdebug("(SmartRoute) Encounter TCB ",fsigd)
- tcbseq[#tcbseq+1] = fsigd
- -- TS validity check: ensure that the TS the back connid refers to is the same as the one at the start
- local re_ts_id = tcb[bconnid].ts_id
- if re_ts_id ~= cur_ts_id then
- atwarn("(SmartRoute) Found TS Inconsistency: entered in section",cur_ts_id,"but TCB backref is section",re_ts_id)
- ildb.check_and_repair_ts_at_pos(pos, bconnid, notify_pname)
- -- nothing needs to be updated on our side
- end
- -- check if this is a possible route endpoint
- local tcbs = tcb[connid]
- 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"
- -- record the found route in the results
- result_table[#result_table+1] = {
- tcbseq = table.copy(tcbseq),
- mark_pos = table.copy(mark_pos),
- shunt_route = not is_mainsignal,
- to_end_of_track = false,
- name = tcbs.signal_name or atil.sigd_to_string(fsigd)
- }
- -- 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("(SmartRoute) Terminating here because it is main or only shunt routes searched")
- return
- end
- end
- end
- end
- -- decrease tscnt
- tscnt_limit = tscnt_limit - 1
- atdebug("(SmartRoute) Remaining TS Count:",tscnt_limit)
- if tscnt_limit <= 0 then
- break
- end
- -- update the cur_ts_id
- cur_ts_id = tcb[connid].ts_id
- atdebug("(SmartRoute) Now in section:",cur_ts_id)
- end
- -- Go forward
- last_pos = pos
- pos, connid, bconnid = ti:next_track()
- until not pos -- this stops the loop when either the track end is reached or the limit is hit
- atdebug("(SmartRoute) Reached track end or limit at", last_pos, ". This path is not saved, returning")
-end
+-- 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 = {
@@ -123,17 +45,99 @@ local function build_route_from_foundroute(froute, name)
return route
end
--- Maximum scan length for track iterator
-local TS_MAX_SCAN = 1000
+-- Maximum num of sections for routes to be found
+local RTE_MAX_SECS = 16
-function sr.rescan(pname, sigd, tcbs, tscnt_limit, searching_shunt, pname)
- local result_table = {}
- recursively_find_routes(sigd.p, sigd.s, searching_shunt, {}, {}, result_table, TS_MAX_SCAN, tscnt_limit, tcbs.ts_id, pname)
- return result_table
+-- 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(fsigd)
+ }
+ -- 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, tscnt_limit, searching_shunt)
+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!")
@@ -143,40 +147,93 @@ function sr.propose_next(pname, sigd, tscnt_limit, searching_shunt)
return
end
-- Step 1: search for routes using the current settings
- local found_routes = sr.rescan(pname, sigd, tcbs, tscnt_limit, searching_shunt, pname)
- -- Step 2: 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 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)
- 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)
+ 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
- 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])
+
+ -- 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
+ tcbs.routes[1].ars = {default=true}
+ end
+ atdebug("Smartroute done!")
+ advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte)
+ players_smartroute_actions[pname] = nil
end
-
- -- All remaining routes will be shown to user now.
- -- TODO: show a form. Right now still shortcircuit
- local sel_rte = #tcbs.routes+1
- for idx, froute in ipairs(new_frte) do
- tcbs.routes[#tcbs.routes+1] = build_route_from_foundroute(froute)
+ if fields.quit then
+ players_smartroute_actions[pname] = nil
end
- atdebug("Smartroute done!")
- advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte)
-end
+end)
advtrains.interlocking.smartroute = sr
diff --git a/advtrains_interlocking/tcb_ts_ui.lua b/advtrains_interlocking/tcb_ts_ui.lua
index 4f755af..edc6921 100755
--- a/advtrains_interlocking/tcb_ts_ui.lua
+++ b/advtrains_interlocking/tcb_ts_ui.lua
@@ -871,14 +871,15 @@ function advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte, calle
form = form.."button[0.5,6; 5,1;setroute;Set Route]"
form = form.."button[0.5,7;2,1;dsproute;Show]"
if hasprivs then
- form = form.."button[2.5,7;1,1;setarsdefault;Set Def.]"
+ form = form.."button[5.5,3.3;1,0.3;setarsdefault;D]tooltip[setarsdefault;Set ARS default route]"
form = form.."button[3.5,7;2,1;editroute;Edit]"
if sel_rte > 1 then
- form = form .. "button[5.5,4;0.5,0.3;moveup;↑]"
+ form = form .. "button[5.5,4;1,0.3;moveup;↑]"
end
if sel_rte < #strtab then
- form = form .. "button[5.5,4.7;0.5,0.3;movedown;↓]"
+ form = form .. "button[5.5,4.7;1,0.3;movedown;↓]"
end
+ form = form.."button[5.5,5.4;1,0.3;delroute;X]tooltip[delroute;Delete this route]"
end
else
form = form .. "]"
@@ -1097,6 +1098,9 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end
end
end
+ if fields.delroute and hasprivs then
+ table.remove(tcbs.routes,sel_rte)
+ end
end
end
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097
-- Track Circuit Breaks and Track Sections - Player interaction

local players_assign_tcb = {}
local players_assign_signal = {}
local players_assign_xlink = {}
local players_link_ts = {}
local players_assign_fixedlocks = {}

local atil = advtrains.interlocking
local ildb = advtrains.interlocking.db
local ilrs = advtrains.interlocking.route

local sigd_equal = advtrains.interlocking.sigd_equal

local lntrans = { "A", "B" }

local function sigd_to_string(sigd)
	return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s]
end
advtrains.interlocking.sigd_to_string = sigd_to_string

minetest.register_node("advtrains_interlocking:tcb_node", {
	drawtype = "mesh",
	paramtype="light",
	paramtype2="facedir",
	walkable = false,
	selection_box = {
		type = "fixed",
		fixed = {-1/6, -1/2, -1/6, 1/6, 1/4, 1/6},
	},
	mesh = "at_il_tcb_node.obj",
	tiles = {"at_il_tcb_node.png"},
	description="Track Circuit Break",
	sunlight_propagates=true,
	groups = {
		cracky=3,
		not_blocking_trains=1,
		--save_in_at_nodedb=2,
		at_il_track_circuit_break = 1,
	},
	after_place_node = function(pos, node, player)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", "Unconfigured Track Circuit Break, right-click to assign.")
	end,
	on_rightclick = function(pos, node, player)
		local pname = player:get_player_name()
		if not minetest.check_player_privs(pname, "interlocking") then
			minetest.chat_send_player(pname, "Insufficient privileges to use this!")
			return
		end
		
		local meta = minetest.get_meta(pos)
		local tcbpts = meta:get_string("tcb_pos")
		if tcbpts ~= "" then
			local tcbpos = minetest.string_to_pos(tcbpts)
			local tcb = ildb.get_tcb(tcbpos)
			if tcb then
				advtrains.interlocking.show_tcb_form(tcbpos, pname)
			else
				minetest.chat_send_player(pname, "This TCB has been removed. Please dig marker.")
			end
		else
			--unconfigured
			minetest.chat_send_player(pname, "Configuring TCB: Please punch the rail you want to assign this TCB to.")
			
			players_assign_tcb[pname] = pos
		end	
	end,
	--on_punch = function(pos, node, player)
	--	local meta = minetest.get_meta(pos)
	--	local tcbpts = meta:get_string("tcb_pos")
	--	if tcbpts ~= "" then
	--		local tcbpos = minetest.string_to_pos(tcbpts)
	--		advtrains.interlocking.show_tcb_marker(tcbpos)
	--	end	
	--end,
	can_dig = function(pos, player)
		if player == nil then return false end

		local pname = player:get_player_name()

		-- Those markers can only be dug when all adjacent TS's are set
		-- as EOI.
		local meta = minetest.get_meta(pos)
		local tcbpts = meta:get_string("tcb_pos")
		if tcbpts ~= "" then
			if not minetest.check_player_privs(pname, "interlocking") then
				minetest.chat_send_player(pname, "Insufficient privileges to use this!")
				return
			end			
			local tcbpos = minetest.string_to_pos(tcbpts)
			local tcb = ildb.get_tcb(tcbpos)
			if not tcb then return true end
			for connid=1,2 do
				if tcb[connid].signal then
					minetest.chat_send_player(pname, "Can't remove TCB: Both sides must have no signal assigned!")
					return false
				end
			end
		end	
		return true
	end,
	after_dig_node = function(pos, oldnode, oldmetadata, player)
		if not oldmetadata or not oldmetadata.fields then return end
		local pname = player:get_player_name()
		local tcbpts = oldmetadata.fields.tcb_pos
		if tcbpts and tcbpts ~= "" then
			local tcbpos = minetest.string_to_pos(tcbpts)
			ildb.remove_tcb_at(tcbpos, pname)
		end
	end,
})


-- Crafting

-- set some fallbacks
local tcb_core = "default:mese_crystal"
local tcb_secondary = "default:mese_crystal_fragment"

--alternative recipe items
--core
if minetest.get_modpath("basic_materials") then
	tcb_core = "basic_materials:ic"
elseif minetest.get_modpath("technic") then
	tcb_core = "technic:control_logic_unit"
end
--print("TCB Core: "..tcb_core)
--secondary
if minetest.get_modpath("mesecons") then
	tcb_secondary = 'mesecons:wire_00000000_off'
end
--print("TCB Secondary: "..tcb_secondary)

minetest.register_craft({
	output = 'advtrains_interlocking:tcb_node 4',
	recipe = {
		{tcb_secondary,tcb_core,tcb_secondary},
		{'advtrains:dtrack_placer','','advtrains:dtrack_placer'}
	},
	--actually use track in the tcb recipe
	replacements = {
		{"advtrains:dtrack_placer","advtrains:dtrack_placer"},
		{"advtrains:dtrack_placer","advtrains:dtrack_placer"},
	}
})

--nil the temp crafting variables
tcb_core= nil
tcb_secondary = nil

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
	-- TCB assignment
	local tcbnpos = players_assign_tcb[pname]
	if tcbnpos then
		if vector.distance(pos, tcbnpos)<=20 then
			local node_ok, conns, rhe = advtrains.get_rail_info_at(pos)
			if node_ok and #conns == 2 then
				-- if there is already a tcb here, reassign it
				if ildb.get_tcb(pos) then
					minetest.chat_send_player(pname, "Configuring TCB: Already existed at this position, it is now linked to this TCB marker")
				else
					ildb.create_tcb_at(pos, pname)
				end

				local meta = minetest.get_meta(tcbnpos)
				meta:set_string("tcb_pos", minetest.pos_to_string(pos))
				meta:set_string("infotext", "TCB assigned to "..minetest.pos_to_string(pos))
				minetest.chat_send_player(pname, "Configuring TCB: Successfully configured TCB")
				advtrains.interlocking.show_tcb_marker(pos)
			else
				minetest.chat_send_player(pname, "Configuring TCB: This is not a normal two-connection rail! Aborted.")
			end
		else
			minetest.chat_send_player(pname, "Configuring TCB: Node is too far away. Aborted.")
		end
		players_assign_tcb[pname] = nil
	end
	
	-- Signal assignment
	local sigd = players_assign_signal[pname]
	if sigd then
		if vector.distance(pos, sigd.p)<=50 then
			local is_signal = minetest.get_item_group(node.name, "advtrains_signal") >= 2
			if is_signal then
				local ndef = minetest.registered_nodes[node.name]
				if ndef and ndef.advtrains and ndef.advtrains.apply_aspect then
					local tcbs = ildb.get_tcbs(sigd)
					if tcbs then
						ildb.assign_signal_to_tcbs(pos, sigd)
						-- use auto-naming
						advtrains.interlocking.add_autoname_to_tcbs(tcbs, pname)
						minetest.chat_send_player(pname, "Configuring TCB: Successfully assigned signal.")
						advtrains.interlocking.show_ip_form(pos, pname, true)
					else
						minetest.chat_send_player(pname, "Configuring TCB: Internal error, TCBS doesn't exist. Aborted.")
					end
				else
					minetest.chat_send_player(pname, "Configuring TCB: Cannot use static signals for routesetting. Aborted.")
				end
			else
				minetest.chat_send_player(pname, "Configuring TCB: Not a compatible signal. Aborted.")
			end
		else
			minetest.chat_send_player(pname, "Configuring TCB: Node is too far away. Aborted.")
		end
		players_assign_signal[pname] = nil
	end
	
	-- FixedLocks assignment
	local ts_id = players_assign_fixedlocks[pname]
	if ts_id then
		if advtrains.is_passive(pos) then
			local pts = advtrains.encode_pos(pos)
			local state = advtrains.getstate(pos)
			local ts = ildb.get_ts(ts_id)
			if ts and ts.fixed_locks then
				minetest.chat_send_player(pname, minetest.pos_to_string(pos).." locks in state "..state)
				ts.fixed_locks[pts] = state
			else
				minetest.chat_send_player(pname, "Error: TS modified, abort!")
				players_assign_fixedlocks[pname] = nil
			end
		else
			minetest.chat_send_player(pname, "Setting fixed locks finished!")
			players_assign_fixedlocks[pname] = nil
			ildb.update_rs_cache(ts_id)
			advtrains.interlocking.show_ts_form(ts_id, pname)
		end
	end
end)

-- "Self-contained TCB"
-- 2024-11-25: Buffers should become their own TCB (and signal) automatically to permit setting routes to them
-- These are support functions for this kind of node.

-- Create an after_place_node callback for a self-contained TCB node. The parameters control additional behavior:
-- fail_silently_on_noprivs: (boolean) Does not give an error in case the placer does not have the interlocking privilege
-- auto_create_self_signal: (boolean) Automatically assign this same node as signal to the A side of the newly-created TCB
--                          (this is useful for buffers as they serve both as TCB and as an always-halt signal)
function advtrains.interlocking.self_tcb_make_after_place_callback(fail_silently_on_noprivs, auto_create_self_signal)
	return function(pos, player, itemstack, pointed_thing)
		local pname = player:get_player_name()
		if not minetest.check_player_privs(pname, "interlocking") then
			if not fail_silently_on_noprivs then
				minetest.chat_send_player(pname, "Insufficient privileges to use this!")
			end
			return
		end
		if ildb.get_tcb(pos) then
			minetest.chat_send_player(pname, "TCB already existed at this position, now linked to this node")
		else
			ildb.create_tcb_at(pos, pname)
		end
		if auto_create_self_signal then
			local sigd = { p = pos, s = 1 }
			local tcbs = ildb.get_tcbs(sigd)
			-- make sure signal doesn't already exist
			if tcbs.signal then
				minetest.chat_send_player(pname, "Signal on B side already assigned!")
				return
			end
			ildb.assign_signal_to_tcbs(pos, sigd)
			-- assign influence point to itself
			ildb.set_ip_signal(advtrains.roundfloorpts(pos), 1, pos)
			-- use auto-naming
			advtrains.interlocking.add_autoname_to_tcbs(tcbs, pname)
		end
	end
end

-- Create an can_dig callback for a self-contained TCB node. The parameters control additional behavior:
-- is_signal: (boolean) Whether this node is also a signal (in addition to being a TCB), e.g. when auto_create_self_signal was set.
--            Causes also the signal API's can_dig to be called
function advtrains.interlocking.self_tcb_make_can_dig_callback(is_signal)
	return function(pos, player)
		local pname = player and player:get_player_name() or ""
		-- need to duplicate logic of the regular "can_dig_or_modify_track()" function in core/tracks.lua
		if advtrains.get_train_at_pos(pos) then
			minetest.chat_send_player(pname, "Can't remove track, a train is here!")
			return false
		end
		-- end of standard checks
		local tcb = ildb.get_tcb(pos)
		if not tcb then
			-- digging always allowed because the TCB hasn't been created (unless signal callback interjects)
			if is_signal then
				return advtrains.interlocking.signal.can_dig(pos, player)
			else
				return true
			end
		end
		-- TCB exists
		if not minetest.check_player_privs(pname, "interlocking") then
			return false
		end
		-- fine to remove (unless signal callback interjects)
		if is_signal then
			return advtrains.interlocking.signal.can_dig(pos, player)
		else
			return true
		end
	end
end

-- Create an after_dig_node callback for a self-contained TCB node. The parameters control additional behavior:
-- is_signal: (boolean) Whether this node is also a signal (in addition to being a TCB), e.g. when auto_create_self_signal was set.
--            Causes also the signal API's after_dig_node to be called
function advtrains.interlocking.self_tcb_make_after_dig_callback(is_signal)
	return function(pos, oldnode, oldmetadata, player)
		local pname = player:get_player_name()
		if is_signal then
			-- "dig" the signal first
			advtrains.interlocking.signal.after_dig(pos, oldnode, oldmetadata, player)
		end
		if ildb.get_tcb(pos) then
			-- remove the TCB
			ildb.remove_tcb_at(pos, pname, true)
		end
	end
end

-- Create an on_rightclick callback for a self-contained TCB node. The rightclick callback tries to repeat the TCB assignment
-- if necessary and otherwise shows the TCB formspec. The parameters control additional behavior:
-- fail_silently_on_noprivs: (boolean) Does not give an error in case the placer does not have the interlocking privilege
-- auto_create_self_signal: (boolean) Automatically assign this same node as signal to the B side of the
--                           newly-created TCB if that has not already happened during place.
--                           Otherwise, opens the signal dialog instead of the TCB dialog on rightclick
function advtrains.interlocking.self_tcb_make_on_rightclick_callback(fail_silently_on_noprivs, auto_create_self_signal)
	return function(pos, node, player, itemstack, pointed_thing)
		local pname = player:get_player_name()
		if not minetest.check_player_privs(pname, "interlocking") then
			if not fail_silently_on_noprivs then
				minetest.chat_send_player(pname, "Insufficient privileges to use this!")
			end
			return
		end
		if ildb.get_tcb(pos) then
			-- TCB already here. go on
		else
			-- otherwise create tcb
			ildb.create_tcb_at(pos, pname)
		end
		if auto_create_self_signal then
			local sigd = { p = pos, s = 1 }
			local tcbs = ildb.get_tcbs(sigd)
			-- make sure signal doesn't already exist
			if not tcbs.signal then
				-- go ahead and assign signal
				ildb.assign_signal_to_tcbs(pos, sigd)
				-- assign influence point to itself
				ildb.set_ip_signal(advtrains.roundfloorpts(pos), 1, pos)
				-- use auto-naming
				advtrains.interlocking.add_autoname_to_tcbs(tcbs, pname)
			end
			-- in any case open the signalling form nouw
			local control = player:get_player_control()
			advtrains.interlocking.show_signal_form(pos, node, pname, control.aux1)
			return
		else
			-- not an autosignal. Then show the TCB form
			advtrains.interlocking.show_tcb_form(pos, pname)
			return
		end
	end
end

-- TCB Form

local function mktcbformspec(pos, side, tcbs, offset, pname)
	local form = ""
	local btnpref = side==1 and "A" or "B"
	local ts
	-- ensure that mapping and xlink are up to date
	ildb.tcbs_ensure_ts_ref_exists({p=pos, s=side, tcbs=tcbs})
	ildb.validate_tcb_xlink({p=pos, s=side, tcbs=tcbs})
	-- Note: repair operations may have been triggered by this
	if tcbs.ts_id then
		ts = ildb.get_ts(tcbs.ts_id)
	end
	if ts then
		form = form.."label[0.5,"..offset..";Side "..btnpref..": "..minetest.formspec_escape(ts.name or tcbs.ts_id).."]"
		form = form.."button[0.5,"..(offset+0.5)..";5,1;"..btnpref.."_gotots;Show track section]"
	else
		tcbs.ts_id = nil
		form = form.."label[0.5,"..offset..";Side "..btnpref..": ".."End of interlocking]"
		form = form.."button[0.5,"..(offset+0.5)..";5,1;"..btnpref.."_makeil;Create Interlocked Track Section]"
	end
	-- xlink
	if tcbs.xlink then
		form = form.."label[0.5,"..(offset+1.5)..";Link:"..ildb.sigd_to_string(tcbs.xlink).."]"
		form = form.."button[4.5,"..(offset+1.5)..";1,1;"..btnpref.."_xlinkdel;X]"
	else
		if players_assign_xlink[pname] then
			form = form.."button[0.5,"..(offset+1.5)..";4,1;"..btnpref.."_xlinklink;Link "..ildb.sigd_to_string(players_assign_xlink[pname]).."]"
			form = form.."button[4.5,"..(offset+1.5)..";1,1;"..btnpref.."_xlinkabrt;X]"
		else
			form = form.."label[0.5,"..(offset+1.5)..";No Link]"
			form = form.."button[4.5,"..(offset+1.5)..";1,1;"..btnpref.."_xlinkadd;+]"
		end
	end
	if tcbs.signal then
		form = form.."button[0.5,"..(offset+2.5)..";5,1;"..btnpref.."_sigdia;Signalling]"	
	else
		form = form.."button[0.5,"..(offset+2.5)..";5,1;"..btnpref.."_asnsig;Assign a signal]"
	end
	return form
end


function advtrains.interlocking.show_tcb_form(pos, pname)
	if not minetest.check_player_privs(pname, "interlocking") then
		minetest.chat_send_player(pname, "Insufficient privileges to use this!")
		return
	end
	local tcb = ildb.get_tcb(pos)
	if not tcb then return end
	
	local form = "size[6,9] label[0.5,0.5;Track Circuit Break Configuration]"
	form = form .. mktcbformspec(pos, 1, tcb[1], 1, pname)
	form = form .. mktcbformspec(pos, 2, tcb[2], 5, pname)
	
	minetest.show_formspec(pname, "at_il_tcbconfig_"..minetest.pos_to_string(pos), form)
	advtrains.interlocking.show_tcb_marker(pos)
end

--helper: length of nil table is 0
local function nlen(t)
	if not t then return 0 end
	return #t
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
	local pts = string.match(formname, "^at_il_tcbconfig_(.+)$")
	local pos
	if pts then
		pos = minetest.string_to_pos(pts)
	end
	if pos and not fields.quit then
		local tcb = ildb.get_tcb(pos)
		if not tcb then return end
		local f_gotots = {fields.A_gotots, fields.B_gotots}
		local f_makeil = {fields.A_makeil, fields.B_makeil}
		local f_asnsig = {fields.A_asnsig, fields.B_asnsig}
		local f_sigdia = {fields.A_sigdia, fields.B_sigdia}
		local f_xlinkadd = {fields.A_xlinkadd, fields.B_xlinkadd}
		local f_xlinkdel = {fields.A_xlinkdel, fields.B_xlinkdel}
		local f_xlinklink = {fields.A_xlinklink, fields.B_xlinklink}
		local f_xlinkabrt = {fields.A_xlinkabrt, fields.B_xlinkabrt}
		
		for connid=1,2 do
			local tcbs = tcb[connid]
			if tcbs.ts_id then
				if f_gotots[connid] then
					advtrains.interlocking.show_ts_form(tcbs.ts_id, pname)
					return
				end
			else
				if f_makeil[connid] then
					if not tcbs.ts_id then
						ildb.create_ts_from_tcbs({p=pos, s=connid})
					end
				end
			end
			if tcbs.xlink then
				if f_xlinkdel[connid] then
					ildb.remove_tcb_xlink({p=pos, s=connid})
				end
			else
				local osigd = players_assign_xlink[pname]
				if osigd then
					if f_xlinklink[connid] then
						ildb.add_tcb_xlink({p=pos, s=connid}, osigd)
						players_assign_xlink[pname] = nil
					elseif f_xlinkabrt[connid] then
						players_assign_xlink[pname] = nil
					end
				else
					if f_xlinkadd[connid] then
						players_assign_xlink[pname] = {p=pos, s=connid}
						minetest.chat_send_player(pname, "TCB Link: Select linked TCB now!")
						minetest.close_formspec(pname, formname)
						return -- to not reopen form
					end
				end
			end
			if f_asnsig[connid] and not tcbs.signal then
				minetest.chat_send_player(pname, "Configuring TCB: Please punch the signal to assign.")
				players_assign_signal[pname] = {p=pos, s=connid}
				minetest.close_formspec(pname, formname)
				return
			end
			if f_sigdia[connid] and tcbs.signal then
				advtrains.interlocking.show_signalling_form({p=pos, s=connid}, pname)
				return
			end

		end
		advtrains.interlocking.show_tcb_form(pos, pname)
	end

end)



-- TS Formspec

function advtrains.interlocking.show_ts_form(ts_id, pname)
	if not minetest.check_player_privs(pname, "interlocking") then
		minetest.chat_send_player(pname, "Insufficient privileges to use this!")
		return
	end
	local ts = ildb.get_ts(ts_id)
	if not ts_id then return end
	
	local form = "size[10.5,10]label[0.5,0.5;Track Section Detail - "..ts_id.."]"
	form = form.."field[0.8,2;5.2,1;name;Section name;"..minetest.formspec_escape(ts.name or "").."]"