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. 8
      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. 6
      sources/Text/Parser.uc
  20. 39
      sources/Text/TextAPI.uc

24
config/AcediaSystem.ini

@ -17,6 +17,30 @@
; at `2`. ; at `2`.
usedInjectionLevel=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] [AcediaCore_0_2.Commands]
; This feature provides a mechanism to define commands that automatically ; This feature provides a mechanism to define commands that automatically
; parse their arguments into standard Acedia collection. It also allows to ; 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. // Otherwise only stores first loaded alias.
var private AssociativeArray aliasHash; var private AssociativeArray aliasHash;
var LoggerAPI.Definition errIncorrectAliasPair, warnDuplicateAlias;
// Load and hash all the data `AliasSource` creation. // Load and hash all the data `AliasSource` creation.
protected function OnCreated() protected function OnCreated()
{ {
@ -71,8 +73,7 @@ private final function bool AssertAliasesClassIsOwnedByThisSource()
{ {
if (aliasesClass == none) return true; if (aliasesClass == none) return true;
if (aliasesClass.default.sourceClass == class) return true; if (aliasesClass.default.sourceClass == class) return true;
_.logger.Failure("`AliasSource`-`Aliases` class pair is incorrectly" _.logger.Auto(errIncorrectAliasPair).ArgClass(class);
@ "setup for source `" $ string(class) $ "`. Omitting it.");
Destroy(); Destroy();
return false; return false;
} }
@ -294,18 +295,10 @@ public final function RemoveAlias(Text aliasToRemove)
private final function LogDuplicateAliasWarning(Text alias, Text existingValue) private final function LogDuplicateAliasWarning(Text alias, Text existingValue)
{ {
_.logger.Warning("Alias source `" $ string(class) _.logger.Auto(warnDuplicateAlias)
$ "` has duplicate record for alias \"" $ alias.ToPlainString() .ArgClass(class)
$ "\". This is likely due to an erroneous config. \"" .Arg(alias.Copy())
$ existingValue.ToPlainString() .Arg(existingValue.Copy());
$ "\" 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.");
} }
// Tries to find a loaded `Aliases` config object that stores aliases for // Tries to find a loaded `Aliases` config object that stores aliases for
@ -333,4 +326,6 @@ defaultproperties
{ {
// Source main parameters // Source main parameters
aliasesClass = class'Aliases' 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 * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * 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 * Provides an easier access to the instance of the `AliasSource` of
@ -52,17 +56,15 @@ public final function AliasSource GetWeaponSource()
local AliasSource weaponSource; local AliasSource weaponSource;
local class<AliasSource> sourceClass; local class<AliasSource> sourceClass;
sourceClass = class'AliasService'.default.weaponAliasesSource; sourceClass = class'AliasService'.default.weaponAliasesSource;
if (sourceClass == none) { if (sourceClass == none)
_.logger.Failure("No weapon aliases source configured for Acedia's" {
@ "alias API. Error is most likely cause by erroneous config."); _.logger.Auto(noWeaponAliasSource);
return none; return none;
} }
weaponSource = AliasSource(sourceClass.static.GetInstance(true)); weaponSource = AliasSource(sourceClass.static.GetInstance(true));
if (weaponSource == none) { if (weaponSource == none)
_.logger.Failure("`AliasSource` class `" $ string(sourceClass) $ "` is" {
@ "configured to store weapon aliases, but it seems to be invalid." _.logger.Auto(invalidWeaponAliasSource).ArgClass(sourceClass);
@ "This is a bug and not configuration file problem, but issue"
@ "might be avoided by using a different `AliasSource`.");
return none; return none;
} }
return weaponSource; return weaponSource;
@ -85,17 +87,15 @@ public final function AliasSource GetColorSource()
local AliasSource colorSource; local AliasSource colorSource;
local class<AliasSource> sourceClass; local class<AliasSource> sourceClass;
sourceClass = class'AliasService'.default.colorAliasesSource; sourceClass = class'AliasService'.default.colorAliasesSource;
if (sourceClass == none) { if (sourceClass == none)
_.logger.Failure("No color aliases source configured for Acedia's" {
@ "alias API. Error is most likely cause by erroneous config."); _.logger.Auto(noColorAliasSource);
return none; return none;
} }
colorSource = AliasSource(sourceClass.static.GetInstance(true)); colorSource = AliasSource(sourceClass.static.GetInstance(true));
if (colorSource == none) { if (colorSource == none)
_.logger.Failure("`AliasSource` class `" $ string(sourceClass) $ "` is" {
@ "configured to store color aliases, but it seems to be invalid." _.logger.Auto(invalidColorAliasSource).ArgClass(sourceClass);
@ "This is a bug and not configuration file problem, but issue"
@ "might be avoided by using a different `AliasSource`.");
return none; return none;
} }
return colorSource; return colorSource;
@ -167,4 +167,8 @@ public final function Text ResolveColor(Text alias, optional bool copyOnFailure)
defaultproperties 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 * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * 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) 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. * Returns name (in lower case) of the caller command class.
* *
* @return 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() 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) // Array of parameters we are currently filling (either required or optional)
var private array<Command.Parameter> selectedParameterArray; var private array<Command.Parameter> selectedParameterArray;
var LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong;
var LoggerAPI.Definition warnSameLongName, warnSameShortName;
protected function Constructor() protected function Constructor()
{ {
// Fill empty subcommand (no special key word) by default // Fill empty subcommand (no special key word) by default
@ -313,23 +316,17 @@ private final function Text.Character GetValidShortName(
} }
if (longName.GetLength() < 2) if (longName.GetLength() < 2)
{ {
_.logger.Failure("Command" @ self.class @ "is trying to register" _.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy());
@ "an option with a name that is way too short (<2 characters)."
@ "Option will be discarded:" @ longName.ToPlainString());
return _.text.GetInvalidCharacter(); return _.text.GetInvalidCharacter();
} }
// Validate `shortName`, // Validate `shortName`,
// deriving if from `longName` if necessary & possible // deriving if from `longName` if necessary & possible
if (shortName == none) if (shortName == none) {
{
return longName.GetCharacter(0); return longName.GetCharacter(0);
} }
if (shortName.IsEmpty() || shortName.GetLength() > 1) if (shortName.IsEmpty() || shortName.GetLength() > 1)
{ {
_.logger.Failure("Command" @ self.class @ "is trying to register" _.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy());
@ "an option with a short name that doesn't consist of just"
@ "one character. Option will be discarded:"
@ longName.ToPlainString());
return _.text.GetInvalidCharacter(); return _.text.GetInvalidCharacter();
} }
return shortName.GetCharacter(0); return shortName.GetCharacter(0);
@ -354,22 +351,18 @@ private final function bool VerifyNoOptionNamingConflict(
if ( !_.text.AreEqual(shortName, options[i].shortName) if ( !_.text.AreEqual(shortName, options[i].shortName)
&& longName.Compare(options[i].longName)) && longName.Compare(options[i].longName))
{ {
_ .logger.Warning("Command" @ self.class @ "is trying to register" _.logger.Auto(warnSameLongName)
@ "several options with the same long name" .ArgClass(class)
@ "\"" $ longName.ToPlainString() .Arg(longName.Copy());
$ "\", but different short names. This should not happen,"
@ "do not expect correct behavior.");
return true; return true;
} }
// Is same short name, but different short ones? // Is same short name, but different short ones?
if ( _.text.AreEqual(shortName, options[i].shortName) if ( _.text.AreEqual(shortName, options[i].shortName)
&& !longName.Compare(options[i].longName)) && !longName.Compare(options[i].longName))
{ {
_.logger.Warning("Command" @ self.class @ "is trying to register" _.logger.Auto(warnSameLongName)
@ "several options with the same short name" .ArgClass(class)
@ "\"" $ _.text.CharacterToString(shortName) .Arg(_.text.FromCharacter(shortName));
$ "\", but different long names. This should not have happened,"
@ "do not expect correct behavior.");
return true; return true;
} }
} }
@ -880,4 +873,8 @@ public final function CommandDataBuilder ParamArrayList(
defaultproperties 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.")
} }

