Browse Source

Add support for multiple (custom) loggers

Now `LoggerAPI`, instead of simply loggin messages by itself, uses
logger objects (deriving from `Logger` class) to output log messages for
it (previous way of logging by `Log()` method is available by
`ConsoleLogger`).

Multiple loggers can be configured per each log level, which can be done
via config.
pull/8/head
Anton Tarasenko 4 years ago
parent
commit
6c203e9a89
  1. 24
      config/AcediaSystem.ini
  2. 23
      sources/Aliases/AliasSource.uc
  3. 38
      sources/Aliases/AliasesAPI.uc
  4. 5
      sources/Commands/BuiltInCommands/ACommandHelp.uc
  5. 1
      sources/Commands/Command.uc
  6. 35
      sources/Commands/CommandDataBuilder.uc
  7. 6
      sources/Commands/CommandParser.uc
  8. 11
      sources/Commands/Commands.uc
  9. 5
      sources/Global.uc
  10. 41
      sources/Logger/ConsoleLogger.uc
  11. 355
      sources/Logger/LogMessage.uc
  12. 151
      sources/Logger/Logger.uc
  13. 322
      sources/Logger/LoggerAPI.uc
  14. 166
      sources/Logger/LoggerService.uc
  15. 170
      sources/Logger/Tests/TEST_LogMessage.uc
  16. 1
      sources/Manifest.uc
  17. 15
      sources/Players/ConnectionListener_Player.uc
  18. 14
      sources/Testing/Service/TestingService.uc
  19. 39
      sources/Text/TextAPI.uc

24
config/AcediaSystem.ini

@ -17,6 +17,30 @@
; at `2`.
usedInjectionLevel=2
[AcediaCore_0_2.LoggerAPI]
; Loggers, specified in `allLoggers` will log all levels of messages
allLoggers=(name="default",cls=Class'AcediaCore_0_2.ConsoleLogger')
; Loggers, specified in one of the arrays below will only output logs of
; a particular level (although one can always add the same `Logger` to
; several log levels)
;debugLoggers=
;infoLoggers=
;warningLoggers=
;errorLoggers=
;fatalLoggers=
; Loggers themselves must be defined in per-object-config records like these
; that specify name and class of the logger.
; `ConsoleLogger` is a simple logger that logs messages in the standard
; console and logfile output using `Log()` function.
[default ConsoleLogger]
; Should logger display prefix indicating it's a log message from Acedia?
acediaStamp=true
; Should logger display time stamp prefix in front of log messages?
timeStamp=true
; Should logger display information about what level message was logged?
levelStamp=true
[AcediaCore_0_2.Commands]
; This feature provides a mechanism to define commands that automatically
; parse their arguments into standard Acedia collection. It also allows to

23
sources/Aliases/AliasSource.uc

