Browse Source

Change CommandAPI and feature to use tools

develop
Anton Tarasenko 1 year ago
parent
commit
a7f1a98548
  1. 1566
      sources/BaseAPI/API/Commands/CommandAPI.uc
  2. 59
      sources/BaseAPI/API/Commands/CommandRegistrationJob.uc
  3. 182
      sources/BaseAPI/API/Commands/Commands.uc
  4. 806
      sources/BaseAPI/API/Commands/Commands_Feature.uc

1566
sources/BaseAPI/API/Commands/CommandAPI.uc

File diff suppressed because it is too large Load Diff

59
sources/BaseAPI/API/Commands/CommandRegistrationJob.uc

@ -19,36 +19,63 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class CommandRegistrationJob extends SchedulerJob; class CommandRegistrationJob extends SchedulerJob
dependson(CommandAPI);
var private class<Command> nextCommand; var private CommandAPI.AsyncTask nextItem;
// Expecting 300 units of work, this gives us registering 20 commands per tick
const ADDING_COMMAND_COST = 15;
// Adding voting option is approximately the same as adding a command's
// single sub-command - we'll estimate it as 1/3rd of the full value
const ADDING_VOTING_COST = 5;
// Authorizing is relatively cheap, whether it's commands or voting
const AUTHORIZING_COST = 1;
protected function Constructor() { protected function Constructor() {
nextCommand = _.commands._popPending(); nextItem = _.commands._popPending();
} }
protected function Finalizer() { protected function Finalizer() {
nextCommand = none; _.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
nextItem.entityClass = none;
nextItem.entityName = none;
nextItem.userGroup = none;
nextItem.configName = none;
} }
public function bool IsCompleted() { public function bool IsCompleted() {
return (nextCommand == none); return (nextItem.entityName == none);
} }
public function DoWork(int allottedWorkUnits) { public function DoWork(int allottedWorkUnits) {
local int i, iterationsAmount; while (allottedWorkUnits > 0 && nextItem.entityName != none) {
if (nextItem.type == CAJT_AddCommand) {
// Expected 300 units per tick, to register 20 commands per tick use about 10 allottedWorkUnits -= ADDING_COMMAND_COST;
iterationsAmount = Max(allottedWorkUnits / 10, 1); _.commands.AddCommand(class<Command>(nextItem.entityClass), nextItem.entityName);
for (i = 0; i < iterationsAmount; i += 1) { _.memory.Free(nextItem.entityName);
_.commands.RegisterCommand(nextCommand); } else if (nextItem.type == CAJT_AddVoting) {
nextCommand = _.commands._popPending(); allottedWorkUnits -= ADDING_VOTING_COST;
if (nextCommand == none) { _.commands.AddVoting(class<Voting>(nextItem.entityClass), nextItem.entityName);
break; _.memory.Free(nextItem.entityName);
} else if (nextItem.type == CAJT_AuthorizeCommand) {
allottedWorkUnits -= AUTHORIZING_COST;
_.commands.AuthorizeCommandUsage(
nextItem.entityName,
nextItem.userGroup,
nextItem.configName);
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
} else /*if (nextItem.type == CAJT_AuthorizeVoting)*/ {
allottedWorkUnits -= AUTHORIZING_COST;
_.commands.AuthorizeVotingUsage(
nextItem.entityName,
nextItem.userGroup,
nextItem.configName);
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
} }
nextItem = _.commands._popPending();
} }
} }
defaultproperties defaultproperties {
{
} }

182
sources/BaseAPI/API/Commands/Commands.uc

