diff --git a/sources/Avarice/AvariceAPI.uc b/sources/Avarice/AvariceAPI.uc index 6217064..6896595 100644 --- a/sources/Avarice/AvariceAPI.uc +++ b/sources/Avarice/AvariceAPI.uc @@ -31,7 +31,7 @@ public final function array GetAllLinks() local Avarice_Feature avariceFeature; local array emptyResult; avariceFeature = - Avarice_Feature(class'Avarice_Feature'.static.GetInstance()); + Avarice_Feature(class'Avarice_Feature'.static.GetEnabledInstance()); if (avariceFeature != none) { return avariceFeature.GetAllLinks(); } diff --git a/sources/Commands/BuiltInCommands/ACommandHelp.uc b/sources/Commands/BuiltInCommands/ACommandHelp.uc index 2725d89..1f902a5 100644 --- a/sources/Commands/BuiltInCommands/ACommandHelp.uc +++ b/sources/Commands/BuiltInCommands/ACommandHelp.uc @@ -72,7 +72,7 @@ private final function DisplayCommandList(EPlayer player) local Commands_Feature commandsFeature; if (player == none) return; commandsFeature = - Commands_Feature(class'Commands_Feature'.static.GetInstance()); + Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); if (commandsFeature == none) return; console = player.BorrowConsole(); @@ -101,7 +101,7 @@ private final function DisplayCommandHelpPages( local Commands_Feature commandsFeature; if (player == none) return; commandsFeature = - Commands_Feature(class'Commands_Feature'.static.GetInstance()); + Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); if (commandsFeature == none) return; // If arguments were empty - at least display our own help page diff --git a/sources/Commands/Commands_Feature.uc b/sources/Commands/Commands_Feature.uc index ef8b7ce..ae25dfb 100644 --- a/sources/Commands/Commands_Feature.uc +++ b/sources/Commands/Commands_Feature.uc @@ -58,12 +58,6 @@ protected function OnEnabled() commandDelimiters[2] = P("["); // Negation of the selector commandDelimiters[3] = P("!"); - // `SwapConfig()` will no longer touch `_.unreal.mutator.OnMutate(self)` - // with `emergencyEnabledMutate` set to `true`, so we need to give - // access here - if (emergencyEnabledMutate) { - _.unreal.mutator.OnMutate(self).connect = HandleMutate; - } } protected function OnDisabled() @@ -90,6 +84,7 @@ protected function OnDisabled() protected function SwapConfig(FeatureConfig config) { local Commands newConfig; + newConfig = Commands(config); if (newConfig == none) { return; @@ -132,13 +127,14 @@ public final static function EmergencyEnable() { local Text autoConfig; local Commands_Feature feature; + if (!IsEnabled()) { autoConfig = GetAutoEnabledConfig(); EnableMe(autoConfig); __().memory.Free(autoConfig); } - feature = Commands_Feature(GetInstance()); + feature = Commands_Feature(GetEnabledInstance()); if ( !feature.emergencyEnabledMutate && !feature.IsUsingMutateInput() && !feature.IsUsingChatInput()) { @@ -159,7 +155,8 @@ public final static function EmergencyEnable() public final static function bool IsUsingChatInput() { local Commands_Feature instance; - instance = Commands_Feature(GetInstance()); + + instance = Commands_Feature(GetEnabledInstance()); if (instance != none) { return instance.useChatInput; } @@ -177,7 +174,8 @@ public final static function bool IsUsingChatInput() public final static function bool IsUsingMutateInput() { local Commands_Feature instance; - instance = Commands_Feature(GetInstance()); + + instance = Commands_Feature(GetEnabledInstance()); if (instance != none) { return instance.useMutateInput; } @@ -194,7 +192,8 @@ public final static function bool IsUsingMutateInput() public final static function Text GetChatPrefix() { local Commands_Feature instance; - instance = Commands_Feature(GetInstance()); + + instance = Commands_Feature(GetEnabledInstance()); if (instance != none && instance.chatCommandPrefix != none) { return instance.chatCommandPrefix.Copy(); } @@ -214,6 +213,7 @@ public final function RegisterCommand(class commandClass) { local Text commandName; local Command newCommandInstance, existingCommandInstance; + if (commandClass == none) return; if (registeredCommands == none) return; @@ -252,6 +252,7 @@ public final function RemoveCommand(class commandClass) local Command nextCommand; local Text nextCommandName; local array keysToRemove; + if (commandClass == none) return; if (registeredCommands == none) return; @@ -282,8 +283,10 @@ public final function Command GetCommand(BaseText commandName) { local Text commandNameLowerCase; local Command commandInstance; + if (commandName == none) return none; if (registeredCommands == none) return none; + commandNameLowerCase = commandName.LowerCopy(); commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase)); commandNameLowerCase.FreeSelf(); @@ -301,8 +304,10 @@ public final function array GetCommandNames() local array keys; local Text nextKeyAsText; local array keysAsText; - if (registeredCommands == none) return keysAsText; - + + if (registeredCommands == none) { + return keysAsText; + } keys = registeredCommands.GetKeys(); for (i = 0; i < keys.length; i += 1) { @@ -327,6 +332,7 @@ public final function HandleInput(Parser parser, EPlayer callerPlayer) local Command commandInstance; local Command.CallData callData; local MutableText commandName; + if (parser == none) return; if (!parser.Ok()) return; @@ -347,6 +353,7 @@ private function bool HandleCommands( bool teamMessage) { local Parser parser; + // We are only interested in messages that start with `chatCommandPrefix` parser = _.text.Parse(message); if (!parser.Match(chatCommandPrefix).Ok()) @@ -364,6 +371,11 @@ private function HandleMutate(string command, PlayerController sendingPlayer) { local Parser parser; local EPlayer sender; + + // Ignore just "help", since a lot of other mutators use it + if (command ~= "help") { + return; + } parser = _.text.ParseString(command); sender = _.players.FromController(sendingPlayer); HandleInput(parser, sender); diff --git a/sources/Config/AcediaConfig.uc b/sources/Config/AcediaConfig.uc index f426372..49b3210 100644 --- a/sources/Config/AcediaConfig.uc +++ b/sources/Config/AcediaConfig.uc @@ -110,8 +110,6 @@ public static function Initialize() if (default.existingConfigs != none) { return; } - CoreService(class'CoreService'.static.GetInstance()) - ._registerObjectClass(default.class); default.existingConfigs = __().collections.EmptyAssociativeArray(); names = GetPerObjectNames( default.configName, string(default.class.name), MaxInt); diff --git a/sources/CoreRealm/AcediaEnvironment.uc b/sources/CoreRealm/AcediaEnvironment.uc new file mode 100644 index 0000000..e00b0c6 --- /dev/null +++ b/sources/CoreRealm/AcediaEnvironment.uc @@ -0,0 +1,471 @@ +/** + * Container for the information about available resources from other packages. + * Copyright 2022 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 AcediaEnvironment extends AcediaObject; + +/** + * # `AcediaEnvironment` + * + * Instance of this class will be used by Acedia to manage resources available + * from different packages like `Feature`s, aliases, etc.. + * This is mostly necessary to implement Acedia loader (and, possibly, + * its alternatives) that would load available packages and enable `Feature`s + * admin wants to be enabled. + * + * ## Packages + * + * Any package to be used in Acedia should first be *registered* with + * `RegisterPackage()` method. Then a manifest class from it will be read and + * Acedia will become aware of all the resources that package contains. + * Once any of those resources is used, package gets marked as *loaded* and its + * *entry object* (if specified) will be created. + * + * ## `Feature`s + * + * Whether `Feature` is enabled is governed by the `AcediaEnvironment` added + * into the `Global` class. It is possible to create several `Feature` + * instances of the same class instance of each class, but only one can be + * considered enabled at the same time. + */ + +var private array< class<_manifest> > availablePackages; +var private array< class<_manifest> > loadedPackages; + +var private array< class > availableFeatures; +var private array enabledFeatures; +var private array enabledFeaturesLifeVersions; + +var private string manifestSuffix; + +var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered; +var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled; +var private LoggerAPI.Definition warnFeatureAlreadyEnabled; +var private LoggerAPI.Definition errFeatureClassAlreadyEnabled; + +var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal; +var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal; + +/** + * Signal that will be emitted when new `Feature` is enabled. + * Emitted after `Feature`'s `OnEnabled()` method was called. + * + * [Signature] + * void (Feature enabledFeature) + * + * @param enabledFeature `Feature` instance that was just enabled. + */ +/* SIGNAL */ +public final function Environment_FeatureEnabled_Slot OnFeatureEnabled( + AcediaObject receiver) +{ + return Environment_FeatureEnabled_Slot( + onFeatureEnabledSignal.NewSlot(receiver)); +} + +/** + * Signal that will be emitted when new `Feature` is disabled. + * Emitted after `Feature`'s `OnDisabled()` method was called. + * + * [Signature] + * void (class disabledFeatureClass) + * + * @param disabledFeatureClass Class of the `Feature` instance that was + * just disabled. + */ +/* SIGNAL */ +public final function Environment_FeatureDisabled_Slot OnFeatureDisabled( + AcediaObject receiver) +{ + return Environment_FeatureDisabled_Slot( + onFeatureEnabledSignal.NewSlot(receiver)); +} + +protected function Constructor() +{ + // Always register our core package + RegisterPackage_S("AcediaCore"); + onFeatureEnabledSignal = Environment_FeatureEnabled_Signal( + _.memory.Allocate(class'Environment_FeatureEnabled_Signal')); + onFeatureDisabledSignal = Environment_FeatureDisabled_Signal( + _.memory.Allocate(class'Environment_FeatureDisabled_Signal')); +} + +protected function Finalizer() +{ + _.memory.Free(onFeatureEnabledSignal); + _.memory.Free(onFeatureDisabledSignal); +} + +/** + * Registers an Acedia package with name given by `packageName`. + * + * @param packageName Name of the package to register. Must not be `none`. + * This package must exist and not have yet been registered in this + * environment. + * @return `true` if package was successfully registered, `false` if it + * either does not exist, was already registered or `packageName` is + * `none`. + */ +public final function bool RegisterPackage(BaseText packageName) +{ + local class<_manifest> manifestClass; + + if (packageName == none) { + return false; + } + _.logger.Auto(infoRegisteringPackage).Arg(packageName.Copy()); + manifestClass = class<_manifest>(DynamicLoadObject( + packageName.ToString() $ manifestSuffix, class'Class', true)); + if (manifestClass == none) + { + _.logger.Auto(errNotRegistered).Arg(packageName.Copy()); + return false; + } + if (IsManifestRegistered(manifestClass)) + { + _.logger.Auto(infoAlreadyRegistered).Arg(packageName.Copy()); + return false; + } + availablePackages[availablePackages.length] = manifestClass; + ReadManifest(manifestClass); + return true; +} + +/** + * Registers an Acedia package with name given by `packageName`. + * + * @param packageName Name of the package to register. + * This package must exist and not have yet been registered in this + * environment. + * @return `true` if package was successfully registered, `false` if it + * either does not exist or was already registered. + */ +public final function RegisterPackage_S(string packageName) +{ + local Text wrapper; + + wrapper = _.text.FromString(packageName); + RegisterPackage(wrapper); + _.memory.Free(wrapper); +} + +private final function bool IsManifestRegistered(class<_manifest> manifestClass) +{ + local int i; + + for (i = 0; i < availablePackages.length; i += 1) + { + if (manifestClass == availablePackages[i]) { + return true; + } + } + return false; +} + +private final function ReadManifest(class<_manifest> manifestClass) +{ + local int i; + + for (i = 0; i < manifestClass.default.aliasSources.length; i += 1) + { + if (manifestClass.default.aliasSources[i] == none) { + continue; + } + _.memory.Allocate(manifestClass.default.aliasSources[i]); + } + for (i = 0; i < manifestClass.default.features.length; i += 1) + { + if (manifestClass.default.features[i] == none) { + continue; + } + manifestClass.default.features[i].static.LoadConfigs(); + 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]); + } +} + +/** + * Returns all packages registered in the caller `AcediaEnvironment`. + * + * NOTE: package being registered doesn't mean it's actually loaded. + * Package must either be explicitly loaded or automatically when one of its + * resources is being used. + * + * @return All packages registered in caller `AcediaEnvironment`. + */ +public final function array< class<_manifest> > GetAvailablePackages() +{ + return availablePackages; +} + +/** + * Returns all packages loaded in the caller `AcediaEnvironment`. + * + * NOTE: package being registered doesn't mean it's actually loaded. + * Package must either be explicitly loaded or automatically when one of its + * resources is being used. + * + * @return All packages loaded in caller `AcediaEnvironment`. + */ +public final function array< class<_manifest> > GetLoadedPackages() +{ + return loadedPackages; +} + +/** + * Returns all `Feature`s available in the caller `AcediaEnvironment`. + * + * @return All `Feature`s available in the caller `AcediaEnvironment`. + */ +public final function array< class > GetAvailableFeatures() +{ + return availableFeatures; +} + +/** + * Returns all `Feature` instances enabled in the caller `AcediaEnvironment`. + * + * @return All `Feature`s enabled in the caller `AcediaEnvironment`. + */ +public final function array GetEnabledFeatures() +{ + local int i; + for (i = 0; i < enabledFeatures.length; i += 1) { + enabledFeatures[i].NewRef(); + } + return enabledFeatures; +} + +// CleanRemove `Feature`s that got deallocated. +// This shouldn't happen unless someone messes up. +private final function CleanEnabledFeatures() +{ + local int i; + while (i < enabledFeatures.length) + { + if ( enabledFeatures[i].GetLifeVersion() + != enabledFeaturesLifeVersions[i]) + { + enabledFeatures.Remove(i, 1); + } + else { + i += 1; + } + } +} + +/** + * Checks if `Feature` of given class `featureClass` is enabled. + * + * NOTE: even if If feature of class `featureClass` is enabled, it's not + * necessarily that the instance you have reference to is enabled. + * Although unlikely, it is possible that someone spawned another instance + * of the same class that isn't considered enabled. If you want to check + * whether some particular instance of given class `featureClass` is enabled, + * use `IsFeatureEnabled()` method instead. + * + * @param featureClass Feature class to check for being enabled. + * @return `true` if feature of class `featureClass` is currently enabled and + * `false` otherwise. + */ +public final function bool IsFeatureClassEnabled(class featureClass) +{ + local int i; + if (featureClass == none) { + return false; + } + CleanEnabledFeatures(); + for (i = 0; i < enabledFeatures.length; i += 1) + { + if (featureClass == enabledFeatures[i].class) { + return true; + } + } + return false; +} + +/** + * Checks if given `Feature` instance is enabled. + * + * If you want to check if any instance instance of given class + * `classToCheck` is enabled (and not `feature` specifically), use + * `IsFeatureClassEnabled()` method instead. + * + * @param feature Feature instance to check for being enabled. + * @return `true` if feature `feature` is currently enabled and + * `false` otherwise. + */ +public final function bool IsFeatureEnabled(Feature feature) +{ + local int i; + if (feature == none) return false; + if (!feature.IsAllocated()) return false; + + CleanEnabledFeatures(); + for (i = 0; i < enabledFeatures.length; i += 1) + { + if (feature == enabledFeatures[i]) { + return true; + } + } + return false; +} + +/** + * Returns enabled `Feature` instance of the given class `featureClass`. + * + * @param featureClass Feature class to find enabled instance for. + * @return Enabled `Feature` instance of the given class `featureClass`. + * If no feature of `featureClass` is enabled, returns `none`. + */ +public final function Feature GetEnabledFeature(class featureClass) +{ + local int i; + if (featureClass == none) { + return none; + } + CleanEnabledFeatures(); + for (i = 0; i < enabledFeatures.length; i += 1) + { + if (featureClass == enabledFeatures[i].class) + { + enabledFeatures[i].NewRef(); + return enabledFeatures[i]; + } + } + return none; +} + +/** + * Enables given `Feature` instance `newEnabledFeature` with a given config. + * + * @see `Feature::EnableMe()`. + * + * @param newEnabledFeature Instance to enable. + * @param configName Name of the config to enable `newEnabledFeature` + * feature with. `none` means "default" config (will be created, if + * necessary). + * @return `true` if given `newEnabledFeature` was enabled and `false` + * otherwise (including if feature of the same class has already been + * enabled). + */ +public final function bool EnableFeature( + Feature newEnabledFeature, + BaseText configName) +{ + local int i; + if (newEnabledFeature == none) return false; + if (!newEnabledFeature.IsAllocated()) return false; + + CleanEnabledFeatures(); + for (i = 0; i < enabledFeatures.length; i += 1) + { + if (newEnabledFeature.class == enabledFeatures[i].class) + { + if (newEnabledFeature == enabledFeatures[i]) + { + _.logger + .Auto(warnFeatureAlreadyEnabled) + .Arg(_.text.FromClass(newEnabledFeature.class)); + } + else + { + _.logger + .Auto(errFeatureClassAlreadyEnabled) + .Arg(_.text.FromClass(newEnabledFeature.class)); + } + return false; + } + } + newEnabledFeature.NewRef(); + enabledFeatures[enabledFeatures.length] = newEnabledFeature; + enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] = + newEnabledFeature.GetLifeVersion(); + newEnabledFeature.EnableInternal(configName); + onFeatureEnabledSignal.Emit(newEnabledFeature); + return true; +} + +/** + * Disables given `Feature` instance `featureToDisable`. + * + * @see `Feature::EnableMe()`. + * + * @param featureToDisable Instance to disable. + * @return `true` if given `newEnabledFeature` was disabled and `false` + * otherwise (including if it already was disabled). + */ +public final function bool DisableFeature(Feature featureToDisable) +{ + local int i; + if (featureToDisable == none) return false; + if (!featureToDisable.IsAllocated()) return false; + + CleanEnabledFeatures(); + for (i = 0; i < enabledFeatures.length; i += 1) + { + if (featureToDisable == enabledFeatures[i]) + { + enabledFeatures.Remove(i, 1); + enabledFeaturesLifeVersions.Remove(i, 1); + featureToDisable.DisableInternal(); + onFeatureDisabledSignal.Emit(featureToDisable.class); + _.memory.Free(featureToDisable); + } + return true; + } + return false; +} + +/** + * Disables all currently enabled `Feature`s. + * + * Mainly intended for the clean up when Acedia shuts down. + */ +public final function DisableAllFeatures() +{ + local int i; + local array featuresCopy; + + CleanEnabledFeatures(); + featuresCopy = enabledFeatures; + enabledFeatures.length = 0; + enabledFeaturesLifeVersions.length = 0; + for (i = 0; i < enabledFeatures.length; i += 1) + { + featuresCopy[i].DisableInternal(); + onFeatureDisabledSignal.Emit(featuresCopy[i].class); + } + _.memory.FreeMany(featuresCopy); +} + +defaultproperties +{ + manifestSuffix = ".Manifest" + infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".") + infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.") + errNotRegistered = (l=LOG_Error,m="Package \"%2\" has failed to be registered.") + warnFeatureAlreadyEnabled = (l=LOG_Warning,m="Same instance of `Feature` class `%1` is already enabled.") + errFeatureClassAlreadyEnabled = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.") +} \ No newline at end of file diff --git a/sources/CoreRealm/Events/Environment_FeatureDisabled_Signal.uc b/sources/CoreRealm/Events/Environment_FeatureDisabled_Signal.uc new file mode 100644 index 0000000..91cd09a --- /dev/null +++ b/sources/CoreRealm/Events/Environment_FeatureDisabled_Signal.uc @@ -0,0 +1,38 @@ +/** + * Signal class for `AcediaEnvironment`'s `FeatureDisabled()` signal. + * Copyright 2022 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 Environment_FeatureDisabled_Signal extends Signal; + +public final function Emit(class disabledFeatureClass) +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + Environment_FeatureDisabled_Slot(nextSlot).connect(disabledFeatureClass); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'Environment_FeatureDisabled_Slot' +} \ No newline at end of file diff --git a/sources/CoreRealm/Events/Environment_FeatureDisabled_Slot.uc b/sources/CoreRealm/Events/Environment_FeatureDisabled_Slot.uc new file mode 100644 index 0000000..83336dd --- /dev/null +++ b/sources/CoreRealm/Events/Environment_FeatureDisabled_Slot.uc @@ -0,0 +1,40 @@ +/** + * Slot class for `AcediaEnvironment`'s `FeatureDisabled()` signal. + * Copyright 2022 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 Environment_FeatureDisabled_Slot extends Slot; + +delegate connect(class disabledFeatureClass) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/CoreRealm/Events/Environment_FeatureEnabled_Signal.uc b/sources/CoreRealm/Events/Environment_FeatureEnabled_Signal.uc new file mode 100644 index 0000000..ed8b7a5 --- /dev/null +++ b/sources/CoreRealm/Events/Environment_FeatureEnabled_Signal.uc @@ -0,0 +1,38 @@ +/** + * Signal class for `AcediaEnvironment`'s `FeatureEnabled()` signal. + * Copyright 2022 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 Environment_FeatureEnabled_Signal extends Signal; + +public final function Emit(Feature enabledFeature) +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + Environment_FeatureEnabled_Slot(nextSlot).connect(enabledFeature); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'Environment_FeatureEnabled_Slot' +} \ No newline at end of file diff --git a/sources/CoreRealm/Events/Environment_FeatureEnabled_Slot.uc b/sources/CoreRealm/Events/Environment_FeatureEnabled_Slot.uc new file mode 100644 index 0000000..afd9853 --- /dev/null +++ b/sources/CoreRealm/Events/Environment_FeatureEnabled_Slot.uc @@ -0,0 +1,40 @@ +/** + * Slot class for `AcediaEnvironment`'s `FeatureEnabled()` signal. + * Copyright 2022 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 Environment_FeatureEnabled_Slot extends Slot; + +delegate connect(Feature enabledFeature) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Global.uc b/sources/CoreRealm/Global.uc similarity index 68% rename from sources/Global.uc rename to sources/CoreRealm/Global.uc index 4e10b91..12296b5 100644 --- a/sources/Global.uc +++ b/sources/CoreRealm/Global.uc @@ -25,25 +25,27 @@ class Global extends Object; // main instance in this variable's default value. var protected Global myself; -var public RefAPI ref; -var public BoxAPI box; -var public LoggerAPI logger; -var public CollectionsAPI collections; -var public UnrealAPI unreal; -var public TimeAPI time; -var public AliasesAPI alias; -var public TextAPI text; -var public MemoryAPI memory; -var public ConsoleAPI console; -var public ChatAPI chat; -var public ColorAPI color; -var public UserAPI users; -var public PlayersAPI players; -var public JSONAPI json; -var public DBAPI db; -var public AvariceAPI avarice; +var public RefAPI ref; +var public BoxAPI box; +var public LoggerAPI logger; +var public CollectionsAPI collections; +var public UnrealAPI unreal; +var public TimeAPI time; +var public AliasesAPI alias; +var public TextAPI text; +var public MemoryAPI memory; +var public ConsoleAPI console; +var public ChatAPI chat; +var public ColorAPI color; +var public UserAPI users; +var public PlayersAPI players; +var public JSONAPI json; +var public DBAPI db; +var public AvariceAPI avarice; -var public KFFrontend kf; +var public AcediaEnvironment environment; + +var public KFFrontend kf; public final static function Global GetInstance() { @@ -79,7 +81,22 @@ protected function Initialize() db = DBAPI(memory.Allocate(class'DBAPI')); avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); kf = KFFrontend(memory.Allocate(class'KF1_Frontend')); - json.StaticConstructor(); + environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); + class'InfoQueryHandler'.static.StaticConstructor(); +} + +public final function bool ConnectServerLevelCore() +{ + if (class'ServerLevelCore'.static.GetInstance() == none) { + return false; + } + class'UnrealService'.static.Require(); + class'ConnectionService'.static.Require(); + // TODO: this is hack as fuck, needs to be redone + unreal.mutator.OnMutate( + ServiceAnchor(memory.Allocate(class'ServiceAnchor'))) + .connect = EnableCommandsFeature; + return true; } public function DropGameplayAPI() @@ -88,6 +105,15 @@ public function DropGameplayAPI() kf = none; } +private final function EnableCommandsFeature( + string command, + PlayerController sendingPlayer) +{ + if (command ~= "acediacommands") { + class'Commands_Feature'.static.EmergencyEnable(); + } +} + public function DropCoreAPI() { memory = none; diff --git a/sources/CoreRealm/LevelCore.uc b/sources/CoreRealm/LevelCore.uc new file mode 100644 index 0000000..1b12850 --- /dev/null +++ b/sources/CoreRealm/LevelCore.uc @@ -0,0 +1,107 @@ +/** + * Provides Acedia with access to actors of a certain level (server, client or + * even entry level). + * Copyright 2022 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 LevelCore extends AcediaActor + abstract; + +/** + * # `LevelCore` + * + * Since in UnrealScript mixing objects and actors (or, more precisely, storing + * `Actor`s in non-actor `Object`s) is dangerous, `LevelCore`s are needed to + * provide an access to a certain level and its `Actor`s. + * + * `LevelCore is a singleton, however this isn't a design choice, but + * a side-effect of granting `Object`s safe access to an `Actor` through class + * and `static` methods and `default` fields. + * + * ## Using `LevelCore`s + * + * `LevelCore` itself is `abstract` and cannot be instantiated, so to make use + * of it, child classes must be declared. This allows one to specialize + * `LevelCore`: for example, Acedia's `ServerLevelCore` checks that net mode of + * the level it is created on is either `NM_DedicatedServer` or + * `NM_ListenServer`. + * Then Acedia's server API can only be created and used with such a level. + * + * To create you own `LevelCore`, extend simply this base class. + * If you also want to specialize your level core, overload `CreateLevelCore()` + * method to add your checks. + */ + +// Allows to force creation of `LevelCore` only through `CreateLevelCore()` +// method +var private bool blockSpawning; +// Default value of this variable will store one and only existing version +// `LevelCore` +var private LevelCore activeInstance; + +protected function Finalizer() +{ + default.activeInstance = none; +} + +public static function LevelCore CreateLevelCore(Actor source) +{ + if (GetInstance() != none) return none; + if (source == none) return none; + + default.blockSpawning = false; + default.activeInstance = source.Spawn(default.class); + default.blockSpawning = true; + return default.activeInstance; +} + +public final static function LevelCore GetInstance() +{ + local bool instanceExists; + instanceExists = default.activeInstance != none + && !default.activeInstance.bPendingDelete; + if (instanceExists) { + return default.activeInstance; + } + return none; +} + +// Make sure only one instance of 'LevelCore' exists at any point in time. +event PreBeginPlay() +{ + if (default.blockSpawning || GetInstance() != none) + { + Destroy(); + return; + } + default.activeInstance = self; + super.PreBeginPlay(); +} + +// Clean up +event Destroyed() +{ + if (self == default.activeInstance) { + default.activeInstance = none; + } + super.Destroyed(); +} + +defaultproperties +{ + blockSpawning = true +} \ No newline at end of file diff --git a/sources/_manifest.uc b/sources/CoreRealm/_manifest.uc similarity index 93% rename from sources/_manifest.uc rename to sources/CoreRealm/_manifest.uc index e6b389d..d5dd829 100644 --- a/sources/_manifest.uc +++ b/sources/CoreRealm/_manifest.uc @@ -30,9 +30,6 @@ var public const array< class > features; // List of test cases in this manifest's package. var public const array< class > testCases; -// List of required services. -var public const array< class > services; - defaultproperties { } \ No newline at end of file diff --git a/sources/CoreService.uc b/sources/CoreService.uc deleted file mode 100644 index 14a6f46..0000000 --- a/sources/CoreService.uc +++ /dev/null @@ -1,307 +0,0 @@ -/** - * 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.") -} \ No newline at end of file diff --git a/sources/Features/Feature.uc b/sources/Features/Feature.uc index 4f7ba2b..287522f 100644 --- a/sources/Features/Feature.uc +++ b/sources/Features/Feature.uc @@ -1,19 +1,8 @@ /** - * Feature represents a certain subset of Acedia's functionality that - * can be enabled or disabled, according to server owner's wishes. - * In the current version of Acedia enabling or disabling a feature requires - * manually editing configuration file and restarting a server. - * Creating a `Feature` instance should be done by using - * `EnableMe()` / `DisableMe()` methods; instead of regular `Constructor()` - * and `Finalizer()` one should use `OnEnabled() and `OnDisabled()` methods. - * Any instances created through other means will be automatically deallocated, - * enforcing `Singleton`-like behavior for the `Feature` class. - * `Feature`s store their configuration in a different object - * `FeatureConfig`, that uses per-object-config and allows users to define - * several different versions of `Feature`'s settings. Each `Feature` must be - * in 1-to-1 relationship with one sub-class of `FeatureConfig`, that should be - * defined in `configClass` variable. - * Copyright 2019 - 2021 Anton Tarasenko + * Features are intended as a replacement for mutators: a certain subset of + * functionality that can be enabled or disabled, according to server owner's + * wishes. + * Copyright 2019 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -33,83 +22,126 @@ class Feature extends AcediaObject abstract; -// Default value of this variable will store one and only existing version -// of `Feature` of this class. -var private Feature activeInstance; -var private int activeInstanceLifeVersion; +/** + * # `Feature` + * + * This class is Acedia's replacement for `Mutators`: a certain subset of + * functionality that can be enabled or disabled, according to server owner's + * wishes. Unlike `Mutator`s: + * * There is not limit for the amount of `Feature`s that can be active + * at the same time; + * * They also provide built-in ability to have several different configs + * that can be swapped during the runtime; + * * They can be enabled / disabled during the runtime. + * Achieving these points currently comes at the cost of developer having to + * perform additional work. + * + * ## Enabling `Feature` + * + * Creating a `Feature` instance should be done by using + * `EnableMe()` / `DisableMe()` methods; instead of regular `Constructor()` + * and `Finalizer()` one should use `OnEnabled() and `OnDisabled()` methods. + * There is nothing preventing you from allocating several more instances, + * however only one `Feature` per its class can be considered "enabled" at + * the same time. This is governed by `AcediaEnvironment` residing in + * the acting `Global` class. + * + * ## Configuration + * + * `Feature`s store their configuration in a different object + * `FeatureConfig`, that uses per-object-config and allows users to define + * several different versions of `Feature`'s settings. Each `Feature` must be + * in 1-to-1 relationship with one sub-class of `FeatureConfig`, that should be + * defined in `configClass` variable. + * + * ## Creating new `Feature` classes + * + * To create a new `Feature` one need: + * 1. Create child class for `Feature` (usual naming scheme + * "MyAwesomeThing_Feature") and a child class for feature config + * `FeatureConfig` (usual naming scheme is simply "MyAwesomeThing" to + * make config files more readable) and link them by setting + * `configClass` variable in `defaultproperties` in your `Feature` + * child class. + * 2. Properly setup `FeatureConfig` (read more in its own documentation); + * 3. Define `OnEnabled()` / `OnDisabled()` / `SwapConfig()` methods in + * a way that accounts for the possibility of them running during + * the gameplay (meaning that this must be possible - it can still be + * considered a heavy operation and it is allowed to cause lag). + * NOTE: `SwapConfig()` is always called just before `OnEnabled()` to + * set initial configuration up, so the bulk of `Feature` configuration + * can be done there. + * 4. Implement whatever it is your `Feature` will be doing. + */ -// Variables that store name and data from the config object that was -// chosen for this `Feature`. -// Data is expected to be in format that allows for JSON deserialization -// (see `JSONAPI.IsCompatible()` for details). -var private Text currentConfigName; -var private AssociativeArray currentConfig; +// Remembers if `EnableMe()` was called to indicate that `DisableMe()` +// should be called. +var private bool wasEnabled; +// Variable that store name of the config object that was chosen for this +// `Feature`. +var private Text currentConfigName; -// Class of this `Feature`'s config objects. Classes must be in 1-to-1 -// correspondence. +// Class of this `Feature`'s config objects. +// These classes must be in 1-to-1 correspondence. var public const class configClass; -// Setting default value of this variable to 'true' prevents creation of -// a `Feature`, even if no instances of it exist. This is used to ensure active -// `Feature`s can only be created through the proper means and behave like -// singletons. -// Only a default value is ever used. -var protected bool blockSpawning; - -// TODO: remove this? -// Setting that tells Acedia whether or not to enable this feature -// during initialization. -// Only it's default value is ever used. -var private config bool autoEnable; - // `Service` that will be launched and shut down along with this `Feature`. -// One should never launch or shut down this service manually. +// One should never launch or shut down this `Service` manually. var protected const class serviceClass; -var private string defaultConfigName; - var private LoggerAPI.Definition errorBadConfigData; -/** - * Loads all configs defined for the caller `Feature`'s class into internal - * collections. - * - * This method must be called only once, by initialization routines. - */ -public static final function LoadConfigs() +const defaultConfigName = "default"; + +protected function Finalizer() { - if (default.configClass != none) { - default.configClass.static.Initialize(); - } + DisableInternal(); + _.memory.Free(currentConfigName); + currentConfigName = none; } -protected function Constructor() +/** + * Calling this method for `Feature` instance that was added to the `Global`'s + * `AcediaEnvironment` will actually enable `Feature`, including calling + * `OnEnabled()` method. Otherwise this method will do nothing. + * + * This is internal method, it should not be called manually and neither will + * it do anything. + * + * @param newConfigName Config name to enable caller `Feature` with. + */ +public final /* internal */ function EnableInternal(BaseText newConfigName) { local FeatureService myService; - if (default.blockSpawning) - { - FreeSelf(); - return; - } + if (wasEnabled) return; + if (!_.environment.IsFeatureEnabled(self)) return; + + wasEnabled = true; if (serviceClass != none) { myService = FeatureService(serviceClass.static.Require()); } if (myService != none) { myService.SetOwnerFeature(self); } - currentConfigName = none; - ApplyConfig(default.currentConfigName); - _.memory.Free(default.currentConfigName); - default.currentConfigName = none; + ApplyConfig(newConfigName); OnEnabled(); } -protected function Finalizer() +/** + * Calling this for once enabled `Feature` instance that is no longer added to + * the active `Global`'s `AcediaEnvironment` will actually disable `Feature`, + * including calling `OnDisabled()` method. Otherwise this method will do + * nothing. + * + * This is internal method, it should not be called manually and neither will + * it do anything. + */ +public final /* internal */ function DisableInternal() { local FeatureService service; - if (GetInstance() != self) { - return; - } + if (!wasEnabled) return; + if (_.environment.IsFeatureEnabled(self)) return; + OnDisabled(); if (serviceClass != none) { service = FeatureService(serviceClass.static.GetInstance()); @@ -117,28 +149,33 @@ protected function Finalizer() if (service != none) { service.Destroy(); } - if (currentConfig != none) { - currentConfig.Empty(true); - } _.memory.Free(currentConfigName); - _.memory.Free(currentConfig); - default.currentConfigName = none; - currentConfigName = none; - currentConfig = none; - default.activeInstance = none; + currentConfigName = none; + wasEnabled = false; } -// TODO: free `newConfigName`? /** - * Changes config for the caller `Feature` class. + * Loads all configs defined for the caller `Feature`'s class into internal + * collections. * - * This method should only be called when caller `Feature` is enabled - * (allocated). To set initial config on this `Feature`'s start - specify it - * as a parameter to `EnableMe()` method. + * This method must be called only once, by initialization routines. + */ +public static final function LoadConfigs() +{ + if (default.configClass != none) { + default.configClass.static.Initialize(); + } +} + +/** + * Changes config for the caller `Feature` class. * - * Method will do nothing if `newConfigName` parameter is set to `none`. + * This method should only be called when caller `Feature` is enabled. + * To set initial config on this `Feature`'s start - specify it as a parameter + * to `EnableMe()` method. * * @param newConfigName Name of the config to apply to the caller `Feature`. + * If `none`, method will use "default" config, creating it if necessary. */ private final function ApplyConfig(BaseText newConfigName) { @@ -168,12 +205,12 @@ private final function ApplyConfig(BaseText newConfigName) * this method. * * @return Active `Feature` instance of the class used to call - * this method (i.e. `class'MyFunFeature'.static.GetInstance()`). + * this method (i.e. `class'MyFunFeature'.static.GetEnabledInstance()`). * `none` if particular `Feature` in question is not currently active. */ -public final static function Feature GetInstance() +public final static function Feature GetEnabledInstance() { - return default.activeInstance; + return __().environment.GetEnabledFeature(default.class); } /** @@ -214,12 +251,16 @@ public static final function Text GetAutoEnabledConfig() */ public static final function Text GetCurrentConfig() { - local Feature myInstance; - myInstance = GetInstance(); + local Text configNameCopy; + local Feature myInstance; + + myInstance = GetEnabledInstance(); if (myInstance == none) return none; if (myInstance.currentConfigName == none) return none; - return myInstance.currentConfigName.Copy(); + configNameCopy = myInstance.currentConfigName.Copy(); + __().memory.Free(myInstance); + return configNameCopy; } /** @@ -230,15 +271,14 @@ public static final function Text GetCurrentConfig() */ public static final function bool IsEnabled() { - return (GetInstance() != none); + return __().environment.IsFeatureClassEnabled(default.class); } /** * Enables the feature and returns it's active instance. * - * Cannot fail as long as `configName != none`. Any checks on whether it's - * appropriate to enable `Feature` must be done separately, before calling - * this method. + * Any checks on whether it's appropriate to enable `Feature` must be done + * separately, before calling this method. * * If `Feature` is already enabled - changes its config to `configName`. * @@ -249,23 +289,14 @@ public static final function bool IsEnabled() public static final function Feature EnableMe(BaseText configName) { local Feature myInstance; - myInstance = GetInstance(); + myInstance = GetEnabledInstance(); if (myInstance != none) { myInstance.ApplyConfig(configName); return myInstance; } - if (configName != none) { - default.currentConfigName = configName.Copy(); - } - else { - default.currentConfigName = none; - } - default.blockSpawning = false; myInstance = Feature(__().memory.Allocate(default.class)); - default.activeInstance = myInstance; - default.activeInstanceLifeVersion = myInstance.GetLifeVersion(); - default.blockSpawning = true; + __().environment.EnableFeature(myInstance, configName); return myInstance; } @@ -277,11 +308,12 @@ public static final function Feature EnableMe(BaseText configName) */ public static final function bool DisableMe() { - local Feature myself; - myself = GetInstance(); - if (myself != none) + local Feature myInstance; + myInstance = GetEnabledInstance(); + if (myInstance != none) { - myself.FreeSelf(); + __().environment.DisableFeature(myInstance); + __().memory.Free(myInstance); return true; } return false; @@ -319,12 +351,7 @@ protected function SwapConfig(FeatureConfig newConfig){} defaultproperties { - autoEnable = false - blockSpawning = true configClass = none serviceClass = none - - defaultConfigName = "default" - errorBadConfigData = (l=LOG_Error,m="Bad config value was provided for `%1`. Falling back to the \"default\".") } \ No newline at end of file diff --git a/sources/InfoQueryHandler/InfoQueryHandler.uc b/sources/InfoQueryHandler/InfoQueryHandler.uc index 35504e7..31070b0 100644 --- a/sources/InfoQueryHandler/InfoQueryHandler.uc +++ b/sources/InfoQueryHandler/InfoQueryHandler.uc @@ -288,11 +288,11 @@ defaultproperties TACEDIA_HELP = 2 stringConstants(2) = "Acedia always supports four commands: {$TextEmphasis help}, {$TextEmphasis status}, {$TextEmphasis version} and {$TextEmphasis credits}" TACEDIA_HELP_COMMANDS_CHAT = 3 - stringConstants(3) = "To get detailed information about available to you commands, please type {$TextEmphasis %PREFIX%help -l} in chat" + stringConstants(3) = "To get detailed information about available to you commands, please type {$TextEmphasis %PREFIX%help} in chat" TACEDIA_HELP_COMMANDS_CONSOLE = 4 stringConstants(4) = "To get detailed information about available to you commands, please type {$TextEmphasis mutate help -l} in console" TACEDIA_HELP_COMMANDS_CHAT_AND_CONSOLE = 5 - stringConstants(5) = "To get detailed information about available to you commands, please type {$TextEmphasis %PREFIX%help -l} in chat or {$TextEmphasis mutate help -l} in console" + stringConstants(5) = "To get detailed information about available to you commands, please type {$TextEmphasis %PREFIX%help} in chat or {$TextEmphasis mutate help -l} in console" TACEDIA_HELP_COMMANDS_NO = 6 stringConstants(6) = "Unfortunately other commands aren't available right now. To enable them please type {$TextEmphasis mutate acediacommands} in console if you have enough rights to reenable them." TACEDIA_HELP_COMMANDS_USELESS = 7 diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 0ca6470..e1fead3 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -24,7 +24,6 @@ defaultproperties { features(0) = class'Commands_Feature' features(1) = class'Avarice_Feature' - services(0) = class'ConnectionService' aliasSources(0) = class'AliasSource' aliasSources(1) = class'WeaponAliasSource' aliasSources(2) = class'ColorAliasSource' @@ -42,7 +41,7 @@ defaultproperties testCases(10) = class'TEST_Parser' testCases(11) = class'TEST_JSON' testCases(12) = class'TEST_TextCache' - testCases(13) = class'TEST_FormattedStrings' + testCases(13) = class'TEST_FormattedStrings' testCases(14) = class'TEST_User' testCases(15) = class'TEST_Memory' testCases(16) = class'TEST_DynamicArray' diff --git a/sources/Memory/MemoryAPI.uc b/sources/Memory/MemoryAPI.uc index 139cd32..dceda8a 100644 --- a/sources/Memory/MemoryAPI.uc +++ b/sources/Memory/MemoryAPI.uc @@ -104,7 +104,7 @@ public final function Object Allocate( actorClassToAllocate = class(classToAllocate); if (actorClassToAllocate != none) { - allocatedObject = class'CoreService'.static + allocatedObject = class'ServerLevelCore'.static .GetInstance() .Spawn(actorClassToAllocate); } @@ -278,7 +278,7 @@ public final function CollectGarbage(optional bool keepAcediaPools) } } // This makes Unreal Engine do garbage collection - class'CoreService'.static.GetInstance().ConsoleCommand("obj garbage"); + class'ServerLevelCore'.static.GetInstance().ConsoleCommand("obj garbage"); } defaultproperties diff --git a/sources/ServerLevelCore.uc b/sources/ServerLevelCore.uc new file mode 100644 index 0000000..8f1a11d --- /dev/null +++ b/sources/ServerLevelCore.uc @@ -0,0 +1,31 @@ +/** + * Copyright 2022 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 ServerLevelCore extends LevelCore; + +public static function LevelCore CreateLevelCore(Actor source) +{ + if (source == none) return none; + if (source.level.netMode != NM_DedicatedServer) return none; + + return super.CreateLevelCore(source); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Types/AcediaActor.uc b/sources/Types/AcediaActor.uc index ca55b4c..f3dbe47 100644 --- a/sources/Types/AcediaActor.uc +++ b/sources/Types/AcediaActor.uc @@ -114,8 +114,6 @@ protected final static function bool StaticConstructorGuard() if (!default._staticConstructorWasCalled) { default._staticConstructorWasCalled = true; - CoreService(class'CoreService'.static.GetInstance()) - ._registerActorClass(default.class); return false; } return true; diff --git a/sources/Types/AcediaObject.uc b/sources/Types/AcediaObject.uc index 6938f27..6435836 100644 --- a/sources/Types/AcediaObject.uc +++ b/sources/Types/AcediaObject.uc @@ -158,8 +158,6 @@ protected final static function bool StaticConstructorGuard() if (!default._staticConstructorWasCalled) { default._staticConstructorWasCalled = true; - CoreService(class'CoreService'.static.GetInstance()) - ._registerObjectClass(default.class); return false; } return true; diff --git a/sources/Unreal/BroadcastsAPI/BroadcastAPI.uc b/sources/Unreal/BroadcastsAPI/BroadcastAPI.uc index c97c4e2..024c9cc 100644 --- a/sources/Unreal/BroadcastsAPI/BroadcastAPI.uc +++ b/sources/Unreal/BroadcastsAPI/BroadcastAPI.uc @@ -282,7 +282,9 @@ public final function BroadcastHandler Add( // I don't know why, I don't know when exactly, but not resetting it // can lead to certain issues, including infinite recursion crashes. class'BroadcastHandler'.default.nextBroadcastHandlerClass = none; - newBroadcastHandler = class'CoreService'.static.Require().Spawn(newBHClass); + newBroadcastHandler = class'ServerLevelCore'.static + .GetInstance() + .Spawn(newBHClass); if (injectionLevel == BHIJ_Registered) { // There is guaranteed to be SOME broadcast handler diff --git a/sources/Unreal/UnrealAPI.uc b/sources/Unreal/UnrealAPI.uc index e58e259..bc6a2dc 100644 --- a/sources/Unreal/UnrealAPI.uc +++ b/sources/Unreal/UnrealAPI.uc @@ -111,7 +111,7 @@ public final function SimpleSlot OnDestructionFor( */ public final function LevelInfo GetLevel() { - return class'CoreService'.static.GetInstance().level; + return class'ServerLevelCore'.static.GetInstance().level; } /** @@ -123,7 +123,7 @@ public final function LevelInfo GetLevel() */ public final function GameReplicationInfo GetGameRI() { - return class'CoreService'.static.GetInstance().level.GRI; + return class'ServerLevelCore'.static.GetInstance().level.GRI; } /** @@ -150,7 +150,7 @@ public final function KFGameReplicationInfo GetKFGameRI() */ public final function GameInfo GetGameType() { - return class'CoreService'.static.GetInstance().level.game; + return class'ServerLevelCore'.static.GetInstance().level.game; } /** @@ -177,10 +177,10 @@ public final function KFGameType GetKFGameType() */ public final function Actor FindActorInstance(class classToFind) { - local Actor result; - local Service service; - service = class'CoreService'.static.Require(); - foreach service.AllActors(classToFind, result) + local Actor result; + local LevelCore core; + core = class'ServerLevelCore'.static.GetInstance(); + foreach core.AllActors(classToFind, result) { if (result != none) { break; @@ -198,7 +198,7 @@ public final function Actor FindActorInstance(class classToFind) */ public final function PlayerController GetLocalPlayer() { - return class'CoreService'.static.GetInstance().level + return class'ServerLevelCore'.static.GetInstance().level .GetLocalPlayerController(); }