@ -19,7 +19,9 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Commands_Feature extends Feature;
class Commands_Feature extends Feature
dependson(CommandAPI)
dependson(Commands);
//! This feature manages commands that automatically parse their arguments into standard Acedia
//! collections.
@ -38,83 +40,81 @@ class Commands_Feature extends Feature;
//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that
//! enforces connecting to the "mutate" input.
/// Pairs [`Voting`] class with a name its registered under in lower case for quick search.
struct NamedVoting {
var public class<Voting> processClass;
/// Must be guaranteed to not be `none` and lower case as an invariant
var public Text processName;
};
/// Auxiliary struct for passing name of the command to call plus, optionally, additional
/// Auxiliary struct for passing name of the command to call with pre-specified
/// sub-command name.
///
/// Normally sub-command name is parsed by the command itself, however command aliases can try to
/// enforce one.
/// Normally sub-command name is parsed by the command itself, however command
/// aliases can try to enforce one.
struct CommandCallPair {
var MutableText commandName;
/// I n case it is enforced by an alias
/// Not `none` in case it is enforced by an alias
var MutableText subCommandName;
};
/// Describes possible outcomes of starting a voting by its name
enum StartVotingResult {
/// Voting was successfully started
SVR_Success,
/// Voting wasn't started because another one was still in progress
SVR_AlreadyInProgress,
/// Voting wasn't started because voting with that name hasn't been registered
SVR_UnknownVoting
/// Auxiliary struct that stores all the information needed to load
/// a certain command
struct EntityLoadInfo {
/// Command class to load.
var public class<AcediaObject> entityClass;
/// Name to load that command class under.
var public Text name;
/// Groups that are authorized to use that command.
var public array<Text> authorizedGroups;
/// Groups that are authorized to use that command.
var public array<Text> groupsConfig;
};
/// Auxiliary struct for describing adding a particular command set to
/// a particular group of users.
struct CommandListGroupPair {
/// Name of the command set to add
var public Text commandListName;
/// Name of the group, for which to add this set
var public Text permissionGroup;
};
/// Auxiliary struct for describing a rule to rename a particular command for
/// compatibility reasons.
struct RenamingRulePair {
/// Command class to rename
var public class<AcediaObject> class;
/// Name to use for that class
var public Text newName;
};
/// Tools that provide functionality of managing registered commands and votings
var private CommandAPI.CommandFeatureTools tools;
/// Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters;
/// Registered commands, recorded as (<command_name>, <command_instance>) pairs.
/// Keys should be deallocated when their entry is removed.
var private HashTable registeredCommands;
/// [`HashTable`] of "<command_group_name>" <-> [`ArrayList`] of commands pairs to allow quick fetch
/// of commands belonging to a single group
var private HashTable groupedCommands;
/// [`Voting`]s that were already successfully loaded, ensuring that each has a unique name
var private array<NamedVoting> loadedVotings;
/// Currently running voting process.
/// This feature doesn't actively track when voting ends, so reference can be non-`none` even if
/// voting has already ended.
var private Voting currentVoting;
/// An array of [`Voting`] objects that have been successfully loaded and
/// each object has a unique name.
var private array<NamedVoting> registeredVotings;
/// When this flag is set to true, mutate input becomes available despite [`useMutateInput`] flag to
/// allow to unlock server in case of an error
/// 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;
var LoggerAPI.Definition errCommandDuplicate, errServerAPIUnavailable;
var LoggerAPI.Definition errVotingWithSameNameAlreadyRegistered, errYesNoVotingNamesReserved;
var public /*config*/ array<string> commandGroup;
var public /*config*/ array<Commands.CommandSetGroupPair> addCommandSet;
var public /*config*/ array<Commands.RenamingRulePair> renamingRule;
var public /*config*/ array<Commands.RenamingRulePair> votingRenamingRule;
// Converted version of `commandGroup`
var private array<Text> permissionGroupOrder;
/// Converted version of `addCommandSet`
var private array<CommandListGroupPair> usedCommandLists;
/// Converted version of `renamingRule` and `votingRenamingRule`
var private array<RenamingRulePair> commandRenamingRules;
var private array<RenamingRulePair> votingRenamingRules;
// Name, under which `ACommandHelp` is registered
var private Text helpCommandName;
var LoggerAPI.Definition errServerAPIUnavailable, warnDuplicateRenaming, warnNoCommandList;
var LoggerAPI.Definition infoCommandAdded, infoVotingAdded;
protected function OnEnabled() {
registeredCommands = _.collections.EmptyHashTable();
groupedCommands = _.collections.EmptyHashTable();
RegisterCommand(class'ACommandHelp');
RegisterCommand(class'ACommandNotify');
RegisterCommand(class'ACommandVote');
RegisterCommand(class'ACommandSideEffects');
if (_.environment.IsDebugging()) {
RegisterCommand(class'ACommandFakers');
}
RegisterVotingClass(class'Voting');
helpCommandName = P("help");
// Macro selector
commandDelimiters[0] = _.text.FromString("@");
// Key selector
@ -137,6 +137,16 @@ protected function OnEnabled() {
_.logger.Auto(errServerAPIUnavailable);
}
}
LoadConfigArrays();
// `SetPermissionGroupOrder()` must be called *after* loading configs
tools.commands = CommandsTool(_.memory.Allocate(class'CommandsTool'));
tools.votings = VotingsTool(_.memory.Allocate(class'VotingsTool'));
tools.commands.SetPermissionGroupOrder(permissionGroupOrder);
tools.votings.SetPermissionGroupOrder(permissionGroupOrder);
_.commands._reloadFeature();
// Uses `CommandAPI`, so must be done after `_reloadFeature()` call
LoadCommands();
LoadVotings();
}
protected function OnDisabled() {
@ -146,18 +156,27 @@ protected function OnDisabled() {
if (useMutateInput && __server() != none) {
__server().unreal.mutator.OnMutate(self).Disconnect();
}
useChatInput = false;
useMutateInput = false;
_.memory.Free3(registeredCommands, groupedCommand s, chatCommandPrefix);
registeredC ommands = none;
groupedCommand s = none;
_.memory.Free3(tools.commands, tools.voting s, chatCommandPrefix);
tools.c ommands = none;
tools.voting s = none;
chatCommandPrefix = none;
_.memory.FreeMany(commandDelimiters);
commandDelimiters.length = 0;
ReleaseNameVotingsArray(/*out*/ registeredVotings);
_.memory.FreeMany(permissionGroupOrder);
permissionGroupOrder.length = 0;
FreeUsedCommandSets();
FreeRenamingRules();
_.commands._reloadFeature();
}
protected function SwapConfig(FeatureConfig config)
{
protected function SwapConfig(FeatureConfig config) {
local Commands newConfig;
newConfig = Commands(config);
@ -168,10 +187,14 @@ protected function SwapConfig(FeatureConfig config)
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
useChatInput = newConfig.useChatInput;
useMutateInput = newConfig.useMutateInput;
commandGroup = newConfig.commandGroup;
addCommandSet = newConfig.addCommandList;
renamingRule = newConfig.renamingRule;
votingRenamingRule = newConfig.votingRenamingRule;
}
/// This method allows to forcefully enable `Command_Feature` along with "mutate" input in case
/// something goes wrong.
/// This method allows to forcefully enable `Command_Feature` along with
/// "mutate" input in case something goes wrong.
///
/// `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
@ -229,7 +252,8 @@ public final static function bool IsUsingMutateInput() {
return false;
}
/// Returns prefix that will indicate that chat message is intended to be a command. By default "!".
/// Returns prefix that will indicate that chat message is intended to be
/// a command. By default "!".
///
/// If `Commands_Feature` is disabled, always returns `none`.
public final static function Text GetChatPrefix() {
@ -242,309 +266,34 @@ public final static function Text GetChatPrefix() {
return none;
}
/// Returns `true` iff some voting is currently active.
public final function bool IsVotingRunning() {
if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
return (currentVoting != none);
}
/// Returns instance of the active voting.
///
/// `none` iff no voting is currently active.
public final function Voting GetCurrentVoting() {
if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
if (currentVoting != none) {
currentVoting.NewRef();
}
return currentVoting;
}
/// `true` if voting under the given name (case-insensitive) is already registered.
public final function bool IsVotingRegistered(BaseText processName) {
local int i;
for (i = 0; i < registeredVotings.length; i += 1) {
if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) {
return true;
}
}
return false;
}
/// Returns class of the [`Voting`] registered under given name.
public final function StartVotingResult StartVoting(BaseText processName) {
local int i;
local Text votingSettingsName;
local class<Voting> processClass;
if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
if (currentVoting != none) {
return SVR_AlreadyInProgress;
}
for (i = 0; i < registeredVotings.length; i += 1) {
if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) {
processClass = registeredVotings[i].processClass;
}
}
if (processClass == none) {
return SVR_UnknownVoting;
}
currentVoting = Voting(_.memory.Allocate(processClass));
currentVoting.Start(votingSettingsName);
return SVR_Success;
}
/// Registers a new voting class to be accessible through the [`Commands_Feature`].
///
/// When a voting class is registered, players can access it using the standard AcediaCore's "vote"
/// command.
/// However, note that registering a voting class is not mandatory for it to be usable.
/// In fact, if you want to prevent players from initiating a particular voting, you should avoid
/// registering it in this feature.
public final function RegisterVotingClass(class<Voting> newVotingClass) {
local int i;
local ACommandVote votingCommand;
local NamedVoting newRecord;
local Text votingName;
if (newVotingClass == none) return;
votingCommand = GetVotingCommand();
if (votingCommand == none) return;
// We can freely release this reference here, since another reference is guaranteed to be kept in registered command
_.memory.Free(votingCommand);
// But just to make sure
if (!votingCommand.IsAllocated()) return;
// First we check whether we already added this class
for (i = 0; i < registeredVotings.length; i += 1) {
if (registeredVotings[i].processClass == newVotingClass) {
return;
}
}
votingName = newVotingClass.static.GetPreferredName();
if (votingName.Compare(P("yes")) || votingName.Compare(P("no"))) {
_.logger.Auto(errYesNoVotingNamesReserved).ArgClass(newVotingClass).Arg(votingName);
return;
}
// Check for duplicates
for (i = 0; i < registeredVotings.length; i += 1) {
if (registeredVotings[i].processName.Compare(votingName)) {
_.logger
.Auto(errVotingWithSameNameAlreadyRegistered)
.ArgClass(newVotingClass)
.Arg(votingName)
.ArgClass(registeredVotings[i].processClass);
return;
}
}
newRecord.processClass = newVotingClass;
newRecord.processName = votingName;
registeredVotings[registeredVotings.length] = newRecord;
votingCommand.AddVotingInfo(votingName, newVotingClass);
}
/// Unregisters a voting class from the [`Commands_Feature`], preventing players from accessing it
/// through the standard AcediaCore "vote" command.
///
/// This method does not stop any existing voting processes associated with the unregistered class.
///
/// Use this method to remove a voting class that is no longer needed or to prevent players from
/// initiating a particular voting. Note that removing a voting class is a linear operation that may
/// take some time if many votings are currently registered. It is not expected to be a common
/// operation and should be used sparingly.
public final function RemoveVotingClass(class<Voting> newVotingClass) {
local int i;
local ACommandVote votingCommand;
if (newVotingClass == none) {
return;
}
for (i = 0; i < registeredVotings.length; i += 1) {
if (registeredVotings[i].processClass == newVotingClass) {
_.memory.Free(registeredVotings[i].processName);
registeredVotings.Remove(i, 1);
}
}
votingCommand = GetVotingCommand();
if (votingCommand == none) {
return;
}
// Simply rebuild the whole voting set from scratch
votingCommand.ResetVotingInfo();
for (i = 0; i < registeredVotings.length; i += 1) {
votingCommand.AddVotingInfo(
registeredVotings[i].processName,
registeredVotings[i].processClass);
}
_.memory.Free(votingCommand);
}
/// Registers given command class, making it available.
///
/// # Errors
///
/// Returns `true` if command was successfully registered and `false` otherwise`.
///
/// 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.
public final function bool RegisterCommand(class<Command> commandClass) {
local Text commandName, groupName;
local ArrayList groupArray;
local Command newCommandInstance, existingCommandInstance;
if (commandClass == none) return false;
if (registeredCommands == none) return false;
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 false;
}
// 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.Free4(groupArray, groupName, commandName, newCommandInstance);
return true;
}
/// Removes command of given class from the list of registered commands.
///
/// 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.
public final function RemoveCommand(class<Command> commandClass) {
local int i;
local CollectionIterator iter;
local Command nextCommand;
local Text nextCommandName;
local array<Text> commandGroup;
local array<Text> 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.Free2(nextCommand, 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);
}
/// Returns command based on a given name.
///
/// Name of the registered `Command` to return is case-insensitive.
/// Returns name, under which [`ACommandHelp`] is registered.
///
/// 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.
public final function array<Text> GetCommandNames() {
local array<Text> emptyResult;
if (registeredCommands != none) {
return registeredCommands.GetTextKeys();
}
return emptyResult;
}
/// Returns array of names of all available commands belonging to the group [`groupName`].
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) {
local int i;
local ArrayList groupArray;
local Command nextCommand;
local array<Text> 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.
public final function array<Text> GetGroupsNames() {
local array<Text> emptyResult;
/// If `Commands_Feature` is disabled, always returns `none`.
public final static function Text GetHelpCommandName() {
local Commands_Feature instance;
if (groupedCommands != none) {
return groupedCommands.GetTextKeys();
instance = Commands_Feature(GetEnabledInstance());
if (instance != none && instance.helpCommandName != none) {
return instance.helpCommandName.Copy();
}
return emptyResult ;
return none;
}
/// Executes command based on the input.
///
/// Takes [`commandLine`] as input with command's call, finds appropriate registered command
/// instance and executes it with parameters specified in the [`commandLine`].
/// Takes [`commandLine`] as input with command's call, finds appropriate
/// registered command instance and executes it with parameters specified in
/// the [`commandLine`].
///
/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive
/// appropriate result/error messages.
/// [`callerPlayer`] has to be specified and represents instigator of this
/// command that will receive appropriate result/error messages.
///
/// Returns `true` iff command was successfully executed.
///
/// # Errors
///
/// Doesn't log any errors, but can complain about errors in name or parameters to
/// the [`callerPlayer`]
/// Doesn't log any errors, but can complain about errors in name or parameters
/// to the [`callerPlayer`]
public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
local bool result;
local Parser wrapper;
@ -560,21 +309,23 @@ public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
/// Executes command based on the input.
///
/// Takes [`commandLine`] as input with command's call, finds appropriate registered command
/// instance and executes it with parameters specified in the [`commandLine`].
/// Takes [`commandLine`] as input with command's call, finds appropriate
/// registered command instance and executes it with parameters specified in
/// the [`commandLine`].
///
/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive
/// appropriate result/error messages.
/// [`callerPlayer`] has to be specified and represents instigator of this
/// command that will receive appropriate result/error messages.
///
/// Returns `true` iff command was successfully executed.
///
/// # Errors
///
/// Doesn't log any errors, but can complain about errors in name or parameters to
/// the [`callerPlayer`]
/// Doesn't log any errors, but can complain about errors in name or parameters
/// to t he [`callerPlayer`]
public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) {
local bool errorOccured;
local Command commandInstance;
local User identity;
local CommandAPI.CommandConfigInfo commandPair;
local Command.CallData callData;
local CommandCallPair callPair;
@ -582,34 +333,25 @@ public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer)
if (callerPlayer == none) return false;
if (!parser.Ok()) return false;
identity = callerPlayer.GetIdentity();
callPair = ParseCommandCallPairWith(parser);
commandInstance = GetCommand(callPair.commandName);
if (commandInstance == none && callerPlayer != none && callerPlayer.IsExistent()) {
commandPair = _.commands.ResolveCommandForUser(callPair.commandName, identity);
if (commandPair.instance == none || commandPair.usageForbidden) {
if (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);
errorOccured = commandInstance.Execute(callData, callerPlayer);
commandInstance.DeallocateCallData(callData);
}
_.memory.Free2(callPair.commandName, callPair.subCommandName);
return errorOccured;
}
private final function ACommandVote GetVotingCommand() {
local AcediaObject registeredAsVote;
if (registeredCommands != none) {
registeredAsVote = registeredCommands.GetItem(P("vote"));
if (registeredAsVote != none && registeredAsVote.class == class'ACommandVote') {
return ACommandVote(registeredAsVote);
}
_.memory.Free(registeredAsVote);
if (parser.Ok() && commandPair.instance != none && !commandPair.usageForbidden) {
callData =
commandPair.instance.ParseInputWith(parser, callerPlayer, callPair.subCommandName);
errorOccured = commandPair.instance.Execute(callData, callerPlayer, commandPair.config);
commandPair.instance.DeallocateCallData(callData);
}
return none;
_.memory.Free4(commandPair.instance, callPair.commandName, callPair.subCommandName, identity);
return errorOccured;
}
// Parses command's name into `CommandCallPair` - sub-command is filled in case
@ -677,44 +419,290 @@ private function HandleMutate(string command, PlayerController sendingPlayer) {
parser.FreeSelf();
}
private final function RemoveClassFromGroup(class<Command> commandClass, BaseText commandGroup ) {
private final function LoadConfigArrays( ) {
local int i;
local ArrayList groupArray;
local Command nextCommand;
local CommandListGroupPair nextCommandSetGroupPair;
groupArray = groupedCommands.GetArrayList(commandGroup);
if (groupArray == none) {
return;
for (i = 0; i < commandGroup.length; i += 1) {
permissionGroupOrder[i] = _.text.FromString(commandGroup[i]);
}
for (i = 0; i < addCommandSet.length; i += 1) {
nextCommandSetGroupPair.commandListName = _.text.FromString(addCommandSet[i].name);
nextCommandSetGroupPair.permissionGroup = _.text.FromString(addCommandSet[i].for);
usedCommandLists[i] = nextCommandSetGroupPair;
}
FreeRenamingRules();
commandRenamingRules = LoadRenamingRules(renamingRule);
votingRenamingRules = LoadRenamingRules(votingRenamingRule);
}
private final function array<RenamingRulePair> LoadRenamingRules(
array<Commands.RenamingRulePair> inputRules) {
local int i, j;
local RenamingRulePair nextRule;
local array<RenamingRulePair> result;
// Clear away duplicates
for (i = 0; i < inputRules.length; i += 1) {
j = i + 1;
while (j < inputRules.length) {
if (inputRules[i].rename == inputRules[j].rename) {
_.logger.Auto(warnDuplicateRenaming)
.ArgClass(inputRules[i].rename)
.Arg(_.text.FromString(inputRules[i].to))
.Arg(_.text.FromString(inputRules[j].to));
inputRules.Remove(j, 1);
} else {
j += 1;
}
}
}
// Translate rules
for (i = 0; i < inputRules.length; i += 1) {
nextRule.class = inputRules[i].rename;
nextRule.newName = _.text.FromString(inputRules[i].to);
if (nextRule.class == class'ACommandHelp') {
_.memory.Free(helpCommandName);
helpCommandName = nextRule.newName.Copy();
}
result[result.length] = nextRule;
}
return result;
}
private final function LoadCommands() {
local int i, j;
local Text nextName;
local array<EntityLoadInfo> commandClassesToLoad;
commandClassesToLoad = CollectAllCommandClasses();
// Load command names to use, according to preferred names and name rules
for (i = 0; i < commandClassesToLoad.length; i += 1) {
nextName = none;
for (j = 0; j < commandRenamingRules.length; j += 1) {
if (commandClassesToLoad[i].entityClass == commandRenamingRules[j].class) {
nextName = commandRenamingRules[j].newName.Copy();
break;
}
while (i < groupArray.GetLength()) {
nextCommand = Command(groupArray.GetItem(i));
if (nextCommand != none && nextCommand.class == commandClass) {
groupArray.RemoveIndex(i);
}
if (nextName == none) {
nextName = class<Command>(commandClassesToLoad[i].entityClass)
.static.GetPreferredName();
}
commandClassesToLoad[i].name = nextName;
}
// Actually load commands
for (i = 0; i < commandClassesToLoad.length; i += 1) {
_.commands.AddCommandAsync(
class<Command>(commandClassesToLoad[i].entityClass),
commandClassesToLoad[i].name);
for (j = 0; j < commandClassesToLoad[i].authorizedGroups.length; j += 1) {
_.commands.AuthorizeCommandUsageAsync(
commandClassesToLoad[i].name,
commandClassesToLoad[i].authorizedGroups[j],
commandClassesToLoad[i].groupsConfig[j]);
}
_.logger.Auto(infoCommandAdded)
.ArgClass(commandClassesToLoad[i].entityClass)
.Arg(/*take*/ commandClassesToLoad[i].name);
}
for (i = 0; i < commandClassesToLoad.length; i += 1) {
// `name` field was already released through `Arg()` logger function
_.memory.FreeMany(commandClassesToLoad[i].authorizedGroups);
_.memory.FreeMany(commandClassesToLoad[i].groupsConfig);
}
}
private final function LoadVotings() {
local int i, j;
local Text nextName;
local array<EntityLoadInfo> votingClassesToLoad;
votingClassesToLoad = CollectAllVotingClasses();
// Load voting names to use, according to preferred names and name rules
for (i = 0; i < votingClassesToLoad.length; i += 1) {
nextName = none;
for (j = 0; j < votingRenamingRules.length; j += 1) {
if (votingClassesToLoad[i].entityClass == votingRenamingRules[j].class) {
nextName = votingRenamingRules[j].newName.Copy();
break;
}
}
if (nextName == none) {
nextName = class<Voting>(votingClassesToLoad[i].entityClass)
.static.GetPreferredName();
}
votingClassesToLoad[i].name = nextName;
}
// Actually load votings
for (i = 0; i < votingClassesToLoad.length; i += 1) {
_.commands.AddVotingAsync(
class<Voting>(votingClassesToLoad[i].entityClass),
votingClassesToLoad[i].name);
for (j = 0; j < votingClassesToLoad[i].authorizedGroups.length; j += 1) {
_.commands.AuthorizeVotingUsageAsync(
votingClassesToLoad[i].name,
votingClassesToLoad[i].authorizedGroups[j],
votingClassesToLoad[i].groupsConfig[j]);
}
_.logger.Auto(infoVotingAdded)
.ArgClass(votingClassesToLoad[i].entityClass)
.Arg(/*take*/ votingClassesToLoad[i].name);
}
for (i = 0; i < votingClassesToLoad.length; i += 1) {
// `name` field was already released through `Arg()` logger function
_.memory.FreeMany(votingClassesToLoad[i].authorizedGroups);
_.memory.FreeMany(votingClassesToLoad[i].groupsConfig);
}
}
// Guaranteed to not return `none` items in the array
private final function array<EntityLoadInfo> CollectAllCommandClasses() {
local int i;
local bool debugging;
local CommandList nextList;
local array<EntityLoadInfo> result;
debugging = _.environment.IsDebugging();
class'CommandList'.static.Initialize();
for (i = 0; i < usedCommandLists.length; i += 1) {
nextList = CommandList(class'CommandList'.static
.GetConfigInstance(usedCommandLists[i].commandListName));
if (nextList != none) {
if (!debugging && nextList.debugOnly) {
continue;
}
MergeEntityClassArrays(
result,
/*take*/ nextList.GetCommandData(),
usedCommandLists[i].permissionGroup);
} else {
i += 1;
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy()) ;
}
_.memory.Free(nextCommand);
}
if (groupArray.GetLength() == 0) {
groupedCommands.RemoveItem(commandGroup);
return result;
}
// Guaranteed to not return `none` items in the array
private final function array<EntityLoadInfo> CollectAllVotingClasses() {
local int i;
local bool debugging;
local CommandList nextList;
local array<EntityLoadInfo> result;
debugging = _.environment.IsDebugging();
class'CommandList'.static.Initialize();
for (i = 0; i < usedCommandLists.length; i += 1) {
nextList = CommandList(class'CommandList'.static
.GetConfigInstance(usedCommandLists[i].commandListName));
if (nextList != none) {
if (!debugging && nextList.debugOnly) {
continue;
}
MergeEntityClassArrays(
result,
/*take*/ nextList.GetVotingData(),
usedCommandLists[i].permissionGroup);
} else {
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
}
}
return result;
}
_.memory.Free(groupArray);
// Adds `newCommands` into `infoArray`, adding `commandsGroup` to
// their array `authorizedGroups`
//
// Assumes that all arguments aren't `none`.
//
// Guaranteed to not add `none` commands from `newCommands`.
//
// Assumes that items from `infoArray` all have `name` field set to `none`,
// will also leave them as `none`.
private final function MergeEntityClassArrays(
out array<EntityLoadInfo> infoArray,
/*take*/ array<CommandList.EntityConfigPair> newCommands,
Text commandsGroup
) {
local int i, infoToEditIndex;
local EntityLoadInfo infoToEdit;
local array<Text> editedArray;
for (i = 0; i < newCommands.length; i += 1) {
if (newCommands[i].class == none) {
continue;
}
// Search for an existing record of class `newCommands[i]` in
// `infoArray`. If found, copy to `infoToEdit` and index into
// `infoToEditIndex`, else `infoToEditIndex` will hold the next unused
// index in `infoArray`.
infoToEditIndex = 0;
while (infoToEditIndex < infoArray.length) {
if (infoArray[infoToEditIndex].entityClass == newCommands[i].class) {
infoToEdit = infoArray[infoToEditIndex];
break;
}
infoToEditIndex += 1;
}
// Update data inside `infoToEdit`.
infoToEdit.entityClass = newCommands[i].class;
editedArray = infoToEdit.authorizedGroups;
editedArray[editedArray.length] = commandsGroup.Copy();
infoToEdit.authorizedGroups = editedArray;
editedArray = infoToEdit.groupsConfig;
editedArray[editedArray.length] = newCommands[i].config; // moving here
infoToEdit.groupsConfig = editedArray;
// Update `infoArray` with the modified record.
infoArray[infoToEditIndex] = infoToEdit;
// Forget about data `authorizedGroups` and `groupsConfig`:
//
// 1. Their references have already been moved into `infoArray` and
// don't need to be released;
// 2. If we don't find corresponding struct inside `infoArray` on
// the next iteration, we'll override `commandClass`,
// but not `authorizedGroups`/`groupsConfig`, so we'll just reset them
// to empty now.
// (`name` field is expected to be `none` during this method)
infoToEdit.authorizedGroups.length = 0;
infoToEdit.groupsConfig.length = 0;
}
}
private final function FreeUsedCommandSets() {
local int i;
for (i = 0; i < usedCommandLists.length; i += 1) {
_.memory.Free(usedCommandLists[i].commandListName);
_.memory.Free(usedCommandLists[i].permissionGroup);
}
usedCommandLists.length = 0;
}
private final function ReleaseNameVotingsArray(out array<NamedVoting> toRelease) {
private final function FreeRenamingRules( ) {
local int i;
for (i = 0; i < toRelease.length; i += 1) {
_.memory.Free(toRelease[i].processName);
toRelease[i].processName = none;
for (i = 0; i < commandRenamingRules.length; i += 1) {
_.memory.Free(commandRenamingRules[i].newName);
}
commandRenamingRules.length = 0;
for (i = 0; i < votingRenamingRules.length; i += 1) {
_.memory.Free(votingRenamingRules[i].newName);
}
toRelease.length = 0;
votingRenamingRules.length = 0;
}
public final function CommandAPI.CommandFeatureTools _borrowTools() {
return tools;
}
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.")
errServerAPIUnavailable = (l=LOG_Error,m="Server API is currently unavailable. Input methods through chat and mutate command will not be available.")
errVotingWithSameNameAlreadyRegistered = (l=LOG_Error,m="Attempting to register voting process with class `%1` under the name \"%2\" when voting process `%3` is already registered. This is likely caused by conflicting mods.")
errYesNoVotingNamesReserved = (l=LOG_Error,m="Attempting to register voting process with class `%1` under the reserved name \"%2\". This is an issue with the mod that provided the voting, please contact its author.")
warnDuplicateRenaming = (l=LOG_Warning,m="Class `%1` has duplicate renaming rules: \"%2\" and \"%3\". Latter will be discarded. It is recommended that you fix your `AcediaCommands` configuration file.")
warnNoCommandList = (l=LOG_Warning,m="Command list \"%1\" not found.")
infoCommandAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load command `%1` as \"%2\".")
infoVotingAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load voting `%1` as \"%2\".")
}