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