From 7dc5149281deebfeb3d714e8b04c88d60b37b0fa Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Thu, 6 Jan 2022 06:47:19 +0700 Subject: [PATCH] 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. --- sources/Color/ColorAPI.uc | 20 +- .../Commands/BuiltInCommands/ACommandHelp.uc | 33 +- .../Commands/BuiltInCommands/ACommandTest.uc | 4 +- sources/Commands/Command.uc | 252 ++++++++--- sources/Commands/CommandCall.uc | 393 ------------------ sources/Commands/CommandParser.uc | 67 +-- sources/Commands/Commands_Feature.uc | 31 +- sources/Commands/PlayersParser.uc | 43 +- sources/Commands/Tests/TEST_Command.uc | 215 +++++----- sources/Console/ConsoleAPI.uc | 2 +- sources/Console/ConsoleWriter.uc | 82 ++-- .../{ => Frontend}/BaseFrontend.uc | 0 .../BaseClasses/Frontend/EInterface.uc | 104 +++++ .../KillingFloor/{ => Frontend}/KFFrontend.uc | 0 .../Trading/ATradingComponent.uc | 12 +- .../Trading/ETrader.uc} | 56 +-- .../Trading/Events/Trading_OnSelect_Signal.uc | 4 +- .../Trading/Events/Trading_OnSelect_Slot.uc | 4 +- .../BaseImplementation/EKFInventory.uc | 17 +- .../KF1Frontend/Trading/KF1_Trader.uc | 191 +++++++-- .../Trading/KF1_TradingComponent.uc | 95 ++++- sources/Manifest.uc | 1 - sources/Players/{APlayer.uc => EPlayer.uc} | 200 +++++---- .../Events/PlayerAPI_OnLostPlayer_Signal.uc | 38 ++ .../Events/PlayerAPI_OnLostPlayer_Slot.uc} | 22 +- .../Events/PlayerAPI_OnNewPlayer_Signal.uc | 38 ++ .../Events/PlayerAPI_OnNewPlayer_Slot.uc | 40 ++ sources/Players/Inventory/EInventory.uc | 4 +- sources/Players/PlayerService.uc | 183 -------- sources/Players/PlayersAPI.uc | 152 +++++-- .../Unreal/Connections/ConnectionService.uc | 2 +- sources/Users/User.uc | 3 +- 32 files changed, 1226 insertions(+), 1082 deletions(-) delete mode 100644 sources/Commands/CommandCall.uc rename sources/Gameplay/BaseClasses/{ => Frontend}/BaseFrontend.uc (100%) create mode 100644 sources/Gameplay/BaseClasses/Frontend/EInterface.uc rename sources/Gameplay/BaseClasses/KillingFloor/{ => Frontend}/KFFrontend.uc (100%) rename sources/Gameplay/BaseClasses/KillingFloor/{ => Frontend}/Trading/ATradingComponent.uc (95%) rename sources/Gameplay/BaseClasses/KillingFloor/{Trading/ATrader.uc => Frontend/Trading/ETrader.uc} (76%) rename sources/Gameplay/BaseClasses/KillingFloor/{ => Frontend}/Trading/Events/Trading_OnSelect_Signal.uc (91%) rename sources/Gameplay/BaseClasses/KillingFloor/{ => Frontend}/Trading/Events/Trading_OnSelect_Slot.uc (91%) rename sources/Players/{APlayer.uc => EPlayer.uc} (64%) create mode 100644 sources/Players/Events/PlayerAPI_OnLostPlayer_Signal.uc rename sources/{Gameplay/BaseClasses/BaseBackend.uc => Players/Events/PlayerAPI_OnLostPlayer_Slot.uc} (72%) create mode 100644 sources/Players/Events/PlayerAPI_OnNewPlayer_Signal.uc create mode 100644 sources/Players/Events/PlayerAPI_OnNewPlayer_Slot.uc delete mode 100644 sources/Players/PlayerService.uc diff --git a/sources/Color/ColorAPI.uc b/sources/Color/ColorAPI.uc index 7cf4ba3..d34ad8b 100644 --- a/sources/Color/ColorAPI.uc +++ b/sources/Color/ColorAPI.uc @@ -899,7 +899,7 @@ private final function Color ParseRGBA(Parser parser) if (!parser.Ok()) { parser.RestoreState(initialParserState) - .Match(T(TRGBA), SCASE_INSENSITIVE) + .Match(T(TRGBA), SCASE_INSENSITIVE) .Match(T(TR_COMPONENT), SCASE_INSENSITIVE) .MInteger(redComponent).Match(T(TCOMMA)) .Match(T(TG_COMPONENT), SCASE_INSENSITIVE) @@ -1472,15 +1472,15 @@ defaultproperties browndarken4=(R=62,G=39,B=35,A=255) bluegrey=(R=96,G=125,B=139,A=255) vuebluegrey=(R=96,G=125,B=139,A=255) - bluegreylighten5#rgb(R=236,G=239,B=241,A=255) - bluegreylighten4#rgb(R=207,G=216,B=220,A=255) - bluegreylighten3#rgb(R=176,G=190,B=197,A=255) - bluegreylighten2#rgb(R=144,G=164,B=174,A=255) - bluegreylighten1#rgb(R=120,G=144,B=156,A=255) - bluegreydarken1#rgb(R=84,G=110,B=122,A=255) - bluegreydarken2#rgb(R=69,G=90,B=100,A=255) - bluegreydarken3#rgb(R=55,G=71,B=79,A=255) - bluegreydarken4#rgb(R=38,G=50,B=56,A=255) + bluegreylighten5=(R=236,G=239,B=241,A=255) + bluegreylighten4=(R=207,G=216,B=220,A=255) + bluegreylighten3=(R=176,G=190,B=197,A=255) + bluegreylighten2=(R=144,G=164,B=174,A=255) + bluegreylighten1=(R=120,G=144,B=156,A=255) + bluegreydarken1=(R=84,G=110,B=122,A=255) + bluegreydarken2=(R=69,G=90,B=100,A=255) + bluegreydarken3=(R=55,G=71,B=79,A=255) + bluegreydarken4=(R=38,G=50,B=56,A=255) grey=(R=158,G=158,B=158,A=255) vuegrey=(R=158,G=158,B=158,A=255) greylighten5=(R=250,G=250,B=250,A=255) diff --git a/sources/Commands/BuiltInCommands/ACommandHelp.uc b/sources/Commands/BuiltInCommands/ACommandHelp.uc index 909a5db..88bdcf5 100644 --- a/sources/Commands/BuiltInCommands/ACommandHelp.uc +++ b/sources/Commands/BuiltInCommands/ACommandHelp.uc @@ -1,6 +1,6 @@ /** * Command for displaying help information about registered Acedia's commands. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -40,18 +40,14 @@ protected function BuildData(CommandDataBuilder builder) .Describe(P("Display list of all available commands.")); } -protected function Executed(CommandCall callInfo) +protected function Executed(Command.CallData callData, EPlayer callerPlayer) { - local AssociativeArray parameters; + local AssociativeArray parameters, options;; local DynamicArray commandsToDisplay; - local APlayer callerPlayer; - callerPlayer = callInfo.GetCallerPlayer(); - if (callerPlayer == none) { - return; - } - + parameters = callData.parameters; + options = callData.options; // Print command list if "--list" option was specified - if (callInfo.GetOptions().HasKey(P("list"))) { + if (options.HasKey(P("list"))) { DisplayCommandList(callerPlayer); } // Help pages. @@ -59,16 +55,17 @@ protected function Executed(CommandCall callInfo) // 1. Any commands are specified as parameters; // 2. No commands or "--list" option was specified, then we want to // print a help page for this command. - if ( !callInfo.GetOptions().HasKey(P("list")) - || callInfo.GetParameters().HasKey(P("commands"))) + if (!options.HasKey(P("list")) || parameters.HasKey(P("commands"))) { - parameters = callInfo.GetParameters(); commandsToDisplay = DynamicArray(parameters.GetItem(P("commands"))); DisplayCommandHelpPages(callerPlayer, commandsToDisplay); } + _.memory.Free(callerPlayer); + _.memory.Free(parameters); + _.memory.Free(options); } -private final function DisplayCommandList(APlayer player) +private final function DisplayCommandList(EPlayer player) { local int i; local ConsoleWriter console; @@ -81,7 +78,7 @@ private final function DisplayCommandList(APlayer player) Commands_Feature(class'Commands_Feature'.static.GetInstance()); if (commandsFeature == none) return; - console = player.Console(); + console = player.BorrowConsole(); commandNames = commandsFeature.GetCommandNames(); for (i = 0; i < commandNames.length; i += 1) { @@ -99,7 +96,7 @@ private final function DisplayCommandList(APlayer player) } private final function DisplayCommandHelpPages( - APlayer player, + EPlayer player, DynamicArray commandList) { local int i; @@ -113,7 +110,7 @@ private final function DisplayCommandHelpPages( // If arguments were empty - at least display our own help page if (commandList == none) { - PrintHelpPage(player.Console(), GetData()); + PrintHelpPage(player.BorrowConsole(), GetData()); return; } // Otherwise - print help for specified commands @@ -121,7 +118,7 @@ private final function DisplayCommandHelpPages( { nextCommand = commandsFeature.GetCommand(Text(commandList.GetItem(i))); if (nextCommand == none) continue; - PrintHelpPage(player.Console(), nextCommand.GetData()); + PrintHelpPage(player.BorrowConsole(), nextCommand.GetData()); } } diff --git a/sources/Commands/BuiltInCommands/ACommandTest.uc b/sources/Commands/BuiltInCommands/ACommandTest.uc index 731d232..299472a 100644 --- a/sources/Commands/BuiltInCommands/ACommandTest.uc +++ b/sources/Commands/BuiltInCommands/ACommandTest.uc @@ -26,7 +26,7 @@ protected function BuildData(CommandDataBuilder builder) .ParamText(P("option")); } -protected function Executed(CommandCall result) +protected function Executed(Command.CallData result, EPlayer callerPlayer) { local Parser parser; local AssociativeArray root; @@ -64,7 +64,7 @@ protected function Executed(CommandCall result) }*/ parser = _.text.ParseString("{\"innerObject\":{\"my_bool\":true,\"array\":[\"Engine.Actor\",false,null,{\"something \\\"here\\\"\":\"yes\",\"maybe\":0.003},56.6],\"one more\":{\"nope\":324532,\"whatever\":false,\"o rly?\":\"ya rly\"},\"my_int\":-9823452},\"some_var\":-7.32,\"another_var\":\"aye!\"}"); root = _.json.ParseObjectWith(parser); - result.GetCallerPlayer().Console().WriteLine(_.json.PrettyPrint(root)); + callerPlayer.BorrowConsole().WriteLine(_.json.PrettyPrint(root)); } defaultproperties diff --git a/sources/Commands/Command.uc b/sources/Commands/Command.uc index 80a805b..54fdb5f 100644 --- a/sources/Commands/Command.uc +++ b/sources/Commands/Command.uc @@ -1,9 +1,12 @@ /** * This class is meant to represent a command type: to create new command * one should extend it, then simply define required sub-commands/options and - * parameters in `BuildData()` and use `Execute()` / `ExecuteFor()` to perform - * necessary actions when command is executed by a player. - * Copyright 2021 Anton Tarasenko + * parameters in `BuildData()` and overload `Execute()` / `ExecuteFor()` + * to perform required actions when command is executed by a player. + * `Execute()` is called first, whenever command is executed and + * `ExecuteFor()` is called only for targeted commands, once for each + * targeted player. + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -24,7 +27,8 @@ class Command extends AcediaObject dependson(Text); /** - * Possible errors that can arise when producing `CommandCall` from user input + * Possible errors that can arise when parsing command parameters from + * user input */ enum ErrorType { @@ -56,6 +60,25 @@ enum ErrorType CET_EmptyTargetList }; +/** + * Structure that contains all the information about how `Command` was called. + */ +struct CallData +{ + // Targeted players (if applicable) + var public array targetPlayers; + // Specified sub-command and parameters/options + var public Text subCommandName; + // Provided parameters and specified options + var public AssociativeArray parameters; + var public AssociativeArray options; + // Errors that occurred during command call processing are described by + // error type and optional error textual name of the object + // (parameter, option, etc.) that caused it. + var public ErrorType parsingError; + var public Text errorCause; +}; + /** * Possible types of parameters. */ @@ -216,25 +239,32 @@ private final function CleanParameters(array parameters) protected function BuildData(CommandDataBuilder builder){} /** - * Overload this method to perform what is needed when your command is called. + * Overload this method to perform required actions when + * your command is called. * - * @param callInfo Object filled with parameters that your command has - * been called with. Guaranteed to not be in error state. + * @param callData `struct` filled with parameters that your command + * has been called with. Guaranteed to not be in error state. + * @param callerPlayer Player that instigated this execution. */ -protected function Executed(CommandCall callInfo){} +protected function Executed(CallData callData, EPlayer callerPlayer){} /** - * Overload this method to perform what is needed when your command is called + * Overload this method to perform required actions when your command is called * with a given player as a target. If several players have been specified - * this method will be called once for each. * * If your command does not require a target - this method will not be called. * * @param targetPlayer Player that this command must perform an action on. - * @param callInfo Object filled with parameters that your command has - * been called with. Guaranteed to not be in error state. + * @param callData `struct` filled with parameters that your command + * has been called with. Guaranteed to not be in error state and contain + * all the required data. + * @param callerPlayer Player that instigated this call. */ -protected function ExecutedFor(APlayer targetPlayer, CommandCall callInfo){} +protected function ExecutedFor( + EPlayer targetPlayer, + CallData callData, + EPlayer callerPlayer){} /** * Returns an instance of command (of particular class) that is stored @@ -249,112 +279,211 @@ public final static function Command GetInstance() } /** - * Forces command to process (parse and, if successful, execute itself) - * player's input. + * Forces command to process (parse) player's input, producing a structure + * with parsed data in Acedia's format instead. * - * @param parser Parser that contains player's input. - * @param callerPlayer Player that initiated this command's call. - * @return `CommandCall` object that described parsed command call. - * Guaranteed to be not `none`. + * @see `Execute()` for actually performing command's actions. + * + * @param parser Parser that contains command input. + * @param callerPlayer Player that initiated this command's call, + * necessary for parsing player list (since it can point at + * the caller player). + * @return `CallData` structure that contains all the information about + * parameters specified in `parser`'s contents. + * Returned structure contains objects that must be deallocated, + * which can easily be done by the auxiliary `DeallocateCallData()` method. */ -public final function CommandCall ProcessInput( +public final function CallData ParseInputWith( Parser parser, - APlayer callerPlayer) + EPlayer callerPlayer) { - local int i; - local array targetPlayers; + local array targetPlayers; local CommandParser commandParser; - local CommandCall callInfo; - if (parser == none || !parser.Ok()) { - return MakeAndReportError(callerPlayer, CET_BadParser); + local CallData callData; + if (parser == none || !parser.Ok()) + { + callData.parsingError = CET_BadParser; + return callData; } // Parse targets and handle errors that can arise here if (commandData.requiresTarget) { targetPlayers = ParseTargets(parser, callerPlayer); - if (!parser.Ok()) { - return MakeAndReportError(callerPlayer, CET_IncorrectTargetList); + if (!parser.Ok()) + { + callData.parsingError = CET_IncorrectTargetList; + return callData; } - if (targetPlayers.length <= 0) { - return MakeAndReportError(callerPlayer, CET_EmptyTargetList); + if (targetPlayers.length <= 0) + { + callData.parsingError = CET_EmptyTargetList; + return callData; } } // Parse parameters themselves commandParser = CommandParser(_.memory.Allocate(class'CommandParser')); - callInfo = commandParser.ParseWith(parser, commandData) - .SetCallerPlayer(callerPlayer) - .SetTargetPlayers(targetPlayers); + callData = commandParser.ParseWith(parser, commandData); + callData.targetPlayers = targetPlayers; commandParser.FreeSelf(); + return callData; +} + +/** + * Executes caller `Command` with data provided by `callData` if it is in + * a correct state and reports error to `callerPlayer` if + * `callData` is invalid. + * + * @param callData Data about parameters, options, etc. with which + * caller `Command` is to be executed. + * @param callerPlayer Player that should be considered responsible for + * executing this `Command`. + * @return `true` if command was successfully executed and `false` otherwise. + * Execution is considered successful if `Execute()` call was made, + * regardless of whether `Command` can actually perform required action. + * For example, giving a weapon to a player can fail because he does not + * have enough space in his inventory, but it will still be considered + * a successful execution as far as return value is concerned. + */ +public final function bool Execute(CallData callData, EPlayer callerPlayer) +{ + local int i; + local array targetPlayers; + if (callerPlayer == none) return false; + if (!callerPlayer.IsExistent()) return false; + // Report or execute - if (!callInfo.IsSuccessful()) + if (callData.parsingError != CET_None) { - ReportError(callerPlayer, callInfo); - return callInfo; + ReportError(callData, callerPlayer); + return false; } - Executed(callInfo); + Executed(callData, callerPlayer); if (commandData.requiresTarget) { + targetPlayers = callData.targetPlayers; for (i = 0; i < targetPlayers.length; i += 1) { - ExecutedFor(targetPlayers[i], callInfo); + ExecutedFor(targetPlayers[i], callData, callerPlayer); } } - return callInfo; + return true; +} + +/** + * Auxiliary method that cleans up all data and deallocates all objects inside + * provided `callData` structure. + * + * @param callData Structure to clean. All stored data will be cleared, + * meaning that `DeallocateCallData()` method takes ownership of + * this parameter. + */ +public final static function DeallocateCallData(/* take */ CallData callData) +{ + __().memory.Free(callData.subCommandName); + __().memory.Free(callData.parameters); + __().memory.Free(callData.options); + __().memory.Free(callData.errorCause); + __().memory.FreeMany(callData.targetPlayers); + if (callData.targetPlayers.length > 0) { + callData.targetPlayers.length = 0; + } } // Reports given error to the `callerPlayer`, appropriately picking // message color -private final function ReportError( - APLayer callerPlayer, - CommandCall callInfo) +private final function ReportError(CallData callData, EPlayer callerPlayer) { local Text errorMessage; local ConsoleWriter console; if (callerPlayer == none) return; - if (callInfo == none) return; - if (callInfo.IsSuccessful()) return; + if (!callerPlayer.IsExistent()) return; // Setup console color - console = callerPlayer.Console(); - if (callInfo.GetError() == CET_EmptyTargetList) { + console = callerPlayer.BorrowConsole(); + if (callData.parsingError == CET_EmptyTargetList) { console.UseColor(_.color.textWarning); } else { console.UseColor(_.color.textFailure); } // Send message - errorMessage = callInfo.PrintErrorMessage(); + errorMessage = PrintErrorMessage(callData); console.Say(errorMessage); errorMessage.FreeSelf(); // Restore console color console.ResetColor().Flush(); } -// Creates (and returns) empty `CommandCall` with given error type and -// empty error cause and reports it -private final function CommandCall MakeAndReportError( - APLayer callerPlayer, - ErrorType errorType) +private final function Text PrintErrorMessage(CallData callData) { - local CommandCall dummyCall; - if (errorType == CET_None) return none; - - dummyCall = class'CommandCall'.static.MakeError(errorType, callerPlayer); - ReportError(callerPlayer, dummyCall); - return dummyCall; + local Text result; + local MutableText builder; + builder = _.text.Empty(); + switch (callData.parsingError) + { + case CET_BadParser: + builder.Append(P("Internal error occurred: invalid parser")); + break; + case CET_NoSubCommands: + builder.Append(P("Ill defined command: no subcommands")); + break; + case CET_NoRequiredParam: + builder.Append(P("Missing required parameter: ")) + .Append(callData.errorCause); + break; + case CET_NoRequiredParamForOption: + builder.Append(P("Missing required parameter for option: ")) + .Append(callData.errorCause); + break; + case CET_UnknownOption: + builder.Append(P("Invalid option specified: ")) + .Append(callData.errorCause); + break; + case CET_UnknownShortOption: + builder.Append(P("Invalid short option specified")); + break; + case CET_RepeatedOption: + builder.Append(P("Option specified several times: ")) + .Append(callData.errorCause); + break; + case CET_UnusedCommandParameters: + builder.Append(P("Part of command could not be parsed: ")) + .Append(callData.errorCause); + break; + case CET_MultipleOptionsWithParams: + builder.Append(P( "Multiple short options in one declarations" + @ "require parameters: ")) + .Append(callData.errorCause); + break; + case CET_IncorrectTargetList: + builder.Append(P("Target players are incorrectly specified.")) + .Append(callData.errorCause); + break; + case CET_EmptyTargetList: + builder.Append(P("List of target players is empty")) + .Append(callData.errorCause); + break; + default: + } + result = builder.Copy(); + builder.FreeSelf(); + return result; } // Auxiliary method for parsing list of targeted players. // Assumes given parser is not `none` and not in a failed state. -private final function array ParseTargets( +// If parsing failed, guaranteed to return an empty array. +private final function array ParseTargets( Parser parser, - APlayer callerPlayer) + EPlayer callerPlayer) { - local array targetPlayers; + local array targetPlayers; local PlayersParser targetsParser; targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser')); targetsParser.SetSelf(callerPlayer); targetsParser.ParseWith(parser); - targetPlayers = targetsParser.GetPlayers(); + if (parser.Ok()) { + targetPlayers = targetsParser.GetPlayers(); + } targetsParser.FreeSelf(); return targetPlayers; } @@ -384,6 +513,7 @@ public final function Text GetName() return commandData.name.LowerCopy(); } +// TODO: use `SharedRef` instead /** * Returns `Command.Data` struct that describes caller `Command`. * diff --git a/sources/Commands/CommandCall.uc b/sources/Commands/CommandCall.uc deleted file mode 100644 index d10b86f..0000000 --- a/sources/Commands/CommandCall.uc +++ /dev/null @@ -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 . - */ -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 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 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 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." -} \ No newline at end of file diff --git a/sources/Commands/CommandParser.uc b/sources/Commands/CommandParser.uc index 0e142fe..c0ff2bd 100644 --- a/sources/Commands/CommandParser.uc +++ b/sources/Commands/CommandParser.uc @@ -1,9 +1,9 @@ /** - * Auxiliary class for parsing user's input into a `CommandCall` based on + * Auxiliary class for parsing user's input into a `Command.CallData` based on * a given `Command.Data`. While it's meant to be allocated for * a `self.ParseWith()` call and deallocated right after, it can be reused * without deallocation. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -21,7 +21,6 @@ * along with Acedia. If not, see . */ class CommandParser extends AcediaObject - dependson(CommandCall) dependson(Command); /** @@ -78,7 +77,7 @@ var private Command.SubCommand pickedSubCommand; var private array availableOptions; // Result variable we are filling during the parsing process, // should be `none` outside of `self.ParseWith()` method call. -var private CommandCall nextResult; +var private Command.CallData nextResult; // Describes which parameters we are currently parsing, classifying them // as either "necessary" or "extra". @@ -131,8 +130,9 @@ protected function Finalizer() // Zero important variables private final function Reset() { + local Command.CallData blankCallData; commandParser = none; - nextResult = none; + nextResult = blankCallData; currentTarget = CPT_NecessaryParameter; currentTargetIsOption = false; usedOptions.length = 0; @@ -143,8 +143,9 @@ private final function DeclareError( Command.ErrorType type, optional Text cause) { - if (nextResult != none) { - nextResult.DeclareError(type, cause); + nextResult.parsingError = type; + if (cause != none) { + nextResult.errorCause = cause.Copy(); } if (commandParser != none) { commandParser.Fail(); @@ -200,50 +201,51 @@ private final function PickSubCommand(Command.Data commandData) * to be parsed as a command's call. * @param commandData Describes what parameters and options should be * expected in user's input. `Text` values from `commandData` can be used - * inside resulting object `CommandCall`, so deallocating them can + * inside resulting `Command.CallData`, so deallocating them can * invalidate returned value. - * @return Results of parsing as described by `CommandCall`. + * @return Results of parsing, described by `Command.CallData`. * Returned object is guaranteed to be not `none`. */ -public final function CommandCall ParseWith( +public final function Command.CallData ParseWith( Parser parser, Command.Data commandData) { local AssociativeArray commandParameters; // Temporary object to return `nextResult` while setting variable to `none` - local CommandCall toReturn; - Reset(); - nextResult = CommandCall(_.memory.Allocate(class'CommandCall')); + local Command.CallData toReturn; + nextResult.parameters = _.collections.EmptyAssociativeArray(); + nextResult.options = _.collections.EmptyAssociativeArray(); if (commandData.subCommands.length == 0) { DeclareError(CET_NoSubCommands, none); - return nextResult; + toReturn = nextResult; + Reset(); + return toReturn; } if (parser == none || !parser.Ok()) { DeclareError(CET_BadParser, none); - return nextResult; + toReturn = nextResult; + Reset(); + return toReturn; } commandParser = parser; availableOptions = commandData.options; // (subcommand) (parameters, possibly with options) and nothing else! PickSubCommand(commandData); - nextResult.SetSubCommand(pickedSubCommand.name); + nextResult.subCommandName = pickedSubCommand.name.Copy(); commandParameters = ParseParameterArrays( pickedSubCommand.required, pickedSubCommand.optional); AssertNoTrailingInput(); // make sure there is nothing else if (commandParser.Ok()) { - nextResult.SetParameters(commandParameters); + nextResult.parameters = commandParameters; } else { _.memory.Free(commandParameters); } // Clean up - commandParser = none; - usedOptions.length = 0; - currentTargetIsOption = false; - toReturn = nextResult; - nextResult = none; + toReturn = nextResult; + Reset(); return toReturn; } @@ -338,7 +340,7 @@ private final function ParseOptionalParameterArray( if (!ParseParameter(parsedParameters, optionalParameters[i])) { // Propagate errors - if (!nextResult.IsSuccessful()) { + if (nextResult.parsingError != CET_None) { return; } // Failure to parse optional parameter is fine if @@ -379,7 +381,7 @@ private final function bool ParseParameter( } // We only succeeded in parsing if we've parsed enough for // a given parameter and did not encounter any errors - if (parsedEnough && nextResult.IsSuccessful()) { + if (parsedEnough && nextResult.parsingError == CET_None) { commandParser.RestoreState(confirmedState); return true; } @@ -416,7 +418,7 @@ private final function bool ParseSingleValue( } while (TryParsingOptions()); // Propagate errors after parsing options - if (!nextResult.IsSuccessful()) { + if (nextResult.parsingError != CET_None) { return false; } // Try parsing one of the variable types @@ -685,7 +687,7 @@ private final function bool TryParsingOptions() // possible parameters with `commandParser`. // Returns `true` on success and `false` otherwise. At the point this // method is called, option declaration is already assumed to be detected -// and any failure implies parsing error (ending in failed `CommandCall`). +// and any failure implies parsing error (ending in failed `Command.CallData`). private final function bool ParseLongOption() { local int i, optionIndex; @@ -724,7 +726,7 @@ private final function bool ParseLongOption() // possible parameters with `commandParser`. // Returns `true` on success and `false` otherwise. At the point this // method is called, option declaration is already assumed to be detected -// and any failure implies parsing error (ending in failed `CommandCall`). +// and any failure implies parsing error (ending in failed `Command.CallData`). private final function bool ParseShortOption() { local int i; @@ -738,14 +740,14 @@ private final function bool ParseShortOption() } for (i = 0; i < optionsList.GetLength(); i += 1) { - if (!nextResult.IsSuccessful()) break; + if (nextResult.parsingError != CET_None) break; pickedOptionWithParameters = AddOptionByCharacter( optionsList.GetCharacter(i), optionsList, pickedOptionWithParameters) || pickedOptionWithParameters; } optionsList.FreeSelf(); - return nextResult.IsSuccessful(); + return (nextResult.parsingError == CET_None); } // Assumes `commandParser` and `nextResult` are not `none`. @@ -819,7 +821,9 @@ private final function bool ParseOptionParameters(Command.Option pickedOption) } if (pickedOption.required.length == 0 && pickedOption.optional.length == 0) { - nextResult.SetOptionParameters(pickedOption, none); + // Here `optionParameters == none` + nextResult.options + .SetItem(pickedOption.longName, optionParameters, true); return true; } currentTargetIsOption = true; @@ -829,7 +833,8 @@ private final function bool ParseOptionParameters(Command.Option pickedOption) currentTargetIsOption = false; if (commandParser.Ok()) { - nextResult.SetOptionParameters(pickedOption, optionParameters); + nextResult.options + .SetItem(pickedOption.longName, optionParameters, true); return true; } return false; diff --git a/sources/Commands/Commands_Feature.uc b/sources/Commands/Commands_Feature.uc index a064b5c..9760f36 100644 --- a/sources/Commands/Commands_Feature.uc +++ b/sources/Commands/Commands_Feature.uc @@ -3,7 +3,7 @@ * parse their arguments into standard Acedia collection. It also allows to * manage them (and specify limitation on how they can be called) in a * centralized manner. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -202,18 +202,22 @@ public final function array GetCommandNames() * contain command's name and it's parameters. * @param callerPlayer Player that caused this command call. */ -public final function HandleInput(Parser parser, APlayer callerPlayer) +public final function HandleInput(Parser parser, EPlayer callerPlayer) { - local Command commandInstance; - local MutableText commandName; + local Command commandInstance; + local Command.CallData callData; + local MutableText commandName; if (parser == none) return; if (!parser.Ok()) return; parser.MUntilMany(commandName, commandDelimiters, true, true); commandInstance = GetCommand(commandName); commandName.FreeSelf(); - if (parser.Ok() && commandInstance != none) { - commandInstance.ProcessInput(parser, callerPlayer).FreeSelf(); + if (parser.Ok() && commandInstance != none) + { + callData = commandInstance.ParseInputWith(parser, callerPlayer); + commandInstance.Execute(callData, callerPlayer); + commandInstance.DeallocateCallData(callData); } } @@ -223,10 +227,9 @@ private function bool HandleText( name messageType, bool teamMessage) { - local Text messageAsText; - local APlayer callerPlayer; - local Parser parser; - local PlayerService service; + local Text messageAsText; + local EPlayer callerPlayer; + local Parser parser; // We only want to catch chat messages // and only if `Commands` feature is active if (messageType != 'Say') return true; @@ -242,13 +245,11 @@ private function bool HandleText( messageAsText.FreeSelf(); return true; } - // Extract `APlayer` from the `sender` - service = PlayerService(class'PlayerService'.static.Require()); - if (service != none) { - callerPlayer = service.GetPlayer(PlayerController(sender)); - } + // Extract `EPlayer` from the `sender` + callerPlayer = _.players.FromController(PlayerController(sender)); // Pass input to command feature HandleInput(parser, callerPlayer); + _.memory.Free(callerPlayer); parser.FreeSelf(); return false; } diff --git a/sources/Commands/PlayersParser.uc b/sources/Commands/PlayersParser.uc index 82ddff8..0d9546b 100644 --- a/sources/Commands/PlayersParser.uc +++ b/sources/Commands/PlayersParser.uc @@ -1,6 +1,6 @@ /** * Object for parsing what converting textual description of a group of - * players into array of `APlayer`s. Depends on the game context. + * players into array of `EPlayer`s. Depends on the game context. * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -62,12 +62,12 @@ class PlayersParser extends AcediaObject */ // Player for which "@" and "@self" macros will refer -var private APlayer selfPlayer; +var private EPlayer selfPlayer; // Copy of the list of current players at the moment of allocation of // this `PlayersParser`. -var private array playersSnapshot; +var private array playersSnapshot; // Players, selected according to selectors we have parsed so far -var private array currentSelection; +var private array currentSelection; // Have we parsed our first selector? // We need this to know whether to start with the list of // all players (if first selector removes them) or @@ -81,6 +81,10 @@ var const int TOPEN_BRACKET, TCLOSE_BRACKET; protected function Finalizer() { + // No need to deallocate `currentSelection`, + // since it has `EPlayer`s from `playersSnapshot` or `selfPlayer` + _.memory.Free(selfPlayer); + _.memory.FreeMany(playersSnapshot); selfPlayer = none; parsedFirstSelector = false; playersSnapshot.length = 0; @@ -94,15 +98,18 @@ protected function Finalizer() * "@self" macros. Passing `none` will make it so no one is * referred by them. */ -public final function SetSelf(APLayer newSelfPlayer) +public final function SetSelf(EPlayer newSelfPlayer) { - selfPlayer = newSelfPlayer; + _.memory.Free(selfPlayer); + if (newSelfPlayer != none) { + selfPlayer = EPlayer(newSelfPlayer.Copy()); + } } // Insert a new player into currently selected list of players // (`currentSelection`) such that there will be no duplicates. // `none` values are auto-discarded. -private final function InsertPlayer(APLayer toInsert) +private final function InsertPlayer(EPlayer toInsert) { local int i; if (toInsert == none) { @@ -391,9 +398,17 @@ private final function MutableText ParseLiteral(Parser parser) * * @return players parsed by the last `ParseWith()` or `Parse()` call. */ -public final function array GetPlayers() +public final function array GetPlayers() { - return currentSelection; + local int i; + local array result; + for (i = 0; i < currentSelection.length; i += 1) + { + if (currentSelection[i].IsExistent()) { + result[result.length] = EPlayer(currentSelection[i].Copy()); + } + } + return result; } /** @@ -410,6 +425,7 @@ public final function bool ParseWith(Parser parser) local Parser.ParserState confirmedState; if (parser == none) return false; if (!parser.Ok()) return false; + Reset(); confirmedState = parser.Skip().GetCurrentState(); if (!parser.Match(T(TOPEN_BRACKET)).Ok()) @@ -441,14 +457,11 @@ public final function bool ParseWith(Parser parser) // `playersSnapshot` to contain current players. private final function Reset() { - local PlayerService service; parsedFirstSelector = false; - playersSnapshot.length = 0; currentSelection.length = 0; - service = PlayerService(class'PlayerService'.static.Require()); - if (service != none) { - playersSnapshot = service.GetAllPlayers(); - } + _.memory.FreeMany(playersSnapshot); + playersSnapshot.length = 0; + playersSnapshot = _.players.GetAll(); selectorDelimiters.length = 0; selectorDelimiters[0] = T(TCOMMA); selectorDelimiters[1] = T(TCLOSE_BRACKET); diff --git a/sources/Commands/Tests/TEST_Command.uc b/sources/Commands/Tests/TEST_Command.uc index d418e70..a7caf68 100644 --- a/sources/Commands/Tests/TEST_Command.uc +++ b/sources/Commands/Tests/TEST_Command.uc @@ -1,6 +1,6 @@ /** * Set of tests for `Command` class. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -40,8 +40,8 @@ protected static function TESTS() Test_MockA(); Context("Testing `Command` parsing (options)."); Test_MockB(); - Context("Testing `CommandCall` error messages."); - Test_CommandCallErrors(); + Context("Testing `Command.CallData` error messages."); + Test_CallDataErrors(); Context("Testing sub-command determination."); Test_SubCommandName(); } @@ -62,131 +62,132 @@ protected static function Test_MockB() SubTest_MockBQ3Remainder(); } -protected static function Test_CommandCallErrors() +protected static function Test_CallDataErrors() { - SubTest_CommandCallErrorBadParser(); - SubTest_CommandCallErrorNoRequiredParam(); - SubTest_CommandCallErrorUnknownOption(); - SubTest_CommandCallErrorRepeatedOption(); - SubTest_CommandCallErrorMultipleOptionsWithParams(); - SubTest_CommandCallErrorUnusedCommandParameters(); - SubTest_CommandCallErrorNoRequiredParamForOption(); + SubTest_CallDataErrorBadParser(); + SubTest_CallDataErrorNoRequiredParam(); + SubTest_CallDataErrorUnknownOption(); + SubTest_CallDataErrorRepeatedOption(); + SubTest_CallDataErrorMultipleOptionsWithParams(); + SubTest_CallDataErrorUnusedCommandParameters(); + SubTest_CallDataErrorNoRequiredParamForOption(); } -protected static function SubTest_CommandCallErrorBadParser() +protected static function SubTest_CallDataErrorBadParser() { - local CommandCall result; + local Command.CallData result; Issue("`CET_BadParser` errors are incorrectly reported."); - result = class'MockCommandA'.static.GetInstance().ProcessInput(none, none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_BadParser);// - TEST_ExpectNone(result.GetErrorCause()); result = class'MockCommandA'.static.GetInstance() - .ProcessInput(__().text.ParseString("stuff").Fail(), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_BadParser);// - TEST_ExpectNone(result.GetErrorCause()); + .ParseInputWith(none, none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_BadParser); + TEST_ExpectNone(result.errorCause); + result = class'MockCommandA'.static.GetInstance() + .ParseInputWith(__().text.ParseString("stuff").Fail(), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_BadParser); + TEST_ExpectNone(result.errorCause); } -protected static function SubTest_CommandCallErrorNoRequiredParam() +protected static function SubTest_CallDataErrorNoRequiredParam() { - local CommandCall result; + local Command.CallData result; Issue("`CET_NoRequiredParam` errors are incorrectly reported."); result = class'MockCommandA'.static.GetInstance() - .ProcessInput(PRS(default.queryAFailure1), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_NoRequiredParam); - TEST_ExpectTrue( result.GetErrorCause().ToString() + .ParseInputWith(PRS(default.queryAFailure1), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_NoRequiredParam); + TEST_ExpectTrue( result.errorCause.ToString() == "integer variable"); result = class'MockCommandA'.static.GetInstance() - .ProcessInput(PRS(default.queryAFailure2), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_NoRequiredParam); - TEST_ExpectTrue( result.GetErrorCause().ToString() + .ParseInputWith(PRS(default.queryAFailure2), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_NoRequiredParam); + TEST_ExpectTrue( result.errorCause.ToString() == "isItSimple?"); } -protected static function SubTest_CommandCallErrorUnknownOption() +protected static function SubTest_CallDataErrorUnknownOption() { - local CommandCall result; + local Command.CallData result; Issue("`CET_UnknownOption` errors are incorrectly reported."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBFailureUnknownOptionLong), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_UnknownOption); - TEST_ExpectTrue( result.GetErrorCause().ToString() + .ParseInputWith(PRS(default.queryBFailureUnknownOptionLong), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_UnknownOption); + TEST_ExpectTrue( result.errorCause.ToString() == "kest"); Issue("`CET_UnknownShortOption` errors are incorrectly reported."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBFailureUnknownOptionShort), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_UnknownShortOption); - TEST_ExpectNone(result.GetErrorCause()); + .ParseInputWith(PRS(default.queryBFailureUnknownOptionShort), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_UnknownShortOption); + TEST_ExpectNone(result.errorCause); } -protected static function SubTest_CommandCallErrorRepeatedOption() +protected static function SubTest_CallDataErrorRepeatedOption() { - local CommandCall result; + local Command.CallData result; Issue("`CET_RepeatedOption` errors are incorrectly reported."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBFailure2), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_RepeatedOption); - TEST_ExpectTrue( result.GetErrorCause().ToString() + .ParseInputWith(PRS(default.queryBFailure2), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_RepeatedOption); + TEST_ExpectTrue( result.errorCause.ToString() == "forced"); } -protected static function SubTest_CommandCallErrorUnusedCommandParameters() +protected static function SubTest_CallDataErrorUnusedCommandParameters() { - local CommandCall result; + local Command.CallData result; Issue("`CET_UnusedCommandParameters` errors are incorrectly reported."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBFailureUnused), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_UnusedCommandParameters); - TEST_ExpectTrue( result.GetErrorCause().ToString() + .ParseInputWith(PRS(default.queryBFailureUnused), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_UnusedCommandParameters); + TEST_ExpectTrue( result.errorCause.ToString() == "text -j"); } -protected static function SubTest_CommandCallErrorMultipleOptionsWithParams() +protected static function SubTest_CallDataErrorMultipleOptionsWithParams() { - local CommandCall result; + local Command.CallData result; Issue("`CET_MultipleOptionsWithParams` errors are incorrectly reported."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBFailure1), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_MultipleOptionsWithParams); - TEST_ExpectTrue(result.GetErrorCause().ToString() == "tv"); + .ParseInputWith(PRS(default.queryBFailure1), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_MultipleOptionsWithParams); + TEST_ExpectTrue(result.errorCause.ToString() == "tv"); } -protected static function SubTest_CommandCallErrorNoRequiredParamForOption() +protected static function SubTest_CallDataErrorNoRequiredParamForOption() { - local CommandCall result; + local Command.CallData result; Issue("`CET_NoRequiredParamForOption` errors are incorrectly reported."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBFailureNoReqParamOption1), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_NoRequiredParamForOption); - TEST_ExpectTrue(result.GetErrorCause().ToString() == "long"); + .ParseInputWith(PRS(default.queryBFailureNoReqParamOption1), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_NoRequiredParamForOption); + TEST_ExpectTrue(result.errorCause.ToString() == "long"); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBFailureNoReqParamOption2), none); - TEST_ExpectFalse(result.IsSuccessful()); - TEST_ExpectTrue(result.GetError() == CET_NoRequiredParamForOption); - TEST_ExpectTrue(result.GetErrorCause().ToString() == "values"); + .ParseInputWith(PRS(default.queryBFailureNoReqParamOption2), none); + TEST_ExpectFalse(result.parsingError == CET_None); + TEST_ExpectTrue(result.parsingError == CET_NoRequiredParamForOption); + TEST_ExpectTrue(result.errorCause.ToString() == "values"); } protected static function Test_SubCommandName() { - local CommandCall result; + local Command.CallData result; Issue("Cannot determine subcommands."); result = class'MockCommandA'.static.GetInstance() - .ProcessInput(PRS(default.queryASuccess1), none); - TEST_ExpectTrue(result.GetSubCommand().ToString() == "simple"); + .ParseInputWith(PRS(default.queryASuccess1), none); + TEST_ExpectTrue(result.subCommandName.ToString() == "simple"); Issue("Cannot determine when subcommands are missing."); result = class'MockCommandA'.static.GetInstance() - .ProcessInput(PRS(default.queryASuccess2), none); - TEST_ExpectTrue(result.GetSubCommand().IsEmpty()); + .ParseInputWith(PRS(default.queryASuccess2), none); + TEST_ExpectTrue(result.subCommandName.IsEmpty()); } protected static function SubTest_MockAQ1AndFailed() @@ -199,14 +200,16 @@ protected static function SubTest_MockAQ1AndFailed() command = class'MockCommandA'.static.GetInstance(); Issue("Command queries that should fail succeed instead."); parser.InitializeS(default.queryAFailure1); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); parser.InitializeS(default.queryAFailure2); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); Issue("Cannot parse command queries without optional parameters."); parameters = - command.ProcessInput(parser.InitializeS(default.queryASuccess1), none) - .GetParameters(); + command.ParseInputWith(parser.InitializeS(default.queryASuccess1), none) + .Parameters; TEST_ExpectTrue(parameters.GetLength() == 2); paramArray = DynamicArray(parameters.GetItem(P("isItSimple?"))); TEST_ExpectTrue(paramArray.GetLength() == 1); @@ -222,7 +225,7 @@ protected static function SubTest_MockAQ2() local AssociativeArray result, subObject; Issue("Cannot parse command queries without optional parameters."); result = class'MockCommandA'.static.GetInstance() - .ProcessInput(PRS(default.queryASuccess2), none).GetParameters(); + .ParseInputWith(PRS(default.queryASuccess2), none).Parameters; TEST_ExpectTrue(result.GetLength() == 2); subObject = AssociativeArray(result.GetItem(P("just_obj"))); TEST_ExpectTrue(IntBox(subObject.GetItem(P("var"))).Get() == 7); @@ -252,7 +255,7 @@ protected static function SubTest_MockAQ3() local AssociativeArray result; Issue("Cannot parse command queries with optional parameters."); result = class'MockCommandA'.static.GetInstance() - .ProcessInput(PRS(default.queryASuccess3), none).GetParameters(); + .ParseInputWith(PRS(default.queryASuccess3), none).Parameters; // Booleans paramArray = DynamicArray(result.GetItem(P("isItSimple?"))); TEST_ExpectTrue(paramArray.GetLength() == 7); @@ -286,7 +289,7 @@ protected static function SubTest_MockAQ4() local AssociativeArray result, subObject; Issue("Cannot parse command queries with optional parameters."); result = class'MockCommandA'.static.GetInstance() - .ProcessInput(PRS(default.queryASuccess4), none).GetParameters(); + .ParseInputWith(PRS(default.queryASuccess4), none).Parameters; TEST_ExpectTrue(result.GetLength() == 3); subObject = AssociativeArray(result.GetItem(P("just_obj"))); TEST_ExpectTrue(IntBox(subObject.GetItem(P("var"))).Get() == 7); @@ -306,32 +309,40 @@ protected static function SubTest_MockBFailed() command = class'MockCommandB'.static.GetInstance(); Issue("Command queries that should fail succeed instead."); parser.InitializeS(default.queryBFailure1); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); parser.InitializeS(default.queryBFailure2); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); parser.InitializeS(default.queryBFailure3); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); parser.InitializeS(default.queryBFailureNoReqParamOption1); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); parser.InitializeS(default.queryBFailureNoReqParamOption2); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); parser.InitializeS(default.queryBFailureUnknownOptionLong); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); parser.InitializeS(default.queryBFailureUnknownOptionShort); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); parser.InitializeS(default.queryBFailureUnused); - TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful()); + TEST_ExpectFalse( + command.ParseInputWith(parser, none).parsingError == CET_None); } protected static function SubTest_MockBQ1() { - local CommandCall result; + local Command.CallData result; local DynamicArray subArray; local AssociativeArray params, options, subObject; Issue("Cannot parse command queries with options."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBSuccess1), none); - params = result.GetParameters(); + .ParseInputWith(PRS(default.queryBSuccess1), none); + params = result.Parameters; TEST_ExpectTrue(params.GetLength() == 2); subArray = DynamicArray(params.GetItem(P("just_array"))); TEST_ExpectTrue(subArray.GetLength() == 2); @@ -339,7 +350,7 @@ protected static function SubTest_MockBQ1() TEST_ExpectNone(subArray.GetItem(1)); TEST_ExpectTrue( Text(params.GetItem(P("just_text"))).ToString() == "text"); - options = result.GetOptions(); + options = result.options; TEST_ExpectTrue(options.GetLength() == 1); subObject = AssociativeArray(options.GetItem(P("values"))); TEST_ExpectTrue(subObject.GetLength() == 1); @@ -354,14 +365,14 @@ protected static function SubTest_MockBQ1() protected static function SubTest_MockBQ2() { - local CommandCall result; + local Command.CallData result; local DynamicArray subArray; local AssociativeArray options, subObject; Issue("Cannot parse command queries with mixed-in options."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBSuccess2), none); - TEST_ExpectTrue(result.GetParameters().GetLength() == 0); - options = result.GetOptions(); + .ParseInputWith(PRS(default.queryBSuccess2), none); + TEST_ExpectTrue(result.Parameters.GetLength() == 0); + options = result.options; TEST_ExpectTrue(options.GetLength() == 7); TEST_ExpectTrue(options.HasKey(P("actual"))); TEST_ExpectNone(options.GetItem(P("actual"))); @@ -384,17 +395,17 @@ protected static function SubTest_MockBQ2() protected static function SubTest_MockBQ3Remainder() { - local CommandCall result; + local Command.CallData result; local DynamicArray subArray; local AssociativeArray options, subObject; Issue("Cannot parse command queries with `CPT_Remainder` type parameters."); result = class'MockCommandB'.static.GetInstance() - .ProcessInput(PRS(default.queryBSuccess3), none); - TEST_ExpectTrue(result.GetParameters().GetLength() == 1); - subArray = DynamicArray(result.GetParameters().GetItem(P("list"))); + .ParseInputWith(PRS(default.queryBSuccess3), none); + TEST_ExpectTrue(result.Parameters.GetLength() == 1); + subArray = DynamicArray(result.Parameters.GetItem(P("list"))); TEST_ExpectTrue(FloatBox(subArray.GetItem(0)).Get() == 3); TEST_ExpectTrue(FloatBox(subArray.GetItem(1)).Get() == -76); - options = result.GetOptions(); + options = result.Options; TEST_ExpectTrue(options.GetLength() == 1); TEST_ExpectTrue(options.HasKey(P("remainder"))); subObject = AssociativeArray(options.GetItem(P("remainder"))); diff --git a/sources/Console/ConsoleAPI.uc b/sources/Console/ConsoleAPI.uc index 0c836f9..66ed7e0 100644 --- a/sources/Console/ConsoleAPI.uc +++ b/sources/Console/ConsoleAPI.uc @@ -221,7 +221,7 @@ public final function ConsoleWriter ForAll() * write into consoles of all players. * Guaranteed to not be `none`. */ -public final function ConsoleWriter For(APlayer targetPlayer) +public final function ConsoleWriter For(EPlayer targetPlayer) { local ConsoleDisplaySettings globalSettings; globalSettings.defaultColor = defaultColor; diff --git a/sources/Console/ConsoleWriter.uc b/sources/Console/ConsoleWriter.uc index 1f076ea..55bcf15 100644 --- a/sources/Console/ConsoleWriter.uc +++ b/sources/Console/ConsoleWriter.uc @@ -44,7 +44,8 @@ enum ConsoleWriterTarget var private ConsoleWriterTarget targetType; // Players that will receive output passed to this `ConsoleWriter`. // Only used when `targetType == CWTARGET_Players` -var private array outputTargets; +// Cannot be allowed to contain `none` values. +var private array outputTargets; var private ConsoleBuffer outputBuffer; var private bool needToResetColor; @@ -59,6 +60,14 @@ var private ConsoleAPI.ConsoleDisplaySettings displaySettings; // color information instead. var private Color defaultColor; +protected function Finalizer() +{ + _.memory.FreeMany(outputTargets); + _.memory.Free(outputBuffer); + outputTargets.length = 0; + outputBuffer = none; +} + public final function ConsoleWriter Initialize( ConsoleAPI.ConsoleDisplaySettings newDisplaySettings) { @@ -326,7 +335,7 @@ public final function ConsoleWriter ForAll() * throw messages away. * @return Returns caller `ConsoleWriter` to allow for method chaining. */ -public final function ConsoleWriter ForPlayer(APlayer targetPlayer) +public final function ConsoleWriter ForPlayer(EPlayer targetPlayer) { if (targetPlayer == none) { @@ -339,7 +348,7 @@ public final function ConsoleWriter ForPlayer(APlayer targetPlayer) } outputTargets.length = 0; targetType = CWTARGET_Players; - outputTargets[0] = targetPlayer; + outputTargets[0] = EPlayer(targetPlayer.Copy()); return self; } @@ -353,15 +362,16 @@ public final function ConsoleWriter ForPlayer(APlayer targetPlayer) * If `none` - this method will do nothing. * @return Returns caller `ConsoleWriter` to allow for method chaining. */ -public final function ConsoleWriter AndPlayer(APlayer targetPlayer) +public final function ConsoleWriter AndPlayer(EPlayer targetPlayer) { local int i; - if (targetPlayer == none) return self; - if (!targetPlayer.IsConnected()) return self; + if (targetPlayer == none) return self; + if (!targetPlayer.IsExistent()) return self; if (targetType != CWTARGET_Players) { Flush(); + _.memory.FreeMany(outputTargets); if (targetType == CWTARGET_None) { outputTargets.length = 0; } @@ -372,12 +382,12 @@ public final function ConsoleWriter AndPlayer(APlayer targetPlayer) targetType = CWTARGET_Players; for (i = 0; i < outputTargets.length; i += 1) { - if (outputTargets[i] == targetPlayer) { + if (targetPlayer.SameAs(outputTargets[i])) { return self; } } Flush(); - outputTargets[outputTargets.length] = targetPlayer; + outputTargets[outputTargets.length] = EPlayer(targetPlayer.Copy()); return self; } @@ -391,24 +401,26 @@ public final function ConsoleWriter AndPlayer(APlayer targetPlayer) * If `none` - this method will do nothing. * @return Returns caller `ConsoleWriter` to allow for method chaining. */ -public final function ConsoleWriter ButPlayer(APlayer playerToRemove) +public final function ConsoleWriter ButPlayer(EPlayer playerToRemove) { local int i; if (targetType == CWTARGET_None) return self; if (playerToRemove == none) return self; - if (!playerToRemove.IsConnected()) return self; + if (!playerToRemove.IsExistent()) return self; if (targetType == CWTARGET_All) { Flush(); + _.memory.FreeMany(outputTargets); outputTargets = _.players.GetAll(); } targetType = CWTARGET_Players; while (i < outputTargets.length) { - if (outputTargets[i] == playerToRemove) + if (playerToRemove.SameAs(outputTargets[i])) { Flush(); + _.memory.Free(outputTargets[i]); outputTargets.Remove(i, 1); break; } @@ -432,39 +444,45 @@ public final function ConsoleWriterTarget CurrentTarget() } /** - * Returns `APlayer` to whom console caller `ConsoleWriter` is + * Returns `EPlayer` to whom console caller `ConsoleWriter` is * outputting messages. * If caller `ConsoleWriter` is setup to message several different players, * returns an arbitrary one of them. * - * @return Player (`APlayer` class) to whom console caller `ConsoleWriter` is + * @return Player (`EPlayer` class) to whom console caller `ConsoleWriter` is * outputting messages. Returns `none` iff it currently outputs to * every player or to no one. */ -public final function APlayer GetTargetPlayer() +public final function EPlayer GetTargetPlayer() { if (targetType == CWTARGET_All) return none; - if (outputTargets.length <= 0) return none; - return outputTargets[0]; + if (outputTargets.length <= 0) return none; + + return EPlayer(outputTargets[0].Copy()); } /** - * Returns `APlayer` to whom console caller `ConsoleWriter` is + * Returns array of `EPlayer`s, to whom console caller `ConsoleWriter` is * outputting messages. - * If caller `ConsoleWriter` is setup to message several different players, - * returns an arbitrary one of them. * - * @return Player (`APlayer` class) to whom console caller `ConsoleWriter` is - * outputting messages. Returns `none` iff it currently outputs to - * every player or to no one. + * @return Player (`EPlayer` class) to whom console caller `ConsoleWriter` is + * outputting messages. Returned array is guaranteed to not contain `none`s + * or `EPlayer` interfaces with non-existent status. */ -public final function array GetTargetPlayers() +public final function array GetTargetPlayers() { - local array emptyArray; - if (targetType == CWTARGET_None) return emptyArray; + local int i; + local array result; + if (targetType == CWTARGET_None) return result; if (targetType == CWTARGET_All) return _.players.GetAll(); - return outputTargets; + for (i = 0; i < outputTargets.length; i += 1) + { + if (outputTargets[i].IsExistent()) { + result[result.length] = EPlayer(outputTargets[i].Copy()); + } + } + return result; } /** @@ -576,8 +594,7 @@ private final function SendBuffer(optional bool asIndented, optional bool asSay) } } -// Assumes `playerService != none` and `connectionService != none`, -// caller function must ensure that. +// Assumes `connectionService != none`, caller function must ensure that. private final function SendConsoleMessage( array recipients, string message, @@ -604,7 +621,6 @@ private final function array GetRecipientsControllers() { local int i; local PlayerController nextRecipient; - local PlayerService playerService; local ConnectionService connectionService; local array recipients; local array connections; @@ -612,16 +628,12 @@ private final function array GetRecipientsControllers() if (targetType == CWTARGET_None) { return recipients; } - // Single target case + // Selected targets case if (targetType != CWTARGET_All) { - playerService = PlayerService(class'PlayerService'.static.Require()); - if (playerService == none) { - return recipients; - } for (i = 0; i < outputTargets.length; i += 1) { - nextRecipient = playerService.GetController(outputTargets[i]); + nextRecipient = outputTargets[i].GetController(); if (nextRecipient != none) { recipients[recipients.length] = nextRecipient; } diff --git a/sources/Gameplay/BaseClasses/BaseFrontend.uc b/sources/Gameplay/BaseClasses/Frontend/BaseFrontend.uc similarity index 100% rename from sources/Gameplay/BaseClasses/BaseFrontend.uc rename to sources/Gameplay/BaseClasses/Frontend/BaseFrontend.uc diff --git a/sources/Gameplay/BaseClasses/Frontend/EInterface.uc b/sources/Gameplay/BaseClasses/Frontend/EInterface.uc new file mode 100644 index 0000000..cb580ea --- /dev/null +++ b/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 . + */ + 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 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 +{ +} \ No newline at end of file diff --git a/sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/KFFrontend.uc similarity index 100% rename from sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc rename to sources/Gameplay/BaseClasses/KillingFloor/Frontend/KFFrontend.uc diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ATradingComponent.uc similarity index 95% rename from sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc rename to sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ATradingComponent.uc index bf1804a..2fe9a2a 100644 --- a/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc +++ b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ATradingComponent.uc @@ -1,6 +1,6 @@ /** * Subset of functionality for dealing with everything related to traders. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -58,7 +58,7 @@ public final function SimpleSlot OnStart(AcediaObject receiver) * Signal that will be emitted whenever trading time ends. * * [Signature] - * void (ATrader oldTrader, ATrader newTrader) + * void (ETrader oldTrader, ETrader newTrader) * * @param oldTrader Trader that was selected before this event. * @param newTrader Trader that will be selected after this event. @@ -90,7 +90,7 @@ public final function Trading_OnSelect_Slot OnTraderSelected( * `none`-references. None of them should be deallocated, * otherwise Acedia's behavior is undefined. */ -public function array GetTraders(); +public function array GetTraders(); /** * Checks whether trading is currently active. @@ -188,19 +188,19 @@ public function SetCountDownPause(bool doPause); * Changing a selected trader in any way should always be followed * by emitting `OnTraderSelected()` signal. * After `SelectTrader()` call `GetSelectedTrader()` should return - * specified `ATrader`. If selected trader changes in some other way, it should + * specified `ETrader`. If selected trader changes in some other way, it should * first result in emitted `OnTraderSelected()` signal. * * @return Currently selected trader. */ -public function ATrader GetSelectedTrader(); +public function ETrader GetSelectedTrader(); /** * Changes currently selected trader. * * @see `GetSelectedTrader()` for more details. */ -public function SelectTrader(ATrader newSelection); +public function SelectTrader(ETrader newSelection); defaultproperties { diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATrader.uc b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ETrader.uc similarity index 76% rename from sources/Gameplay/BaseClasses/KillingFloor/Trading/ATrader.uc rename to sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ETrader.uc index c98d36c..138016e 100644 --- a/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATrader.uc +++ b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/ETrader.uc @@ -2,7 +2,7 @@ * Class, objects of which are expected to represent traders located on * the map. In classic KF game mode it would represent areas behind closed * doors that open during trader time and allow to purchase weapons and ammo. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -19,7 +19,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class ATrader extends AcediaObject +class ETrader extends EInterface abstract; /** @@ -60,7 +60,7 @@ public function Text GetName(); * @param newName New name of the caller trader. * @return `true` if trader is currently enabled and `false` otherwise. */ -public function ATrader SetName(Text newName); +public function ETrader SetName(Text newName); /** * Checks if caller trader is currently enabled. @@ -78,7 +78,7 @@ public function ATrader SetName(Text newName); public function bool IsEnabled(); /** - * Sets whether caller `ATrader`'s is currently enabled. + * Sets whether caller `ETrader`'s is currently enabled. * * Disabling the trader should automatically "boot" players out * (see `BootPlayers()`). @@ -87,14 +87,14 @@ public function bool IsEnabled(); * * @param doEnable `true` if trader is currently enabled and * `false` otherwise. - * @return Caller `ATrader` to allow for method chaining. + * @return Caller `ETrader` to allow for method chaining. */ -public function ATrader SetEnabled(bool doEnable); +public function ETrader SetEnabled(bool doEnable); /** - * Checks whether caller `ATrader` will auto-open when trading gets activated. + * Checks whether caller `ETrader` will auto-open when trading gets activated. * - * This setting must be ignored if trader is disabled, but disabling `ATrader` + * This setting must be ignored if trader is disabled, but disabling `ETrader` * should not reset it. * * @return `true` if trader is marked to always auto-open upon activating @@ -103,28 +103,28 @@ public function ATrader SetEnabled(bool doEnable); public function bool IsAutoOpen(); /** - * Checks whether caller `ATrader` will auto-open when trading gets activated. + * Checks whether caller `ETrader` will auto-open when trading gets activated. * * @see `IsAutoOpen()` for more info. * * @param doAutoOpen `true` if trader should be marked to always auto-open * upon activating trading and `false` otherwise. - * @return Caller `ATrader` to allow for method chaining. + * @return Caller `ETrader` to allow for method chaining. */ -public function ATrader SetAutoOpen(bool doAutoOpen); +public function ETrader SetAutoOpen(bool doAutoOpen); /** - * Checks whether caller `ATrader` is currently open. + * Checks whether caller `ETrader` is currently open. * - * `ATrader` being open means that players can "enter" (whatever that means for - * an implementation) and use `ATrader` to buy/sell equipment. + * `ETrader` being open means that players can "enter" (whatever that means for + * an implementation) and use `ETrader` to buy/sell equipment. * * @return `true` if it is open and `false` otherwise. */ public function bool IsOpen(); /** - * Changes whether caller `ATrader` is open. + * Changes whether caller `ETrader` is open. * * Closing the trader should not automatically "boot" players out * (see `BootPlayers()`). @@ -132,27 +132,27 @@ public function bool IsOpen(); * @see `IsOpen()` for more details. * * @param doOpen `true` if it is open and `false` otherwise. - * @return Caller `ATrader` to allow for method chaining. + * @return Caller `ETrader` to allow for method chaining. */ -public function ATrader SetOpen(bool doOpen); +public function ETrader SetOpen(bool doOpen); /** - * Checks whether caller `ATrader` is currently marked as selected. + * Checks whether caller `ETrader` is currently marked as selected. * * @see `ATradingComponent.GetSelectedTrader()` for more details. * - * @return `true` if caller `ATrader` is selected and `false` otherwise. + * @return `true` if caller `ETrader` is selected and `false` otherwise. */ public function bool IsSelected(); /** - * Marks caller `ATrader` as a selected trader. + * Marks caller `ETrader` as a selected trader. * * @see `ATradingComponent.GetSelectedTrader()` for more details. * - * @return Caller `ATrader` to allow for method chaining. + * @return Caller `ETrader` to allow for method chaining. */ -public function ATrader Select(); +public function ETrader Select(); /** * Removes players from the trader's place. @@ -163,17 +163,17 @@ public function ATrader Select(); * after it is closed. If that is impossible (for traders resembling * KF2's one), then this method should do nothing. * - * @return Caller `ATrader` to allow for method chaining. + * @return Caller `ETrader` to allow for method chaining. */ -public function ATrader BootPlayers(); +public function ETrader BootPlayers(); /** * Shortcut method to open the caller trader, guaranteed to be equivalent to * `SetOpen(true)`. Provided for better interface. * - * @return Caller `ATrader` to allow for method chaining. + * @return Caller `ETrader` to allow for method chaining. */ -public final function ATrader Open() +public final function ETrader Open() { SetOpen(true); return self; @@ -183,9 +183,9 @@ public final function ATrader Open() * Shortcut method to close the caller trader, guaranteed to be equivalent to * `SetOpen(false)`. Provided for better interface. * - * @return Caller `ATrader` to allow for method chaining. + * @return Caller `ETrader` to allow for method chaining. */ -public final function ATrader Close() +public final function ETrader Close() { SetOpen(false); return self; diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Signal.uc b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Signal.uc similarity index 91% rename from sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Signal.uc rename to sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Signal.uc index 72a6efa..f743bee 100644 --- a/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Signal.uc +++ b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Signal.uc @@ -1,7 +1,7 @@ /** * Signal class implementation for `ATradingComponent`, for detecting when * another trader is selected. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -20,7 +20,7 @@ */ class Trading_OnSelect_Signal extends Signal; -public final function Emit(ATrader oldTrader, ATrader newTrader) +public final function Emit(ETrader oldTrader, ETrader newTrader) { local Slot nextSlot; StartIterating(); diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Slot.uc b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Slot.uc similarity index 91% rename from sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Slot.uc rename to sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Slot.uc index 4a625b0..0919b98 100644 --- a/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Slot.uc +++ b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/Trading/Events/Trading_OnSelect_Slot.uc @@ -1,7 +1,7 @@ /** * Slot class implementation for `ATradingComponent`'s signal for * detecting when another trader is selected. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -20,7 +20,7 @@ */ class Trading_OnSelect_Slot extends Slot; -delegate connect(ATrader oldTrader, ATrader newTrader) +delegate connect(ETrader oldTrader, ETrader newTrader) { DummyCall(); } diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc index 1623425..bb6f078 100644 --- a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc @@ -28,30 +28,33 @@ struct DualiesPair var class dual; }; -var private APlayer inventoryOwner; +var private EPlayer inventoryOwner; var private config array dualiesClasses; protected function Finalizer() { + _.memory.Free(inventoryOwner); inventoryOwner = none; } -public function Initialize(APlayer player) +public function Initialize(EPlayer player) { if (inventoryOwner != none) { return; } - inventoryOwner = player; + if (player != none) { + inventoryOwner = EPlayer(player.Copy()); + } } private function Pawn GetOwnerPawn() { - local PlayerService service; - service = PlayerService(class'PlayerService'.static.Require()); - if (service == none) { + local PlayerController myController; + myController = inventoryOwner.GetController(); + if (myController == none) { return none; } - return service.GetPawn(inventoryOwner); + return myController.pawn; } public function bool Add(EItem newItem, optional bool forceAddition) diff --git a/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc b/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc index ed1cb8d..f1636eb 100644 --- a/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc +++ b/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc @@ -1,7 +1,7 @@ /** - * `ATrader`'s implementation for `KF1_Frontend`. + * `ETrader`'s implementation for `KF1_Frontend`. * Wrapper for KF1's `ShopVolume`s. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -18,7 +18,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class KF1_Trader extends ATrader; +class KF1_Trader extends ETrader; // We do not use any vanilla value as a name, instead storing and tracking it // entirely as our own value. @@ -26,6 +26,27 @@ var protected Text myName; // Reference to `ShopVolume` actor that this `KF1_Trader` represents. var protected NativeActorRef myShopVolume; +// We want to assign each trader (`ShopVolume`) a unique name that does +// not change. For that we will use `namedShopVolumes` array's default value to +// store `ShopVolume`-`Text` pairs. +// Whenever new `KF1_Trader` is created we check with this list to see if +// appropriate name was already assigned. +struct NamedShopVolume +{ + var public Text name; + var public NativeActorRef reference; +}; +var private array namedShopVolumes; + +protected function Constructor() +{ + // We only care about `default` value of this array variable, + // so do not bother storing needless references in each object + if (namedShopVolumes.length > 0) { + namedShopVolumes.length = 0; + } +} + protected function Finalizer() { _.memory.Free(myName); @@ -34,40 +55,84 @@ protected function Finalizer() myName = none; } +protected static function StaticFinalizer() +{ + local int i; + if (default.namedShopVolumes.length <= 0) { + return; + } + for (i = 0; i < default.namedShopVolumes.length; i += 1) + { + __().memory.Free(default.namedShopVolumes[i].name); + __().memory.Free(default.namedShopVolumes[i].reference); + } + default.namedShopVolumes.length = 0; +} + +private static function Text GetShopVolumeName(ShopVolume newShopVolume) +{ + local int i; + local NamedShopVolume newRecord; + local array namedShopVolumesCopy; + if (newShopVolume == none) { + return none; + } + namedShopVolumesCopy = default.namedShopVolumes; + for (i = 0; i < namedShopVolumesCopy.length; i += 1) + { + if (namedShopVolumesCopy[i].reference.Get() == newShopVolume) { + return namedShopVolumesCopy[i].name.Copy(); + } + } + newRecord.reference = __().unreal.ActorRef(newShopVolume); + newRecord.name = + __().text.FromString("trader" $ namedShopVolumesCopy.length); + default.namedShopVolumes[default.namedShopVolumes.length] = newRecord; + return newRecord.name.Copy(); +} + /** - * Detect all existing traders on the level and created a `KF1_Trader` for - * each of them. + * Initializes caller `KF1_Trader`. Should be called right after `KF1_Trader` + * was allocated. + * + * 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 - * be `none`. + * Initialization can fail if: + * 1. `initShopVolume == none`; + * 2. Caller `KF1_Trader` already was successfully initialized. + * 3. `initShopVolume` is objective-mode only `ShopVolume` and we are + * not running an objective mode right now. + * + * @param initShopVolume `ShopVolume` that caller `KF1_Trader` will + * correspond to. + * @return `true` if initialization was successful and `false` otherwise. */ -public static function array WrapVanillaShops() -{ - local int shopCounter; - local MutableText textBuilder; - local LevelInfo level; - local KFGameType kfGame; - local KF1_Trader nextTrader; - local array 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; - } +public final /* unreal */ function bool Initialize(ShopVolume initShopVolume) +{ + if (initShopVolume == none) { + return false; } - textBuilder.FreeSelf(); - return allTraders; + if ( initShopVolume.bObjectiveModeOnly + && !__().unreal.GetKFGameType().bUsingObjectiveMode) { + return false; + } + myName = GetShopVolumeName(initShopVolume); + myShopVolume = _.unreal.ActorRef(initShopVolume); + return true; +} + +/** + * Returns `ShopVolume`, associated with the caller `KF1_Trader`. + * + * @return `ShopVolume`, associated with the caller `KF1_Trader`. + */ +public final /* unreal */ function ShopVolume GetShopVolume() +{ + if (myShopVolume == none) { + return none; + } + return ShopVolume(myShopVolume.Get()); } public function Text GetName() @@ -78,7 +143,8 @@ public function Text GetName() return myName.Copy(); } -public function ATrader SetName(Text newName) +// TODO: it is broken, needs fixing +public function ETrader SetName(Text newName) { if (newName == none) return self; if (newName.IsEmpty()) return self; @@ -108,7 +174,7 @@ public function bool IsEnabled() return false; } -public function ATrader SetEnabled(bool doEnable) +public function ETrader SetEnabled(bool doEnable) { local ShopVolume vanillaShopVolume; vanillaShopVolume = ShopVolume(myShopVolume.Get()); @@ -138,7 +204,7 @@ protected function UpdateShopList() local ShopVolume nextShopVolume; local KF1_Trader nextTrader; local array shopVolumes; - local array availableTraders; + local array availableTraders; availableTraders = _.kf.trading.GetTraders(); for (i = 0; i < availableTraders.length; i += 1) { @@ -163,7 +229,7 @@ public function bool IsAutoOpen() return false; } -public function ATrader SetAutoOpen(bool doAutoOpen) +public function ETrader SetAutoOpen(bool doAutoOpen) { local ShopVolume vanillaShopVolume; vanillaShopVolume = ShopVolume(myShopVolume.Get()); @@ -189,7 +255,7 @@ public function bool IsOpen() return false; } -public function ATrader SetOpen(bool doOpen) +public function ETrader SetOpen(bool doOpen) { local ShopVolume vanillaShopVolume; if (doOpen && !IsEnabled()) return self; @@ -220,7 +286,7 @@ public function bool IsSelected() return false; } -public function ATrader Select() +public function ETrader Select() { local ShopVolume vanillaShopVolume; local KFGameReplicationInfo kfGameRI; @@ -235,7 +301,7 @@ public function ATrader Select() return self; } -public function ATrader BootPlayers() +public function ETrader BootPlayers() { local ShopVolume vanillaShopVolume; vanillaShopVolume = ShopVolume(myShopVolume.Get()); @@ -245,6 +311,51 @@ public function ATrader BootPlayers() return self; } +/** + * Makes a copy of the caller interface, producing a new `EInterface` of + * the exactly the same class (`EWeapon` will produce another `EWeapon`). + * + * This should never fail. Even if referred entity is already gone. + * + * @return Copy of the caller `EInterface`, of the exactly the same class. + * Guaranteed to not be `none`. + */ +public function EInterface Copy() +{ + local KF1_Trader traderCopy; + traderCopy = KF1_Trader(_.memory.Allocate(class'KF1_Trader')); + if (myShopVolume == none) + { + // Should not really happen, since then caller `KF1_Trader` was + // not initialized + return traderCopy; + } + traderCopy.Initialize(ShopVolume(myShopVolume.Get())); + return traderCopy; +} + +public function bool IsExistent() +{ + if (myShopVolume == none) { + return false; + } + return (myShopVolume.Get() != none); +} + +public function bool SameAs(EInterface other) +{ + local KF1_Trader asTrader; + local NativeActorRef otherShopVolume; + if (other == none) return false; + if (myShopVolume == none) return false; + asTrader = KF1_Trader(other); + if (asTrader == none) return false; + otherShopVolume = asTrader.myShopVolume; + if (otherShopVolume == none) return false; + + return (myShopVolume.Get() == otherShopVolume.Get()); +} + defaultproperties { } \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc b/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc index b504285..f1a54de 100644 --- a/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc +++ b/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc @@ -1,7 +1,7 @@ /** * `ATradingComponent`'s implementation for `KF1_Frontend`. * Only supports `KF1_Trader` as a possible trader class. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -28,16 +28,38 @@ var protected int pausedCountDownValue; // For detecting events of trading becoming active/inactive and selecting // a different trader, to account for these changing through non-Acedia means var protected bool wasActiveLastCheck; -var protected Atrader lastSelectedTrader; +var protected ETrader lastSelectedTrader; -// All known traders on map -var protected array registeredTraders; +var protected array registeredTraders; protected function Constructor() { + local LevelInfo level; + local KFGameType kfGame; + local KF1_Trader nextTrader; + local ShopVolume nextShopVolume; super.Constructor(); _.unreal.OnTick(self).connect = Tick; - registeredTraders = class'KF1_Trader'.static.WrapVanillaShops(); + // Build `registeredTraders` cache to avoid looking through + // all actors each time + level = __().unreal.GetLevel(); + kfGame = __().unreal.GetKFGameType(); + foreach level.AllActors(class'ShopVolume', nextShopVolume) + { + if (nextShopVolume == none) { + continue; + } + if (nextShopVolume.bObjectiveModeOnly && !kfGame.bUsingObjectiveMode) { + continue; + } + nextTrader = KF1_Trader(__().memory.Allocate(class'KF1_Trader')); + if (nextTrader.Initialize(nextShopVolume)) { + registeredTraders[registeredTraders.length] = nextTrader; + } + else { + _.memory.Free(nextTrader); + } + } lastSelectedTrader = GetSelectedTrader(); wasActiveLastCheck = IsTradingActive(); } @@ -52,9 +74,14 @@ protected function Finalizer() registeredTraders.length = 0; } -public function array GetTraders() +public function array GetTraders() { - return registeredTraders; + local int i; + local array result; + for (i = 0; i < registeredTraders.length; i += 1) { + result[i] = ETrader(registeredTraders[i].Copy()); + } + return result; } public function bool IsTradingActive() @@ -91,21 +118,22 @@ public function SetTradingStatus(bool makeActive) kfGameRI.maxMonsters = 0; } -public function ATrader GetSelectedTrader() +public function ETrader GetSelectedTrader() { local int i; for (i = 0; i < registeredTraders.length; i += 1) { if (registeredTraders[i].IsSelected()) { - return registeredTraders[i]; + return ETrader(registeredTraders[i].Copy()); } } return none; } -public function SelectTrader(ATrader newSelection) +public function SelectTrader(ETrader newSelection) { - local ATrader oldSelection; + local bool traderChanged; + local ETrader oldSelection; local KFGameReplicationInfo kfGameRI; if (newSelection != none) { newSelection.Select(); @@ -121,10 +149,20 @@ public function SelectTrader(ATrader newSelection) // in case someone decides it would be a grand idea to call `SelectTrader` // during `onTraderSelectSignal` signal. oldSelection = lastSelectedTrader; - lastSelectedTrader = newSelection; - if (lastSelectedTrader != newSelection) { + if (newSelection != none) + { + lastSelectedTrader = ETrader(newSelection.Copy()); + traderChanged = lastSelectedTrader.SameAs(oldSelection); + } + else + { + lastSelectedTrader = none; + traderChanged = (oldSelection == none); + } + if (traderChanged) { onTraderSelectSignal.Emit(oldSelection, newSelection); } + _.memory.Free(oldSelection); } public function int GetTradingInterval() @@ -179,26 +217,19 @@ public function SetCountDownPause(bool doPause) protected function Tick(float delta, float timeScaleCoefficient) { - local bool isActiveNow; - local ATrader newSelectedTrader; + local bool isActiveNow; // Enforce pause if (tradingCountDownPaused) { _.unreal.GetKFGameType().waveCountDown = pausedCountDownValue; } // Selected trader check - newSelectedTrader = GetSelectedTrader(); - if (lastSelectedTrader != newSelectedTrader) - { - onTraderSelectSignal.Emit(lastSelectedTrader, newSelectedTrader); - lastSelectedTrader = newSelectedTrader; - } + CheckNativeTraderSwap(); // Active status check isActiveNow = IsTradingActive(); if (wasActiveLastCheck != isActiveNow) { wasActiveLastCheck = isActiveNow; - if (isActiveNow) - { + if (isActiveNow) { onStartSignal.Emit(); } else @@ -210,6 +241,24 @@ protected function Tick(float delta, float timeScaleCoefficient) } } +// Detect when selected trader is swapped be swapped by non-Acedia means +protected function CheckNativeTraderSwap() +{ + local ETrader newSelectedTrader; + if ( lastSelectedTrader == none + && _.unreal.GetKFGameRI().currentShop == none) { + return; + } + if (lastSelectedTrader != none && lastSelectedTrader.IsSelected()) { + return; + } + // Currently selected trader actually differs from `lastSelectedTrader` + newSelectedTrader = GetSelectedTrader(); + onTraderSelectSignal.Emit(lastSelectedTrader, newSelectedTrader); + _.memory.Free(lastSelectedTrader); + lastSelectedTrader = newSelectedTrader; +} + defaultproperties { } \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 6e80d83..25d3c81 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -25,7 +25,6 @@ defaultproperties features(0) = class'Commands_Feature' features(1) = class'Avarice_Feature' services(0) = class'ConnectionService' - services(1) = class'PlayerService' aliasSources(0) = class'AliasSource' aliasSources(1) = class'WeaponAliasSource' aliasSources(2) = class'ColorAliasSource' diff --git a/sources/Players/APlayer.uc b/sources/Players/EPlayer.uc similarity index 64% rename from sources/Players/APlayer.uc rename to sources/Players/EPlayer.uc index b774b0b..ad58c7e 100644 --- a/sources/Players/APlayer.uc +++ b/sources/Players/EPlayer.uc @@ -1,12 +1,6 @@ /** - * Represents a connected player connection and serves to provide access to - * both it's server data and in-game pawn representation. - * Unlike `User`, - changes when player reconnects to the server. - * This object SHOULD NOT be created manually, please rely on - * Acedia for that. - * Due to being relatively rarely created, does not use object pools, - * which simplifies their usage and comparison. - * Copyright 2021 Anton Tarasenko + * Provides a common interface to a connected player connection. + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -23,9 +17,9 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class APlayer extends AcediaObject; +class EPlayer extends EInterface; -// How this `APlayer` is identified by the server +// How this `EPlayer` is identified by the server var private User identity; // Writer that can be used to write into this player's console @@ -33,6 +27,7 @@ var private ConsoleWriter consoleInstance; // Remember version to reallocate writer in case someone deallocates it var private int consoleLifeVersion; +// `PlayerController` reference var private NativeActorRef controller; // These variables record name of this player; @@ -54,21 +49,112 @@ enum AdminStatus protected function Finalizer() { _.memory.Free(controller); - 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, - * otherwise a location caller `APlayer` is currently spectating the map from - * will be returned. + * Every `EPlayer` must be initialized, using non-initialized `EPlayer` + * instances is invalid. + * + * Initialization can fail if: + * 1. `initController == none`; + * 2. Its id hash (from `GetPlayerIDHash()`) was not properly setup yet + * (not steam id); + * 3. Caller `EPlayer` already was successfully initialized. + * + * @param initController Controller that caller `EPlayer` will correspond to. + * @return `true` if initialization was successful and `false` otherwise. + */ +public final /* unreal */ function bool Initialize( + PlayerController initController) +{ + local Text idHash; + local PlayerReplicationInfo myReplicationInfo; + if (controller != none) return false; // Already initialized! + if (initController == none) return false; + + if (identity == none) + { + // Only fetch `User` object if it was not yet setup, which can happen + // if `EPlayer` is making a copy and wants to avoid + // re-fetching `identity` + idHash = _.text.FromString(initController.GetPlayerIDHash()); + identity = _.users.FetchByIDHash(idHash); + idHash.FreeSelf(); + idHash = none; + } + controller = _.unreal.ActorRef(initController); + myReplicationInfo = initController.playerReplicationInfo; + // Hash current name + if (myReplicationInfo != none) + { + hashedName = myReplicationInfo.playerName; + textName = _.text.FromColoredString(hashedName); + } + return true; +} + +public function bool IsExistent() +{ + if (controller == none) { + return false; + } + return (controller.Get() != none); +} + +public function EInterface Copy() +{ + local EPlayer playerCopy; + playerCopy = EPlayer(_.memory.Allocate(class'EPlayer')); + if (controller == none) + { + // Should not really happen, since then caller `EPlayer` was + // not initialized + return playerCopy; + } + playerCopy.identity = identity; + playerCopy.Initialize(PlayerController(controller.Get())); + return playerCopy; +} + +public function bool SameAs(EInterface other) +{ + local EPlayer asPlayer; + local NativeActorRef otherController; + if (other == none) return false; + if (controller == none) return false; + asPlayer = EPlayer(other); + if (asPlayer != none) return false; + otherController = asPlayer.controller; + if (otherController == none) return false; + + return (controller.Get() == otherController.Get()); +} + +/** + * Returns location of the caller `EPlayer`. + * + * If caller `EPlayer` has a pawn, then it's location will be returned, + * otherwise a location from which caller `EPlayer` is currently spectating is + * considered caller's location. + * + * @return Location of the caller `EPlayer` has a pawn. */ public final function Vector GetLocation() { local Pawn myPawn; local PlayerController myController; - myController = PlayerController(controller.Get()); + if (controller != none) { + myController = PlayerController(controller.Get()); + } if (myController != none) { myPawn = myController.pawn; @@ -80,68 +166,38 @@ public final function Vector GetLocation() return Vect(0.0, 0.0, 0.0); } -// `PlayerReplicationInfo` associated with the caller `APLayer`. +// `PlayerReplicationInfo` associated with the caller `EPlayer`. // Can return `none` if: -// 1. Caller `APlayer` has already disconnected; +// 1. Caller `EPlayer` has already disconnected; // 2. It was not properly initialized; -// 3. There is an issue running `PlayerService`. private final function PlayerReplicationInfo GetRI() { local PlayerController myController; + if (controller == none) return none; myController = PlayerController(controller.Get()); - if (myController != none) { - return myController.playerReplicationInfo; - } - return none; -} + if (myController == none) return none; -/** - * Checks if player, corresponding to `APlayer`, is still connected to - * the server. If player is disconnected - `APlayer` instance should be - * considered useless. - * - * @return `true` if player is connected and `false` otherwise. - */ -public final function bool IsConnected() -{ - return (controller.Get() != none); + return myController.playerReplicationInfo; } /** - * Initializes caller `APlayer`. Should be called right after `APlayer` - * was spawned. - * - * 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. + * Returns `PlayerController`, associated with the caller `EPlayer`. * - * @param newController Controller that caller `APlayer` will correspond to. + * @return `PlayerController`, associated with the caller `EPlayer`. */ -public final function Initialize(Text idHash) +public final /* unreal */ function PlayerController GetController() { - local PlayerService service; - local PlayerController myController; - local PlayerReplicationInfo myReplicationInfo; - identity = _.users.FetchByIDHash(idHash); - // Retrieve controller and replication info - service = PlayerService(class'PlayerService'.static.Require()); - myController = service.GetController(self); - controller = _.unreal.ActorRef(myController); - if (myController != none) { - myReplicationInfo = myController.playerReplicationInfo; - } - // Hash current name - if (myReplicationInfo != none) { - hashedName = myReplicationInfo.playerName; - textName = _.text.FromColoredString(hashedName); + if (controller == none) { + return none; } + return PlayerController(controller.Get()); } /** - * Returns `User` object that is corresponding to the caller `APlayer`. + * Returns `User` object that is corresponding to the caller `EPlayer`. * - * @return `User` corresponding to the caller `APlayer`. Guarantee to be - * not `none` for correctly initialized `APlayer` (it remembers `User` + * @return `User` corresponding to the caller `EPlayer`. Guarantee to be + * not `none` for correctly initialized `EPlayer` (it remembers `User` * record even if player has disconnected). */ public final function User GetIdentity() @@ -154,7 +210,7 @@ public final function User GetIdentity() * * @return `Text` containing current name of the caller player. * Guaranteed to not be `none`. Returned object is not managed by caller - * `APlayer` and should be manually deallocated. + * `EPlayer` and should be manually deallocated. */ public final function Text GetName() { @@ -173,9 +229,9 @@ public final function Text GetName() } /** - * Set new displayed name for the caller `APlayer`. + * Set new displayed name for the caller `EPlayer`. * - * @param newPlayerName New name of the caller `APlayer`. This value will + * @param newPlayerName New name of the caller `EPlayer`. This value will * be copied. Passing `none` will result in an empty name. */ public final function SetName(Text newPlayerName) @@ -237,7 +293,7 @@ public final function EInventory GetInventory() * Different from `IsAdmin()` since this method allows to distinguish between * different types of admin login (like silent admins). * - * @return Admin status of the caller `APLayer`. + * @return Admin status of the caller `EPlayer`. */ public final function AdminStatus GetAdminStatus() { @@ -271,10 +327,10 @@ public final function bool IsAdmin() } /** - * Changes admin status of the caller `APlayer`. - * Can only fail if caller `APlayer` has already disconnected. + * Changes admin status of the caller `EPlayer`. + * Can only fail if caller `EPlayer` has already disconnected. * - * @param newAdminStatus New admin status of the `APlayer`. + * @param newAdminStatus New admin status of the `EPlayer`. */ public final function SetAdminStatus(AdminStatus newAdminStatus) { @@ -300,9 +356,9 @@ public final function SetAdminStatus(AdminStatus newAdminStatus) } /** - * Returns current amount of money caller `APlayer` has. + * Returns current amount of money caller `EPlayer` has. * - * @return Amount of money `APlayer` has. If player has already disconnected + * @return Amount of money `EPlayer` has. If player has already disconnected * method will return `0`. */ public final function int GetDosh() @@ -316,9 +372,9 @@ public final function int GetDosh() } /** - * Sets amount of money that caller `APlayer` will have. + * Sets amount of money that caller `EPlayer` will have. * - * @param newDoshAmount New amount of money that caller `APlayer` must have. + * @param newDoshAmount New amount of money that caller `EPlayer` must have. */ public final function SetDosh(int newDoshAmount) { @@ -340,7 +396,7 @@ public final function SetDosh(int newDoshAmount) * console. Returned object should not be deallocated, but it is * guaranteed to be valid for non-disconnected players. */ -public final function ConsoleWriter Console() +public final function /* borrow */ ConsoleWriter BorrowConsole() { if ( consoleInstance == none || consoleInstance.GetLifeVersion() != consoleLifeVersion) diff --git a/sources/Players/Events/PlayerAPI_OnLostPlayer_Signal.uc b/sources/Players/Events/PlayerAPI_OnLostPlayer_Signal.uc new file mode 100644 index 0000000..b7fa2e3 --- /dev/null +++ b/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 . + */ +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' +} \ No newline at end of file diff --git a/sources/Gameplay/BaseClasses/BaseBackend.uc b/sources/Players/Events/PlayerAPI_OnLostPlayer_Slot.uc similarity index 72% rename from sources/Gameplay/BaseClasses/BaseBackend.uc rename to sources/Players/Events/PlayerAPI_OnLostPlayer_Slot.uc index 295a4d6..122d34f 100644 --- a/sources/Gameplay/BaseClasses/BaseBackend.uc +++ b/sources/Players/Events/PlayerAPI_OnLostPlayer_Slot.uc @@ -1,6 +1,5 @@ /** - * Base class for all backends. Does not define anything meaningful, which - * also means it does not put any limitations on it's implementation. + * Slot class implementation for `PlayerAPI`'s `OnLostPlayer` signal. * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -18,8 +17,23 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ - class BaseBackend extends AcediaObject - abstract; +class PlayerAPI_OnLostPlayer_Slot extends Slot; + +delegate connect(User identity) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} defaultproperties { diff --git a/sources/Players/Events/PlayerAPI_OnNewPlayer_Signal.uc b/sources/Players/Events/PlayerAPI_OnNewPlayer_Signal.uc new file mode 100644 index 0000000..02efbca --- /dev/null +++ b/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 . + */ +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' +} \ No newline at end of file diff --git a/sources/Players/Events/PlayerAPI_OnNewPlayer_Slot.uc b/sources/Players/Events/PlayerAPI_OnNewPlayer_Slot.uc new file mode 100644 index 0000000..1c20c3a --- /dev/null +++ b/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 . + */ +class PlayerAPI_OnNewPlayer_Slot extends Slot; + +delegate connect(EPlayer newPlayer) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Players/Inventory/EInventory.uc b/sources/Players/Inventory/EInventory.uc index 2e101b4..74ce303 100644 --- a/sources/Players/Inventory/EInventory.uc +++ b/sources/Players/Inventory/EInventory.uc @@ -33,9 +33,9 @@ class EInventory extends AcediaObject * Cannot fail for any connected player and can assume it will not be called * for not connected ones. * - * @param player `APlayer` for which to initialize this inventory. + * @param player `EPlayer` for which to initialize this inventory. */ -public function Initialize(APlayer player) {} +public function Initialize(EPlayer player) {} /** * Adds passed `EItem` to the caller inventory system. diff --git a/sources/Players/PlayerService.uc b/sources/Players/PlayerService.uc deleted file mode 100644 index 5ddc42b..0000000 --- a/sources/Players/PlayerService.uc +++ /dev/null @@ -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 . - */ -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 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 GetAllPlayers() -{ - local int i; - local array 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 -{ -} \ No newline at end of file diff --git a/sources/Players/PlayersAPI.uc b/sources/Players/PlayersAPI.uc index 8765847..fae24cc 100644 --- a/sources/Players/PlayersAPI.uc +++ b/sources/Players/PlayersAPI.uc @@ -1,5 +1,5 @@ /** - * API that provides functions for working player references (`APlayer`). + * API that provides functions for working player references (`EPlayer`). * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -18,6 +18,7 @@ * along with Acedia. If not, see . */ class PlayersAPI extends AcediaObject + dependson(ConnectionService) dependson(Text); // Writer that can be used to write into this player's console @@ -25,41 +26,76 @@ var private ConsoleWriter consoleInstance; // Remember version to reallocate writer in case someone deallocates it var private int consoleLifeVersion; +var protected bool connectedToConnectionServer; +var protected PlayerAPI_OnNewPlayer_Signal onNewPlayerSignal; +var protected PlayerAPI_OnLostPlayer_Signal onLostPlayerSignal; + protected function Constructor() { - local ConnectionService service; - service = ConnectionService(class'ConnectionService'.static.Require()); - service.OnConnectionEstablished(self).connect = MakePlayer; + onNewPlayerSignal = PlayerAPI_OnNewPlayer_Signal( + _.memory.Allocate(class'PlayerAPI_OnNewPlayer_Signal')); + onLostPlayerSignal = PlayerAPI_OnLostPlayer_Signal( + _.memory.Allocate(class'PlayerAPI_OnLostPlayer_Signal')); } protected function Finalizer() { local ConnectionService service; - service = ConnectionService(class'ConnectionService'.static.Require()); - service.OnConnectionEstablished(self).Disconnect(); + connectedToConnectionServer = false; + if (class'ConnectionService'.static.IsRunning()) + { + service = ConnectionService(class'ConnectionService'.static.Require()); + service.OnConnectionEstablished(self).Disconnect(); + service.OnConnectionLost(self).Disconnect(); + } + _.memory.Free(onNewPlayerSignal); + _.memory.Free(onLostPlayerSignal); + onNewPlayerSignal = none; + onLostPlayerSignal = none; +} + +/** + * Signal that will be emitted once new player is connected. + * + * [Signature] + * void (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 (User identity) + * + * @param identity `User` object that corresponds to + * the disconnected player. + */ +/* SIGNAL */ +public function PlayerAPI_OnLostPlayer_Slot OnLostPlayerHandle( + AcediaObject receiver) { - local APlayer newPlayer; - local Text textIdHash; - local PlayerService service; - // Make new player controller and link it to `newConnection` - newPlayer = APlayer(_.memory.Allocate(class'APlayer')); - service = PlayerService(class'PlayerService'.static.Require()); - service.RegisterPair(newConnection.controllerReference, newPlayer); - // Initialize new `APlayer` - textIdHash = _.text.FromString(newConnection.idHash); - newPlayer.Initialize(textIdHash); - textIdHash.FreeSelf(); + ConnectToConnectionService(); + return PlayerAPI_OnLostPlayer_Slot(onLostPlayerSignal.NewSlot(receiver)); } /** * Return `ConsoleWriter` that can be used to write into every player's * console. * - * Provided that returned object is never deallocated - returns the same object - * with each call, otherwise can allocate new instance of `ConsoleWriter`. + * Provided that returned object is never deallocated - method returns + * the same object with each call, otherwise can allocate new instance of + * `ConsoleWriter`. * * @return `ConsoleWriter` that can be used to write into every player's * console. Returned object should not be deallocated, but it is @@ -77,21 +113,83 @@ public final function ConsoleWriter Console() return consoleInstance.ForAll(); } +private final function ConnectToConnectionService() +{ + local ConnectionService service; + if (connectedToConnectionServer) { + return; + } + service = ConnectionService(class'ConnectionService'.static.Require()); + service.OnConnectionEstablished(self).connect = AnnounceNewPlayer; + service.OnConnectionLost(self).connect = AnnounceLostPlayer; + connectedToConnectionServer = true; +} + +private final function AnnounceNewPlayer( + ConnectionService.Connection newConnection) +{ + if (onNewPlayerSignal == none) { + return; + } + onNewPlayerSignal.Emit(FromController(newConnection.controllerReference)); +} + +private final function AnnounceLostPlayer( + ConnectionService.Connection lostConnection) +{ + local Text idHash; + local User lostIdentity; + if (onLostPlayerSignal == none) { + return; + } + idHash = _.text.FromString(lostConnection.idHash); + lostIdentity = _.users.FetchByIDHash(idHash); + _.memory.Free(idHash); + idHash = none; + onLostPlayerSignal.Emit(lostIdentity); +} + +/** + * Creates `EPlayer` instance from passed `PlayerController` reference. + * Can fail if passed parameter is not `controller`. + * + * @param controller `PlayerController` for which to create + * `EPlayer` instance. Should not be `none`. + * @return Instance of `EPlayer` that refers to passed `controller`. + * Returns `none` iff `controller == none`. + */ +public final /* unreal */ function EPlayer FromController( + PlayerController controller) +{ + local EPlayer result; + result = EPlayer(_.memory.Allocate(class'EPlayer')); + result.Initialize(controller); + return result; +} + /** * Fetches current array of all players. * * @return Current array of all players. * Guaranteed to not contain `none` values. */ -public final function array GetAll() +public final function array GetAll() { - local PlayerService service; - local array emptyResult; - service = PlayerService(class'PlayerService'.static.GetInstance()); - if (service != none) { - return service.GetAllPlayers(); + local int i; + local array result; + local ConnectionService service; + local array activeConnections; + local PlayerController nextControllerReference; + service = ConnectionService(class'ConnectionService'.static.Require()); + activeConnections = service.GetActiveConnections(); + for (i = 0; i < activeConnections.length; i += 1) + { + nextControllerReference = activeConnections[i].controllerReference; + if (nextControllerReference != none) { + result[result.length] = FromController(nextControllerReference); + } } - return emptyResult; + return result; } defaultproperties diff --git a/sources/Unreal/Connections/ConnectionService.uc b/sources/Unreal/Connections/ConnectionService.uc index 30ac2fa..e41c9ea 100644 --- a/sources/Unreal/Connections/ConnectionService.uc +++ b/sources/Unreal/Connections/ConnectionService.uc @@ -58,7 +58,7 @@ public final function Connection_Slot OnConnectionEstablished( * Signal that will be emitted when the player connection is lost. * * [Signature] - * void (ConnectionService.Connection newConnection) + * void (ConnectionService.Connection lostConnection) * * @param newConnection Structure that describes lost connection. */ diff --git a/sources/Users/User.uc b/sources/Users/User.uc index 919ef35..765f721 100644 --- a/sources/Users/User.uc +++ b/sources/Users/User.uc @@ -34,9 +34,10 @@ var private JSONPointer persistentSettingsPointer; var private LoggerAPI.Definition errNoUserDataDatabase; +// TODO: redo this comment /** * Initializes caller `User` with id and it's session key. Should be called - * right after `APlayer` was created. + * right after `EPlayer` was created. * * Initialization should (and can) only be done once. * Before a `Initialize()` call, any other method calls on such `User`