8
sources/Commands/CommandParser.uc

@ -77,7 +77,7 @@ var private Command.SubCommand pickedSubCommand;
var private array<Command.Option> availableOptions; var private array<Command.Option> availableOptions;
// Result variable we are filling during the parsing process, // Result variable we are filling during the parsing process,
// should be `none` outside of `self.ParseWith()` method call. // should be `none` outside of `self.ParseWith()` method call.
var private CommandCall nextResult; var private CommandCall nextResult;
// Describes which parameters we are currently parsing, classifying them // Describes which parameters we are currently parsing, classifying them
// as either "necessary" or "extra". // as either "necessary" or "extra".
@ -120,6 +120,8 @@ var private array<Command.Option> usedOptions;
var private array<string> booleanTrueEquivalents; var private array<string> booleanTrueEquivalents;
var private array<string> booleanFalseEquivalents; var private array<string> booleanFalseEquivalents;
var LoggerAPI.Definition errNoSubCommands;
protected function Finalizer() protected function Finalizer()
{ {
Reset(); Reset();
@ -160,8 +162,7 @@ private final function PickSubCommand(Command.Data commandData)
allSubCommands = commandData.subCommands; allSubCommands = commandData.subCommands;
if (allSubcommands.length == 0) if (allSubcommands.length == 0)
{ {
_.logger.Failure("`GetSubCommand()` method was called on a command" _.logger.Auto(errNoSubCommands).ArgClass(class);
@ class @ "with zero defined sub-commands.");
pickedSubCommand = emptySubCommand; pickedSubCommand = emptySubCommand;
return; return;
} }
@ -797,4 +798,5 @@ defaultproperties
booleanFalseEquivalents(1) = "disable" booleanFalseEquivalents(1) = "disable"
booleanFalseEquivalents(2) = "off" booleanFalseEquivalents(2) = "off"
booleanFalseEquivalents(3) = "no" 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. // by prepending them with "!" character.
var public config bool useChatInput; var public config bool useChatInput;
var LoggerAPI.Definition errCommandDuplicate;
protected function OnEnabled() protected function OnEnabled()
{ {
registeredCommands = _.collections.EmptyAssociativeArray(); registeredCommands = _.collections.EmptyAssociativeArray();
@ -72,10 +74,10 @@ public final function RegisterCommand(class<Command> commandClass)
commandInstance = Command(registeredCommands.GetItem(commandName)); commandInstance = Command(registeredCommands.GetItem(commandName));
if (commandInstance != none) if (commandInstance != none)
{ {
_.logger.Failure("Command `" $ string(commandInstance.class) _.logger.Auto(errCommandDuplicate)
$ "` with name '" $ commandName.ToPlainString() .ArgClass(commandInstance.class)
$ "' is already registered. Command `" $ string(commandClass) .Arg(commandName.Copy())
$ "` will be ignored."); .ArgClass(commandClass);
commandName.FreeSelf(); commandName.FreeSelf();
return; return;
} }
@ -155,4 +157,5 @@ defaultproperties
{ {
useChatInput = true useChatInput = true
requiredListeners(0) = class'BroadcastListener_Commands' 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 // Special case that we cannot spawn with memory API since it obviously
// does not exist yet! // does not exist yet!
memory = new class'MemoryAPI'; memory = new class'MemoryAPI';
// `TextAPI` and `CollectionsAPI` need to be loaded before `LoggerAPI`
ref = RefAPI(memory.Allocate(class'RefAPI')); ref = RefAPI(memory.Allocate(class'RefAPI'));
box = BoxAPI(memory.Allocate(class'BoxAPI')); box = BoxAPI(memory.Allocate(class'BoxAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI')); text = TextAPI(memory.Allocate(class'TextAPI'));
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI')); collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI'));
alias = AliasesAPI(memory.Allocate(class'AliasesAPI')); alias = AliasesAPI(memory.Allocate(class'AliasesAPI'));
text = TextAPI(memory.Allocate(class'TextAPI'));
console = ConsoleAPI(memory.Allocate(class'ConsoleAPI')); console = ConsoleAPI(memory.Allocate(class'ConsoleAPI'));
color = ColorAPI(memory.Allocate(class'ColorAPI')); color = ColorAPI(memory.Allocate(class'ColorAPI'));
users = UserAPI(memory.Allocate(class'UserAPI')); 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 * API that provides functions quick access to Acedia's
* logging functionality. * 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. * This file is part of Acedia.
* *
@ -18,73 +23,318 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * 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); AddLogger(allLoggers[i], LOG_Debug);
return; 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); case LOG_Debug:
return; 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; 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); instance = LogMessage(_.memory.Allocate(class'LogMessage'));
return; 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); case LOG_Debug:
return; 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) local int i;
{ for (i = 0; i < debugLoggerInstances.length; i += 1) {
class'LoggerService'.static.LogMessageToKFLog(LOG_Fatal, message); debugLoggerInstances[i].Write(message, LOG_Debug);
return; }
}
/**
* 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 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(14) = class'TEST_Iterator'
testCases(15) = class'TEST_Command' testCases(15) = class'TEST_Command'
testCases(16) = class'TEST_CommandDataBuilder' 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; class ConnectionListener_Player extends ConnectionListenerBase;
var LoggerAPI.Definition fatalNoPlayerService;
static function ConnectionEstablished(ConnectionService.Connection connection) static function ConnectionEstablished(ConnectionService.Connection connection)
{ {
local PlayerService service; local PlayerService service;
service = PlayerService(class'PlayerService'.static.Require()); service = PlayerService(class'PlayerService'.static.Require());
if (service == none) { if (service == none)
__().logger.Fatal("Cannot start `PlayerService` service" {
@ "Acedia will not properly work from now on."); __().logger.Auto(default.fatalNoPlayerService);
return; return;
} }
service.RegisterPlayer(connection.controllerReference); service.RegisterPlayer(connection.controllerReference);
@ -35,9 +37,9 @@ static function ConnectionLost(ConnectionService.Connection connection)
{ {
local PlayerService service; local PlayerService service;
service = PlayerService(class'PlayerService'.static.Require()); service = PlayerService(class'PlayerService'.static.Require());
if (service == none) { if (service == none)
__().logger.Fatal("Cannot start `PlayerService` service" {
@ "Acedia will not properly work from now on."); __().logger.Auto(default.fatalNoPlayerService);
return; return;
} }
service.UpdateAllPlayers(); service.UpdateAllPlayers();
@ -46,4 +48,5 @@ static function ConnectionLost(ConnectionService.Connection connection)
defaultproperties defaultproperties
{ {
relatedEvents = class'ConnectionEvents' 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. // class'TestingEvents' every time.
var const class<TestingEvents> events; var const class<TestingEvents> events;
var LoggerAPI.Definition warnDuplicateTestCases;
/** /**
* Registers another `TestCase` class for later testing. * Registers another `TestCase` class for later testing.
* *
@ -75,13 +76,11 @@ public final static function bool RegisterTestCase(class<TestCase> newTestCase)
~= newTestCase.static.GetName())) { ~= newTestCase.static.GetName())) {
continue; continue;
} }
default._.logger.Warning("Two different test cases with name \"" __().logger.Auto(default.warnDuplicateTestCases)
$ newTestCase.static.GetName() $ "\" in the same group \"" .Arg(__().text.FromString(newTestCase.static.GetName()))
$ newTestCase.static.GetGroup() $ "\"have been registered:" .Arg(__().text.FromString(newTestCase.static.GetGroup()))
@ "\"" $ string(newTestCase) $ "\" and \"" .ArgClass(newTestCase)
$ string(default.registeredTestCases[i]) .ArgClass(default.registeredTestCases[i]);
$ "\". This can lead to issues and it is not something you can fix,"
@ "- contact developers of the relevant packages.");
} }
default.registeredTestCases[default.registeredTestCases.length] = default.registeredTestCases[default.registeredTestCases.length] =
newTestCase; newTestCase;
@ -250,4 +249,5 @@ defaultproperties
{ {
runTestsOnStartUp = false runTestsOnStartUp = false
events = class'TestingEvents' 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.")
} }

6
sources/Text/Parser.uc

@ -791,10 +791,10 @@ public final function Parser MUntil(
* @return Returns the caller `Parser`, to allow for function chaining. * @return Returns the caller `Parser`, to allow for function chaining.
*/ */
public final function Parser MUntilS( public final function Parser MUntilS(
out string result, out string result,
optional Text.Character characterBreak, optional Text.Character characterBreak,
optional bool whitespacesBreak, optional bool whitespacesBreak,
optional bool quotesBreak) optional bool quotesBreak)
{ {
local MutableText wrapper; local MutableText wrapper;
if (!Ok()) return self; if (!Ok()) return self;

39
sources/Text/TextAPI.uc

@ -665,6 +665,19 @@ public final function Parser ParseString(string source)
return parser; 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`. * Method for converting `bool` values into immutable `Text`.
* *
@ -749,6 +762,32 @@ public final function MutableText FromIntM(int value)
return FromStringM(string(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`. * Method for converting `float` values into immutable `Text`.
* *

Loading…
Cancel
Save