Anton Tarasenko 2 years ago
parent
commit
f7cbf54045
  1. 24
      sources/Commands/Commands_Feature.uc
  2. 383
      sources/CoreRealm/AcediaEnvironment.uc
  3. 38
      sources/CoreRealm/Events/Environment_FeatureDisabled_Signal.uc
  4. 40
      sources/CoreRealm/Events/Environment_FeatureDisabled_Slot.uc
  5. 38
      sources/CoreRealm/Events/Environment_FeatureEnabled_Signal.uc
  6. 40
      sources/CoreRealm/Events/Environment_FeatureEnabled_Slot.uc
  7. 2
      sources/CoreRealm/Global.uc
  8. 0
      sources/CoreRealm/LevelCore.uc
  9. 0
      sources/CoreRealm/_manifest.uc
  10. 140
      sources/Features/Feature.uc

24
sources/Commands/Commands_Feature.uc

@ -58,12 +58,6 @@ protected function OnEnabled()
commandDelimiters[2] = P("["); commandDelimiters[2] = P("[");
// Negation of the selector // Negation of the selector
commandDelimiters[3] = P("!"); 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() protected function OnDisabled()
@ -90,6 +84,7 @@ protected function OnDisabled()
protected function SwapConfig(FeatureConfig config) protected function SwapConfig(FeatureConfig config)
{ {
local Commands newConfig; local Commands newConfig;
newConfig = Commands(config); newConfig = Commands(config);
if (newConfig == none) { if (newConfig == none) {
return; return;
@ -132,6 +127,7 @@ public final static function EmergencyEnable()
{ {
local Text autoConfig; local Text autoConfig;
local Commands_Feature feature; local Commands_Feature feature;
if (!IsEnabled()) if (!IsEnabled())
{ {
autoConfig = GetAutoEnabledConfig(); autoConfig = GetAutoEnabledConfig();
@ -159,6 +155,7 @@ public final static function EmergencyEnable()
public final static function bool IsUsingChatInput() public final static function bool IsUsingChatInput()
{ {
local Commands_Feature instance; local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance()); instance = Commands_Feature(GetEnabledInstance());
if (instance != none) { if (instance != none) {
return instance.useChatInput; return instance.useChatInput;
@ -177,6 +174,7 @@ public final static function bool IsUsingChatInput()
public final static function bool IsUsingMutateInput() public final static function bool IsUsingMutateInput()
{ {
local Commands_Feature instance; local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance()); instance = Commands_Feature(GetEnabledInstance());
if (instance != none) { if (instance != none) {
return instance.useMutateInput; return instance.useMutateInput;
@ -194,6 +192,7 @@ public final static function bool IsUsingMutateInput()
public final static function Text GetChatPrefix() public final static function Text GetChatPrefix()
{ {
local Commands_Feature instance; local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance()); instance = Commands_Feature(GetEnabledInstance());
if (instance != none && instance.chatCommandPrefix != none) { if (instance != none && instance.chatCommandPrefix != none) {
return instance.chatCommandPrefix.Copy(); return instance.chatCommandPrefix.Copy();
@ -214,6 +213,7 @@ public final function RegisterCommand(class<Command> commandClass)
{ {
local Text commandName; local Text commandName;
local Command newCommandInstance, existingCommandInstance; local Command newCommandInstance, existingCommandInstance;
if (commandClass == none) return; if (commandClass == none) return;
if (registeredCommands == none) return; if (registeredCommands == none) return;
@ -252,6 +252,7 @@ public final function RemoveCommand(class<Command> commandClass)
local Command nextCommand; local Command nextCommand;
local Text nextCommandName; local Text nextCommandName;
local array<Text> keysToRemove; local array<Text> keysToRemove;
if (commandClass == none) return; if (commandClass == none) return;
if (registeredCommands == none) return; if (registeredCommands == none) return;
@ -282,8 +283,10 @@ public final function Command GetCommand(BaseText commandName)
{ {
local Text commandNameLowerCase; local Text commandNameLowerCase;
local Command commandInstance; local Command commandInstance;
if (commandName == none) return none; if (commandName == none) return none;
if (registeredCommands == none) return none; if (registeredCommands == none) return none;
commandNameLowerCase = commandName.LowerCopy(); commandNameLowerCase = commandName.LowerCopy();
commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase)); commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase));
commandNameLowerCase.FreeSelf(); commandNameLowerCase.FreeSelf();
@ -301,8 +304,10 @@ public final function array<Text> GetCommandNames()
local array<AcediaObject> keys; local array<AcediaObject> keys;
local Text nextKeyAsText; local Text nextKeyAsText;
local array<Text> keysAsText; local array<Text> keysAsText;
if (registeredCommands == none) return keysAsText;
if (registeredCommands == none) {
return keysAsText;
}
keys = registeredCommands.GetKeys(); keys = registeredCommands.GetKeys();
for (i = 0; i < keys.length; i += 1) 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 commandInstance;
local Command.CallData callData; local Command.CallData callData;
local MutableText commandName; local MutableText commandName;
if (parser == none) return; if (parser == none) return;
if (!parser.Ok()) return; if (!parser.Ok()) return;
@ -347,6 +353,7 @@ private function bool HandleCommands(
bool teamMessage) bool teamMessage)
{ {
local Parser parser; local Parser parser;
// We are only interested in messages that start with `chatCommandPrefix` // We are only interested in messages that start with `chatCommandPrefix`
parser = _.text.Parse(message); parser = _.text.Parse(message);
if (!parser.Match(chatCommandPrefix).Ok()) if (!parser.Match(chatCommandPrefix).Ok())
@ -364,6 +371,7 @@ private function HandleMutate(string command, PlayerController sendingPlayer)
{ {
local Parser parser; local Parser parser;
local EPlayer sender; local EPlayer sender;
// Ignore just "help", since a lot of other mutators use it // Ignore just "help", since a lot of other mutators use it
if (command ~= "help") { if (command ~= "help") {
return; return;

383
sources/AcediaEnvironment.uc → sources/CoreRealm/AcediaEnvironment.uc

@ -35,6 +35,13 @@ class AcediaEnvironment extends AcediaObject;
* Acedia will become aware of all the resources that package contains. * 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 * Once any of those resources is used, package gets marked as *loaded* and its
* *entry object* (if specified) will be created. * *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> > availablePackages;
@ -51,149 +58,58 @@ var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled;
var private LoggerAPI.Definition warnFeatureAlreadyEnabled; var private LoggerAPI.Definition warnFeatureAlreadyEnabled;
var private LoggerAPI.Definition errFeatureClassAlreadyEnabled; var private LoggerAPI.Definition errFeatureClassAlreadyEnabled;
protected function Constructor() var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal;
{ var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal;
// 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<Feature> 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;
}
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 <slot>(Feature enabledFeature)
*
* @param enabledFeature `Feature` instance that was just enabled.
*/
/* SIGNAL */
public final function Environment_FeatureEnabled_Slot OnFeatureEnabled(
AcediaObject receiver)
{ {
local int i; return Environment_FeatureEnabled_Slot(
if (featureToCheck == none) return false; onFeatureEnabledSignal.NewSlot(receiver));
if (!featureToCheck.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1)
{
if (featureToCheck == enabledFeatures[i]) {
return true;
}
}
return false;
} }
public final function Feature GetEnabledFeature(class<Feature> featureClass) /**
{ * Signal that will be emitted when new `Feature` is disabled.
local int i; * Emitted after `Feature`'s `OnDisabled()` method was called.
if (featureClass == none) { *
return none; * [Signature]
} * void <slot>(class<Feature> disabledFeatureClass)
CleanEnabledFeatures(); *
for (i = 0; i < enabledFeatures.length; i += 1) * @param disabledFeatureClass Class of the `Feature` instance that was
{ * just disabled.
if (featureClass == enabledFeatures[i].class) */
{ /* SIGNAL */
enabledFeatures[i].NewRef(); public final function Environment_FeatureDisabled_Slot OnFeatureDisabled(
return enabledFeatures[i]; AcediaObject receiver)
}
}
return none;
}
public final function bool EnableFeature(
Feature newEnabledFeature,
BaseText configName)
{ {
local int i; return Environment_FeatureDisabled_Slot(
if (newEnabledFeature == none) return false; onFeatureEnabledSignal.NewSlot(receiver));
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;
} }
public final function bool DisableFeature(Feature featureToDisable) protected function Constructor()
{ {
local int i; // Always register our core package
if (featureToDisable == none) return false; RegisterPackage_S("AcediaCore");
if (!featureToDisable.IsAllocated()) return false; onFeatureEnabledSignal = Environment_FeatureEnabled_Signal(
_.memory.Allocate(class'Environment_FeatureEnabled_Signal'));
CleanEnabledFeatures(); onFeatureDisabledSignal = Environment_FeatureDisabled_Signal(
for (i = 0; i < enabledFeatures.length; i += 1) _.memory.Allocate(class'Environment_FeatureDisabled_Signal'));
{
if (featureToDisable.class == enabledFeatures[i].class)
{
if (featureToDisable == enabledFeatures[i])
{
enabledFeatures.Remove(i, 1);
enabledFeaturesLifeVersions.Remove(i, 1);
featureToDisable.DisableInternal();
}
return true;
}
}
return false;
} }
public final function DisableAllFeatures() protected function Finalizer()
{ {
local int i; _.memory.Free(onFeatureEnabledSignal);
local array<Feature> featuresCopy; _.memory.Free(onFeatureDisabledSignal);
CleanEnabledFeatures();
featuresCopy = enabledFeatures;
enabledFeatures.length = 0;
enabledFeaturesLifeVersions.length = 0;
for (i = 0; i < enabledFeatures.length; i += 1) {
featuresCopy[i].DisableInternal();
}
_.memory.FreeMany(featuresCopy);
} }
/** /**
@ -341,6 +257,209 @@ public final function array<Feature> GetEnabledFeatures()
return enabledFeatures; 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<Feature> 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<Feature> 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<Feature> 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 defaultproperties
{ {
manifestSuffix = ".Manifest" manifestSuffix = ".Manifest"

38
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 <https://www.gnu.org/licenses/>.
*/
class Environment_FeatureDisabled_Signal extends Signal;
public final function Emit(class<Feature> 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'
}

40
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 <https://www.gnu.org/licenses/>.
*/
class Environment_FeatureDisabled_Slot extends Slot;
delegate connect(class<Feature> disabledFeatureClass)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

38
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 <https://www.gnu.org/licenses/>.
*/
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'
}

40
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 <https://www.gnu.org/licenses/>.
*/
class Environment_FeatureEnabled_Slot extends Slot;
delegate connect(Feature enabledFeature)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

2
sources/Global.uc → sources/CoreRealm/Global.uc

@ -82,7 +82,7 @@ protected function Initialize()
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); avarice = AvariceAPI(memory.Allocate(class'AvariceAPI'));
kf = KFFrontend(memory.Allocate(class'KF1_Frontend')); kf = KFFrontend(memory.Allocate(class'KF1_Frontend'));
environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment'));
//class'InfoQueryHandler'.static.StaticConstructor(); class'InfoQueryHandler'.static.StaticConstructor();
} }
public final function bool ConnectServerLevelCore() public final function bool ConnectServerLevelCore()

0
sources/LevelCore.uc → sources/CoreRealm/LevelCore.uc

0
sources/_manifest.uc → sources/CoreRealm/_manifest.uc

140
sources/Features/Feature.uc

@ -1,19 +1,8 @@
/** /**
* Feature represents a certain subset of Acedia's functionality that * Features are intended as a replacement for mutators: a certain subset of
* can be enabled or disabled, according to server owner's wishes. * functionality that can be enabled or disabled, according to server owner's
* In the current version of Acedia enabling or disabling a feature requires * wishes.
* manually editing configuration file and restarting a server. * Copyright 2019 - 2022 Anton Tarasenko
* 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
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -33,26 +22,94 @@
class Feature extends AcediaObject class Feature extends AcediaObject
abstract; 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 // Remembers if `EnableMe()` was called to indicate that `DisableMe()`
// chosen for this `Feature`. // should be called.
// Data is expected to be in format that allows for JSON deserialization var private bool wasEnabled;
// (see `JSONAPI.IsCompatible()` for details). // Variable that store name of the config object that was chosen for this
// `Feature`.
var private Text currentConfigName; var private Text currentConfigName;
// Class of this `Feature`'s config objects. Classes must be in 1-to-1 // Class of this `Feature`'s config objects.
// correspondence. // These classes must be in 1-to-1 correspondence.
var public const class<FeatureConfig> configClass; var public const class<FeatureConfig> configClass;
// `Service` that will be launched and shut down along with this `Feature`. // `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<FeatureService> serviceClass; var protected const class<FeatureService> serviceClass;
var private string defaultConfigName;
var private LoggerAPI.Definition errorBadConfigData; 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) public final /* internal */ function EnableInternal(BaseText newConfigName)
{ {
local FeatureService myService; local FeatureService myService;
@ -70,6 +127,15 @@ public final /* internal */ function EnableInternal(BaseText newConfigName)
OnEnabled(); 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() public final /* internal */ function DisableInternal()
{ {
local FeatureService service; 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. * Changes config for the caller `Feature` class.
* *
* This method should only be called when caller `Feature` is enabled * This method should only be called when caller `Feature` is enabled.
* (allocated). To set initial config on this `Feature`'s start - specify it * To set initial config on this `Feature`'s start - specify it as a parameter
* as a parameter to `EnableMe()` method. * to `EnableMe()` method.
*
* Method will do nothing if `newConfigName` parameter is set to `none`.
* *
* @param newConfigName Name of the config to apply to the caller `Feature`. * @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) 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. * Enables the feature and returns it's active instance.
* *
* Cannot fail as long as `configName != none`. Any checks on whether it's * Any checks on whether it's appropriate to enable `Feature` must be done
* appropriate to enable `Feature` must be done separately, before calling * separately, before calling this method.
* this method.
* *
* If `Feature` is already enabled - changes its config to `configName`. * If `Feature` is already enabled - changes its config to `configName`.
* *
@ -296,8 +353,5 @@ defaultproperties
{ {
configClass = none configClass = none
serviceClass = none serviceClass = none
defaultConfigName = "default"
errorBadConfigData = (l=LOG_Error,m="Bad config value was provided for `%1`. Falling back to the \"default\".") errorBadConfigData = (l=LOG_Error,m="Bad config value was provided for `%1`. Falling back to the \"default\".")
} }
Loading…
Cancel
Save