diff --git a/sources/Core/Memory/MemoryAPI.uc b/sources/Core/Memory/MemoryAPI.uc new file mode 100644 index 0000000..810e72a --- /dev/null +++ b/sources/Core/Memory/MemoryAPI.uc @@ -0,0 +1,290 @@ +/** + * API that provides functions for managing objects and actors by providing + * easy and general means to create and destroy them, that allow to make use of + * temporary `Object`s in a more efficient way. + * This is a low-level API that most users of Acedia, most likely, + * would not have to use, since creation of most objects would use their own + * wrapper functions around this API. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class MemoryAPI extends Singleton; + +// This variable counts ticks and should be different each new tick. +var private int currentTick; + +// Stores instance of an `Object` that can be borrowed from the pool. +struct BorrowableRecord +{ + // Borrowable instance + var Object instance; + // Was this object borrowed? + // This flag will persist unless object was explicitly freed, + // even if borrowed reference timed out. + var bool borrowed; + // When was this object borrowed? + // Used to automatically free borrowed objects after the tick has passed. + var int borrowTick; +}; + +// Available object pools +var private array borrowPool; + +// Checks if instance in the given `record` is borrowed. +private final function bool IsBorrowed(BorrowableRecord record) +{ + // `record.borrowed` means instance was borrowed, + // but not explicitly freed; + // `record.borrowTick >= currentTick` means that rights to the borrowed + // instance hasn't yet ran out. + return (record.borrowed && record.borrowTick >= currentTick); +} + +// Loads a reference to class instance from it's string representation. +private final function class LoadClass(string classReference) +{ + return class(DynamicLoadObject(classReference, class'Class', true)); +} + +/** + * Creates a new `Object` / `Actor` of a given class. + * + * If uses a proper spawning mechanism for both objects (`new`) + * and actors (`Spawn`). + * + * @param classToAllocate Class of the `Object` / `Actor` that this method + * must create. + * @return Newly created object, might be `none` if creation has failed. + */ +public final function Object Allocate(class classToAllocate) +{ + local class actorClassToSpawn; + if (classToAllocate == none) return none; + + actorClassToSpawn = class(classToAllocate); + if (actorClassToSpawn != none) + { + return Spawn(actorClassToSpawn); + } + return (new classToAllocate); +} + +/** + * Creates a new `Object` / `Actor` of a given class. + * + * If uses a proper spawning mechanism for both objects (`new`) + * and actors (`Spawn`). + * + * @param classToAllocate Text representation (name) of the class of the + * `Object` / `Actor` that this method must create. + * Should contain full package-path. + * @return Newly created object, might be `none` if creation has failed. + */ +public final function Object AllocateByReference(string refToClassToAllocate) +{ + return Allocate(LoadClass(refToClassToAllocate)); +} + +/** + * Borrows an instance of an `Object` / `Actor` of the given class + * from the pool. + * Borrowed instance will be auto-freed during next tick. + * + * @param classToBorrow Class of an `Object` / `Actor` we want to borrow. + * @return Borrowed object, might be `none` if borrow pool is empty and + * creation of a new `Object` / `Actor` has failed. + */ +public final function Object Borrow(class classToBorrow) +{ + local int i; + local BorrowableRecord newRecord; + for (i = 0; i < borrowPool.length; i += 1) + { + if (IsBorrowed(borrowPool[i])) continue; + if (borrowPool[i].instance == none) continue; + if (borrowPool[i].instance.class != classToBorrow) continue; + + borrowPool[i].borrowed = true; + borrowPool[i].borrowTick = currentTick; + return borrowPool[i].instance; + } + // Create a new instance to borrow, if there isn't any available for + // the given class. + newRecord.borrowed = false; + newRecord.instance = Allocate(classToBorrow); + if (newRecord.instance != none) + { + borrowPool[borrowPool.length] = newRecord; + } + return newRecord.instance; +} + +/** + * Borrows an instance of an `Object` / `Actor` of the given class + * from the pool. + * Borrowed instance will be auto-freed during next tick. + * + * @param classToBorrow Text representation (name) of the class of + * an `Object` / `Actor` we want to borrow. + * @return Borrowed object, might be `none` if borrow pool is empty and + * creation of a new `Object` / `Actor` has failed. + */ +public final function Object BorrowByReference(string refToClassToBorrow) +{ + return Borrow(LoadClass(refToClassToBorrow)); +} + +/** + * Claims an instance of an `Object` / `Actor` of the given class + * from the pool. + * Claimed instances are removed from the borrow pool and + * will not be automatically freed. + * + * @param classToClaim Class of an `Object` / `Actor` we wish to borrow. + * @return Borrowed object, might be `none` if borrow pool is empty and + * creation of a new `Object` / `Actor` has failed. + */ +public final function Object Claim(class classToClaim) +{ + local int i; + local Object instance; + for (i = 0; i < borrowPool.length; i += 1) + { + if (IsBorrowed(borrowPool[i])) continue; + if (borrowPool[i].instance == none) continue; + if (borrowPool[i].instance.class != classToClaim) continue; + + instance = borrowPool[i].instance; + borrowPool.Remove(i, 1); + return instance; + } + // Create a new instance to borrow, if there isn't any available for + // the given class. + return Allocate(classToClaim); +} + +/** + * Claims an instance of an `Object` / `Actor` of the given class + * from the pool. + * Claimed instances are removed from the borrow pool and + * will not be automatically freed. + * + * @param classToClaim Text representation (name) of the class of + * an `Object` / `Actor` we wish to claim. + * @return Borrowed object, might be `none` if borrow pool is empty and + * creation of a new `Object` / `Actor` has failed. + */ +public final function Object ClaimByReference(string refToClassToClaim) +{ + return Claim(LoadClass(refToClassToClaim)); +} + +/** + * Frees given `Object` / `Actor` resource. + * + * By default `Actor`s are destroyed. + * Due to limitations of the engine objects cannot be outright destroyed. + * Instead, they are put into a "borrow pool", from where they can later be + * taken for a reuse. + * + * @param objectToDelete `Object` / `Actor` that must be freed. + * @param forceMakeBorrowable Only has an effect if `objectToDelete` + * is an `Actor`, in which case it forces it to be added + * to the borrow pool, instead of being destroyed. + */ +public final function Free +( + Object objectToDelete, + optional bool forceMakeBorrowable +) +{ + local int i; + local Actor actorToDelete; + local BorrowableRecord newRecord; + if (objectToDelete == none) return; + + actorToDelete = Actor(objectToDelete); + if (actorToDelete != none && !forceMakeBorrowable) + { + actorToDelete.Destroy(); + return; + } + // Check if `objectToDelete` is already in our records. + for (i = 0; i < borrowPool.length; i += 1) + { + if (borrowPool[i].instance == objectToDelete) + { + borrowPool[i].borrowed = false; + return; + } + } + // If not - add it + newRecord.instance = objectToDelete; + newRecord.borrowed = false; + borrowPool[borrowPool.length] = newRecord; +} + +/** + * Forces Unreal Engine to do garbage collection. + * By default also cleans up all the objects in the borrow object pool. + * + * Process of garbage collection causes significant lag spike during the game + * and should be used carefully. + * + * NOTE: method does not guarantee that borrow pool will be empty after + * this call (even with `keepBorrowedObjectPool = true`), + * since some of the borrowable objects might be currently in use and, + * therefore, cannot be garbage collected. + * + * @param keepBorrowedObjectPool Set this to `true` to NOT garbage collect + * objects in a borrow pool. Otherwise keep it `false`. + */ +public final function CollectGarbage(optional bool keepBorrowedObjectPool) +{ + local int i; + if (!keepBorrowedObjectPool) + { + // Dereference all non-borrowed objects from borrow pool, + // so that they can be garbage collected. + i = 0; + while (i < borrowPool.length) + { + if ( borrowPool[i].instance == none + || !IsBorrowed(borrowPool[i]) ) + { + borrowPool.Remove(i, 1); + } + else + { + i += 1; + } + } + } + // This makes Unreal Engine do garbage collection + ConsoleCommand("obj garbage"); +} + +event Tick(float delta) +{ + currentTick += 1; +} + +// TODO: add cleaning on cooldown +defaultproperties +{ + currentTick = 0 +} \ No newline at end of file