diff --git a/sources/Data/Database/BigInt.uc b/sources/Data/Database/BigInt.uc
new file mode 100644
index 0000000..3a41954
--- /dev/null
+++ b/sources/Data/Database/BigInt.uc
@@ -0,0 +1,280 @@
+/**
+ * A simply big integer implementation, mostly to allow Acedia's databases to
+ * store integers of arbitrary size. Should not be used in regular
+ * computations, designed to store player statistic values that are incremented
+ * from time to time.
+ * Copyright 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 BigInt extends AcediaObject
+ dependson(MathAPI);
+
+var private bool negative;
+// Digits array, from least to most significant:
+// For example, for 13524:
+// `digits[0] = 4`
+// `digits[1] = 2`
+// `digits[2] = 5`
+// `digits[3] = 3`
+// `digits[4] = 1`
+// Valid `BigInt` should not have this array empty.
+var private array digits;
+
+protected function Constructor()
+{
+ // Init with zero
+ digits[0] = 0;
+}
+
+protected function Finalizer()
+{
+ negative = false;
+ digits.length = 0;
+}
+
+// Minimal `int` value `-2,147,483,648` is slightly a pain to handle, so just
+// use this pre-made constructor for it
+private static function BigInt CreateMinimalNegative()
+{
+ local array newDigits;
+ local BigInt result;
+ newDigits[0] = 8;
+ newDigits[1] = 4;
+ newDigits[2] = 6;
+ newDigits[3] = 3;
+ newDigits[4] = 8;
+ newDigits[5] = 4;
+ newDigits[6] = 7;
+ newDigits[7] = 4;
+ newDigits[8] = 1;
+ newDigits[9] = 2;
+ result = BigInt(__().memory.Allocate(class'BigInt'));
+ result.digits = newDigits;
+ result.negative = true;
+ return result;
+}
+
+// Removes unnecessary zeroes from leading digit positions `digits`.
+// Does not change contained value.
+private final static function TrimLeadingZeroes()
+{
+ local int i, zeroesToRemove;
+
+ // Find how many leading zeroes there is.
+ // Since `digits` stores digits from least to most significant, we need
+ // to check from the end of `digits` array.
+ for (i = digits.length - 1; i >= 0; i -= 1)
+ {
+ if (digits[i] != 0) {
+ break;
+ }
+ zeroesToRemove += 1;
+ }
+ // `digits` must not be empty, enforce `0` value in that case
+ if (zeroesToRemove >= digits.length)
+ {
+ digits.length = 1;
+ digits[0] = 0;
+ negative = false;
+ }
+ else {
+ digits.length = digits.length - zeroesToRemove;
+ }
+}
+
+public final static function BigInt FromInt(int value)
+{
+ local bool valueIsNegative;
+ local array newDigits;
+ local BigInt result;
+ local MathAPI.IntegerDivisionResult divisionResult;
+
+ if (value < 0)
+ {
+ // Treat special case of minimal `int` value `-2,147,483,648` that
+ // won't fit into positive `int` as special and use pre-made
+ // specialized constructor `CreateMinimalNegative()`
+ if (value < -MaxInt) {
+ return CreateMinimalNegative();
+ }
+ else
+ {
+ valueIsNegative = true;
+ value *= -1;
+ }
+ }
+ if (value == 0) {
+ newDigits[0] = 0;
+ }
+ else
+ {
+ while (value > 0)
+ {
+ divisionResult = __().math.IntegerDivision(value, 10);
+ value = divisionResult.quotient;
+ newDigits[newDigits.length] = divisionResult.remainder;
+ }
+ }
+ result = BigInt(__().memory.Allocate(class'BigInt'));
+ result.digits = newDigits;
+ result.negative = valueIsNegative;
+ result.TrimLeadingZeroes();
+ return result;
+}
+
+public final static function BigInt FromDecimal(BaseText value)
+{
+ local int i;
+ local bool valueIsNegative;
+ local byte nextDigit;
+ local array newDigits;
+ local Parser parser;
+ local BigInt result;
+ local Basetext.Character nextCharacter;
+
+ if (value == none) {
+ return none;
+ }
+ parser = value.Parse();
+ if (parser.Match(P("-")).Ok())
+ {
+ valueIsNegative = true;
+ parser.Confirm();
+ }
+ parser.R();
+ newDigits.length = parser.GetRemainingLength();
+ i = newDigits.length - 1;
+ while (!parser.HasFinished())
+ {
+ // This should not happen, but just in case
+ if (i < 0) {
+ break;
+ }
+ parser.MCharacter(nextCharacter);
+ nextDigit = Clamp(__().text.CharacterToInt(nextCharacter), 0, 9);
+ newDigits[i] = nextDigit;
+ i -= 1;
+ }
+ result = BigInt(__().memory.Allocate(class'BigInt'));
+ result.digits = newDigits;
+ result.negative = valueIsNegative;
+ parser.FreeSelf();
+ result.TrimLeadingZeroes();
+ return result;
+}
+
+public final static function BigInt FromDecimal_S(string value)
+{
+ local MutableText wrapper;
+ local BigInt result;
+
+ wrapper = __().text.FromStringM(value);
+ result = FromDecimal(wrapper);
+ wrapper.FreeSelf();
+ return result;
+}
+
+private function _add(BigInt other)
+{
+ local int i;
+ local byte carry, digitSum;
+ local array otherDigits;
+
+ if (other == none) {
+ return;
+ }
+ otherDigits = other.digits;
+ if (digits.length < otherDigits.length) {
+ digits.length = otherDigits.length;
+ }
+ carry = 0;
+ for (i = 0; i < digits.length; i += 1)
+ {
+ digitSum = digits[i] + otherDigits[i] + carry;
+ digits[i] = _.math.Remainder(digitSum, 10);
+ carry = (digitSum - digits[i]) / 10;
+ }
+ if (carry > 0) {
+ digits[digits.length] = carry;
+ }
+}
+
+public function BigInt Add(BigInt other)
+{
+ _add(other);
+ return self;
+}
+
+public function BigInt AddInt(int other)
+{
+ local BigInt otherObject;
+
+ otherObject = FromInt(other);
+ Add(otherObject);
+ _.memory.Free(otherObject);
+ return self;
+}
+
+/*public function BigInt Multiply(BigInt other);
+public function BigInt MultiplyInt(int other);
+
+public function bool IsNegative();
+public function (int other);
+
+public function int ToInt();
+
+public function Text ToText();
+
+public function Text ToText_M();*/
+
+public function Text ToText()
+{
+ return ToText_M().IntoText();
+}
+
+public function MutableText ToText_M()
+{
+ local int i;
+ local MutableText result;
+
+ result = _.text.Empty();
+ if (negative) {
+ result.AppendCharacter(_.text.GetCharacter("-"));
+ }
+ for (i = digits.length - 1; i >= 0; i -= 1) {
+ result.AppendCharacter(_.text.CharacterFromCodePoint(digits[i] + 48));
+ }
+ return result;
+}
+
+public function string ToString()
+{
+ local int i;
+ local string result;
+
+ if (negative) {
+ result = "-";
+ }
+ for (i = digits.length - 1; i >= 0; i -= 1) {
+ result = result $ digits[i];
+ }
+ return result;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Data/Database/Tests/TEST_BigInt.uc b/sources/Data/Database/Tests/TEST_BigInt.uc
new file mode 100644
index 0000000..b5f8845
--- /dev/null
+++ b/sources/Data/Database/Tests/TEST_BigInt.uc
@@ -0,0 +1,106 @@
+/**
+ * Set of tests for `BigInt` class.
+ * Copyright 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 TEST_BigInt extends TestCase
+ abstract;
+
+protected static function TESTS()
+{
+ // Here we use `ToString()` method to check `BigInt` creation,
+ // therefore also testing it
+ Test_Creating();
+ // So here we nee to test `ToText()` methods separately
+ Test_ToText();
+ Test_LeadingZeroes();
+ //Test_AddingValues();
+}
+// TODO: leading zeroes
+protected static function Test_Creating()
+{
+ Context("Testing creation of `BigInt`s.");
+ Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
+ "a positive `int`.");
+ TEST_ExpectTrue(class'BigInt'.static.FromInt(13524).ToString() == "13524");
+ TEST_ExpectTrue(
+ class'BigInt'.static.FromInt(MaxInt).ToString() == "2147483647");
+
+ Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
+ "a positive integer inside `string`.");
+ TEST_ExpectTrue(
+ class'BigInt'.static.FromDecimal_S("2147483647").ToString()
+ == "2147483647");
+ TEST_ExpectTrue(
+ class'BigInt'.static.FromDecimal_S("4238756872643464981264982128742389")
+ .ToString() == "4238756872643464981264982128742389");
+
+ Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
+ "a negative `int`.");
+ TEST_ExpectTrue(class'BigInt'.static.FromInt(-666).ToString() == "-666");
+ TEST_ExpectTrue(
+ class'BigInt'.static.FromInt(-MaxInt).ToString() == "-2147483647");
+ TEST_ExpectTrue(
+ class'BigInt'.static.FromInt(-MaxInt - 1).ToString() == "-2147483648");
+
+ Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
+ "a negative integer inside `string`.");
+ TEST_ExpectTrue(
+ class'BigInt'.static.FromDecimal_S("-2147483648").ToString()
+ == "-2147483648");
+ TEST_ExpectTrue(
+ class'BigInt'.static.FromDecimal_S("-238473846327894632879097410348127")
+ .ToString() == "-238473846327894632879097410348127");
+}
+
+protected static function Test_ToText()
+{
+ Context("Testing `ToText()` method of `BigInt`s.");
+ Issue("`ToText()` doesn't return value `BigInt` was initialized with" @
+ "a positive integer inside `string`.");
+ TEST_ExpectTrue(class'BigInt'.static
+ .FromDecimal_S("2147483647")
+ .ToText()
+ .ToString() == "2147483647");
+ TEST_ExpectTrue(class'BigInt'.static
+ .FromDecimal_S("65784236592763459236597823645978236592378659110571388")
+ .ToText()
+ .ToString() == "65784236592763459236597823645978236592378659110571388");
+
+ Issue("`ToText()` doesn't return value `BigInt` was initialized with" @
+ "a negative integer inside `string`.");
+ TEST_ExpectTrue(class'BigInt'.static
+ .FromDecimal_S("-2147483648")
+ .ToText()
+ .ToString() == "-2147483648");
+ TEST_ExpectTrue(class'BigInt'.static
+ .FromDecimal_S("-9827657892365923510176386357863078603212901078175829")
+ .ToText()
+ .ToString() == "-9827657892365923510176386357863078603212901078175829");
+}
+
+/*protected static function Test_AddingValues()
+{
+ Context("Testing adding values to `BigInt`");
+ Issue("`JSONPointer` is incorrectly extracted.");
+}*/
+
+defaultproperties
+{
+ caseGroup = "Database"
+ caseName = "BigInt"
+}
\ No newline at end of file
diff --git a/sources/Manifest.uc b/sources/Manifest.uc
index 581c733..57aebf8 100644
--- a/sources/Manifest.uc
+++ b/sources/Manifest.uc
@@ -50,9 +50,10 @@ defaultproperties
testCases(22) = class'TEST_CommandDataBuilder'
testCases(23) = class'TEST_LogMessage'
testCases(24) = class'TEST_SchedulerAPI'
- testCases(25) = class'TEST_DatabaseCommon'
- testCases(26) = class'TEST_LocalDatabase'
- testCases(27) = class'TEST_AcediaConfig'
- testCases(28) = class'TEST_UTF8EncoderDecoder'
- testCases(29) = class'TEST_AvariceStreamReader'
+ testCases(25) = class'TEST_BigInt'
+ testCases(26) = class'TEST_DatabaseCommon'
+ testCases(27) = class'TEST_LocalDatabase'
+ testCases(28) = class'TEST_AcediaConfig'
+ testCases(29) = class'TEST_UTF8EncoderDecoder'
+ testCases(30) = class'TEST_AvariceStreamReader'
}
\ No newline at end of file