/** * Core service that is always running alongside Acedia framework, must be * created by a launcher. * Used for booting up and shutting down Acedia. * Also used for spawning `Actor`s as the only must-have `Service`. * Copyright 2020 - 2021 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 CoreService extends Service dependson(BroadcastEventsObserver); // Package's manifest is supposed to always have a name of // ".Manifest", this variable stores the ".Manifest" part var private const string manifestSuffix; // Classes that will need to do some cleaning before Acedia shuts down var private array< class > usedObjectClasses; var private array< class > usedActorClasses; // `Singleton`s are handled as a special case and cleaned up after // the rest of the classes. var private array< class > usedSingletonClasses; var private array< class<_manifest> > availableManifests; var private array packagesToLoad; struct FeatureConfigPair { var public class featureClass; var public Text configName; }; var private array automaticConfigs; var private array< class > availableFeatures; var private LoggerAPI.Definition infoLoadingPackage; var private LoggerAPI.Definition infoBootingUp, infoBootingUpFinished; var private LoggerAPI.Definition infoShuttingDown; var private LoggerAPI.Definition errorNoManifest, errorCannotRunTests; // We do not implement `OnShutdown()`, because total Acedia's clean up // is supposed to happen before that event. protected function OnLaunch() { BootUp(); default.packagesToLoad.length = 0; } /** * Static method that starts everything needed by Acedia framework to function. * Must be called before attempting to use any of the Acedia's functionality. * * Acedia needs to be able to spawn actors and for that it first needs to * spawn `CoreService`. To make that possible you need to provide * an `Actor` instance from current level. It can be any valid actor. * * @param source Valid actor instance that Acedia will use to * spawn `CoreService` * @param packages List of acedia packages to load. * Using array of `string`s since Acedia's `Text` wouldn't yet * be available. */ public final static function LaunchAcedia( Actor source, optional array packages) { default.packagesToLoad = packages; default.blockSpawning = false; // Actual work will be done inside `BootUp()` private method that will be // called from `OnCreated()` event. source.Spawn(class'CoreService'); default.blockSpawning = true; } /** * Shuts down Acedia, cleaning up created actors, default values, * changes made to the standard game classes, etc.. * * This method must be called before the level change (map change), otherwise * Acedia is not guaranteed to work on the next map and you might * even experience game crashes. */ public final function ShutdownAcedia() { local int i; local AcediaActor nextActor; local MemoryService memoryService; _.logger.Auto(infoShuttingDown); memoryService = MemoryService(class'MemoryService'.static.GetInstance()); // Turn off gameplay-related stuff first class'Global'.static.GetInstance().DropGameplayAPI(); // Get rid of actors foreach AllActors(class'AcediaActor', nextActor) { if (nextActor == self) continue; if (nextActor == memoryService) continue; nextActor.Destroy(); } // Clean all used classes, except for singletons for (i = 0; i < usedObjectClasses.length; i += 1) { usedObjectClasses[i].static._cleanup(); } for (i = 0; i < usedActorClasses.length; i += 1) { usedActorClasses[i].static._cleanup(); } // Remove remaining objects _.unreal.broadcasts.Remove(class'BroadcastEventsObserver'); memoryService.ClearAll(); // Finally clean up singletons for (i = 0; i < usedSingletonClasses.length; i += 1) { usedSingletonClasses[i].static._cleanup(); } // Clean API class'Global'.static.GetInstance().DropCoreAPI(); _ = none; // Get rid of the `MemoryService` and `CoreService` last memoryService.Destroy(); Destroy(); Log("Acedia has shut down."); } // Loads packages, injects broadcast handler and optionally runs tests private final function BootUp() { local int i; local Text nextPackageName; local class<_manifest> nextManifest; _.logger.Auto(infoBootingUp); LoadManifest(class'AcediaCore.Manifest'); // Load packages for (i = 0; i < packagesToLoad.length; i += 1) { nextPackageName = _.text.FromString(packagesToLoad[i]); _.logger.Auto(infoLoadingPackage).Arg(nextPackageName.Copy()); nextManifest = LoadManifestClass(packagesToLoad[i]); if (nextManifest == none) { _.logger.Auto(errorNoManifest).Arg(nextPackageName.Copy()); continue; } availableManifests[availableManifests.length] = nextManifest; LoadManifest(nextManifest); _.memory.Free(nextPackageName); } nextPackageName = none; _.logger.Auto(infoBootingUpFinished); // Other initialization class'UnrealService'.static.Require(); if (class'TestingService'.default.runTestsOnStartUp) { RunStartUpTests(); } class'InfoQueryHandler'.static.StaticConstructor(); _.unreal.mutator.OnMutate(_self).connect = EnableCommandsFeature; } private final function LoadManifest(class<_manifest> manifestClass) { local int i; local FeatureConfigPair nextPair; for (i = 0; i < manifestClass.default.aliasSources.length; i += 1) { if (manifestClass.default.aliasSources[i] == none) continue; _.memory.Allocate(manifestClass.default.aliasSources[i]); } LaunchManifestServices(manifestClass); for (i = 0; i < manifestClass.default.features.length; i += 1) { if (manifestClass.default.features[i] == none) continue; manifestClass.default.features[i].static.LoadConfigs(); nextPair.featureClass = manifestClass.default.features[i]; nextPair.configName = manifestClass.default.features[i].static .GetAutoEnabledConfig(); automaticConfigs[automaticConfigs.length] = nextPair; availableFeatures[availableFeatures.length] = manifestClass.default.features[i]; } for (i = 0; i < manifestClass.default.testCases.length; i += 1) { class'TestingService'.static .RegisterTestCase(manifestClass.default.testCases[i]); } } public final function array GetAutoConfigurationInfo() { local int i; local array result; for (i = 0; i < automaticConfigs.length; i += 1) { result[i] = automaticConfigs[i]; if (result[i].configName != none) { result[i].configName = result[i].configName.Copy(); } } return result; } public final function array< class > GetAvailableFeatures() { return availableFeatures; } private final function class<_manifest> LoadManifestClass(string packageName) { return class<_manifest>(DynamicLoadObject( packageName $ manifestSuffix, class'Class', true)); } private final function LaunchManifestServices(class<_manifest> manifestClass) { local int i; for (i = 0; i < manifestClass.default.services.length; i += 1) { if (manifestClass.default.services[i] != none) { manifestClass.default.services[i].static.Require(); } } } private final function RunStartUpTests() { local TestingService testService; testService = TestingService(class'TestingService'.static.Require()); testService.PrepareTests(); if (testService.filterTestsByName) { testService.FilterByName(testService.requiredName); } if (testService.filterTestsByGroup) { testService.FilterByGroup(testService.requiredGroup); } if (!testService.Run()) { _.logger.Auto(errorCannotRunTests); } } private final function EnableCommandsFeature( string command, PlayerController sendingPlayer) { if (command ~= "acediacommands") { class'Commands_Feature'.static.EmergencyEnable(); } } /** * Registers class derived from `AcediaObject` for clean up when * Acedia shuts down. * * Does not check for duplicates. * * This is an internal function and should not be used outside of * AcediaCore package. */ public final function _registerObjectClass(class classToClean) { if (classToClean != none) { usedObjectClasses[usedObjectClasses.length] = classToClean; } } /** * Registers class derived from `AcediaActor` for clean up when * Acedia shuts down. * * Does not check for duplicates. * * This is an internal function and should not be used outside of * AcediaCore package. */ public final function _registerActorClass(class classToClean) { local class singletonClass; if (classToClean == none) { return; } singletonClass = class(classToClean); if (singletonClass != none) { usedSingletonClasses[usedSingletonClasses.length] = singletonClass; } else { usedActorClasses[usedActorClasses.length] = classToClean; } } defaultproperties { manifestSuffix = ".Manifest" infoBootingUp = (l=LOG_Info,m="Initializing Acedia.") infoBootingUpFinished = (l=LOG_Info,m="Acedia initialized.") infoShuttingDown = (l=LOG_Info,m="Shutting down Acedia.") infoLoadingPackage = (l=LOG_Info,m="Loading package \"%1\".") errorNoManifest = (l=LOG_Error,m="Cannot load `Manifest` for package \"%1\". Check if it's missing or if its name is spelled incorrectly.") errorCannotRunTests = (l=LOG_Error,m="Could not perform Acedia's tests.") }