package kha; class TimeTask { public var task: Void->Bool; public var start: Float; public var period: Float; public var duration: Float; public var next: Float; public var id: Int; public var groupId: Int; public var active: Bool; public var paused: Bool; public function new() {} } class FrameTask { public var task: Void->Bool; public var priority: Int; public var id: Int; public var active: Bool; public var paused: Bool; public function new(task: Void->Bool, priority: Int, id: Int) { this.task = task; this.priority = priority; this.id = id; active = true; paused = false; } } class Scheduler { static var timeTasks: Array; static var pausedTimeTasks: Array; static var outdatedTimeTasks: Array; static var timeTasksScratchpad: Array; static inline var timeWarpSaveTime: Float = 10.0; static var frameTasks: Array; static var toDeleteFrame: Array; static var current: Float; static var lastTime: Float; static var lastFrameEnd: Float; static var frame_tasks_sorted: Bool; static var stopped: Bool; static var vsync: Bool; // Html5 target can update display frequency after some delay #if kha_html5 static var onedifhz(get, never): Float; static inline function get_onedifhz(): Float { return 1.0 / Display.primary.frequency; } #else static var onedifhz: Float; #end static var currentFrameTaskId: Int; static var currentTimeTaskId: Int; static var currentGroupId: Int; static var DIF_COUNT = 3; static var maxframetime = 0.5; static var deltas: Array; static var startTime: Float = 0; static var activeTimeTask: TimeTask = null; public static function init(): Void { deltas = new Array(); for (i in 0...DIF_COUNT) deltas[i] = 0; stopped = true; frame_tasks_sorted = true; current = lastTime = lastFrameEnd = realTime(); currentFrameTaskId = 0; currentTimeTaskId = 0; currentGroupId = 0; timeTasks = []; pausedTimeTasks = []; outdatedTimeTasks = []; timeTasksScratchpad = []; frameTasks = []; toDeleteFrame = []; } public static function start(restartTimers: Bool = false): Void { vsync = Window.get(0).vSynced; #if !kha_html5 var hz = Display.primary != null ? Display.primary.frequency : 60; if (hz >= 57 && hz <= 63) hz = 60; onedifhz = 1.0 / hz; #end stopped = false; resetTime(); lastTime = realTime() - startTime; for (i in 0...DIF_COUNT) deltas[i] = 0; if (restartTimers) { for (timeTask in timeTasks) { timeTask.paused = false; } for (frameTask in frameTasks) { frameTask.paused = false; } } } public static function stop(): Void { stopped = true; } public static function isStopped(): Bool { return stopped; } static function warpTimeTasksBack(time: Float, tasks: Array): Void { for (timeTask in tasks) { if (timeTask.start >= time) { timeTask.next = timeTask.start; } else if (timeTask.period > 0) { var sinceStart = time - timeTask.start; var times = Math.ceil(sinceStart / timeTask.period); timeTask.next = timeTask.start + times * timeTask.period; } } } public static function warp(time: Float): Void { if (time < lastTime) { current = time; lastTime = time; lastFrameEnd = time; warpTimeTasksBack(time, outdatedTimeTasks); warpTimeTasksBack(time, timeTasks); for (task in outdatedTimeTasks) { if (task.next >= time) { timeTasksScratchpad.push(task); } } for (task in timeTasksScratchpad) { outdatedTimeTasks.remove(task); } for (task in timeTasksScratchpad) { insertSorted(timeTasks, task); } while (timeTasksScratchpad.length > 0) { timeTasksScratchpad.remove(timeTasksScratchpad[0]); } } else if (time > lastTime) { // TODO: Changing startTime line prevents clients from falling into a // warp-forward-then-wait-for-systemtime-to-catch-up-loop that causes // choppy movement (e.g. every 3rd frame forward 3 times). // But it causes backwards jumps in originally constant movements. // And on HTML5 packets are received while no frames are executed, // which causes the client to overtakes the server and then move // farther away with each packet while being unable to synch back // (backwards warping is not allowed to change startTime). startTime -= (time - lastTime); current = time; lastTime = time; lastFrameEnd = time; executeTimeTasks(time); } } public static function executeFrame(): Void { var real = realTime(); var now: Float = real - startTime; var delta = now - lastTime; var frameEnd: Float = lastFrameEnd; if (delta >= 0) { if (kha.netsync.Session.the() == null) { // tdif = 1.0 / 60.0; //force fixed frame rate if (delta > maxframetime) { startTime += delta - maxframetime; now = real - startTime; delta = maxframetime; frameEnd += delta; } else { if (vsync) { // var measured = delta; // this is optimized not to run at exact speed // but to run as fluid as possible var frames = Math.round(delta / onedifhz); if (frames < 1) { return; } var realdif = frames * onedifhz; delta = realdif; for (i in 0...DIF_COUNT - 2) { delta += deltas[i]; deltas[i] = deltas[i + 1]; } delta += deltas[DIF_COUNT - 2]; delta /= DIF_COUNT; deltas[DIF_COUNT - 2] = realdif; frameEnd += delta; // trace("Measured: " + measured + " Frames: " + frames + " Delta: " + delta + " "); } else { for (i in 0...DIF_COUNT - 1) { deltas[i] = deltas[i + 1]; } deltas[DIF_COUNT - 1] = delta; var next: Float = 0; for (i in 0...DIF_COUNT) { next += deltas[i]; } next /= DIF_COUNT; // delta = interpolated_delta; // average the frame end estimation // lastTime = now; frameEnd += next; } } } else { frameEnd += delta; } lastTime = now; if (!stopped) { // Stop simulation time lastFrameEnd = frameEnd; } // Extend endpoint by paused time (individually paused tasks) for (pausedTask in pausedTimeTasks) { pausedTask.next += delta; } if (stopped) { // Extend endpoint by paused time (running tasks) for (timeTask in timeTasks) { timeTask.next += delta; } } executeTimeTasks(frameEnd); // Maintain outdated task list for (task in outdatedTimeTasks) { if (task.next < frameEnd - timeWarpSaveTime) { timeTasksScratchpad.push(task); } } for (task in timeTasksScratchpad) { outdatedTimeTasks.remove(task); } while (timeTasksScratchpad.length > 0) { timeTasksScratchpad.remove(timeTasksScratchpad[0]); } } current = frameEnd; sortFrameTasks(); for (frameTask in frameTasks) { if (!stopped && !frameTask.paused && frameTask.active) { if (!frameTask.task()) frameTask.active = false; } } for (frameTask in frameTasks) { if (!frameTask.active) { toDeleteFrame.push(frameTask); } } while (toDeleteFrame.length > 0) { frameTasks.remove(toDeleteFrame.pop()); } } static function executeTimeTasks(until: Float) { while (timeTasks.length > 0) { activeTimeTask = timeTasks[0]; if (activeTimeTask.next <= until) { current = activeTimeTask.next; activeTimeTask.next += activeTimeTask.period; timeTasks.remove(activeTimeTask); if (activeTimeTask.active && activeTimeTask.task()) { if (activeTimeTask.period > 0 && (activeTimeTask.duration == 0 || activeTimeTask.duration >= activeTimeTask.start + activeTimeTask.next)) { insertSorted(timeTasks, activeTimeTask); } else { archiveTimeTask(activeTimeTask, until); } } else { activeTimeTask.active = false; archiveTimeTask(activeTimeTask, until); } } else { break; } } activeTimeTask = null; } static function archiveTimeTask(timeTask: TimeTask, frameEnd: Float) { #if sys_server if (timeTask.next > frameEnd - timeWarpSaveTime) { outdatedTimeTasks.push(timeTask); } #end } /** * An approximation of the amount of time (in fractional seconds) that elapsed while the game was active. * This value is optimized for achieving smooth framerates. */ public static function time(): Float { return current; } /** * The amount of time (in fractional seconds) that elapsed since the game started. */ public static function realTime(): Float { return System.time; } public static function resetTime(): Void { var now = System.time; var dif = now - startTime; startTime = now; for (timeTask in timeTasks) { timeTask.start -= dif; timeTask.next -= dif; } for (i in 0...DIF_COUNT) deltas[i] = 0; current = 0; lastTime = 0; lastFrameEnd = 0; } public static function addBreakableFrameTask(task: Void->Bool, priority: Int): Int { frameTasks.push(new FrameTask(task, priority, ++currentFrameTaskId)); frame_tasks_sorted = false; return currentFrameTaskId; } public static function addFrameTask(task: Void->Void, priority: Int): Int { return addBreakableFrameTask(function() { task(); return true; }, priority); } public static function pauseFrameTask(id: Int, paused: Bool): Void { for (frameTask in frameTasks) { if (frameTask.id == id) { frameTask.paused = paused; break; } } } public static function removeFrameTask(id: Int): Void { for (frameTask in frameTasks) { if (frameTask.id == id) { frameTask.active = false; break; } } } public static function generateGroupId(): Int { return ++currentGroupId; } public static function addBreakableTimeTaskToGroup(groupId: Int, task: Void->Bool, start: Float, period: Float = 0, duration: Float = 0): Int { var t = new TimeTask(); t.active = true; t.task = task; t.id = ++currentTimeTaskId; t.groupId = groupId; t.start = current + start; t.period = 0; if (period != 0) t.period = period; t.duration = 0; // infinite if (duration != 0) t.duration = t.start + duration; t.next = t.start; insertSorted(timeTasks, t); return t.id; } public static function addTimeTaskToGroup(groupId: Int, task: Void->Void, start: Float, period: Float = 0, duration: Float = 0): Int { return addBreakableTimeTaskToGroup(groupId, function() { task(); return true; }, start, period, duration); } public static function addBreakableTimeTask(task: Void->Bool, start: Float, period: Float = 0, duration: Float = 0): Int { return addBreakableTimeTaskToGroup(0, task, start, period, duration); } public static function addTimeTask(task: Void->Void, start: Float, period: Float = 0, duration: Float = 0): Int { return addTimeTaskToGroup(0, task, start, period, duration); } static function getTimeTask(id: Int): TimeTask { if (activeTimeTask != null && activeTimeTask.id == id) return activeTimeTask; for (timeTask in timeTasks) { if (timeTask.id == id) { return timeTask; } } for (timeTask in pausedTimeTasks) { if (timeTask.id == id) { return timeTask; } } return null; } public static function pauseTimeTask(id: Int, paused: Bool): Void { var timeTask = getTimeTask(id); if (timeTask != null) { pauseRunningTimeTask(timeTask, paused); } if (activeTimeTask != null && activeTimeTask.id == id) { activeTimeTask.paused = paused; } } static function pauseRunningTimeTask(timeTask: TimeTask, paused: Bool): Void { timeTask.paused = paused; if (paused) { timeTasks.remove(timeTask); pausedTimeTasks.push(timeTask); } else { insertSorted(timeTasks, timeTask); pausedTimeTasks.remove(timeTask); } } public static function pauseTimeTasks(groupId: Int, paused: Bool): Void { if (paused) { for (timeTask in timeTasks) { if (timeTask.groupId == groupId) { pauseRunningTimeTask(timeTask, paused); } } } else { for (timeTask in pausedTimeTasks) { if (timeTask.groupId == groupId) { pauseRunningTimeTask(timeTask, paused); } } } if (activeTimeTask != null && activeTimeTask.groupId == groupId) { activeTimeTask.paused = paused; } } public static function removeTimeTask(id: Int): Void { var timeTask = getTimeTask(id); if (timeTask != null) { timeTask.active = false; timeTasks.remove(timeTask); } } public static function removeTimeTasks(groupId: Int): Void { for (timeTask in timeTasks) { if (timeTask.groupId == groupId) { timeTask.active = false; timeTasksScratchpad.push(timeTask); } } for (timeTask in timeTasksScratchpad) { timeTasks.remove(timeTask); } while (timeTasksScratchpad.length > 0) { timeTasksScratchpad.remove(timeTasksScratchpad[0]); } if (activeTimeTask != null && activeTimeTask.groupId == groupId) { activeTimeTask.active = false; } } public static function numTasksInSchedule(): Int { return timeTasks.length + frameTasks.length; } static function insertSorted(list: Array, task: TimeTask) { for (i in 0...list.length) { if (list[i].next > task.next) { list.insert(i, task); return; } } list.push(task); } static function sortFrameTasks(): Void { if (frame_tasks_sorted) return; frameTasks.sort(function(a: FrameTask, b: FrameTask): Int { return a.priority > b.priority ? 1 : ((a.priority < b.priority) ? -1 : 0); }); frame_tasks_sorted = true; } }