/** * 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 }