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