nosniff Content-Security-Policy: default-src 'none' Content-Type: text/plain; charset=UTF-8 Content-Length: 8608 Content-Disposition: inline; filename="nodedb.lua" Last-Modified: Thu, 22 Oct 2026 08:56:30 GMT Expires: Sun, 19 Oct 2036 08:56:30 GMT ETag: "45a51feadc8e3b6b02ce78af6e1c389e5fee67b5" --nodedb.lua --database of all nodes that have 'save_in_at_nodedb' field set to true in node definition --serialization format: --(2byte z) (2byte y) (2byte x) (2byte contentid) --contentid := (14bit nodeid, 2bit param2) local function int_to_bytes(i) local x=i+32768--clip to positive integers local cH = math.floor(x / 256) % 256; local cL = math.floor(x ) % 256; return(string.char(cH, cL)); end local function bytes_to_int(bytes) local t={string.byte(bytes,1,-1)} local n = t[1] * 256 + t[2] return n-32768 end local function l2b(x) return x%4 end local function u14b(x) return math.floor(x/4) end local ndb={} --local variables for performance local ndb_nodeids={} local ndb_nodes={} local function ndbget(x,y,z) local ny=ndb_nodes[y] if ny then local nx=ny[x] if nx then return nx[z] end end return nil end local function ndbset(x,y,z,v) if not ndb_nodes[y] then ndb_nodes[y]={} end if not ndb_nodes[y][x] then ndb_nodes[y][x]={} end ndb_nodes[y][x][z]=v end local path=minetest.get_worldpath().."/advtrains_ndb2" --load --nodeids get loaded by advtrains init.lua and passed here function ndb.load_data(data) ndb_nodeids = data and data.nodeids or {} local file, err = io.open(path, "r") if not file then atwarn("Couldn't load the node database: ", err or "Unknown Error") else local cnt=0 local hst_z=file:read(2) local hst_y=file:read(2) local hst_x=file:read(2) local cid=file:read(2) while hst_z and hst_y and hst_x and cid and #hst_z==2 and #hst_y==2 and #hst_x==2 and #cid==2 do ndbset(bytes_to_int(hst_x), bytes_to_int(hst_y), bytes_to_int(hst_z), bytes_to_int(cid)) cnt=cnt+1 hst_z=file:read(2) hst_y=file:read(2) hst_x=file:read(2) cid=file:read(2) end atlog("nodedb: read", cnt, "nodes.") file:close() end end --save function ndb.save_data() local file, err = io.open(path, "w") if not file then atwarn("Couldn't save the node database: ", err or "Unknown Error") else for y, ny in pairs(ndb_nodes) do for x, nx in pairs(ny) do for z, cid in pairs(nx) do file:write(int_to_bytes(z)) file:write(int_to_bytes(y)) file:write(int_to_bytes(x)) file:write(int_to_bytes(cid)) end end end file:close() end return {nodeids = ndb_nodeids} end --function to get node. track database is not helpful here. function ndb.get_node_or_nil(pos) -- FIX for bug found on linuxworks server: -- a loaded node might get read before the LBM has updated its state, resulting in wrongly set signals and switches -- -> Using the saved node prioritarily. local node = ndb.get_node_raw(pos) if node then return node else --try reading the node from the map return minetest.get_node_or_nil(pos) end end function ndb.get_node(pos) local n=ndb.get_node_or_nil(pos) if not n then return {name="ignore", param2=0} end return n end function ndb.get_node_raw(pos) local cid=ndbget(pos.x, pos.y, pos.z) if cid then local nodeid = ndb_nodeids[u14b(cid)] if nodeid then return {name=nodeid, param2 = l2b(cid)} end end return nil end function ndb.swap_node(pos, node) minetest.swap_node(pos, node) ndb.update(pos, node) end function ndb.update(pos, pnode) local node = pnode or minetest.get_node_or_nil(pos) if not node or node.name=="ignore" then return end if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].groups.save_in_at_nodedb then local nid for tnid, nname in pairs(ndb_nodeids) do if nname==node.name then nid=tnid end end if not nid then nid=#ndb_nodeids+1 ndb_nodeids[nid]=node.name end ndbset(pos.x, pos.y, pos.z, (nid * 4) + (l2b(node.param2 or 0)) ) --atprint("nodedb: updating node", pos, "stored nid",nid,"assigned",ndb_nodeids[nid],"resulting cid",ndb_nodes[hash]) else --at this position there is no longer a node that needs to be tracked. ndbset(pos.x, pos.y, pos.z, nil) end end function ndb.clear(pos) ndbset(pos.x, pos.y, pos.z, nil) end --get_node with pseudoload. now we only need track data, so we can use the trackdb as second fallback --nothing new will be saved inside the trackdb. --returns: --true, conn1, conn2, rely1, rely2, railheight in case everything's right. --false if it's not a rail or the train does not drive on this rail, but it is loaded or --nil if the node is neither loaded nor in trackdb --the distraction between false and nil will be needed only in special cases.(train initpos) function advtrains.get_rail_info_at(pos, drives_on) local rdp=advtrains.round_vector_floor_y(pos) local node=ndb.get_node_or_nil(rdp) --still no node? --advtrains.trackdb is nil when there's no data available. if not node then if advtrains.trackdb then --try raildb (see trackdb_legacy.lua) local dbe=(advtrains.trackdb[rdp.y] and advtrains.trackdb[rdp.y][rdp.x] and advtrains.trackdb[rdp.y][rdp.x][rdp.z]) if dbe then for tt,_ in pairs(drives_on) do if not dbe.tracktype or tt==dbe.tracktype then return true, dbe.conn1, dbe.conn2, dbe.rely1 or 0, dbe.rely2 or 0, dbe.railheight or 0 end end end end return nil end local nodename=node.name if(not advtrains.is_track_and_drives_on(nodename, drives_on)) then return false end local conn1, conn2, rely1, rely2, railheight, tracktype=advtrains.get_track_connections(node.name, node.param2) return true, conn1, conn2, rely1, rely2, railheight end ndb.run_lbm = function(pos, node) return advtrains.pcall(function() local cid=ndbget(pos.x, pos.y, pos.z) if cid then --if in database, detect changes and apply. local nodeid = ndb_nodeids[u14b(cid)] local param2 = l2b(cid) if not nodeid then --something went wrong atwarn("Node Database corruption, couldn't determine node to set at", pos) ndb.update(pos, node) else if (nodeid~=node.name or param2~=node.param2) then atprint("nodedb: lbm replaced", pos, "with nodeid", nodeid, "param2", param2, "cid is", cid) minetest.swap_node(pos, {name=nodeid, param2 = param2}) local ndef=minetest.registered_nodes[nodeid] if ndef and ndef.on_updated_from_nodedb then ndef.on_updated_from_nodedb(pos, node) end return true end end else --if not in database, take it. atlog("Node Database:", pos, "was not found in the database, have you used worldedit?") ndb.update(pos, node) end return false end) end minetest.register_lbm({ name = "advtrains:nodedb_on_load_update", nodenames = {"group:save_in_at_nodedb"}, run_at_every_load = true, run_on_every_load = true, action = ndb.run_lbm, interval=30, chance=1, }) --used when restoring stuff after a crash ndb.restore_all = function() atwarn("Updating the map from the nodedb, this may take a while") local cnt=0 for y, ny in pairs(ndb_nodes) do for x, nx in pairs(ny) do for z, _ in pairs(nx) do local pos={x=x, y=y, z=z} local node=minetest.get_node_or_nil(pos) if node then local ori_ndef=minetest.registered_nodes[node.name] local ndbnode=ndb.get_node_raw(pos) if ori_ndef and ori_ndef.groups.save_in_at_nodedb then --check if this node has been worldedited, and don't replace then if (ndbnode.name~=node.name or ndbnode.param2~=node.param2) then minetest.swap_node(pos, ndbnode) atwarn("Replaced",node.name,"@",pos,"with",ndbnode.name) end else ndb.clear(pos) atwarn("Found ghost node (former",ndbnode.name,") @",pos,"deleting") end end end end end atwarn("Updated",cnt,"nodes") end minetest.register_on_dignode(function(pos, oldnode, digger) return advtrains.pcall(function() ndb.clear(pos) end) end) function ndb.get_nodes() return ndb_nodes end function ndb.get_nodeids() return ndb_nodeids end advtrains.ndb=ndb local ptime=0 minetest.register_chatcommand("at_sync_ndb", { params = "", -- Short parameter description description = "Write node db back to map and find ghost nodes", -- Full description privs = {train_operator=true, worldedit=true}, -- Require the "privs" privilege to run func = function(name, param) return advtrains.pcall(function() if not minetest.check_player_privs(name, {server=true}) and os.time() < ptime+30 then return false, "Please wait at least 30s from the previous execution of /at_restore_ndb!" end ndb.restore_all() ptime=os.time() return true end) end, privs = {train_operator=true}, -- Require the "privs" privilege to run }) f='#n161'>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
local S = atltrans

