Browse Source

Refactor `APlayer`/`ATrader` into `EInterface`s

`APlayer` and `ATrader` represented player and trader (`ShopVolume`)
with a single object instance. Such design, if used for all actors,
could have led to mutitute of problems rooted in need to find that
single object for any given native actor: we'd need to store
object-actor pairs separately and look through pairs lists, which is
hardly a sane design.

Now Acedia switches to a different design, where a single in-game entity
(i.e. actor) can have several interfaces referring to it. All equaly
valid. Refactoring `APlayer` and `ATrader` into `EPlayer` and `ETrader`
is a first step in that direction.
pull/8/head
Anton Tarasenko 3 years ago
parent
commit
7dc5149281
  1. 18
      sources/Color/ColorAPI.uc
  2. 33
      sources/Commands/BuiltInCommands/ACommandHelp.uc
  3. 4
      sources/Commands/BuiltInCommands/ACommandTest.uc
  4. 250
      sources/Commands/Command.uc
  5. 393
      sources/Commands/CommandCall.uc
  6. 65
      sources/Commands/CommandParser.uc
  7. 23
      sources/Commands/Commands_Feature.uc
  8. 43
      sources/Commands/PlayersParser.uc
  9. 215
      sources/Commands/Tests/TEST_Command.uc
  10. 2
      sources/Console/ConsoleAPI.uc
  11. 78
      sources/Console/ConsoleWriter.uc
  12. 0
      sources/Gameplay/BaseClasses/Frontend/BaseFrontend.uc
  13. 104
      sources/Gameplay/BaseClasses/Frontend/EInterface.uc
  14. 0
      sources/Gameplay/BaseClasses/KillingFloor/Frontend/KFFrontend.uc
  15. 12
      sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ATradingComponent.uc
  16. 56
      sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ETrader.uc
  17. 4
      sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Signal.uc
  18. 4
      sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Slot.uc
  19. 17
      sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc
  20. 187
      sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc
  21. 93
      sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc
  22. 1
      sources/Manifest.uc
  23. 196
      sources/Players/EPlayer.uc
  24. 38
      sources/Players/Events/PlayerAPI_OnLostPlayer_Signal.uc
  25. 22
      sources/Players/Events/PlayerAPI_OnLostPlayer_Slot.uc
  26. 38
      sources/Players/Events/PlayerAPI_OnNewPlayer_Signal.uc
  27. 40
      sources/Players/Events/PlayerAPI_OnNewPlayer_Slot.uc
  28. 4
      sources/Players/Inventory/EInventory.uc
  29. 183
      sources/Players/PlayerService.uc
  30. 148
      sources/Players/PlayersAPI.uc
  31. 2
      sources/Unreal/Connections/ConnectionService.uc
  32. 3
      sources/Users/User.uc

18
sources/Color/ColorAPI.uc

@ -1472,15 +1472,15 @@ defaultproperties
browndarken4=(R=62,G=39,B=35,A=255)
bluegrey=(R=96,G=125,B=139,A=255)
vuebluegrey=(R=96,G=125,B=139,A=255)
bluegreylighten5#rgb(R=236,G=239,B=241,A=255)
bluegreylighten4#rgb(R=207,G=216,B=220,A=255)
bluegreylighten3#rgb(R=176,G=190,B=197,A=255)
bluegreylighten2#rgb(R=144,G=164,B=174,A=255)
bluegreylighten1#rgb(R=120,G=144,B=156,A=255)
bluegreydarken1#rgb(R=84,G=110,B=122,A=255)
bluegreydarken2#rgb(R=69,G=90,B=100,A=255)
bluegreydarken3#rgb(R=55,G=71,B=79,A=255)
bluegreydarken4#rgb(R=38,G=50,B=56,A=255)
bluegreylighten5=(R=236,G=239,B=241,A=255)
bluegreylighten4=(R=207,G=216,B=220,A=255)
bluegreylighten3=(R=176,G=190,B=197,A=255)
bluegreylighten2=(R=144,G=164,B=174,A=255)
bluegreylighten1=(R=120,G=144,B=156,A=255)
bluegreydarken1=(R=84,G=110,B=122,A=255)
bluegreydarken2=(R=69,G=90,B=100,A=255)
bluegreydarken3=(R=55,G=71,B=79,A=255)
bluegreydarken4=(R=38,G=50,B=56,A=255)
grey=(R=158,G=158,B=158,A=255)
vuegrey=(R=158,G=158,B=158,A=255)
greylighten5=(R=250,G=250,B=250,A=255)

33
sources/Commands/BuiltInCommands/ACommandHelp.uc

@ -1,6 +1,6 @@
/**
* Command for displaying help information about registered Acedia's commands.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -40,18 +40,14 @@ protected function BuildData(CommandDataBuilder builder)
.Describe(P("Display list of all available commands."));
}
protected function Executed(CommandCall callInfo)
protected function Executed(Command.CallData callData, EPlayer callerPlayer)
{
local AssociativeArray parameters;
local AssociativeArray parameters, options;;
local DynamicArray commandsToDisplay;
local APlayer callerPlayer;
callerPlayer = callInfo.GetCallerPlayer();
if (callerPlayer == none) {
return;
}
parameters = callData.parameters;
options = callData.options;
// Print command list if "--list" option was specified
if (callInfo.GetOptions().HasKey(P("list"))) {
if (options.HasKey(P("list"))) {
DisplayCommandList(callerPlayer);
}
// Help pages.
@ -59,16 +55,17 @@ protected function Executed(CommandCall callInfo)
// 1. Any commands are specified as parameters;
// 2. No commands or "--list" option was specified, then we want to
// print a help page for this command.
if ( !callInfo.GetOptions().HasKey(P("list"))
|| callInfo.GetParameters().HasKey(P("commands")))
if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
{
parameters = callInfo.GetParameters();
commandsToDisplay = DynamicArray(parameters.GetItem(P("commands")));
DisplayCommandHelpPages(callerPlayer, commandsToDisplay);
}
_.memory.Free(callerPlayer);
_.memory.Free(parameters);
_.memory.Free(options);
}
private final function DisplayCommandList(APlayer player)
private final function DisplayCommandList(EPlayer player)
{
local int i;
local ConsoleWriter console;
@ -81,7 +78,7 @@ private final function DisplayCommandList(APlayer player)
Commands_Feature(class'Commands_Feature'.static.GetInstance());
if (commandsFeature == none) return;
console = player.Console();
console = player.BorrowConsole();
commandNames = commandsFeature.GetCommandNames();
for (i = 0; i < commandNames.length; i += 1)
{
@ -99,7 +96,7 @@ private final function DisplayCommandList(APlayer player)
}
private final function DisplayCommandHelpPages(
APlayer player,
EPlayer player,
DynamicArray commandList)
{
local int i;
@ -113,7 +110,7 @@ private final function DisplayCommandHelpPages(
// If arguments were empty - at least display our own help page
if (commandList == none)
{
PrintHelpPage(player.Console(), GetData());
PrintHelpPage(player.BorrowConsole(), GetData());
return;
}
// Otherwise - print help for specified commands
@ -121,7 +118,7 @@ private final function DisplayCommandHelpPages(
{
nextCommand = commandsFeature.GetCommand(Text(commandList.GetItem(i)));
if (nextCommand == none) continue;
PrintHelpPage(player.Console(), nextCommand.GetData());
PrintHelpPage(player.BorrowConsole(), nextCommand.GetData());
}
}

4
sources/Commands/BuiltInCommands/ACommandTest.uc

@ -26,7 +26,7 @@ protected function BuildData(CommandDataBuilder builder)
.ParamText(P("option"));
}
protected function Executed(CommandCall result)
protected function Executed(Command.CallData result, EPlayer callerPlayer)
{
local Parser parser;
local AssociativeArray root;
@ -64,7 +64,7 @@ protected function Executed(CommandCall result)
}*/
parser = _.text.ParseString("{\"innerObject\":{\"my_bool\":true,\"array\":[\"Engine.Actor\",false,null,{\"something \\\"here\\\"\":\"yes\",\"maybe\":0.003},56.6],\"one more\":{\"nope\":324532,\"whatever\":false,\"o rly?\":\"ya rly\"},\"my_int\":-9823452},\"some_var\":-7.32,\"another_var\":\"aye!\"}");
root = _.json.ParseObjectWith(parser);
result.GetCallerPlayer().Console().WriteLine(_.json.PrettyPrint(root));
callerPlayer.BorrowConsole().WriteLine(_.json.PrettyPrint(root));
}
defaultproperties

250
sources/Commands/Command.uc

