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