mirror of
				https://github.com/luanti-org/luanti.git
				synced 2025-10-30 23:15:32 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			175 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			175 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| -- This is an implementation of a job sheduling mechanism. It guarantees that
 | |
| -- coexisting jobs will execute primarily in order of least expiry, and
 | |
| -- secondarily in order of first registration.
 | |
| 
 | |
| -- These functions implement an intrusive singly linked list of one or more
 | |
| -- elements where the first element has a pointer to the last. The next pointer
 | |
| -- is stored with key list_next. The pointer to the last is with key list_end.
 | |
| 
 | |
| local function list_init(first)
 | |
| 	first.list_end = first
 | |
| end
 | |
| 
 | |
| local function list_append(first, append)
 | |
| 	first.list_end.list_next = append
 | |
| 	first.list_end = append
 | |
| end
 | |
| 
 | |
| local function list_append_list(first, first_append)
 | |
| 	first.list_end.list_next = first_append
 | |
| 	first.list_end = first_append.list_end
 | |
| end
 | |
| 
 | |
| -- The jobs are stored in a map from expiration times to linked lists of jobs
 | |
| -- as above. The expiration times are also stored in an array representing a
 | |
| -- binary min heap, which is a particular arrangement of binary tree. A parent
 | |
| -- at index i has children at indices i*2 and i*2+1. Out-of-bounds indices
 | |
| -- represent nonexistent children. A parent is never greater than its children.
 | |
| -- This structure means that, if there is at least one job, the next expiration
 | |
| -- time is the first item in the array.
 | |
| 
 | |
| -- Push element on a binary min-heap,
 | |
| -- "bubbling up" the element by swapping with larger parents.
 | |
| local function heap_push(heap, element)
 | |
| 	local index = #heap + 1
 | |
| 	while index > 1 do
 | |
| 		local parent_index = math.floor(index / 2)
 | |
| 		local parent = heap[parent_index]
 | |
| 		if element < parent then
 | |
| 			heap[index] = parent
 | |
| 			index = parent_index
 | |
| 		else
 | |
| 			break
 | |
| 		end
 | |
| 	end
 | |
| 	heap[index] = element
 | |
| end
 | |
| 
 | |
| -- Pop smallest element from the heap,
 | |
| -- "sinking down" the last leaf on the last layer of the heap
 | |
| -- by swapping with the smaller child.
 | |
| local function heap_pop(heap)
 | |
| 	local removed_element = heap[1]
 | |
| 	local length = #heap
 | |
| 	local element = heap[length]
 | |
| 	heap[length] = nil
 | |
| 	length = length - 1
 | |
| 	if length > 0 then
 | |
| 		local index = 1
 | |
| 		while true do
 | |
| 			local old_index = index
 | |
| 			local smaller_element = element
 | |
| 			local left_index = index * 2
 | |
| 			local right_index = index * 2 + 1
 | |
| 			if left_index <= length then
 | |
| 				local left_element = heap[left_index]
 | |
| 				if left_element < smaller_element then
 | |
| 					index = left_index
 | |
| 					smaller_element = left_element
 | |
| 				end
 | |
| 			end
 | |
| 			if right_index <= length then
 | |
| 				if heap[right_index] < smaller_element then
 | |
| 					index = right_index
 | |
| 				end
 | |
| 			end
 | |
| 			if old_index ~= index then
 | |
| 				heap[old_index] = heap[index]
 | |
| 			else
 | |
| 				break
 | |
| 			end
 | |
| 		end
 | |
| 		heap[index] = element
 | |
| 	end
 | |
| 	return removed_element
 | |
| end
 | |
| 
 | |
| local job_map = {}
 | |
| local expiries = {}
 | |
| 
 | |
| -- Adds an individual job with the given expiry.
 | |
| -- The worst-case complexity is O(log n), where n is the number of distinct
 | |
| -- expiration times.
 | |
| local function add_job(expiry, job)
 | |
| 	local list = job_map[expiry]
 | |
| 	if list then
 | |
| 		list_append(list, job)
 | |
| 	else
 | |
| 		list_init(job)
 | |
| 		job_map[expiry] = job
 | |
| 		heap_push(expiries, expiry)
 | |
| 	end
 | |
| end
 | |
| 
 | |
| -- Removes the next expiring jobs and returns the linked list of them.
 | |
| -- The worst-case complexity is O(log n), where n is the number of distinct
 | |
| -- expiration times.
 | |
| local function remove_first_jobs()
 | |
| 	local removed_expiry = heap_pop(expiries)
 | |
| 	local removed = job_map[removed_expiry]
 | |
| 	job_map[removed_expiry] = nil
 | |
| 	return removed
 | |
| end
 | |
| 
 | |
| local time = 0.0
 | |
| local time_next = math.huge
 | |
| 
 | |
| core.register_globalstep(function(dtime)
 | |
| 	time = time + dtime
 | |
| 
 | |
| 	if time < time_next then
 | |
| 		return
 | |
| 	end
 | |
| 
 | |
| 	-- Remove the expired jobs.
 | |
| 	local expired = remove_first_jobs()
 | |
| 
 | |
| 	-- Remove other expired jobs and append them to the list.
 | |
| 	while true do
 | |
| 		time_next = expiries[1] or math.huge
 | |
| 		if time_next > time then
 | |
| 			break
 | |
| 		end
 | |
| 		list_append_list(expired, remove_first_jobs())
 | |
| 	end
 | |
| 
 | |
| 	-- Run the callbacks afterward to prevent infinite loops with core.after(0, ...).
 | |
| 	local last_expired = expired.list_end
 | |
| 	while true do
 | |
| 		core.set_last_run_mod(expired.mod_origin)
 | |
| 		expired.func(unpack(expired.args, 1, expired.args.n))
 | |
| 		if expired == last_expired then
 | |
| 			break
 | |
| 		end
 | |
| 		expired = expired.list_next
 | |
| 	end
 | |
| end)
 | |
| 
 | |
| local job_metatable = {__index = {}}
 | |
| 
 | |
| local function dummy_func() end
 | |
| function job_metatable.__index:cancel()
 | |
| 	self.func = dummy_func
 | |
| 	self.args = {n = 0}
 | |
| end
 | |
| 
 | |
| function core.after(after, func, ...)
 | |
| 	assert(tonumber(after) and not core.is_nan(after) and type(func) == "function",
 | |
| 		"Invalid core.after invocation")
 | |
| 
 | |
| 	local new_job = {
 | |
| 		mod_origin = core.get_last_run_mod(),
 | |
| 		func = func,
 | |
| 		args = {
 | |
| 			n = select("#", ...),
 | |
| 			...
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	local expiry = time + after
 | |
| 	add_job(expiry, new_job)
 | |
| 	time_next = math.min(time_next, expiry)
 | |
| 
 | |
| 	return setmetatable(new_job, job_metatable)
 | |
| end
 |