diff --git a/config/AcediaSystem.ini b/config/AcediaSystem.ini index 41b996f..433346b 100644 --- a/config/AcediaSystem.ini +++ b/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 diff --git a/sources/Aliases/AliasSource.uc b/sources/Aliases/AliasSource.uc index 9f06378..1cb9a69 100644 --- a/sources/Aliases/AliasSource.uc +++ b/sources/Aliases/AliasSource.uc @@ -51,6 +51,8 @@ var private config array 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.") } \ No newline at end of file diff --git a/sources/Aliases/AliasesAPI.uc b/sources/Aliases/AliasesAPI.uc index 84250b7..ca16c8c 100644 --- a/sources/Aliases/AliasesAPI.uc +++ b/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 . */ -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 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 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`.") } \ No newline at end of file diff --git a/sources/Commands/BuiltInCommands/ACommandHelp.uc b/sources/Commands/BuiltInCommands/ACommandHelp.uc index 8fd29e9..ec1b877 100644 --- a/sources/Commands/BuiltInCommands/ACommandHelp.uc +++ b/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 . */ -class ACommandHelp extends Command; +class ACommandHelp extends Command + dependson(LoggerAPI); + +var LoggerAPI.Definition testMsg; protected function BuildData(CommandDataBuilder builder) { diff --git a/sources/Commands/Command.uc b/sources/Commands/Command.uc index f3d3ecf..7ec8dd6 100644 --- a/sources/Commands/Command.uc +++ b/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() { diff --git a/sources/Commands/CommandDataBuilder.uc b/sources/Commands/CommandDataBuilder.uc index b79b6b8..14871e1 100644 --- a/sources/Commands/CommandDataBuilder.uc +++ b/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 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.") } \ No newline at end of file diff --git a/sources/Commands/CommandParser.uc b/sources/Commands/CommandParser.uc index 65cbd19..59c2ea3 100644 --- a/sources/Commands/CommandParser.uc +++ b/sources/Commands/CommandParser.uc @@ -77,7 +77,7 @@ var private Command.SubCommand pickedSubCommand; var private array availableOptions; // Result variable we are filling during the parsing process, // 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 // as either "necessary" or "extra". @@ -120,6 +120,8 @@ var private array usedOptions; var private array booleanTrueEquivalents; var private array 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.") } \ No newline at end of file diff --git a/sources/Commands/Commands.uc b/sources/Commands/Commands.uc index 54d74c0..cc28388 100644 --- a/sources/Commands/Commands.uc +++ b/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 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.") } \ No newline at end of file diff --git a/sources/Global.uc b/sources/Global.uc index eb8bdc8..a0dfa61 100644 --- a/sources/Global.uc +++ b/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')); diff --git a/sources/Logger/ConsoleLogger.uc b/sources/Logger/ConsoleLogger.uc new file mode 100644 index 0000000..1331404 --- /dev/null +++ b/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 . + */ +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 +{ +} \ No newline at end of file diff --git a/sources/Logger/LogMessage.uc b/sources/Logger/LogMessage.uc new file mode 100644 index 0000000..10e2dbb --- /dev/null +++ b/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 "%" 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 "%" 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 . + */ +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 "% tags +var private array 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 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 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 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 "%" + 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 "%" 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 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` + * 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 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 +{ +} \ No newline at end of file diff --git a/sources/Logger/Logger.uc b/sources/Logger/Logger.uc new file mode 100644 index 0000000..7d3e08b --- /dev/null +++ b/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 . + */ +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) = " " +} \ No newline at end of file diff --git a/sources/Logger/LoggerAPI.uc b/sources/Logger/LoggerAPI.uc index 3338419..fb30291 100644 --- a/sources/Logger/LoggerAPI.uc +++ b/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 . */ -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 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 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 debugLoggers; +var private config array infoLoggers; +var private config array warningLoggers; +var private config array errorLoggers; +var private config array fatalLoggers; -protected function OnCreated() +// `Logger`s currently created for each log level +var private config array debugLoggerInstances; +var private config array infoLoggerInstances; +var private config array warningLoggerInstances; +var private config array errorLoggerInstances; +var private config array 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 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 "%" 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 diff --git a/sources/Logger/LoggerService.uc b/sources/Logger/LoggerService.uc deleted file mode 100644 index 87d5693..0000000 --- a/sources/Logger/LoggerService.uc +++ /dev/null @@ -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 . - */ -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 > 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 traceMessages; -var private array debugMessages; -var private array infoMessages; -var private array warningMessages; -var private array errorMessages; -var private array 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" -} \ No newline at end of file diff --git a/sources/Logger/Tests/TEST_LogMessage.uc b/sources/Logger/Tests/TEST_LogMessage.uc new file mode 100644 index 0000000..69df945 --- /dev/null +++ b/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 . + */ +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" +} \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 9afe4fe..921527a 100644 --- a/sources/Manifest.uc +++ b/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' } \ No newline at end of file diff --git a/sources/Players/ConnectionListener_Player.uc b/sources/Players/ConnectionListener_Player.uc index a6924da..607c86a 100644 --- a/sources/Players/ConnectionListener_Player.uc +++ b/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.") } \ No newline at end of file diff --git a/sources/Testing/Service/TestingService.uc b/sources/Testing/Service/TestingService.uc index 896a932..9ca5160 100644 --- a/sources/Testing/Service/TestingService.uc +++ b/sources/Testing/Service/TestingService.uc @@ -51,6 +51,7 @@ var public config const string requiredGroup; // class'TestingEvents' every time. var const class events; +var LoggerAPI.Definition warnDuplicateTestCases; /** * Registers another `TestCase` class for later testing. * @@ -75,13 +76,11 @@ public final static function bool RegisterTestCase(class 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.") } \ No newline at end of file diff --git a/sources/Text/Parser.uc b/sources/Text/Parser.uc index 65705cf..86a9265 100644 --- a/sources/Text/Parser.uc +++ b/sources/Text/Parser.uc @@ -791,10 +791,10 @@ public final function Parser MUntil( * @return Returns the caller `Parser`, to allow for function chaining. */ public final function Parser MUntilS( - out string result, + out string result, optional Text.Character characterBreak, - optional bool whitespacesBreak, - optional bool quotesBreak) + optional bool whitespacesBreak, + optional bool quotesBreak) { local MutableText wrapper; if (!Ok()) return self; diff --git a/sources/Text/TextAPI.uc b/sources/Text/TextAPI.uc index ee819c1..27ba512 100644 --- a/sources/Text/TextAPI.uc +++ b/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` values into immutable `Text`. + * + * To create `MutableText` instead use `FromClassM()` method. + * + * @param value `class` value to be displayed as `Text`. + * @return Text representation of given `class` value. + */ +public final function Text FromClass(class value) +{ + return FromString(string(value)); +} + +/** + * Method for converting `class` values into mutable `MutableText`. + * + * To create `Text` instead use `FromClass()` method. + * + * @param value `class` value to be displayed as `MutableText`. + * @return Text representation of given `class` value. + */ +public final function MutableText FromClassM(class value) +{ + return FromStringM(string(value)); +} + /** * Method for converting `float` values into immutable `Text`. *