From f7cbf540450e9a3a91e9908c5d0bbe3448d23077 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Thu, 23 Jun 2022 02:45:20 +0700 Subject: [PATCH] Done --- sources/Commands/Commands_Feature.uc | 24 +- sources/{ => CoreRealm}/AcediaEnvironment.uc | 383 ++++++++++++------ .../Environment_FeatureDisabled_Signal.uc | 38 ++ .../Environment_FeatureDisabled_Slot.uc | 40 ++ .../Environment_FeatureEnabled_Signal.uc | 38 ++ .../Events/Environment_FeatureEnabled_Slot.uc | 40 ++ sources/{ => CoreRealm}/Global.uc | 2 +- sources/{ => CoreRealm}/LevelCore.uc | 0 sources/{ => CoreRealm}/_manifest.uc | 0 sources/Features/Feature.uc | 140 +++++-- 10 files changed, 521 insertions(+), 184 deletions(-) rename sources/{ => CoreRealm}/AcediaEnvironment.uc (66%) create mode 100644 sources/CoreRealm/Events/Environment_FeatureDisabled_Signal.uc create mode 100644 sources/CoreRealm/Events/Environment_FeatureDisabled_Slot.uc create mode 100644 sources/CoreRealm/Events/Environment_FeatureEnabled_Signal.uc create mode 100644 sources/CoreRealm/Events/Environment_FeatureEnabled_Slot.uc rename sources/{ => CoreRealm}/Global.uc (98%) rename sources/{ => CoreRealm}/LevelCore.uc (100%) rename sources/{ => CoreRealm}/_manifest.uc (100%) diff --git a/sources/Commands/Commands_Feature.uc b/sources/Commands/Commands_Feature.uc index acdf1e0..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,6 +127,7 @@ public final static function EmergencyEnable() { local Text autoConfig; local Commands_Feature feature; + if (!IsEnabled()) { autoConfig = GetAutoEnabledConfig(); @@ -159,6 +155,7 @@ public final static function EmergencyEnable() public final static function bool IsUsingChatInput() { local Commands_Feature instance; + instance = Commands_Feature(GetEnabledInstance()); if (instance != none) { return instance.useChatInput; @@ -177,6 +174,7 @@ public final static function bool IsUsingChatInput() public final static function bool IsUsingMutateInput() { local Commands_Feature instance; + instance = Commands_Feature(GetEnabledInstance()); if (instance != none) { return instance.useMutateInput; @@ -194,6 +192,7 @@ public final static function bool IsUsingMutateInput() public final static function Text GetChatPrefix() { local Commands_Feature instance; + 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,7 @@ 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; diff --git a/sources/AcediaEnvironment.uc b/sources/CoreRealm/AcediaEnvironment.uc similarity index 66% rename from sources/AcediaEnvironment.uc rename to sources/CoreRealm/AcediaEnvironment.uc index 499e8cd..e00b0c6 100644 --- a/sources/AcediaEnvironment.uc +++ b/sources/CoreRealm/AcediaEnvironment.uc @@ -35,6 +35,13 @@ class AcediaEnvironment extends AcediaObject; * 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; @@ -51,149 +58,58 @@ var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled; var private LoggerAPI.Definition warnFeatureAlreadyEnabled; var private LoggerAPI.Definition errFeatureClassAlreadyEnabled; -protected function Constructor() -{ - // Always register our core package - RegisterPackage_S("AcediaCore"); -} - -private final function CleanEnabledFeatures() -{ - local int i; - while (i < enabledFeatures.length) - { - if ( enabledFeatures[i].GetLifeVersion() - != enabledFeaturesLifeVersions[i]) - { - enabledFeatures.Remove(i, 1); - } - else { - i += 1; - } - } -} - -public final function bool IsFeatureClassEnabled(class classToCheck) -{ - local int i; - if (classToCheck == none) { - return false; - } - CleanEnabledFeatures(); - for (i = 0; i < enabledFeatures.length; i += 1) - { - if (classToCheck == enabledFeatures[i].class) { - return true; - } - } - return false; -} +var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal; +var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal; -public final function bool IsFeatureEnabled(Feature featureToCheck) +/** + * 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) { - local int i; - if (featureToCheck == none) return false; - if (!featureToCheck.IsAllocated()) return false; - - CleanEnabledFeatures(); - for (i = 0; i < enabledFeatures.length; i += 1) - { - if (featureToCheck == enabledFeatures[i]) { - return true; - } - } - return false; + return Environment_FeatureEnabled_Slot( + onFeatureEnabledSignal.NewSlot(receiver)); } -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; -} - -public final function bool EnableFeature( - Feature newEnabledFeature, - BaseText configName) +/** + * 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) { - 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; - } - } - enabledFeatures[enabledFeatures.length] = newEnabledFeature; - enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] = - newEnabledFeature.GetLifeVersion(); - newEnabledFeature.EnableInternal(configName); - return true; + return Environment_FeatureDisabled_Slot( + onFeatureEnabledSignal.NewSlot(receiver)); } -public final function bool DisableFeature(Feature featureToDisable) +protected function Constructor() { - local int i; - if (featureToDisable == none) return false; - if (!featureToDisable.IsAllocated()) return false; - - CleanEnabledFeatures(); - for (i = 0; i < enabledFeatures.length; i += 1) - { - if (featureToDisable.class == enabledFeatures[i].class) - { - if (featureToDisable == enabledFeatures[i]) - { - enabledFeatures.Remove(i, 1); - enabledFeaturesLifeVersions.Remove(i, 1); - featureToDisable.DisableInternal(); - } - return true; - } - } - return false; + // 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')); } -public final function DisableAllFeatures() +protected function Finalizer() { - 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(); - } - _.memory.FreeMany(featuresCopy); + _.memory.Free(onFeatureEnabledSignal); + _.memory.Free(onFeatureDisabledSignal); } /** @@ -341,6 +257,209 @@ public final function array GetEnabledFeatures() 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" 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 98% rename from sources/Global.uc rename to sources/CoreRealm/Global.uc index ebdce04..12296b5 100644 --- a/sources/Global.uc +++ b/sources/CoreRealm/Global.uc @@ -82,7 +82,7 @@ protected function Initialize() avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); kf = KFFrontend(memory.Allocate(class'KF1_Frontend')); environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); - //class'InfoQueryHandler'.static.StaticConstructor(); + class'InfoQueryHandler'.static.StaticConstructor(); } public final function bool ConnectServerLevelCore() diff --git a/sources/LevelCore.uc b/sources/CoreRealm/LevelCore.uc similarity index 100% rename from sources/LevelCore.uc rename to sources/CoreRealm/LevelCore.uc diff --git a/sources/_manifest.uc b/sources/CoreRealm/_manifest.uc similarity index 100% rename from sources/_manifest.uc rename to sources/CoreRealm/_manifest.uc diff --git a/sources/Features/Feature.uc b/sources/Features/Feature.uc index e3cc6ab..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,26 +22,94 @@ class Feature extends AcediaObject abstract; -var private bool wasEnabled; +/** + * # `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). +// 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; // `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; +const defaultConfigName = "default"; + +protected function Finalizer() +{ + DisableInternal(); + _.memory.Free(currentConfigName); + currentConfigName = none; +} + +/** + * 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; @@ -70,6 +127,15 @@ public final /* internal */ function EnableInternal(BaseText newConfigName) OnEnabled(); } +/** + * 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; @@ -101,23 +167,15 @@ public static final function LoadConfigs() } } -protected function Finalizer() -{ - DisableInternal(); - _.memory.Free(currentConfigName); - currentConfigName = none; -} - /** * Changes config for the caller `Feature` class. * - * 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. - * - * 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) { @@ -219,9 +277,8 @@ public static final function bool IsEnabled() /** * 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`. * @@ -296,8 +353,5 @@ defaultproperties { 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