Anton Tarasenko
4 years ago
3 changed files with 1046 additions and 0 deletions
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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 |
||||
} |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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<LineRecord> 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<Text.Character> 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<Text.Character> 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 + <redByte> + <greenByte> + <blueByte> |
||||
COLOR_SEQUENCE_LENGTH = 4 |
||||
} |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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<ConnectionService.Connection> 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 = " " |
||||
} |
Reference in new issue