diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 87f06ed..c60fa1e 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -31,4 +31,5 @@ defaultproperties testCases(3) = class'TEST_Text' testCases(4) = class'TEST_TextAPI' testCases(5) = class'TEST_Parser' + testCases(6) = class'TEST_Player' } \ No newline at end of file diff --git a/sources/Players/APlayer.uc b/sources/Players/APlayer.uc new file mode 100644 index 0000000..53a0360 --- /dev/null +++ b/sources/Players/APlayer.uc @@ -0,0 +1,26 @@ +/** + * Main and only Acedia mutator used for loading Acedia packages + * and providing access to mutator events' calls. + * Name is chosen to make config files more readable. + * Copyright 2020 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 APlayer extends AcediaObject; + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Players/APlayerID.uc b/sources/Players/APlayerID.uc new file mode 100644 index 0000000..26ed045 --- /dev/null +++ b/sources/Players/APlayerID.uc @@ -0,0 +1,295 @@ +/** + * Acedia's class for storing player's ID. + * This class is inherently linked to steam and it's SteamIDs, since + * Killing Floor 1 is all but guaranteed to be steam-exclusive. + * Still, if you wish to use it in a portable manner, limit yourself to + * `Initialize()`, `IsInitialized()`, `IsEqual()` and `GetUniqueID()`. + * Copyright 2020 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 APlayerID extends AcediaObject; + +/** + * Stead data corresponding to a SteamID that relevant `APlayerID` was + * initialized with. + * + * For more info read: https://developer.valvesoftware.com/wiki/SteamID + */ +struct SteamData +{ + var public byte accountType; + var public byte universe; + var public int instance; + // 32 lowest bits of SteadID64. + // Corresponds to a combination of "Y" and "Z" in "STEAM_X:Y:Z". + var public int steamID32; + // Other 4 fields fully define a SteamID and `steamID64` can be generated + // from them, but it is easier to simply cache it in a separate variable. + var string steamID64; +}; +var protected SteamData initializedData; +// To make it safe to pass `APlayerID` to users, prevent any modifications +// after `initialized` is set to `true`. +var protected bool initialized; + +// Given a number in form of array (`digits`) of it's digits +// (425327 <-> [4, 2, 5, 3, 2, 7]) +// return given number mod 2 and +// divide that number by two (record result in that same array) +private final function int DivideDigitArrayByTwo(out array digits) +{ + local int i; + local int wasOdd; + if (digits[digits.length - 1] % 2 == 1) + { + wasOdd = 1; + digits[digits.length - 1] -= 1; + } + for (i = digits.length - 1; i >= 0; i -= 1) + { + if (digits[i] % 2 == 1) + { + digits[i] -= 1; + // `digits[digits.length - 1]` was guaranteed to be even before + // this cycle, so it is safe to add 1 to the index here + digits[i + 1] += 5; + } + digits[i] = digits[i] / 2; + } + return wasOdd; +} + +// Given a number in form of array (`digits`) of it's digits +// (425327 <-> [4, 2, 5, 3, 2, 7]) +// extracts `bitsToRead` of lower bits from it and returns them as an `int`. +private final function int ReadBitsFromDigitArray( + out array digits, + int bitsToRead) +{ + local int i; + local int result; + local int binaryPadding; + result = 0; + binaryPadding = 1; + for (i = 0; i < bitsToRead; i += 1) { + result += DivideDigitArrayByTwo(digits) * binaryPadding; + binaryPadding *= 2; + } + return result; +} + +// Explanation of what that is: +// https://developer.valvesoftware.com/wiki/SteamID#Types_of_Steam_Accounts +private final function string GetSteamAccountTypeCharacter() +{ + // Individual + if (initializedData.accountType == 1) return "U"; + // Multiseat + if (initializedData.accountType == 2) return "M"; + // GameServer + if (initializedData.accountType == 3) return "G"; + // AnonGameServer + if (initializedData.accountType == 4) return "A"; + // Pending + if (initializedData.accountType == 5) return "P"; + // ContentServer + if (initializedData.accountType == 6) return "C"; + // Clan + if (initializedData.accountType == 7) return "g"; + // Chat + if (initializedData.accountType == 8) return "c"; + // P2P SuperSeeder + if (initializedData.accountType == 9) return ""; + // AnonUser + if (initializedData.accountType == 10) return "a"; + // Invalid + return "I"; +} + +/** + * Initializes caller `APlayerID` from a given `string` ID. + * + * Each `APLayerID` can only be initialized once and becomes immutable + * afterwards. + * + * @param steamID64 `string` with unique ID, provided by the game + * (Steam64 ID used in profile permalink, + * like http://steamcommunity.com/profiles/76561198025127722) + * + * @return `true` if initialization was successful and `false` otherwise + * (can only happen if caller `APlayerID` was already initialized). + */ +public final function bool Initialize(string steamID64) +{ + local int i; + local array characters; + local array digits; + if (initialized) return false; + + characters = _().text.StringToRaw(steamID64); + for (i = 0; i < characters.length; i += 1) { + digits[digits.length] = _().text.CharacterToInt(characters[i]); + } + initializedData.steamID64 = steamID64; + // Refer to https://developer.valvesoftware.com/wiki/SteamID + // The lowest bit represents Y. + // The next 31 bits represents the account number. + // ^ these two can be combined into a "SteamID32". + initializedData.steamID32 = ReadBitsFromDigitArray(digits, 32); + // The next 20 bits represents the instance of the account. + initializedData.instance = ReadBitsFromDigitArray(digits, 20); + // The next 4 bits represents the type of account. + initializedData.accountType = ReadBitsFromDigitArray(digits, 4); + // The next 8 bits represents the "Universe" the steam account belongs to. + initializedData.universe = ReadBitsFromDigitArray(digits, 8); + initialized = true; + return true; +} + +/** + * Checks if caller `APlayerID` was already initialized + * (and is, therefore, immutable). + * + * @return `true` if it was initialized and `false` otherwise. + */ +public final function bool IsInitialized() +{ + return initialized; +} + +/** + * Returns steam data (see `APlayerID.SteamData`) of the caller `APlayerData`. + * + * Only returns a valid value if caller `APLayerData` was already initialized. + * + * @return `APlayerID.SteamData` of a caller `APlayerID`; + * structure will be filled with default values if caller `APlayerID` + * was not initialized. + */ +public final function SteamData GetSteamData() +{ + return initializedData; +} + +/** + * Checks if two `APlayerID`s are the same. + * + * @param otherID `APlayerID` to compare caller object to. + * @return `true` if caller `APlayerID` is identical to `otherID` and + * `false` otherwise. If at least one of the `APlayerID`s being compared is + * uninitialized, the result will be `false`. + */ +public final function bool IsEqual(APlayerID otherID) +{ + if (!IsInitialized()) return false; + if (!otherID.IsInitialized()) return false; + return (initializedData.steamID32 == otherID.initializedData.steamID32); +} + +/** + * Returns unique string representation of the caller `APlayerData`. + * + * Only returns a valid value if caller `APLayerData` was already initialized. + * + * @return Unique string representation of the caller `APlayerData` + * if it was initialized and `false` otherwise. + */ +public final function string GetUniqueID() +{ + if (!IsInitialized()) return ""; + return initializedData.steamID64; +} + +/** + * Returns string representation of the caller `APlayerData` in + * following format: "STEAM_X:Y:Z". + * + * Only returns a valid value if caller `APLayerData` was already initialized. + * + * @return String representation of the caller `APlayerData` in + * form "STEAM_X:Y:Z" if it was initialized and empty `string` otherwise. + */ +public final function string GetSteamID() +{ + local int Y, Z; + Y = 0; + Z = initializedData.steamID32; + if (Z % 2 == 1) + { + Y = 1; + Z -= 1; + } + Z = Z / 2; + return ("STEAM_" $ initializedData.universe $ ":" $ Y $ ":" $ Z); +} + +/** + * Returns string representation of the caller `APlayerData` in + * following format: "C:U:A", where + * C is character representation of Account Type; + * U is "Universe" steam account belongs to; + * A is account ID. + * + * Only returns a valid value if caller `APLayerData` was already initialized. + * + * @return String representation of the caller `APlayerData` in + * form "C:U:A" if it was initialized and empty `string` otherwise. + */ +public final function string GetSteamID3() +{ + return (GetSteamAccountTypeCharacter() + $ ":" $ initializedData.universe + $ ":" $ initializedData.steamID32); +} + +/** + * Returns Steam32 ID for the caller `APlayerData`. It is a lowest 32 bits of + * the full Steam64 ID. + * + * Only returns a valid value if caller `APLayerData` was already initialized. + * + * @return Unique `int` representation of the caller `APlayerData` + * if it was initialized and `-1` otherwise. + */ +public final function int GetSteamID32() +{ + if (!IsInitialized()) return -1; + return initializedData.steamID32; +} + +/** + * Returns Steam64 ID for the caller `APlayerData`. + * + * Only returns a valid value if caller `APLayerData` was already initialized. + * + * Since UnrealEngine 2 does not support 64-bit integer values, it is returned + * simply as a decimal representation of a whole Steam64 ID + * (Steam64 ID used in profile permalink, + * like http://steamcommunity.com/profiles/76561198025127722). + * + * @return String representation of the Steam64 ID of the caller `APlayerData` + * if it was initialized and empty `string` otherwise. + */ +public final function string GetSteamID64() +{ + if (!IsInitialized()) return ""; + return initializedData.steamID64; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Players/Tests/TEST_Player.uc b/sources/Players/Tests/TEST_Player.uc new file mode 100644 index 0000000..28bbcd0 --- /dev/null +++ b/sources/Players/Tests/TEST_Player.uc @@ -0,0 +1,69 @@ +/** + * Set of tests for `APlayer` and related classes. + * Copyright 2020 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 TEST_Player extends TestCase + abstract; + +protected static function TESTS() +{ + Test_PlayerID(); +} + + +protected static function Test_PlayerID() +{ + local APlayerID.SteamData steamData; + local APlayerID testID, testID2, testID3; + testID = APlayerID(_().memory.Allocate(class'APlayerID')); + Context("Testing Acedia's player ID (`APlayerID`)."); + Issue("`APlayerID` initialization works incorrectly."); + TEST_ExpectFalse(testID.IsInitialized()); + TEST_ExpectTrue(testID.Initialize("76561198025127722")); + TEST_ExpectTrue(testID.IsInitialized()); + TEST_ExpectFalse(testID.Initialize("76561198044316328")); + + Issue("`APlayerID` incorrectly handles SteamID."); + TEST_ExpectTrue(testID.GetUniqueID() == "76561198025127722"); + TEST_ExpectTrue(testID.GetSteamID() == "STEAM_1:0:32430997"); + TEST_ExpectTrue(testID.GetSteamID3() == "U:1:64861994"); + TEST_ExpectTrue(testID.GetSteamID32() == 64861994); + TEST_ExpectTrue(testID.GetSteamID64() == "76561198025127722"); + + Issue("Two `APlayerID` equality check is incorrect."); + testID2 = APlayerID(_().memory.Allocate(class'APlayerID')); + testID3 = APlayerID(_().memory.Allocate(class'APlayerID')); + testID2.Initialize("76561198025127722"); + testID3.Initialize("76561198044316328"); + TEST_ExpectTrue(testID.IsEqual(testID)); + TEST_ExpectTrue(testID.IsEqual(testID2)); + TEST_ExpectFalse(testID3.IsEqual(testID)); + + Issue("Steam data returned by `APlayerID` is incorrect."); + steamData = testID3.GetSteamData(); + TEST_ExpectTrue(steamData.accountType == 1); + TEST_ExpectTrue(steamData.universe == 1); + TEST_ExpectTrue(steamData.instance == 1); + TEST_ExpectTrue(steamData.steamID32 == 84050600); + TEST_ExpectTrue(steamData.steamID64 == "76561198044316328"); +} + +defaultproperties +{ + caseName = "Player" +} \ No newline at end of file