352 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Ucode
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Ucode
		
	
	
	
	
	
| /**
 | |
|  *  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 <https://www.gnu.org/licenses/>.
 | |
|  */
 | |
| class BitStreamWriter extends Object;
 | |
| 
 | |
| //  Array of fully written bytes
 | |
| var private array<byte> 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<byte> 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<byte> GetData()
 | |
| {
 | |
|     local array<byte> 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
 | |
| } |