local ac = {nodes={}}

function ac.load(data)
	if data then
		ac.nodes=data.nodes
	end
end
function ac.save()
	return {nodes = ac.nodes}
end

function ac.after_place_node(pos, player)
	local meta=minetest.get_meta(pos)
	meta:set_string("formspec", ac.getform(pos, meta))
	meta:set_string("infotext", S("Unconfigured LuaATC component"))
	local ph=minetest.pos_to_string(pos)
	--just get first available key!
	for en,_ in pairs(atlatc.envs) do
		ac.nodes[ph]={env=en}
		return
	end
end
function ac.getform(pos, meta_p)
	local meta = meta_p or minetest.get_meta(pos)
	local envs_asvalues={}
	
	local ph=minetest.pos_to_string(pos)
	local nodetbl = ac.nodes[ph]
	local env, code, err = nil, "", ""
	if nodetbl then
		code=nodetbl.code or ""
		err=nodetbl.err or ""
		env=nodetbl.env or ""
	end
	local sel = 1
	for n,_ in pairs(atlatc.envs) do
		envs_asvalues[#envs_asvalues+1]=minetest.formspec_escape(n)
		if n==env then
			sel=#envs_asvalues
		end
	end
	local form = "size["..atlatc.CODE_FORM_SIZE.."]"
		.."style[code;font=mono]"
		.."label[0,-0.1;"..S("LuaATC Environment").."]"
		.."dropdown[0,0.3;3;env;"..table.concat(envs_asvalues, ",")..";"..sel.."]"
		.."button[5,0.2;2,1;save;"..S("Save").."]"
		.."button[7,0.2;3,1;cle;"..S("Clear Local Environment").."]"
		.."textarea[0.3,1.5;"..atlatc.CODE_FORM_SIZE..";code;"..S("Code")..";"..minetest.formspec_escape(code).."]"
		.."label["..atlatc.CODE_FORM_ERRLABELPOS..";"..err.."]"
	return form
end

function ac.after_dig_node(pos, node, player)
	advtrains.invalidate_all_paths(pos)
	advtrains.ndb.clear(pos)
	atlatc.interrupt.clear_ints_at_pos(pos)
	if advtrains.lines and advtrains.lines.sched then
		advtrains.lines.sched.discard_all(advtrains.encode_pos(pos))
	end
	local ph=minetest.pos_to_string(pos)
	ac.nodes[ph]=nil
end

function ac.on_receive_fields(pos, formname, fields, player)
	if not minetest.check_player_privs(player:get_player_name(), {atlatc=true}) then
		minetest.chat_send_player(player:get_player_name(), S("You are not allowed to configure this LuaATC component without the @1 privilege.", "atlatc"))
		return
	end
	
	local meta=minetest.get_meta(pos)
	local ph=minetest.pos_to_string(pos)
	local nodetbl = ac.nodes[ph] or {}
	--if fields.quit then return end
	if fields.env then
		nodetbl.env=fields.env
	end
	if fields.code then
		nodetbl.code=fields.code
	end
	if fields.save then
		-- reset certain things
		nodetbl.err=nil
		if advtrains.lines and advtrains.lines.sched then
			-- discard all schedules for this node
			advtrains.lines.sched.discard_all(advtrains.encode_pos(pos))
		end
	end
	if fields.cle then
		nodetbl.data={}
	end
	
	ac.nodes[ph]=nodetbl
	
	meta:set_string("formspec", ac.getform(pos, meta))
	if nodetbl.env then
		meta:set_string("infotext", S("LuaATC component assigned to environment '@1'", nodetbl.env))
	else
		meta:set_string("infotext", S("LuaATC component assigned to an invalid environment"))
	end
end

function ac.run_in_env(pos, evtdata, customfct_p, ignore_no_code)
	local ph=minetest.pos_to_string(pos)
	local nodetbl = ac.nodes[ph]
	if not nodetbl then
		atwarn("LuaATC component at",ph,": Data not in memory! Please visit component and click 'Save'!")
		return
	end
	
	local meta
	if advtrains.is_node_loaded(pos) then
		meta=minetest.get_meta(pos)
	end
	
	if not nodetbl.env or not atlatc.envs[nodetbl.env] then
		atwarn("LuaATC component at",ph,": Not an existing environment: "..(nodetbl.env or "<nil>"))
		return false
	end
	local env = atlatc.envs[nodetbl.env]
	if not nodetbl.code or nodetbl.code=="" then
		if not ignore_no_code then
			env:log("warning", "LuaATC component at",ph,": No code to run! (insert -- to suppress warning)")
		end
		return false
	end
	
	local customfct=customfct_p or {}
	-- add interrupt function
	customfct.interrupt=function(t, imesg)
		assertt(t, "number")
		assert(t >= 0)
		atlatc.interrupt.add(t, pos, {type="int", int=true, message=imesg, msg=imesg}) --Compatiblity "message" field.
	end
	customfct.interrupt_safe=function(t, imesg)
		assertt(t, "number")
		assert(t >= 0)
		if atlatc.interrupt.has_at_pos(pos) then
			return false
		end
		atlatc.interrupt.add(t, pos, {type="int", int=true, message=imesg, msg=imesg}) --Compatiblity "message" field.
		return true
	end
	customfct.clear_interrupts=function()
		atlatc.interrupt.clear_ints_at_pos(pos)
	end
	-- add digiline_send function, if digiline is loaded
	if minetest.global_exists("digiline") then
		customfct.digiline_send=function(channel, msg)
			assertt(channel, "string")
			if advtrains.is_node_loaded(pos) then
				digiline:receptor_send(pos, digiline.rules.default, channel, msg)
			end
		end
	end
	-- add lines scheduler if enabled
	if advtrains.lines and advtrains.lines.sched then
		customfct.schedule = function(rwtime, msg)
			return advtrains.lines.sched.enqueue(rwtime, "atlatc_env", {pos=pos, msg=msg}, advtrains.encode_pos(pos), 1)
		end
		customfct.schedule_in = function(rwtime, msg)
			return advtrains.lines.sched.enqueue_in(rwtime, "atlatc_env", {pos=pos, msg=msg}, advtrains.encode_pos(pos), 1)
		end
	end
	
	local datain=nodetbl.data or {}
	local succ, dataout = env:execute_code(datain, nodetbl.code, evtdata, customfct)
	if succ then
		atlatc.active.nodes[ph].data=atlatc.remove_invalid_data(dataout)
	else
		atlatc.active.nodes[ph].err=dataout
		env:log("error", "LuaATC component at",ph,": LUA Error:",dataout)
		if meta then
			meta:set_string("infotext", S("LuaATC component with error: @1", dataout))
		end
		--TODO temporary
		--if customfct.atc_id then
		--	advtrains.drb_dump(customfct.atc_id)
		--	error("Debug: LuaATC error hit!")
		--end
	end
	if meta then
		meta:set_string("formspec", ac.getform(pos, meta))
	end
end

function ac.on_digiline_receive(pos, node, channel, msg)
	atlatc.interrupt.add(0, pos, {type="digiline", digiline=true, channel = channel, msg = msg})
end

if advtrains.lines and advtrains.lines.sched then
	advtrains.lines.sched.register_callback("atlatc_env", function(data)
		-- This adds another interrupt to the atlatc queue... there might be a better way
		atlatc.interrupt.add(0, data.pos, {type="schedule",schedule=true, msg=data.msg})
	end)
end

ac.trackdef_advtrains_defs = {
	on_train_enter = function(pos, train_id)
		--do async. Event is fired in train steps
		atlatc.interrupt.add(0, pos, {type="train", train=true, id=train_id})
	end,
}

atlatc.active=ac