From c423fdf9834703776845cc3023c2defa024713da Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 5 Jul 2020 19:12:10 +0700 Subject: [PATCH] Add bit stream support --- sources/NicePack.uc | 47 +++- sources/_acedia/BitStreamReader.uc | 274 ++++++++++++++++++++++ sources/_acedia/BitStreamWriter.uc | 352 +++++++++++++++++++++++++++++ 3 files changed, 672 insertions(+), 1 deletion(-) create mode 100644 sources/_acedia/BitStreamReader.uc create mode 100644 sources/_acedia/BitStreamWriter.uc diff --git a/sources/NicePack.uc b/sources/NicePack.uc index ea2140b..e573ad4 100644 --- a/sources/NicePack.uc +++ b/sources/NicePack.uc @@ -634,13 +634,15 @@ function GiveProgressiveDosh(NicePlayerController nicePlayer){ } } simulated function Mutate(string MutateString, PlayerController kfPlayer){ - local int i; + local int i, readLenght; local NicePlayerController nicePlayer; local NiceServerData remoteData; // Tokens from 'MutateString' local array wordsArray; local String command, mod; local String white; + local BitStreamWriter inputStream; + local BitStreamReader outputStream; // Array with command modifiers. // Always contains at least 10 elements, that may be empty strings if there wasn't enough modifiers. // Done for safe access without the need to check for bounds. @@ -732,8 +734,51 @@ simulated function Mutate(string MutateString, PlayerController kfPlayer){ else if(command ~= "PRINT"){ nicePlayer.ClientPrint(); } + else if(command ~= "TEST"){ + inputStream = new class'BitStreamWriter'; + outputStream = new class'BitStreamReader'; + inputStream.WriteInt(Len(mod), 5); + inputStream.WriteClassName(mod); + outputStream.Initialize(inputStream.GetData()); + readLenght = outputStream.ReadInt(5); + nicePlayer.ClientMessage("Input lenght:" @ string(Len(mod))); + nicePlayer.ClientMessage("Compressed lenght:" @ string(inputStream.GetSizeInBytes()) ); + nicePlayer.ClientMessage("Output:"@outputStream.ReadClassName(readLenght)); + } Super.Mutate(MutateString, kfPlayer); } +/* Good test for writer +else if(command ~= "TEST"){ + inputStream = new class'BitStreamWriter'; + outputStream = new class'BitStreamReader'; + //stream.PushByte(167, int(mod)); + inputStream.WriteInt(3, 3); + inputStream.WriteByte(49, 7); + inputStream.WriteInt(1651779982, 25); + inputStream.WriteInt(2, 2); + bytes = inputStream.GetData(); + bits = inputStream.GetSize(); + nicePlayer.ClientMessage("SIZE:" @ string(bits)); + for (i = 0; i < bytes.length; i += 1) { + nicePlayer.ClientMessage("Content:" @ string(bytes[i])); + } + outputStream.Initialize(bytes); + //nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(3))); + //nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(7))); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(8))); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(4))); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(4))); + outputStream.Initialize(bytes); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(3))); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(7))); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(5))); + outputStream.Initialize(bytes); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadInt(3))); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(7))); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadInt(25))); + nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadInt(2))); + } + */ // Event functions // Called at the start of the match function MatchBegan(){ diff --git a/sources/_acedia/BitStreamReader.uc b/sources/_acedia/BitStreamReader.uc new file mode 100644 index 0000000..23c01fa --- /dev/null +++ b/sources/_acedia/BitStreamReader.uc @@ -0,0 +1,274 @@ +/** + * Class for packaging various data types into an array of bytes. + * 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 BitStreamReader extends Object; + +// Array of bytes to read from +var private array stream; +var private int currentBytePointer; +var private int currentBitPointer; +// Write `byte` from `stream[currentBytePointer]` to avoid unnecessary +// array access calls. +var private byte currentByte; + +var private const array bitMask; +var private const array bitMaskBefore; +var private const array bitMaskAfter; + +var private int tempInt; +var private string tempString; +var private byte temp; +var private byte result; +var private byte rightBoundary; +var private byte remainingToRead; +var private byte byte1, byte2, byte3, byte4; + +public final function Initialize(array newStream) +{ + stream = newStream; + currentBytePointer = 0; + currentBitPointer = 0; + if (stream.length > 0) { + currentByte = stream[0]; + } +} + +private final function ShiftBytePointer() +{ + currentBitPointer = 0; + currentBytePointer += 1; + if (currentBytePointer < stream.length) { + currentByte = stream[currentBytePointer]; + } + else { + currentByte = 0; + } +} + +public final function byte ReadBit() +{ + if (currentBytePointer >= stream.length) return 0; + if ((currentByte & bitMask[currentBitPointer]) > 0) { + currentBitPointer += 1; + if (currentBitPointer >= 8) { + ShiftBytePointer(); + } + return 1; + } + currentBitPointer += 1; + if (currentBitPointer >= 8) { + ShiftBytePointer(); + } + return 0; +} + +public final function bool ReadBool() +{ + if (currentBytePointer >= stream.length) return false; + if ((currentByte & bitMask[currentBitPointer]) > 0) { + currentBitPointer += 1; + if (currentBitPointer >= 8) { + ShiftBytePointer(); + } + return true; + } + currentBitPointer += 1; + if (currentBitPointer >= 8) { + ShiftBytePointer(); + } + return false; +} + +public final function byte ReadByte(optional int bitLimit) +{ + if (currentBytePointer >= stream.length) return 0; + // Default `bitLimit` + if (bitLimit <= 0 || bitLimit > 8) { + bitLimit = 8; + } + // Throw away already read part + result = currentByte & bitMaskAfter[currentBitPointer]; + if (bitLimit <= 8 - currentBitPointer) + { + // If all we are asked to read is contained in cuurent `byte` - + // just throw away excessive info by shifting to the right + result = result >>> (8 - currentBitPointer - bitLimit); + currentBitPointer += bitLimit; + if (currentBitPointer >= 8) { + ShiftBytePointer(); + } + return result; + } + // Otherwise shift already read part up + rightBoundary = bitLimit - (8 - currentBitPointer); + result = result << rightBoundary; + // Move to the next byte + ShiftBytePointer(); + currentBitPointer = rightBoundary; + // Record required part from the second byte + temp = currentByte & bitMaskBefore[rightBoundary]; + temp = temp >>> (8 - rightBoundary); + result = result | temp; + return result; +} + +public final function int ReadInt(optional int bitLimit) +{ + // Default `bitLimit` + if (bitLimit <= 0 || bitLimit > 32) { + bitLimit = 32; + } + byte1 = ReadByte(bitLimit); + if (bitLimit <= 8) { + return byte1; + } + bitLimit -= 8; + byte2 = ReadByte(bitLimit); + if (bitLimit <= 8) + { + tempInt = byte1 << bitLimit; + tempInt = tempInt | byte2; + return tempInt; + } + bitLimit -= 8; + byte3 = ReadByte(bitLimit); + if (bitLimit <= 8) + { + tempInt = byte1 << (bitLimit + 8); + tempInt = tempInt | (byte2 << bitLimit); + tempInt = tempInt | byte3; + return tempInt; + } + bitLimit -= 8; + byte4 = ReadByte(bitLimit); + tempInt = byte1 << (bitLimit + 16); + tempInt = tempInt | (byte2 << (bitLimit + 8)); + tempInt = tempInt | (byte3 << bitLimit); + tempInt = tempInt | byte4; + return tempInt; +} + +public final function float ReadFloat( + int precisionLevel, + optional byte bitLimit) +{ + if (bitLimit < 0 || bitLimit > 32) { + bitLimit = 32; + } + precisionLevel = Max(0, precisionLevel); + return float(ReadInt(bitLimit)) * (0.1 ** precisionLevel); +} + +public final function string ReadString(int length) +{ + if (length <= 0) return ""; + while (length > 0) + { + length -= 1; + tempString $= Chr(ReadByte()); + } + return tempString; +} + +// String representation of `class`es has a more limited character range, +// which allows us to fit every chgaracter in `class`' name into 6 bits, +// saving 25% space. +// Allowed characters are: digits, upper and lower case letters, dot '.' +// and underscore '_'. +// Any other symbol is converted into an underscore. +private final function byte CompressClassCharacter(byte source) +{ + // 26 upper case character + if (source >= 65 && source <= 90) { + return source - 65; + } + // 26 lower case character // 52 total + if (source >= 97 && source <= 122) { + return 26 + (source - 97); + } + // 10 digits // 62 total + if (source >= 48 && source <= 57) { + return 52 + (source - 48); + } + // dot + if (source == 46) { + return 62; + } + // underscore and everything else + return 63; +} + +private final function byte DecompressClassCharacter(byte source) +{ + if (source >= 63) return 95; + // 26 upper case character + if (source <= 25) { + return source + 65; + } + // 26 lower case character + if (source <= 51) { + return source + 71; + } + // 10 digits + if (source <= 61) { + return source - 4; + } + // dot + // if (source == 62) + return 46; +} + +public final function string ReadClassName(int length) +{ + if (length <= 0) return ""; + while (length > 0) + { + length -= 1; + tempString $= Chr(DecompressClassCharacter(ReadByte(6))); + } + return tempString; +} + +defaultproperties +{ + bitMask(0) = 128 // 1 0 0 0 0 0 0 0 + bitMask(1) = 64 // 0 1 0 0 0 0 0 0 + bitMask(2) = 32 // 0 0 1 0 0 0 0 0 + bitMask(3) = 16 // 0 0 0 1 0 0 0 0 + bitMask(4) = 8 // 0 0 0 0 1 0 0 0 + bitMask(5) = 4 // 0 0 0 0 0 1 0 0 + bitMask(6) = 2 // 0 0 0 0 0 0 1 0 + bitMask(7) = 1 // 0 0 0 0 0 0 0 1 + bitMaskBefore(0) = 128 // 1 0 0 0 0 0 0 0 + bitMaskBefore(1) = 192 // 1 1 0 0 0 0 0 0 + bitMaskBefore(2) = 224 // 1 1 1 0 0 0 0 0 + bitMaskBefore(3) = 240 // 1 1 1 1 0 0 0 0 + bitMaskBefore(4) = 248 // 1 1 1 1 1 0 0 0 + bitMaskBefore(5) = 252 // 1 1 1 1 1 1 0 0 + bitMaskBefore(6) = 254 // 1 1 1 1 1 1 1 0 + bitMaskBefore(7) = 255 // 1 1 1 1 1 1 1 1 + bitMaskAfter(0) = 255 // 1 1 1 1 1 1 1 1 + bitMaskAfter(1) = 127 // 0 1 1 1 1 1 1 1 + bitMaskAfter(2) = 63 // 0 0 1 1 1 1 1 1 + bitMaskAfter(3) = 31 // 0 0 0 1 1 1 1 1 + bitMaskAfter(4) = 15 // 0 0 0 0 1 1 1 1 + bitMaskAfter(5) = 7 // 0 0 0 0 0 1 1 1 + bitMaskAfter(6) = 3 // 0 0 0 0 0 0 1 1 + bitMaskAfter(7) = 1 // 0 0 0 0 0 0 0 1 +} \ No newline at end of file diff --git a/sources/_acedia/BitStreamWriter.uc b/sources/_acedia/BitStreamWriter.uc new file mode 100644 index 0000000..079709d --- /dev/null +++ b/sources/_acedia/BitStreamWriter.uc @@ -0,0 +1,352 @@ +/** + * Class for packaging various data types into an array of bytes. + * 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 BitStreamWriter extends Object; + +// Array of fully written bytes +var private array stream; +// Last byte that still has some space to fit data into. +// We start writing data into it's least significant bits and shift them up +// when we want to write more data inside or when we are asked to return +// recorded data. +var private byte unfinishedByte; +// How much space is left in `unfinishedByte` +var private byte bitsLeft; + +// Can be used to erase unnecessary bits from a byte +var private const array bitMaskAfter; + +// We'll declare all auxiliary variables as globals to avoid their +// unnecessary creation inside functions, to make them more lightweight. +var private byte shift, altShift; +var private byte temp; +var private int tempInt; +var private byte byte1, byte2, byte3, byte4; + +/** + * Resets contents of the `BitStreamWriter`, making it the same as a + * brand new writer. + * + * @return `BitStreamWriter` to allow for function chaining. + */ +public final function BitStreamWriter InitializeStream() +{ + stream.length = 0; + unfinishedByte = 0; + bitsLeft = 8; + return self; +} + +/** + * Records a bit inside a `BitStreamWriter`. + * + * @param source Byte that defines a bit. We don't look at any actual bits in + * `source`'s representation, but simply consired the bit to be + * `1` if `source > 0` and `0` if `source == 0`. + * @return `BitStreamWriter` to allow for function chaining. + */ +public final function BitStreamWriter WriteBit(byte source) +{ + unfinishedByte = unfinishedByte << 1; + if (source > 0) { + unfinishedByte += 1; + } + bitsLeft -= 1; + if (bitsLeft <= 0) { + stream[stream.length] = unfinishedByte; + unfinishedByte = 0; + bitsLeft = 8; + } + return self; +} + +/** + * Records a bit inside a `BitStreamWriter`. + * + * @param source Boolean value that defines a bit. `true` means bit is equal + * to `1` and `false` means that it is equal to `0`. + * @return `BitStreamWriter` to allow for function chaining. + */ +public final function BitStreamWriter WriteBoolean(bool isOne) +{ + unfinishedByte = unfinishedByte << 1; + if (isOne) { + unfinishedByte += 1; + } + // Update storage + bitsLeft -= 1; + if (bitsLeft <= 0) { + stream[stream.length] = unfinishedByte; + unfinishedByte = 0; + bitsLeft = 8; + } + return self; +} + +/** + * Records a `byte` inside a `BitStreamWriter`. + * + * @param source Boolean value that defines a bit. `true` means bit is + * equal to `1` and `false` means that it is equal to `0`. + * @param bitLimit How many of the (least significant) bits to record; + * can be used to compress values that don't need the full `byte` + * value range. + * @return `BitStreamWriter` to allow for function chaining. + */ +public final function BitStreamWriter WriteByte( + byte source, + optional byte bitLimit) +{ + // Default `bitLimit` + if (bitLimit < 0 || bitLimit > 8) { + bitLimit = 8; + } + // Zero unnecessary bits + source = source & bitMaskAfter[8 - bitLimit]; + if (bitLimit < bitsLeft) + { + // We have enough space to fit all the bits in `unfinishedByte` + unfinishedByte = unfinishedByte << bitLimit; + unfinishedByte = unfinishedByte | source; + // Update storage + bitsLeft -= bitLimit; + if (bitsLeft <= 0) + { + stream[stream.length] = unfinishedByte; + unfinishedByte = 0; + bitsLeft = 8; + } + return self; + } + // If we don't have enough space, - record what can fit in + // current `unfinishedByte` + unfinishedByte = unfinishedByte << bitsLeft; + altShift = bitLimit - bitsLeft; + temp = source >>> altShift; + unfinishedByte = unfinishedByte | temp; + // And add it to the storage + stream[stream.length] = unfinishedByte; + // Create new byte by erasing recorded bits from the `source` + temp = temp << altShift; + unfinishedByte = source ^ temp; + bitsLeft = 8 - altShift; + return self; +} + +/** + * Records an `int` inside a `BitStreamWriter`. + * + * @param source Integer value to record into `BitStreamWriter`. + * @param bitLimit How many of the (least significant) bits to record; + * can be used to compress values that don't need the full `int` + * value range. + * @return `BitStreamWriter` to allow for function chaining. + */ +public final function BitStreamWriter WriteInt( + int source, + optional byte bitLimit) +{ + // Default `bitLimit` + if (bitLimit == 0) { + bitLimit = 32; + } + byte1 = byte((source & 0xff000000) >>> 24); + byte2 = byte((source & 0x00ff0000) >>> 16); + byte3 = byte((source & 0x0000ff00) >>> 8); + byte4 = source & 0x000000ff; + if (bitLimit > 24) { + WriteByte(byte1, bitLimit - 24); + } + if (bitLimit > 16) { + WriteByte(byte2, bitLimit - 16); + } + if (bitLimit > 8) { + WriteByte(byte3, bitLimit - 8); + } + WriteByte(byte4, bitLimit); + return self; +} + +/** + * Records an `float` inside a `BitStreamWriter`. + * + * `floats` can only be recorded with a specified amount of decimal places. + * This is because we can't really get precise float bit representation or + * easily truncate it's value bit-wise, so we multiply it by a specified + * power of 10 and then transfer integer part of the result as `int`. + * Specified precision level IS NOT recorded into `BitStreamWriter`. + * + * @param source Integer value to record into `BitStreamWriter`. + * @param precisionLevel How many decimal places after the dot to record. + * @param bitLimit How many of the (least significant) bits to record; + * can be used to compress values that don't need the full value range of + * truncated float. + * @return `BitStreamWriter` to allow for function chaining. + */ +public final function BitStreamWriter WriteFloat( + float source, + int precisionLevel, + optional byte bitLimit) +{ + if (bitLimit < 0 || bitLimit > 32) { + bitLimit = 32; + } + precisionLevel = Max(0, precisionLevel); + tempInt = int(Round( source * (10 ** precisionLevel) )); + WriteInt(tempInt, bitLimit); + return self; +} + +/** + * Records an `string` inside a `BitStreamWriter`, treating each code point as + * a byte, which will lead to loss of data when recording `string`s that + * contain code points with values `> 255`. + * + * Does not record `string`'s length or where it ends. + * + * @param source `string` value to record into `BitStreamWriter`. + * @return `BitStreamWriter` to allow for function chaining. + */ +public final function BitStreamWriter WriteString(string source) +{ + while (source != "") + { + WriteByte(Asc(source)); + source = Mid(0, 1); + } + return self; +} + +/** + * Records an string representation of a `class` name inside + * a `BitStreamWriter`, compressing it's characters to 6 bit representation. + * + * Allowed characters are: digits, upper and lower case letters, dot '.' + * and underscore '_'. + * Any other character will be converted into underscore. + * + * Does not record `string`'s length or where it ends. + * + * @param source String representation of a `class`' name value to record + * into `BitStreamWriter`. + * @return `BitStreamWriter` to allow for function chaining. + */ +public final function BitStreamWriter WriteClassName(string source) +{ + while (source != "") + { + WriteByte(CompressClassCharacter(Asc(source)), 6); + source = Mid(source, 1); + } + return self; +} + +// String representation of `class`es has a more limited character range, +// which allows us to fit every chgaracter in `class`' name into 6 bits, +// saving 25% space. +// Allowed characters are: digits, upper and lower case letters, dot '.' +// and underscore '_'. +// Any other symbol is converted into an underscore. +private final function byte CompressClassCharacter(byte source) +{ + // 26 upper case character + if (source >= 65 && source <= 90) { + return source - 65; + } + // 26 lower case character // 52 total + if (source >= 97 && source <= 122) { + return 26 + (source - 97); + } + // 10 digits // 62 total + if (source >= 48 && source <= 57) { + return 52 + (source - 48); + } + // dot + if (source == 46) { + return 62; + } + // underscore (`95`) and everything else + return 63; +} + +/** + * Returns all data recorded so far in a caller `BitStreamWriter` as + * an array of bytes. Data is written ion order, starting from bytes with + * lesser indecies and their most significant bits. + * + * @return Array of bytes that contains all data written into caller + * `BitStreamWriter`. + */ +public final function array GetData() +{ + local array result; + result = stream; + if (bitsLeft < 8) { + result[result.length] = unfinishedByte << bitsLeft; + } + return result; +} + +/** + * Returns amount of data (in bits) recorded into caller `BitStreamWriter`. + * + * @return Amount (in bits) of recorded data. + */ +public final function int GetSize() +{ + local int sizeInBits; + sizeInBits = stream.length * 8; + if (bitsLeft < 8) { + sizeInBits += (8 - bitsLeft); + } + return sizeInBits; +} + +/** + * Returns amount of data (in bytes) recorded into caller `BitStreamWriter`. + * + * @param onlyFullBytes Only count filled bytes, i.e. if last byte only has + * 5 bits (or any amount `<8`) of info written in it - + * method will not count it. + * @return Amount (in bytes) of recorded data. + */ +public final function int GetSizeInBytes(optional bool onlyFullBytes) +{ + local int sizeInBytes; + sizeInBytes = stream.length; + if (!onlyFullBytes && bitsLeft < 8) { + sizeInBytes += 1; + } + return sizeInBytes; +} + +defaultproperties +{ + // With this we do not need to call `Initialize` to use a newly created + // `BitStreamWriter` + bitsLeft = 8 + bitMaskAfter(0) = 255 // 1 1 1 1 1 1 1 1 + bitMaskAfter(1) = 127 // 0 1 1 1 1 1 1 1 + bitMaskAfter(2) = 63 // 0 0 1 1 1 1 1 1 + bitMaskAfter(3) = 31 // 0 0 0 1 1 1 1 1 + bitMaskAfter(4) = 15 // 0 0 0 0 1 1 1 1 + bitMaskAfter(5) = 7 // 0 0 0 0 0 1 1 1 + bitMaskAfter(6) = 3 // 0 0 0 0 0 0 1 1 + bitMaskAfter(7) = 1 // 0 0 0 0 0 0 0 1 +} \ No newline at end of file