diff --git a/sources/BaseRealm/API/Scheduler/SchedulerAPI.uc b/sources/BaseRealm/API/Scheduler/SchedulerAPI.uc index 161316f..f6fb765 100644 --- a/sources/BaseRealm/API/Scheduler/SchedulerAPI.uc +++ b/sources/BaseRealm/API/Scheduler/SchedulerAPI.uc @@ -1,9 +1,8 @@ /** - * API that provides functions for scheduling jobs and expensive tasks such - * as writing onto the disk. Also provides methods for users to inform API that - * they've recently did an expensive operation, so that `SchedulerAPI` is to - * try and use less resources when managing jobs. - * Copyright 2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -20,139 +19,81 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class SchedulerAPI extends AcediaObject +class SchedulerApi extends AcediaObject config(AcediaSystem); -/** - * # `SchedulerAPI` - * - * UnrealScript is inherently single-threaded and whatever method you call, - * it will be completely executed within a single game's tick. - * This API is meant for scheduling various actions over time to help emulating - * multi-threading by spreading some code executions over several different - * game/server ticks. - * - * ## Usage - * - * ### Job scheduling - * - * One of the reasons which is faulty infinite loop detection system that - * will crash the game/server if it thinks UnrealScript code has executed too - * many operations (it is not about execution time, logging a lot of messages - * with `Log()` can take a lot of time and not crash anything, while simple - * loop, that would've finished much sooner, can trigger a crash). - * This is a very atypical problem for mods to have, but Acedia's - * introduction of databases and avarice link can lead to users trying to read - * (from database or network) an object that is too big, leading to a crash. - * Jobs are not about performance, they're about crash prevention. - * - * In case you have such a job of your own, that can potentially take too - * many steps to finish without crashing, you can convert it into - * a `SchedulerJob` (you make a subclass for your type of the job and - * instantiate it for each execution of the job). This requires you to - * restructure your algorithm in such a way, that it is able to run for some - * finite (maybe small) amount of steps and postpone the rest of calculations - * to the next tick and put it into a method - * `SchedulerJob.DoWork(int allottedWorkUnits)`, where `allottedWorkUnits` is - * how much your method is allowed to do during this call, assuming `10000` - * units of work on their own won't lead to a crash. - * Another method `SchedulerJob.IsCompleted()` needs to be setup to return - * `true` iff your job is done. - * After you prepared an instance of your job subclass, simply pass it to - * `_.scheduler.AddJob()`. - * - * ### Disk usage requests - * - * Writing to the disk (saving data into config file, saving local database - * changes) can be an expensive operation and to avoid lags in gameplay you - * might want to spread such operations over time. - * `_.scheduler.RequestDiskAccess()` method allows you to do that. It is not - * exactly a signal, but it acts similar to one: to request a right to save to - * the disk, just do the following: - * `_.scheduler.RequestDiskAccess().connect = ` - * and `disk_writing_method()` will be called once your turn come up. - * - * ## Manual ticking - * - * If any kind of level core (either server or client one) was created, - * this API will automatically perform necessary actions every tick. - * Otherwise, if only base API is available, there's no way to do that, but - * you can manually decide when to tick this API by calling `ManualTick()` - * method. - */ +//! This API is meant for scheduling various actions over time to help emulating +//! multi-threading by spreading some code executions over several different +//! game/server ticks. +//! +//! UnrealScript is inherently single-threaded and whatever method you call, +//! it will be completely executed within a single game's tick. -/** - * How often can files be saved on disk. This is a relatively expensive - * operation and we don't want to write a lot of different files at once. - * But since we lack a way to exactly measure how much time that saving will - * take, AcediaCore falls back to simply performing every saving with same - * uniform time intervals in-between. - * This variable decides how much time there should be between two file - * writing accesses. - * Negative and zero values mean that all writing disk access will be - * granted as soon as possible, without any cooldowns. - */ +// How often can files be saved on disk. +// +// This is a relatively expensive operation and we don't want to write a lot of different files +// at once. +// But since we lack a way to exactly measure how much time that saving will take, AcediaCore falls +// back to simply performing every saving with same uniform time intervals in-between. +// This variable decides how much time there should be between two file writing accesses. +// Negative and zero values mean that all writing disk access will be granted as soon as possible, +// without any cooldowns. var private config float diskSaveCooldown; -/** - * Maximum total work units for jobs allowed per tick. Jobs are expected to be - * constructed such that they don't lead to a crash if they have to perform - * this much work. - * - * Changing default value of `10000` is not advised. - */ + +// Maximum total work units for jobs allowed per tick. +// +// Jobs are expected to be constructed such that they don't lead to a crash if they have to perform +// this much work. +// Changing default value of `10000` is not advised. var private config int maxWorkUnits; -/** - * How many different jobs can be performed per tick. This limit is added so - * that `maxWorkUnits` won't be spread too thin if a lot of jobs get registered - * at once. - */ + +// How many different jobs can be performed per tick. +// +// This limit is added so that `maxWorkUnits` won't be spread too thin if a lot of jobs +// get registered at once. var private config int maxJobsPerTick; -// We can (and will) automatically tick +// We can (and will) automatically tick var private bool tickAvailable; -// `true` == it is safe to use server API for a tick -// `false` == it is safe to use client API for a tick +// `true` == it is safe to use server API for a tick +// `false` == it is safe to use client API for a tick var private bool tickFromServer; -// Our `Tick()` method is currently connected to the `OnTick()` signal. -// Keeping track of this allows us to disconnect from `OnTick()` signal -// when it is not necessary. +// Our `Tick()` method is currently connected to the `OnTick()` signal. +// +// Keeping track of this allows us to disconnect from `OnTick()` signal when it is not necessary. var private bool connectedToTick; // How much time if left until we can write to the disk again? var private float currentDiskCooldown; -// There is a limit (`maxJobsPerTick`) to how many different jobs we can -// perform per tick and if we register an amount jobs over that limit, we need -// to uniformly spread execution time between them. -// To achieve that we simply cyclically (in order) go over `currentJobs` -// array, each time executing exactly `maxJobsPerTick` jobs. -// `nextJobToPerform` remembers what job is to be executed next tick. -var private int nextJobToPerform; +// There is a limit (`maxJobsPerTick`) to how many different jobs we can perform per tick and if we +// register an amount jobs over that limit, we need to uniformly spread execution time between them. +// +// To achieve that we simply cyclically (in order) go over `currentJobs` array, each time executing +// exactly `maxJobsPerTick` jobs. +// +// `nextJobToPerform` remembers what job is to be executed next tick. +var private int nextJobToPerform; var private array currentJobs; -// Storing receiver objects, following example of signals/slots, is done -// without increasing their reference count, allowing them to get deallocated -// while we are still keeping their reference. -// To avoid using such deallocated receivers, we keep track of the life -// versions they've had when their disk requests were registered. +// Storing receiver objects, following example of signals/slots, is done without increasing their +// reference count, allowing them to get deallocated while we are still keeping their reference. +// +// To avoid using such deallocated receivers, we keep track of the life versions they've had when +// their disk requests were registered. var private array diskQueue; -var private array receivers; -var private array receiversLifeVersions; +var private array receivers; +var private array receiversLifeVersions; -/** - * Registers new scheduler job `newJob` to be executed in the API. - * - * @param newJob New job to be scheduled for execution. - * Does nothing if given `newJob` is already added. - */ -public function AddJob(SchedulerJob newJob) -{ +/// Registers new scheduler job to be executed in the API. +/// +/// Does nothing if given `newJob` is already added. +public function AddJob(SchedulerJob newJob) { local int i; if (newJob == none) { return; } - for (i = 0; i < currentJobs.length; i += 1) - { + for (i = 0; i < currentJobs.length; i += 1) { if (currentJobs[i] == newJob) { return; } @@ -162,116 +103,102 @@ public function AddJob(SchedulerJob newJob) UpdateTickConnection(); } -/** - * Requests another disk access. - * - * Use it like signal: `RequestDiskAccess().connect = `. - * Since it is meant to be used as a signal, so DO NOT STORE/RELEASE returned - * wrapper object `SchedulerDiskRequest`. - * - * @param receiver Same as for signal/slots, this is an object, responsible - * for the disk request. If this object gets deallocated - request will be - * thrown away. - * Typically this should be an object in which connected method will be - * executed. - * @return Wrapper object that provides `connect` delegate. - */ -public function SchedulerDiskRequest RequestDiskAccess(AcediaObject receiver) -{ +/// Requests another disk access. +/// +/// Use it like signal: `RequestDiskAccess().connect = `. +/// Since it is meant to be used as a signal, so DO NOT STORE/RELEASE returned wrapper object +/// [`SchedulerDiskRequest`]. +/// +/// Same as for signal/slots, [`receiver`] is an object, responsible for the disk request. +/// If this object gets deallocated - request will be thrown away. +/// Typically this should be an object in which connected method will be executed. +/// Returns wrapper object that provides `connect` delegate. +/// +/// # Examples +/// +/// ``` +/// _.scheduler.RequestDiskAccess(self).connect = MethodThatSaves(); +/// ``` +public function SchedulerDiskRequest RequestDiskAccess(AcediaObject receiver) { local SchedulerDiskRequest newRequest; if (receiver == none) return none; if (!receiver.IsAllocated()) return none; - newRequest = - SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest')); + newRequest = SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest')); diskQueue[diskQueue.length] = newRequest; receivers[receivers.length] = receiver; - receiversLifeVersions[receiversLifeVersions.length] = - receiver.GetLifeVersion(); + receiversLifeVersions[receiversLifeVersions.length] = receiver.GetLifeVersion(); UpdateTickConnection(); return newRequest; } -/** - * Tells you how many incomplete jobs are currently registered in - * the scheduler. - * - * @return How many incomplete jobs are currently registered in the scheduler. - */ -public function int GetJobsAmount() -{ +/// Returns amount of incomplete jobs are currently registered in the scheduler. +public function int GetJobsAmount() { CleanCompletedJobs(); return currentJobs.length; } -/** - * Tells you how many disk access requests are currently registered in - * the scheduler. - * - * @return How many incomplete disk access requests are currently registered - * in the scheduler. - */ -public function int GetDiskQueueSize() -{ +/// Returns amount of disk access requests are currently registered in the scheduler. +public function int GetDiskQueueSize() { CleanDiskQueue(); return diskQueue.length; } -/** - * In case neither server, nor client core is registered, scheduler must be - * ticked manually. For that call this method each separate tick (or whatever - * is your closest approximation available for that). - * - * Before manually invoking this method, you should check if scheduler - * actually started to tick *automatically*. Use `_.scheduler.IsAutomated()` - * for that. - * - * NOTE: If neither server-/client- core is created, nor `ManualTick()` is - * invoked manually, `SchedulerAPI` won't actually do anything. - * - * @param delta Time (real one) that is supposedly passes from the moment - * `ManualTick()` was called last time. Used for tracking disk access - * cooldowns. How `SchedulerJob`s are executed is independent from this - * value. - */ -public final function ManualTick(optional float delta) -{ +/// Performs another batch of scheduled tasks. +/// +/// In case neither server, nor client core is registered, scheduler must be ticked manually. +/// For that call this method each separate tick (or whatever is your closest approximation +/// available for that). +/// Before manually invoking this method, you should check if scheduler actually started to tick +/// *automatically*. +/// Use `_.scheduler.IsAutomated()` for that. +/// +/// Argument is a time (real, not in-game one) that is supposedly passes from the moment +/// [`SchedulerApi::ManualTick()`] was called last time. +/// Used for tracking disk access cooldowns. +/// How [`SchedulerJob`]s are executed is independent from this value. +/// +/// Returns time (real, not in-game one) that is supposedly passes from the moment +/// [`SchedulerApi::ManualTick()`] was called last time. +/// +/// # Examples +/// +/// ``` +/// if (!_.scheduler.IsAutomated()) { +/// _.scheduler.ManualTick(0.05); +/// } +/// ``` +/// +/// # Note +/// +/// If neither server-/client- core is created, nor [`SchedulerApi::ManualTick()`] is invoked +/// manually, [`SchedulerApi`] won't actually do anything. +public final function ManualTick(optional float delta) { Tick(delta, 1.0); } -/** - * Is scheduler ticking automated? It can only be automated if either - * server or client level cores are created. Scheduler can automatically enable - * automation and it cannot be prevented, but can be helped by using - * `UpdateTickConnection()` method. - * - * @return `true` if scheduler's tick is automatically called and `false` - * otherwise (and calling `ManualTick()` is required). - */ -public function bool IsAutomated() -{ +/// Returns whether scheduler ticking automated. +/// +/// It can only be automated if either server or client level cores are created. +/// Scheduler can automatically enable automation and it cannot be prevented, but can be helped by +/// using [`SchedulerApi::UpdateTickConnection()`] method. +public function bool IsAutomated() { return tickAvailable; } -/** - * Causes `SchedulerAPI` to try automating itself by searching for level cores - * (checking if server/client APIs are enabled). - */ -public function UpdateTickConnection() -{ - local bool needsConnection; +/// Causes `SchedulerApi` to try automating itself by searching for level cores (checking if +/// server/client APIs are enabled). +public function UpdateTickConnection() { + local bool needsConnection; local UnrealAPI api; - if (!tickAvailable) - { - if (_server.IsAvailable()) - { + if (!tickAvailable) { + if (_server.IsAvailable()) { tickAvailable = true; tickFromServer = true; } - else if (_client.IsAvailable()) - { + else if (_client.IsAvailable()) { tickAvailable = true; tickFromServer = false; } @@ -285,28 +212,23 @@ public function UpdateTickConnection() } if (tickFromServer) { api = _server.unreal; - } - else { + } else { api = _client.unreal; } if (connectedToTick && !needsConnection) { api.OnTick(self).Disconnect(); - } - else if (!connectedToTick && needsConnection) { + } else if (!connectedToTick && needsConnection) { api.OnTick(self).connect = Tick; } connectedToTick = needsConnection; } -private function Tick(float delta, float dilationCoefficient) -{ +private function Tick(float delta, float dilationCoefficient) { delta = delta / dilationCoefficient; - // Manage disk cooldown if (currentDiskCooldown > 0) { currentDiskCooldown -= delta; } - if (currentDiskCooldown <= 0 && diskQueue.length > 0) - { + if (currentDiskCooldown <= 0 && diskQueue.length > 0) { currentDiskCooldown = diskSaveCooldown; ProcessDiskQueue(); } @@ -328,8 +250,7 @@ private function ProcessJobs() return; } unitsPerJob = maxWorkUnits / jobsToPerform; - while (jobsToPerform > 0) - { + while (jobsToPerform > 0) { if (nextJobToPerform >= currentJobs.length) { nextJobToPerform = 0; } @@ -350,8 +271,7 @@ private function ProcessDiskQueue() if (diskQueue.length <= 0) { return; } - if (diskSaveCooldown > 0) - { + if (diskSaveCooldown > 0) { if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { diskQueue[i].connect(); } @@ -361,8 +281,7 @@ private function ProcessDiskQueue() receiversLifeVersions.Remove(0, 1); return; } - for (i = 0; i < diskQueue.length; i += 1) - { + for (i = 0; i < diskQueue.length; i += 1) { if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { diskQueue[i].connect(); } @@ -378,31 +297,25 @@ private function CleanCompletedJobs() { local int i; - while (i < currentJobs.length) - { - if (currentJobs[i].IsCompleted()) - { + while (i < currentJobs.length) { + if (currentJobs[i].IsCompleted()) { if (i < nextJobToPerform) { nextJobToPerform -= 1; } currentJobs[i].FreeSelf(); currentJobs.Remove(i, 1); - } - else { + } else { i += 1; } } } // Remove disk requests with deallocated receivers -private function CleanDiskQueue() -{ +private function CleanDiskQueue() { local int i; - while (i < diskQueue.length) - { - if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) - { + while (i < diskQueue.length) { + if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { i += 1; continue; } @@ -413,8 +326,7 @@ private function CleanDiskQueue() } } -defaultproperties -{ +defaultproperties { diskSaveCooldown = 0.25 maxWorkUnits = 10000 maxJobsPerTick = 5 diff --git a/sources/BaseRealm/API/Scheduler/SchedulerDiskRequest.uc b/sources/BaseRealm/API/Scheduler/SchedulerDiskRequest.uc index 06a09e0..06098ce 100644 --- a/sources/BaseRealm/API/Scheduler/SchedulerDiskRequest.uc +++ b/sources/BaseRealm/API/Scheduler/SchedulerDiskRequest.uc @@ -1,7 +1,8 @@ /** - * Slot-like object that represents a request for a writing disk access, - * capable of being scheduled on the `SchedulerAPI`. - * Copyright 2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -20,10 +21,11 @@ */ class SchedulerDiskRequest extends AcediaObject; -delegate connect() -{ +//! Slot-like object that represents a request for a writing disk access, capable of being scheduled +//! on the [`SchedulerApi`]. + +delegate connect() { } -defaultproperties -{ +defaultproperties { } \ No newline at end of file diff --git a/sources/BaseRealm/API/Scheduler/SchedulerJob.uc b/sources/BaseRealm/API/Scheduler/SchedulerJob.uc index b1fc45c..4aa09b7 100644 --- a/sources/BaseRealm/API/Scheduler/SchedulerJob.uc +++ b/sources/BaseRealm/API/Scheduler/SchedulerJob.uc @@ -1,7 +1,8 @@ /** - * Template object that represents a job, capable of being scheduled on the - * `SchedulerAPI`. Use `IsCompleted()` to mark job as completed. - * Copyright 2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -21,27 +22,23 @@ class SchedulerJob extends AcediaObject abstract; -/** - * Checks if caller `SchedulerJob` was completed. - * Once this method returns `true`, it shouldn't start returning `false` again. - * - * @return `true` if `SchedulerJob` is already completed and doesn't need to - * be further executed and `false` otherwise. - */ +//! Template object that represents a job, capable of being scheduled on the [`SchedulerAPI`]. +//! Use [`IsCompleted()`] to mark job as completed. + +/// Checks if caller [`SchedulerJob`] was completed. +/// +/// Returns `true` if [`SchedulerJob`] is already completed and doesn't need to be further executed +/// and `false` otherwise. +/// Once this method returns `true`, it shouldn't start returning `false` again. public function bool IsCompleted(); -/** - * Called when scheduler decides that `SchedulerJob` should be executed, taking - * amount of abstract "work units" that it is allowed to spend for work. - * - * @param allottedWorkUnits Work units allotted to the caller - * `SchedulerJob`. By default there is `10000` work units per second, so - * you can expect about 10000 / 1000 = 10 work units per millisecond or, - * on servers with 30 tick rate, about 10000 * (30 / 1000) = 300 work units - * per tick to be allotted to all the scheduled jobs. - */ +/// Called when scheduler decides that [`SchedulerJob`] should be executed, taking amount of abstract +/// "work units" that it is allowed to spend for work. +/// +/// By default there is `10000` work units per second, so you can expect about 10000 / 1000 = 10 +/// work units per millisecond or, on servers with `30` tick rate, about `10000 * (30 / 1000) = 300` +/// work units per tick to be allotted to all the scheduled jobs. public function DoWork(int allottedWorkUnits); -defaultproperties -{ +defaultproperties { } \ No newline at end of file diff --git a/sources/BaseRealm/API/Scheduler/API/MockJob.uc b/sources/BaseRealm/API/Scheduler/Tests/MockJob.uc similarity index 100% rename from sources/BaseRealm/API/Scheduler/API/MockJob.uc rename to sources/BaseRealm/API/Scheduler/Tests/MockJob.uc diff --git a/sources/BaseRealm/API/Scheduler/API/TEST_SchedulerAPI.uc b/sources/BaseRealm/API/Scheduler/Tests/TEST_SchedulerAPI.uc similarity index 100% rename from sources/BaseRealm/API/Scheduler/API/TEST_SchedulerAPI.uc rename to sources/BaseRealm/API/Scheduler/Tests/TEST_SchedulerAPI.uc