diff --git a/sources/BaseAPI/API/Commands/Commands_Feature.uc b/sources/BaseAPI/API/Commands/Commands_Feature.uc
index f02ae16..cf80c5d 100644
--- a/sources/BaseAPI/API/Commands/Commands_Feature.uc
+++ b/sources/BaseAPI/API/Commands/Commands_Feature.uc
@@ -79,6 +79,7 @@ protected function OnEnabled() {
registeredCommands = _.collections.EmptyHashTable();
groupedCommands = _.collections.EmptyHashTable();
RegisterCommand(class'ACommandHelp');
+ RegisterCommand(class'ACommandNotify');
// Macro selector
commandDelimiters[0] = _.text.FromString("@");
// Key selector
diff --git a/sources/LevelAPI/Features/Commands/Commands_Feature.uc b/sources/LevelAPI/Features/Commands/Commands_Feature.uc
deleted file mode 100644
index 2f9008c..0000000
--- a/sources/LevelAPI/Features/Commands/Commands_Feature.uc
+++ /dev/null
@@ -1,627 +0,0 @@
-/**
- * This feature provides a mechanism to define commands that automatically
- * parse their arguments into standard Acedia collection. It also allows to
- * manage them (and specify limitation on how they can be called) in a
- * centralized manner.
- * Copyright 2021-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 Commands_Feature extends Feature;
-
-/**
- * # `Commands_Feature`
- *
- * This feature provides a mechanism to define commands that automatically
- * parse their arguments into standard Acedia collection. It also allows to
- * manage them (and specify limitation on how they can be called) in a
- * centralized manner.
- * Support command input from chat and "mutate" command.
- *
- * ## Usage
- *
- * Should be enabled like any other feature. Additionally support
- * `EmergencyEnable()` enabling method that bypasses regular settings to allow
- * admins to start this feature while forcefully enabling "mutate" command
- * input method.
- * Available configuration:
- *
- * 1. Whether to use command input from chat and what prefix is used to
- * denote a command (by default "!");
- * 2. Whether to use command input from "mutate" command.
- *
- * To add new commands into the system - get enabled instance of this
- * feature and call its `RegisterCommand()` method to add your custom
- * `Command` class. `RemoveCommand()` can also be used to de-register
- * a command, if you need this for some reason.
- *
- * ## Implementation
- *
- * Implementation is simple: calling a method `RegisterCommand()` adds
- * command into two caches `registeredCommands` for obtaining registered
- * commands by name and `groupedCommands` for obtaining arrays of commands by
- * their group name. These arrays are used for providing methods for fetching
- * arrays of commands and obtaining pre-allocated `Command` instances by their
- * name.
- * Depending on settings, this feature also connects to corresponding
- * signals for catching "mutate"/chat input, then it checks user-specified name
- * for being an alias and picks correct command from `registeredCommands`.
- * Emergency enabling this feature sets `emergencyEnabledMutate` flag that
- * enforces connecting to the "mutate" input.
- */
-
-// Delimiters that always separate command name from it's parameters
-var private array commandDelimiters;
-// Registered commands, recorded as (, ) pairs.
-// Keys should be deallocated when their entry is removed.
-var private HashTable registeredCommands;
-// `HashTable` of "" <-> `ArrayList` of commands pairs
-// to allow quick fetch of commands belonging to a single group
-var private HashTable groupedCommands;
-
-// When this flag is set to true, mutate input becomes available
-// despite `useMutateInput` flag to allow to unlock server in case of an error
-var private bool emergencyEnabledMutate;
-
-// Setting this to `true` enables players to input commands right in the chat
-// by prepending them with `chatCommandPrefix`.
-// Default is `true`.
-var private /*config*/ bool useChatInput;
-// Setting this to `true` enables players to input commands with "mutate"
-// console command.
-// Default is `true`.
-var private /*config*/ bool useMutateInput;
-// Chat messages, prepended by this prefix will be treated as commands.
-// Default is "!". Empty values are also treated as "!".
-var private /*config*/ Text chatCommandPrefix;
-// List of steam IDs of players allowed to use commands.
-// Temporary measure until a better solution is finished.
-var private /*config*/ array allowedPlayers;
-
-// Contains name of the command to call plus, optionally,
-// additional sub-command name.
-// Normally sub-command name is parsed by the command itself, however
-// command aliases can try to enforce one.
-struct CommandCallPair
-{
- var MutableText commandName;
- // In case it is enforced by an alias
- var MutableText subCommandName;
-};
-
-var LoggerAPI.Definition errCommandDuplicate;
-
-protected function OnEnabled()
-{
- registeredCommands = _.collections.EmptyHashTable();
- groupedCommands = _.collections.EmptyHashTable();
- RegisterCommand(class'ACommandHelp');
- RegisterCommand(class'ACommandNotify');
- // Macro selector
- commandDelimiters[0] = _.text.FromString("@");
- // Key selector
- commandDelimiters[1] = _.text.FromString("#");
- // Player array (possibly JSON array)
- commandDelimiters[2] = _.text.FromString("[");
- // Negation of the selector
- commandDelimiters[3] = _.text.FromString("!");
-}
-
-protected function OnDisabled()
-{
- if (useChatInput) {
- _.chat.OnMessage(self).Disconnect();
- }
- if (useMutateInput) {
- _server.unreal.mutator.OnMutate(self).Disconnect();
- }
- useChatInput = false;
- useMutateInput = false;
- _.memory.Free(registeredCommands);
- _.memory.Free(groupedCommands);
- _.memory.Free(chatCommandPrefix);
- _.memory.FreeMany(commandDelimiters);
- registeredCommands = none;
- groupedCommands = none;
- chatCommandPrefix = none;
- commandDelimiters.length = 0;
-}
-
-protected function SwapConfig(FeatureConfig config)
-{
- local Commands newConfig;
-
- newConfig = Commands(config);
- if (newConfig == none) {
- return;
- }
- _.memory.Free(chatCommandPrefix);
- chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
- allowedPlayers = newConfig.allowedPlayers;
- if (useChatInput != newConfig.useChatInput)
- {
- useChatInput = newConfig.useChatInput;
- if (newConfig.useChatInput) {
- _.chat.OnMessage(self).connect = HandleCommands;
- }
- else {
- _.chat.OnMessage(self).Disconnect();
- }
- }
- // Do not make any modifications here in case "mutate" was
- // emergency-enabled
- if (useMutateInput != newConfig.useMutateInput && !emergencyEnabledMutate)
- {
- useMutateInput = newConfig.useMutateInput;
- if (newConfig.useMutateInput) {
- _server.unreal.mutator.OnMutate(self).connect = HandleMutate;
- }
- else {
- _server.unreal.mutator.OnMutate(self).Disconnect();
- }
- }
-}
-
-/**
- * `Command_Feature` is a critical command to have running on your server and,
- * if disabled by accident, there will be no way of starting it again without
- * restarting the level or even editing configs.
- *
- * This method allows to enable it along with "mutate" input in case something
- * goes wrong.
- */
-public final static function EmergencyEnable()
-{
- local Text autoConfig;
- local Commands_Feature feature;
-
- if (!IsEnabled())
- {
- autoConfig = GetAutoEnabledConfig();
- EnableMe(autoConfig);
- __().memory.Free(autoConfig);
- }
- feature = Commands_Feature(GetEnabledInstance());
- if ( !feature.emergencyEnabledMutate
- && !feature.IsUsingMutateInput() && !feature.IsUsingChatInput())
- {
- default.emergencyEnabledMutate = true;
- feature.emergencyEnabledMutate = true;
- __server().unreal.mutator.OnMutate(feature).connect = HandleMutate;
- }
-}
-
-/**
- * Checks if `Commands_Feature` currently uses chat as input.
- * If `Commands_Feature` is not enabled, then it does not use anything
- * as input.
- *
- * @return `true` if `Commands_Feature` is currently enabled and is using chat
- * as input and `false` otherwise.
- */
-public final static function bool IsUsingChatInput()
-{
- local Commands_Feature instance;
-
- instance = Commands_Feature(GetEnabledInstance());
- if (instance != none) {
- return instance.useChatInput;
- }
- return false;
-}
-
-/**
- * Checks if `Commands_Feature` currently uses mutate command as input.
- * If `Commands_Feature` is not enabled, then it does not use anything
- * as input.
- *
- * @return `true` if `Commands_Feature` is currently enabled and is using
- * mutate command as input and `false` otherwise.
- */
-public final static function bool IsUsingMutateInput()
-{
- local Commands_Feature instance;
-
- instance = Commands_Feature(GetEnabledInstance());
- if (instance != none) {
- return instance.useMutateInput;
- }
- return false;
-}
-
-/**
- * Returns prefix that will indicate that chat message is intended to be
- * a command. By default "!".
- *
- * @return Prefix that indicates that chat message is intended to be a command.
- * If `Commands_Feature` is disabled, always returns `false`.
- */
-public final static function Text GetChatPrefix()
-{
- local Commands_Feature instance;
-
- instance = Commands_Feature(GetEnabledInstance());
- if (instance != none && instance.chatCommandPrefix != none) {
- return instance.chatCommandPrefix.Copy();
- }
- return none;
-}
-
-/**
- * Registers given command class, making it available for usage.
- *
- * If `commandClass` provides command with a name that is already taken
- * (comparison is case-insensitive) by a different command - a warning will be
- * logged and newly passed `commandClass` discarded.
- *
- * @param commandClass New command class to register.
- */
-public final function RegisterCommand(class commandClass)
-{
- local Text commandName, groupName;
- local ArrayList groupArray;
- local Command newCommandInstance, existingCommandInstance;
-
- if (commandClass == none) return;
- if (registeredCommands == none) return;
-
- newCommandInstance = Command(_.memory.Allocate(commandClass, true));
- commandName = newCommandInstance.GetName();
- groupName = newCommandInstance.GetGroupName();
- // Check for duplicates and report them
- existingCommandInstance = Command(registeredCommands.GetItem(commandName));
- if (existingCommandInstance != none)
- {
- _.logger.Auto(errCommandDuplicate)
- .ArgClass(existingCommandInstance.class)
- .Arg(commandName)
- .ArgClass(commandClass);
- _.memory.Free(groupName);
- _.memory.Free(newCommandInstance);
- _.memory.Free(existingCommandInstance);
- return;
- }
- // Otherwise record new command
- // `commandName` used as a key, do not deallocate it
- registeredCommands.SetItem(commandName, newCommandInstance);
- // Add to grouped collection
- groupArray = groupedCommands.GetArrayList(groupName);
- if (groupArray == none) {
- groupArray = _.collections.EmptyArrayList();
- }
- groupArray.AddItem(newCommandInstance);
- groupedCommands.SetItem(groupName, groupArray);
- _.memory.Free(groupArray);
- _.memory.Free(groupName);
- _.memory.Free(commandName);
- _.memory.Free(newCommandInstance);
-}
-
-/**
- * Removes command of class `commandClass` from the list of
- * registered commands.
- *
- * WARNING: removing once registered commands is not an action that is expected
- * to be performed under normal circumstances and it is not efficient.
- * It is linear on the current amount of commands.
- *
- * @param commandClass Class of command to remove from being registered.
- */
-public final function RemoveCommand(class commandClass)
-{
- local int i;
- local CollectionIterator iter;
- local Command nextCommand;
- local Text nextCommandName;
- local array commandGroup;
- local array keysToRemove;
-
- if (commandClass == none) return;
- if (registeredCommands == none) return;
-
- for (iter = registeredCommands.Iterate(); !iter.HasFinished(); iter.Next())
- {
- nextCommand = Command(iter.Get());
- nextCommandName = Text(iter.GetKey());
- if ( nextCommand == none || nextCommandName == none
- || nextCommand.class != commandClass)
- {
- _.memory.Free(nextCommand);
- _.memory.Free(nextCommandName);
- continue;
- }
- keysToRemove[keysToRemove.length] = nextCommandName;
- commandGroup[commandGroup.length] = nextCommand.GetGroupName();
- _.memory.Free(nextCommand);
- }
- iter.FreeSelf();
- for (i = 0; i < keysToRemove.length; i += 1)
- {
- registeredCommands.RemoveItem(keysToRemove[i]);
- _.memory.Free(keysToRemove[i]);
- }
-
- for (i = 0; i < commandGroup.length; i += 1) {
- RemoveClassFromGroup(commandClass, commandGroup[i]);
- }
- _.memory.FreeMany(commandGroup);
-}
-
-private final function RemoveClassFromGroup(
- class commandClass,
- BaseText commandGroup)
-{
- local int i;
- local ArrayList groupArray;
- local Command nextCommand;
-
- groupArray = groupedCommands.GetArrayList(commandGroup);
- if (groupArray == none) {
- return;
- }
- while (i < groupArray.GetLength())
- {
- nextCommand = Command(groupArray.GetItem(i));
- if (nextCommand != none && nextCommand.class == commandClass) {
- groupArray.RemoveIndex(i);
- }
- else {
- i += 1;
- }
- _.memory.Free(nextCommand);
- }
- if (groupArray.GetLength() == 0) {
- groupedCommands.RemoveItem(commandGroup);
- }
- _.memory.Free(groupArray);
-}
-
-/**
- * Returns command based on a given name.
- *
- * @param commandName Name of the registered `Command` to return.
- * Case-insensitive.
- * @return Command, registered with a given name `commandName`.
- * If no command with such name was registered - returns `none`.
- */
-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();
- return commandInstance;
-}
-
-/**
- * Returns array of names of all available commands.
- *
- * @return Array of names of all available (registered) commands.
- */
-public final function array GetCommandNames()
-{
- local array emptyResult;
-
- if (registeredCommands != none) {
- return registeredCommands.GetTextKeys();
- }
- return emptyResult;
-}
-
-/**
- * Returns array of names of all available commands belonging to the group
- * `groupName`.
- *
- * @return Array of names of all available (registered) commands, belonging to
- * the group `groupName`.
- */
-public final function array GetCommandNamesInGroup(BaseText groupName)
-{
- local int i;
- local ArrayList groupArray;
- local Command nextCommand;
- local array result;
-
- if (groupedCommands == none) return result;
- groupArray = groupedCommands.GetArrayList(groupName);
- if (groupArray == none) return result;
-
- for (i = 0; i < groupArray.GetLength(); i += 1)
- {
- nextCommand = Command(groupArray.GetItem(i));
- if (nextCommand != none) {
- result[result.length] = nextCommand.GetName();
- }
- _.memory.Free(nextCommand);
- }
- return result;
-}
-
-/**
- * Returns all available command groups' names.
- *
- * @return Array of all available command groups' names.
- */
-public final function array GetGroupsNames()
-{
- local array emptyResult;
-
- if (groupedCommands != none) {
- return groupedCommands.GetTextKeys();
- }
- return emptyResult;
-}
-
-/**
- * Handles user input: finds appropriate command and passes the rest of
- * the arguments to it for further processing.
- *
- * @param input Test that contains user's command input.
- * @param callerPlayer Player that caused this command call.
- */
-public final function HandleInput(BaseText input, EPlayer callerPlayer)
-{
- local Parser wrapper;
-
- if (input == none) {
- return;
- }
- wrapper = input.Parse();
- HandleInputWith(wrapper, callerPlayer);
- wrapper.FreeSelf();
-}
-
-/**
- * Handles user input: finds appropriate command and passes the rest of
- * the arguments to it for further processing.
- *
- * @param parser Parser filled with user input that is expected to
- * contain command's name and it's parameters.
- * @param callerPlayer Player that caused this command call.
- */
-public final function HandleInputWith(Parser parser, EPlayer callerPlayer)
-{
- local int i;
- local bool foundID;
- local string steamID;
- local PlayerController controller;
- local Command commandInstance;
- local Command.CallData callData;
- local CommandCallPair callPair;
-
- if (parser == none) return;
- if (callerPlayer == none) return;
- if (!parser.Ok()) return;
- controller = callerPlayer.GetController();
- if (controller == none) return;
-
- steamID = controller.GetPlayerIDHash();
- for (i = 0; i < allowedPlayers.length; i += 1)
- {
- if (allowedPlayers[i] == steamID)
- {
- foundID = true;
- break;
- }
- }
- if (!foundID) {
- return;
- }
- callPair = ParseCommandCallPairWith(parser);
- commandInstance = GetCommand(callPair.commandName);
- if ( commandInstance == none
- && callerPlayer != none && callerPlayer.IsExistent())
- {
- callerPlayer
- .BorrowConsole()
- .Flush()
- .Say(F("{$TextFailure Command not found!}"));
- }
- if (parser.Ok() && commandInstance != none)
- {
- callData = commandInstance
- .ParseInputWith(parser, callerPlayer, callPair.subCommandName);
- commandInstance.Execute(callData, callerPlayer);
- commandInstance.DeallocateCallData(callData);
- }
- _.memory.Free(callPair.commandName);
- _.memory.Free(callPair.subCommandName);
-}
-
-// Parses command's name into `CommandCallPair` - sub-command is filled in case
-// specified name is an alias with specified sub-command name.
-private final function CommandCallPair ParseCommandCallPairWith(Parser parser)
-{
- local Text resolvedValue;
- local MutableText userSpecifiedName;
- local CommandCallPair result;
- local Text.Character dotCharacter;
-
- if (parser == none) return result;
- if (!parser.Ok()) return result;
-
- parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true);
- resolvedValue = _.alias.ResolveCommand(userSpecifiedName);
- // This isn't an alias
- if (resolvedValue == none)
- {
- result.commandName = userSpecifiedName;
- return result;
- }
- // It is an alias - parse it
- dotCharacter = _.text.GetCharacter(".");
- resolvedValue.Parse()
- .MUntil(result.commandName, dotCharacter)
- .MatchS(".")
- .MUntil(result.subCommandName, dotCharacter)
- .FreeSelf();
- if (result.subCommandName.IsEmpty())
- {
- result.subCommandName.FreeSelf();
- result.subCommandName = none;
- }
- resolvedValue.FreeSelf();
- return result;
-}
-
-private function bool HandleCommands(
- EPlayer sender,
- MutableText message,
- bool teamMessage)
-{
- local Parser parser;
-
- // We are only interested in messages that start with `chatCommandPrefix`
- parser = _.text.Parse(message);
- if (!parser.Match(chatCommandPrefix).Ok())
- {
- parser.FreeSelf();
- return true;
- }
- // Pass input to command feature
- HandleInputWith(parser, sender);
- parser.FreeSelf();
- return false;
-}
-
-private function HandleMutate(string command, PlayerController sendingPlayer)
-{
- local Parser parser;
- local EPlayer sender;
-
- // A lot of other mutators use these commands
- if (command ~= "help") return;
- if (command ~= "version") return;
- if (command ~= "status") return;
- if (command ~= "credits") return;
-
- parser = _.text.ParseString(command);
- sender = _.players.FromController(sendingPlayer);
- HandleInputWith(parser, sender);
- sender.FreeSelf();
- parser.FreeSelf();
-}
-
-defaultproperties
-{
- configClass = class'Commands'
- errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.")
-}
\ No newline at end of file