diff --git a/sources/Core/Console/ConsoleAPI.uc b/sources/Core/Console/ConsoleAPI.uc
new file mode 100644
index 0000000..5365626
--- /dev/null
+++ b/sources/Core/Console/ConsoleAPI.uc
@@ -0,0 +1,280 @@
+/**
+ * API that provides functions for outputting text in
+ * Killing Floor's console. It takes care of coloring output and breaking up
+ * long lines (since allowing game to handle line breaking completely
+ * messes up console output).
+ *
+ * Actual output is taken care of by `ConsoleWriter` objects that this
+ * API generates.
+ * 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 ConsoleAPI extends Singleton
+ config(AcediaSystem);
+
+/**
+ * Main issue with console output in Killing Floor is
+ * automatic line breaking of long enough messages:
+ * it breaks formatting and can lead to an ugly text overlapping.
+ * To fix this we will try to break up user's output into lines ourselves,
+ * before game does it for us.
+ *
+ * We are not 100% sure how Killing Floor decides when to break the line,
+ * but it seems to calculate how much text can actually fit in a certain
+ * area on screen.
+ * There are two issues:
+ * 1. We do not know for sure what this limit value is.
+ * Even if we knew how to compute it, we cannot do that in server mode,
+ * since it depends on a screen resolution and font, which
+ * can vary for different players.
+ * 2. Even invisible characters, such as color change sequences,
+ * that do not take any space on the screen, contribute towards
+ * that limit. So for a heavily colored text we will have to
+ * break line much sooner than for the plain text.
+ * Both issues are solved by introducing two limits that users themselves
+ * are allowed to change: visible character limit and total character limit.
+ * ~ Total character limit will be a hard limit on a character amount in
+ * a line (including hidden ones used for color change sequences) that
+ * will be used to prevent Killing Floor's native line breaks.
+ * ~ Visible character limit will be a lower limit on amount of actually
+ * visible character. It introduction basically reserves some space that can be
+ * used only for color change sequences. Without this limit lines with
+ * colored lines will appear to be shorter that mono-colored ones.
+ * Visible limit will help to alleviate this problem.
+ *
+ * For example, if we set total limit to `120` and visible limit to `80`:
+ * 1. Line not formatted with color will all break at
+ * around length of `80`.
+ * 2. Since color change sequence consists of 4 characters:
+ * we can fit up to `(120 - 80) / 4 = 10` color swaps into each line,
+ * while still breaking them at a around the same length of `80`.
+ * ~ To differentiate our line breaks from line breaks intended by
+ * the user, we will also add 2 symbols worth of padding in front of all our
+ * output:
+ * 1. Before intended new line they will be just two spaces.
+ * 2. After our line break we will replace first space with "|" to indicate
+ * that we had to break a long line.
+ *
+ * Described measures are not perfect:
+ * 1. Since Killing Floor's console doe not use monospaced font,
+ * the same amount of characters on the line does not mean lines of
+ * visually the same length;
+ * 2. Heavily enough colored lines are still going to be shorter;
+ * 3. Depending on a resolution, default limits may appear to either use
+ * too little space (for high resolutions) or, on the contrary,
+ * not prevent native line breaks (low resolutions).
+ * In these cases user might be required to manually set limits;
+ * 4. There are probably more.
+ * But if seems to provide good enough results for the average use case.
+ */
+
+/**
+ * Configures how text will be rendered in target console(s).
+ */
+struct ConsoleDisplaySettings
+{
+ // What color to use for text by default
+ var Color defaultColor;
+ // How many visible characters in be displayed in a line?
+ var int maxVisibleLineWidth;
+ // How many total characters can be output at once?
+ var int maxTotalLineWidth;
+};
+// We will store data for `ConsoleDisplaySettings` separately for the ease of
+// configuration.
+var private config Color defaultColor;
+var private config int maxVisibleLineWidth;
+var private config int maxTotalLineWidth;
+
+/**
+ * Return current global visible limit that describes how many (at most)
+ * visible characters can be output in the console line.
+ *
+ * Instances of `ConsoleWriter` are initialized with this value,
+ * but can later change this value independently.
+ * Changes to global values do not affect already created `ConsoleWriters`.
+ *
+ * @return Current global visible limit.
+ */
+public final function int GetVisibleLineLength()
+{
+ return maxVisibleLineWidth;
+}
+
+/**
+ * Sets current global visible limit that describes how many (at most) visible
+ * characters can be output in the console line.
+ *
+ * Instances of `ConsoleWriter` are initialized with this value,
+ * but can later change this value independently.
+ * Changes to global values do not affect already created `ConsoleWriters`.
+ *
+ * @param newMaxVisibleLineWidth New global visible character limit.
+ */
+public final function SetVisibleLineLength(int newMaxVisibleLineWidth)
+{
+ maxVisibleLineWidth = newMaxVisibleLineWidth;
+}
+
+/**
+ * Return current global total limit that describes how many (at most)
+ * characters can be output in the console line.
+ *
+ * Instances of `ConsoleWriter` are initialized with this value,
+ * but can later change this value independently.
+ * Changes to global values do not affect already created `ConsoleWriters`.
+ *
+ * @return Current global total limit.
+ */
+public final function int GetTotalLineLength()
+{
+ return maxTotalLineWidth;
+}
+
+/**
+ * Sets current global total limit that describes how many (at most)
+ * characters can be output in the console line, counting both visible symbols
+ * and color change sequences.
+ *
+ * Instances of `ConsoleWriter` are initialized with this value,
+ * but can later change this value independently.
+ * Changes to global values do not affect already created `ConsoleWriters`.
+ *
+ * @param newMaxTotalLineWidth New global total character limit.
+ */
+public final function SetTotalLineLength(int newMaxTotalLineWidth)
+{
+ maxTotalLineWidth = newMaxTotalLineWidth;
+}
+
+/**
+ * Return current global total limit that describes how many (at most)
+ * characters can be output in the console line.
+ *
+ * Instances of `ConsoleWriter` are initialized with this value,
+ * but can later change this value independently.
+ * Changes to global values do not affect already created `ConsoleWriters`.
+ *
+ * @return Current default output color.
+ */
+public final function Color GetDefaultColor(int newMaxTotalLineWidth)
+{
+ return defaultColor;
+}
+
+/**
+ * Sets current global default color for console output.,
+ *
+ * Instances of `ConsoleWriter` are initialized with this value,
+ * but can later change this value independently.
+ * Changes to global values do not affect already created `ConsoleWriters`.
+ *
+ * @param newMaxTotalLineWidth New global default output color.
+ */
+public final function SetDefaultColor(Color newDefaultColor)
+{
+ defaultColor = newDefaultColor;
+}
+
+/**
+ * Returns borrowed `ConsoleWriter` instance that will write into
+ * consoles of all players.
+ *
+ * @return ConsoleWriter Borrowed `ConsoleWriter` instance, configured to
+ * write into consoles of all players.
+ * Never `none`.
+ */
+public final function ConsoleWriter ForAll()
+{
+ local ConsoleDisplaySettings globalSettings;
+ globalSettings.defaultColor = defaultColor;
+ globalSettings.maxTotalLineWidth = maxTotalLineWidth;
+ globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
+ return ConsoleWriter(_.memory.Claim(class'ConsoleWriter'))
+ .Initialize(globalSettings).ForAll();
+}
+
+/**
+ * Returns borrowed `ConsoleWriter` instance that will write into
+ * console of the player with a given controller.
+ *
+ * @param targetController Player, to whom console we want to write.
+ * If `none` - returned `ConsoleWriter` would be configured to
+ * throw messages away.
+ * @return Borrowed `ConsoleWriter` instance, configured to
+ * write into consoles of all players.
+ * Never `none`.
+ */
+public final function ConsoleWriter For(PlayerController targetController)
+{
+ local ConsoleDisplaySettings globalSettings;
+ globalSettings.defaultColor = defaultColor;
+ globalSettings.maxTotalLineWidth = maxTotalLineWidth;
+ globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
+ return ConsoleWriter(_.memory.Claim(class'ConsoleWriter'))
+ .Initialize(globalSettings).ForController(targetController);
+}
+
+/**
+ * Returns new `ConsoleWriter` instance that will write into
+ * consoles of all players.
+ * Should be freed after use.
+ *
+ * @return ConsoleWriter New `ConsoleWriter` instance, configured to
+ * write into consoles of all players.
+ * Never `none`.
+ */
+public final function ConsoleWriter MakeForAll()
+{
+ local ConsoleDisplaySettings globalSettings;
+ globalSettings.defaultColor = defaultColor;
+ globalSettings.maxTotalLineWidth = maxTotalLineWidth;
+ globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
+ return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter'))
+ .Initialize(globalSettings).ForAll();
+}
+
+/**
+ * Returns new `ConsoleWriter` instance that will write into
+ * console of the player with a given controller.
+ * Should be freed after use.
+ *
+ * @param targetController Player, to whom console we want to write.
+ * If `none` - returned `ConsoleWriter` would be configured to
+ * throw messages away.
+ * @return New `ConsoleWriter` instance, configured to
+ * write into consoles of all players.
+ * Never `none`.
+ */
+public final function ConsoleWriter MakeFor(PlayerController targetController)
+{
+ local ConsoleDisplaySettings globalSettings;
+ globalSettings.defaultColor = defaultColor;
+ globalSettings.maxTotalLineWidth = maxTotalLineWidth;
+ globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
+ return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter'))
+ .Initialize(globalSettings).ForController(targetController);
+}
+
+defaultproperties
+{
+ defaultColor = (R=255,G=255,B=255,A=255)
+ // These should guarantee decent text output even at
+ // 640x480 shit resolution
+ maxVisibleLineWidth = 80
+ maxTotalLineWidth = 108
+}
\ No newline at end of file
diff --git a/sources/Core/Console/ConsoleBuffer.uc b/sources/Core/Console/ConsoleBuffer.uc
new file mode 100644
index 0000000..a7f477a
--- /dev/null
+++ b/sources/Core/Console/ConsoleBuffer.uc
@@ -0,0 +1,393 @@
+/**
+ * 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).
+ */
+
+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, in `STRING_Colored` format
+ 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.
+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 `InsertString()`. 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 `string` to be added to the current line in caller
+ * `ConsoleBuffer`.
+ * @param inputType How to treat given `string` regarding coloring.
+ * @return Returns caller `ConsoleBuffer` to allow method chaining.
+ */
+public final function ConsoleBuffer InsertString(
+ string input,
+ Text.StringType inputType)
+{
+ local int inputConsumed;
+ local array rawInput;
+ rawInput = _().text.StringToRaw(input, inputType);
+ while (rawInput.length > 0)
+ {
+ // Fill word buffer, remove consumed input from `rawInput`
+ inputConsumed = 0;
+ while (inputConsumed < rawInput.length)
+ {
+ if (_().text.IsWhitespace(rawInput[inputConsumed])) break;
+ InsertIntoWordBuffer(rawInput[inputConsumed]);
+ inputConsumed += 1;
+ }
+ rawInput.Remove(0, inputConsumed);
+ // If we didn't encounter any whitespace symbols - bail
+ if (rawInput.length <= 0) {
+ return self;
+ }
+ FlushWordBuffer();
+ // Dump whitespaces into lines
+ inputConsumed = 0;
+ while (inputConsumed < rawInput.length)
+ {
+ if (!_().text.IsWhitespace(rawInput[inputConsumed])) break;
+ AppendWhitespaceToCurrentLine(rawInput[inputConsumed]);
+ inputConsumed += 1;
+ }
+ rawInput.Remove(0, inputConsumed);
+ }
+ 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 Color oldColor, newColor;
+ newCharacterIndex = wordBuffer.length;
+ // Fix text color in the buffer to remember default color, if we use it.
+ newCharacter.color =
+ _().text.GetCharacterColor(newCharacter, displaySettings.defaultColor);
+ newCharacter.colorType = STRCOLOR_Struct;
+ wordBuffer[newCharacterIndex] = newCharacter;
+ if (newCharacterIndex <= 0) {
+ return;
+ }
+ oldColor = wordBuffer[newCharacterIndex].color;
+ newColor = wordBuffer[newCharacterIndex - 1].color;
+ 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 = wordBuffer[i].color;
+ 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(nextCharacter.color)) {
+ 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(true);
+ 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(wordBuffer[0].color)) {
+ totalCharactersInWord += COLOR_SEQUENCE_LENGTH;
+ }
+ return (totalCharactersInWord <= totalLimit);
+}
+
+defaultproperties
+{
+ CODEPOINT_ESCAPE = 27
+ CODEPOINT_NEWLINE = 10
+ // CODEPOINT_ESCAPE + + +
+ COLOR_SEQUENCE_LENGTH = 4
+}
\ No newline at end of file
diff --git a/sources/Core/Console/ConsoleWriter.uc b/sources/Core/Console/ConsoleWriter.uc
new file mode 100644
index 0000000..7408530
--- /dev/null
+++ b/sources/Core/Console/ConsoleWriter.uc
@@ -0,0 +1,373 @@
+/**
+ * Object that provides simple access to console output.
+ * Can either write to a certain player's console or to all consoles at once.
+ * Supports "fancy" and "raw" output (for more details @see `ConsoleAPI`).
+ * 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 ConsoleWriter extends AcediaObject
+ dependson(ConsoleAPI)
+ dependson(ConnectionService);
+
+// Prefixes we output before every line to signify whether they were broken
+// or not
+var private string NEWLINE_PREFIX;
+var private string BROKENLINE_PREFIX;
+
+/**
+ * Describes current output target of the `ConsoleWriter`.
+ */
+enum ConsoleWriterTarget
+{
+ // No one. Can happed if our target disconnects.
+ CWTARGET_None,
+ // A certain player.
+ CWTARGET_Player,
+ // All players.
+ CWTARGET_All
+};
+var private ConsoleWriterTarget targetType;
+// Controller of the player that will receive output passed
+// to this `ConsoleWriter`.
+// Only used when `targetType == CWTARGET_Player`
+var private PlayerController outputTarget;
+var private ConsoleBuffer outputBuffer;
+
+var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
+
+public final function ConsoleWriter Initialize(
+ ConsoleAPI.ConsoleDisplaySettings newDisplaySettings)
+{
+ displaySettings = newDisplaySettings;
+ if (outputBuffer == none) {
+ outputBuffer = ConsoleBuffer(_().memory.Allocate(class'ConsoleBuffer'));
+ }
+ else {
+ outputBuffer.Clear();
+ }
+ outputBuffer.SetSettings(displaySettings);
+ return self;
+}
+
+/**
+ * Return current default color for caller `ConsoleWriter`.
+ *
+ * This method returns default color, i.e. color that will be used if no other
+ * is specified by text you're outputting.
+ * If color is specified, this value will be ignored.
+ *
+ * This value is not synchronized with the global value from `ConsoleAPI`
+ * (or such value from any other `ConsoleWriter`) and affects only
+ * output produced by this `ConsoleWriter`.
+ *
+ * @return Current default color.
+ */
+public final function Color GetColor()
+{
+ return displaySettings.defaultColor;
+}
+
+/**
+ * Sets default color for caller 'ConsoleWriter`'s output.
+ *
+ * This only changes default color, i.e. color that will be used if no other is
+ * specified by text you're outputting.
+ * If color is specified, this value will be ignored.
+ *
+ * This value is not synchronized with the global value from `ConsoleAPI`
+ * (or such value from any other `ConsoleWriter`) and affects only
+ * output produced by this `ConsoleWriter`.
+ *
+ * @param newDefaultColor New color to use when none specified by text itself.
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter SetColor(Color newDefaultColor)
+{
+ displaySettings.defaultColor = newDefaultColor;
+ if (outputBuffer != none) {
+ outputBuffer.SetSettings(displaySettings);
+ }
+ return self;
+}
+
+/**
+ * Return current visible limit that describes how many (at most)
+ * visible characters can be output in the console line.
+ *
+ * This value is not synchronized with the global value from `ConsoleAPI`
+ * (or such value from any other `ConsoleWriter`) and affects only
+ * output produced by this `ConsoleWriter`.
+ *
+ * @return Current global visible limit.
+ */
+public final function int GetVisibleLineLength()
+{
+ return displaySettings.maxVisibleLineWidth;
+}
+
+/**
+ * Sets current visible limit that describes how many (at most) visible
+ * characters can be output in the console line.
+ *
+ * This value is not synchronized with the global value from `ConsoleAPI`
+ * (or such value from any other `ConsoleWriter`) and affects only
+ * output produced by this `ConsoleWriter`.
+ *
+ * @param newVisibleLimit New global visible limit.
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter SetVisibleLineLength(
+ int newMaxVisibleLineWidth
+)
+{
+ displaySettings.maxVisibleLineWidth = newMaxVisibleLineWidth;
+ if (outputBuffer != none) {
+ outputBuffer.SetSettings(displaySettings);
+ }
+ return self;
+}
+
+/**
+ * Return current total limit that describes how many (at most)
+ * characters can be output in the console line.
+ *
+ * This value is not synchronized with the global value from `ConsoleAPI`
+ * (or such value from any other `ConsoleWriter`) and affects only
+ * output produced by this `ConsoleWriter`.
+ *
+ * @return Current global total limit.
+ */
+public final function int GetTotalLineLength()
+{
+ return displaySettings.maxTotalLineWidth;
+}
+
+/**
+ * Sets current total limit that describes how many (at most)
+ * characters can be output in the console line.
+ *
+ * This value is not synchronized with the global value from `ConsoleAPI`
+ * (or such value from any other `ConsoleWriter`) and affects only
+ * output produced by this `ConsoleWriter`.
+ *
+ * @param newTotalLimit New global total limit.
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter SetTotalLineLength(int newMaxTotalLineWidth)
+{
+ displaySettings.maxTotalLineWidth = newMaxTotalLineWidth;
+ if (outputBuffer != none) {
+ outputBuffer.SetSettings(displaySettings);
+ }
+ return self;
+}
+
+/**
+ * Configures caller `ConsoleWriter` to output to all players.
+ * `Flush()` will be automatically called between target change.
+ *
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter ForAll()
+{
+ Flush();
+ targetType = CWTARGET_All;
+ return self;
+}
+
+/**
+ * Configures caller `ConsoleWriter` to output only to a player,
+ * given by a passed `PlayerController`.
+ * `Flush()` will be automatically called between target change.
+ *
+ * @param targetController Player, to whom console we want to write.
+ * If `none` - caller `ConsoleWriter` would be configured to
+ * throw messages away.
+ * @return ConsoleWriter Returns caller `ConsoleWriter` to allow for
+ * method chaining.
+ */
+public final function ConsoleWriter ForController(
+ PlayerController targetController
+)
+{
+ Flush();
+ if (targetController != none)
+ {
+ targetType = CWTARGET_Player;
+ outputTarget = targetController;
+ }
+ else {
+ targetType = CWTARGET_None;
+ }
+ return self;
+}
+
+/**
+ * Returns type of current target for the caller `ConsoleWriter`.
+ *
+ * @return `ConsoleWriterTarget` value, describing current target of
+ * the caller `ConsoleWriter`.
+ */
+public final function ConsoleWriterTarget CurrentTarget()
+{
+ if (targetType == CWTARGET_Player && outputTarget == none) {
+ targetType = CWTARGET_None;
+ }
+ return targetType;
+}
+
+/**
+ * Returns `PlayerController` of the player to whom console caller
+ * `ConsoleWriter` is outputting messages.
+ *
+ * @return `PlayerController` of the player to whom console caller
+ * `ConsoleWriter` is outputting messages.
+ * Returns `none` iff it currently outputs to every player or to no one.
+ */
+public final function PlayerController GetTargetPlayerController()
+{
+ if (targetType == CWTARGET_All) return none;
+ return outputTarget;
+}
+
+/**
+ * Outputs all buffered input and moves further output onto a new line.
+ *
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter Flush()
+{
+ outputBuffer.Flush();
+ SendBuffer();
+ return self;
+}
+
+/**
+ * Writes a formatted string into console.
+ *
+ * Does not trigger console output, for that use `WriteLine()` or `Flush()`.
+ *
+ * To output a different type of string into a console, use `WriteT()`.
+ *
+ * @param message Formatted string to output.
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter Write(string message)
+{
+ outputBuffer.InsertString(message, STRING_Formatted);
+ return self;
+}
+
+/**
+ * Writes a formatted string into console.
+ * Result will be output immediately, starts a new line.
+ *
+ * To output a different type of string into a console, use `WriteLineT()`.
+ *
+ * @param message Formatted string to output.
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter WriteLine(string message)
+{
+ outputBuffer.InsertString(message, STRING_Formatted);
+ Flush();
+ return self;
+}
+
+/**
+ * Writes a `string` of specified type into console.
+ *
+ * Does not trigger console output, for that use `WriteLineT()` or `Flush()`.
+ *
+ * To output a formatted string you might want to simply use `Write()`.
+ *
+ * @param message String of a given type to output.
+ * @param inputType Type of the string method should output.
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter WriteT(
+ string message,
+ Text.StringType inputType)
+{
+ outputBuffer.InsertString(message, inputType);
+ return self;
+}
+
+/**
+ * Writes a `string` of specified type into console.
+ * Result will be output immediately, starts a new line.
+ *
+ * To output a formatted string you might want to simply use `WriteLine()`.
+ *
+ * @param message String of a given type to output.
+ * @param inputType Type of the string method should output.
+ * @return Returns caller `ConsoleWriter` to allow for method chaining.
+ */
+public final function ConsoleWriter WriteLineT(
+ string message,
+ Text.StringType inputType)
+{
+ outputBuffer.InsertString(message, inputType);
+ Flush();
+ return self;
+}
+
+// Send all completed lines from an `outputBuffer`
+private final function SendBuffer()
+{
+ local string prefix;
+ local ConnectionService service;
+ local ConsoleBuffer.LineRecord nextLineRecord;
+ while (outputBuffer.HasCompletedLines())
+ {
+ nextLineRecord = outputBuffer.PopNextLine();
+ if (nextLineRecord.wrappedLine) {
+ prefix = NEWLINE_PREFIX;
+ }
+ else {
+ prefix = BROKENLINE_PREFIX;
+ }
+ service = ConnectionService(class'ConnectionService'.static.Require());
+ SendConsoleMessage(service, prefix $ nextLineRecord.contents);
+ }
+}
+
+// Assumes `service != none`, caller function must ensure that.
+private final function SendConsoleMessage(
+ ConnectionService service,
+ string message)
+{
+ local int i;
+ local array connections;
+ if (targetType != CWTARGET_All)
+ {
+ if (outputTarget != none) {
+ outputTarget.ClientMessage(message);
+ }
+ return;
+ }
+ connections = service.GetActiveConnections();
+ for (i = 0; i < connections.length; i += 1) {
+ connections[i].controllerReference.ClientMessage(message);
+ }
+}
+
+defaultproperties
+{
+ NEWLINE_PREFIX = "| "
+ BROKENLINE_PREFIX = " "
+}
\ No newline at end of file