/** * Provides a common interface to a connected player connection. * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * * Acedia is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, or * (at your option) any later version. * * Acedia is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ class EPlayer extends EInterface; // How this `EPlayer` is identified by the server var private User identity; // Writer that can be used to write into this player's console 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; // `hashedName` is used to track outside changes that bypass our getter/setter. var private Text textName; var private string hashedName; // Describes the player's admin status (as defined by standard KF classes) enum AdminStatus { // Not an admin AS_None, // (Publicly visible) admin AS_Admin, // Admin with their admin status hidden AS_SilentAdmin }; protected function Finalizer() { _.memory.Free(controller); _.memory.Free(consoleInstance); controller = none; consoleInstance = none; // No need to deallocate `User` objects, since they are all have unique // instance for every player on the server identity = none; } /** * Initializes caller `EPlayer`. Should be called right after `EPlayer` * was allocated. * * Every `EPlayer` must be initialized, using non-initialized `EPlayer` * instances is invalid. * * Initialization can fail if: * 1. `initController == none`; * 2. Its id hash (from `GetPlayerIDHash()`) was not properly setup yet * (not steam id); * 3. Caller `EPlayer` already was successfully initialized. * * @param initController Controller that caller `EPlayer` will correspond to. * @return `true` if initialization was successful and `false` otherwise. */ public final /* unreal */ function bool Initialize( PlayerController initController) { local Text idHash; local PlayerReplicationInfo myReplicationInfo; if (controller != none) return false; // Already initialized! if (initController == none) return false; if (identity == none) { // Only fetch `User` object if it was not yet setup, which can happen // if `EPlayer` is making a copy and wants to avoid // re-fetching `identity` idHash = _.text.FromString(initController.GetPlayerIDHash()); identity = _.users.FetchByIDHash(idHash); idHash.FreeSelf(); idHash = none; } controller = _.unreal.ActorRef(initController); myReplicationInfo = initController.playerReplicationInfo; // Hash current name if (myReplicationInfo != none) { hashedName = myReplicationInfo.playerName; textName = _.text.FromColoredString(hashedName); } return true; } public function bool IsExistent() { if (controller == none) { return false; } return (controller.Get() != none); } public function EInterface Copy() { local EPlayer playerCopy; playerCopy = EPlayer(_.memory.Allocate(class'EPlayer')); if (controller == none) { // Should not really happen, since then caller `EPlayer` was // not initialized return playerCopy; } playerCopy.identity = identity; playerCopy.Initialize(PlayerController(controller.Get())); return playerCopy; } public function bool SameAs(EInterface other) { local EPlayer asPlayer; local NativeActorRef otherController; if (other == none) return false; if (controller == none) return false; asPlayer = EPlayer(other); if (asPlayer != none) return false; otherController = asPlayer.controller; if (otherController == none) return false; return (controller.Get() == otherController.Get()); } /** * Returns location of the caller `EPlayer`. * * If caller `EPlayer` has a pawn, then it's location will be returned, * otherwise a location from which caller `EPlayer` is currently spectating is * considered caller's location. * * @return Location of the caller `EPlayer` has a pawn. */ public final function Vector GetLocation() { local Pawn myPawn; local PlayerController myController; if (controller != none) { myController = PlayerController(controller.Get()); } if (myController != none) { myPawn = myController.pawn; if (myPawn != none) { return myPawn.location; } return myController.location; } return Vect(0.0, 0.0, 0.0); } // `PlayerReplicationInfo` associated with the caller `EPlayer`. // Can return `none` if: // 1. Caller `EPlayer` has already disconnected; // 2. It was not properly initialized; private final function PlayerReplicationInfo GetRI() { local PlayerController myController; if (controller == none) return none; myController = PlayerController(controller.Get()); if (myController == none) return none; return myController.playerReplicationInfo; } /** * Returns `PlayerController`, associated with the caller `EPlayer`. * * @return `PlayerController`, associated with the caller `EPlayer`. */ public final /* unreal */ function PlayerController GetController() { if (controller == none) { return none; } return PlayerController(controller.Get()); } /** * Returns `User` object that is corresponding to the caller `EPlayer`. * * @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() { return identity; } /** * Returns current displayed name of the caller player. * * @return `Text` containing current name of the caller player. * Guaranteed to not be `none`. Returned object is not managed by caller * `EPlayer` and should be manually deallocated. */ public final function Text GetName() { local PlayerReplicationInfo myReplicationInfo; myReplicationInfo = GetRI(); if (myReplicationInfo == none) { return P("").Copy(); } if (textName != none && myReplicationInfo.playerName == hashedName) { return textName.Copy(); } _.memory.Free(textName); hashedName = myReplicationInfo.playerName; textName = _.text.FromColoredString(hashedName); return textName.Copy(); } /** * Set new displayed name for the caller `EPlayer`. * * @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) { local Text.Formatting endingFormatting; local PlayerReplicationInfo myReplicationInfo; myReplicationInfo = GetRI(); if (myReplicationInfo == none) return; _.memory.Free(textName); // Filter both `none` and empty `newPlayerName`, so that we can // later rely on it having at least one character if (newPlayerName == none || newPlayerName.IsEmpty()) { textName = P("").Copy(); } else { textName = newPlayerName.Copy(); } hashedName = textName.ToColoredString(,, _.color.white); // To correctly display nicknames we want to drop default color tag // at the beginning (the one `ToColoredString()` adds if first character // has no defined color). // This is a compatibility consideration with vanilla UIs that use // color codes from `myReplicationInfo.playerName` for displaying nicknames // and whose expected behavior can get broken by default color tag. if (!textName.GetFormatting(0).isColored) { hashedName = Mid(hashedName, 4); } // This is another compatibility consideration with vanilla UIs: unless // we restore color to neutral white, Killing Floor will paint any chat // messages we send in the color our nickname ended with. endingFormatting = textName.GetFormatting(textName.GetLength() - 1); if ( endingFormatting.isColored && !_.color.AreEqual(endingFormatting.color, _.color.white, true)) { hashedName $= _.color.GetColorTag(_.color.white); } myReplicationInfo.playerName = hashedName; } // TODO: replace this, it has no place here // ^ works as a temporary solution before we add pawn wrappers public final function EInventory GetInventory() { local EKFInventory inventory; if (controller != none && controller.Get() != none) { inventory = EKFInventory(_.memory.Allocate(class'EKFInventory')); inventory.Initialize(self); return inventory; } return none; } /** * Returns admin status of the caller player. * Disconnected players are never admins. * * Different from `IsAdmin()` since this method allows to distinguish between * different types of admin login (like silent admins). * * @return Admin status of the caller `EPlayer`. */ public final function AdminStatus GetAdminStatus() { local PlayerReplicationInfo myReplicationInfo; myReplicationInfo = GetRI(); if (myReplicationInfo == none) { return AS_None; } if (myReplicationInfo.bAdmin) { return AS_Admin; } if (myReplicationInfo.bSilentAdmin) { return AS_SilentAdmin; } return AS_None; } /** * Checks if caller player has admin rights. * Disconnected players never have admin rights. * * Different from `GetAdminStatus()` since this method simply checks admin * rights, without distinguishing between different types of admin login * (like silent admins). * * @return `true` if player has admin rights and `false` otherwise. */ public final function bool IsAdmin() { return (GetAdminStatus() != AS_None); } /** * Changes admin status of the caller `EPlayer`. * Can only fail if caller `EPlayer` has already disconnected. * * @param newAdminStatus New admin status of the `EPlayer`. */ public final function SetAdminStatus(AdminStatus newAdminStatus) { local PlayerReplicationInfo myReplicationInfo; myReplicationInfo = GetRI(); if (myReplicationInfo == none) { return; } switch (newAdminStatus) { case AS_Admin: myReplicationInfo.bAdmin = true; myReplicationInfo.bSilentAdmin = false; break; case AS_SilentAdmin: myReplicationInfo.bAdmin = false; myReplicationInfo.bSilentAdmin = true; break; default: myReplicationInfo.bAdmin = false; myReplicationInfo.bSilentAdmin = false; } } /** * Returns current amount of money caller `EPlayer` has. * * @return Amount of money `EPlayer` has. If player has already disconnected * method will return `0`. */ public final function int GetDosh() { local PlayerReplicationInfo myReplicationInfo; myReplicationInfo = GetRI(); if (myReplicationInfo == none) { return 0; } return myReplicationInfo.score; } /** * Sets amount of money that caller `EPlayer` will have. * * @param newDoshAmount New amount of money that caller `EPlayer` must have. */ public final function SetDosh(int newDoshAmount) { local PlayerReplicationInfo myReplicationInfo; myReplicationInfo = GetRI(); if (myReplicationInfo == none) { return; } myReplicationInfo.score = newDoshAmount; } /** * Return `ConsoleWriter` that can be used to write into this player's console. * * Provided that returned object is never deallocated - returns the same object * with each call, otherwise can allocate new instance of `ConsoleWriter`. * * @return `ConsoleWriter` that can be used to write into this player's * console. Returned object should not be deallocated, but it is * guaranteed to be valid for non-disconnected players. */ public final function /* borrow */ ConsoleWriter BorrowConsole() { if ( consoleInstance == none || consoleInstance.GetLifeVersion() != consoleLifeVersion) { consoleInstance = _.console.For(self); consoleLifeVersion = consoleInstance.GetLifeVersion(); } // Set us as target in case someone messed with this setting return consoleInstance.ForPlayer(self); } defaultproperties { usesObjectPool = false }