@ -51,6 +51,8 @@ var private config array<AliasValuePair> record;
// Otherwise only stores first loaded alias.
var private AssociativeArray aliasHash;
var LoggerAPI.Definition errIncorrectAliasPair, warnDuplicateAlias;
// Load and hash all the data `AliasSource` creation.
protected function OnCreated()
{
@ -71,8 +73,7 @@ private final function bool AssertAliasesClassIsOwnedByThisSource()
{
if (aliasesClass == none) return true;
if (aliasesClass.default.sourceClass == class) return true;
_.logger.Failure("`AliasSource`-`Aliases` class pair is incorrectly"
@ "setup for source `" $ string(class) $ "`. Omitting it.");
_.logger.Auto(errIncorrectAliasPair).ArgClass(class);
Destroy();
return false;
}
@ -294,18 +295,10 @@ public final function RemoveAlias(Text aliasToRemove)
private final function LogDuplicateAliasWarning(Text alias, Text existingValue)
{
_.logger.Warning("Alias source `" $ string(class)
$ "` has duplicate record for alias \"" $ alias.ToPlainString()
$ "\". This is likely due to an erroneous config. \""
$ existingValue.ToPlainString()
$ "\" value will be used.");
}
private final function LogInvalidAliasWarning(Text invalidAlias)
{
_.logger.Warning("Alias source `" $ string(class)
$ "` contains invalid alias name \"" $ invalidAlias.ToPlainString()
$ "\". This alias will not be loaded.");
_.logger.Auto(warnDuplicateAlias)
.ArgClass(class)
.Arg(alias.Copy())
.Arg(existingValue.Copy());
}
// Tries to find a loaded `Aliases` config object that stores aliases for
@ -333,4 +326,6 @@ defaultproperties
{
// Source main parameters
aliasesClass = class'Aliases'
errIncorrectAliasPair = (l=LOG_Error,m="`AliasSource`-`Aliases` class pair is incorrectly setup for source `%1`. Omitting it.")
warnDuplicateAlias = (l=LOG_Warning,m="Alias source `%1` has duplicate record for alias \"%2\". This is likely due to an erroneous config. \"%3\" value will be used.")
}

38
sources/Aliases/AliasesAPI.uc

@ -17,7 +17,11 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AliasesAPI extends AcediaObject;
class AliasesAPI extends AcediaObject
dependson(LoggerAPI);
var LoggerAPI.Definition noWeaponAliasSource, invalidWeaponAliasSource;
var LoggerAPI.Definition noColorAliasSource, invalidColorAliasSource;
/**
* Provides an easier access to the instance of the `AliasSource` of
@ -52,17 +56,15 @@ public final function AliasSource GetWeaponSource()
local AliasSource weaponSource;
local class<AliasSource> sourceClass;
sourceClass = class'AliasService'.default.weaponAliasesSource;
if (sourceClass == none) {
_.logger.Failure("No weapon aliases source configured for Acedia's"
@ "alias API. Error is most likely cause by erroneous config.");
if (sourceClass == none)
{
_.logger.Auto(noWeaponAliasSource);
return none;
}
weaponSource = AliasSource(sourceClass.static.GetInstance(true));
if (weaponSource == none) {
_.logger.Failure("`AliasSource` class `" $ string(sourceClass) $ "` is"
@ "configured to store weapon aliases, but it seems to be invalid."
@ "This is a bug and not configuration file problem, but issue"
@ "might be avoided by using a different `AliasSource`.");
if (weaponSource == none)
{
_.logger.Auto(invalidWeaponAliasSource).ArgClass(sourceClass);
return none;
}
return weaponSource;
@ -85,17 +87,15 @@ public final function AliasSource GetColorSource()
local AliasSource colorSource;
local class<AliasSource> sourceClass;
sourceClass = class'AliasService'.default.colorAliasesSource;
if (sourceClass == none) {
_.logger.Failure("No color aliases source configured for Acedia's"
@ "alias API. Error is most likely cause by erroneous config.");
if (sourceClass == none)
{
_.logger.Auto(noColorAliasSource);
return none;
}
colorSource = AliasSource(sourceClass.static.GetInstance(true));
if (colorSource == none) {
_.logger.Failure("`AliasSource` class `" $ string(sourceClass) $ "` is"
@ "configured to store color aliases, but it seems to be invalid."
@ "This is a bug and not configuration file problem, but issue"
@ "might be avoided by using a different `AliasSource`.");
if (colorSource == none)
{
_.logger.Auto(invalidColorAliasSource).ArgClass(sourceClass);
return none;
}
return colorSource;
@ -167,4 +167,8 @@ public final function Text ResolveColor(Text alias, optional bool copyOnFailure)
defaultproperties
{
noWeaponAliasSource = (l=LOG_Error,m="No weapon aliases source configured for Acedia's alias API. Error is most likely cause by erroneous config.")
invalidWeaponAliasSource = (l=LOG_Error,m="`AliasSource` class `%1` is configured to store weapon aliases, but it seems to be invalid. This is a bug and not configuration file problem, but issue might be avoided by using a different `AliasSource`.")
noColorAliasSource = (l=LOG_Error,m="No color aliases source configured for Acedia's alias API. Error is most likely cause by erroneous config.")
invalidColorAliasSource = (l=LOG_Error,m="`AliasSource` class `%1` is configured to store color aliases, but it seems to be invalid. This is a bug and not configuration file problem, but issue might be avoided by using a different `AliasSource`.")
}

5
sources/Commands/BuiltInCommands/ACommandHelp.uc

@ -17,7 +17,10 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandHelp extends Command;
class ACommandHelp extends Command
dependson(LoggerAPI);
var LoggerAPI.Definition testMsg;
protected function BuildData(CommandDataBuilder builder)
{

1
sources/Commands/Command.uc

@ -208,6 +208,7 @@ public final static function Command GetInstance()
* Returns name (in lower case) of the caller command class.
*
* @return Name (in lower case) of the caller command class.
* Guaranteed to be not `none`.
*/
public final static function Text GetName()
{

35
sources/Commands/CommandDataBuilder.uc

@ -66,6 +66,9 @@ var private bool selectionIsOptional;
// Array of parameters we are currently filling (either required or optional)
var private array<Command.Parameter> selectedParameterArray;
var LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong;
var LoggerAPI.Definition warnSameLongName, warnSameShortName;
protected function Constructor()
{
// Fill empty subcommand (no special key word) by default
@ -313,23 +316,17 @@ private final function Text.Character GetValidShortName(
}
if (longName.GetLength() < 2)
{
_.logger.Failure("Command" @ self.class @ "is trying to register"
@ "an option with a name that is way too short (<2 characters)."
@ "Option will be discarded:" @ longName.ToPlainString());
_.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy());
return _.text.GetInvalidCharacter();
}
// Validate `shortName`,
// deriving if from `longName` if necessary & possible
if (shortName == none)
{
if (shortName == none) {
return longName.GetCharacter(0);
}
if (shortName.IsEmpty() || shortName.GetLength() > 1)
{
_.logger.Failure("Command" @ self.class @ "is trying to register"
@ "an option with a short name that doesn't consist of just"
@ "one character. Option will be discarded:"
@ longName.ToPlainString());
_.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy());
return _.text.GetInvalidCharacter();
}
return shortName.GetCharacter(0);
@ -354,22 +351,18 @@ private final function bool VerifyNoOptionNamingConflict(
if ( !_.text.AreEqual(shortName, options[i].shortName)
&& longName.Compare(options[i].longName))
{
_ .logger.Warning("Command" @ self.class @ "is trying to register"
@ "several options with the same long name"
@ "\"" $ longName.ToPlainString()
$ "\", but different short names. This should not happen,"
@ "do not expect correct behavior.");
_.logger.Auto(warnSameLongName)
.ArgClass(class)
.Arg(longName.Copy());
return true;
}
// Is same short name, but different short ones?
if ( _.text.AreEqual(shortName, options[i].shortName)
&& !longName.Compare(options[i].longName))
{
_.logger.Warning("Command" @ self.class @ "is trying to register"
@ "several options with the same short name"
@ "\"" $ _.text.CharacterToString(shortName)
$ "\", but different long names. This should not have happened,"
@ "do not expect correct behavior.");
_.logger.Auto(warnSameLongName)
.ArgClass(class)
.Arg(_.text.FromCharacter(shortName));
return true;
}
}
@ -880,4 +873,8 @@ public final function CommandDataBuilder ParamArrayList(
defaultproperties
{
errLongNameTooShort = (l=LOG_Error,m="Command `%1` is trying to register an option with a name that is way too short (<2 characters). Option will be discarded: %2")
errShortNameTooLong = (l=LOG_Error,m="Command `%1` is trying to register an option with a short name that doesn't consist of just one character. Option will be discarded: %2")
warnSameLongName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same long name \"%2\", but different short names. This should not happen, do not expect correct behavior.")
warnSameShortName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same short name \"%2\", but different long names. This should not have happened, do not expect correct behavior.")
}

6
sources/Commands/CommandParser.uc

@ -120,6 +120,8 @@ var private array<Command.Option> usedOptions;
var private array<string> booleanTrueEquivalents;
var private array<string> booleanFalseEquivalents;
var LoggerAPI.Definition errNoSubCommands;
protected function Finalizer()
{
Reset();
@ -160,8 +162,7 @@ private final function PickSubCommand(Command.Data commandData)
allSubCommands = commandData.subCommands;
if (allSubcommands.length == 0)
{
_.logger.Failure("`GetSubCommand()` method was called on a command"
@ class @ "with zero defined sub-commands.");
_.logger.Auto(errNoSubCommands).ArgClass(class);
pickedSubCommand = emptySubCommand;
return;
}
@ -797,4 +798,5 @@ defaultproperties
booleanFalseEquivalents(1) = "disable"
booleanFalseEquivalents(2) = "off"
booleanFalseEquivalents(3) = "no"
errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.")
}

11
sources/Commands/Commands.uc

@ -32,6 +32,8 @@ var private AssociativeArray registeredCommands;
// by prepending them with "!" character.
var public config bool useChatInput;
var LoggerAPI.Definition errCommandDuplicate;
protected function OnEnabled()
{
registeredCommands = _.collections.EmptyAssociativeArray();
@ -72,10 +74,10 @@ public final function RegisterCommand(class<Command> commandClass)
commandInstance = Command(registeredCommands.GetItem(commandName));
if (commandInstance != none)
{
_.logger.Failure("Command `" $ string(commandInstance.class)
$ "` with name '" $ commandName.ToPlainString()
$ "' is already registered. Command `" $ string(commandClass)
$ "` will be ignored.");
_.logger.Auto(errCommandDuplicate)
.ArgClass(commandInstance.class)
.Arg(commandName.Copy())
.ArgClass(commandClass);
commandName.FreeSelf();
return;
}
@ -155,4 +157,5 @@ defaultproperties
{
useChatInput = true
requiredListeners(0) = class'BroadcastListener_Commands'
errCommandDuplicate = (l=LOG_Error,m="Command `%1` with name '%2' is already registered. Command `%3` will be ignored.")
}

5
sources/Global.uc

@ -54,12 +54,13 @@ protected function Initialize()
// Special case that we cannot spawn with memory API since it obviously
// does not exist yet!
memory = new class'MemoryAPI';
// `TextAPI` and `CollectionsAPI` need to be loaded before `LoggerAPI`
ref = RefAPI(memory.Allocate(class'RefAPI'));
box = BoxAPI(memory.Allocate(class'BoxAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI'));
text = TextAPI(memory.Allocate(class'TextAPI'));
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI'));
alias = AliasesAPI(memory.Allocate(class'AliasesAPI'));
text = TextAPI(memory.Allocate(class'TextAPI'));
console = ConsoleAPI(memory.Allocate(class'ConsoleAPI'));
color = ColorAPI(memory.Allocate(class'ColorAPI'));
users = UserAPI(memory.Allocate(class'UserAPI'));

41
sources/Logger/ConsoleLogger.uc

@ -0,0 +1,41 @@
/**
* Simple logger class that uses `Log` method to print all of the
* log messages. Supports all of the default `acediaStamp`, `timeStamp` and
* `levelStamp` settings
* Copyright 2021 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 ConsoleLogger extends Logger
perObjectConfig
config(AcediaSystem)
dependson(LoggerAPI);
public function Write(Text message, LoggerAPI.LogLevel messageLevel)
{
local MutableText builder;
if (message != none)
{
builder = GetPrefix(messageLevel);
builder.Append(message);
Log(builder.ToPlainString());
builder.FreeSelf();
}
}
defaultproperties
{
}

355
sources/Logger/LogMessage.uc

@ -0,0 +1,355 @@
/**
* Object of this class is meant to represent a single log message that
* can have parameters, specified by "%<number>" definitions
* (e.g. "Thing %1 conflicts with thing %2, so we will have to remove %3").
* Log message only has to prepare (break into parts) provided human-readable
* string once and then will be able to quickly perform argument insertion
* (for which several convenient `Arg*()` methods are provided).
* The supposed way to use `LogMessage` is is in conjunction with
* `LoggerAPI`'s `Auto()` method that takes `Definition` with pre-filled
* message (`m`) and type (`t`), then:
* 1. (first time only) Generates a new `LogMessage` from them;
* 2. Returns `LogMessage` object that, whos arguments are supposed to be
* filled with `Arg*()` methods;
* 3. When the appropriate amount of `Arg*()` calls (by the number of
* specified "%<number>" tags) was made - logs resulting message.
* (4). If message takes no arguments - no `Arg*()` calls are necessary;
* (5). If new `Auto()` call is made before previous message was provided
* enough arguments - error will be logged and previous message
* will be discarded (along with it's arguments).
* For more information about using it - refer to the documentation.
* Copyright 2021 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 LogMessage extends AcediaObject;
// Flag to prevent `Initialize()` being called several times in a row
var private bool isInitialized;
// With what level was this message initialized?
var private LoggerAPI.LogLevel myLevel;
/**
* We essentially break specified log string into parts
* (replacing empty parts with `none`) like this:
* "This %1 is %3, not %2" =>
* * "This "
* * " is "
* * ", not "
* * `none`
* Arguments always fit between these parts, so if there is `N` parts,
* there will be `N - 1` arguments. We consider that we are done filling
* arguments when amount of `Arg*()` calls reaches that number.
*
* For future localization purposes we do not assume that arguments are
* specified from left to right in order: in our example,
* if we make following calls: `.Arg("one").Arg("caged").Arg("free")`,
* we will get the string: "This one is free, not caged".
* To remember the order we keep a special array, in our case it would be
* [1, 3, 2], except normalized to start from zero: [0, 2, 1].
*/
// Parts of the initial message, broken by "%<number> tags
var private array<Text> logParts;
// Defines order of arguments:
// `i` -> argument at what number to use at insertion place `i`?
// (`i` starting from zero instead of `1`).
var private array<int> normalizedArguments;
// Only default value is used for this variable: it remembers what
// `LogMessage` currently stores "garbage": temporary `Text` object to create
// a log message. Making an `Arg*()` call on any other `LogMessage` will cause
// progress of `default.dirtyLogMessage` to be reset, thus enforcing that only
// one `LogMessage` can be in the process of filling itself with arguments at
// a time and, therefore, only one can be "dirty": contain temporary
// `Text` objects.
// This way using `LogMessage` would not lead to accumulating large
// amounts of trash objects, since only one of them can "make a mess".
var private LogMessage dirtyLogMessage;
// Arguments, collected so far by the `Arg*()` calls
var private array<Text> collectedArguments;
protected function Finalizer()
{
isInitialized = false;
_.memory.FreeMany(logParts);
_.memory.FreeMany(collectedArguments);
logParts.length = 0;
collectedArguments.length = 0;
normalizedArguments.length = 0;
}
/**
* Initialize new `LogMessage` object by a given definition.
* Can only be done once.
*
* Correct functionality is guaranteed when arguments start from either
* `0` or `1` and then increase in order, without gaps or repetitions.
* `Initialize()` will attempt to correctly initialize `LogMessage` in case
* these rules are broken, by making assumptions about user's intentions,
* but behavior in that case should be considered undefined.
*
* @param logMessageDefinition Definition to take message parameter from.
*/
public final function Initialize(LoggerAPI.Definition logMessageDefinition)
{
local int nextArgument;
local Parser parser;
local MutableText nextLogPart, nextChunk;
local Text.Character percentCharacter;
local array<int> parsedArguments;
if (isInitialized) {
return;
}
isInitialized = true;
myLevel = logMessageDefinition.l;
percentCharacter = _.text.GetCharacter("%");
parser = _.text.ParseString(logMessageDefinition.m);
nextLogPart = _.text.Empty();
// General idea is simply to repeat: parse until "%" -> parse "%<number>"
while (!parser.HasFinished())
{
parser.MUntil(nextChunk, percentCharacter).Confirm();
nextLogPart.Append(nextChunk);
// If we cannot parse "%" after `MUntil(nextChunk, percentCharacter)`,
// then we have parsed everything
if (!parser.Match(P("%")).Confirm()) {
break;
}
// We need to check whether it i really "%<number>" tag and not
// just a "%" symbol without number
if (parser.MInteger(nextArgument).Confirm())
{
parsedArguments[parsedArguments.length] = nextArgument;
logParts[logParts.length] = nextLogPart.Copy();
nextLogPart.Clear();
}
else
{
// If it is just a symbol - simply add it
nextLogPart.AppendCharacter(percentCharacter);
parser.R();
}
}
logParts[logParts.length] = nextLogPart.Copy();
parser.FreeSelf();
nextLogPart.FreeSelf();
CleanupEmptyLogParts();
NormalizeArguments(parsedArguments);
}
// Since `none` and empty `Text` will be treated the same way by the `Append()`
// operation, we do not need to keep empty `Text` objects and can simply
// replace them with `none`s
private final function CleanupEmptyLogParts()
{
local int i;
for (i = 0; i < logParts.length; i += 1)
{
if (logParts[i].IsEmpty())
{
logParts[i].FreeSelf();
logParts[i] = none;
}
}
}
// Normalize enumeration by replacing them with natural numbers sequence:
// [0, 1, 2, ...] in the same order:
// [2, 6, 3] -> [0, 2, 1]
// [-2, 0, 4, -7] -> [1, 2, 3, 0]
// [1, 1, 2, 1] -> [0, 1, 3, 2]
private final function NormalizeArguments(array<int> argumentsOrder)
{
local int i;
local int nextArgument;
local int lowestArgument, lowestArgumentIndex;
normalizedArguments.length = 0;
normalizedArguments.length = argumentsOrder.length;
while (nextArgument < normalizedArguments.length)
{
// Find next minimal index and record next natural number
// (`nextArgument`) into it
for (i = 0; i < argumentsOrder.length; i += 1)
{
if (argumentsOrder[i] < lowestArgument || i == 0)
{
lowestArgumentIndex = i;
lowestArgument = argumentsOrder[i];
}
}
argumentsOrder[lowestArgumentIndex] = MaxInt;
normalizedArguments[lowestArgumentIndex] = nextArgument;
nextArgument += 1;
}
}
/**
* Fills next argument in caller `LogMessage` with given `Text` argument.
*
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling
* all arguments leads to message being logged.
*
* @param argument This argument will be managed by the `LogMessage`:
* you should not deallocate it by hand or rely on passed `Text` to not
* be deallocated. This also means that you should not pass `Text` objects
* returned by `P()`, `C()` or `F()` calls.
* @return Caller `LogMessage` to allow for method chaining.
*/
public final function LogMessage Arg(Text argument)
{
local Text assembledMessage;
if (IsArgumentListFull()) {
return self;
}
// Do we need to clean old `LogMessage` from it's arguments first?
if (default.dirtyLogMessage != none && default.dirtyLogMessage != self) {
default.dirtyLogMessage.Reset();
}
default.dirtyLogMessage = self; // `self` is dirty with arguments now
collectedArguments[collectedArguments.length] = argument;
if (IsArgumentListFull())
{
// Last argument - have to log what we have collected
assembledMessage = Collect();
_.logger.LogAtLevel(assembledMessage, myLevel);
assembledMessage.FreeSelf();
return self;
}
return self;
}
// Check whether we have enough arguments to completely make log message:
// each argument goes in between two log parts, so there is
// `logParts.length - 1` arguments total.
private final function bool IsArgumentListFull()
{
return collectedArguments.length >= logParts.length - 1;
}
/**
* Fills next argument in caller `LogMessage` with given `int` argument.
*
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling
* all arguments leads to message being logged.
*
* @param argument This value will be converted into `Text` and pasted
* into the log message.
* @return Caller `LogMessage` to allow for method chaining.
*/
public final function LogMessage ArgInt(int argument)
{
return Arg(_.text.FromInt(argument));
}
/**
* Fills next argument in caller `LogMessage` with given `float` argument.
*
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling
* all arguments leads to message being logged.
*
* @param argument This value will be converted into `Text` and pasted
* into the log message.
* @return Caller `LogMessage` to allow for method chaining.
*/
public final function LogMessage ArgFloat(float argument)
{
return Arg(_.text.FromFloat(argument));
}
/**
* Fills next argument in caller `LogMessage` with given `bool` argument.
*
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling
* all arguments leads to message being logged.
*
* @param argument This value will be converted into `Text` and pasted
* into the log message.
* @return Caller `LogMessage` to allow for method chaining.
*/
public final function LogMessage ArgBool(bool argument)
{
return Arg(_.text.FromBool(argument));
}
/**
* Fills next argument in caller `LogMessage` with given `class<Object>`
* argument.
*
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling
* all arguments leads to message being logged.
*
* @param argument This value will be converted into `Text` and pasted
* into the log message.
* @return Caller `LogMessage` to allow for method chaining.
*/
public final function LogMessage ArgClass(class<Object> argument)
{
return Arg(_.text.FromClass(argument));
}
/**
* Resets current progress of filling caller `LogMessage` with arguments,
* deallocating already passed ones.
*
* @return Caller `LogMessage` to allow for method chaining.
*/
public final function LogMessage Reset()
{
_.memory.FreeMany(collectedArguments);
collectedArguments.length = 0;
return self;
}
/**
* Returns `LogMessage`, assembled with it's arguments into the `Text`.
*
* If some arguments were not yet filled - they will treated as empty `Text`
* values.
*
* This result will be reset if `Reset()` method is called or another
* `LogMessage` starts filling itself with arguments.
*
* @return Caller `LogMessage`, assembled with it's arguments into the `Text`.
*/
public final function Text Collect()
{
local int i, argumentIndex;
local Text result;
local Text nextArgument;
local MutableText builder;
if (logParts.length == 0) {
return P("").Copy();
}
builder = _.text.Empty();
for (i = 0; i < logParts.length - 1; i += 1)
{
nextArgument = none;
// Since arguments might not be specified in order -
argumentIndex = normalizedArguments[i];
if (argumentIndex < collectedArguments.length) {
nextArgument = collectedArguments[argumentIndex];
}
builder.Append(logParts[i]).Append(nextArgument);
}
builder.Append(logParts[logParts.length - 1]);
result = builder.Copy();
builder.FreeSelf();
return result;
}
defaultproperties
{
}

151
sources/Logger/Logger.uc

@ -0,0 +1,151 @@
/**
* Base class for implementing "loggers" - objects that actually write log
* messages somewhere. To use it - simply implement `Write()` method,
* preferably making use of `GetPrefix()` method.
* Copyright 2021 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 Logger extends AcediaObject
perObjectConfig
config(AcediaSystem)
dependson(LoggerAPI)
abstract;
// Named loggers are stored here to avoid recreating them
var private AssociativeArray loadedLoggers;
// Should `Logger` display prefix indicating it's a log message from Acedia?
var protected config bool acediaStamp;
// Should `Logger` display time stamp prefix in front of log messages?
var protected config bool timeStamp;
// Should `Logger` display information about what level message was logged?
var protected config bool levelStamp;
var protected const int TDEBUG, TINFO, TWARNING, TERROR, TFATAL, TTIME, TACEDIA;
var protected const int TSPACE;
/**
* Method for creating named `Logger`s that can have their settings prepared
* in the config file. Only one `Logger` is made for every
* unique (case insensitive) `loggerName`.
*
* @param loggerName Name of the logger instance to return. Consequent calls
* with the same `loggerName` value will return the same `Logger`,
* unless it is deallocated.
* @return Logger with object name `loggerName`.
*/
public final static function Logger GetLogger(Text loggerName)
{
local Logger loggerInstance;
local Text loggerKey;
if (default.loadedLoggers == none)
{
// TODO: do this in static constructor
default.loadedLoggers = __().collections.EmptyAssociativeArray();
}
if (loggerName == none) {
return none;
}
loggerKey = loggerName.LowerCopy();
loggerInstance = Logger(default.loadedLoggers.GetItem(loggerKey));
if (loggerInstance == none)
{
// TODO: important to redo this via `MemoryAPI` to call constructors
loggerInstance = new(none, loggerName.ToPlainString()) default.class;
loggerInstance._constructor();
default.loadedLoggers.SetItem(loggerKey, loggerInstance);
}
loggerKey.FreeSelf();
return loggerInstance;
}
/**
* Auxiliary method for generating log message prefix based on `acediaStamp`,
* `timeStamp` and `levelStamp` flags according to their description.
* Method does not provide any guarantees on how exactly.
*
* @param messageLevel Message level for which to generate prefix.
* @return Text (mutable) representation of generated prefix.
*/
protected function MutableText GetPrefix(LoggerAPI.LogLevel messageLevel)
{
local MutableText builder;
builder = _.text.Empty();
if (acediaStamp) {
builder.Append(T(TACEDIA));
}
if (timeStamp) {
builder.Append(T(TTIME));
}
// Make output prettier by adding a space after the "[...]" prefixes
if (!levelStamp && (acediaStamp || timeStamp)) {
builder.Append(T(TSPACE));
}
if (!levelStamp) {
return builder;
}
switch (messageLevel)
{
case LOG_Debug:
builder.Append(T(TDEBUG));
break;
case LOG_Info:
builder.Append(T(TINFO));
break;
case LOG_Warning:
builder.Append(T(TWARNING));
break;
case LOG_Error:
builder.Append(T(TERROR));
break;
case LOG_Fatal:
builder.Append(T(TFATAL));
break;
default:
}
return builder;
}
/**
* Method that must perform an actual work of outputting message `message`
* at level `messageLevel`.
*
* @param message Message to output.
* @param messageLevel Level, at which message must be output.
*/
public function Write(Text message, LoggerAPI.LogLevel messageLevel){}
defaultproperties
{
// Parts of the prefix for our log messages, redirected into kf log file.
TDEBUG = 0
stringConstants(0) = "[Debug] "
TINFO = 1
stringConstants(1) = "[Info] "
TWARNING = 2
stringConstants(2) = "[Warning] "
TERROR = 3
stringConstants(3) = "[Error] "
TFATAL = 4
stringConstants(4) = "[Fatal] "
TTIME = 5
stringConstants(5) = "[hh:mm:ss]"
TACEDIA = 6
stringConstants(6) = "[Acedia]"
TSPACE = 7
stringConstants(7) = " "
}

322
sources/Logger/LoggerAPI.uc

@ -1,7 +1,12 @@
/**
* API that provides functions quick access to Acedia's
* logging functionality.
* Copyright 2020 Anton Tarasenko
* Every message can be logged at five different levels: debug, info,
* warning, error, fatal. For each of the levels it keeps the list of `Logger`
* objects that then do the actual logging. `Logger` class itself is abstract
* and can have different implementations, depending on where do you want to
* output log information.
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -18,73 +23,318 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class LoggerAPI extends AcediaObject;
class LoggerAPI extends AcediaObject
config(AcediaSystem);
var private LoggerService logService;
// Struct used to define `Logger`s list in Acedia's config files.
struct LoggerRecord
{
// Name of the `Logger`
var public string name;
// Class of the logger to load
var public class<Logger> cls;
};
// To add a new `Logger` one first must to create a named object record with
// appropriate settings and then specify the name and the class of that logger
// in one of the `*Loggers` arrays, depending on what messages you want logger
// to store.
// Loggers, specified in `allLoggers` will log all levels of messages
var private config array<LoggerRecord> allLoggers;
// Loggers, specified in one of the arrays below will only output logs of
// a particular level (although one can always add the same `Logger` to
// several log levels)
var private config array<LoggerRecord> debugLoggers;
var private config array<LoggerRecord> infoLoggers;
var private config array<LoggerRecord> warningLoggers;
var private config array<LoggerRecord> errorLoggers;
var private config array<LoggerRecord> fatalLoggers;
protected function OnCreated()
// `Logger`s currently created for each log level
var private config array<Logger> debugLoggerInstances;
var private config array<Logger> infoLoggerInstances;
var private config array<Logger> warningLoggerInstances;
var private config array<Logger> errorLoggerInstances;
var private config array<Logger> fatalLoggerInstances;
// Log levels, available in Acedia.
enum LogLevel
{
logService = LoggerService(class'LoggerService'.static.Require());
}
// Do not output log message anywhere. Added as a default value to
// avoid outputting message at unintended level by omission.
LOG_None,
// Information that can be used to track down errors that occur on
// other people's systems, that developer cannot otherwise pinpoint.
// Use this to log information about internal objects' state that
// might be helpful to figuring out what the problem is when something
// breaks.
LOG_Debug,
// Information about important events that should be occurring under
// normal conditions, such as initializations/shutdowns,
// successful completion of significant events, configuration assumptions.
// Should not occur too often.
LOG_Info,
// For recoverable issues, anything that might cause errors or
// oddities in behavior.
// Should be used sparingly, i.e. player disconnecting might cause
// interruption in some logic, but should not cause a warning,
// since it is something expected to happen normally.
LOG_Warning,
// Use this for errors, - events that some operation cannot recover from,
// but still does not require your feature / module to shut down.
LOG_Error,
// Anything that does not allow your feature / module or game to
// function, completely irrecoverable failure state.
LOG_Fatal
};
public final function Track(string message)
// This structure can be used to initialize and store new `LogMessage`, based
// on `string` description (not a text to use it inside `defaultproperties`).
struct Definition
{
if (logService == none)
// Message
var private string m;
// Level of the message
// (not actually used to create an `instance`, but to later know
// how to report it)
var private LogLevel l;
// Once created, `LogMessage` will be hashed here
var private LogMessage instance;
};
var private const int TDEBUG, TINFO, TWARNING, TERROR, TFATAL, TKFLOG;
// Constructor simply adds `Logger`s as specified by the config
protected function Constructor()
{
local int i;
for (i = 0; i < debugLoggers.length; i += 1) {
AddLogger(debugLoggers[i], LOG_Debug);
}
for (i = 0; i < infoLoggers.length; i += 1) {
AddLogger(infoLoggers[i], LOG_Info);
}
for (i = 0; i < warningLoggers.length; i += 1) {
AddLogger(warningLoggers[i], LOG_Warning);
}
for (i = 0; i < errorLoggers.length; i += 1) {
AddLogger(errorLoggers[i], LOG_Error);
}
for (i = 0; i < fatalLoggers.length; i += 1) {
AddLogger(fatalLoggers[i], LOG_Fatal);
}
for (i = 0; i < allLoggers.length; i += 1)
{
class'LoggerService'.static.LogMessageToKFLog(LOG_Track, message);
return;
AddLogger(allLoggers[i], LOG_Debug);
AddLogger(allLoggers[i], LOG_Info);
AddLogger(allLoggers[i], LOG_Warning);
AddLogger(allLoggers[i], LOG_Error);
AddLogger(allLoggers[i], LOG_Fatal);
}
logService.LogMessage(LOG_Track, message);
}
public final function Debug(string message)
/**
* Adds another `Logger` to a particular log level (`messageLevel`).
* Once added, a logger cannot be removed.
*
* @param record Logger that must be added to track a specified level
* of log messages.
* @param messageLevel Level of messages passed logger must track.
* @return `LoggerAPI` instance to allow for method chaining.
*/
public final function LoggerAPI AddLogger(
LoggerRecord record,
LogLevel messageLevel)
{
if (logService == none)
if (record.cls == none) {
return none;
}
switch (messageLevel)
{
class'LoggerService'.static.LogMessageToKFLog(LOG_Debug, message);
return;
case LOG_Debug:
AddLoggerTo(record, debugLoggerInstances);
break;
case LOG_Info:
AddLoggerTo(record, infoLoggerInstances);
break;
case LOG_Warning:
AddLoggerTo(record, warningLoggerInstances);
break;
case LOG_Error:
AddLoggerTo(record, errorLoggerInstances);
break;
case LOG_Fatal:
AddLoggerTo(record, fatalLoggerInstances);
break;
default:
}
logService.LogMessage(LOG_Debug, message);
return self;
}
public final function Info(string message)
// Add logger, described by `record` into `loggers` array.
// Report errors with `Log()`, since we cannot use `LoggerAPI` yet.
private final function AddLoggerTo(
LoggerRecord record,
out array<Logger> loggers)
{
if (logService == none)
local int i;
local Text loggerName;
local Logger newInstance;
if (record.cls == none)
{
// Cannot use `LoggerAPI` here ¯\_(ツ)_/¯
Log("[Acedia/LoggerAPI] Failure to add logger: empty class for \""
$ record.name $ "\" is specified");
return;
}
// Try to get the instance
loggerName = _.text.FromString(record.name);
newInstance = record.cls.static.GetLogger(loggerName);
loggerName.FreeSelf();
if (newInstance == none)
{
class'LoggerService'.static.LogMessageToKFLog(LOG_Info, message);
Log("[Acedia/LoggerAPI] Failure to add logger: could not create logger"
@ "of class `" $ record.cls $ "` named \"" $ record.name $ "\"");
return;
}
logService.LogMessage(LOG_Info, message);
// Ensure it was not already added
for (i = 0; i < loggers.length; i += 1) {
if (newInstance == loggers[i]) return;
}
loggers[loggers.length] = newInstance;
}
public final function Warning(string message)
/**
* This method accepts "definition struct" for `LogMessage` only to create and
* return it, allowing you to make `Arg*()` calls to fill-in missing arguments
* (defined in `LogMessage` by "%<number>" tags).
*
* Once all necessary `Arg*()` calls have been made, `LogMessage` will
* automatically send prepared message into `LoggerAPI`.
* Typical usage usually looks like:
* `_.logger.Auto(myErrorDef).Arg(objectName).ArgInt(objectID);`
* See `LogMessage` class for more information.
*
* @param definition "Definition" filled with `string` message to log and
* message level at which resulting message must be logged.
* @return `LogMessage` generated by given `definition`. Once created it will
* be hashed and reused when the same struct value is passed again
* (`LogMessage` will be stored in passed `definition`, so creating a
* new struct with the same message/log level will erase
* the hashed `LogMessage`).
*/
public final function LogMessage Auto(out Definition definition)
{
if (logService == none)
local LogMessage instance;
instance = definition.instance;
if (instance == none)
{
class'LoggerService'.static.LogMessageToKFLog(LOG_Warning, message);
return;
instance = LogMessage(_.memory.Allocate(class'LogMessage'));
instance.Initialize(definition);
definition.instance = instance;
}
logService.LogMessage(LOG_Warning, message);
return instance.Reset();
}
public final function Failure(string message)
/**
* This method causes passed message `message` to be passed to loggers for
* `messageLevel` message level.
*
* @param message Message to log.
* @param messageLevel Level at which to log message.
*/
public final function LogAtLevel(Text message, LogLevel messageLevel)
{
if (logService == none)
switch (messageLevel)
{
class'LoggerService'.static.LogMessageToKFLog(LOG_Failure, message);
return;
case LOG_Debug:
self.Debug(message);
break;
case LOG_Info:
self.Info(message);
break;
case LOG_Warning:
self.Warning(message);
break;
case LOG_Error:
self.Error(message);
break;
case LOG_Fatal:
self.Fatal(message);
break;
default:
}
logService.LogMessage(LOG_Failure, message);
}
public final function Fatal(string message)
/**
* This method causes passed message `message` to be passed to loggers for
* debug message level.
*
* @param message Message to log.
*/
public final function Debug(Text message)
{
if (logService == none)
{
class'LoggerService'.static.LogMessageToKFLog(LOG_Fatal, message);
return;
local int i;
for (i = 0; i < debugLoggerInstances.length; i += 1) {
debugLoggerInstances[i].Write(message, LOG_Debug);
}
}
/**
* This method causes passed message `message` to be passed to loggers for
* info message level.
*
* @param message Message to log.
*/
public final function Info(Text message)
{
local int i;
for (i = 0; i < infoLoggerInstances.length; i += 1) {
infoLoggerInstances[i].Write(message, LOG_Info);
}
}
/**
* This method causes passed message `message` to be passed to loggers for
* warning message level.
*
* @param message Message to log.
*/
public final function Warning(Text message)
{
local int i;
for (i = 0; i < warningLoggerInstances.length; i += 1) {
warningLoggerInstances[i].Write(message, LOG_Warning);
}
}
/**
* This method causes passed message `message` to be passed to loggers for
* error message level.
*
* @param message Message to log.
*/
public final function Error(Text message)
{
local int i;
for (i = 0; i < errorLoggerInstances.length; i += 1) {
errorLoggerInstances[i].Write(message, LOG_Error);
}
}
/**
* This method causes passed message `message` to be passed to loggers for
* fatal message level.
*
* @param message Message to log.
*/
public final function Fatal(Text message)
{
local int i;
for (i = 0; i < fatalLoggerInstances.length; i += 1) {
fatalLoggerInstances[i].Write(message, LOG_Fatal);
}
logService.LogMessage(LOG_Fatal, message);
}
defaultproperties

166
sources/Logger/LoggerService.uc

@ -1,166 +0,0 @@
/**
* Logger that allows to separate log messages into several levels of
* significance and lets users and admins to access only the ones they want
* and/or receive notifications when they happen.
* 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 LoggerService extends Service
config(AcediaLogger);
// Log levels, available in Acedia.
enum LogLevel
{
// For the purposes of "tracing" the code, when trying to figure out
// where exactly problems occurred.
// Should not be used in any released version of
// your packages/mutators.
LOG_Track,
// Information that can be used to track down errors that occur on
// other people's systems, that developer cannot otherwise pinpoint.
// Should be used with purpose of tracking a certain issue and
// not "just in case".
LOG_Debug,
// Information about important events that should be occurring under
// normal conditions, such as initializations/shutdowns,
// successful completion of significant events, configuration assumptions.
// Should not occur too often.
LOG_Info,
// For recoverable issues, anything that might cause errors or
// oddities in behavior.
// Should be used sparingly, i.e. player disconnecting might cause
// interruption in some logic, but should not cause a warning,
// since it is something expected to happen normally.
LOG_Warning,
// Use this for errors, - events that some operation cannot recover from,
// but still does not require your module to shut down.
LOG_Failure,
// Anything that does not allow your module or game to function,
// completely irrecoverable failure state.
LOG_Fatal
};
var private const string kfLogPrefix;
var private const string traceLevelName;
var private const string DebugLevelName;
var private const string infoLevelName;
var private const string warningLevelName;
var private const string errorLevelName;
var private const string fatalLevelName;
var private config array< class<Manifest> > registeredManifests;
var private config bool logTraceInKFLog;
var private config bool logDebugInKFLog;
var private config bool logInfoInKFLog;
var private config bool logWarningInKFLog;
var private config bool logErrorInKFLog;
var private config bool logFatalInKFLog;
var private array<string> traceMessages;
var private array<string> debugMessages;
var private array<string> infoMessages;
var private array<string> warningMessages;
var private array<string> errorMessages;
var private array<string> fatalMessages;
public final function bool ShouldAddToKFLog(LogLevel messageLevel)
{
if (messageLevel == LOG_Track && logTraceInKFLog) return true;
if (messageLevel == LOG_Debug && logDebugInKFLog) return true;
if (messageLevel == LOG_Info && logInfoInKFLog) return true;
if (messageLevel == LOG_Warning && logWarningInKFLog) return true;
if (messageLevel == LOG_Failure && logErrorInKFLog) return true;
if (messageLevel == LOG_Fatal && logFatalInKFLog) return true;
return false;
}
public final static function LogMessageToKFLog
(
LogLevel messageLevel,
string message
)
{
local string levelPrefix;
levelPrefix = default.kfLogPrefix;
switch (messageLevel)
{
case LOG_Track:
levelPrefix = levelPrefix $ default.traceLevelName;
break;
case LOG_Debug:
levelPrefix = levelPrefix $ default.debugLevelName;
break;
case LOG_Info:
levelPrefix = levelPrefix $ default.infoLevelName;
break;
case LOG_Warning:
levelPrefix = levelPrefix $ default.warningLevelName;
break;
case LOG_Failure:
levelPrefix = levelPrefix $ default.errorLevelName;
break;
case LOG_Fatal:
levelPrefix = levelPrefix $ default.fatalLevelName;
break;
default:
}
Log(levelPrefix @ message);
}
public final function LogMessage(LogLevel messageLevel, string message)
{
switch (messageLevel)
{
case LOG_Track:
traceMessages[traceMessages.length] = message;
case LOG_Debug:
debugMessages[debugMessages.length] = message;
case LOG_Info:
infoMessages[infoMessages.length] = message;
case LOG_Warning:
warningMessages[warningMessages.length] = message;
case LOG_Failure:
errorMessages[errorMessages.length] = message;
case LOG_Fatal:
fatalMessages[fatalMessages.length] = message;
default:
}
if (ShouldAddToKFLog(messageLevel))
{
LogMessageToKFLog(messageLevel, message);
}
}
defaultproperties
{
// Log everything by default, if someone does not like it -
// he/she can disable it themselves.
logTraceInKFLog = true
logDebugInKFLog = true
logInfoInKFLog = true
logWarningInKFLog = true
logErrorInKFLog = true
logFatalInKFLog = true
// Parts of the prefix for our log messages, redirected into kf log file.
kfLogPrefix = "Acedia:"
traceLevelName = "Trace"
debugLevelName = "Debug"
infoLevelName = "Info"
warningLevelName = "Warning"
errorLevelName = "Error"
fatalLevelName = "Fatal"
}

170
sources/Logger/Tests/TEST_LogMessage.uc

@ -0,0 +1,170 @@
/**
* Set of tests related to `LoggerAPI`.
* Copyright 2021 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 TEST_LogMessage extends TestCase
abstract;
// Short-hand for creating disposable `Text` out of a `string`
// We need it, since `P()` always returns the same value, which might lead to
// a conflict.
protected static function Text A(string message)
{
return __().text.FromString(message);
}
// Short-hand for quickly producing `LogMessage.Definition`
protected static function LoggerAPI.Definition DEF(string message)
{
local LoggerAPI.Definition result;
result.m = message;
return result;
}
protected static function TESTS()
{
Context("Testing how `LogMessage` collects given arguments.");
Test_SimpleArgumentCollection();
Test_ArgumentCollection();
Test_ArgumentCollectionOrder();
Test_TypedArgumentCollection();
}
protected static function Test_SimpleArgumentCollection()
{
local LogMessage message;
Issue("`Text` arguments are not correctly pasted.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Message %1and%2: %3"));
message.Arg(A("umbra")).Arg(A("mumbra")).Arg(A("eleven! "));
TEST_ExpectTrue( message.Collect().ToPlainString()
== "Message umbraandmumbra: eleven! ");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("%1 - was pasted."));
message.Arg(A("Heheh"));
TEST_ExpectTrue(message.Collect().ToPlainString() == "Heheh - was pasted.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("This %%%1 and that %2"));
message.Arg(A("one")).Arg(A("two"));
TEST_ExpectTrue( message.Collect().ToPlainString()
== "This %%one and that two");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("%1%2"));
message.Arg(A("one")).Arg(A("two"));
TEST_ExpectTrue(message.Collect().ToPlainString() == "onetwo");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("%1"));
message.Arg(A("only"));
TEST_ExpectTrue(message.Collect().ToPlainString() == "only");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Just some string."));
TEST_ExpectTrue(message.Collect().ToPlainString() == "Just some string.");
}
protected static function Test_ArgumentCollection()
{
local LogMessage message;
Issue("`Text` arguments are not correctly collected after reset.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("This %1 and that %2"));
message.Arg(A("one")).Arg(A("two")).Reset().Arg(A("huh")).Arg(A("muh"));
TEST_ExpectTrue( message.Collect().ToPlainString()
== "This huh and that muh");
Issue("`Text` arguments are not correctly collected after specifying"
@ "too many.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Just %1, %2, %3, %4 and %5"));
message.Arg(A("1")).Arg(A("2")).Arg(A("3")).Arg(A("4")).Arg(A("5"))
.Arg(A("6")).Arg(A("7"));
TEST_ExpectTrue( message.Collect().ToPlainString()
== "Just 1, 2, 3, 4 and 5");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Just"));
TEST_ExpectTrue(message.Arg(A("arg")).Collect().ToPlainString() == "Just");
Issue("`Text` arguments are not correctly collected after specifying"
@ "too little.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Just %1, %2, %3, %4 and %5"));
message.Arg(A("1")).Arg(A("2")).Arg(A("3"));
TEST_ExpectTrue( message.Collect().ToPlainString()
== "Just 1, 2, 3, and ");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Maybe %1"));
TEST_ExpectTrue(message.Collect().ToPlainString() == "Maybe ");
}
protected static function Test_ArgumentCollectionOrder()
{
local LogMessage message;
Issue("`Text` arguments are not correctly collected if are not specified"
@ "in order.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("This %2 and that %1"));
message.Arg(A("huh")).Arg(A("muh"));
TEST_ExpectTrue( message.Collect().ToPlainString()
== "This muh and that huh");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Just %5, %3, %4, %1 and %2"));
message.Arg(A("1")).Arg(A("2")).Arg(A("3")).Arg(A("4")).Arg(A("5"));
TEST_ExpectTrue( message.Collect().ToPlainString()
== "Just 5, 3, 4, 1 and 2");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
Issue("`Text` arguments are not correctly collected if are not specified"
@ "in order and not enough of them was specified.");
message.Initialize(DEF("Just %5, %3, %4, %1 and %2"));
message.Arg(A("1")).Arg(A("2")).Arg(A("3"));
TEST_ExpectTrue( message.Collect().ToPlainString()
== "Just , 3, , 1 and 2");
}
protected static function Test_TypedArgumentCollection()
{
local LogMessage message;
Issue("`int` arguments are not correctly collected.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Int: %1"));
TEST_ExpectTrue(message.ArgInt(-7).Collect().ToPlainString()
== "Int: -7");
Issue("`float` arguments are not correctly collected.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Float: %1"));
TEST_ExpectTrue(message.ArgFloat(3.14).Collect().ToPlainString()
== "Float: 3.14");
Issue("`bool` arguments are not correctly collected.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Bool: %1 and %2"));
TEST_ExpectTrue(message.ArgBool(true).ArgBool(false).Collect()
.ToPlainString() == "Bool: true and false");
Issue("`Class` arguments are not correctly collected.");
message = LogMessage(__().memory.Allocate(class'LogMessage'));
message.Initialize(DEF("Class: %1"));
TEST_ExpectTrue(message.ArgClass(class'M14EBRBattleRifle').Collect()
.ToPlainString() == "Class: KFMod.M14EBRBattleRifle");
}
defaultproperties
{
caseGroup = "Logger"
caseName = "LogMessage"
}

1
sources/Manifest.uc

@ -48,4 +48,5 @@ defaultproperties
testCases(14) = class'TEST_Iterator'
testCases(15) = class'TEST_Command'
testCases(16) = class'TEST_CommandDataBuilder'
testCases(17) = class'TEST_LogMessage'
}

15
sources/Players/ConnectionListener_Player.uc

@ -19,13 +19,15 @@
*/
class ConnectionListener_Player extends ConnectionListenerBase;
var LoggerAPI.Definition fatalNoPlayerService;
static function ConnectionEstablished(ConnectionService.Connection connection)
{
local PlayerService service;
service = PlayerService(class'PlayerService'.static.Require());
if (service == none) {
__().logger.Fatal("Cannot start `PlayerService` service"
@ "Acedia will not properly work from now on.");
if (service == none)
{
__().logger.Auto(default.fatalNoPlayerService);
return;
}
service.RegisterPlayer(connection.controllerReference);
@ -35,9 +37,9 @@ static function ConnectionLost(ConnectionService.Connection connection)
{
local PlayerService service;
service = PlayerService(class'PlayerService'.static.Require());
if (service == none) {
__().logger.Fatal("Cannot start `PlayerService` service"
@ "Acedia will not properly work from now on.");
if (service == none)
{
__().logger.Auto(default.fatalNoPlayerService);
return;
}
service.UpdateAllPlayers();
@ -46,4 +48,5 @@ static function ConnectionLost(ConnectionService.Connection connection)
defaultproperties
{
relatedEvents = class'ConnectionEvents'
fatalNoPlayerService = (l=LOG_Fatal,m="Cannot start `PlayerService` service Acedia will not properly work from now on.")
}

14
sources/Testing/Service/TestingService.uc

@ -51,6 +51,7 @@ var public config const string requiredGroup;
// class'TestingEvents' every time.
var const class<TestingEvents> events;
var LoggerAPI.Definition warnDuplicateTestCases;
/**
* Registers another `TestCase` class for later testing.
*
@ -75,13 +76,11 @@ public final static function bool RegisterTestCase(class<TestCase> newTestCase)
~= newTestCase.static.GetName())) {
continue;
}
default._.logger.Warning("Two different test cases with name \""
$ newTestCase.static.GetName() $ "\" in the same group \""
$ newTestCase.static.GetGroup() $ "\"have been registered:"
@ "\"" $ string(newTestCase) $ "\" and \""
$ string(default.registeredTestCases[i])
$ "\". This can lead to issues and it is not something you can fix,"
@ "- contact developers of the relevant packages.");
__().logger.Auto(default.warnDuplicateTestCases)
.Arg(__().text.FromString(newTestCase.static.GetName()))
.Arg(__().text.FromString(newTestCase.static.GetGroup()))
.ArgClass(newTestCase)
.ArgClass(default.registeredTestCases[i]);
}
default.registeredTestCases[default.registeredTestCases.length] =
newTestCase;
@ -250,4 +249,5 @@ defaultproperties
{
runTestsOnStartUp = false
events = class'TestingEvents'
warnDuplicateTestCases = (l=LOG_Fatal,m="Two different test cases with name \"%1\" in the same group \"%2\"have been registered: \"%3\" and \"%4\". This can lead to issues and it is not something you can fix, - contact developers of the relevant packages.")
}

39
sources/Text/TextAPI.uc

@ -665,6 +665,19 @@ public final function Parser ParseString(string source)
return parser;
}
/**
* Creates a `Text` that consists only of a given character.
*
* @param character Character that will be converted into a string.
* @return `Text` that consists only of a given character,
* if given character is valid. Empty `Text` otherwise.
* Guaranteed to be not `none`.
*/
public final function Text FromCharacter(Text.Character character)
{
return _.text.FromString(CharacterToString(character));
}
/**
* Method for converting `bool` values into immutable `Text`.
*
@ -749,6 +762,32 @@ public final function MutableText FromIntM(int value)
return FromStringM(string(value));
}
/**
* Method for converting `class<object>` values into immutable `Text`.
*
* To create `MutableText` instead use `FromClassM()` method.
*
* @param value `class<object>` value to be displayed as `Text`.
* @return Text representation of given `class<object>` value.
*/
public final function Text FromClass(class<object> value)
{
return FromString(string(value));
}
/**
* Method for converting `class<Object>` values into mutable `MutableText`.
*
* To create `Text` instead use `FromClass()` method.
*
* @param value `class<Object>` value to be displayed as `MutableText`.
* @return Text representation of given `class<Object>` value.
*/
public final function MutableText FromClassM(class<Object> value)
{
return FromStringM(string(value));
}
/**
* Method for converting `float` values into immutable `Text`.
*

Loading…
Cancel
Save