@ -1,9 +1,12 @@
/**
* This class is meant to represent a command type: to create new command
* one should extend it, then simply define required sub-commands/options and
* parameters in `BuildData()` and use `Execute()` / `ExecuteFor()` to perform
* necessary actions when command is executed by a player.
* Copyright 2021 Anton Tarasenko
* parameters in `BuildData()` and overload `Execute()` / `ExecuteFor()`
* to perform required actions when command is executed by a player.
* `Execute()` is called first, whenever command is executed and
* `ExecuteFor()` is called only for targeted commands, once for each
* targeted player.
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -24,7 +27,8 @@ class Command extends AcediaObject
dependson(Text);
/**
* Possible errors that can arise when producing `CommandCall` from user input
* Possible errors that can arise when parsing command parameters from
* user input
*/
enum ErrorType
{
@ -56,6 +60,25 @@ enum ErrorType
CET_EmptyTargetList
};
/**
* Structure that contains all the information about how `Command` was called.
*/
struct CallData
{
// Targeted players (if applicable)
var public array<EPlayer> targetPlayers;
// Specified sub-command and parameters/options
var public Text subCommandName;
// Provided parameters and specified options
var public AssociativeArray parameters;
var public AssociativeArray options;
// Errors that occurred during command call processing are described by
// error type and optional error textual name of the object
// (parameter, option, etc.) that caused it.
var public ErrorType parsingError;
var public Text errorCause;
};
/**
* Possible types of parameters.
*/
@ -216,25 +239,32 @@ private final function CleanParameters(array<Parameter> parameters)
protected function BuildData(CommandDataBuilder builder){}
/**
* Overload this method to perform what is needed when your command is called.
* Overload this method to perform required actions when
* your command is called.
*
* @param callInfo Object filled with parameters that your command has
* been called with. Guaranteed to not be in error state.
* @param callData `struct` filled with parameters that your command
* has been called with. Guaranteed to not be in error state.
* @param callerPlayer Player that instigated this execution.
*/
protected function Executed(CommandCall callInfo){}
protected function Executed(CallData callData, EPlayer callerPlayer){}
/**
* Overload this method to perform what is needed when your command is called
* Overload this method to perform required actions when your command is called
* with a given player as a target. If several players have been specified -
* this method will be called once for each.
*
* If your command does not require a target - this method will not be called.
*
* @param targetPlayer Player that this command must perform an action on.
* @param callInfo Object filled with parameters that your command has
* been called with. Guaranteed to not be in error state.
* @param callData `struct` filled with parameters that your command
* has been called with. Guaranteed to not be in error state and contain
* all the required data.
* @param callerPlayer Player that instigated this call.
*/
protected function ExecutedFor(APlayer targetPlayer, CommandCall callInfo){}
protected function ExecutedFor(
EPlayer targetPlayer,
CallData callData,
EPlayer callerPlayer){}
/**
* Returns an instance of command (of particular class) that is stored
@ -249,112 +279,211 @@ public final static function Command GetInstance()
}
/**
* Forces command to process (parse and, if successful, execute itself)
* player's input.
* Forces command to process (parse) player's input, producing a structure
* with parsed data in Acedia's format instead.
*
* @param parser Parser that contains player's input.
* @param callerPlayer Player that initiated this command's call.
* @return `CommandCall` object that described parsed command call.
* Guaranteed to be not `none`.
* @see `Execute()` for actually performing command's actions.
*
* @param parser Parser that contains command input.
* @param callerPlayer Player that initiated this command's call,
* necessary for parsing player list (since it can point at
* the caller player).
* @return `CallData` structure that contains all the information about
* parameters specified in `parser`'s contents.
* Returned structure contains objects that must be deallocated,
* which can easily be done by the auxiliary `DeallocateCallData()` method.
*/
public final function CommandCall ProcessInput(
public final function CallData ParseInputWith(
Parser parser,
APlayer callerPlayer)
EPlayer callerPlayer)
{
local int i;
local array<APlayer> targetPlayers;
local array<EPlayer> targetPlayers;
local CommandParser commandParser;
local CommandCall callInfo;
if (parser == none || !parser.Ok()) {
return MakeAndReportError(callerPlayer, CET_BadParser);
local CallData callData;
if (parser == none || !parser.Ok())
{
callData.parsingError = CET_BadParser;
return callData;
}
// Parse targets and handle errors that can arise here
if (commandData.requiresTarget)
{
targetPlayers = ParseTargets(parser, callerPlayer);
if (!parser.Ok()) {
return MakeAndReportError(callerPlayer, CET_IncorrectTargetList);
if (!parser.Ok())
{
callData.parsingError = CET_IncorrectTargetList;
return callData;
}
if (targetPlayers.length <= 0) {
return MakeAndReportError(callerPlayer, CET_EmptyTargetList);
if (targetPlayers.length <= 0)
{
callData.parsingError = CET_EmptyTargetList;
return callData;
}
}
// Parse parameters themselves
commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
callInfo = commandParser.ParseWith(parser, commandData)
.SetCallerPlayer(callerPlayer)
.SetTargetPlayers(targetPlayers);
callData = commandParser.ParseWith(parser, commandData);
callData.targetPlayers = targetPlayers;
commandParser.FreeSelf();
return callData;
}
/**
* Executes caller `Command` with data provided by `callData` if it is in
* a correct state and reports error to `callerPlayer` if
* `callData` is invalid.
*
* @param callData Data about parameters, options, etc. with which
* caller `Command` is to be executed.
* @param callerPlayer Player that should be considered responsible for
* executing this `Command`.
* @return `true` if command was successfully executed and `false` otherwise.
* Execution is considered successful if `Execute()` call was made,
* regardless of whether `Command` can actually perform required action.
* For example, giving a weapon to a player can fail because he does not
* have enough space in his inventory, but it will still be considered
* a successful execution as far as return value is concerned.
*/
public final function bool Execute(CallData callData, EPlayer callerPlayer)
{
local int i;
local array<EPlayer> targetPlayers;
if (callerPlayer == none) return false;
if (!callerPlayer.IsExistent()) return false;
// Report or execute
if (!callInfo.IsSuccessful())
if (callData.parsingError != CET_None)
{
ReportError(callerPlayer, callInfo);
return callInfo;
ReportError(callData, callerPlayer);
return false;
}
Executed(callInfo);
Executed(callData, callerPlayer);
if (commandData.requiresTarget)
{
targetPlayers = callData.targetPlayers;
for (i = 0; i < targetPlayers.length; i += 1) {
ExecutedFor(targetPlayers[i], callInfo);
ExecutedFor(targetPlayers[i], callData, callerPlayer);
}
}
return callInfo;
return true;
}
/**
* Auxiliary method that cleans up all data and deallocates all objects inside
* provided `callData` structure.
*
* @param callData Structure to clean. All stored data will be cleared,
* meaning that `DeallocateCallData()` method takes ownership of
* this parameter.
*/
public final static function DeallocateCallData(/* take */ CallData callData)
{
__().memory.Free(callData.subCommandName);
__().memory.Free(callData.parameters);
__().memory.Free(callData.options);
__().memory.Free(callData.errorCause);
__().memory.FreeMany(callData.targetPlayers);
if (callData.targetPlayers.length > 0) {
callData.targetPlayers.length = 0;
}
}
// Reports given error to the `callerPlayer`, appropriately picking
// message color
private final function ReportError(
APLayer callerPlayer,
CommandCall callInfo)
private final function ReportError(CallData callData, EPlayer callerPlayer)
{
local Text errorMessage;
local ConsoleWriter console;
if (callerPlayer == none) return;
if (callInfo == none) return;
if (callInfo.IsSuccessful()) return;
if (!callerPlayer.IsExistent()) return;
// Setup console color
console = callerPlayer.Console();
if (callInfo.GetError() == CET_EmptyTargetList) {
console = callerPlayer.BorrowConsole();
if (callData.parsingError == CET_EmptyTargetList) {
console.UseColor(_.color.textWarning);
}
else {
console.UseColor(_.color.textFailure);
}
// Send message
errorMessage = callInfo.PrintErrorMessage();
errorMessage = PrintErrorMessage(callData);
console.Say(errorMessage);
errorMessage.FreeSelf();
// Restore console color
console.ResetColor().Flush();
}
// Creates (and returns) empty `CommandCall` with given error type and
// empty error cause and reports it
private final function CommandCall MakeAndReportError(
APLayer callerPlayer,
ErrorType errorType)
private final function Text PrintErrorMessage(CallData callData)
{
local CommandCall dummyCall;
if (errorType == CET_None) return none;
dummyCall = class'CommandCall'.static.MakeError(errorType, callerPlayer);
ReportError(callerPlayer, dummyCall);
return dummyCall;
local Text result;
local MutableText builder;
builder = _.text.Empty();
switch (callData.parsingError)
{
case CET_BadParser:
builder.Append(P("Internal error occurred: invalid parser"));
break;
case CET_NoSubCommands:
builder.Append(P("Ill defined command: no subcommands"));
break;
case CET_NoRequiredParam:
builder.Append(P("Missing required parameter: "))
.Append(callData.errorCause);
break;
case CET_NoRequiredParamForOption:
builder.Append(P("Missing required parameter for option: "))
.Append(callData.errorCause);
break;
case CET_UnknownOption:
builder.Append(P("Invalid option specified: "))
.Append(callData.errorCause);
break;
case CET_UnknownShortOption:
builder.Append(P("Invalid short option specified"));
break;
case CET_RepeatedOption:
builder.Append(P("Option specified several times: "))
.Append(callData.errorCause);
break;
case CET_UnusedCommandParameters:
builder.Append(P("Part of command could not be parsed: "))
.Append(callData.errorCause);
break;
case CET_MultipleOptionsWithParams:
builder.Append(P( "Multiple short options in one declarations"
@ "require parameters: "))
.Append(callData.errorCause);
break;
case CET_IncorrectTargetList:
builder.Append(P("Target players are incorrectly specified."))
.Append(callData.errorCause);
break;
case CET_EmptyTargetList:
builder.Append(P("List of target players is empty"))
.Append(callData.errorCause);
break;
default:
}
result = builder.Copy();
builder.FreeSelf();
return result;
}
// Auxiliary method for parsing list of targeted players.
// Assumes given parser is not `none` and not in a failed state.
private final function array<APlayer> ParseTargets(
// If parsing failed, guaranteed to return an empty array.
private final function array<EPlayer> ParseTargets(
Parser parser,
APlayer callerPlayer)
EPlayer callerPlayer)
{
local array<APlayer> targetPlayers;
local array<EPlayer> targetPlayers;
local PlayersParser targetsParser;
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
targetsParser.SetSelf(callerPlayer);
targetsParser.ParseWith(parser);
if (parser.Ok()) {
targetPlayers = targetsParser.GetPlayers();
}
targetsParser.FreeSelf();
return targetPlayers;
}
@ -384,6 +513,7 @@ public final function Text GetName()
return commandData.name.LowerCopy();
}
// TODO: use `SharedRef` instead
/**
* Returns `Command.Data` struct that describes caller `Command`.
*

393
sources/Commands/CommandCall.uc

@ -1,393 +0,0 @@
/**
* This object describes a call attempt for one of the `Command`s.
* `Command`s are meant to be be executed from user's console input,
* so this object should only be created while parsing their input. Any other
* use of this object is not guaranteed to be supported.
* Copyright 2021 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 CommandCall extends AcediaObject
dependson(Command);
// Once this value is set to `true`, the command call is considered fully
// described and will prevent any changes to it's internal state
// (except deallocation).
var private bool locked;
// Player who initiated the call and targeted players (if applicable)
var private APlayer callerPlayer;
var private array<APlayer> targetPlayers;
// Specified sub-command and parameters/options
var private Text subCommandName;
var private AssociativeArray commandParameters, commandOptions;
// Errors that occurred during command call processing are described by
// error type and optional error textual name of the object
// (parameter, option, etc.) that caused it.
var private Command.ErrorType parsingError;
var private Text errorCause;
var public const int TBAD_PARSER, TNOSUB_COMMAND, TNO_REQ_PARAM;
var public const int TNO_REQ_PARAM_FOR_OPTION, TUNKNOW_NOPTION;
var public const int TUNKNOWN_SHORT_OPTION, TREPEATED_OPTION, TUNUSED_INPUT;
var public const int TMULTIPLE_OPTIONS_WITH_PARAMS;
var public const int TINCORRECT_TARGET, TEMPTY_TARGETS;
protected function Constructor()
{
// We simply take ownership and record into `commandParameters` whatever
// `AssociativeArray` was passed to us, but fill (and therefore create)
// `commandOptions` ourselves.
commandOptions = _.collections.EmptyAssociativeArray();
}
protected function Finalizer()
{
_.memory.Free(commandParameters);
_.memory.Free(commandOptions);
_.memory.Free(subCommandName);
_.memory.Free(errorCause);
commandParameters = none;
commandOptions = none;
subCommandName = none;
errorCause = none;
parsingError = CET_None;
targetPlayers.length = 0;
locked = false;
}
/**
* Method for producing erroneous `CommandCall` for the needs of
* error reporting.
*
* @param type Type of error resulting `CommandCall` must have.
* @param callerPlayer Player that caused erroneous command call.
* @return `CommandCall` with specified error type and `APlayer`.
*/
public final static function CommandCall MakeError(
Command.ErrorType type,
APlayer callerPlayer)
{
return CommandCall(__().memory.Allocate(class'CommandCall'))
.DeclareError(type)
.SetCallerPlayer(callerPlayer);
}
/**
* Put caller `CommandCall` into erroneous state.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @param type Type of error caller `CommandCall` must have.
* Once error (not `CET_None`) was set - calling this method with
* `CET_None` to erase error will not work.
* @param cause Textual description of offender command part to supplement
* error report (will be used when reporting error to the caller).
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall DeclareError(
Command.ErrorType type,
optional Text cause)
{
if (locked) return self;
if (parsingError != CET_None) return self;
parsingError = type;
_.memory.Free(errorCause);
errorCause = none;
if (cause != none) {
errorCause = cause.Copy();
}
return self;
}
/**
* Checks if caller `CommandCall` is in erroneous state.
*
* @return `true` if `CommandCall` has not recorded any errors so far and
* `false` otherwise.
*/
public final function bool IsSuccessful()
{
return parsingError == CET_None;
}
/**
* Returns current error type (including `CET_None` if there were no errors).
*
* @return Error type for caller `CommandCall` error.
*/
public final function Command.ErrorType GetError()
{
return parsingError;
}
/**
* In case there were any errors - returns textual description of offender
* command part. Mostly used for reporting errors to players.
*
* @return Textual description of command part that caused an error.
*/
public final function Text GetErrorCause()
{
if (errorCause != none) {
return errorCause.Copy();
}
return none;
}
/**
* After this method is called - changes to the `CommandCall` will be
* prevented.
*
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall Finish()
{
locked = true;
return self;
}
/**
* Returns player, that initiated command, that produced caller `CommandCall`.
*
* @return Player, that initiated command, that produced caller `CommandCall`
*/
public final function APlayer GetCallerPlayer()
{
return callerPlayer;
}
/**
* Sets player, that initiated command, that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetCallerPlayer(APlayer player)
{
callerPlayer = player;
return self;
}
/**
* Returns players that were targeted by command that produced caller
* `CommandCall`.
*
* @return Players, targeted by caller `CommandCall`.
*/
public final function array<APlayer> GetTargetPlayers()
{
return targetPlayers;
}
/**
* Sets players, targeted by command that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetTargetPlayers(array<APlayer> newTargets)
{
if (!locked) {
targetPlayers = newTargets;
}
return self;
}
/**
* Returns picked sub-command of command that produced caller `CommandCall`.
*
* @return Sub-command of command that produced caller `CommandCall`.
* Returns stored value that will be deallocated along with
* caller `CommandCall` - do not deallocate returned `Text` manually.
*/
public final function Text GetSubCommand()
{
return subCommandName;
}
/**
* Sets sub-command of command that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @param newSubCommandName New sub command name.
* Copy of passed object will be stored.
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetSubCommand(Text newSubCommandName)
{
if (!locked)
{
_.memory.Free(subCommandName);
subCommandName = newSubCommandName.Copy();
}
return self;
}
/**
* Returns parameters of command that produced caller `CommandCall`.
*
* @return Parameters of command that produced caller `CommandCall`.
* Returns stored value that will be deallocated along with
* caller `CommandCall` - do not deallocate returned `AssociativeArray`
* manually.
*/
public final function AssociativeArray GetParameters()
{
return commandParameters;
}
/**
* Sets parameters of command that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @param parameters New set of parameters. Passed value will be managed by
* caller `CommandCall` and should not be deallocated manually after
* calling `SetParameters()`.
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetParameters(AssociativeArray parameters)
{
if (!locked)
{
_.memory.Free(commandParameters);
commandParameters = parameters;
}
return self;
}
/**
* Returns options of command that produced caller `CommandCall`.
*
* If option without parameters was specified - it will be recorded as
* a key with value `none`.
* If option has parameters - `AssociativeArray` with them will be
* recorded as value instead.
*
* @return Options of command that produced caller `CommandCall`.
* Returns stored value that will be deallocated along with
* caller `CommandCall` - do not deallocate returned `AssociativeArray`
* manually.
*/
public final function AssociativeArray GetOptions()
{
return commandOptions;
}
/**
* Sets option parameters of command that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* For recording options without parameters simply pass `none` instead of them.
*
* @param option Option to record (along with it's possible parameters).
* @param parameters Option parameters. Passed value will be managed by
* caller `CommandCall` and should not be deallocated manually after
* calling `SetParameters()`.
* Pass `none` if option has no parameters.
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetOptionParameters(
Command.Option option,
optional AssociativeArray parameters)
{
if (locked) return self;
if (commandOptions == none) return self;
commandOptions.SetItem(option.longName, parameters, true);
return self;
}
/**
* Prints error message as a human-readable message that can be reported to
* the caller player.
*
* In case there was no error - empty text is returned.
*
* @return Error message in a human-readable form.
*/
public final function Text PrintErrorMessage()
{
local Text result;
local MutableText builder;
builder = _.text.Empty();
switch (parsingError)
{
case CET_BadParser:
builder.Append(T(TBAD_PARSER));
break;
case CET_NoSubCommands:
builder.Append(T(TNOSUB_COMMAND));
break;
case CET_NoRequiredParam:
builder.Append(T(TNO_REQ_PARAM)).Append(errorCause);
break;
case CET_NoRequiredParamForOption:
builder.Append(T(TNO_REQ_PARAM_FOR_OPTION)).Append(errorCause);
break;
case CET_UnknownOption:
builder.Append(T(TUNKNOW_NOPTION)).Append(errorCause);
break;
case CET_UnknownShortOption:
builder.Append(T(TUNKNOWN_SHORT_OPTION));
break;
case CET_RepeatedOption:
builder.Append(T(TREPEATED_OPTION)).Append(errorCause);
break;
case CET_UnusedCommandParameters:
builder.Append(T(TUNUSED_INPUT)).Append(errorCause);
break;
case CET_MultipleOptionsWithParams:
builder.Append(T(TMULTIPLE_OPTIONS_WITH_PARAMS)).Append(errorCause);
break;
case CET_IncorrectTargetList:
builder.Append(T(TINCORRECT_TARGET)).Append(errorCause);
break;
case CET_EmptyTargetList:
builder.Append(T(TEMPTY_TARGETS)).Append(errorCause);
break;
default:
}
result = builder.Copy();
builder.FreeSelf();
return result;
}
defaultproperties
{
TBAD_PARSER = 0
stringConstants(0) = "Internal error occurred: invalid parser."
TNOSUB_COMMAND = 1
stringConstants(1) = "Ill defined command: no subcommands"
TNO_REQ_PARAM = 2
stringConstants(2) = "Missing required parameter: "
TNO_REQ_PARAM_FOR_OPTION = 3
stringConstants(3) = "Missing required parameter for option: "
TUNKNOW_NOPTION = 4
stringConstants(4) = "Invalid option specified: "
TUNKNOWN_SHORT_OPTION = 5
stringConstants(5) = "Invalid short option specified."
TREPEATED_OPTION = 6
stringConstants(6) = "Option specified several times: "
TUNUSED_INPUT = 7
stringConstants(7) = "Part of command could not be parsed: "
TMULTIPLE_OPTIONS_WITH_PARAMS = 8
stringConstants(8) = "Multiple short options in one declarations require parameters:"
TINCORRECT_TARGET = 9
stringConstants(9) = "Target players are incorrectly specified."
TEMPTY_TARGETS = 10
stringConstants(10) = "List of target players is empty."
}

65
sources/Commands/CommandParser.uc

@ -1,9 +1,9 @@
/**
* Auxiliary class for parsing user's input into a `CommandCall` based on
* Auxiliary class for parsing user's input into a `Command.CallData` based on
* a given `Command.Data`. While it's meant to be allocated for
* a `self.ParseWith()` call and deallocated right after, it can be reused
* without deallocation.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -21,7 +21,6 @@
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandParser extends AcediaObject
dependson(CommandCall)
dependson(Command);
/**
@ -78,7 +77,7 @@ var private Command.SubCommand pickedSubCommand;
var private array<Command.Option> availableOptions;
// Result variable we are filling during the parsing process,
// should be `none` outside of `self.ParseWith()` method call.
var private CommandCall nextResult;
var private Command.CallData nextResult;
// Describes which parameters we are currently parsing, classifying them
// as either "necessary" or "extra".
@ -131,8 +130,9 @@ protected function Finalizer()
// Zero important variables
private final function Reset()
{
local Command.CallData blankCallData;
commandParser = none;
nextResult = none;
nextResult = blankCallData;
currentTarget = CPT_NecessaryParameter;
currentTargetIsOption = false;
usedOptions.length = 0;
@ -143,8 +143,9 @@ private final function DeclareError(
Command.ErrorType type,
optional Text cause)
{
if (nextResult != none) {
nextResult.DeclareError(type, cause);
nextResult.parsingError = type;
if (cause != none) {
nextResult.errorCause = cause.Copy();
}
if (commandParser != none) {
commandParser.Fail();
@ -200,50 +201,51 @@ private final function PickSubCommand(Command.Data commandData)
* to be parsed as a command's call.
* @param commandData Describes what parameters and options should be
* expected in user's input. `Text` values from `commandData` can be used
* inside resulting object `CommandCall`, so deallocating them can
* inside resulting `Command.CallData`, so deallocating them can
* invalidate returned value.
* @return Results of parsing as described by `CommandCall`.
* @return Results of parsing, described by `Command.CallData`.
* Returned object is guaranteed to be not `none`.
*/
public final function CommandCall ParseWith(
public final function Command.CallData ParseWith(
Parser parser,
Command.Data commandData)
{
local AssociativeArray commandParameters;
// Temporary object to return `nextResult` while setting variable to `none`
local CommandCall toReturn;
Reset();
nextResult = CommandCall(_.memory.Allocate(class'CommandCall'));
local Command.CallData toReturn;
nextResult.parameters = _.collections.EmptyAssociativeArray();
nextResult.options = _.collections.EmptyAssociativeArray();
if (commandData.subCommands.length == 0)
{
DeclareError(CET_NoSubCommands, none);
return nextResult;
toReturn = nextResult;
Reset();
return toReturn;
}
if (parser == none || !parser.Ok())
{
DeclareError(CET_BadParser, none);
return nextResult;
toReturn = nextResult;
Reset();
return toReturn;
}
commandParser = parser;
availableOptions = commandData.options;
// (subcommand) (parameters, possibly with options) and nothing else!
PickSubCommand(commandData);
nextResult.SetSubCommand(pickedSubCommand.name);
nextResult.subCommandName = pickedSubCommand.name.Copy();
commandParameters = ParseParameterArrays( pickedSubCommand.required,
pickedSubCommand.optional);
AssertNoTrailingInput(); // make sure there is nothing else
if (commandParser.Ok()) {
nextResult.SetParameters(commandParameters);
nextResult.parameters = commandParameters;
}
else {
_.memory.Free(commandParameters);
}
// Clean up
commandParser = none;
usedOptions.length = 0;
currentTargetIsOption = false;
toReturn = nextResult;
nextResult = none;
Reset();
return toReturn;
}
@ -338,7 +340,7 @@ private final function ParseOptionalParameterArray(
if (!ParseParameter(parsedParameters, optionalParameters[i]))
{
// Propagate errors
if (!nextResult.IsSuccessful()) {
if (nextResult.parsingError != CET_None) {
return;
}
// Failure to parse optional parameter is fine if
@ -379,7 +381,7 @@ private final function bool ParseParameter(
}
// We only succeeded in parsing if we've parsed enough for
// a given parameter and did not encounter any errors
if (parsedEnough && nextResult.IsSuccessful()) {
if (parsedEnough && nextResult.parsingError == CET_None) {
commandParser.RestoreState(confirmedState);
return true;
}
@ -416,7 +418,7 @@ private final function bool ParseSingleValue(
}
while (TryParsingOptions());
// Propagate errors after parsing options
if (!nextResult.IsSuccessful()) {
if (nextResult.parsingError != CET_None) {
return false;
}
// Try parsing one of the variable types
@ -685,7 +687,7 @@ private final function bool TryParsingOptions()
// possible parameters with `commandParser`.
// Returns `true` on success and `false` otherwise. At the point this
// method is called, option declaration is already assumed to be detected
// and any failure implies parsing error (ending in failed `CommandCall`).
// and any failure implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseLongOption()
{
local int i, optionIndex;
@ -724,7 +726,7 @@ private final function bool ParseLongOption()
// possible parameters with `commandParser`.
// Returns `true` on success and `false` otherwise. At the point this
// method is called, option declaration is already assumed to be detected
// and any failure implies parsing error (ending in failed `CommandCall`).
// and any failure implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseShortOption()
{
local int i;
@ -738,14 +740,14 @@ private final function bool ParseShortOption()
}
for (i = 0; i < optionsList.GetLength(); i += 1)
{
if (!nextResult.IsSuccessful()) break;
if (nextResult.parsingError != CET_None) break;
pickedOptionWithParameters =
AddOptionByCharacter( optionsList.GetCharacter(i), optionsList,
pickedOptionWithParameters)
|| pickedOptionWithParameters;
}
optionsList.FreeSelf();
return nextResult.IsSuccessful();
return (nextResult.parsingError == CET_None);
}
// Assumes `commandParser` and `nextResult` are not `none`.
@ -819,7 +821,9 @@ private final function bool ParseOptionParameters(Command.Option pickedOption)
}
if (pickedOption.required.length == 0 && pickedOption.optional.length == 0)
{
nextResult.SetOptionParameters(pickedOption, none);
// Here `optionParameters == none`
nextResult.options
.SetItem(pickedOption.longName, optionParameters, true);
return true;
}
currentTargetIsOption = true;
@ -829,7 +833,8 @@ private final function bool ParseOptionParameters(Command.Option pickedOption)
currentTargetIsOption = false;
if (commandParser.Ok())
{
nextResult.SetOptionParameters(pickedOption, optionParameters);
nextResult.options
.SetItem(pickedOption.longName, optionParameters, true);
return true;
}
return false;

23
sources/Commands/Commands_Feature.uc

@ -3,7 +3,7 @@
* 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 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -202,9 +202,10 @@ public final function array<Text> GetCommandNames()
* contain command's name and it's parameters.
* @param callerPlayer Player that caused this command call.
*/
public final function HandleInput(Parser parser, APlayer callerPlayer)
public final function HandleInput(Parser parser, EPlayer callerPlayer)
{
local Command commandInstance;
local Command.CallData callData;
local MutableText commandName;
if (parser == none) return;
if (!parser.Ok()) return;
@ -212,8 +213,11 @@ public final function HandleInput(Parser parser, APlayer callerPlayer)
parser.MUntilMany(commandName, commandDelimiters, true, true);
commandInstance = GetCommand(commandName);
commandName.FreeSelf();
if (parser.Ok() && commandInstance != none) {
commandInstance.ProcessInput(parser, callerPlayer).FreeSelf();
if (parser.Ok() && commandInstance != none)
{
callData = commandInstance.ParseInputWith(parser, callerPlayer);
commandInstance.Execute(callData, callerPlayer);
commandInstance.DeallocateCallData(callData);
}
}
@ -224,9 +228,8 @@ private function bool HandleText(
bool teamMessage)
{
local Text messageAsText;
local APlayer callerPlayer;
local EPlayer callerPlayer;
local Parser parser;
local PlayerService service;
// We only want to catch chat messages
// and only if `Commands` feature is active
if (messageType != 'Say') return true;
@ -242,13 +245,11 @@ private function bool HandleText(
messageAsText.FreeSelf();
return true;
}
// Extract `APlayer` from the `sender`
service = PlayerService(class'PlayerService'.static.Require());
if (service != none) {
callerPlayer = service.GetPlayer(PlayerController(sender));
}
// Extract `EPlayer` from the `sender`
callerPlayer = _.players.FromController(PlayerController(sender));
// Pass input to command feature
HandleInput(parser, callerPlayer);
_.memory.Free(callerPlayer);
parser.FreeSelf();
return false;
}

43
sources/Commands/PlayersParser.uc

@ -1,6 +1,6 @@
/**
* Object for parsing what converting textual description of a group of
* players into array of `APlayer`s. Depends on the game context.
* players into array of `EPlayer`s. Depends on the game context.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@ -62,12 +62,12 @@ class PlayersParser extends AcediaObject
*/
// Player for which "@" and "@self" macros will refer
var private APlayer selfPlayer;
var private EPlayer selfPlayer;
// Copy of the list of current players at the moment of allocation of
// this `PlayersParser`.
var private array<APlayer> playersSnapshot;
var private array<EPlayer> playersSnapshot;
// Players, selected according to selectors we have parsed so far
var private array<APlayer> currentSelection;
var private array<EPlayer> currentSelection;
// Have we parsed our first selector?
// We need this to know whether to start with the list of
// all players (if first selector removes them) or
@ -81,6 +81,10 @@ var const int TOPEN_BRACKET, TCLOSE_BRACKET;
protected function Finalizer()
{
// No need to deallocate `currentSelection`,
// since it has `EPlayer`s from `playersSnapshot` or `selfPlayer`
_.memory.Free(selfPlayer);
_.memory.FreeMany(playersSnapshot);
selfPlayer = none;
parsedFirstSelector = false;
playersSnapshot.length = 0;
@ -94,15 +98,18 @@ protected function Finalizer()
* "@self" macros. Passing `none` will make it so no one is
* referred by them.
*/
public final function SetSelf(APLayer newSelfPlayer)
public final function SetSelf(EPlayer newSelfPlayer)
{
selfPlayer = newSelfPlayer;
_.memory.Free(selfPlayer);
if (newSelfPlayer != none) {
selfPlayer = EPlayer(newSelfPlayer.Copy());
}
}
// Insert a new player into currently selected list of players
// (`currentSelection`) such that there will be no duplicates.
// `none` values are auto-discarded.
private final function InsertPlayer(APLayer toInsert)
private final function InsertPlayer(EPlayer toInsert)
{
local int i;
if (toInsert == none) {
@ -391,9 +398,17 @@ private final function MutableText ParseLiteral(Parser parser)
*
* @return players parsed by the last `ParseWith()` or `Parse()` call.
*/
public final function array<APlayer> GetPlayers()
public final function array<EPlayer> GetPlayers()
{
return currentSelection;
local int i;
local array<EPlayer> result;
for (i = 0; i < currentSelection.length; i += 1)
{
if (currentSelection[i].IsExistent()) {
result[result.length] = EPlayer(currentSelection[i].Copy());
}
}
return result;
}
/**
@ -410,6 +425,7 @@ public final function bool ParseWith(Parser parser)
local Parser.ParserState confirmedState;
if (parser == none) return false;
if (!parser.Ok()) return false;
Reset();
confirmedState = parser.Skip().GetCurrentState();
if (!parser.Match(T(TOPEN_BRACKET)).Ok())
@ -441,14 +457,11 @@ public final function bool ParseWith(Parser parser)
// `playersSnapshot` to contain current players.
private final function Reset()
{
local PlayerService service;
parsedFirstSelector = false;
playersSnapshot.length = 0;
currentSelection.length = 0;
service = PlayerService(class'PlayerService'.static.Require());
if (service != none) {
playersSnapshot = service.GetAllPlayers();
}
_.memory.FreeMany(playersSnapshot);
playersSnapshot.length = 0;
playersSnapshot = _.players.GetAll();
selectorDelimiters.length = 0;
selectorDelimiters[0] = T(TCOMMA);
selectorDelimiters[1] = T(TCLOSE_BRACKET);

215
sources/Commands/Tests/TEST_Command.uc

@ -1,6 +1,6 @@
/**
* Set of tests for `Command` class.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -40,8 +40,8 @@ protected static function TESTS()
Test_MockA();
Context("Testing `Command` parsing (options).");
Test_MockB();
Context("Testing `CommandCall` error messages.");
Test_CommandCallErrors();
Context("Testing `Command.CallData` error messages.");
Test_CallDataErrors();
Context("Testing sub-command determination.");
Test_SubCommandName();
}
@ -62,131 +62,132 @@ protected static function Test_MockB()
SubTest_MockBQ3Remainder();
}
protected static function Test_CommandCallErrors()
protected static function Test_CallDataErrors()
{
SubTest_CommandCallErrorBadParser();
SubTest_CommandCallErrorNoRequiredParam();
SubTest_CommandCallErrorUnknownOption();
SubTest_CommandCallErrorRepeatedOption();
SubTest_CommandCallErrorMultipleOptionsWithParams();
SubTest_CommandCallErrorUnusedCommandParameters();
SubTest_CommandCallErrorNoRequiredParamForOption();
SubTest_CallDataErrorBadParser();
SubTest_CallDataErrorNoRequiredParam();
SubTest_CallDataErrorUnknownOption();
SubTest_CallDataErrorRepeatedOption();
SubTest_CallDataErrorMultipleOptionsWithParams();
SubTest_CallDataErrorUnusedCommandParameters();
SubTest_CallDataErrorNoRequiredParamForOption();
}
protected static function SubTest_CommandCallErrorBadParser()
protected static function SubTest_CallDataErrorBadParser()
{
local CommandCall result;
local Command.CallData result;
Issue("`CET_BadParser` errors are incorrectly reported.");
result = class'MockCommandA'.static.GetInstance().ProcessInput(none, none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_BadParser);//
TEST_ExpectNone(result.GetErrorCause());
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(__().text.ParseString("stuff").Fail(), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_BadParser);//
TEST_ExpectNone(result.GetErrorCause());
.ParseInputWith(none, none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_BadParser);
TEST_ExpectNone(result.errorCause);
result = class'MockCommandA'.static.GetInstance()
.ParseInputWith(__().text.ParseString("stuff").Fail(), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_BadParser);
TEST_ExpectNone(result.errorCause);
}
protected static function SubTest_CommandCallErrorNoRequiredParam()
protected static function SubTest_CallDataErrorNoRequiredParam()
{
local CommandCall result;
local Command.CallData result;
Issue("`CET_NoRequiredParam` errors are incorrectly reported.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryAFailure1), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_NoRequiredParam);
TEST_ExpectTrue( result.GetErrorCause().ToString()
.ParseInputWith(PRS(default.queryAFailure1), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_NoRequiredParam);
TEST_ExpectTrue( result.errorCause.ToString()
== "integer variable");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryAFailure2), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_NoRequiredParam);
TEST_ExpectTrue( result.GetErrorCause().ToString()
.ParseInputWith(PRS(default.queryAFailure2), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_NoRequiredParam);
TEST_ExpectTrue( result.errorCause.ToString()
== "isItSimple?");
}
protected static function SubTest_CommandCallErrorUnknownOption()
protected static function SubTest_CallDataErrorUnknownOption()
{
local CommandCall result;
local Command.CallData result;
Issue("`CET_UnknownOption` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureUnknownOptionLong), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_UnknownOption);
TEST_ExpectTrue( result.GetErrorCause().ToString()
.ParseInputWith(PRS(default.queryBFailureUnknownOptionLong), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_UnknownOption);
TEST_ExpectTrue( result.errorCause.ToString()
== "kest");
Issue("`CET_UnknownShortOption` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureUnknownOptionShort), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_UnknownShortOption);
TEST_ExpectNone(result.GetErrorCause());
.ParseInputWith(PRS(default.queryBFailureUnknownOptionShort), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_UnknownShortOption);
TEST_ExpectNone(result.errorCause);
}
protected static function SubTest_CommandCallErrorRepeatedOption()
protected static function SubTest_CallDataErrorRepeatedOption()
{
local CommandCall result;
local Command.CallData result;
Issue("`CET_RepeatedOption` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailure2), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_RepeatedOption);
TEST_ExpectTrue( result.GetErrorCause().ToString()
.ParseInputWith(PRS(default.queryBFailure2), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_RepeatedOption);
TEST_ExpectTrue( result.errorCause.ToString()
== "forced");
}
protected static function SubTest_CommandCallErrorUnusedCommandParameters()
protected static function SubTest_CallDataErrorUnusedCommandParameters()
{
local CommandCall result;
local Command.CallData result;
Issue("`CET_UnusedCommandParameters` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureUnused), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_UnusedCommandParameters);
TEST_ExpectTrue( result.GetErrorCause().ToString()
.ParseInputWith(PRS(default.queryBFailureUnused), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_UnusedCommandParameters);
TEST_ExpectTrue( result.errorCause.ToString()
== "text -j");
}
protected static function SubTest_CommandCallErrorMultipleOptionsWithParams()
protected static function SubTest_CallDataErrorMultipleOptionsWithParams()
{
local CommandCall result;
local Command.CallData result;
Issue("`CET_MultipleOptionsWithParams` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailure1), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_MultipleOptionsWithParams);
TEST_ExpectTrue(result.GetErrorCause().ToString() == "tv");
.ParseInputWith(PRS(default.queryBFailure1), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_MultipleOptionsWithParams);
TEST_ExpectTrue(result.errorCause.ToString() == "tv");
}
protected static function SubTest_CommandCallErrorNoRequiredParamForOption()
protected static function SubTest_CallDataErrorNoRequiredParamForOption()
{
local CommandCall result;
local Command.CallData result;
Issue("`CET_NoRequiredParamForOption` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureNoReqParamOption1), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_NoRequiredParamForOption);
TEST_ExpectTrue(result.GetErrorCause().ToString() == "long");
.ParseInputWith(PRS(default.queryBFailureNoReqParamOption1), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_NoRequiredParamForOption);
TEST_ExpectTrue(result.errorCause.ToString() == "long");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureNoReqParamOption2), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_NoRequiredParamForOption);
TEST_ExpectTrue(result.GetErrorCause().ToString() == "values");
.ParseInputWith(PRS(default.queryBFailureNoReqParamOption2), none);
TEST_ExpectFalse(result.parsingError == CET_None);
TEST_ExpectTrue(result.parsingError == CET_NoRequiredParamForOption);
TEST_ExpectTrue(result.errorCause.ToString() == "values");
}
protected static function Test_SubCommandName()
{
local CommandCall result;
local Command.CallData result;
Issue("Cannot determine subcommands.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess1), none);
TEST_ExpectTrue(result.GetSubCommand().ToString() == "simple");
.ParseInputWith(PRS(default.queryASuccess1), none);
TEST_ExpectTrue(result.subCommandName.ToString() == "simple");
Issue("Cannot determine when subcommands are missing.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess2), none);
TEST_ExpectTrue(result.GetSubCommand().IsEmpty());
.ParseInputWith(PRS(default.queryASuccess2), none);
TEST_ExpectTrue(result.subCommandName.IsEmpty());
}
protected static function SubTest_MockAQ1AndFailed()
@ -199,14 +200,16 @@ protected static function SubTest_MockAQ1AndFailed()
command = class'MockCommandA'.static.GetInstance();
Issue("Command queries that should fail succeed instead.");
parser.InitializeS(default.queryAFailure1);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
parser.InitializeS(default.queryAFailure2);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
Issue("Cannot parse command queries without optional parameters.");
parameters =
command.ProcessInput(parser.InitializeS(default.queryASuccess1), none)
.GetParameters();
command.ParseInputWith(parser.InitializeS(default.queryASuccess1), none)
.Parameters;
TEST_ExpectTrue(parameters.GetLength() == 2);
paramArray = DynamicArray(parameters.GetItem(P("isItSimple?")));
TEST_ExpectTrue(paramArray.GetLength() == 1);
@ -222,7 +225,7 @@ protected static function SubTest_MockAQ2()
local AssociativeArray result, subObject;
Issue("Cannot parse command queries without optional parameters.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess2), none).GetParameters();
.ParseInputWith(PRS(default.queryASuccess2), none).Parameters;
TEST_ExpectTrue(result.GetLength() == 2);
subObject = AssociativeArray(result.GetItem(P("just_obj")));
TEST_ExpectTrue(IntBox(subObject.GetItem(P("var"))).Get() == 7);
@ -252,7 +255,7 @@ protected static function SubTest_MockAQ3()
local AssociativeArray result;
Issue("Cannot parse command queries with optional parameters.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess3), none).GetParameters();
.ParseInputWith(PRS(default.queryASuccess3), none).Parameters;
// Booleans
paramArray = DynamicArray(result.GetItem(P("isItSimple?")));
TEST_ExpectTrue(paramArray.GetLength() == 7);
@ -286,7 +289,7 @@ protected static function SubTest_MockAQ4()
local AssociativeArray result, subObject;
Issue("Cannot parse command queries with optional parameters.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess4), none).GetParameters();
.ParseInputWith(PRS(default.queryASuccess4), none).Parameters;
TEST_ExpectTrue(result.GetLength() == 3);
subObject = AssociativeArray(result.GetItem(P("just_obj")));
TEST_ExpectTrue(IntBox(subObject.GetItem(P("var"))).Get() == 7);
@ -306,32 +309,40 @@ protected static function SubTest_MockBFailed()
command = class'MockCommandB'.static.GetInstance();
Issue("Command queries that should fail succeed instead.");
parser.InitializeS(default.queryBFailure1);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
parser.InitializeS(default.queryBFailure2);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
parser.InitializeS(default.queryBFailure3);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
parser.InitializeS(default.queryBFailureNoReqParamOption1);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
parser.InitializeS(default.queryBFailureNoReqParamOption2);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
parser.InitializeS(default.queryBFailureUnknownOptionLong);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
parser.InitializeS(default.queryBFailureUnknownOptionShort);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
parser.InitializeS(default.queryBFailureUnused);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
TEST_ExpectFalse(
command.ParseInputWith(parser, none).parsingError == CET_None);
}
protected static function SubTest_MockBQ1()
{
local CommandCall result;
local Command.CallData result;
local DynamicArray subArray;
local AssociativeArray params, options, subObject;
Issue("Cannot parse command queries with options.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBSuccess1), none);
params = result.GetParameters();
.ParseInputWith(PRS(default.queryBSuccess1), none);
params = result.Parameters;
TEST_ExpectTrue(params.GetLength() == 2);
subArray = DynamicArray(params.GetItem(P("just_array")));
TEST_ExpectTrue(subArray.GetLength() == 2);
@ -339,7 +350,7 @@ protected static function SubTest_MockBQ1()
TEST_ExpectNone(subArray.GetItem(1));
TEST_ExpectTrue( Text(params.GetItem(P("just_text"))).ToString()
== "text");
options = result.GetOptions();
options = result.options;
TEST_ExpectTrue(options.GetLength() == 1);
subObject = AssociativeArray(options.GetItem(P("values")));
TEST_ExpectTrue(subObject.GetLength() == 1);
@ -354,14 +365,14 @@ protected static function SubTest_MockBQ1()
protected static function SubTest_MockBQ2()
{
local CommandCall result;
local Command.CallData result;
local DynamicArray subArray;
local AssociativeArray options, subObject;
Issue("Cannot parse command queries with mixed-in options.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBSuccess2), none);
TEST_ExpectTrue(result.GetParameters().GetLength() == 0);
options = result.GetOptions();
.ParseInputWith(PRS(default.queryBSuccess2), none);
TEST_ExpectTrue(result.Parameters.GetLength() == 0);
options = result.options;
TEST_ExpectTrue(options.GetLength() == 7);
TEST_ExpectTrue(options.HasKey(P("actual")));
TEST_ExpectNone(options.GetItem(P("actual")));
@ -384,17 +395,17 @@ protected static function SubTest_MockBQ2()
protected static function SubTest_MockBQ3Remainder()
{
local CommandCall result;
local Command.CallData result;
local DynamicArray subArray;
local AssociativeArray options, subObject;
Issue("Cannot parse command queries with `CPT_Remainder` type parameters.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBSuccess3), none);
TEST_ExpectTrue(result.GetParameters().GetLength() == 1);
subArray = DynamicArray(result.GetParameters().GetItem(P("list")));
.ParseInputWith(PRS(default.queryBSuccess3), none);
TEST_ExpectTrue(result.Parameters.GetLength() == 1);
subArray = DynamicArray(result.Parameters.GetItem(P("list")));
TEST_ExpectTrue(FloatBox(subArray.GetItem(0)).Get() == 3);
TEST_ExpectTrue(FloatBox(subArray.GetItem(1)).Get() == -76);
options = result.GetOptions();
options = result.Options;
TEST_ExpectTrue(options.GetLength() == 1);
TEST_ExpectTrue(options.HasKey(P("remainder")));
subObject = AssociativeArray(options.GetItem(P("remainder")));

2
sources/Console/ConsoleAPI.uc

@ -221,7 +221,7 @@ public final function ConsoleWriter ForAll()
* write into consoles of all players.
* Guaranteed to not be `none`.
*/
public final function ConsoleWriter For(APlayer targetPlayer)
public final function ConsoleWriter For(EPlayer targetPlayer)
{
local ConsoleDisplaySettings globalSettings;
globalSettings.defaultColor = defaultColor;

78
sources/Console/ConsoleWriter.uc

@ -44,7 +44,8 @@ enum ConsoleWriterTarget
var private ConsoleWriterTarget targetType;
// Players that will receive output passed to this `ConsoleWriter`.
// Only used when `targetType == CWTARGET_Players`
var private array<APlayer> outputTargets;
// Cannot be allowed to contain `none` values.
var private array<EPlayer> outputTargets;
var private ConsoleBuffer outputBuffer;
var private bool needToResetColor;
@ -59,6 +60,14 @@ var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
// color information instead.
var private Color defaultColor;
protected function Finalizer()
{
_.memory.FreeMany(outputTargets);
_.memory.Free(outputBuffer);
outputTargets.length = 0;
outputBuffer = none;
}
public final function ConsoleWriter Initialize(
ConsoleAPI.ConsoleDisplaySettings newDisplaySettings)
{
@ -326,7 +335,7 @@ public final function ConsoleWriter ForAll()
* throw messages away.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter ForPlayer(APlayer targetPlayer)
public final function ConsoleWriter ForPlayer(EPlayer targetPlayer)
{
if (targetPlayer == none)
{
@ -339,7 +348,7 @@ public final function ConsoleWriter ForPlayer(APlayer targetPlayer)
}
outputTargets.length = 0;
targetType = CWTARGET_Players;
outputTargets[0] = targetPlayer;
outputTargets[0] = EPlayer(targetPlayer.Copy());
return self;
}
@ -353,15 +362,16 @@ public final function ConsoleWriter ForPlayer(APlayer targetPlayer)
* If `none` - this method will do nothing.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter AndPlayer(APlayer targetPlayer)
public final function ConsoleWriter AndPlayer(EPlayer targetPlayer)
{
local int i;
if (targetPlayer == none) return self;
if (!targetPlayer.IsConnected()) return self;
if (!targetPlayer.IsExistent()) return self;
if (targetType != CWTARGET_Players)
{
Flush();
_.memory.FreeMany(outputTargets);
if (targetType == CWTARGET_None) {
outputTargets.length = 0;
}
@ -372,12 +382,12 @@ public final function ConsoleWriter AndPlayer(APlayer targetPlayer)
targetType = CWTARGET_Players;
for (i = 0; i < outputTargets.length; i += 1)
{
if (outputTargets[i] == targetPlayer) {
if (targetPlayer.SameAs(outputTargets[i])) {
return self;
}
}
Flush();
outputTargets[outputTargets.length] = targetPlayer;
outputTargets[outputTargets.length] = EPlayer(targetPlayer.Copy());
return self;
}
@ -391,24 +401,26 @@ public final function ConsoleWriter AndPlayer(APlayer targetPlayer)
* If `none` - this method will do nothing.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter ButPlayer(APlayer playerToRemove)
public final function ConsoleWriter ButPlayer(EPlayer playerToRemove)
{
local int i;
if (targetType == CWTARGET_None) return self;
if (playerToRemove == none) return self;
if (!playerToRemove.IsConnected()) return self;
if (!playerToRemove.IsExistent()) return self;
if (targetType == CWTARGET_All)
{
Flush();
_.memory.FreeMany(outputTargets);
outputTargets = _.players.GetAll();
}
targetType = CWTARGET_Players;
while (i < outputTargets.length)
{
if (outputTargets[i] == playerToRemove)
if (playerToRemove.SameAs(outputTargets[i]))
{
Flush();
_.memory.Free(outputTargets[i]);
outputTargets.Remove(i, 1);
break;
}
@ -432,39 +444,45 @@ public final function ConsoleWriterTarget CurrentTarget()
}
/**
* Returns `APlayer` to whom console caller `ConsoleWriter` is
* Returns `EPlayer` to whom console caller `ConsoleWriter` is
* outputting messages.
* If caller `ConsoleWriter` is setup to message several different players,
* returns an arbitrary one of them.
*
* @return Player (`APlayer` class) to whom console caller `ConsoleWriter` is
* @return Player (`EPlayer` class) to whom console caller `ConsoleWriter` is
* outputting messages. Returns `none` iff it currently outputs to
* every player or to no one.
*/
public final function APlayer GetTargetPlayer()
public final function EPlayer GetTargetPlayer()
{
if (targetType == CWTARGET_All) return none;
if (outputTargets.length <= 0) return none;
return outputTargets[0];
return EPlayer(outputTargets[0].Copy());
}
/**
* Returns `APlayer` to whom console caller `ConsoleWriter` is
* Returns array of `EPlayer`s, to whom console caller `ConsoleWriter` is
* outputting messages.
* If caller `ConsoleWriter` is setup to message several different players,
* returns an arbitrary one of them.
*
* @return Player (`APlayer` class) to whom console caller `ConsoleWriter` is
* outputting messages. Returns `none` iff it currently outputs to
* every player or to no one.
* @return Player (`EPlayer` class) to whom console caller `ConsoleWriter` is
* outputting messages. Returned array is guaranteed to not contain `none`s
* or `EPlayer` interfaces with non-existent status.
*/
public final function array<APlayer> GetTargetPlayers()
public final function array<EPlayer> GetTargetPlayers()
{
local array<APlayer> emptyArray;
if (targetType == CWTARGET_None) return emptyArray;
local int i;
local array<EPlayer> result;
if (targetType == CWTARGET_None) return result;
if (targetType == CWTARGET_All) return _.players.GetAll();
return outputTargets;
for (i = 0; i < outputTargets.length; i += 1)
{
if (outputTargets[i].IsExistent()) {
result[result.length] = EPlayer(outputTargets[i].Copy());
}
}
return result;
}
/**
@ -576,8 +594,7 @@ private final function SendBuffer(optional bool asIndented, optional bool asSay)
}
}
// Assumes `playerService != none` and `connectionService != none`,
// caller function must ensure that.
// Assumes `connectionService != none`, caller function must ensure that.
private final function SendConsoleMessage(
array<PlayerController> recipients,
string message,
@ -604,7 +621,6 @@ private final function array<PlayerController> GetRecipientsControllers()
{
local int i;
local PlayerController nextRecipient;
local PlayerService playerService;
local ConnectionService connectionService;
local array<PlayerController> recipients;
local array<ConnectionService.Connection> connections;
@ -612,16 +628,12 @@ private final function array<PlayerController> GetRecipientsControllers()
if (targetType == CWTARGET_None) {
return recipients;
}
// Single target case
// Selected targets case
if (targetType != CWTARGET_All)
{
playerService = PlayerService(class'PlayerService'.static.Require());
if (playerService == none) {
return recipients;
}
for (i = 0; i < outputTargets.length; i += 1)
{
nextRecipient = playerService.GetController(outputTargets[i]);
nextRecipient = outputTargets[i].GetController();
if (nextRecipient != none) {
recipients[recipients.length] = nextRecipient;
}

0
sources/Gameplay/BaseClasses/BaseFrontend.uc → sources/Gameplay/BaseClasses/Frontend/BaseFrontend.uc

104
sources/Gameplay/BaseClasses/Frontend/EInterface.uc

@ -0,0 +1,104 @@
/**
* Base class for all entity interfaces. Entity interface is a reference to
* an entity inside the game world that provides a specific API for
* that entity.
* A single entity is not bound to a single `EInterface` class,
* e.g. a weapon can provide `EWeapon` and `ESellable` interfaces.
* An entity can also have several `EInterface` instances reference it at
* once (including those of the same type). Deallocating one such reference
* should not affect referred entity in any way and should be treated as simply
* getting rid of one of the references.
* Copyright 2021 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 EInterface extends AcediaObject
abstract;
/**
* Makes a copy of the caller interface, producing a new `EInterface` of
* the exactly the same class (`EWeapon` will produce another `EWeapon`).
*
* This should never fail. Even if referred entity is already gone.
*
* @return Copy of the caller `EInterface`, of the exactly the same class.
* Guaranteed to not be `none`.
*/
public function EInterface Copy()
{
return none;
}
/**
* Provides `EInterface` reference of given class `newInterfaceClass` to
* the entity, referred to by the caller `EInterface` (if supported).
*
* Can be used to access entity's other API.
*
* @param newInterfaceClass Class of the new `EInterface` for the entity,
* referred to by the caller `EInterface`.
* @return `EInterface` of the given class `newInterfaceClass` that refers to
* the caller `EInterface`'s entity.
* Can only be `none` if either caller `EInterface`'s entity does not
* support `EInterface` of the specified class or caller `EInterface`
* no longer exists (`self.IsExistent() == false`).
*/
public function EInterface As(class<EInterface> newInterfaceClass)
{
return none;
}
/**
* Checks whether caller `EInterface` refers to the entity that still exists
* in the game world.
*
* Once destroyed, same entity will not come into existence again (but can be
* replaced by its exact copy), so once `EInterface`'s `IsExistent()` call
* returns `false` it will never return `true` again.
*
* `EInterface`'s entity being gone is not the same as that `EInterface` being
* deallocated - deallocation of such `EInterface` still has to be manually
* done.
*
* @return `true` if caller `EInterface` refers to the entity that exists in
* the game world and `false` otherwise.
*/
public function bool IsExistent()
{
return false;
}
/**
* Checks whether caller interface refers to the same entity as
* the `other` argument.
*
* If two `EInterface`s referred to the same entity
* (`SameAs()` returned `true`), but that entity got destroyed,
* these `EInterface`s will not longer be considered "same"
* (`SameAs()` will return false).
*
* @param other `EInterface` to check for referring to the same entity.
* @return `true` if `other` refers to the same entity as the caller
* `EInterface` and `false` otherwise.
*/
public function bool SameAs(EInterface other)
{
return false;
}
defaultproperties
{
}

0
sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc → sources/Gameplay/BaseClasses/KillingFloor/Frontend/KFFrontend.uc

12
sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc → sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ATradingComponent.uc

@ -1,6 +1,6 @@
/**
* Subset of functionality for dealing with everything related to traders.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -58,7 +58,7 @@ public final function SimpleSlot OnStart(AcediaObject receiver)
* Signal that will be emitted whenever trading time ends.
*
* [Signature]
* void <slot>(ATrader oldTrader, ATrader newTrader)
* void <slot>(ETrader oldTrader, ETrader newTrader)
*
* @param oldTrader Trader that was selected before this event.
* @param newTrader Trader that will be selected after this event.
@ -90,7 +90,7 @@ public final function Trading_OnSelect_Slot OnTraderSelected(
* `none`-references. None of them should be deallocated,
* otherwise Acedia's behavior is undefined.
*/
public function array<ATrader> GetTraders();
public function array<ETrader> GetTraders();
/**
* Checks whether trading is currently active.
@ -188,19 +188,19 @@ public function SetCountDownPause(bool doPause);
* Changing a selected trader in any way should always be followed
* by emitting `OnTraderSelected()` signal.
* After `SelectTrader()` call `GetSelectedTrader()` should return
* specified `ATrader`. If selected trader changes in some other way, it should
* specified `ETrader`. If selected trader changes in some other way, it should
* first result in emitted `OnTraderSelected()` signal.
*
* @return Currently selected trader.
*/
public function ATrader GetSelectedTrader();
public function ETrader GetSelectedTrader();
/**
* Changes currently selected trader.
*
* @see `GetSelectedTrader()` for more details.
*/
public function SelectTrader(ATrader newSelection);
public function SelectTrader(ETrader newSelection);
defaultproperties
{

56
sources/Gameplay/BaseClasses/KillingFloor/Trading/ATrader.uc → sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ETrader.uc

@ -2,7 +2,7 @@
* Class, objects of which are expected to represent traders located on
* the map. In classic KF game mode it would represent areas behind closed
* doors that open during trader time and allow to purchase weapons and ammo.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -19,7 +19,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ATrader extends AcediaObject
class ETrader extends EInterface
abstract;
/**
@ -60,7 +60,7 @@ public function Text GetName();
* @param newName New name of the caller trader.
* @return `true` if trader is currently enabled and `false` otherwise.
*/
public function ATrader SetName(Text newName);
public function ETrader SetName(Text newName);
/**
* Checks if caller trader is currently enabled.
@ -78,7 +78,7 @@ public function ATrader SetName(Text newName);
public function bool IsEnabled();
/**
* Sets whether caller `ATrader`'s is currently enabled.
* Sets whether caller `ETrader`'s is currently enabled.
*
* Disabling the trader should automatically "boot" players out
* (see `BootPlayers()`).
@ -87,14 +87,14 @@ public function bool IsEnabled();
*
* @param doEnable `true` if trader is currently enabled and
* `false` otherwise.
* @return Caller `ATrader` to allow for method chaining.
* @return Caller `ETrader` to allow for method chaining.
*/
public function ATrader SetEnabled(bool doEnable);
public function ETrader SetEnabled(bool doEnable);
/**
* Checks whether caller `ATrader` will auto-open when trading gets activated.
* Checks whether caller `ETrader` will auto-open when trading gets activated.
*
* This setting must be ignored if trader is disabled, but disabling `ATrader`
* This setting must be ignored if trader is disabled, but disabling `ETrader`
* should not reset it.
*
* @return `true` if trader is marked to always auto-open upon activating
@ -103,28 +103,28 @@ public function ATrader SetEnabled(bool doEnable);
public function bool IsAutoOpen();
/**
* Checks whether caller `ATrader` will auto-open when trading gets activated.
* Checks whether caller `ETrader` will auto-open when trading gets activated.
*
* @see `IsAutoOpen()` for more info.
*
* @param doAutoOpen `true` if trader should be marked to always auto-open
* upon activating trading and `false` otherwise.
* @return Caller `ATrader` to allow for method chaining.
* @return Caller `ETrader` to allow for method chaining.
*/
public function ATrader SetAutoOpen(bool doAutoOpen);
public function ETrader SetAutoOpen(bool doAutoOpen);
/**
* Checks whether caller `ATrader` is currently open.
* Checks whether caller `ETrader` is currently open.
*
* `ATrader` being open means that players can "enter" (whatever that means for
* an implementation) and use `ATrader` to buy/sell equipment.
* `ETrader` being open means that players can "enter" (whatever that means for
* an implementation) and use `ETrader` to buy/sell equipment.
*
* @return `true` if it is open and `false` otherwise.
*/
public function bool IsOpen();
/**
* Changes whether caller `ATrader` is open.
* Changes whether caller `ETrader` is open.
*
* Closing the trader should not automatically "boot" players out
* (see `BootPlayers()`).
@ -132,27 +132,27 @@ public function bool IsOpen();
* @see `IsOpen()` for more details.
*
* @param doOpen `true` if it is open and `false` otherwise.
* @return Caller `ATrader` to allow for method chaining.
* @return Caller `ETrader` to allow for method chaining.
*/
public function ATrader SetOpen(bool doOpen);
public function ETrader SetOpen(bool doOpen);
/**
* Checks whether caller `ATrader` is currently marked as selected.
* Checks whether caller `ETrader` is currently marked as selected.
*
* @see `ATradingComponent.GetSelectedTrader()` for more details.
*
* @return `true` if caller `ATrader` is selected and `false` otherwise.
* @return `true` if caller `ETrader` is selected and `false` otherwise.
*/
public function bool IsSelected();
/**
* Marks caller `ATrader` as a selected trader.
* Marks caller `ETrader` as a selected trader.
*
* @see `ATradingComponent.GetSelectedTrader()` for more details.
*
* @return Caller `ATrader` to allow for method chaining.
* @return Caller `ETrader` to allow for method chaining.
*/
public function ATrader Select();
public function ETrader Select();
/**
* Removes players from the trader's place.
@ -163,17 +163,17 @@ public function ATrader Select();
* after it is closed. If that is impossible (for traders resembling
* KF2's one), then this method should do nothing.
*
* @return Caller `ATrader` to allow for method chaining.
* @return Caller `ETrader` to allow for method chaining.
*/
public function ATrader BootPlayers();
public function ETrader BootPlayers();
/**
* Shortcut method to open the caller trader, guaranteed to be equivalent to
* `SetOpen(true)`. Provided for better interface.
*
* @return Caller `ATrader` to allow for method chaining.
* @return Caller `ETrader` to allow for method chaining.
*/
public final function ATrader Open()
public final function ETrader Open()
{
SetOpen(true);
return self;
@ -183,9 +183,9 @@ public final function ATrader Open()
* Shortcut method to close the caller trader, guaranteed to be equivalent to
* `SetOpen(false)`. Provided for better interface.
*
* @return Caller `ATrader` to allow for method chaining.
* @return Caller `ETrader` to allow for method chaining.
*/
public final function ATrader Close()
public final function ETrader Close()
{
SetOpen(false);
return self;

4
sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Signal.uc → sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Signal.uc

@ -1,7 +1,7 @@
/**
* Signal class implementation for `ATradingComponent`, for detecting when
* another trader is selected.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -20,7 +20,7 @@
*/
class Trading_OnSelect_Signal extends Signal;
public final function Emit(ATrader oldTrader, ATrader newTrader)
public final function Emit(ETrader oldTrader, ETrader newTrader)
{
local Slot nextSlot;
StartIterating();

4
sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Slot.uc → sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Slot.uc

@ -1,7 +1,7 @@
/**
* Slot class implementation for `ATradingComponent`'s signal for
* detecting when another trader is selected.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -20,7 +20,7 @@
*/
class Trading_OnSelect_Slot extends Slot;
delegate connect(ATrader oldTrader, ATrader newTrader)
delegate connect(ETrader oldTrader, ETrader newTrader)
{
DummyCall();
}

17
sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc

@ -28,30 +28,33 @@ struct DualiesPair
var class<KFWeaponPickup> dual;
};
var private APlayer inventoryOwner;
var private EPlayer inventoryOwner;
var private config array<DualiesPair> dualiesClasses;
protected function Finalizer()
{
_.memory.Free(inventoryOwner);
inventoryOwner = none;
}
public function Initialize(APlayer player)
public function Initialize(EPlayer player)
{
if (inventoryOwner != none) {
return;
}
inventoryOwner = player;
if (player != none) {
inventoryOwner = EPlayer(player.Copy());
}
}
private function Pawn GetOwnerPawn()
{
local PlayerService service;
service = PlayerService(class'PlayerService'.static.Require());
if (service == none) {
local PlayerController myController;
myController = inventoryOwner.GetController();
if (myController == none) {
return none;
}
return service.GetPawn(inventoryOwner);
return myController.pawn;
}
public function bool Add(EItem newItem, optional bool forceAddition)

187
sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc

@ -1,7 +1,7 @@
/**
* `ATrader`'s implementation for `KF1_Frontend`.
* `ETrader`'s implementation for `KF1_Frontend`.
* Wrapper for KF1's `ShopVolume`s.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -18,7 +18,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class KF1_Trader extends ATrader;
class KF1_Trader extends ETrader;
// We do not use any vanilla value as a name, instead storing and tracking it
// entirely as our own value.
@ -26,6 +26,27 @@ var protected Text myName;
// Reference to `ShopVolume` actor that this `KF1_Trader` represents.
var protected NativeActorRef myShopVolume;
// We want to assign each trader (`ShopVolume`) a unique name that does
// not change. For that we will use `namedShopVolumes` array's default value to
// store `ShopVolume`-`Text` pairs.
// Whenever new `KF1_Trader` is created we check with this list to see if
// appropriate name was already assigned.
struct NamedShopVolume
{
var public Text name;
var public NativeActorRef reference;
};
var private array<NamedShopVolume> namedShopVolumes;
protected function Constructor()
{
// We only care about `default` value of this array variable,
// so do not bother storing needless references in each object
if (namedShopVolumes.length > 0) {
namedShopVolumes.length = 0;
}
}
protected function Finalizer()
{
_.memory.Free(myName);
@ -34,40 +55,84 @@ protected function Finalizer()
myName = none;
}
protected static function StaticFinalizer()
{
local int i;
if (default.namedShopVolumes.length <= 0) {
return;
}
for (i = 0; i < default.namedShopVolumes.length; i += 1)
{
__().memory.Free(default.namedShopVolumes[i].name);
__().memory.Free(default.namedShopVolumes[i].reference);
}
default.namedShopVolumes.length = 0;
}
private static function Text GetShopVolumeName(ShopVolume newShopVolume)
{
local int i;
local NamedShopVolume newRecord;
local array<NamedShopVolume> namedShopVolumesCopy;
if (newShopVolume == none) {
return none;
}
namedShopVolumesCopy = default.namedShopVolumes;
for (i = 0; i < namedShopVolumesCopy.length; i += 1)
{
if (namedShopVolumesCopy[i].reference.Get() == newShopVolume) {
return namedShopVolumesCopy[i].name.Copy();
}
}
newRecord.reference = __().unreal.ActorRef(newShopVolume);
newRecord.name =
__().text.FromString("trader" $ namedShopVolumesCopy.length);
default.namedShopVolumes[default.namedShopVolumes.length] = newRecord;
return newRecord.name.Copy();
}
/**
* Detect all existing traders on the level and created a `KF1_Trader` for
* each of them.
* Initializes caller `KF1_Trader`. Should be called right after `KF1_Trader`
* was allocated.
*
* @return Array of created `KF1_Trader`s. All of them are guaranteed to not
* be `none`.
* Every `KF1_Trader` must be initialized, using non-initialized `KF1_Trader`
* instances is invalid.
*
* Initialization can fail if:
* 1. `initShopVolume == none`;
* 2. Caller `KF1_Trader` already was successfully initialized.
* 3. `initShopVolume` is objective-mode only `ShopVolume` and we are
* not running an objective mode right now.
*
* @param initShopVolume `ShopVolume` that caller `KF1_Trader` will
* correspond to.
* @return `true` if initialization was successful and `false` otherwise.
*/
public static function array<KF1_Trader> WrapVanillaShops()
public final /* unreal */ function bool Initialize(ShopVolume initShopVolume)
{
local int shopCounter;
local MutableText textBuilder;
local LevelInfo level;
local KFGameType kfGame;
local KF1_Trader nextTrader;
local array<KF1_Trader> allTraders;
local ShopVolume nextShopVolume;
level = __().unreal.GetLevel();
kfGame = __().unreal.GetKFGameType();
textBuilder = __().text.Empty();
foreach level.AllActors(class'ShopVolume', nextShopVolume)
{
if (nextShopVolume == none) continue;
if (!nextShopVolume.bObjectiveModeOnly || kfGame.bUsingObjectiveMode)
{
nextTrader = KF1_Trader(__().memory.Allocate(class'KF1_Trader'));
nextTrader.myShopVolume = __().unreal.ActorRef(nextShopVolume);
textBuilder.Clear().AppendString("trader" $ shopCounter);
nextTrader.myName = textBuilder.Copy();
allTraders[allTraders.length] = nextTrader;
shopCounter += 1;
if (initShopVolume == none) {
return false;
}
if ( initShopVolume.bObjectiveModeOnly
&& !__().unreal.GetKFGameType().bUsingObjectiveMode) {
return false;
}
myName = GetShopVolumeName(initShopVolume);
myShopVolume = _.unreal.ActorRef(initShopVolume);
return true;
}
/**
* Returns `ShopVolume`, associated with the caller `KF1_Trader`.
*
* @return `ShopVolume`, associated with the caller `KF1_Trader`.
*/
public final /* unreal */ function ShopVolume GetShopVolume()
{
if (myShopVolume == none) {
return none;
}
textBuilder.FreeSelf();
return allTraders;
return ShopVolume(myShopVolume.Get());
}
public function Text GetName()
@ -78,7 +143,8 @@ public function Text GetName()
return myName.Copy();
}
public function ATrader SetName(Text newName)
// TODO: it is broken, needs fixing
public function ETrader SetName(Text newName)
{
if (newName == none) return self;
if (newName.IsEmpty()) return self;
@ -108,7 +174,7 @@ public function bool IsEnabled()
return false;
}
public function ATrader SetEnabled(bool doEnable)
public function ETrader SetEnabled(bool doEnable)
{
local ShopVolume vanillaShopVolume;
vanillaShopVolume = ShopVolume(myShopVolume.Get());
@ -138,7 +204,7 @@ protected function UpdateShopList()
local ShopVolume nextShopVolume;
local KF1_Trader nextTrader;
local array<ShopVolume> shopVolumes;
local array<ATrader> availableTraders;
local array<ETrader> availableTraders;
availableTraders = _.kf.trading.GetTraders();
for (i = 0; i < availableTraders.length; i += 1)
{
@ -163,7 +229,7 @@ public function bool IsAutoOpen()
return false;
}
public function ATrader SetAutoOpen(bool doAutoOpen)
public function ETrader SetAutoOpen(bool doAutoOpen)
{
local ShopVolume vanillaShopVolume;
vanillaShopVolume = ShopVolume(myShopVolume.Get());
@ -189,7 +255,7 @@ public function bool IsOpen()
return false;
}
public function ATrader SetOpen(bool doOpen)
public function ETrader SetOpen(bool doOpen)
{
local ShopVolume vanillaShopVolume;
if (doOpen && !IsEnabled()) return self;
@ -220,7 +286,7 @@ public function bool IsSelected()
return false;
}
public function ATrader Select()
public function ETrader Select()
{
local ShopVolume vanillaShopVolume;
local KFGameReplicationInfo kfGameRI;
@ -235,7 +301,7 @@ public function ATrader Select()
return self;
}
public function ATrader BootPlayers()
public function ETrader BootPlayers()
{
local ShopVolume vanillaShopVolume;
vanillaShopVolume = ShopVolume(myShopVolume.Get());
@ -245,6 +311,51 @@ public function ATrader BootPlayers()
return self;
}
/**
* Makes a copy of the caller interface, producing a new `EInterface` of
* the exactly the same class (`EWeapon` will produce another `EWeapon`).
*
* This should never fail. Even if referred entity is already gone.
*
* @return Copy of the caller `EInterface`, of the exactly the same class.
* Guaranteed to not be `none`.
*/
public function EInterface Copy()
{
local KF1_Trader traderCopy;
traderCopy = KF1_Trader(_.memory.Allocate(class'KF1_Trader'));
if (myShopVolume == none)
{
// Should not really happen, since then caller `KF1_Trader` was
// not initialized
return traderCopy;
}
traderCopy.Initialize(ShopVolume(myShopVolume.Get()));
return traderCopy;
}
public function bool IsExistent()
{
if (myShopVolume == none) {
return false;
}
return (myShopVolume.Get() != none);
}
public function bool SameAs(EInterface other)
{
local KF1_Trader asTrader;
local NativeActorRef otherShopVolume;
if (other == none) return false;
if (myShopVolume == none) return false;
asTrader = KF1_Trader(other);
if (asTrader == none) return false;
otherShopVolume = asTrader.myShopVolume;
if (otherShopVolume == none) return false;
return (myShopVolume.Get() == otherShopVolume.Get());
}
defaultproperties
{
}

93
sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc

@ -1,7 +1,7 @@
/**
* `ATradingComponent`'s implementation for `KF1_Frontend`.
* Only supports `KF1_Trader` as a possible trader class.
* Copyright 2021 Anton Tarasenko
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -28,16 +28,38 @@ var protected int pausedCountDownValue;
// For detecting events of trading becoming active/inactive and selecting
// a different trader, to account for these changing through non-Acedia means
var protected bool wasActiveLastCheck;
var protected Atrader lastSelectedTrader;
var protected ETrader lastSelectedTrader;
// All known traders on map
var protected array<ATrader> registeredTraders;
var protected array<ETrader> registeredTraders;
protected function Constructor()
{
local LevelInfo level;
local KFGameType kfGame;
local KF1_Trader nextTrader;
local ShopVolume nextShopVolume;
super.Constructor();
_.unreal.OnTick(self).connect = Tick;
registeredTraders = class'KF1_Trader'.static.WrapVanillaShops();
// Build `registeredTraders` cache to avoid looking through
// all actors each time
level = __().unreal.GetLevel();
kfGame = __().unreal.GetKFGameType();
foreach level.AllActors(class'ShopVolume', nextShopVolume)
{
if (nextShopVolume == none) {
continue;
}
if (nextShopVolume.bObjectiveModeOnly && !kfGame.bUsingObjectiveMode) {
continue;
}
nextTrader = KF1_Trader(__().memory.Allocate(class'KF1_Trader'));
if (nextTrader.Initialize(nextShopVolume)) {
registeredTraders[registeredTraders.length] = nextTrader;
}
else {
_.memory.Free(nextTrader);
}
}
lastSelectedTrader = GetSelectedTrader();
wasActiveLastCheck = IsTradingActive();
}
@ -52,9 +74,14 @@ protected function Finalizer()
registeredTraders.length = 0;
}
public function array<ATrader> GetTraders()
public function array<ETrader> GetTraders()
{
return registeredTraders;
local int i;
local array<ETrader> result;
for (i = 0; i < registeredTraders.length; i += 1) {
result[i] = ETrader(registeredTraders[i].Copy());
}
return result;
}
public function bool IsTradingActive()
@ -91,21 +118,22 @@ public function SetTradingStatus(bool makeActive)
kfGameRI.maxMonsters = 0;
}
public function ATrader GetSelectedTrader()
public function ETrader GetSelectedTrader()
{
local int i;
for (i = 0; i < registeredTraders.length; i += 1)
{
if (registeredTraders[i].IsSelected()) {
return registeredTraders[i];
return ETrader(registeredTraders[i].Copy());
}
}
return none;
}
public function SelectTrader(ATrader newSelection)
public function SelectTrader(ETrader newSelection)
{
local ATrader oldSelection;
local bool traderChanged;
local ETrader oldSelection;
local KFGameReplicationInfo kfGameRI;
if (newSelection != none) {
newSelection.Select();
@ -121,10 +149,20 @@ public function SelectTrader(ATrader newSelection)
// in case someone decides it would be a grand idea to call `SelectTrader`
// during `onTraderSelectSignal` signal.
oldSelection = lastSelectedTrader;
lastSelectedTrader = newSelection;
if (lastSelectedTrader != newSelection) {
if (newSelection != none)
{
lastSelectedTrader = ETrader(newSelection.Copy());
traderChanged = lastSelectedTrader.SameAs(oldSelection);
}
else
{
lastSelectedTrader = none;
traderChanged = (oldSelection == none);
}
if (traderChanged) {
onTraderSelectSignal.Emit(oldSelection, newSelection);
}
_.memory.Free(oldSelection);
}
public function int GetTradingInterval()
@ -180,25 +218,18 @@ public function SetCountDownPause(bool doPause)
protected function Tick(float delta, float timeScaleCoefficient)
{
local bool isActiveNow;
local ATrader newSelectedTrader;
// Enforce pause
if (tradingCountDownPaused) {
_.unreal.GetKFGameType().waveCountDown = pausedCountDownValue;
}
// Selected trader check
newSelectedTrader = GetSelectedTrader();
if (lastSelectedTrader != newSelectedTrader)
{
onTraderSelectSignal.Emit(lastSelectedTrader, newSelectedTrader);
lastSelectedTrader = newSelectedTrader;
}
CheckNativeTraderSwap();
// Active status check
isActiveNow = IsTradingActive();
if (wasActiveLastCheck != isActiveNow)
{
wasActiveLastCheck = isActiveNow;
if (isActiveNow)
{
if (isActiveNow) {
onStartSignal.Emit();
}
else
@ -210,6 +241,24 @@ protected function Tick(float delta, float timeScaleCoefficient)
}
}
// Detect when selected trader is swapped be swapped by non-Acedia means
protected function CheckNativeTraderSwap()
{
local ETrader newSelectedTrader;
if ( lastSelectedTrader == none
&& _.unreal.GetKFGameRI().currentShop == none) {
return;
}
if (lastSelectedTrader != none && lastSelectedTrader.IsSelected()) {
return;
}
// Currently selected trader actually differs from `lastSelectedTrader`
newSelectedTrader = GetSelectedTrader();
onTraderSelectSignal.Emit(lastSelectedTrader, newSelectedTrader);
_.memory.Free(lastSelectedTrader);
lastSelectedTrader = newSelectedTrader;
}
defaultproperties
{
}

1
sources/Manifest.uc

@ -25,7 +25,6 @@ defaultproperties
features(0) = class'Commands_Feature'
features(1) = class'Avarice_Feature'
services(0) = class'ConnectionService'
services(1) = class'PlayerService'
aliasSources(0) = class'AliasSource'
aliasSources(1) = class'WeaponAliasSource'
aliasSources(2) = class'ColorAliasSource'

196
sources/Players/APlayer.uc → sources/Players/EPlayer.uc

@ -1,12 +1,6 @@
/**
* Represents a connected player connection and serves to provide access to
* both it's server data and in-game pawn representation.
* Unlike `User`, - changes when player reconnects to the server.
* This object SHOULD NOT be created manually, please rely on
* Acedia for that.
* Due to being relatively rarely created, does not use object pools,
* which simplifies their usage and comparison.
* Copyright 2021 Anton Tarasenko
* Provides a common interface to a connected player connection.
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -23,9 +17,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 APlayer extends AcediaObject;
class EPlayer extends EInterface;
// How this `APlayer` is identified by the server
// How this `EPlayer` is identified by the server
var private User identity;
// Writer that can be used to write into this player's console
@ -33,6 +27,7 @@ var private ConsoleWriter consoleInstance;
// Remember version to reallocate writer in case someone deallocates it
var private int consoleLifeVersion;
// `PlayerController` reference
var private NativeActorRef controller;
// These variables record name of this player;
@ -54,21 +49,112 @@ enum AdminStatus
protected function Finalizer()
{
_.memory.Free(controller);
_.memory.Free(consoleInstance);
controller = none;
consoleInstance = none;
// No need to deallocate `User` objects, since they are all have unique
// instance for every player on the server
identity = none;
}
/**
* Returns location of the caller `APlayer`.
* Initializes caller `EPlayer`. Should be called right after `EPlayer`
* was allocated.
*
* @return If caller `APlayer` has a pawn, then it's location will be returned,
* otherwise a location caller `APlayer` is currently spectating the map from
* will be returned.
* Every `EPlayer` must be initialized, using non-initialized `EPlayer`
* instances is invalid.
*
* Initialization can fail if:
* 1. `initController == none`;
* 2. Its id hash (from `GetPlayerIDHash()`) was not properly setup yet
* (not steam id);
* 3. Caller `EPlayer` already was successfully initialized.
*
* @param initController Controller that caller `EPlayer` will correspond to.
* @return `true` if initialization was successful and `false` otherwise.
*/
public final /* unreal */ function bool Initialize(
PlayerController initController)
{
local Text idHash;
local PlayerReplicationInfo myReplicationInfo;
if (controller != none) return false; // Already initialized!
if (initController == none) return false;
if (identity == none)
{
// Only fetch `User` object if it was not yet setup, which can happen
// if `EPlayer` is making a copy and wants to avoid
// re-fetching `identity`
idHash = _.text.FromString(initController.GetPlayerIDHash());
identity = _.users.FetchByIDHash(idHash);
idHash.FreeSelf();
idHash = none;
}
controller = _.unreal.ActorRef(initController);
myReplicationInfo = initController.playerReplicationInfo;
// Hash current name
if (myReplicationInfo != none)
{
hashedName = myReplicationInfo.playerName;
textName = _.text.FromColoredString(hashedName);
}
return true;
}
public function bool IsExistent()
{
if (controller == none) {
return false;
}
return (controller.Get() != none);
}
public function EInterface Copy()
{
local EPlayer playerCopy;
playerCopy = EPlayer(_.memory.Allocate(class'EPlayer'));
if (controller == none)
{
// Should not really happen, since then caller `EPlayer` was
// not initialized
return playerCopy;
}
playerCopy.identity = identity;
playerCopy.Initialize(PlayerController(controller.Get()));
return playerCopy;
}
public function bool SameAs(EInterface other)
{
local EPlayer asPlayer;
local NativeActorRef otherController;
if (other == none) return false;
if (controller == none) return false;
asPlayer = EPlayer(other);
if (asPlayer != none) return false;
otherController = asPlayer.controller;
if (otherController == none) return false;
return (controller.Get() == otherController.Get());
}
/**
* Returns location of the caller `EPlayer`.
*
* If caller `EPlayer` has a pawn, then it's location will be returned,
* otherwise a location from which caller `EPlayer` is currently spectating is
* considered caller's location.
*
* @return Location of the caller `EPlayer` has a pawn.
*/
public final function Vector GetLocation()
{
local Pawn myPawn;
local PlayerController myController;
if (controller != none) {
myController = PlayerController(controller.Get());
}
if (myController != none)
{
myPawn = myController.pawn;
@ -80,68 +166,38 @@ public final function Vector GetLocation()
return Vect(0.0, 0.0, 0.0);
}
// `PlayerReplicationInfo` associated with the caller `APLayer`.
// `PlayerReplicationInfo` associated with the caller `EPlayer`.
// Can return `none` if:
// 1. Caller `APlayer` has already disconnected;
// 1. Caller `EPlayer` has already disconnected;
// 2. It was not properly initialized;
// 3. There is an issue running `PlayerService`.
private final function PlayerReplicationInfo GetRI()
{
local PlayerController myController;
if (controller == none) return none;
myController = PlayerController(controller.Get());
if (myController != none) {
return myController.playerReplicationInfo;
}
return none;
}
if (myController == none) return none;
/**
* Checks if player, corresponding to `APlayer`, is still connected to
* the server. If player is disconnected - `APlayer` instance should be
* considered useless.
*
* @return `true` if player is connected and `false` otherwise.
*/
public final function bool IsConnected()
{
return (controller.Get() != none);
return myController.playerReplicationInfo;
}
/**
* Initializes caller `APlayer`. Should be called right after `APlayer`
* was spawned.
* Returns `PlayerController`, associated with the caller `EPlayer`.
*
* Initialization should (and can) only be done once.
* Before a `Initialize()` call, any other method calls on such `User`
* must be considerate to have undefined behavior.
*
* @param newController Controller that caller `APlayer` will correspond to.
* @return `PlayerController`, associated with the caller `EPlayer`.
*/
public final function Initialize(Text idHash)
public final /* unreal */ function PlayerController GetController()
{
local PlayerService service;
local PlayerController myController;
local PlayerReplicationInfo myReplicationInfo;
identity = _.users.FetchByIDHash(idHash);
// Retrieve controller and replication info
service = PlayerService(class'PlayerService'.static.Require());
myController = service.GetController(self);
controller = _.unreal.ActorRef(myController);
if (myController != none) {
myReplicationInfo = myController.playerReplicationInfo;
}
// Hash current name
if (myReplicationInfo != none) {
hashedName = myReplicationInfo.playerName;
textName = _.text.FromColoredString(hashedName);
if (controller == none) {
return none;
}
return PlayerController(controller.Get());
}
/**
* Returns `User` object that is corresponding to the caller `APlayer`.
* Returns `User` object that is corresponding to the caller `EPlayer`.
*
* @return `User` corresponding to the caller `APlayer`. Guarantee to be
* not `none` for correctly initialized `APlayer` (it remembers `User`
* @return `User` corresponding to the caller `EPlayer`. Guarantee to be
* not `none` for correctly initialized `EPlayer` (it remembers `User`
* record even if player has disconnected).
*/
public final function User GetIdentity()
@ -154,7 +210,7 @@ public final function User GetIdentity()
*
* @return `Text` containing current name of the caller player.
* Guaranteed to not be `none`. Returned object is not managed by caller
* `APlayer` and should be manually deallocated.
* `EPlayer` and should be manually deallocated.
*/
public final function Text GetName()
{
@ -173,9 +229,9 @@ public final function Text GetName()
}
/**
* Set new displayed name for the caller `APlayer`.
* Set new displayed name for the caller `EPlayer`.
*
* @param newPlayerName New name of the caller `APlayer`. This value will
* @param newPlayerName New name of the caller `EPlayer`. This value will
* be copied. Passing `none` will result in an empty name.
*/
public final function SetName(Text newPlayerName)
@ -237,7 +293,7 @@ public final function EInventory GetInventory()
* Different from `IsAdmin()` since this method allows to distinguish between
* different types of admin login (like silent admins).
*
* @return Admin status of the caller `APLayer`.
* @return Admin status of the caller `EPlayer`.
*/
public final function AdminStatus GetAdminStatus()
{
@ -271,10 +327,10 @@ public final function bool IsAdmin()
}
/**
* Changes admin status of the caller `APlayer`.
* Can only fail if caller `APlayer` has already disconnected.
* Changes admin status of the caller `EPlayer`.
* Can only fail if caller `EPlayer` has already disconnected.
*
* @param newAdminStatus New admin status of the `APlayer`.
* @param newAdminStatus New admin status of the `EPlayer`.
*/
public final function SetAdminStatus(AdminStatus newAdminStatus)
{
@ -300,9 +356,9 @@ public final function SetAdminStatus(AdminStatus newAdminStatus)
}
/**
* Returns current amount of money caller `APlayer` has.
* Returns current amount of money caller `EPlayer` has.
*
* @return Amount of money `APlayer` has. If player has already disconnected
* @return Amount of money `EPlayer` has. If player has already disconnected
* method will return `0`.
*/
public final function int GetDosh()
@ -316,9 +372,9 @@ public final function int GetDosh()
}
/**
* Sets amount of money that caller `APlayer` will have.
* Sets amount of money that caller `EPlayer` will have.
*
* @param newDoshAmount New amount of money that caller `APlayer` must have.
* @param newDoshAmount New amount of money that caller `EPlayer` must have.
*/
public final function SetDosh(int newDoshAmount)
{
@ -340,7 +396,7 @@ public final function SetDosh(int newDoshAmount)
* console. Returned object should not be deallocated, but it is
* guaranteed to be valid for non-disconnected players.
*/
public final function ConsoleWriter Console()
public final function /* borrow */ ConsoleWriter BorrowConsole()
{
if ( consoleInstance == none
|| consoleInstance.GetLifeVersion() != consoleLifeVersion)

38
sources/Players/Events/PlayerAPI_OnLostPlayer_Signal.uc

@ -0,0 +1,38 @@
/**
* Signal class implementation for `PlayerAPI`'s `OnLostPlayer` signal.
* Copyright 2021 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 PlayerAPI_OnLostPlayer_Signal extends Signal;
public final function Emit(User identity)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
PlayerAPI_OnLostPlayer_Slot(nextSlot).connect(identity);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'PlayerAPI_OnLostPlayer_Slot'
}

22
sources/Gameplay/BaseClasses/BaseBackend.uc → sources/Players/Events/PlayerAPI_OnLostPlayer_Slot.uc

@ -1,6 +1,5 @@
/**
* Base class for all backends. Does not define anything meaningful, which
* also means it does not put any limitations on it's implementation.
* Slot class implementation for `PlayerAPI`'s `OnLostPlayer` signal.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@ -18,8 +17,23 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class BaseBackend extends AcediaObject
abstract;
class PlayerAPI_OnLostPlayer_Slot extends Slot;
delegate connect(User identity)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{

38
sources/Players/Events/PlayerAPI_OnNewPlayer_Signal.uc

@ -0,0 +1,38 @@
/**
* Signal class implementation for `PlayerAPI`'s `OnNewPlayer` signal.
* Copyright 2021 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 PlayerAPI_OnNewPlayer_Signal extends Signal;
public final function Emit(EPlayer newPlayer)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
PlayerAPI_OnNewPlayer_Slot(nextSlot).connect(EPlayer(newPlayer.Copy()));
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'PlayerAPI_OnNewPlayer_Slot'
}

40
sources/Players/Events/PlayerAPI_OnNewPlayer_Slot.uc

@ -0,0 +1,40 @@
/**
* Slot class implementation for `PlayerAPI`'s `OnNewPlayer` signal.
* Copyright 2021 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 PlayerAPI_OnNewPlayer_Slot extends Slot;
delegate connect(EPlayer newPlayer)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

4
sources/Players/Inventory/EInventory.uc

@ -33,9 +33,9 @@ class EInventory extends AcediaObject
* Cannot fail for any connected player and can assume it will not be called
* for not connected ones.
*
* @param player `APlayer` for which to initialize this inventory.
* @param player `EPlayer` for which to initialize this inventory.
*/
public function Initialize(APlayer player) {}
public function Initialize(EPlayer player) {}
/**
* Adds passed `EItem` to the caller inventory system.

183
sources/Players/PlayerService.uc

@ -1,183 +0,0 @@
/**
* Service for tracking currently connected players and remembering what
* `APlayer` is connected to what `PlayerController` (`PlayerController`
* instance is an `Actor` and therefore should not be stores as `APlayer`'s
* variable, since `APlayer` is not an `Actor`).
* Copyright 2021 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 PlayerService extends Service;
// Used to 1-to-1 associate `APlayer` objects with `PlayerController` actors.
struct PlayerControllerPair
{
var APlayer player;
var PlayerController controller;
};
// Records of all known pairs
var private array<PlayerControllerPair> allPlayers;
protected function Contructor()
{
SetTimer(1.0, true);
}
protected function Finalizer()
{
SetTimer(0.0, false);
}
/**
* Creates a new `APlayer` instance for a given `newPlayerController`
* controller.
*
* If given controller is `none` or it's `APlayer` was already created,
* - does nothing.
*
* @param newPlayerController Controller for which we must
* create new `APlayer`.
* @return `true` if new `APlayer` was created and `false` otherwise.
*/
public final function bool RegisterPair(
PlayerController newController,
APlayer newPlayer)
{
local int i;
local PlayerControllerPair newPair;
if (newController == none) return false;
if (newPlayer == none) return false;
for (i = 0; i < allPlayers.length; i += 1)
{
if (allPlayers[i].controller == newController) {
return false;
}
if (allPlayers[i].player == newPlayer) {
return false;
}
}
// Record new pair in service's data
newPair.controller = newController;
newPair.player = newPlayer;
allPlayers[allPlayers.length] = newPair;
return true;
}
/**
* Fetches current array of all players (registered `APlayer`s).
*
* @return Current array of all players (registered `APlayer`s). Guaranteed to
* not contain `none` values.
*/
public final function array<APlayer> GetAllPlayers()
{
local int i;
local array<APlayer> result;
for (i = 0; i < allPlayers.length; i += 1)
{
if (allPlayers[i].controller != none) {
result[result.length] = allPlayers[i].player;
}
}
return result;
}
/**
* Returns `APlayer` associated with a given `PlayerController`.
*
* @param controller Controller for which we want to find associated player.
* @return `APlayer` that is associated with a given `PlayerController`.
* Can return `none` if player has already "expired".
*/
public final function APlayer GetPlayer(Controller controller)
{
local int i;
if (controller == none) {
return none;
}
for (i = 0; i < allPlayers.length; i += 1)
{
if (controller == allPlayers[i].controller) {
return allPlayers[i].player;
}
}
return none;
}
/**
* Returns `PlayerController` associated with a given `APlayer`.
*
* @param player Player for which we want to find associated `Controller`.
* @return Controller that is associated with a given player.
* Can return `none` if controller has already "expired".
*/
public final function PlayerController GetController(APlayer player)
{
local int i;
if (player == none) {
return none;
}
for (i = 0; i < allPlayers.length; i += 1)
{
if (player == allPlayers[i].player) {
return allPlayers[i].controller;
}
}
return none;
}
/**
* Returns `Pawn` associated with a given `APlayer`.
*
* @param player Player for which we want to find associated `Pawn`.
* @return `Pawn` that is associated with a given player.
* Can return `none` if controller has already "expired".
*/
public final function Pawn GetPawn(APlayer player)
{
local Controller controller;
controller = GetController(player);
if (controller != none) {
return controller.pawn;
}
return none;
}
/**
* IMPORTANT: this is a helper function that is not supposed to be
* called manually.
*
* Causes status of all players to update.
* See `APlayer.Update()` for details.
*/
event Timer()
{
local int i;
while (i < allPlayers.length)
{
if (allPlayers[i].controller == none || allPlayers[i].player == none) {
allPlayers.Remove(i, 1);
}
else {
i += 1;
}
}
}
defaultproperties
{
}

148
sources/Players/PlayersAPI.uc

@ -1,5 +1,5 @@
/**
* API that provides functions for working player references (`APlayer`).
* API that provides functions for working player references (`EPlayer`).
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@ -18,6 +18,7 @@
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class PlayersAPI extends AcediaObject
dependson(ConnectionService)
dependson(Text);
// Writer that can be used to write into this player's console
@ -25,41 +26,76 @@ var private ConsoleWriter consoleInstance;
// Remember version to reallocate writer in case someone deallocates it
var private int consoleLifeVersion;
var protected bool connectedToConnectionServer;
var protected PlayerAPI_OnNewPlayer_Signal onNewPlayerSignal;
var protected PlayerAPI_OnLostPlayer_Signal onLostPlayerSignal;
protected function Constructor()
{
local ConnectionService service;
service = ConnectionService(class'ConnectionService'.static.Require());
service.OnConnectionEstablished(self).connect = MakePlayer;
onNewPlayerSignal = PlayerAPI_OnNewPlayer_Signal(
_.memory.Allocate(class'PlayerAPI_OnNewPlayer_Signal'));
onLostPlayerSignal = PlayerAPI_OnLostPlayer_Signal(
_.memory.Allocate(class'PlayerAPI_OnLostPlayer_Signal'));
}
protected function Finalizer()
{
local ConnectionService service;
connectedToConnectionServer = false;
if (class'ConnectionService'.static.IsRunning())
{
service = ConnectionService(class'ConnectionService'.static.Require());
service.OnConnectionEstablished(self).Disconnect();
service.OnConnectionLost(self).Disconnect();
}
_.memory.Free(onNewPlayerSignal);
_.memory.Free(onLostPlayerSignal);
onNewPlayerSignal = none;
onLostPlayerSignal = none;
}
/**
* Signal that will be emitted once new player is connected.
*
* [Signature]
* void <slot>(EPlayer newPlayer)
*
* @param handle Base `EPlayer` interface for the newly connected player.
* Each handler will receive its own copy of `EPlayer` that has to
* be deallocated.
*/
/* SIGNAL */
public function PlayerAPI_OnNewPlayer_Slot OnNewPlayer(
AcediaObject receiver)
{
ConnectToConnectionService();
return PlayerAPI_OnNewPlayer_Slot(onNewPlayerSignal.NewSlot(receiver));
}
private final function MakePlayer(ConnectionService.Connection newConnection)
/**
* Signal that will be emitted once player has disconnected.
*
* [Signature]
* void <slot>(User identity)
*
* @param identity `User` object that corresponds to
* the disconnected player.
*/
/* SIGNAL */
public function PlayerAPI_OnLostPlayer_Slot OnLostPlayerHandle(
AcediaObject receiver)
{
local APlayer newPlayer;
local Text textIdHash;
local PlayerService service;
// Make new player controller and link it to `newConnection`
newPlayer = APlayer(_.memory.Allocate(class'APlayer'));
service = PlayerService(class'PlayerService'.static.Require());
service.RegisterPair(newConnection.controllerReference, newPlayer);
// Initialize new `APlayer`
textIdHash = _.text.FromString(newConnection.idHash);
newPlayer.Initialize(textIdHash);
textIdHash.FreeSelf();
ConnectToConnectionService();
return PlayerAPI_OnLostPlayer_Slot(onLostPlayerSignal.NewSlot(receiver));
}
/**
* Return `ConsoleWriter` that can be used to write into every player's
* console.
*
* Provided that returned object is never deallocated - returns the same object
* with each call, otherwise can allocate new instance of `ConsoleWriter`.
* Provided that returned object is never deallocated - method returns
* the same object with each call, otherwise can allocate new instance of
* `ConsoleWriter`.
*
* @return `ConsoleWriter` that can be used to write into every player's
* console. Returned object should not be deallocated, but it is
@ -77,21 +113,83 @@ public final function ConsoleWriter Console()
return consoleInstance.ForAll();
}
private final function ConnectToConnectionService()
{
local ConnectionService service;
if (connectedToConnectionServer) {
return;
}
service = ConnectionService(class'ConnectionService'.static.Require());
service.OnConnectionEstablished(self).connect = AnnounceNewPlayer;
service.OnConnectionLost(self).connect = AnnounceLostPlayer;
connectedToConnectionServer = true;
}
private final function AnnounceNewPlayer(
ConnectionService.Connection newConnection)
{
if (onNewPlayerSignal == none) {
return;
}
onNewPlayerSignal.Emit(FromController(newConnection.controllerReference));
}
private final function AnnounceLostPlayer(
ConnectionService.Connection lostConnection)
{
local Text idHash;
local User lostIdentity;
if (onLostPlayerSignal == none) {
return;
}
idHash = _.text.FromString(lostConnection.idHash);
lostIdentity = _.users.FetchByIDHash(idHash);
_.memory.Free(idHash);
idHash = none;
onLostPlayerSignal.Emit(lostIdentity);
}
/**
* Creates `EPlayer` instance from passed `PlayerController` reference.
* Can fail if passed parameter is not `controller`.
*
* @param controller `PlayerController` for which to create
* `EPlayer` instance. Should not be `none`.
* @return Instance of `EPlayer` that refers to passed `controller`.
* Returns `none` iff `controller == none`.
*/
public final /* unreal */ function EPlayer FromController(
PlayerController controller)
{
local EPlayer result;
result = EPlayer(_.memory.Allocate(class'EPlayer'));
result.Initialize(controller);
return result;
}
/**
* Fetches current array of all players.
*
* @return Current array of all players.
* Guaranteed to not contain `none` values.
*/
public final function array<APlayer> GetAll()
public final function array<EPlayer> GetAll()
{
local PlayerService service;
local array<APlayer> emptyResult;
service = PlayerService(class'PlayerService'.static.GetInstance());
if (service != none) {
return service.GetAllPlayers();
local int i;
local array<EPlayer> result;
local ConnectionService service;
local array<ConnectionService.Connection> activeConnections;
local PlayerController nextControllerReference;
service = ConnectionService(class'ConnectionService'.static.Require());
activeConnections = service.GetActiveConnections();
for (i = 0; i < activeConnections.length; i += 1)
{
nextControllerReference = activeConnections[i].controllerReference;
if (nextControllerReference != none) {
result[result.length] = FromController(nextControllerReference);
}
}
return emptyResult;
return result;
}
defaultproperties

2
sources/Unreal/Connections/ConnectionService.uc

@ -58,7 +58,7 @@ public final function Connection_Slot OnConnectionEstablished(
* Signal that will be emitted when the player connection is lost.
*
* [Signature]
* void <slot>(ConnectionService.Connection newConnection)
* void <slot>(ConnectionService.Connection lostConnection)
*
* @param newConnection Structure that describes lost connection.
*/

3
sources/Users/User.uc

@ -34,9 +34,10 @@ var private JSONPointer persistentSettingsPointer;
var private LoggerAPI.Definition errNoUserDataDatabase;
// TODO: redo this comment
/**
* Initializes caller `User` with id and it's session key. Should be called
* right after `APlayer` was created.
* right after `EPlayer` was created.
*
* Initialization should (and can) only be done once.
* Before a `Initialize()` call, any other method calls on such `User`

Loading…
Cancel
Save