/**
* Object that provides a buffer functionality for Killing Floor's (in-game)
* console output: it accepts content that user want to output and breaks it
* into lines that will be well-rendered according to the given
* `ConsoleDisplaySettings`.
* 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 ConsoleBuffer extends AcediaObject
dependson(Text)
dependson(ConsoleAPI);
/**
* `ConsoleBuffer` works by breaking it's input into words, counting how much
* space they take up and only then deciding to which line to append them
* (new or the next, new one).
*
* It is implemented making heavier use of `string`s instead of `Text`.
* This is because:
* 1. `string`s that are passed to console are broken into lines,
* that need to be specifically prepared anyway;
* 2. It was coded before I the switch to mostly using `Text`,
* when a lot of methods were also accepting `string`s
* and `array` types as parameters.
* And i really do not want to have to reimplement it.
*/
var private int CODEPOINT_ESCAPE;
var private int CODEPOINT_NEWLINE;
var private int COLOR_SEQUENCE_LENGTH;
// Display settings according to which to format our output
var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
/**
* This structure is used to both share results of our work and for tracking
* information about the line we are currently filling.
*/
struct LineRecord
{
// Contents of the line, as a colored `string`.
// Not a `Text`, because it has to be prepared exactly how we want it.
var string contents;
// Is this a wrapped line?
// `true` means that this line was supposed to be part part of another,
// singular line of text, that had to be broken into smaller pieces.
// Such lines will start with "|" in front of them in Acedia's
// `ConsoleWriter`.
var bool wrappedLine;
// Information variables that describe how many visible and total symbols
// (visible + color change sequences) are stored int the `line`
var int visibleSymbolsStored;
var int totalSymbolsStored;
// Does `contents` contain a color change sequence?
// Non-empty line can have no such sequence if they consist of whitespaces.
var private bool colorInserted;
// If `colorInserted == true`, stores the last inserted color.
var private Color endColor;
};
// Lines that are ready to be output to the console
var private array completedLines;
// Line we are currently building
var private LineRecord currentLine;
// Word we are currently building, colors of it's characters will be
// automatically converted into `STRCOLOR_Struct`, according to the default
// color setting at the time of their addition.
// We are using array of `Character`s instead of `MutableText` since
// we want to have a more directly control over how it is converted into
// a colored string anyway and otherwise only need an ability to
// append `Character`s to it.
var private array wordBuffer;
// Amount of color swaps inside `wordBuffer`
var private int colorSwapsInWordBuffer;
/**
* Returns current setting used by this buffer to break up it's input into
* lines fit to be output in console.
*
* @return Currently used `ConsoleDisplaySettings`.
*/
public final function ConsoleAPI.ConsoleDisplaySettings GetSettings()
{
return displaySettings;
}
/**
* Sets new setting to be used by this buffer to break up it's input into
* lines fit to be output in console.
*
* It is recommended (although not required) to call `Flush()` before
* changing settings. Not doing so would not lead to any errors or warnings,
* but can lead to some wonky results and is considered an undefined behavior.
*
* @param newSettings New `ConsoleDisplaySettings` to be used.
* @return Returns caller `ConsoleBuffer` to allow for method chaining.
*/
public final function ConsoleBuffer SetSettings(
ConsoleAPI.ConsoleDisplaySettings newSettings)
{
displaySettings = newSettings;
return self;
}
/**
* Does caller `ConsoleBuffer` has any completed lines that can be output?
*
* "Completed line" means that nothing else will be added to it.
* So negative (`false`) response does not mean that the buffer is empty, -
* it can still contain an uncompleted and non-empty line that can still be
* expanded with `Insert()`. If you want to completely empty the buffer -
* call the `Flush()` method.
* Also see `IsEmpty()`.
*
* @return `true` if caller `ConsoleBuffer` has no completed lines and
* `false` otherwise.
*/
public final function bool HasCompletedLines()
{
return (completedLines.length > 0);
}
/**
* Does caller `ConsoleBuffer` has any unprocessed input?
*
* Note that `ConsoleBuffer` can be non-empty, but no completed line if it
* currently builds one.
* See `Flush()` and `HasCompletedLines()` methods.
*
* @return `true` if `ConsoleBuffer` is completely empty
* (either did not receive or already returned all processed input) and
* `false` otherwise.
*/
public final function bool IsEmpty()
{
if (HasCompletedLines()) return false;
if (currentLine.totalSymbolsStored > 0) return false;
if (wordBuffer.length > 0) return false;
return true;
}
/**
* Clears the buffer of all data, but leaving current settings intact.
* After this calling method `IsEmpty()` should return `true`.
*
* @return Returns caller `ConsoleBuffer` to allow method chaining.
*/
public final function ConsoleBuffer Clear()
{
local LineRecord newLineRecord;
currentLine = newLineRecord;
completedLines.length = 0;
return self;
}
/**
* Inserts a string into the buffer. This method does not automatically break
* the line after the `input`, call `Flush()` or add line feed symbol "\n"
* at the end of the `input` if you want that.
*
* @param input `Text` to be added to the current line in caller
* `ConsoleBuffer`. Does nothing if passed `none`.
* @param inputType How to treat given `string` regarding coloring.
* @return Returns caller `ConsoleBuffer` to allow method chaining.
*/
public final function ConsoleBuffer Insert(Text input)
{
local int inputConsumed;
local Text.Character nextCharacter;
if (input == none) {
return self;
}
// Regular symbols and whitespaces are treated differently when
// breaking input into lines, so alternate between adding them,
// switching the logic appropriately
while (inputConsumed < input.GetLength())
{
while (inputConsumed < input.GetLength())
{
nextCharacter = input.GetCharacter(inputConsumed);
if (_.text.IsWhitespace(nextCharacter)) {
break;
}
InsertIntoWordBuffer(input.GetCharacter(inputConsumed));
inputConsumed += 1;
}
// If we didn't encounter any whitespace symbols - bail
if (inputConsumed >= input.GetLength()) {
return self;
}
FlushWordBuffer();
// Dump whitespaces into lines
while (inputConsumed < input.GetLength())
{
nextCharacter = input.GetCharacter(inputConsumed);
if (!_.text.IsWhitespace(nextCharacter)) {
break;
}
AppendWhitespaceToCurrentLine(nextCharacter);
inputConsumed += 1;
}
}
return self;
}
/**
* Returns (and makes caller `ConsoleBuffer` forget) next completed line that
* can be output to console in `STRING_Colored` format.
*
* If there are no completed line to return - returns an empty one.
*
* @return Next completed line that can be output, in `STRING_Colored` format.
*/
public final function LineRecord PopNextLine()
{
local LineRecord result;
if (completedLines.length <= 0) return result;
result = completedLines[0];
completedLines.Remove(0, 1);
return result;
}
/**
* Forces all buffered data into "completed line" array, making it retrievable
* by `PopNextLine()`.
*
* @return Next completed line that can be output, in `STRING_Colored` format.
*/
public final function ConsoleBuffer Flush()
{
FlushWordBuffer();
BreakLine(false);
return self;
}
// It is assumed that passed characters are not whitespace, -
// responsibility to check is on the one calling this method.
private final function InsertIntoWordBuffer(Text.Character newCharacter)
{
local int newCharacterIndex;
local Text.Formatting newFormatting;
local Color oldColor, newColor;
// Fix text color in the buffer to remember default color, if we use it.
newFormatting = _.text.GetCharacterFormatting(newCharacter);
newFormatting.color =
_.text.GetCharacterColor(newCharacter, displaySettings.defaultColor);
newFormatting.isColored = true;
newCharacter = _.text.SetFormatting(newCharacter, newFormatting);
// Add new character and check if color swapped
newCharacterIndex = wordBuffer.length;
wordBuffer[newCharacterIndex] = newCharacter;
if (newCharacterIndex <= 0) {
return;
}
newColor = newFormatting.color;
oldColor = _.text.GetCharacterColor(wordBuffer[newCharacterIndex - 1]);
if (!_.color.AreEqual(oldColor, newColor, true)) {
colorSwapsInWordBuffer += 1;
}
}
// Pushes whole `wordBuffer` into lines
private final function FlushWordBuffer()
{
local int i;
local Color newColor;
if (!WordCanFitInCurrentLine() && WordCanFitInNewLine()) {
BreakLine(true);
}
for (i = 0; i < wordBuffer.length; i += 1)
{
if (!CanAppendNonWhitespaceIntoLine(wordBuffer[i])) {
BreakLine(true);
}
newColor = _.text.GetCharacterColor(wordBuffer[i]);
if (MustSwapColorsFor(newColor))
{
currentLine.contents $= _.color.GetColorTag(newColor);
currentLine.totalSymbolsStored += COLOR_SEQUENCE_LENGTH;
currentLine.colorInserted = true;
currentLine.endColor = newColor;
}
currentLine.contents $= Chr(wordBuffer[i].codePoint);
currentLine.totalSymbolsStored += 1;
currentLine.visibleSymbolsStored += 1;
}
wordBuffer.length = 0;
colorSwapsInWordBuffer = 0;
}
private final function BreakLine(bool makeWrapped)
{
local LineRecord newLineRecord;
if (currentLine.visibleSymbolsStored > 0) {
completedLines[completedLines.length] = currentLine;
}
currentLine = newLineRecord;
currentLine.wrappedLine = makeWrapped;
}
private final function bool MustSwapColorsFor(Color newColor)
{
if (!currentLine.colorInserted) return true;
return !_.color.AreEqual(currentLine.endColor, newColor, true);
}
private final function bool CanAppendWhitespaceIntoLine()
{
// We always allow to append at least something into empty line,
// otherwise we can never insert it anywhere
if (currentLine.totalSymbolsStored <= 0) return true;
if (currentLine.totalSymbolsStored >= displaySettings.maxTotalLineWidth)
{
return false;
}
if (currentLine.visibleSymbolsStored >= displaySettings.maxVisibleLineWidth)
{
return false;
}
return true;
}
private final function bool CanAppendNonWhitespaceIntoLine(
Text.Character nextCharacter)
{
// We always allow to insert at least something into empty line,
// otherwise we can never insert it anywhere
if (currentLine.totalSymbolsStored <= 0) {
return true;
}
// Check if we can fit a single character by fitting a whitespace symbol.
if (!CanAppendWhitespaceIntoLine()) {
return false;
}
if (!MustSwapColorsFor(_.text.GetCharacterColor(nextCharacter))) {
return true;
}
// Can we fit character + color swap sequence?
return ( currentLine.totalSymbolsStored + COLOR_SEQUENCE_LENGTH + 1
<= displaySettings.maxTotalLineWidth);
}
// For performance reasons assumes that passed character is a whitespace,
// the burden of checking is on the caller.
private final function AppendWhitespaceToCurrentLine(Text.Character whitespace)
{
if (_.text.IsCodePoint(whitespace, CODEPOINT_NEWLINE)) {
BreakLine(false);
return;
}
if (!CanAppendWhitespaceIntoLine()) {
BreakLine(true);
}
currentLine.contents $= Chr(whitespace.codePoint);
currentLine.totalSymbolsStored += 1;
currentLine.visibleSymbolsStored += 1;
}
private final function bool WordCanFitInNewLine()
{
local int totalCharactersInWord;
if (wordBuffer.length <= 0) return true;
if (wordBuffer.length > displaySettings.maxVisibleLineWidth) {
return false;
}
// `(colorSwapsInWordBuffer + 1)` counts how many times we must
// switch color inside a word + 1 for setting initial color
totalCharactersInWord = wordBuffer.length
+ (colorSwapsInWordBuffer + 1) * COLOR_SEQUENCE_LENGTH;
return (totalCharactersInWord <= displaySettings.maxTotalLineWidth);
}
private final function bool WordCanFitInCurrentLine()
{
local int totalLimit, visibleLimit;
local int totalCharactersInWord;
if (wordBuffer.length <= 0) return true;
totalLimit =
displaySettings.maxTotalLineWidth - currentLine.totalSymbolsStored;
visibleLimit =
displaySettings.maxVisibleLineWidth - currentLine.visibleSymbolsStored;
// Visible symbols check
if (wordBuffer.length > visibleLimit) {
return false;
}
// Total symbols check
totalCharactersInWord = wordBuffer.length
+ colorSwapsInWordBuffer * COLOR_SEQUENCE_LENGTH;
if (MustSwapColorsFor(_.text.GetCharacterColor(wordBuffer[0]))) {
totalCharactersInWord += COLOR_SEQUENCE_LENGTH;
}
return (totalCharactersInWord <= totalLimit);
}
defaultproperties
{
CODEPOINT_ESCAPE = 27
CODEPOINT_NEWLINE = 10
// CODEPOINT_ESCAPE + + +
COLOR_SEQUENCE_LENGTH = 4
}