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. 20
      sources/Color/ColorAPI.uc
  2. 33
      sources/Commands/BuiltInCommands/ACommandHelp.uc
  3. 4
      sources/Commands/BuiltInCommands/ACommandTest.uc
  4. 252
      sources/Commands/Command.uc
  5. 393
      sources/Commands/CommandCall.uc
  6. 67
      sources/Commands/CommandParser.uc
  7. 31
      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. 82
      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. 191
      sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc
  21. 95
      sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc
  22. 1
      sources/Manifest.uc
  23. 200
      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. 152
      sources/Players/PlayersAPI.uc
  31. 2
      sources/Unreal/Connections/ConnectionService.uc
  32. 3
      sources/Users/User.uc

20
sources/Color/ColorAPI.uc

@ -899,7 +899,7 @@ private final function Color ParseRGBA(Parser parser)
if (!parser.Ok()) if (!parser.Ok())
{ {
parser.RestoreState(initialParserState) parser.RestoreState(initialParserState)
.Match(T(TRGBA), SCASE_INSENSITIVE) .Match(T(TRGBA), SCASE_INSENSITIVE)
.Match(T(TR_COMPONENT), SCASE_INSENSITIVE) .Match(T(TR_COMPONENT), SCASE_INSENSITIVE)
.MInteger(redComponent).Match(T(TCOMMA)) .MInteger(redComponent).Match(T(TCOMMA))
.Match(T(TG_COMPONENT), SCASE_INSENSITIVE) .Match(T(TG_COMPONENT), SCASE_INSENSITIVE)
@ -1472,15 +1472,15 @@ defaultproperties
browndarken4=(R=62,G=39,B=35,A=255) browndarken4=(R=62,G=39,B=35,A=255)
bluegrey=(R=96,G=125,B=139,A=255) bluegrey=(R=96,G=125,B=139,A=255)
vuebluegrey=(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) bluegreylighten5=(R=236,G=239,B=241,A=255)
bluegreylighten4#rgb(R=207,G=216,B=220,A=255) bluegreylighten4=(R=207,G=216,B=220,A=255)
bluegreylighten3#rgb(R=176,G=190,B=197,A=255) bluegreylighten3=(R=176,G=190,B=197,A=255)
bluegreylighten2#rgb(R=144,G=164,B=174,A=255) bluegreylighten2=(R=144,G=164,B=174,A=255)
bluegreylighten1#rgb(R=120,G=144,B=156,A=255) bluegreylighten1=(R=120,G=144,B=156,A=255)
bluegreydarken1#rgb(R=84,G=110,B=122,A=255) bluegreydarken1=(R=84,G=110,B=122,A=255)
bluegreydarken2#rgb(R=69,G=90,B=100,A=255) bluegreydarken2=(R=69,G=90,B=100,A=255)
bluegreydarken3#rgb(R=55,G=71,B=79,A=255) bluegreydarken3=(R=55,G=71,B=79,A=255)
bluegreydarken4#rgb(R=38,G=50,B=56,A=255) bluegreydarken4=(R=38,G=50,B=56,A=255)
grey=(R=158,G=158,B=158,A=255) grey=(R=158,G=158,B=158,A=255)
vuegrey=(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) 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. * 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. * This file is part of Acedia.
* *
@ -40,18 +40,14 @@ protected function BuildData(CommandDataBuilder builder)
.Describe(P("Display list of all available commands.")); .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 DynamicArray commandsToDisplay;
local APlayer callerPlayer; parameters = callData.parameters;
callerPlayer = callInfo.GetCallerPlayer(); options = callData.options;
if (callerPlayer == none) {
return;
}
// Print command list if "--list" option was specified // Print command list if "--list" option was specified
if (callInfo.GetOptions().HasKey(P("list"))) { if (options.HasKey(P("list"))) {
DisplayCommandList(callerPlayer); DisplayCommandList(callerPlayer);
} }
// Help pages. // Help pages.
@ -59,16 +55,17 @@ protected function Executed(CommandCall callInfo)
// 1. Any commands are specified as parameters; // 1. Any commands are specified as parameters;
// 2. No commands or "--list" option was specified, then we want to // 2. No commands or "--list" option was specified, then we want to
// print a help page for this command. // print a help page for this command.
if ( !callInfo.GetOptions().HasKey(P("list")) if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
|| callInfo.GetParameters().HasKey(P("commands")))
{ {
parameters = callInfo.GetParameters();
commandsToDisplay = DynamicArray(parameters.GetItem(P("commands"))); commandsToDisplay = DynamicArray(parameters.GetItem(P("commands")));
DisplayCommandHelpPages(callerPlayer, commandsToDisplay); 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 int i;
local ConsoleWriter console; local ConsoleWriter console;
@ -81,7 +78,7 @@ private final function DisplayCommandList(APlayer player)
Commands_Feature(class'Commands_Feature'.static.GetInstance()); Commands_Feature(class'Commands_Feature'.static.GetInstance());
if (commandsFeature == none) return; if (commandsFeature == none) return;
console = player.Console(); console = player.BorrowConsole();
commandNames = commandsFeature.GetCommandNames(); commandNames = commandsFeature.GetCommandNames();
for (i = 0; i < commandNames.length; i += 1) for (i = 0; i < commandNames.length; i += 1)
{ {
@ -99,7 +96,7 @@ private final function DisplayCommandList(APlayer player)
} }
private final function DisplayCommandHelpPages( private final function DisplayCommandHelpPages(
APlayer player, EPlayer player,
DynamicArray commandList) DynamicArray commandList)
{ {
local int i; local int i;
@ -113,7 +110,7 @@ private final function DisplayCommandHelpPages(
// If arguments were empty - at least display our own help page // If arguments were empty - at least display our own help page
if (commandList == none) if (commandList == none)
{ {
PrintHelpPage(player.Console(), GetData()); PrintHelpPage(player.BorrowConsole(), GetData());
return; return;
} }
// Otherwise - print help for specified commands // Otherwise - print help for specified commands
@ -121,7 +118,7 @@ private final function DisplayCommandHelpPages(
{ {
nextCommand = commandsFeature.GetCommand(Text(commandList.GetItem(i))); nextCommand = commandsFeature.GetCommand(Text(commandList.GetItem(i)));
if (nextCommand == none) continue; 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")); .ParamText(P("option"));
} }
protected function Executed(CommandCall result) protected function Executed(Command.CallData result, EPlayer callerPlayer)
{ {
local Parser parser; local Parser parser;
local AssociativeArray root; 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!\"}"); 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); root = _.json.ParseObjectWith(parser);
result.GetCallerPlayer().Console().WriteLine(_.json.PrettyPrint(root)); callerPlayer.BorrowConsole().WriteLine(_.json.PrettyPrint(root));
} }
defaultproperties defaultproperties

252
sources/Commands/Command.uc

@ -1,9 +1,12 @@
/** /**
* This class is meant to represent a command type: to create new command * 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 * one should extend it, then simply define required sub-commands/options and
* parameters in `BuildData()` and use `Execute()` / `ExecuteFor()` to perform * parameters in `BuildData()` and overload `Execute()` / `ExecuteFor()`
* necessary actions when command is executed by a player. * to perform required actions when command is executed by a player.
* Copyright 2021 Anton Tarasenko * `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. * This file is part of Acedia.
* *
@ -24,7 +27,8 @@ class Command extends AcediaObject
dependson(Text); 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 enum ErrorType
{ {
@ -56,6 +60,25 @@ enum ErrorType
CET_EmptyTargetList 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. * Possible types of parameters.
*/ */
@ -216,25 +239,32 @@ private final function CleanParameters(array<Parameter> parameters)
protected function BuildData(CommandDataBuilder builder){} 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 * @param callData `struct` filled with parameters that your command
* been called with. Guaranteed to not be in error state. * 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 - * with a given player as a target. If several players have been specified -
* this method will be called once for each. * this method will be called once for each.
* *
* If your command does not require a target - this method will not be called. * 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 targetPlayer Player that this command must perform an action on.
* @param callInfo Object filled with parameters that your command has * @param callData `struct` filled with parameters that your command
* been called with. Guaranteed to not be in error state. * 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 * 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) * Forces command to process (parse) player's input, producing a structure
* player's input. * with parsed data in Acedia's format instead.
* *
* @param parser Parser that contains player's input. * @see `Execute()` for actually performing command's actions.
* @param callerPlayer Player that initiated this command's call. *
* @return `CommandCall` object that described parsed command call. * @param parser Parser that contains command input.
* Guaranteed to be not `none`. * @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, Parser parser,
APlayer callerPlayer) EPlayer callerPlayer)
{ {
local int i; local array<EPlayer> targetPlayers;
local array<APlayer> targetPlayers;
local CommandParser commandParser; local CommandParser commandParser;
local CommandCall callInfo; local CallData callData;
if (parser == none || !parser.Ok()) { if (parser == none || !parser.Ok())
return MakeAndReportError(callerPlayer, CET_BadParser); {
callData.parsingError = CET_BadParser;
return callData;
} }
// Parse targets and handle errors that can arise here // Parse targets and handle errors that can arise here
if (commandData.requiresTarget) if (commandData.requiresTarget)
{ {
targetPlayers = ParseTargets(parser, callerPlayer); targetPlayers = ParseTargets(parser, callerPlayer);
if (!parser.Ok()) { if (!parser.Ok())
return MakeAndReportError(callerPlayer, CET_IncorrectTargetList); {
callData.parsingError = CET_IncorrectTargetList;
return callData;
} }
if (targetPlayers.length <= 0) { if (targetPlayers.length <= 0)
return MakeAndReportError(callerPlayer, CET_EmptyTargetList); {
callData.parsingError = CET_EmptyTargetList;
return callData;
} }
} }
// Parse parameters themselves // Parse parameters themselves
commandParser = CommandParser(_.memory.Allocate(class'CommandParser')); commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
callInfo = commandParser.ParseWith(parser, commandData) callData = commandParser.ParseWith(parser, commandData);
.SetCallerPlayer(callerPlayer) callData.targetPlayers = targetPlayers;
.SetTargetPlayers(targetPlayers);
commandParser.FreeSelf(); 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 // Report or execute
if (!callInfo.IsSuccessful()) if (callData.parsingError != CET_None)
{ {
ReportError(callerPlayer, callInfo); ReportError(callData, callerPlayer);
return callInfo; return false;
} }
Executed(callInfo); Executed(callData, callerPlayer);
if (commandData.requiresTarget) if (commandData.requiresTarget)
{ {
targetPlayers = callData.targetPlayers;
for (i = 0; i < targetPlayers.length; i += 1) { 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 // Reports given error to the `callerPlayer`, appropriately picking
// message color // message color
private final function ReportError( private final function ReportError(CallData callData, EPlayer callerPlayer)
APLayer callerPlayer,
CommandCall callInfo)
{ {
local Text errorMessage; local Text errorMessage;
local ConsoleWriter console; local ConsoleWriter console;
if (callerPlayer == none) return; if (callerPlayer == none) return;
if (callInfo == none) return; if (!callerPlayer.IsExistent()) return;
if (callInfo.IsSuccessful()) return;
// Setup console color // Setup console color
console = callerPlayer.Console(); console = callerPlayer.BorrowConsole();
if (callInfo.GetError() == CET_EmptyTargetList) { if (callData.parsingError == CET_EmptyTargetList) {
console.UseColor(_.color.textWarning); console.UseColor(_.color.textWarning);
} }
else { else {
console.UseColor(_.color.textFailure); console.UseColor(_.color.textFailure);
} }
// Send message // Send message
errorMessage = callInfo.PrintErrorMessage(); errorMessage = PrintErrorMessage(callData);
console.Say(errorMessage); console.Say(errorMessage);
errorMessage.FreeSelf(); errorMessage.FreeSelf();
// Restore console color // Restore console color
console.ResetColor().Flush(); console.ResetColor().Flush();
} }
// Creates (and returns) empty `CommandCall` with given error type and private final function Text PrintErrorMessage(CallData callData)
// empty error cause and reports it
private final function CommandCall MakeAndReportError(
APLayer callerPlayer,
ErrorType errorType)
{ {
local CommandCall dummyCall; local Text result;
if (errorType == CET_None) return none; local MutableText builder;
builder = _.text.Empty();
dummyCall = class'CommandCall'.static.MakeError(errorType, callerPlayer); switch (callData.parsingError)
ReportError(callerPlayer, dummyCall); {
return dummyCall; 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. // Auxiliary method for parsing list of targeted players.
// Assumes given parser is not `none` and not in a failed state. // 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, Parser parser,
APlayer callerPlayer) EPlayer callerPlayer)
{ {
local array<APlayer> targetPlayers; local array<EPlayer> targetPlayers;
local PlayersParser targetsParser; local PlayersParser targetsParser;
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser')); targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
targetsParser.SetSelf(callerPlayer); targetsParser.SetSelf(callerPlayer);
targetsParser.ParseWith(parser); targetsParser.ParseWith(parser);
targetPlayers = targetsParser.GetPlayers(); if (parser.Ok()) {
targetPlayers = targetsParser.GetPlayers();
}
targetsParser.FreeSelf(); targetsParser.FreeSelf();
return targetPlayers; return targetPlayers;
} }
@ -384,6 +513,7 @@ public final function Text GetName()
return commandData.name.LowerCopy(); return commandData.name.LowerCopy();
} }
// TODO: use `SharedRef` instead
/** /**
* Returns `Command.Data` struct that describes caller `Command`. * 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."
}

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

31
sources/Commands/Commands_Feature.uc

@ -3,7 +3,7 @@
* parse their arguments into standard Acedia collection. It also allows to * parse their arguments into standard Acedia collection. It also allows to
* manage them (and specify limitation on how they can be called) in a * manage them (and specify limitation on how they can be called) in a
* centralized manner. * centralized manner.
* Copyright 2021 Anton Tarasenko * Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -202,18 +202,22 @@ public final function array<Text> GetCommandNames()
* contain command's name and it's parameters. * contain command's name and it's parameters.
* @param callerPlayer Player that caused this command call. * @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 commandInstance;
local MutableText commandName; local Command.CallData callData;
local MutableText commandName;
if (parser == none) return; if (parser == none) return;
if (!parser.Ok()) return; if (!parser.Ok()) return;
parser.MUntilMany(commandName, commandDelimiters, true, true); parser.MUntilMany(commandName, commandDelimiters, true, true);
commandInstance = GetCommand(commandName); commandInstance = GetCommand(commandName);
commandName.FreeSelf(); commandName.FreeSelf();
if (parser.Ok() && commandInstance != none) { if (parser.Ok() && commandInstance != none)
commandInstance.ProcessInput(parser, callerPlayer).FreeSelf(); {
callData = commandInstance.ParseInputWith(parser, callerPlayer);
commandInstance.Execute(callData, callerPlayer);
commandInstance.DeallocateCallData(callData);
} }
} }
@ -223,10 +227,9 @@ private function bool HandleText(
name messageType, name messageType,
bool teamMessage) bool teamMessage)
{ {
local Text messageAsText; local Text messageAsText;
local APlayer callerPlayer; local EPlayer callerPlayer;
local Parser parser; local Parser parser;
local PlayerService service;
// We only want to catch chat messages // We only want to catch chat messages
// and only if `Commands` feature is active // and only if `Commands` feature is active
if (messageType != 'Say') return true; if (messageType != 'Say') return true;
@ -242,13 +245,11 @@ private function bool HandleText(
messageAsText.FreeSelf(); messageAsText.FreeSelf();
return true; return true;
} }
// Extract `APlayer` from the `sender` // Extract `EPlayer` from the `sender`
service = PlayerService(class'PlayerService'.static.Require()); callerPlayer = _.players.FromController(PlayerController(sender));
if (service != none) {
callerPlayer = service.GetPlayer(PlayerController(sender));
}
// Pass input to command feature // Pass input to command feature
HandleInput(parser, callerPlayer); HandleInput(parser, callerPlayer);
_.memory.Free(callerPlayer);
parser.FreeSelf(); parser.FreeSelf();
return false; return false;
} }

43
sources/Commands/PlayersParser.uc

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

215
sources/Commands/Tests/TEST_Command.uc

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

82
sources/Console/ConsoleWriter.uc

@ -44,7 +44,8 @@ enum ConsoleWriterTarget
var private ConsoleWriterTarget targetType; var private ConsoleWriterTarget targetType;
// Players that will receive output passed to this `ConsoleWriter`. // Players that will receive output passed to this `ConsoleWriter`.
// Only used when `targetType == CWTARGET_Players` // 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 ConsoleBuffer outputBuffer;
var private bool needToResetColor; var private bool needToResetColor;
@ -59,6 +60,14 @@ var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
// color information instead. // color information instead.
var private Color defaultColor; var private Color defaultColor;
protected function Finalizer()
{
_.memory.FreeMany(outputTargets);
_.memory.Free(outputBuffer);
outputTargets.length = 0;
outputBuffer = none;
}
public final function ConsoleWriter Initialize( public final function ConsoleWriter Initialize(
ConsoleAPI.ConsoleDisplaySettings newDisplaySettings) ConsoleAPI.ConsoleDisplaySettings newDisplaySettings)
{ {
@ -326,7 +335,7 @@ public final function ConsoleWriter ForAll()
* throw messages away. * throw messages away.
* @return Returns caller `ConsoleWriter` to allow for method chaining. * @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) if (targetPlayer == none)
{ {
@ -339,7 +348,7 @@ public final function ConsoleWriter ForPlayer(APlayer targetPlayer)
} }
outputTargets.length = 0; outputTargets.length = 0;
targetType = CWTARGET_Players; targetType = CWTARGET_Players;
outputTargets[0] = targetPlayer; outputTargets[0] = EPlayer(targetPlayer.Copy());
return self; return self;
} }
@ -353,15 +362,16 @@ public final function ConsoleWriter ForPlayer(APlayer targetPlayer)
* If `none` - this method will do nothing. * If `none` - this method will do nothing.
* @return Returns caller `ConsoleWriter` to allow for method chaining. * @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; local int i;
if (targetPlayer == none) return self; if (targetPlayer == none) return self;
if (!targetPlayer.IsConnected()) return self; if (!targetPlayer.IsExistent()) return self;
if (targetType != CWTARGET_Players) if (targetType != CWTARGET_Players)
{ {
Flush(); Flush();
_.memory.FreeMany(outputTargets);
if (targetType == CWTARGET_None) { if (targetType == CWTARGET_None) {
outputTargets.length = 0; outputTargets.length = 0;
} }
@ -372,12 +382,12 @@ public final function ConsoleWriter AndPlayer(APlayer targetPlayer)
targetType = CWTARGET_Players; targetType = CWTARGET_Players;
for (i = 0; i < outputTargets.length; i += 1) for (i = 0; i < outputTargets.length; i += 1)
{ {
if (outputTargets[i] == targetPlayer) { if (targetPlayer.SameAs(outputTargets[i])) {
return self; return self;
} }
} }
Flush(); Flush();
outputTargets[outputTargets.length] = targetPlayer; outputTargets[outputTargets.length] = EPlayer(targetPlayer.Copy());
return self; return self;
} }
@ -391,24 +401,26 @@ public final function ConsoleWriter AndPlayer(APlayer targetPlayer)
* If `none` - this method will do nothing. * If `none` - this method will do nothing.
* @return Returns caller `ConsoleWriter` to allow for method chaining. * @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; local int i;
if (targetType == CWTARGET_None) return self; if (targetType == CWTARGET_None) return self;
if (playerToRemove == none) return self; if (playerToRemove == none) return self;
if (!playerToRemove.IsConnected()) return self; if (!playerToRemove.IsExistent()) return self;
if (targetType == CWTARGET_All) if (targetType == CWTARGET_All)
{ {
Flush(); Flush();
_.memory.FreeMany(outputTargets);
outputTargets = _.players.GetAll(); outputTargets = _.players.GetAll();
} }
targetType = CWTARGET_Players; targetType = CWTARGET_Players;
while (i < outputTargets.length) while (i < outputTargets.length)
{ {
if (outputTargets[i] == playerToRemove) if (playerToRemove.SameAs(outputTargets[i]))
{ {
Flush(); Flush();
_.memory.Free(outputTargets[i]);
outputTargets.Remove(i, 1); outputTargets.Remove(i, 1);
break; 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. * outputting messages.
* If caller `ConsoleWriter` is setup to message several different players, * If caller `ConsoleWriter` is setup to message several different players,
* returns an arbitrary one of them. * 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 * outputting messages. Returns `none` iff it currently outputs to
* every player or to no one. * every player or to no one.
*/ */
public final function APlayer GetTargetPlayer() public final function EPlayer GetTargetPlayer()
{ {
if (targetType == CWTARGET_All) return none; if (targetType == CWTARGET_All) return none;
if (outputTargets.length <= 0) 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. * 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 * outputting messages. Returned array is guaranteed to not contain `none`s
* every player or to no one. * or `EPlayer` interfaces with non-existent status.
*/ */
public final function array<APlayer> GetTargetPlayers() public final function array<EPlayer> GetTargetPlayers()
{ {
local array<APlayer> emptyArray; local int i;
if (targetType == CWTARGET_None) return emptyArray; local array<EPlayer> result;
if (targetType == CWTARGET_None) return result;
if (targetType == CWTARGET_All) return _.players.GetAll(); 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`, // Assumes `connectionService != none`, caller function must ensure that.
// caller function must ensure that.
private final function SendConsoleMessage( private final function SendConsoleMessage(
array<PlayerController> recipients, array<PlayerController> recipients,
string message, string message,
@ -604,7 +621,6 @@ private final function array<PlayerController> GetRecipientsControllers()
{ {
local int i; local int i;
local PlayerController nextRecipient; local PlayerController nextRecipient;
local PlayerService playerService;
local ConnectionService connectionService; local ConnectionService connectionService;
local array<PlayerController> recipients; local array<PlayerController> recipients;
local array<ConnectionService.Connection> connections; local array<ConnectionService.Connection> connections;
@ -612,16 +628,12 @@ private final function array<PlayerController> GetRecipientsControllers()
if (targetType == CWTARGET_None) { if (targetType == CWTARGET_None) {
return recipients; return recipients;
} }
// Single target case // Selected targets case
if (targetType != CWTARGET_All) if (targetType != CWTARGET_All)
{ {
playerService = PlayerService(class'PlayerService'.static.Require());
if (playerService == none) {
return recipients;
}
for (i = 0; i < outputTargets.length; i += 1) for (i = 0; i < outputTargets.length; i += 1)
{ {
nextRecipient = playerService.GetController(outputTargets[i]); nextRecipient = outputTargets[i].GetController();
if (nextRecipient != none) { if (nextRecipient != none) {
recipients[recipients.length] = nextRecipient; 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. * 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. * 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. * Signal that will be emitted whenever trading time ends.
* *
* [Signature] * [Signature]
* void <slot>(ATrader oldTrader, ATrader newTrader) * void <slot>(ETrader oldTrader, ETrader newTrader)
* *
* @param oldTrader Trader that was selected before this event. * @param oldTrader Trader that was selected before this event.
* @param newTrader Trader that will be selected after 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, * `none`-references. None of them should be deallocated,
* otherwise Acedia's behavior is undefined. * otherwise Acedia's behavior is undefined.
*/ */
public function array<ATrader> GetTraders(); public function array<ETrader> GetTraders();
/** /**
* Checks whether trading is currently active. * 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 * Changing a selected trader in any way should always be followed
* by emitting `OnTraderSelected()` signal. * by emitting `OnTraderSelected()` signal.
* After `SelectTrader()` call `GetSelectedTrader()` should return * 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. * first result in emitted `OnTraderSelected()` signal.
* *
* @return Currently selected trader. * @return Currently selected trader.
*/ */
public function ATrader GetSelectedTrader(); public function ETrader GetSelectedTrader();
/** /**
* Changes currently selected trader. * Changes currently selected trader.
* *
* @see `GetSelectedTrader()` for more details. * @see `GetSelectedTrader()` for more details.
*/ */
public function SelectTrader(ATrader newSelection); public function SelectTrader(ETrader newSelection);
defaultproperties 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 * Class, objects of which are expected to represent traders located on
* the map. In classic KF game mode it would represent areas behind closed * 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. * 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. * This file is part of Acedia.
* *
@ -19,7 +19,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class ATrader extends AcediaObject class ETrader extends EInterface
abstract; abstract;
/** /**
@ -60,7 +60,7 @@ public function Text GetName();
* @param newName New name of the caller trader. * @param newName New name of the caller trader.
* @return `true` if trader is currently enabled and `false` otherwise. * @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. * Checks if caller trader is currently enabled.
@ -78,7 +78,7 @@ public function ATrader SetName(Text newName);
public function bool IsEnabled(); 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 * Disabling the trader should automatically "boot" players out
* (see `BootPlayers()`). * (see `BootPlayers()`).
@ -87,14 +87,14 @@ public function bool IsEnabled();
* *
* @param doEnable `true` if trader is currently enabled and * @param doEnable `true` if trader is currently enabled and
* `false` otherwise. * `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. * should not reset it.
* *
* @return `true` if trader is marked to always auto-open upon activating * @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(); 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. * @see `IsAutoOpen()` for more info.
* *
* @param doAutoOpen `true` if trader should be marked to always auto-open * @param doAutoOpen `true` if trader should be marked to always auto-open
* upon activating trading and `false` otherwise. * 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 * `ETrader` being open means that players can "enter" (whatever that means for
* an implementation) and use `ATrader` to buy/sell equipment. * an implementation) and use `ETrader` to buy/sell equipment.
* *
* @return `true` if it is open and `false` otherwise. * @return `true` if it is open and `false` otherwise.
*/ */
public function bool IsOpen(); 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 * Closing the trader should not automatically "boot" players out
* (see `BootPlayers()`). * (see `BootPlayers()`).
@ -132,27 +132,27 @@ public function bool IsOpen();
* @see `IsOpen()` for more details. * @see `IsOpen()` for more details.
* *
* @param doOpen `true` if it is open and `false` otherwise. * @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. * @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(); public function bool IsSelected();
/** /**
* Marks caller `ATrader` as a selected trader. * Marks caller `ETrader` as a selected trader.
* *
* @see `ATradingComponent.GetSelectedTrader()` for more details. * @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. * 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 * after it is closed. If that is impossible (for traders resembling
* KF2's one), then this method should do nothing. * 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 * Shortcut method to open the caller trader, guaranteed to be equivalent to
* `SetOpen(true)`. Provided for better interface. * `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); SetOpen(true);
return self; return self;
@ -183,9 +183,9 @@ public final function ATrader Open()
* Shortcut method to close the caller trader, guaranteed to be equivalent to * Shortcut method to close the caller trader, guaranteed to be equivalent to
* `SetOpen(false)`. Provided for better interface. * `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); SetOpen(false);
return self; 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 * Signal class implementation for `ATradingComponent`, for detecting when
* another trader is selected. * another trader is selected.
* Copyright 2021 Anton Tarasenko * Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -20,7 +20,7 @@
*/ */
class Trading_OnSelect_Signal extends Signal; 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; local Slot nextSlot;
StartIterating(); 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 * Slot class implementation for `ATradingComponent`'s signal for
* detecting when another trader is selected. * detecting when another trader is selected.
* Copyright 2021 Anton Tarasenko * Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -20,7 +20,7 @@
*/ */
class Trading_OnSelect_Slot extends Slot; class Trading_OnSelect_Slot extends Slot;
delegate connect(ATrader oldTrader, ATrader newTrader) delegate connect(ETrader oldTrader, ETrader newTrader)
{ {
DummyCall(); DummyCall();
} }

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

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

191
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. * Wrapper for KF1's `ShopVolume`s.
* Copyright 2021 Anton Tarasenko * Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -18,7 +18,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class KF1_Trader extends ATrader; class KF1_Trader extends ETrader;
// We do not use any vanilla value as a name, instead storing and tracking it // We do not use any vanilla value as a name, instead storing and tracking it
// entirely as our own value. // entirely as our own value.
@ -26,6 +26,27 @@ var protected Text myName;
// Reference to `ShopVolume` actor that this `KF1_Trader` represents. // Reference to `ShopVolume` actor that this `KF1_Trader` represents.
var protected NativeActorRef myShopVolume; 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() protected function Finalizer()
{ {
_.memory.Free(myName); _.memory.Free(myName);
@ -34,40 +55,84 @@ protected function Finalizer()
myName = none; 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 * Initializes caller `KF1_Trader`. Should be called right after `KF1_Trader`
* each of them. * was allocated.
*
* Every `KF1_Trader` must be initialized, using non-initialized `KF1_Trader`
* instances is invalid.
* *
* @return Array of created `KF1_Trader`s. All of them are guaranteed to not * Initialization can fail if:
* be `none`. * 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; if (initShopVolume == none) {
local MutableText textBuilder; return false;
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;
}
} }
textBuilder.FreeSelf(); if ( initShopVolume.bObjectiveModeOnly
return allTraders; && !__().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;
}
return ShopVolume(myShopVolume.Get());
} }
public function Text GetName() public function Text GetName()
@ -78,7 +143,8 @@ public function Text GetName()
return myName.Copy(); 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 == none) return self;
if (newName.IsEmpty()) return self; if (newName.IsEmpty()) return self;
@ -108,7 +174,7 @@ public function bool IsEnabled()
return false; return false;
} }
public function ATrader SetEnabled(bool doEnable) public function ETrader SetEnabled(bool doEnable)
{ {
local ShopVolume vanillaShopVolume; local ShopVolume vanillaShopVolume;
vanillaShopVolume = ShopVolume(myShopVolume.Get()); vanillaShopVolume = ShopVolume(myShopVolume.Get());
@ -138,7 +204,7 @@ protected function UpdateShopList()
local ShopVolume nextShopVolume; local ShopVolume nextShopVolume;
local KF1_Trader nextTrader; local KF1_Trader nextTrader;
local array<ShopVolume> shopVolumes; local array<ShopVolume> shopVolumes;
local array<ATrader> availableTraders; local array<ETrader> availableTraders;
availableTraders = _.kf.trading.GetTraders(); availableTraders = _.kf.trading.GetTraders();
for (i = 0; i < availableTraders.length; i += 1) for (i = 0; i < availableTraders.length; i += 1)
{ {
@ -163,7 +229,7 @@ public function bool IsAutoOpen()
return false; return false;
} }
public function ATrader SetAutoOpen(bool doAutoOpen) public function ETrader SetAutoOpen(bool doAutoOpen)
{ {
local ShopVolume vanillaShopVolume; local ShopVolume vanillaShopVolume;
vanillaShopVolume = ShopVolume(myShopVolume.Get()); vanillaShopVolume = ShopVolume(myShopVolume.Get());
@ -189,7 +255,7 @@ public function bool IsOpen()
return false; return false;
} }
public function ATrader SetOpen(bool doOpen) public function ETrader SetOpen(bool doOpen)
{ {
local ShopVolume vanillaShopVolume; local ShopVolume vanillaShopVolume;
if (doOpen && !IsEnabled()) return self; if (doOpen && !IsEnabled()) return self;
@ -220,7 +286,7 @@ public function bool IsSelected()
return false; return false;
} }
public function ATrader Select() public function ETrader Select()
{ {
local ShopVolume vanillaShopVolume; local ShopVolume vanillaShopVolume;
local KFGameReplicationInfo kfGameRI; local KFGameReplicationInfo kfGameRI;
@ -235,7 +301,7 @@ public function ATrader Select()
return self; return self;
} }
public function ATrader BootPlayers() public function ETrader BootPlayers()
{ {
local ShopVolume vanillaShopVolume; local ShopVolume vanillaShopVolume;
vanillaShopVolume = ShopVolume(myShopVolume.Get()); vanillaShopVolume = ShopVolume(myShopVolume.Get());
@ -245,6 +311,51 @@ public function ATrader BootPlayers()
return self; 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 defaultproperties
{ {
} }

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

@ -1,7 +1,7 @@
/** /**
* `ATradingComponent`'s implementation for `KF1_Frontend`. * `ATradingComponent`'s implementation for `KF1_Frontend`.
* Only supports `KF1_Trader` as a possible trader class. * Only supports `KF1_Trader` as a possible trader class.
* Copyright 2021 Anton Tarasenko * Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -28,16 +28,38 @@ var protected int pausedCountDownValue;
// For detecting events of trading becoming active/inactive and selecting // For detecting events of trading becoming active/inactive and selecting
// a different trader, to account for these changing through non-Acedia means // a different trader, to account for these changing through non-Acedia means
var protected bool wasActiveLastCheck; var protected bool wasActiveLastCheck;
var protected Atrader lastSelectedTrader; var protected ETrader lastSelectedTrader;
// All known traders on map var protected array<ETrader> registeredTraders;
var protected array<ATrader> registeredTraders;
protected function Constructor() protected function Constructor()
{ {
local LevelInfo level;
local KFGameType kfGame;
local KF1_Trader nextTrader;
local ShopVolume nextShopVolume;
super.Constructor(); super.Constructor();
_.unreal.OnTick(self).connect = Tick; _.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(); lastSelectedTrader = GetSelectedTrader();
wasActiveLastCheck = IsTradingActive(); wasActiveLastCheck = IsTradingActive();
} }
@ -52,9 +74,14 @@ protected function Finalizer()
registeredTraders.length = 0; 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() public function bool IsTradingActive()
@ -91,21 +118,22 @@ public function SetTradingStatus(bool makeActive)
kfGameRI.maxMonsters = 0; kfGameRI.maxMonsters = 0;
} }
public function ATrader GetSelectedTrader() public function ETrader GetSelectedTrader()
{ {
local int i; local int i;
for (i = 0; i < registeredTraders.length; i += 1) for (i = 0; i < registeredTraders.length; i += 1)
{ {
if (registeredTraders[i].IsSelected()) { if (registeredTraders[i].IsSelected()) {
return registeredTraders[i]; return ETrader(registeredTraders[i].Copy());
} }
} }
return none; return none;
} }
public function SelectTrader(ATrader newSelection) public function SelectTrader(ETrader newSelection)
{ {
local ATrader oldSelection; local bool traderChanged;
local ETrader oldSelection;
local KFGameReplicationInfo kfGameRI; local KFGameReplicationInfo kfGameRI;
if (newSelection != none) { if (newSelection != none) {
newSelection.Select(); newSelection.Select();
@ -121,10 +149,20 @@ public function SelectTrader(ATrader newSelection)
// in case someone decides it would be a grand idea to call `SelectTrader` // in case someone decides it would be a grand idea to call `SelectTrader`
// during `onTraderSelectSignal` signal. // during `onTraderSelectSignal` signal.
oldSelection = lastSelectedTrader; oldSelection = lastSelectedTrader;
lastSelectedTrader = newSelection; if (newSelection != none)
if (lastSelectedTrader != newSelection) { {
lastSelectedTrader = ETrader(newSelection.Copy());
traderChanged = lastSelectedTrader.SameAs(oldSelection);
}
else
{
lastSelectedTrader = none;
traderChanged = (oldSelection == none);
}
if (traderChanged) {
onTraderSelectSignal.Emit(oldSelection, newSelection); onTraderSelectSignal.Emit(oldSelection, newSelection);
} }
_.memory.Free(oldSelection);
} }
public function int GetTradingInterval() public function int GetTradingInterval()
@ -179,26 +217,19 @@ public function SetCountDownPause(bool doPause)
protected function Tick(float delta, float timeScaleCoefficient) protected function Tick(float delta, float timeScaleCoefficient)
{ {
local bool isActiveNow; local bool isActiveNow;
local ATrader newSelectedTrader;
// Enforce pause // Enforce pause
if (tradingCountDownPaused) { if (tradingCountDownPaused) {
_.unreal.GetKFGameType().waveCountDown = pausedCountDownValue; _.unreal.GetKFGameType().waveCountDown = pausedCountDownValue;
} }
// Selected trader check // Selected trader check
newSelectedTrader = GetSelectedTrader(); CheckNativeTraderSwap();
if (lastSelectedTrader != newSelectedTrader)
{
onTraderSelectSignal.Emit(lastSelectedTrader, newSelectedTrader);
lastSelectedTrader = newSelectedTrader;
}
// Active status check // Active status check
isActiveNow = IsTradingActive(); isActiveNow = IsTradingActive();
if (wasActiveLastCheck != isActiveNow) if (wasActiveLastCheck != isActiveNow)
{ {
wasActiveLastCheck = isActiveNow; wasActiveLastCheck = isActiveNow;
if (isActiveNow) if (isActiveNow) {
{
onStartSignal.Emit(); onStartSignal.Emit();
} }
else 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 defaultproperties
{ {
} }

1
sources/Manifest.uc

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

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

@ -1,12 +1,6 @@
/** /**
* Represents a connected player connection and serves to provide access to * Provides a common interface to a connected player connection.
* both it's server data and in-game pawn representation. * Copyright 2021 - 2022 Anton Tarasenko
* 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
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -23,9 +17,9 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class 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; var private User identity;
// Writer that can be used to write into this player's console // 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 // Remember version to reallocate writer in case someone deallocates it
var private int consoleLifeVersion; var private int consoleLifeVersion;
// `PlayerController` reference
var private NativeActorRef controller; var private NativeActorRef controller;
// These variables record name of this player; // These variables record name of this player;
@ -54,21 +49,112 @@ enum AdminStatus
protected function Finalizer() protected function Finalizer()
{ {
_.memory.Free(controller); _.memory.Free(controller);
controller = none; _.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, * Every `EPlayer` must be initialized, using non-initialized `EPlayer`
* otherwise a location caller `APlayer` is currently spectating the map from * instances is invalid.
* will be returned. *
* 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() public final function Vector GetLocation()
{ {
local Pawn myPawn; local Pawn myPawn;
local PlayerController myController; local PlayerController myController;
myController = PlayerController(controller.Get()); if (controller != none) {
myController = PlayerController(controller.Get());
}
if (myController != none) if (myController != none)
{ {
myPawn = myController.pawn; myPawn = myController.pawn;
@ -80,68 +166,38 @@ public final function Vector GetLocation()
return Vect(0.0, 0.0, 0.0); return Vect(0.0, 0.0, 0.0);
} }
// `PlayerReplicationInfo` associated with the caller `APLayer`. // `PlayerReplicationInfo` associated with the caller `EPlayer`.
// Can return `none` if: // Can return `none` if:
// 1. Caller `APlayer` has already disconnected; // 1. Caller `EPlayer` has already disconnected;
// 2. It was not properly initialized; // 2. It was not properly initialized;
// 3. There is an issue running `PlayerService`.
private final function PlayerReplicationInfo GetRI() private final function PlayerReplicationInfo GetRI()
{ {
local PlayerController myController; local PlayerController myController;
if (controller == none) return none;
myController = PlayerController(controller.Get()); myController = PlayerController(controller.Get());
if (myController != none) { if (myController == none) return none;
return myController.playerReplicationInfo;
}
return none;
}
/** return myController.playerReplicationInfo;
* 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);
} }
/** /**
* Initializes caller `APlayer`. Should be called right after `APlayer` * Returns `PlayerController`, associated with the caller `EPlayer`.
* was spawned.
*
* 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; if (controller == none) {
local PlayerController myController; return none;
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);
} }
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 * @return `User` corresponding to the caller `EPlayer`. Guarantee to be
* not `none` for correctly initialized `APlayer` (it remembers `User` * not `none` for correctly initialized `EPlayer` (it remembers `User`
* record even if player has disconnected). * record even if player has disconnected).
*/ */
public final function User GetIdentity() public final function User GetIdentity()
@ -154,7 +210,7 @@ public final function User GetIdentity()
* *
* @return `Text` containing current name of the caller player. * @return `Text` containing current name of the caller player.
* Guaranteed to not be `none`. Returned object is not managed by caller * 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() 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. * be copied. Passing `none` will result in an empty name.
*/ */
public final function SetName(Text newPlayerName) 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 from `IsAdmin()` since this method allows to distinguish between
* different types of admin login (like silent admins). * 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() public final function AdminStatus GetAdminStatus()
{ {
@ -271,10 +327,10 @@ public final function bool IsAdmin()
} }
/** /**
* Changes admin status of the caller `APlayer`. * Changes admin status of the caller `EPlayer`.
* Can only fail if caller `APlayer` has already disconnected. * 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) 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`. * method will return `0`.
*/ */
public final function int GetDosh() 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) 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 * console. Returned object should not be deallocated, but it is
* guaranteed to be valid for non-disconnected players. * guaranteed to be valid for non-disconnected players.
*/ */
public final function ConsoleWriter Console() public final function /* borrow */ ConsoleWriter BorrowConsole()
{ {
if ( consoleInstance == none if ( consoleInstance == none
|| consoleInstance.GetLifeVersion() != consoleLifeVersion) || 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 * Slot class implementation for `PlayerAPI`'s `OnLostPlayer` signal.
* also means it does not put any limitations on it's implementation.
* Copyright 2021 Anton Tarasenko * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
@ -18,8 +17,23 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class BaseBackend extends AcediaObject class PlayerAPI_OnLostPlayer_Slot extends Slot;
abstract;
delegate connect(User identity)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties 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 * Cannot fail for any connected player and can assume it will not be called
* for not connected ones. * 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. * 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
{
}

152
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 * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
@ -18,6 +18,7 @@
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class PlayersAPI extends AcediaObject class PlayersAPI extends AcediaObject
dependson(ConnectionService)
dependson(Text); dependson(Text);
// Writer that can be used to write into this player's console // 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 // Remember version to reallocate writer in case someone deallocates it
var private int consoleLifeVersion; var private int consoleLifeVersion;
var protected bool connectedToConnectionServer;
var protected PlayerAPI_OnNewPlayer_Signal onNewPlayerSignal;
var protected PlayerAPI_OnLostPlayer_Signal onLostPlayerSignal;
protected function Constructor() protected function Constructor()
{ {
local ConnectionService service; onNewPlayerSignal = PlayerAPI_OnNewPlayer_Signal(
service = ConnectionService(class'ConnectionService'.static.Require()); _.memory.Allocate(class'PlayerAPI_OnNewPlayer_Signal'));
service.OnConnectionEstablished(self).connect = MakePlayer; onLostPlayerSignal = PlayerAPI_OnLostPlayer_Signal(
_.memory.Allocate(class'PlayerAPI_OnLostPlayer_Signal'));
} }
protected function Finalizer() protected function Finalizer()
{ {
local ConnectionService service; local ConnectionService service;
service = ConnectionService(class'ConnectionService'.static.Require()); connectedToConnectionServer = false;
service.OnConnectionEstablished(self).Disconnect(); 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; ConnectToConnectionService();
local Text textIdHash; return PlayerAPI_OnLostPlayer_Slot(onLostPlayerSignal.NewSlot(receiver));
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();
} }
/** /**
* Return `ConsoleWriter` that can be used to write into every player's * Return `ConsoleWriter` that can be used to write into every player's
* console. * console.
* *
* Provided that returned object is never deallocated - returns the same object * Provided that returned object is never deallocated - method returns
* with each call, otherwise can allocate new instance of `ConsoleWriter`. * 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 * @return `ConsoleWriter` that can be used to write into every player's
* console. Returned object should not be deallocated, but it is * console. Returned object should not be deallocated, but it is
@ -77,21 +113,83 @@ public final function ConsoleWriter Console()
return consoleInstance.ForAll(); 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. * Fetches current array of all players.
* *
* @return Current array of all players. * @return Current array of all players.
* Guaranteed to not contain `none` values. * Guaranteed to not contain `none` values.
*/ */
public final function array<APlayer> GetAll() public final function array<EPlayer> GetAll()
{ {
local PlayerService service; local int i;
local array<APlayer> emptyResult; local array<EPlayer> result;
service = PlayerService(class'PlayerService'.static.GetInstance()); local ConnectionService service;
if (service != none) { local array<ConnectionService.Connection> activeConnections;
return service.GetAllPlayers(); 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 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. * Signal that will be emitted when the player connection is lost.
* *
* [Signature] * [Signature]
* void <slot>(ConnectionService.Connection newConnection) * void <slot>(ConnectionService.Connection lostConnection)
* *
* @param newConnection Structure that describes lost connection. * @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; var private LoggerAPI.Definition errNoUserDataDatabase;
// TODO: redo this comment
/** /**
* Initializes caller `User` with id and it's session key. Should be called * 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. * Initialization should (and can) only be done once.
* Before a `Initialize()` call, any other method calls on such `User` * Before a `Initialize()` call, any other method calls on such `User`

Loading…
Cancel
Save