Anton Tarasenko
2 years ago
7 changed files with 951 additions and 2 deletions
@ -0,0 +1,53 @@
|
||||
/** |
||||
* Author: dkanus |
||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||
* License: GPL |
||||
* Copyright 2023 Anton Tarasenko |
||||
*------------------------------------------------------------------------------ |
||||
* This file is part of Acedia. |
||||
* |
||||
* Acedia is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* Acedia is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class ACommandNotify extends Command; |
||||
|
||||
protected function BuildData(CommandDataBuilder builder) { |
||||
builder.Name(P("notify")); |
||||
builder.Group(P("core")); |
||||
builder.Summary(P("Notifies players with provided message.")); |
||||
builder.ParamText(P("message")); |
||||
builder.OptionalParams(); |
||||
builder.ParamNumber(P("duration")); |
||||
builder.Describe(P("Notify to players message with distinct header and body.")); |
||||
builder.RequireTarget(); |
||||
|
||||
builder.Option(P("title")); |
||||
builder.Describe(P("Specify the optional title of the notification.")); |
||||
builder.ParamText(P("title")); |
||||
} |
||||
|
||||
protected function ExecutedFor(EPlayer target, CallData arguments, EPlayer instigator) { |
||||
local Text title, message, plainTitle, plainMessage; |
||||
|
||||
plainMessage = arguments.parameters.GetText(P("message")); |
||||
if (arguments.options.HasKey(P("title"))) { |
||||
plainTitle = arguments.options.GetTextBy(P("/title/title")); |
||||
} |
||||
title = _.text.FromFormatted(plainTitle); |
||||
message = _.text.FromFormatted(plainMessage); |
||||
target.Notify(title, message, arguments.parameters.GetFloat(P("duration"))); |
||||
_.memory.Free4(title, message, plainTitle, plainMessage); |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,627 @@
|
||||
/** |
||||
* 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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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<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; |
||||
|
||||
// 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<string> 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<Command> 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<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.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<Command> 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<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`. |
||||
* |
||||
* @return Array of names of all available (registered) 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. |
||||
* |
||||
* @return Array of all available command groups' names. |
||||
*/ |
||||
public final function array<Text> GetGroupsNames() |
||||
{ |
||||
local array<Text> 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.") |
||||
} |
@ -0,0 +1,219 @@
|
||||
/** |
||||
* Author: dkanus |
||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||
* License: GPL |
||||
* Copyright 2023 Anton Tarasenko |
||||
*------------------------------------------------------------------------------ |
||||
* This file is part of Acedia. |
||||
* |
||||
* Acedia is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* Acedia is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class PlayerNotificationQueue extends AcediaObject |
||||
config(AcediaSystem); |
||||
|
||||
/// Manages queue of notifications that should be displayed for a certain player. |
||||
|
||||
/// Describes a single notification: title (optional, can be `none`) + message body and timeout |
||||
struct Notification { |
||||
var Text title; |
||||
var Text body; |
||||
var float time; |
||||
}; |
||||
var private array<Notification> notificationQueue; |
||||
/// Reference to the `PlayerController` for the player that owns this queue |
||||
var private NativeActorRef playerControllerRef; |
||||
/// Timer until next notification can be displayed |
||||
/// |
||||
/// `none` if queue currently doesn't display any notifications |
||||
var private Timer nextNotificationTimer; |
||||
|
||||
/// Maximum time that a notification is allowed to be displayed on the player's screen |
||||
var private const config float maximumNotifyTime; |
||||
|
||||
var private const int CODEPOINT_NEWLINE; |
||||
|
||||
protected function Finalizer() { |
||||
local int i; |
||||
|
||||
for (i = 0; i < notificationQueue.length; i += 1) { |
||||
_.memory.Free(notificationQueue[i].title); |
||||
_.memory.Free(notificationQueue[i].body); |
||||
} |
||||
notificationQueue.length = 0; |
||||
_.memory.Free(nextNotificationTimer); |
||||
_.memory.Free(playerControllerRef); |
||||
nextNotificationTimer = none; |
||||
playerControllerRef = none; |
||||
} |
||||
|
||||
/// Set owner `PlayerController` for this queue |
||||
public final /*native*/ function SetupController(NativeActorRef newPlayerControllerRef) { |
||||
local PlayerController oldController, newController; |
||||
|
||||
if (playerControllerRef != none) { |
||||
oldController = PlayerController(playerControllerRef.Get()); |
||||
} |
||||
if (newPlayerControllerRef != none) { |
||||
newController = PlayerController(newPlayerControllerRef.Get()); |
||||
} |
||||
if (oldController != newController) { |
||||
InterruptScheduling(); |
||||
_.memory.Free(playerControllerRef); |
||||
if (newPlayerControllerRef != none) { |
||||
newPlayerControllerRef.NewRef(); |
||||
playerControllerRef = newPlayerControllerRef; |
||||
SetupNextNotification(none); |
||||
} else { |
||||
playerControllerRef = none; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Add new notification to the queue |
||||
public final function AddNotification(BaseText title, BaseText body, float time) { |
||||
local Notification newNotification; |
||||
|
||||
if (body == none) { |
||||
return; |
||||
} |
||||
if (title != none) { |
||||
newNotification.title = title.Copy(); |
||||
} |
||||
newNotification.body = body.Copy(); |
||||
notificationQueue[notificationQueue.length] = newNotification; |
||||
if (!IsUpdateScheduled()) { |
||||
SetupNextNotification(none); |
||||
} |
||||
} |
||||
|
||||
// Sets up [`SetupNextNotification()`] to be called after [`timeUntilNextUpdate`] |
||||
private function ScheduleUpdate(float timeUntilNextUpdate) { |
||||
if (nextNotificationTimer == none) { |
||||
nextNotificationTimer = __level().time.StartRealTimer(timeUntilNextUpdate, false); |
||||
nextNotificationTimer.OnElapsed(nextNotificationTimer).connect = SetupNextNotification; |
||||
} else { |
||||
nextNotificationTimer.SetInterval(timeUntilNextUpdate); |
||||
nextNotificationTimer.Start(); |
||||
} |
||||
} |
||||
|
||||
private function bool IsUpdateScheduled() { |
||||
return (nextNotificationTimer != none); |
||||
} |
||||
|
||||
// Prevents scheduled [`SetupNextNotification()`] call from going off |
||||
private function InterruptScheduling() { |
||||
_.memory.Free(nextNotificationTimer); |
||||
nextNotificationTimer = none; |
||||
} |
||||
|
||||
// Properly translates [`contents`] into a colored [`string`] before setting it up as |
||||
// a [`PlayerController`]'s progress message at the given line index. |
||||
private function SetupProgressLine( |
||||
int lineIndex, |
||||
BaseText contents, |
||||
PlayerController playerController |
||||
) { |
||||
local string contentsAsString; |
||||
local BaseText.Formatting startingFormatting; |
||||
|
||||
if (contents == none) return; |
||||
if (playerController == none) return; |
||||
|
||||
// Drop first colored tag, since we'll set first color through `playerController.progressColor` |
||||
contentsAsString = Mid(contents.ToColoredString(,, _.color.white), 4); |
||||
startingFormatting = contents.GetFormatting(0); |
||||
if (startingFormatting.isColored) { |
||||
playerController.SetProgressMessage(lineIndex, contentsAsString, startingFormatting.color); |
||||
} |
||||
else { |
||||
playerController.SetProgressMessage(lineIndex, contentsAsString, _.color.white); |
||||
} |
||||
} |
||||
|
||||
// Prints [`notification`] on given lines, respecting line breaks inside it as much as possible |
||||
// (creates up to [`maxLines`], replacing the rest of line breaks with whitespaces) |
||||
private function PrintNotifcationAt( |
||||
PlayerController playerController, |
||||
BaseText notification, |
||||
int startingLine, |
||||
int maxLines |
||||
) { |
||||
local int i, j; |
||||
local MutableText lastLine; |
||||
local array<BaseText> lines; |
||||
|
||||
if (notification == none) return; |
||||
if (startingLine < 0) return; |
||||
if (startingLine > 3) return; |
||||
|
||||
lines = notification.SplitByCharacter(_.text.CharacterFromCodePoint(CODEPOINT_NEWLINE),, true); |
||||
for (i = 0; i < lines.length; i += 1) { |
||||
if (i + 1 < maxLines) { |
||||
SetupProgressLine(i + startingLine, lines[i], playerController); |
||||
} else { |
||||
lastLine = lines[i].MutableCopy(); |
||||
for (j = i + 1; j < lines.length; j += 1) { |
||||
lastLine.Append(P(" ")); |
||||
lastLine.Append(lines[j]); |
||||
} |
||||
SetupProgressLine(startingLine + maxLines - 1, lastLine, playerController); |
||||
_.memory.Free(lastLine); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private function SetupNextNotification(Timer callerInstance) { |
||||
local int titleShift; |
||||
local MutableText upperCaseTitle; |
||||
local Notification nextNotification; |
||||
local PlayerController playerController; |
||||
|
||||
// Get appropriate [`PlayerController`] and next notification |
||||
playerController = PlayerController(playerControllerRef.Get()); |
||||
if (playerController == none) { |
||||
_.memory.Free(playerControllerRef); |
||||
playerControllerRef = none; |
||||
} |
||||
if (notificationQueue.length <= 0 || playerController == none) { |
||||
InterruptScheduling(); |
||||
return; |
||||
} |
||||
nextNotification = notificationQueue[0]; |
||||
notificationQueue.Remove(0, 1); |
||||
nextNotification.time = FMin(nextNotification.time, maximumNotifyTime); |
||||
if (nextNotification.time <= 0) { |
||||
nextNotification.time = 10.0; |
||||
} |
||||
|
||||
// And print |
||||
playerController.ClearProgressMessages(); |
||||
playerController.SetProgressTime(nextNotification.time); |
||||
if (nextNotification.title != none) { |
||||
upperCaseTitle = nextNotification.title.UpperMutableCopy(); |
||||
upperCaseTitle.ChangeDefaultColor(_.color.TextHeader); |
||||
PrintNotifcationAt(playerController, upperCaseTitle, 0, 1); |
||||
titleShift = 1; |
||||
_.memory.Free(upperCaseTitle); |
||||
} |
||||
PrintNotifcationAt(playerController, nextNotification.body, titleShift, 4 - titleShift); |
||||
ScheduleUpdate(nextNotification.time); |
||||
_.memory.Free2(nextNotification.title, nextNotification.body); |
||||
} |
||||
|
||||
defaultproperties { |
||||
CODEPOINT_NEWLINE = 10 |
||||
maximumNotifyTime = 20.0 |
||||
} |
Loading…
Reference in new issue