@ -21,40 +21,210 @@
*/ */
class Commands extends FeatureConfig class Commands extends FeatureConfig
perobjectconfig perobjectconfig
config(AcediaSystem); config(AcediaCommands);
var public config bool useChatInput; /// Auxiliary struct for describing adding a particular command set to
/// a particular group of users.
struct CommandSetGroupPair {
/// Name of the command set to add
var public string name;
/// Name of the group, for which to add this set
var public string for;
};
/// Auxiliary struct for describing a rule to rename a particular command for
/// compatibility reasons.
struct RenamingRulePair {
/// Command class to rename
var public class<AcediaObject> rename;
/// Name to use for that class
var public string to;
};
/// Setting this to `true` enables players to input commands with "mutate"
/// console command.
/// Default is `true`.
var public config bool useMutateInput; var public config bool useMutateInput;
/// Setting this to `true` enables players to input commands right in the chat
/// by prepending them with [`chatCommandPrefix`].
/// Default is `true`.
var public config bool useChatInput;
/// Chat messages, prepended by this prefix will be treated as commands.
/// Default is "!". Empty values are also treated as "!".
var public config string chatCommandPrefix; var public config string chatCommandPrefix;
/// Allows to specify which user groups are used in determining command/votings
/// permission.
/// They must be specified in the order of importance: from the group with
/// highest level of permissions to the lowest. When determining player's
/// permission to use a certain command/voting, his group with the highest
/// available permissions will be used.
var public config array<string> commandGroup;
/// Add a specified `CommandList` to the specified user group
var public config array<CommandSetGroupPair> addCommandList;
/// Allows to specify a name for a certain command class
///
/// NOTE:By default command choses that name by itself and its not recommended
/// to override it. You should only use this setting in case there is naming
/// conflict between commands from different packages.
var public config array<RenamingRulePair> renamingRule;
/// Allows to specify a name for a certain voting class
///
/// NOTE:By default voting choses that name by itself and its not recommended
/// to override it. You should only use this setting in case there is naming
/// conflict between votings from different packages.
var public config array<RenamingRulePair> votingRenamingRule;
protected function HashTable ToData() { protected function HashTable ToData() {
local int i;
local HashTable data; local HashTable data;
local ArrayList innerList;
local HashTable innerPair;
data = __().collections.EmptyHashTable(); data = __().collections.EmptyHashTable();
data.SetBool(P("useChatInput"), useChatInput, true); data.SetBool(P("useChatInput"), useChatInput, true);
data.SetBool(P("useMutateInput"), useMutateInput, true); data.SetBool(P("useMutateInput"), useMutateInput, true);
data.SetString(P("chatCommandPrefix"), chatCommandPrefix); data.SetString(P("chatCommandPrefix"), chatCommandPrefix);
// Serialize `commandGroup`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < commandGroup.length; i += 1) {
innerList.AddString(commandGroup[i]);
}
data.SetItem(P("commandGroups"), innerList);
_.memory.Free(innerList);
// Serialize `addCommandSet`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < addCommandList.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("name"), addCommandList[i].name);
innerPair.SetString(P("for"), addCommandList[i].for);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("commandSets"), innerList);
_.memory.Free(innerList);
// Serialize `renamingRule`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < renamingRule.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("rename"), string(renamingRule[i].rename));
innerPair.SetString(P("to"), renamingRule[i].to);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("renamingRules"), innerList);
_.memory.Free(innerList);
// Serialize `votingRenamingRule`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < votingRenamingRule.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("rename"), string(votingRenamingRule[i].rename));
innerPair.SetString(P("to"), votingRenamingRule[i].to);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("votingRenamingRules"), innerList);
_.memory.Free(innerList);
return data; return data;
} }
protected function FromData(HashTable source) { protected function FromData(HashTable source) {
local int i;
local ArrayList innerList;
local HashTable innerPair;
local CommandSetGroupPair nextCommandSetGroupPair;
local RenamingRulePair nextRenamingRule;
local class<AcediaObject> nextClass;
if (source == none) { if (source == none) {
return; return;
} }
useChatInput = source.GetBool(P("useChatInput")); useChatInput = source.GetBool(P("useChatInput"));
useMutateInput = source.GetBool(P("useMutateInput")); useMutateInput = source.GetBool(P("useMutateInput"));
chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!");
// De-serialize `commandGroup`
commandGroup.length = 0;
innerList = source.GetArrayList(P("commandGroups"));
if (innerList != none) {
for (i = 0; i < commandGroup.length; i += 1) {
commandGroup[i] = innerList.GetString(i);
}
_.memory.Free(innerList);
}
// De-serialize `addCommandSet`
addCommandList.length = 0;
innerList = source.GetArrayList(P("commandSets"));
if (innerList != none) {
for (i = 0; i < addCommandList.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextCommandSetGroupPair.name = innerPair.GetString(P("name"));
nextCommandSetGroupPair.for = innerPair.GetString(P("for"));
addCommandList[addCommandList.length] = nextCommandSetGroupPair;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
// De-serialize `renamingRule`
renamingRule.length = 0;
innerList = source.GetArrayList(P("renamingRules"));
if (innerList != none) {
for (i = 0; i < renamingRule.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextClass =
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
nextRenamingRule.rename = nextClass;
nextRenamingRule.to = innerPair.GetString(P("to"));
renamingRule[renamingRule.length] = nextRenamingRule;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
// De-serialize `votingRenamingRule`
votingRenamingRule.length = 0;
innerList = source.GetArrayList(P("votingRenamingRules"));
if (innerList != none) {
for (i = 0; i < votingRenamingRule.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextClass =
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
nextRenamingRule.rename = nextClass;
nextRenamingRule.to = innerPair.GetString(P("to"));
votingRenamingRule[votingRenamingRule.length] = nextRenamingRule;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
} }
protected function DefaultIt() { protected function DefaultIt() {
local CommandSetGroupPair defaultPair;
useChatInput = true; useChatInput = true;
useMutateInput = true; useMutateInput = true;
chatCommandPrefix = "!"; chatCommandPrefix = "!";
commandGroup[0] = "admin";
commandGroup[1] = "moderator";
commandGroup[2] = "trusted";
addCommandList.length = 0;
defaultPair.name = "default";
defaultPair.for = "all";
addCommandList[0] = defaultPair;
renamingRule.length = 0;
} }
defaultproperties { defaultproperties {
configName = "AcediaSystem" configName = "AcediaCommands"
useChatInput = true
useMutateInput = true
chatCommandPrefix = "!"
} }

806
sources/BaseAPI/API/Commands/Commands_Feature.uc

@ -19,7 +19,9 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * 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 //! This feature manages commands that automatically parse their arguments into standard Acedia
//! collections. //! collections.
@ -38,83 +40,81 @@ class Commands_Feature extends Feature;
//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that //! Emergency enabling this feature sets `emergencyEnabledMutate` flag that
//! enforces connecting to the "mutate" input. //! enforces connecting to the "mutate" input.
/// Pairs [`Voting`] class with a name its registered under in lower case for quick search. /// Auxiliary struct for passing name of the command to call with pre-specified
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
/// sub-command name. /// sub-command name.
/// ///
/// Normally sub-command name is parsed by the command itself, however command aliases can try to /// Normally sub-command name is parsed by the command itself, however command
/// enforce one. /// aliases can try to enforce one.
struct CommandCallPair { struct CommandCallPair {
var MutableText commandName; var MutableText commandName;
/// In case it is enforced by an alias /// Not `none` in case it is enforced by an alias
var MutableText subCommandName; var MutableText subCommandName;
}; };
/// Describes possible outcomes of starting a voting by its name /// Auxiliary struct that stores all the information needed to load
enum StartVotingResult { /// a certain command
/// Voting was successfully started struct EntityLoadInfo {
SVR_Success, /// Command class to load.
/// Voting wasn't started because another one was still in progress var public class<AcediaObject> entityClass;
SVR_AlreadyInProgress, /// Name to load that command class under.
/// Voting wasn't started because voting with that name hasn't been registered var public Text name;
SVR_UnknownVoting /// 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 /// Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters; var private array<Text> commandDelimiters;
/// Registered commands, recorded as (<command_name>, <command_instance>) pairs.
/// Keys should be deallocated when their entry is removed. /// When this flag is set to true, mutate input becomes available despite
var private HashTable registeredCommands; /// [`useMutateInput`] flag to allow to unlock server in case of an error
/// [`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
var private bool emergencyEnabledMutate; 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; 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; 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 private /*config*/ Text chatCommandPrefix;
var public /*config*/ array<string> commandGroup;
var LoggerAPI.Definition errCommandDuplicate, errServerAPIUnavailable; var public /*config*/ array<Commands.CommandSetGroupPair> addCommandSet;
var LoggerAPI.Definition errVotingWithSameNameAlreadyRegistered, errYesNoVotingNamesReserved; 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() { protected function OnEnabled() {
registeredCommands = _.collections.EmptyHashTable(); helpCommandName = P("help");
groupedCommands = _.collections.EmptyHashTable();
RegisterCommand(class'ACommandHelp');
RegisterCommand(class'ACommandNotify');
RegisterCommand(class'ACommandVote');
RegisterCommand(class'ACommandSideEffects');
if (_.environment.IsDebugging()) {
RegisterCommand(class'ACommandFakers');
}
RegisterVotingClass(class'Voting');
// Macro selector // Macro selector
commandDelimiters[0] = _.text.FromString("@"); commandDelimiters[0] = _.text.FromString("@");
// Key selector // Key selector
@ -137,6 +137,16 @@ protected function OnEnabled() {
_.logger.Auto(errServerAPIUnavailable); _.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() { protected function OnDisabled() {
@ -146,18 +156,27 @@ protected function OnDisabled() {
if (useMutateInput && __server() != none) { if (useMutateInput && __server() != none) {
__server().unreal.mutator.OnMutate(self).Disconnect(); __server().unreal.mutator.OnMutate(self).Disconnect();
} }
useChatInput = false; useChatInput = false;
useMutateInput = false; useMutateInput = false;
_.memory.Free3(registeredCommands, groupedCommands, chatCommandPrefix); _.memory.Free3(tools.commands, tools.votings, chatCommandPrefix);
registeredCommands = none; tools.commands = none;
groupedCommands = none; tools.votings = none;
chatCommandPrefix = none; chatCommandPrefix = none;
_.memory.FreeMany(commandDelimiters);
commandDelimiters.length = 0; 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; local Commands newConfig;
newConfig = Commands(config); newConfig = Commands(config);
@ -168,10 +187,14 @@ protected function SwapConfig(FeatureConfig config)
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
useChatInput = newConfig.useChatInput; useChatInput = newConfig.useChatInput;
useMutateInput = newConfig.useMutateInput; 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 /// This method allows to forcefully enable `Command_Feature` along with
/// something goes wrong. /// "mutate" input in case something goes wrong.
/// ///
/// `Command_Feature` is a critical command to have running on your server and, /// `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 /// 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; 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`. /// If `Commands_Feature` is disabled, always returns `none`.
public final static function Text GetChatPrefix() { public final static function Text GetChatPrefix() {
@ -242,309 +266,34 @@ public final static function Text GetChatPrefix() {
return none; return none;
} }
/// Returns `true` iff some voting is currently active. /// Returns name, under which [`ACommandHelp`] is registered.
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.
/// ///
/// If no command with such name was registered - returns `none`. /// If `Commands_Feature` is disabled, always returns `none`.
public final function Command GetCommand(BaseText commandName) { public final static function Text GetHelpCommandName() {
local Text commandNameLowerCase; local Commands_Feature instance;
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 (groupedCommands != none) { instance = Commands_Feature(GetEnabledInstance());
return groupedCommands.GetTextKeys(); if (instance != none && instance.helpCommandName != none) {
return instance.helpCommandName.Copy();
} }
return emptyResult; return none;
} }
/// Executes command based on the input. /// Executes command based on the input.
/// ///
/// Takes [`commandLine`] as input with command's call, finds appropriate registered command /// Takes [`commandLine`] as input with command's call, finds appropriate
/// instance and executes it with parameters specified in the [`commandLine`]. /// 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 /// [`callerPlayer`] has to be specified and represents instigator of this
/// appropriate result/error messages. /// command that will receive appropriate result/error messages.
/// ///
/// Returns `true` iff command was successfully executed. /// Returns `true` iff command was successfully executed.
/// ///
/// # Errors /// # Errors
/// ///
/// Doesn't log any errors, but can complain about errors in name or parameters to /// Doesn't log any errors, but can complain about errors in name or parameters
/// the [`callerPlayer`] /// to the [`callerPlayer`]
public final function bool HandleInput(BaseText input, EPlayer callerPlayer) { public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
local bool result; local bool result;
local Parser wrapper; local Parser wrapper;
@ -560,21 +309,23 @@ public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
/// Executes command based on the input. /// Executes command based on the input.
/// ///
/// Takes [`commandLine`] as input with command's call, finds appropriate registered command /// Takes [`commandLine`] as input with command's call, finds appropriate
/// instance and executes it with parameters specified in the [`commandLine`]. /// 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 /// [`callerPlayer`] has to be specified and represents instigator of this
/// appropriate result/error messages. /// command that will receive appropriate result/error messages.
/// ///
/// Returns `true` iff command was successfully executed. /// Returns `true` iff command was successfully executed.
/// ///
/// # Errors /// # Errors
/// ///
/// Doesn't log any errors, but can complain about errors in name or parameters to /// Doesn't log any errors, but can complain about errors in name or parameters
/// the [`callerPlayer`] /// to the [`callerPlayer`]
public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) { public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) {
local bool errorOccured; local bool errorOccured;
local Command commandInstance; local User identity;
local CommandAPI.CommandConfigInfo commandPair;
local Command.CallData callData; local Command.CallData callData;
local CommandCallPair callPair; local CommandCallPair callPair;
@ -582,34 +333,25 @@ public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer)
if (callerPlayer == none) return false; if (callerPlayer == none) return false;
if (!parser.Ok()) return false; if (!parser.Ok()) return false;
identity = callerPlayer.GetIdentity();
callPair = ParseCommandCallPairWith(parser); callPair = ParseCommandCallPairWith(parser);
commandInstance = GetCommand(callPair.commandName); commandPair = _.commands.ResolveCommandForUser(callPair.commandName, identity);
if (commandInstance == none && callerPlayer != none && callerPlayer.IsExistent()) { if (commandPair.instance == none || commandPair.usageForbidden) {
if (callerPlayer != none && callerPlayer.IsExistent()) {
callerPlayer callerPlayer
.BorrowConsole() .BorrowConsole()
.Flush() .Flush()
.Say(F("{$TextFailure Command not found!}")); .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 // 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(); parser.FreeSelf();
} }
private final function RemoveClassFromGroup(class<Command> commandClass, BaseText commandGroup) { private final function LoadConfigArrays() {
local int i; local int i;
local ArrayList groupArray; local CommandListGroupPair nextCommandSetGroupPair;
local Command nextCommand;
groupArray = groupedCommands.GetArrayList(commandGroup); for (i = 0; i < commandGroup.length; i += 1) {
if (groupArray == none) { permissionGroupOrder[i] = _.text.FromString(commandGroup[i]);
return; }
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 (nextName == none) {
if (nextCommand != none && nextCommand.class == commandClass) { nextName = class<Command>(commandClassesToLoad[i].entityClass)
groupArray.RemoveIndex(i); .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 { } else {
i += 1; _.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
} }
_.memory.Free(nextCommand);
} }
if (groupArray.GetLength() == 0) { return result;
groupedCommands.RemoveItem(commandGroup); }
// 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; local int i;
for (i = 0; i < toRelease.length; i += 1) { for (i = 0; i < commandRenamingRules.length; i += 1) {
_.memory.Free(toRelease[i].processName); _.memory.Free(commandRenamingRules[i].newName);
toRelease[i].processName = none; }
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 { defaultproperties {
configClass = class'Commands' 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.") 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.") 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.")
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.") 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\".")
} }
Loading…
Cancel
Save