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