summaryrefslogtreecommitdiff
path: root/src/content_mapblock.cpp
Commit message (Expand)AuthorAge
...
* | | extended content-type rangePerttu Ahola2011-07-23
| |/ |/|
* | changed one "if" to "else if" in content_mapblock.cppPerttu Ahola2011-06-26
|/
* New map generator added (and SQLite, messed up the commits at that time...) (...Perttu Ahola2011-06-25
* Moved stuff from mapblock{h,cpp} to mapblock_mesh.{h,cpp} and content_mapbloc...Perttu Ahola2011-06-17
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
-- lzb.lua
-- Enforced and/or automatic train override control, providing the on_train_approach callback

--[[
Documentation of train.lzb table
train.lzb = {
	trav_index = Current index that the traverser has advanced so far
	checkpoints = table containing oncoming signals, in order of index
		{
			pos = position of the point
			index = where this is on the path
			speed = speed allowed to pass. nil = no effect
			callback = function(pos, id, train, index, speed, lzbdata)
			-- Function that determines what to do on the train in the moment it drives over that point.
			-- When spd==0, called instead when train has stopped in front
			-- nil = no effect
			lzbdata = {}
			-- Table of custom data filled in by approach callbacks
			-- Whenever an approach callback inserts an LZB checkpoint with changed lzbdata,
			-- all consecutive approach callbacks will see these passed as lzbdata table.
			
			udata = arbitrary user data, no official way to retrieve (do not use)
		}
	trav_lzbdata = currently active lzbdata table at traverser index
}
The LZB subsystem keeps track of "checkpoints" the train will pass in the future, and has two main tasks:
1. run approach callbacks, and run callbacks when passing LZB checkpoints
2. keep track of the permitted speed at checkpoints, and make sure that the train brakes accordingly
To perform 2, it populates the train.path_speed table which is handled along with the path subsystem.
This table is used in trainlogic.lua/train_step_b() and applied to the velocity calculations.

Note: in contrast to node enter callbacks, which are called when the train passes the .5 index mark, LZB callbacks are executed on passing the .0 index mark!
If an LZB checkpoint has speed 0, the train will still enter the node (the enter callback will be called), but will stop at the 0.9 index mark (for details, see SLOW_APPROACH in trainlogic.lua)

The start point for the LZB traverser (and thus the first node that will receive an approach callback) is floor(train.index) + 1. This means, once the LZB checkpoint callback has fired,
this path node will not receive any further approach callbacks for the same approach situation
]]


local params = {
	BRAKE_SPACE = 10,
	AWARE_ZONE  = 50,

	ADD_STAND  =  2.5,
	ADD_SLOW   =  1.5,
	ADD_FAST   =  7,
	ZONE_ROLL  =  2,
	ZONE_HOLD  =  5, -- added on top of ZONE_ROLL
	ZONE_VSLOW =  3, -- When speed is <2, still allow accelerating

	DST_FACTOR =  1.5,

	SHUNT_SPEED_MAX = advtrains.SHUNT_SPEED_MAX,
}

function advtrains.set_lzb_param(par, val)
	if params[par] and tonumber(val) then
		params[par] = tonumber(val)
	else
		error("Inexistant param or not a number")
	end
end

local function resolve_latest_lzbdata(ckp, index)
	local i = #ckp
	local ckpi
	while i>0 do
		ckpi = ckp[i]
		if ckpi.index <= index and ckpi.lzbdata then
			return ckpi.lzbdata
		end
		i=i-1
	end
	return {}
end

local function look_ahead(id, train)
	local lzb = train.lzb
	if lzb.zero_checkpoint then
		-- if the checkpoints list contains a zero checkpoint, don't look ahead
		-- in order to not trigger approach callbacks on the wrong path
		return
	end
	
	local acc = advtrains.get_acceleration(train, 1)
	-- worst-case: the starting point is maximum speed
	local vel = train.max_speed or train.velocity
	local brakedst = ( -(vel*vel) / (2*acc) ) * params.DST_FACTOR
	
	--local brake_i = advtrains.path_get_index_by_offset(train, train.index, brakedst + params.BRAKE_SPACE)
	-- worst case (don't use index_by_offset)
	local brake_i = atfloor(train.index + brakedst + params.BRAKE_SPACE)
	atprint("LZB: looking ahead up to ", brake_i)
	
	--local aware_i = advtrains.path_get_index_by_offset(train, brake_i, AWARE_ZONE)
	
	local trav = lzb.trav_index
	-- retrieve latest lzbdata
	if not lzb.trav_lzbdata then
		lzb.trav_lzbdata = resolve_latest_lzbdata(lzb.checkpoints, trav)
	end
	
	if lzb.trav_lzbdata.off_track then
		--previous position was off track, do not scan any further
	end
	
	while trav <= brake_i and not lzb.zero_checkpoint do
		local pos = advtrains.path_get(train, trav)
		-- check offtrack
		if trav - 1 == train.path_trk_f then
			lzb.trav_lzbdata.off_track = true
			advtrains.lzb_add_checkpoint(train, trav - 1, 0, nil, lzb.trav_lzbdata)
		else
			-- run callbacks
			-- Note: those callbacks are defined in trainlogic.lua for consistency with the other node callbacks
			advtrains.tnc_call_approach_callback(pos, id, train, trav, lzb.trav_lzbdata)
			
		end
		trav = trav + 1
		
	end
	
	lzb.trav_index = trav
	
end
advtrains.lzb_look_ahead = look_ahead


local function call_runover_callbacks(id, train)
	if not train.lzb then return end
	
	local i = 1
	local idx = atfloor(train.index)
	local ckp = train.lzb.checkpoints
	while ckp[i] do
		if ckp[i].index <= idx then
			atprint("LZB: checkpoint run over: i=",ckp[i].index,"s=",ckp[i].speed)
			-- call callback
			local it = ckp[i]
			if it.callback then
				it.callback(it.pos, id, train, it.index, it.speed, train.lzb.lzbdata)
			end
			-- note: lzbdata is always defined as look_ahead was called before
			table.remove(ckp, i)
		else
			i = i + 1
		end
	end
end

-- Flood-fills train.path_speed, based on this checkpoint 
local function apply_checkpoint_to_path(train, checkpoint)
	if not checkpoint.speed then
		return
	end
	atprint("LZB: applying checkpoint: i=",checkpoint.index,"s=",checkpoint.speed)
	
	if checkpoint.speed == 0 then
		train.lzb.zero_checkpoint = true
	end
	
	-- make sure path exists until checkpoint
	local pos = advtrains.path_get(train, checkpoint.index)
	
	local brake_accel = advtrains.get_acceleration(train, 11)
	
	-- start with the checkpoint index at specified speed
	local index = checkpoint.index
	local p_speed -- speed in path_speed
	local c_speed = checkpoint.speed -- calculated speed at current index
	while true do
		p_speed = train.path_speed[index]
		if (p_speed and p_speed <= c_speed) or index < train.index then
			--we're done. train already slower than wanted at this position
			return
		end
		-- insert calculated target speed
		train.path_speed[index] = c_speed
		-- calculate c_speed at previous index
		advtrains.path_get(train, index-1)
		local eldist = train.path_dist[index] - train.path_dist[index-1]
		-- Calculate the start velocity the train would have if it had a end velocity of c_speed and accelerating with brake_accel, after a distance of eldist:
		-- v0² = v1² - 2*a*s
		c_speed = math.sqrt( (c_speed * c_speed) - (2 * brake_accel * eldist) )
		index = index - 1
	end
end

--[[
Distance needed to accelerate from v0 to v1 with constant acceleration a:

         v1 - v0     a   / v1 - v0 \ 2     v1^2 - v0^2
s = v0 * -------  +  - * | ------- |    =  -----------
            a        2   \    a    /           2*a
]]

-- Removes all LZB checkpoints and restarts the traverser at the current train index
function advtrains.lzb_invalidate(train)
	train.lzb = {
		trav_index = atfloor(train.index) + 1,
		checkpoints = {},
	}
end

-- LZB part of path_invalidate_ahead. Clears all checkpoints that are ahead of start_idx
-- in contrast to path_inv_ahead, doesn't complain if start_idx is behind train.index, clears everything then
function advtrains.lzb_invalidate_ahead(train, start_idx)
	if train.lzb then
		local idx = atfloor(start_idx)
		local i = 1
		while train.lzb.checkpoints[i] do
			if train.lzb.checkpoints[i].index >= idx then
				table.remove(train.lzb.checkpoints, i)
			else
				i=i+1
			end
		end
		train.lzb.trav_index = idx
		-- FIX reset trav_lzbdata (look_ahead fetches these when required)
		train.lzb.trav_lzbdata = nil
		-- re-apply all checkpoints to path_speed
		train.path_speed = {}
		train.lzb.zero_checkpoint = false
		for _,ckp in ipairs(train.lzb.checkpoints) do
			apply_checkpoint_to_path(train, ckp)
		end
	end
end

-- Add LZB control point
-- lzbdata: If you modify lzbdata in an approach callback, you MUST add a checkpoint AND pass the (modified) lzbdata into it.
-- If you DON'T modify lzbdata, you MUST pass nil as lzbdata. Always modify the lzbdata table in place, never overwrite it!
-- udata: user-defined data, do not use externally
function advtrains.lzb_add_checkpoint(train, index, speed, callback, lzbdata, udata)
	local lzb = train.lzb
	local pos = advtrains.path_get(train, index)
	local lzbdata_c = nil
	if lzbdata then
		-- make a shallow copy of lzbdata
		lzbdata_c = {}
		for k,v in pairs(lzbdata) do lzbdata_c[k] = v end
	end
	local ckp = {
		pos = pos,
		index = index,
		speed = speed,
		callback = callback,
		lzbdata = lzbdata_c,
		udata = udata,
	}
	table.insert(lzb.checkpoints, ckp)
	
	apply_checkpoint_to_path(train, ckp)
end


advtrains.te_register_on_new_path(function(id, train)
	advtrains.lzb_invalidate(train)
	-- Taken care of in pre-move hook (see train_step_b)
	--look_ahead(id, train)
end)

advtrains.te_register_on_invalidate_ahead(function(id, train, start_idx)
	advtrains.lzb_invalidate_ahead(train, start_idx)
end)

advtrains.te_register_on_update(function(id, train)
	if not train.path or not train.lzb then
		atprint("LZB run: no path on train, skip step")
		return
	end
	-- Note: look_ahead called from train_step_b before applying movement
	-- TODO: if more pre-move hooks are added, make a separate callback hook
	--look_ahead(id, train)
	call_runover_callbacks(id, train)
end, true)