From 427d6fea58b152be69ae939ffbf1d655741819e3 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 31 Mar 2020 13:23:55 +0700 Subject: [PATCH 01/59] Add basic JSON data support Add classes to store data that can be transferred in a text JSON format. --- sources/Data/JSONArray.uc | 351 +++++++++++++++++++++++++++++++++++++ sources/Data/JSONBase.uc | 70 ++++++++ sources/Data/JSONObject.uc | 272 ++++++++++++++++++++++++++++ 3 files changed, 693 insertions(+) create mode 100644 sources/Data/JSONArray.uc create mode 100644 sources/Data/JSONBase.uc create mode 100644 sources/Data/JSONObject.uc diff --git a/sources/Data/JSONArray.uc b/sources/Data/JSONArray.uc new file mode 100644 index 0000000..d0e9e71 --- /dev/null +++ b/sources/Data/JSONArray.uc @@ -0,0 +1,351 @@ +/** + * This class implements JSON array storage capabilities. + * Array stores ordered JSON values that can be referred by their index. + * It can contain any mix of JSON value types and cannot have any gaps, + * i.e. in array of length N, there must be a valid value for all indices + * from 0 to N-1. + * 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 JSONArray extends JSONBase; + +// Data will simply be stored as an array of JSON values +var private array data; + +// Return type of value stored at a given index. +// Returns `JSON_Undefined` if and only if given index is out of bounds. +public final function JSONType GetType(int index) +{ + if (index < 0) return JSON_Undefined; + if (index >= data.length) return JSON_Undefined; + + return data[index].type; +} + +// Returns current length of this array. +public final function int GetLength() +{ + return data.length; +} + +// Changes length of this array. +// In case of the increase - fills new indices with `null` values. +public final function SetLength(int newLength) +{ + local int i; + local int oldLength; + oldLength = data.length; + data.length = newLength; + if (oldLength >= newLength) + { + return; + } + i = oldLength; + while (i < newLength) + { + SetNull(i); + i += 1; + } +} + +// Following functions are getters for various types of variables. +// Getter for null value simply checks if it's null +// and returns true/false as a result. +// Getters for simple types (number, string, boolean) can have optional +// default value specified, that will be returned if requested variable +// doesn't exist or has a different type. +// Getters for object and array types don't take default values and +// will simply return `none`. +public final function float GetNumber(int index, optional float defaultValue) +{ + if (index < 0) return defaultValue; + if (index >= data.length) return defaultValue; + if (data[index].type != JSON_Number) return defaultValue; + + return data[index].numberValue; +} + +public final function string GetString(int index, optional string defaultValue) +{ + if (index < 0) return defaultValue; + if (index >= data.length) return defaultValue; + if (data[index].type != JSON_String) return defaultValue; + + return data[index].stringValue; +} + +public final function bool GetBoolean(int index, optional bool defaultValue) +{ + if (index < 0) return defaultValue; + if (index >= data.length) return defaultValue; + if (data[index].type != JSON_Boolean) return defaultValue; + + return data[index].booleanValue; +} + +public final function bool IsNull(int index) +{ + if (index < 0) return false; + if (index >= data.length) return false; + + return (data[index].type == JSON_Null); +} + +public final function JSONArray GetArray(int index) +{ + if (index < 0) return none; + if (index >= data.length) return none; + if (data[index].type != JSON_Array) return none; + + return JSONArray(data[index].complexValue); +} + +public final function JSONObject GetObject(int index) +{ + if (index < 0) return none; + if (index >= data.length) return none; + if (data[index].type != JSON_Object) return none; + + return JSONObject(data[index].complexValue); +} + +// Following functions provide simple setters for boolean, string, number +// and null values. +// If passed index is negative - does nothing. +// If index lies beyond array length (`>= GetLength()`), - +// these functions will expand array in the same way as `GetLength()` function. +// This can be prevented by setting optional parameter `preventExpansion` to +// `false` (nothing will be done in this case). +// They return object itself, allowing user to chain calls like this: +// `array.SetNumber("num1", 1).SetNumber("num2", 2);`. +public final function JSONArray SetNumber +( + int index, + float value, + optional bool preventExpansion +) +{ + local JSONStorageValue newStorageValue; + if (index < 0) return self; + + if (index >= data.length) + { + if (preventExpansion) + { + return self; + } + else + { + SetLength(index + 1); + } + } + newStorageValue.type = JSON_Number; + newStorageValue.numberValue = value; + data[index] = newStorageValue; + return self; +} + +public final function JSONArray SetString +( + int index, + string value, + optional bool preventExpansion +) +{ + local JSONStorageValue newStorageValue; + if (index < 0) return self; + + if (index >= data.length) + { + if (preventExpansion) + { + return self; + } + else + { + SetLength(index + 1); + } + } + newStorageValue.type = JSON_String; + newStorageValue.stringValue = value; + data[index] = newStorageValue; + return self; +} + +public final function JSONArray SetBoolean +( + int index, + bool value, + optional bool preventExpansion +) +{ + local JSONStorageValue newStorageValue; + if (index < 0) return self; + + if (index >= data.length) + { + if (preventExpansion) + { + return self; + } + else + { + SetLength(index + 1); + } + } + newStorageValue.type = JSON_Boolean; + newStorageValue.booleanValue = value; + data[index] = newStorageValue; + return self; +} + +public final function JSONArray SetNull +( + int index, + optional bool preventExpansion +) +{ + local JSONStorageValue newStorageValue; + if (index < 0) return self; + + if (index >= data.length) + { + if (preventExpansion) + { + return self; + } + else + { + SetLength(index + 1); + } + } + newStorageValue.type = JSON_Null; + data[index] = newStorageValue; + return self; +} + +// JSON array and object types don't have setters, but instead have +// functions to create a new, empty array/object under a certain name. +// If passed index is negative - does nothing. +// If index lies beyond array length (`>= GetLength()`), - +// these functions will expand array in the same way as `GetLength()` function. +// This can be prevented by setting optional parameter `preventExpansion` to +// `false` (nothing will be done in this case). +// They return object itself, allowing user to chain calls like this: +// `array.CreateObject("sub object").CreateArray("sub array");`. +public final function JSONArray CreateArray +( + int index, + optional bool preventExpansion +) +{ + local JSONStorageValue newStorageValue; + if (index < 0) return self; + + if (index >= data.length) + { + if (preventExpansion) + { + return self; + } + else + { + SetLength(index + 1); + } + } + newStorageValue.type = JSON_Array; + newStorageValue.complexValue = new class'JSONArray'; + data[index] = newStorageValue; + return self; +} + +public final function JSONArray CreateObject +( + int index, + optional bool preventExpansion +) +{ + local JSONStorageValue newStorageValue; + if (index < 0) return self; + + if (index >= data.length) + { + if (preventExpansion) + { + return self; + } + else + { + SetLength(index + 1); + } + } + newStorageValue.type = JSON_Object; + newStorageValue.complexValue = new class'JSONObject'; + data[index] = newStorageValue; + return self; +} + +// Wrappers for setter functions that don't take index or +// `preventExpansion` parameters and add/create value at the end of the array. +public final function JSONArray AddNumber(float value) +{ + return SetNumber(data.length, value); +} + +public final function JSONArray AddString(string value) +{ + return SetString(data.length, value); +} + +public final function JSONArray AddBoolean(bool value) +{ + return SetBoolean(data.length, value); +} + +public final function JSONArray AddNull() +{ + return SetNull(data.length); +} + +public final function JSONArray AddArray() +{ + return CreateArray(data.length); +} + +public final function JSONArray AddObject() +{ + return CreateObject(data.length); +} + +// Removes up to `amount` of values, starting from a given index. +// If `index` falls outside array boundaries - nothing will be done. +// Returns `true` if value was actually removed and `false` if it didn't exist. +public final function bool RemoveValue(int index, optional int amount) +{ + if (index < 0) return false; + if (index >= data.length) return false; + if (amount < 1) return false; + + amount = Max(amount, 1); + amount = Min(amount, data.length - index); + data.Remove(index, amount); + return true; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Data/JSONBase.uc b/sources/Data/JSONBase.uc new file mode 100644 index 0000000..60e6196 --- /dev/null +++ b/sources/Data/JSONBase.uc @@ -0,0 +1,70 @@ +/** + * JSON is an open standard file format, and data interchange format, + * that uses human-readable text to store and transmit data objects + * consisting of name–value pairs and array data types. + * For more information refer to https://en.wikipedia.org/wiki/JSON + * This is a base class for implementation of JSON data storage for Acedia. + * It does not implement parsing and printing from/into human-readable + * text representation, just provides means to store such information. + * + * JSON data is stored as an object (represented via `JSONObject`) that + * contains a set of name-value pairs, where value can be + * a number, string, boolean value, another object or + * an array (represented by `JSONArray`). + * 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 JSONBase extends Object; + +// Enumeration for possible types of JSON values. +enum JSONType +{ + // Technical type, used to indicate that requested value is missing. + // Undefined values are not part of JSON format. + JSON_Undefined, + // An empty value, in teste representation defined by a single word "null". + JSON_Null, + // A number, recorded as a float. + // JSON itself doesn't specify whether number is an integer or float. + JSON_Number, + // A string. + JSON_String, + // A bool value. + JSON_Boolean, + // Array of other JSON values, stored without names; + // Single array can contain any mix of value types. + JSON_Array, + // Another JSON object, i.e. associative array of name-value pairs + JSON_Object +}; + +// Stores a single JSON value +struct JSONStorageValue +{ + // What type is stored exactly? + // Depending on that, uses one of the other fields as a storage. + var public JSONType type; + var protected float numberValue; + var protected string stringValue; + var protected bool booleanValue; + // Used for storing both JSON objects and arrays. + var protected JSONBase complexValue; +}; + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Data/JSONObject.uc b/sources/Data/JSONObject.uc new file mode 100644 index 0000000..6e1667e --- /dev/null +++ b/sources/Data/JSONObject.uc @@ -0,0 +1,272 @@ +/** + * This class implements JSON object storage capabilities. + * Whenever one wants to store JSON data, they need to define such object. + * It stores name-value pairs, where names are strings and values can be: + * ~ Boolean, string, null or number (float in this implementation) data; + * ~ Other JSON objects; + * ~ JSON Arrays (see `JSONArray` class). + * + * This implementation provides getters and setters for boolean, string, + * null or number types that allow to freely set and fetch their values + * by name. + * JSON objects and arrays can be fetched by getters, but you cannot + * add existing object or array to another object. Instead one has to create + * a new, empty object with a certain name and then fill it with data. + * This allows to avoid loop situations, where object is contained in itself. + * Functions to remove existing values are also provided and are applicable + * to all variable types. + * Setters can also be used to overwrite any value by a different value, + * even of a different type. + * 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 JSONObject extends JSONBase; + +// We will store all our data as a simple array of key(name)-value pairs. +struct JSONKeyValuePair +{ + var string key; + var JSONStorageValue value; +}; +var private array data; + +// Returns index of key-value pair in `data` with a given key. +// Returns `-1` if such a pair does not exist in `data`. +private final function int GetIndex(string key) +{ + local int i; + for (i = 0; i < data.length; i += 1) + { + if (key == data[i].key) + { + return i; + } + } + return -1; +} + +// Returns `JSONType` of a variable with a given key in our data. +// This function can be used to check if certain variable exists +// in this object, since if such variable does not exist - +// function will return `JSON_Undefined`. +public final function JSONType GetType(string key) +{ + local int index; + index = GetIndex(key); + if (index < 0) return JSON_Undefined; + + return data[index].value.type; +} + +// Following functions are getters for various types of variables. +// Getter for null value simply checks if it's null +// and returns true/false as a result. +// Getters for simple types (number, string, boolean) can have optional +// default value specified, that will be returned if requested variable +// doesn't exist or has a different type. +// Getters for object and array types don't take default values and +// will simply return `none`. +public final function float GetNumber(string key, optional float defaultValue) +{ + local int index; + index = GetIndex(key); + if (index < 0) return defaultValue; + if (data[index].value.type != JSON_Number) return defaultValue; + + return data[index].value.numberValue; +} + +public final function string GetString(string key, optional string defaultValue) +{ + local int index; + index = GetIndex(key); + if (index < 0) return defaultValue; + if (data[index].value.type != JSON_String) return defaultValue; + + return data[index].value.stringValue; +} + +public final function bool GetBoolean(string key, optional bool defaultValue) +{ + local int index; + index = GetIndex(key); + if (index < 0) return defaultValue; + if (data[index].value.type != JSON_Boolean) return defaultValue; + + return data[index].value.booleanValue; +} + +public final function bool IsNull(string key) +{ + local int index; + index = GetIndex(key); + if (index < 0) return false; + if (data[index].value.type != JSON_Null) return false; + + return (data[index].value.type == JSON_Null); +} + +public final function JSONArray GetArray(string key) +{ + local int index; + index = GetIndex(key); + if (index < 0) return none; + if (data[index].value.type != JSON_Array) return none; + + return JSONArray(data[index].value.complexValue); +} + +public final function JSONObject GetObject(string key) +{ + local int index; + index = GetIndex(key); + if (index < 0) return none; + if (data[index].value.type != JSON_Object) return none; + + return JSONObject(data[index].value.complexValue); +} + +// Following functions provide simple setters for boolean, string, number +// and null values. +// They return object itself, allowing user to chain calls like this: +// `object.SetNumber("num1", 1).SetNumber("num2", 2);`. +public final function JSONObject SetNumber(string key, float value) +{ + local int index; + local JSONKeyValuePair newKeyValuePair; + local JSONStorageValue newStorageValue; + index = GetIndex(key); + if (index < 0) + { + index = data.length; + } + newStorageValue.type = JSON_Number; + newStorageValue.numberValue = value; + newKeyValuePair.key = key; + newKeyValuePair.value = newStorageValue; + data[index] = newKeyValuePair; + return self; +} + +public final function JSONObject SetString(string key, string value) +{ + local int index; + local JSONKeyValuePair newKeyValuePair; + local JSONStorageValue newStorageValue; + index = GetIndex(key); + if (index < 0) + { + index = data.length; + } + newStorageValue.type = JSON_String; + newStorageValue.stringValue = value; + newKeyValuePair.key = key; + newKeyValuePair.value = newStorageValue; + data[index] = newKeyValuePair; + return self; +} + +public final function JSONObject SetBoolean(string key, bool value) +{ + local int index; + local JSONKeyValuePair newKeyValuePair; + local JSONStorageValue newStorageValue; + index = GetIndex(key); + if (index < 0) + { + index = data.length; + } + newStorageValue.type = JSON_Boolean; + newStorageValue.booleanValue = value; + newKeyValuePair.key = key; + newKeyValuePair.value = newStorageValue; + data[index] = newKeyValuePair; + return self; +} + +public final function JSONObject SetNull(string key) +{ + local int index; + local JSONKeyValuePair newKeyValuePair; + local JSONStorageValue newStorageValue; + index = GetIndex(key); + if (index < 0) + { + index = data.length; + } + newStorageValue.type = JSON_Null; + newKeyValuePair.key = key; + newKeyValuePair.value = newStorageValue; + data[index] = newKeyValuePair; + return self; +} + +// JSON array and object types don't have setters, but instead have +// functions to create a new, empty array/object under a certain name. +// They return object itself, allowing user to chain calls like this: +// `object.CreateObject("folded object").CreateArray("names list");`. +public final function JSONObject CreateArray(string key) +{ + local int index; + local JSONKeyValuePair newKeyValuePair; + local JSONStorageValue newStorageValue; + index = GetIndex(key); + if (index < 0) + { + index = data.length; + } + newStorageValue.type = JSON_Array; + newStorageValue.complexValue = new class'JSONArray'; + newKeyValuePair.key = key; + newKeyValuePair.value = newStorageValue; + data[index] = newKeyValuePair; + return self; +} + +public final function JSONObject CreateObject(string key) +{ + local int index; + local JSONKeyValuePair newKeyValuePair; + local JSONStorageValue newStorageValue; + index = GetIndex(key); + if (index < 0) + { + index = data.length; + } + newStorageValue.type = JSON_Object; + newStorageValue.complexValue = new class'JSONObject'; + newKeyValuePair.key = key; + newKeyValuePair.value = newStorageValue; + data[index] = newKeyValuePair; + return self; +} + +// Removes values with a given name. +// Returns `true` if value was actually removed and `false` if it didn't exist. +public final function bool RemoveValue(string key) +{ + local int index; + index = GetIndex(key); + if (index < 0) return false; + + data.Remove(index, 1); + return true; +} + +defaultproperties +{ +} \ No newline at end of file From a4a1c21cd7942fc820cde238c381c78292deebc9 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 31 Mar 2020 13:28:20 +0700 Subject: [PATCH 02/59] Add basic unit test support Add class that can automatically perform defined tests on user request. Developers that wish to implemet unit tests for some functionality must extend that class (`TestCase`) and add it to the manifest, so that Acedia can read, register and later use it to perform tests. --- sources/Acedia.uc | 9 ++ sources/Manifest.uc | 3 + sources/TestCase.uc | 252 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 sources/TestCase.uc diff --git a/sources/Acedia.uc b/sources/Acedia.uc index fe3cde6..3a7fe12 100644 --- a/sources/Acedia.uc +++ b/sources/Acedia.uc @@ -31,6 +31,9 @@ var private Acedia selfReference; // Array of predefined services that must be started along with Acedia mutator. var private array< class > systemServices; +// All unit tests loaded from all packages. +var private array< class > testCases; + static public final function Acedia GetInstance() { return default.selfReference; @@ -69,6 +72,12 @@ private final function LoadManifest(class manifestClass) manifestClass.default.features[i].static.EnableMe(); } } + // Load unit tests + for (i = 0; i < manifestClass.default.testCases.length; i += 1) + { + if (manifestClass.default.testCases[i] == none) continue; + testCases[testCases.length] = manifestClass.default.testCases[i]; + } } private final function LaunchServices() diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 535e92a..26b21ca 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -26,6 +26,9 @@ // List of features in this manifest's package. var public const array< class > features; +// List of features in this manifest's package. +var public const array< class > testCases; + // Listeners listed here will be automatically activated. var public const array< class > requiredListeners; diff --git a/sources/TestCase.uc b/sources/TestCase.uc new file mode 100644 index 0000000..2512e30 --- /dev/null +++ b/sources/TestCase.uc @@ -0,0 +1,252 @@ +/** + * Base class aimed to contain sets of unit tests for various components of + * Acedia and it's features. + * Currently provides bare-bones testing functions that check boolean + * variables for true/false and objects for whether they're `none` or not. + * Tests: + * ~ can be grouped by their "context", + * describing what they are testing; + * ~ test (or several tests) can be assigned an error message, + * describing what exactly went wrong. + * 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 TestCase extends Actor + abstract; + +// Name by which this set of unit tests can be referred to. +var protected const string caseName; + +// Information about how well testing went for a particular context, +// i.e. subsets of tests for a particular functionality. +struct ContextSummary +{ + // Text, human-readable description of the purpose of + // tests in this context. + var string description; + // `false` if at least one test failed, `true` otherwise. + var bool passed; + // How many test were performed. + var int testsPerformed; + // How many tests failed. + var int testsFailed; + // Error messages generated by failed tests. + var array errors; +}; + +// Collection of summaries for all contexts defined by the user so far. +struct Summary +{ + var bool passed; + var array contextSummaries; +}; + +// Has function for defining context (`Context()`) been called. +var private bool userDefinedContext; +// Were all tests performed? +var private bool finishedTests; +// Error message that will be generated if some test will fail now. +var private string currentErrorMessage; + +// Store complete summary here. +var private Summary currentSummary; +// For quick access store current context's summary here and update it in +// `currentSummary` once done. +var private ContextSummary activeContextSummary; + +// Call this function to define a context for subsequent test +// (until another call). +public final static function Context(string description) +{ + if ( default.userDefinedContext + || default.activeContextSummary.testsPerformed > 0) + { + UpdateContextSummary(default.activeContextSummary); + } + default.userDefinedContext = true; + default.activeContextSummary = GetContextSummary(description); + default.currentErrorMessage = ""; +} + +// Call this function to define an error message for tests that +// would fail after it. +// Message is reset by another call of `Issue()` or +// by changing the context via `Context()`. +public final static function Issue(string errorMessage) +{ + default.currentErrorMessage = errorMessage; +} + +// All tests to be performed can be placed in this function, +// along with appropriate calls to `Context()` and `Issue()`. +// For an example see class `TEST_JSON`. +protected static function TESTS(){} + +// Following functions provide simple test primitives, +public final static function TEST_ExpectTrue(bool result) +{ + RecordTestResult(result, default.currentErrorMessage); +} + +public final static function TEST_ExpectFalse(bool result) +{ + RecordTestResult(!result, default.currentErrorMessage); +} + +public final static function TEST_ExpectNone(Object object) +{ + RecordTestResult(object == none, default.currentErrorMessage); +} + +public final static function TEST_ExpectNotNone(Object object) +{ + RecordTestResult(object != none, default.currentErrorMessage); +} + +// Returns the summary of how testing went. +public final static function Summary GetSummary() +{ + return default.currentSummary; +} + +// Name by which this set of unit tests can be referred to. +public final static function string GetName() +{ + return default.caseName; +} + +// Creates brand new summary for context with a given description, +// marked as "passed" and zero tests done. +private final static function ContextSummary NewContextSummary +( + string description +) +{ + local ContextSummary newSummary; + newSummary.passed = true; + newSummary.description = description; + newSummary.testsPerformed = 0; + newSummary.testsFailed = 0; + newSummary.errors.length = 0; + return newSummary; +} + +// Returns index of summary with given description +// in our records (`currentSummary`). +// Return `-1` if there is no context with such description. +private final static function int GetContextSummaryIndex(string description) +{ + local int i; + for (i = 0; i < default.currentSummary.contextSummaries.length; i += 1) + { + if ( default.currentSummary.contextSummaries[i].description + ~= description) + { + return i; + } + } + return -1; +} +// Returns index summary with given description +// in our records. +// Return new context summary if there is no context with such description. +private final static function ContextSummary GetContextSummary +( + string description +) +{ + local int index; + if (default.activeContextSummary.description ~= description) + { + return default.activeContextSummary; + } + index = GetContextSummaryIndex(description); + if (index < 0) + { + return NewContextSummary(description); + } + + return default.currentSummary.contextSummaries[index]; +} + +// Rewrites summary (with the same name as a given summary) +// in `currentSummary` records. +// If there's no such record - adds a new one. +private final static function UpdateContextSummary +( + ContextSummary relevantSummary +) +{ + local int index; + index = GetContextSummaryIndex(relevantSummary.description); + if (index < 0) + { + index = default.currentSummary.contextSummaries.length; + } + default.currentSummary.contextSummaries[index] = relevantSummary; +} + +// Records (in current context summary) that another test was performed and +// succeeded/failed, along with given error message. +private final static function RecordTestResult +( + bool isSuccessful, + string errorMessage +) +{ + local int i; + local int errorsAmount; + if (default.finishedTests) return; + default.activeContextSummary.testsPerformed += 1; + if (isSuccessful) return; + + default.currentSummary.passed = false; + default.activeContextSummary.passed = false; + default.activeContextSummary.testsFailed += 1; + errorsAmount = default.activeContextSummary.errors.length; + for (i = 0; i < errorsAmount; i += 1) + { + if (default.activeContextSummary.errors[i] ~= errorMessage) + { + return; + } + } + default.activeContextSummary.errors[errorsAmount] = errorMessage; +} + +// Calling this function will perform unit tests defined in `TESTS()` +// function of this test case and will prepare the summary, +// obtainable through `GetSummary()` function. +// Returns `true` if all tests have successfully passed +// and `false` otherwise. +public final static function bool PerformTests() +{ + default.finishedTests = false; + default.userDefinedContext = false; + default.currentSummary.passed = true; + default.currentSummary.contextSummaries.length = 0; + default.activeContextSummary = NewContextSummary(""); + TESTS(); + UpdateContextSummary(default.activeContextSummary); + default.finishedTests = true; + return default.currentSummary.passed; +} + +defaultproperties +{ + caseName = "" +} \ No newline at end of file From dd45e692fc994c7e6a72ce3991a652ae91e66995 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 31 Mar 2020 13:29:11 +0700 Subject: [PATCH 03/59] Add unit tests for JSON implementation --- sources/Data/TEST_JSON.uc | 710 ++++++++++++++++++++++++++++++++++++++ sources/Manifest.uc | 2 + 2 files changed, 712 insertions(+) create mode 100644 sources/Data/TEST_JSON.uc diff --git a/sources/Data/TEST_JSON.uc b/sources/Data/TEST_JSON.uc new file mode 100644 index 0000000..64da167 --- /dev/null +++ b/sources/Data/TEST_JSON.uc @@ -0,0 +1,710 @@ +/** + * Set of tests for JSON data storage, implemented via + * `JSONObject` and `JSONArray`. + * 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 TEST_JSON extends TestCase + abstract; + +protected static function TESTS() +{ + local JSONObject jsonData; + jsonData = new class'JSONObject'; + Test_ObjectGetSetRemove(); + Test_ArrayGetSetRemove(); +} + +protected static function Test_ObjectGetSetRemove() +{ + SubTest_Undefined(); + SubTest_StringGetSetRemove(); + SubTest_BooleanGetSetRemove(); + SubTest_NumberGetSetRemove(); + SubTest_NullGetSetRemove(); + SubTest_MultipleVariablesGetSet(); + SubTest_Object(); +} + +protected static function Test_ArrayGetSetRemove() +{ + Context("Testing get/set/remove functions for JSON arrays"); + SubTest_ArrayUndefined(); + SubTest_ArrayStringGetSetRemove(); + SubTest_ArrayBooleanGetSetRemove(); + SubTest_ArrayNumberGetSetRemove(); + SubTest_ArrayNullGetSetRemove(); + SubTest_ArrayMultipleVariablesStorage(); + SubTest_ArrayMultipleVariablesRemoval(); + SubTest_ArrayRemovingMultipleVariablesAtOnce(); + SubTest_ArrayExpansions(); +} + +protected static function SubTest_Undefined() +{ + local JSONObject testJSON; + testJSON = new class'JSONObject'; + Context("Testing how `JSONObject` handles undefined values"); + Issue("Undefined variable doesn't have proper type."); + TEST_ExpectTrue(testJSON.GetType("some_var") == JSON_Undefined); + + Issue("There is a variable in an empty object after `GetType` call."); + TEST_ExpectTrue(testJSON.GetType("some_var") == JSON_Undefined); + + Issue("Getters don't return default values for undefined variables."); + TEST_ExpectTrue(testJSON.GetNumber("some_var", 0) == 0); + TEST_ExpectTrue(testJSON.GetString("some_var", "") == ""); + TEST_ExpectTrue(testJSON.GetBoolean("some_var", false) == false); + TEST_ExpectNone(testJSON.GetObject("some_var")); + TEST_ExpectNone(testJSON.GetArray("some_var")); +} + +protected static function SubTest_BooleanGetSetRemove() +{ + local JSONObject testJSON; + testJSON = new class'JSONObject'; + testJSON.SetBoolean("some_boolean", true); + + Context("Testing `JSONObject`'s get/set/remove functions for" @ + "boolean variables"); + Issue("Boolean type isn't properly set by `SetBoolean`"); + TEST_ExpectTrue(testJSON.GetType("some_boolean") == JSON_Boolean); + + Issue("Variable value is incorrectly assigned by `SetBoolean`"); + TEST_ExpectTrue(testJSON.GetBoolean("some_boolean") == true); + + Issue("Variable value isn't correctly reassigned by `SetBoolean`"); + testJSON.SetBoolean("some_boolean", false); + TEST_ExpectTrue(testJSON.GetBoolean("some_boolean") == false); + + Issue( "Getting boolean variable as a wrong type" @ + "doesn't yield default value"); + TEST_ExpectTrue(testJSON.GetNumber("some_boolean", 7) == 7); + + Issue("Boolean variable isn't being properly removed"); + testJSON.RemoveValue("some_boolean"); + TEST_ExpectTrue(testJSON.GetType("some_boolean") == JSON_Undefined); + + Issue( "Getters don't return default value for missing key that" @ + "previously stored boolean value, that got removed"); + TEST_ExpectTrue(testJSON.GetBoolean("some_boolean", true) == true); +} + +protected static function SubTest_StringGetSetRemove() +{ + local JSONObject testJSON; + testJSON = new class'JSONObject'; + testJSON.SetString("some_string", "first string"); + + Context("Testing `JSONObject`'s get/set/remove functions for" @ + "string variables"); + Issue("String type isn't properly set by `SetString`"); + TEST_ExpectTrue(testJSON.GetType("some_string") == JSON_String); + + Issue("Value is incorrectly assigned by `SetString`"); + TEST_ExpectTrue(testJSON.GetString("some_string") == "first string"); + + Issue( "Providing default variable value makes 'GetString'" @ + "return wrong value"); + TEST_ExpectTrue( testJSON.GetString("some_string", "alternative") + == "first string"); + + Issue("Variable value isn't correctly reassigned by `SetString`"); + testJSON.SetString("some_string", "new string!~"); + TEST_ExpectTrue(testJSON.GetString("some_string") == "new string!~"); + + Issue( "Getting string variable as a wrong type" @ + "doesn't yield default value"); + TEST_ExpectTrue(testJSON.GetBoolean("some_string", true) == true); + + Issue("String variable isn't being properly removed"); + testJSON.RemoveValue("some_string"); + TEST_ExpectTrue(testJSON.GetType("some_string") == JSON_Undefined); + + Issue( "Getters don't return default value for missing key that" @ + "previously stored string value, but got removed"); + TEST_ExpectTrue(testJSON.GetString("some_string", "other") == "other"); +} + +protected static function SubTest_NumberGetSetRemove() +{ + local JSONObject testJSON; + testJSON = new class'JSONObject'; + testJSON.SetNumber("some_number", 3.5); + + Context("Testing `JSONObject`'s get/set/remove functions for" @ + "number variables"); + Issue("Number type isn't properly set by `SetNumber`"); + TEST_ExpectTrue(testJSON.GetType("some_number") == JSON_Number); + + Issue("Value is incorrectly assigned by `SetNumber`"); + TEST_ExpectTrue(testJSON.GetNumber("some_number") == 3.5); + + Issue( "Providing default variable value makes 'GetNumber'" @ + "return wrong value"); + TEST_ExpectTrue(testJSON.GetNumber("some_number", 5) == 3.5); + + Issue("Variable value isn't correctly reassigned by `SetNumber`"); + testJSON.SetNumber("some_number", 7); + TEST_ExpectTrue(testJSON.GetNumber("some_number") == 7); + + Issue( "Getting number variable as a wrong type" @ + "doesn't yield default value."); + TEST_ExpectTrue(testJSON.GetString("some_number", "default") == "default"); + + Issue("Number type isn't being properly removed"); + testJSON.RemoveValue("some_number"); + TEST_ExpectTrue(testJSON.GetType("some_number") == JSON_Undefined); + + Issue( "Getters don't return default value for missing key that" @ + "previously stored number value, that got removed"); + TEST_ExpectTrue(testJSON.GetNumber("some_number", 13) == 13); +} + +protected static function SubTest_NullGetSetRemove() +{ + local JSONObject testJSON; + testJSON = new class'JSONObject'; + + Context("Testing `JSONObject`'s get/set/remove functions for" @ + "null values"); + Issue("Undefined variable is incorrectly considered `null`"); + TEST_ExpectFalse(testJSON.IsNull("some_var")); + + Issue("Number variable is incorrectly considered `null`"); + testJSON.SetNumber("some_var", 4); + TEST_ExpectFalse(testJSON.IsNull("some_var")); + + Issue("Boolean variable is incorrectly considered `null`"); + testJSON.SetBoolean("some_var", true); + TEST_ExpectFalse(testJSON.IsNull("some_var")); + + Issue("String variable is incorrectly considered `null`"); + testJSON.SetString("some_var", "string"); + TEST_ExpectFalse(testJSON.IsNull("some_var")); + + Issue("Null value is incorrectly assigned"); + testJSON.SetNull("some_var"); + TEST_ExpectTrue(testJSON.IsNull("some_var")); + + Issue("Null type isn't properly set by `SetNumber`"); + TEST_ExpectTrue(testJSON.GetType("some_var") == JSON_Null); + + Issue("Null value isn't being properly removed."); + testJSON.RemoveValue("some_var"); + TEST_ExpectTrue(testJSON.GetType("some_var") == JSON_Undefined); +} + +protected static function SubTest_MultipleVariablesGetSet() +{ + local int i; + local bool correctValue, allValuesCorrect; + local JSONObject testJSON; + testJSON = new class'JSONObject'; + Context("Testing how `JSONObject` handles addition, change and removal" @ + "of relatively large (hundreds) number of variables"); + for (i = 0;i < 2000; i += 1) + { + testJSON.SetNumber("num" $ string(i), 4 * i*i - 2.6 * i + 0.75); + } + for (i = 0;i < 500; i += 1) + { + testJSON.SetString("num" $ string(i), "str" $ string(Sin(i))); + } + for (i = 1500;i < 2000; i += 1) + { + testJSON.RemoveValue("num" $ string(i)); + } + allValuesCorrect = true; + for (i = 0;i < 200; i += 1) + { + if (i < 500) + { + correctValue = ( testJSON.GetString("num" $ string(i)) + == ("str" $ string(Sin(i))) ); + Issue("Variables are incorrectly overwritten"); + } + else if(i < 1500) + { + correctValue = ( testJSON.GetNumber("num" $ string(i)) + == 4 * i*i - 2.6 * i + 0.75); + Issue("Variables are lost"); + } + else + { + correctValue = ( testJSON.GetType("num" $ string(i)) + == JSON_Undefined); + Issue("Variables aren't removed"); + } + if (!correctValue) + { + allValuesCorrect = false; + break; + } + } + TEST_ExpectTrue(allValuesCorrect); +} + +protected static function SubTest_Object() +{ + local JSONObject testObject; + Context("Testing setters and getters for folded objects"); + testObject = new class'JSONObject'; + testObject.CreateObject("folded"); + testObject.GetObject("folded").CreateObject("folded"); + testObject.SetString("out", "string outside"); + testObject.GetObject("folded").SetNumber("mid", 8); + testObject.GetObject("folded") + .GetObject("folded") + .SetString("in", "string inside"); + + Issue("Addressing variables in root object doesn't work"); + TEST_ExpectTrue(testObject.GetString("out", "default") == "string outside"); + + Issue("Addressing variables in folded object doesn't work"); + TEST_ExpectTrue(testObject.GetObject("folded").GetNumber("mid", 1) == 8); + + Issue("Addressing plain variables in folded (twice) object doesn't work"); + TEST_ExpectTrue(testObject.GetObject("folded").GetObject("folded") + .GetString("in", "default") == "string inside"); +} + +protected static function SubTest_ArrayUndefined() +{ + local JSONArray testJSON; + testJSON = new class'JSONArray'; + Context("Testing how `JSONArray` handles undefined values"); + Issue("Undefined variable doesn't have `JSON_Undefined` type"); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + + Issue("There is a variable in an empty object after `GetType` call"); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + + Issue("Negative index refers to a defined value"); + TEST_ExpectTrue(testJSON.GetType(-1) == JSON_Undefined); + + Issue("Getters don't return default values for undefined variables"); + TEST_ExpectTrue(testJSON.GetNumber(0, 0) == 0); + TEST_ExpectTrue(testJSON.GetString(0, "") == ""); + TEST_ExpectTrue(testJSON.GetBoolean(0, false) == false); + TEST_ExpectNone(testJSON.GetObject(0)); + TEST_ExpectNone(testJSON.GetArray(0)); + + Issue( "Getters don't return user-defined default values for" @ + "undefined variables"); + TEST_ExpectTrue(testJSON.GetNumber(0, 10) == 10); + TEST_ExpectTrue(testJSON.GetString(0, "test") == "test"); + TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true); +} + +protected static function SubTest_ArrayBooleanGetSetRemove() +{ + local JSONArray testJSON; + testJSON = new class'JSONArray'; + testJSON.SetBoolean(0, true); + + Context("Testing `JSONArray`'s get/set/remove functions for" @ + "boolean variables"); + Issue("Boolean type isn't properly set by `SetBoolean`"); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_Boolean); + + Issue("Value is incorrectly assigned by `SetBoolean`"); + TEST_ExpectTrue(testJSON.GetBoolean(0) == true); + testJSON.SetBoolean(0, false); + + Issue("Variable value isn't correctly reassigned by `SetBoolean`"); + TEST_ExpectTrue(testJSON.GetBoolean(0) == false); + + Issue( "Getting boolean variable as a wrong type" @ + "doesn't yield default value"); + TEST_ExpectTrue(testJSON.GetNumber(0, 7) == 7); + + Issue("Boolean variable isn't being properly removed"); + testJSON.RemoveValue(0); + TEST_ExpectTrue( testJSON.GetType(0) == JSON_Undefined); + + Issue( "Getters don't return default value for missing key that" @ + "previously stored boolean value, but got removed"); + TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true); +} + +protected static function SubTest_ArrayStringGetSetRemove() +{ + local JSONArray testJSON; + testJSON = new class'JSONArray'; + testJSON.SetString(0, "first string"); + + Context("Testing `JSONArray`'s get/set/remove functions for" @ + "string variables"); + Issue("String type isn't properly set by `SetString`"); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_String); + + Issue("Value is incorrectly assigned by `SetString`"); + TEST_ExpectTrue(testJSON.GetString(0) == "first string"); + + Issue( "Providing default variable value makes 'GetString'" @ + "return incorrect value"); + TEST_ExpectTrue(testJSON.GetString(0, "alternative") == "first string"); + + Issue("Variable value isn't correctly reassigned by `SetString`"); + testJSON.SetString(0, "new string!~"); + TEST_ExpectTrue(testJSON.GetString(0) == "new string!~"); + + Issue( "Getting string variable as a wrong type" @ + "doesn't yield default value"); + TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true); + + Issue("Boolean variable isn't being properly removed"); + testJSON.RemoveValue(0); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + + Issue( "Getters don't return default value for missing key that" @ + "previously stored string value, but got removed"); + TEST_ExpectTrue(testJSON.GetString(0, "other") == "other"); +} + +protected static function SubTest_ArrayNumberGetSetRemove() +{ + local JSONArray testJSON; + testJSON = new class'JSONArray'; + testJSON.SetNumber(0, 3.5); + + Context("Testing `JSONArray`'s get/set/remove functions for" @ + "number variables"); + Issue("Number type isn't properly set by `SetNumber`"); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_Number); + + Issue("Value is incorrectly assigned by `SetNumber`"); + TEST_ExpectTrue(testJSON.GetNumber(0) == 3.5); + + Issue( "Providing default variable value makes 'GetNumber'" @ + "return incorrect value"); + TEST_ExpectTrue(testJSON.GetNumber(0, 5) == 3.5); + + Issue("Variable value isn't correctly reassigned by `SetNumber`"); + testJSON.SetNumber(0, 7); + TEST_ExpectTrue(testJSON.GetNumber(0) == 7); + + Issue( "Getting number variable as a wrong type" @ + "doesn't yield default value"); + TEST_ExpectTrue(testJSON.GetString(0, "default") == "default"); + + Issue("Number type isn't being properly removed"); + testJSON.RemoveValue(0); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + + Issue( "Getters don't return default value for missing key that" @ + "previously stored number value, but got removed"); + TEST_ExpectTrue(testJSON.GetNumber(0, 13) == 13); +} + +protected static function SubTest_ArrayNullGetSetRemove() +{ + local JSONArray testJSON; + testJSON = new class'JSONArray'; + + Context("Testing `JSONArray`'s get/set/remove functions for" @ + "null values"); + + Issue("Undefined variable is incorrectly considered `null`"); + TEST_ExpectFalse(testJSON.IsNull(0)); + TEST_ExpectFalse(testJSON.IsNull(2)); + TEST_ExpectFalse(testJSON.IsNull(-1)); + + Issue("Number variable is incorrectly considered `null`"); + testJSON.SetNumber(0, 4); + TEST_ExpectFalse(testJSON.IsNull(0)); + + Issue("Boolean variable is incorrectly considered `null`"); + testJSON.SetBoolean(0, true); + TEST_ExpectFalse(testJSON.IsNull(0)); + + Issue("String variable is incorrectly considered `null`"); + testJSON.SetString(0, "string"); + TEST_ExpectFalse(testJSON.IsNull(0)); + + Issue("Null value is incorrectly assigned"); + testJSON.SetNull(0); + TEST_ExpectTrue(testJSON.IsNull(0)); + + Issue("Null type isn't properly set by `SetNumber`"); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_Null); + + Issue("Null value isn't being properly removed"); + testJSON.RemoveValue(0); + TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); +} + +// Returns following array: +// [10.0, "test string", "another string", true, 0.0, {"var": 7.0}] +protected static function JSONArray Prepare_Array() +{ + local JSONArray testArray; + testArray = new class'JSONArray'; + testArray.AddNumber(10.0f) + .AddString("test string") + .AddString("another string") + .AddBoolean(true) + .AddNumber(0.0f) + .AddObject(); + testArray.GetObject(5).SetNumber("var", 7); + return testArray; +} + +protected static function SubTest_ArrayMultipleVariablesStorage() +{ + local JSONArray testArray; + testArray = Prepare_Array(); + + Context("Testing how `JSONArray` handles adding and" @ + "changing several variables"); + Issue("Stored values are compromised."); + TEST_ExpectTrue(testArray.GetNumber(0) == 10.0f); + TEST_ExpectTrue(testArray.GetString(1) == "test string"); + TEST_ExpectTrue(testArray.GetString(2) == "another string"); + TEST_ExpectTrue(testArray.GetBoolean(3) == true); + TEST_ExpectTrue(testArray.GetNumber(4) == 0.0f); + TEST_ExpectTrue(testArray.GetObject(5).GetNumber("var") == 7); + + Issue("Values incorrectly change their values."); + testArray.SetString(3, "new string"); + TEST_ExpectTrue(testArray.GetString(3) == "new string"); + + Issue( "After overwriting boolean value with a different type," @ + "attempting go get it as a boolean gives old value," @ + "instead of default"); + TEST_ExpectTrue(testArray.GetBoolean(3, false) == false); + + Issue("Type of the variable is incorrectly changed."); + TEST_ExpectTrue(testArray.GetType(3) == JSON_String); +} + +protected static function SubTest_ArrayMultipleVariablesRemoval() +{ + local JSONArray testArray; + testArray = Prepare_Array(); + // Test removing variables + // After `Prepare_Array`, our array should be: + // [10.0, "test string", "another string", true, 0.0, {"var": 7.0}] + + Context("Testing how `JSONArray` handles adding and" @ + "removing several variables"); + Issue("Values are incorrectly removed"); + testArray.RemoveValue(2); + // [10.0, "test string", true, 0.0, {"var": 7.0}] + Issue("Values are incorrectly removed"); + TEST_ExpectTrue(testArray.GetNumber(0) == 10.0); + TEST_ExpectTrue(testArray.GetString(1) == "test string"); + TEST_ExpectTrue(testArray.GetBoolean(2) == true); + TEST_ExpectTrue(testArray.GetNumber(3) == 0.0f); + TEST_ExpectTrue(testArray.GetType(4) == JSON_Object); + + Issue("First element incorrectly removed"); + testArray.RemoveValue(0); + // ["test string", true, 0.0, {"var": 7.0}] + TEST_ExpectTrue(testArray.GetString(0) == "test string"); + TEST_ExpectTrue(testArray.GetBoolean(1) == true); + TEST_ExpectTrue(testArray.GetNumber(2) == 0.0f); + TEST_ExpectTrue(testArray.GetType(3) == JSON_Object); + TEST_ExpectTrue(testArray.GetObject(3).GetNumber("var") == 7.0); + + Issue("Last element incorrectly removed"); + testArray.RemoveValue(3); + // ["test string", true, 0.0] + TEST_ExpectTrue(testArray.GetLength() == 3); + TEST_ExpectTrue(testArray.GetString(0) == "test string"); + TEST_ExpectTrue(testArray.GetBoolean(1) == true); + TEST_ExpectTrue(testArray.GetNumber(2) == 0.0f); + + Issue("Removing all elements is handled incorrectly"); + testArray.RemoveValue(0); + testArray.RemoveValue(0); + testArray.RemoveValue(0); + TEST_ExpectTrue(testArray.Getlength() == 0); + TEST_ExpectTrue(testArray.GetType(0) == JSON_Undefined); +} + +protected static function SubTest_ArrayRemovingMultipleVariablesAtOnce() +{ + local JSONArray testArray; + testArray = new class'JSONArray'; + testArray.AddNumber(10.0f) + .AddString("test string") + .AddString("another string") + .AddNumber(7.0); + + Context("Testing how `JSONArray`' handles removing" @ + "multiple elements at once"); + Issue("Multiple values are incorrectly removed"); + testArray.RemoveValue(1, 2); + TEST_ExpectTrue(testArray.GetLength() == 2); + TEST_ExpectTrue(testArray.GetNumber(1) == 7.0); + + testArray.AddNumber(4.0f) + .AddString("test string") + .AddString("another string") + .AddNumber(8.0); + + // Current array: + // [10.0, 7.0, 4.0, "test string", "another string", 8.0] + Issue("Last value is incorrectly removed"); + testArray.RemoveValue(5, 1); + TEST_ExpectTrue(testArray.GetLength() == 5); + TEST_ExpectTrue(testArray.GetString(4) == "another string"); + + // Current array: + // [10.0, 7.0, 4.0, "test string", "another string"] + Issue("Tail elements are incorrectly removed"); + testArray.RemoveValue(3, 4); + TEST_ExpectTrue(testArray.GetLength() == 3); + TEST_ExpectTrue(testArray.GetNumber(0) == 10.0); + TEST_ExpectTrue(testArray.GetNumber(2) == 4.0); + + Issue("Array empties incorrectly"); + testArray.RemoveValue(0, testArray.GetLength()); + TEST_ExpectTrue(testArray.GetLength() == 0); + TEST_ExpectTrue(testArray.GetType(0) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetType(1) == JSON_Undefined); +} + +protected static function SubTest_ArrayExpansions() +{ + local JSONArray testArray; + testArray = new class'JSONArray'; + + Context("Testing how `JSONArray`' handles expansions/shrinking " @ + "via `SetLength()`"); + Issue("`SetLength()` doesn't properly expand empty array"); + testArray.SetLength(2); + TEST_ExpectTrue(testArray.GetLength() == 2); + TEST_ExpectTrue(testArray.GetType(0) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); + + Issue("`SetLength()` doesn't properly expand non-empty array"); + testArray.AddNumber(1); + testArray.SetLength(4); + TEST_ExpectTrue(testArray.GetLength() == 4); + TEST_ExpectTrue(testArray.GetType(0) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(2) == JSON_Number); + TEST_ExpectTrue(testArray.GetType(3) == JSON_Null); + TEST_ExpectTrue(testArray.GetNumber(2) == 1); + SubSubTest_ArraySetNumberExpansions(); + SubSubTest_ArraySetStringExpansions(); + SubSubTest_ArraySetBooleanExpansions(); +} + +protected static function SubSubTest_ArraySetNumberExpansions() +{ + local JSONArray testArray; + testArray = new class'JSONArray'; + + Context("Testing how `JSONArray`' handles expansions via" @ + "`SetNumber()` function"); + Issue("Setters don't create correct first element"); + testArray.SetNumber(0, 1); + TEST_ExpectTrue(testArray.GetLength() == 1); + TEST_ExpectTrue(testArray.GetNumber(0) == 1); + + Issue( "`SetNumber()` doesn't properly define array when setting" @ + "value out-of-bounds"); + testArray = new class'JSONArray'; + testArray.AddNumber(1); + testArray.SetNumber(4, 2); + TEST_ExpectTrue(testArray.GetLength() == 5); + TEST_ExpectTrue(testArray.GetNumber(0) == 1); + TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(2) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(3) == JSON_Null); + TEST_ExpectTrue(testArray.GetNumber(4) == 2); + + Issue("`SetNumber()` expands array even when it told not to"); + testArray.SetNumber(6, 7, true); + TEST_ExpectTrue(testArray.GetLength() == 5); + TEST_ExpectTrue(testArray.GetNumber(6) == 0); + TEST_ExpectTrue(testArray.GetType(5) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetType(6) == JSON_Undefined); +} + +protected static function SubSubTest_ArraySetStringExpansions() +{ + local JSONArray testArray; + testArray = new class'JSONArray'; + + Context("Testing how `JSONArray`' handles expansions via" @ + "`SetString()` function"); + Issue("Setters don't create correct first element"); + testArray.SetString(0, "str"); + TEST_ExpectTrue(testArray.GetLength() == 1); + TEST_ExpectTrue(testArray.GetString(0) == "str"); + + Issue( "`SetString()` doesn't properly define array when setting" @ + "value out-of-bounds"); + testArray = new class'JSONArray'; + testArray.AddString("str"); + testArray.SetString(4, "str2"); + TEST_ExpectTrue(testArray.GetLength() == 5); + TEST_ExpectTrue(testArray.GetString(0) == "str"); + TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(2) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(3) == JSON_Null); + TEST_ExpectTrue(testArray.GetString(4) == "str2"); + + Issue("`SetString()` expands array even when it told not to"); + testArray.SetString(6, "new string", true); + TEST_ExpectTrue(testArray.GetLength() == 5); + TEST_ExpectTrue(testArray.GetString(6) == ""); + TEST_ExpectTrue(testArray.GetType(5) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetType(6) == JSON_Undefined); +} + +protected static function SubSubTest_ArraySetBooleanExpansions() +{ + local JSONArray testArray; + testArray = new class'JSONArray'; + + Context("Testing how `JSONArray`' handles expansions via" @ + "`SetBoolean()` function"); + Issue("Setters don't create correct first element"); + testArray.SetBoolean(0, false); + TEST_ExpectTrue(testArray.GetLength() == 1); + TEST_ExpectTrue(testArray.GetBoolean(0) == false); + + Issue( "`SetBoolean()` doesn't properly define array when setting" @ + "value out-of-bounds"); + testArray = new class'JSONArray'; + testArray.AddBoolean(true); + testArray.SetBoolean(4, true); + TEST_ExpectTrue(testArray.GetLength() == 5); + TEST_ExpectTrue(testArray.GetBoolean(0) == true); + TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(2) == JSON_Null); + TEST_ExpectTrue(testArray.GetType(3) == JSON_Null); + TEST_ExpectTrue(testArray.GetBoolean(4) == true); + + Issue("`SetBoolean()` expands array even when it told not to"); + testArray.SetBoolean(6, true, true); + TEST_ExpectTrue(testArray.GetLength() == 5); + TEST_ExpectTrue(testArray.GetBoolean(6) == false); + TEST_ExpectTrue(testArray.GetType(5) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetType(6) == JSON_Undefined); +} + +defaultproperties +{ + caseName = "JSON" +} \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 26b21ca..571093e 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -44,4 +44,6 @@ defaultproperties features(7) = class'FixInventoryAbuse' // Listeners requiredListeners(0) = class'MutatorListener_Connection' + // Unit tests + testCases(0) = class'TEST_JSON' } \ No newline at end of file From 076ebe8fcf1030b705cd7bb6c30124fddbe654d6 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 8 Apr 2020 01:32:30 +0700 Subject: [PATCH 04/59] Add global namespace to Acedia actors and objects We want all actors and objects defined in Acedia to share a global namespace that provides an access to important variables (such as `Acedia` reference) and functions. We add a `Global` singleton and `AcediaActor` / `AcediaObject` base classes with a quick accessor to it's instance (`_` / `_()`). --- sources/Acedia.uc | 1 + sources/AcediaActor.uc | 40 ++++++++++++++++++++++++++++++++++++++++ sources/AcediaObject.uc | 35 +++++++++++++++++++++++++++++++++++ sources/Global.uc | 29 +++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 sources/AcediaActor.uc create mode 100644 sources/AcediaObject.uc create mode 100644 sources/Global.uc diff --git a/sources/Acedia.uc b/sources/Acedia.uc index 3a7fe12..fbe7764 100644 --- a/sources/Acedia.uc +++ b/sources/Acedia.uc @@ -49,6 +49,7 @@ event PreBeginPlay() } default.selfReference = self; // Boot up Acedia + Spawn(class'Global'); LoadManifest(class'Manifest'); LaunchServices(); InjectBroadcastHandler(); // TODO: move this to 'SideEffect' mechanic diff --git a/sources/AcediaActor.uc b/sources/AcediaActor.uc new file mode 100644 index 0000000..1950db2 --- /dev/null +++ b/sources/AcediaActor.uc @@ -0,0 +1,40 @@ +/** + * Actor base class to be used to Acedia instead of an `Actor`. + * The only difference is defined `_` member that provides convenient access to + * Acedia's API. + * It isn't guaranteed that `default._` will be defined for `AcediaActor`s. + * 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 AcediaActor extends Actor + abstract; + +var protected Global _; + +event PreBeginPlay() +{ + super.PreBeginPlay(); + if (_ == none) + { + _ = Global(class'Global'.static.GetInstance()); + default._ = _; + } +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/AcediaObject.uc b/sources/AcediaObject.uc new file mode 100644 index 0000000..ec6cb4d --- /dev/null +++ b/sources/AcediaObject.uc @@ -0,0 +1,35 @@ +/** + * Object base class to be used to Acedia instead of an `Object`. + * The only difference is defined `_` member that provides convenient access to + * Acedia's API. + * Since `Global` is an actor, we wish to avoid storing it's instance in + * the object because it can mess with garbage collection on level change. + * So we provide an accessor function `_()` instead. + * 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 AcediaObject extends Object + abstract; + +public static final function Global _() +{ + return Global(class'Global'.static.GetInstance()); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Global.uc b/sources/Global.uc new file mode 100644 index 0000000..8c46772 --- /dev/null +++ b/sources/Global.uc @@ -0,0 +1,29 @@ +/** + * Class for an object that will provide an access to a Acedia's functionality + * by giving a reference to this actor to all Acedia's objects and actors, + * emulating a global API namespace. + * 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 Global extends Singleton; + +var public Acedia acedia; + +protected function OnCreated() +{ + acedia = class'Acedia'.static.GetInstance(); +} \ No newline at end of file From 53caa5766f524b84937c698e14da0e5f8230f641 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 8 Apr 2020 01:43:11 +0700 Subject: [PATCH 05/59] Refactor JSON implementation Change base class for JSON objects from `Object` to `AcediaObject`. Rename some classes/functions/structures/variables to be more compact and/or better convey their meaning. Add appropriate API that contains constructors for JSON objects. --- sources/Data/{JSONArray.uc => JArray.uc} | 90 ++++---- sources/Data/JObject.uc | 265 ++++++++++++++++++++++ sources/Data/{JSONBase.uc => JSON.uc} | 24 +- sources/Data/JSONAPI.uc | 41 ++++ sources/Data/JSONObject.uc | 272 ----------------------- sources/Data/TEST_JSON.uc | 239 ++++++++++---------- sources/Global.uc | 7 +- 7 files changed, 495 insertions(+), 443 deletions(-) rename sources/Data/{JSONArray.uc => JArray.uc} (75%) create mode 100644 sources/Data/JObject.uc rename sources/Data/{JSONBase.uc => JSON.uc} (77%) create mode 100644 sources/Data/JSONAPI.uc delete mode 100644 sources/Data/JSONObject.uc diff --git a/sources/Data/JSONArray.uc b/sources/Data/JArray.uc similarity index 75% rename from sources/Data/JSONArray.uc rename to sources/Data/JArray.uc index d0e9e71..d160b54 100644 --- a/sources/Data/JSONArray.uc +++ b/sources/Data/JArray.uc @@ -21,14 +21,14 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class JSONArray extends JSONBase; +class JArray extends JSON; // Data will simply be stored as an array of JSON values -var private array data; +var private array data; // Return type of value stored at a given index. // Returns `JSON_Undefined` if and only if given index is out of bounds. -public final function JSONType GetType(int index) +public final function JType GetTypeOf(int index) { if (index < 0) return JSON_Undefined; if (index >= data.length) return JSON_Undefined; @@ -72,27 +72,27 @@ public final function SetLength(int newLength) // will simply return `none`. public final function float GetNumber(int index, optional float defaultValue) { - if (index < 0) return defaultValue; - if (index >= data.length) return defaultValue; - if (data[index].type != JSON_Number) return defaultValue; + if (index < 0) return defaultValue; + if (index >= data.length) return defaultValue; + if (data[index].type != JSON_Number) return defaultValue; return data[index].numberValue; } public final function string GetString(int index, optional string defaultValue) { - if (index < 0) return defaultValue; - if (index >= data.length) return defaultValue; - if (data[index].type != JSON_String) return defaultValue; + if (index < 0) return defaultValue; + if (index >= data.length) return defaultValue; + if (data[index].type != JSON_String) return defaultValue; return data[index].stringValue; } public final function bool GetBoolean(int index, optional bool defaultValue) { - if (index < 0) return defaultValue; - if (index >= data.length) return defaultValue; - if (data[index].type != JSON_Boolean) return defaultValue; + if (index < 0) return defaultValue; + if (index >= data.length) return defaultValue; + if (data[index].type != JSON_Boolean) return defaultValue; return data[index].booleanValue; } @@ -105,22 +105,22 @@ public final function bool IsNull(int index) return (data[index].type == JSON_Null); } -public final function JSONArray GetArray(int index) +public final function JArray GetArray(int index) { - if (index < 0) return none; - if (index >= data.length) return none; - if (data[index].type != JSON_Array) return none; + if (index < 0) return none; + if (index >= data.length) return none; + if (data[index].type != JSON_Array) return none; - return JSONArray(data[index].complexValue); + return JArray(data[index].complexValue); } -public final function JSONObject GetObject(int index) +public final function JObject GetObject(int index) { - if (index < 0) return none; - if (index >= data.length) return none; - if (data[index].type != JSON_Object) return none; + if (index < 0) return none; + if (index >= data.length) return none; + if (data[index].type != JSON_Object) return none; - return JSONObject(data[index].complexValue); + return JObject(data[index].complexValue); } // Following functions provide simple setters for boolean, string, number @@ -132,14 +132,14 @@ public final function JSONObject GetObject(int index) // `false` (nothing will be done in this case). // They return object itself, allowing user to chain calls like this: // `array.SetNumber("num1", 1).SetNumber("num2", 2);`. -public final function JSONArray SetNumber +public final function JArray SetNumber ( int index, float value, optional bool preventExpansion ) { - local JSONStorageValue newStorageValue; + local JStorageAtom newStorageValue; if (index < 0) return self; if (index >= data.length) @@ -159,14 +159,14 @@ public final function JSONArray SetNumber return self; } -public final function JSONArray SetString +public final function JArray SetString ( int index, string value, optional bool preventExpansion ) { - local JSONStorageValue newStorageValue; + local JStorageAtom newStorageValue; if (index < 0) return self; if (index >= data.length) @@ -186,14 +186,14 @@ public final function JSONArray SetString return self; } -public final function JSONArray SetBoolean +public final function JArray SetBoolean ( int index, bool value, optional bool preventExpansion ) { - local JSONStorageValue newStorageValue; + local JStorageAtom newStorageValue; if (index < 0) return self; if (index >= data.length) @@ -213,13 +213,13 @@ public final function JSONArray SetBoolean return self; } -public final function JSONArray SetNull +public final function JArray SetNull ( int index, optional bool preventExpansion ) { - local JSONStorageValue newStorageValue; + local JStorageAtom newStorageValue; if (index < 0) return self; if (index >= data.length) @@ -247,13 +247,13 @@ public final function JSONArray SetNull // `false` (nothing will be done in this case). // They return object itself, allowing user to chain calls like this: // `array.CreateObject("sub object").CreateArray("sub array");`. -public final function JSONArray CreateArray +public final function JArray CreateArray ( int index, optional bool preventExpansion ) { - local JSONStorageValue newStorageValue; + local JStorageAtom newStorageValue; if (index < 0) return self; if (index >= data.length) @@ -268,18 +268,18 @@ public final function JSONArray CreateArray } } newStorageValue.type = JSON_Array; - newStorageValue.complexValue = new class'JSONArray'; + newStorageValue.complexValue = _.json.newArray(); data[index] = newStorageValue; return self; } -public final function JSONArray CreateObject +public final function JArray CreateObject ( int index, optional bool preventExpansion ) { - local JSONStorageValue newStorageValue; + local JStorageAtom newStorageValue; if (index < 0) return self; if (index >= data.length) @@ -294,51 +294,51 @@ public final function JSONArray CreateObject } } newStorageValue.type = JSON_Object; - newStorageValue.complexValue = new class'JSONObject'; + newStorageValue.complexValue = _.json.newObject(); data[index] = newStorageValue; return self; } // Wrappers for setter functions that don't take index or // `preventExpansion` parameters and add/create value at the end of the array. -public final function JSONArray AddNumber(float value) +public final function JArray AddNumber(float value) { return SetNumber(data.length, value); } -public final function JSONArray AddString(string value) +public final function JArray AddString(string value) { return SetString(data.length, value); } -public final function JSONArray AddBoolean(bool value) +public final function JArray AddBoolean(bool value) { return SetBoolean(data.length, value); } -public final function JSONArray AddNull() +public final function JArray AddNull() { return SetNull(data.length); } -public final function JSONArray AddArray() +public final function JArray AddArray() { return CreateArray(data.length); } -public final function JSONArray AddObject() +public final function JArray AddObject() { return CreateObject(data.length); } -// Removes up to `amount` of values, starting from a given index. -// If `index` falls outside array boundaries - nothing will be done. +// Removes up to `amount` (minimum of `1`) of values, starting from +// a given index. +// If `index` falls outside array boundaries - nothing will be done. // Returns `true` if value was actually removed and `false` if it didn't exist. public final function bool RemoveValue(int index, optional int amount) { if (index < 0) return false; if (index >= data.length) return false; - if (amount < 1) return false; amount = Max(amount, 1); amount = Min(amount, data.length - index); diff --git a/sources/Data/JObject.uc b/sources/Data/JObject.uc new file mode 100644 index 0000000..9d81cf8 --- /dev/null +++ b/sources/Data/JObject.uc @@ -0,0 +1,265 @@ +/** + * This class implements JSON object storage capabilities. + * Whenever one wants to store JSON data, they need to define such object. + * It stores name-value pairs, where names are strings and values can be: + * ~ Boolean, string, null or number (float in this implementation) data; + * ~ Other JSON objects; + * ~ JSON Arrays (see `JArray` class). + * + * This implementation provides getters and setters for boolean, string, + * null or number types that allow to freely set and fetch their values + * by name. + * JSON objects and arrays can be fetched by getters, but you cannot + * add existing object or array to another object. Instead one has to create + * a new, empty object with a certain name and then fill it with data. + * This allows to avoid loop situations, where object is contained in itself. + * Functions to remove existing values are also provided and are applicable + * to all variable types. + * Setters can also be used to overwrite any value by a different value, + * even of a different type. + * 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 JObject extends JSON; + +// We will store all our properties as a simple array of name-value pairs. +struct JProperty +{ + var string name; + var JStorageAtom value; +}; +var private array properties; + +// Returns index of name-value pair in `properties` for a given name. +// Returns `-1` if such a pair does not exist. +private final function int GetPropertyIndex(string name) +{ + local int i; + for (i = 0; i < properties.length; i += 1) + { + if (name == properties[i].name) + { + return i; + } + } + return -1; +} + +// Returns `JType` of a variable with a given name in our properties. +// This function can be used to check if certain variable exists +// in this object, since if such variable does not exist - +// function will return `JSON_Undefined`. +public final function JType GetTypeOf(string name) +{ + local int index; + index = GetPropertyIndex(name); + if (index < 0) return JSON_Undefined; + + return properties[index].value.type; +} + +// Following functions are getters for various types of variables. +// Getter for null value simply checks if it's null +// and returns true/false as a result. +// Getters for simple types (number, string, boolean) can have optional +// default value specified, that will be returned if requested variable +// doesn't exist or has a different type. +// Getters for object and array types don't take default values and +// will simply return `none`. +public final function float GetNumber(string name, optional float defaultValue) +{ + local int index; + index = GetPropertyIndex(name); + if (index < 0) return defaultValue; + if (properties[index].value.type != JSON_Number) return defaultValue; + + return properties[index].value.numberValue; +} + +public final function string GetString +( + string name, + optional string defaultValue +) +{ + local int index; + index = GetPropertyIndex(name); + if (index < 0) return defaultValue; + if (properties[index].value.type != JSON_String) return defaultValue; + + return properties[index].value.stringValue; +} + +public final function bool GetBoolean(string name, optional bool defaultValue) +{ + local int index; + index = GetPropertyIndex(name); + if (index < 0) return defaultValue; + if (properties[index].value.type != JSON_Boolean) return defaultValue; + + return properties[index].value.booleanValue; +} + +public final function bool IsNull(string name) +{ + local int index; + index = GetPropertyIndex(name); + if (index < 0) return false; + if (properties[index].value.type != JSON_Null) return false; + + return (properties[index].value.type == JSON_Null); +} + +public final function JArray GetArray(string name) +{ + local int index; + index = GetPropertyIndex(name); + if (index < 0) return none; + if (properties[index].value.type != JSON_Array) return none; + + return JArray(properties[index].value.complexValue); +} + +public final function JObject GetObject(string name) +{ + local int index; + index = GetPropertyIndex(name); + if (index < 0) return none; + if (properties[index].value.type != JSON_Object) return none; + + return JObject(properties[index].value.complexValue); +} + +// Following functions provide simple setters for boolean, string, number +// and null values. +// They return object itself, allowing user to chain calls like this: +// `object.SetNumber("num1", 1).SetNumber("num2", 2);`. +public final function JObject SetNumber(string name, float value) +{ + local int index; + local JProperty newProperty; + index = GetPropertyIndex(name); + if (index < 0) + { + index = properties.length; + } + + newProperty.name = name; + newProperty.value.type = JSON_Number; + newProperty.value.numberValue = value; + properties[index] = newProperty; + return self; +} + +public final function JObject SetString(string name, string value) +{ + local int index; + local JProperty newProperty; + index = GetPropertyIndex(name); + if (index < 0) + { + index = properties.length; + } + newProperty.name = name; + newProperty.value.type = JSON_String; + newProperty.value.stringValue = value; + properties[index] = newProperty; + return self; +} + +public final function JObject SetBoolean(string name, bool value) +{ + local int index; + local JProperty newProperty; + index = GetPropertyIndex(name); + if (index < 0) + { + index = properties.length; + } + newProperty.name = name; + newProperty.value.type = JSON_Boolean; + newProperty.value.booleanValue = value; + properties[index] = newProperty; + return self; +} + +public final function JObject SetNull(string name) +{ + local int index; + local JProperty newProperty; + index = GetPropertyIndex(name); + if (index < 0) + { + index = properties.length; + } + newProperty.name = name; + newProperty.value.type = JSON_Null; + properties[index] = newProperty; + return self; +} + +// JSON array and object types don't have setters, but instead have +// functions to create a new, empty array/object under a certain name. +// They return object itself, allowing user to chain calls like this: +// `object.CreateObject("folded object").CreateArray("names list");`. +public final function JObject CreateArray(string name) +{ + local int index; + local JProperty newProperty; + index = GetPropertyIndex(name); + if (index < 0) + { + index = properties.length; + } + newProperty.name = name; + newProperty.value.type = JSON_Array; + newProperty.value.complexValue = _.json.newArray(); + properties[index] = newProperty; + return self; +} + +public final function JObject CreateObject(string name) +{ + local int index; + local JProperty newProperty; + index = GetPropertyIndex(name); + if (index < 0) + { + index = properties.length; + } + newProperty.name = name; + newProperty.value.type = JSON_Object; + newProperty.value.complexValue = _.json.newObject(); + properties[index] = newProperty; + return self; +} + +// Removes values with a given name. +// Returns `true` if value was actually removed and `false` if it didn't exist. +public final function bool RemoveValue(string name) +{ + local int index; + index = GetPropertyIndex(name); + if (index < 0) return false; + + properties.Remove(index, 1); + return true; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Data/JSONBase.uc b/sources/Data/JSON.uc similarity index 77% rename from sources/Data/JSONBase.uc rename to sources/Data/JSON.uc index 60e6196..8a94157 100644 --- a/sources/Data/JSONBase.uc +++ b/sources/Data/JSON.uc @@ -28,10 +28,11 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class JSONBase extends Object; +class JSON extends AcediaActor + abstract; // Enumeration for possible types of JSON values. -enum JSONType +enum JType { // Technical type, used to indicate that requested value is missing. // Undefined values are not part of JSON format. @@ -53,18 +54,31 @@ enum JSONType }; // Stores a single JSON value -struct JSONStorageValue +struct JStorageAtom { // What type is stored exactly? // Depending on that, uses one of the other fields as a storage. - var public JSONType type; + var protected JType type; var protected float numberValue; var protected string stringValue; var protected bool booleanValue; // Used for storing both JSON objects and arrays. - var protected JSONBase complexValue; + var protected JSON complexValue; }; +// TODO: Rewrite JSON object to use more efficient storage data structures +// that will support subtypes: +// ~ Number: byte, int, float +// ~ String: string, class +// (maybe move to auto generated code?). +// TODO: Add cleanup queue to efficiently and without crashes clean up +// removed objects. +// TODO: Add `JValue` - a reference type for number / string / boolean / null +// TODO: Add accessors for last values. +// TODO: Add path-getters. +// TODO: Add iterators. +// TODO: Add parsing/printing. +// TODO: Add functions for deep copy. defaultproperties { } \ No newline at end of file diff --git a/sources/Data/JSONAPI.uc b/sources/Data/JSONAPI.uc new file mode 100644 index 0000000..12d4c9d --- /dev/null +++ b/sources/Data/JSONAPI.uc @@ -0,0 +1,41 @@ +/** + * Singleton is an auxiliary class, meant to be used as a base for others, + * that allows for only one instance of it to exist. + * To make sure your child class properly works, either don't overload + * 'PreBeginPlay' or make sure to call it's parent's version. + * Copyright 2019 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 JSONAPI extends Singleton; + +public function JObject newObject() +{ + local JObject newObject; + newObject = Spawn(class'JObject'); + return newObject; +} + +public function JArray newArray() +{ + local JArray newArray; + newArray = Spawn(class'JArray'); + return newArray; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Data/JSONObject.uc b/sources/Data/JSONObject.uc deleted file mode 100644 index 6e1667e..0000000 --- a/sources/Data/JSONObject.uc +++ /dev/null @@ -1,272 +0,0 @@ -/** - * This class implements JSON object storage capabilities. - * Whenever one wants to store JSON data, they need to define such object. - * It stores name-value pairs, where names are strings and values can be: - * ~ Boolean, string, null or number (float in this implementation) data; - * ~ Other JSON objects; - * ~ JSON Arrays (see `JSONArray` class). - * - * This implementation provides getters and setters for boolean, string, - * null or number types that allow to freely set and fetch their values - * by name. - * JSON objects and arrays can be fetched by getters, but you cannot - * add existing object or array to another object. Instead one has to create - * a new, empty object with a certain name and then fill it with data. - * This allows to avoid loop situations, where object is contained in itself. - * Functions to remove existing values are also provided and are applicable - * to all variable types. - * Setters can also be used to overwrite any value by a different value, - * even of a different type. - * 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 JSONObject extends JSONBase; - -// We will store all our data as a simple array of key(name)-value pairs. -struct JSONKeyValuePair -{ - var string key; - var JSONStorageValue value; -}; -var private array data; - -// Returns index of key-value pair in `data` with a given key. -// Returns `-1` if such a pair does not exist in `data`. -private final function int GetIndex(string key) -{ - local int i; - for (i = 0; i < data.length; i += 1) - { - if (key == data[i].key) - { - return i; - } - } - return -1; -} - -// Returns `JSONType` of a variable with a given key in our data. -// This function can be used to check if certain variable exists -// in this object, since if such variable does not exist - -// function will return `JSON_Undefined`. -public final function JSONType GetType(string key) -{ - local int index; - index = GetIndex(key); - if (index < 0) return JSON_Undefined; - - return data[index].value.type; -} - -// Following functions are getters for various types of variables. -// Getter for null value simply checks if it's null -// and returns true/false as a result. -// Getters for simple types (number, string, boolean) can have optional -// default value specified, that will be returned if requested variable -// doesn't exist or has a different type. -// Getters for object and array types don't take default values and -// will simply return `none`. -public final function float GetNumber(string key, optional float defaultValue) -{ - local int index; - index = GetIndex(key); - if (index < 0) return defaultValue; - if (data[index].value.type != JSON_Number) return defaultValue; - - return data[index].value.numberValue; -} - -public final function string GetString(string key, optional string defaultValue) -{ - local int index; - index = GetIndex(key); - if (index < 0) return defaultValue; - if (data[index].value.type != JSON_String) return defaultValue; - - return data[index].value.stringValue; -} - -public final function bool GetBoolean(string key, optional bool defaultValue) -{ - local int index; - index = GetIndex(key); - if (index < 0) return defaultValue; - if (data[index].value.type != JSON_Boolean) return defaultValue; - - return data[index].value.booleanValue; -} - -public final function bool IsNull(string key) -{ - local int index; - index = GetIndex(key); - if (index < 0) return false; - if (data[index].value.type != JSON_Null) return false; - - return (data[index].value.type == JSON_Null); -} - -public final function JSONArray GetArray(string key) -{ - local int index; - index = GetIndex(key); - if (index < 0) return none; - if (data[index].value.type != JSON_Array) return none; - - return JSONArray(data[index].value.complexValue); -} - -public final function JSONObject GetObject(string key) -{ - local int index; - index = GetIndex(key); - if (index < 0) return none; - if (data[index].value.type != JSON_Object) return none; - - return JSONObject(data[index].value.complexValue); -} - -// Following functions provide simple setters for boolean, string, number -// and null values. -// They return object itself, allowing user to chain calls like this: -// `object.SetNumber("num1", 1).SetNumber("num2", 2);`. -public final function JSONObject SetNumber(string key, float value) -{ - local int index; - local JSONKeyValuePair newKeyValuePair; - local JSONStorageValue newStorageValue; - index = GetIndex(key); - if (index < 0) - { - index = data.length; - } - newStorageValue.type = JSON_Number; - newStorageValue.numberValue = value; - newKeyValuePair.key = key; - newKeyValuePair.value = newStorageValue; - data[index] = newKeyValuePair; - return self; -} - -public final function JSONObject SetString(string key, string value) -{ - local int index; - local JSONKeyValuePair newKeyValuePair; - local JSONStorageValue newStorageValue; - index = GetIndex(key); - if (index < 0) - { - index = data.length; - } - newStorageValue.type = JSON_String; - newStorageValue.stringValue = value; - newKeyValuePair.key = key; - newKeyValuePair.value = newStorageValue; - data[index] = newKeyValuePair; - return self; -} - -public final function JSONObject SetBoolean(string key, bool value) -{ - local int index; - local JSONKeyValuePair newKeyValuePair; - local JSONStorageValue newStorageValue; - index = GetIndex(key); - if (index < 0) - { - index = data.length; - } - newStorageValue.type = JSON_Boolean; - newStorageValue.booleanValue = value; - newKeyValuePair.key = key; - newKeyValuePair.value = newStorageValue; - data[index] = newKeyValuePair; - return self; -} - -public final function JSONObject SetNull(string key) -{ - local int index; - local JSONKeyValuePair newKeyValuePair; - local JSONStorageValue newStorageValue; - index = GetIndex(key); - if (index < 0) - { - index = data.length; - } - newStorageValue.type = JSON_Null; - newKeyValuePair.key = key; - newKeyValuePair.value = newStorageValue; - data[index] = newKeyValuePair; - return self; -} - -// JSON array and object types don't have setters, but instead have -// functions to create a new, empty array/object under a certain name. -// They return object itself, allowing user to chain calls like this: -// `object.CreateObject("folded object").CreateArray("names list");`. -public final function JSONObject CreateArray(string key) -{ - local int index; - local JSONKeyValuePair newKeyValuePair; - local JSONStorageValue newStorageValue; - index = GetIndex(key); - if (index < 0) - { - index = data.length; - } - newStorageValue.type = JSON_Array; - newStorageValue.complexValue = new class'JSONArray'; - newKeyValuePair.key = key; - newKeyValuePair.value = newStorageValue; - data[index] = newKeyValuePair; - return self; -} - -public final function JSONObject CreateObject(string key) -{ - local int index; - local JSONKeyValuePair newKeyValuePair; - local JSONStorageValue newStorageValue; - index = GetIndex(key); - if (index < 0) - { - index = data.length; - } - newStorageValue.type = JSON_Object; - newStorageValue.complexValue = new class'JSONObject'; - newKeyValuePair.key = key; - newKeyValuePair.value = newStorageValue; - data[index] = newKeyValuePair; - return self; -} - -// Removes values with a given name. -// Returns `true` if value was actually removed and `false` if it didn't exist. -public final function bool RemoveValue(string key) -{ - local int index; - index = GetIndex(key); - if (index < 0) return false; - - data.Remove(index, 1); - return true; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Data/TEST_JSON.uc b/sources/Data/TEST_JSON.uc index 64da167..f9801c4 100644 --- a/sources/Data/TEST_JSON.uc +++ b/sources/Data/TEST_JSON.uc @@ -1,6 +1,6 @@ /** * Set of tests for JSON data storage, implemented via - * `JSONObject` and `JSONArray`. + * `JObject` and `JArray`. * Copyright 2020 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -23,8 +23,8 @@ class TEST_JSON extends TestCase protected static function TESTS() { - local JSONObject jsonData; - jsonData = new class'JSONObject'; + local JObject jsonData; + jsonData = _().json.newObject(); Test_ObjectGetSetRemove(); Test_ArrayGetSetRemove(); } @@ -56,14 +56,15 @@ protected static function Test_ArrayGetSetRemove() protected static function SubTest_Undefined() { - local JSONObject testJSON; - testJSON = new class'JSONObject'; - Context("Testing how `JSONObject` handles undefined values"); - Issue("Undefined variable doesn't have proper type."); - TEST_ExpectTrue(testJSON.GetType("some_var") == JSON_Undefined); + local JObject testJSON; + testJSON = _().json.newObject(); - Issue("There is a variable in an empty object after `GetType` call."); - TEST_ExpectTrue(testJSON.GetType("some_var") == JSON_Undefined); + Context("Testing how `JObject` handles undefined values"); + Issue("Undefined variable doesn't have proper type."); + TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined); + + Issue("There is a variable in an empty object after `GetTypeOf` call."); + TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined); Issue("Getters don't return default values for undefined variables."); TEST_ExpectTrue(testJSON.GetNumber("some_var", 0) == 0); @@ -75,14 +76,14 @@ protected static function SubTest_Undefined() protected static function SubTest_BooleanGetSetRemove() { - local JSONObject testJSON; - testJSON = new class'JSONObject'; + local JObject testJSON; + testJSON = _().json.newObject(); testJSON.SetBoolean("some_boolean", true); - Context("Testing `JSONObject`'s get/set/remove functions for" @ + Context("Testing `JObject`'s get/set/remove functions for" @ "boolean variables"); Issue("Boolean type isn't properly set by `SetBoolean`"); - TEST_ExpectTrue(testJSON.GetType("some_boolean") == JSON_Boolean); + TEST_ExpectTrue(testJSON.GetTypeOf("some_boolean") == JSON_Boolean); Issue("Variable value is incorrectly assigned by `SetBoolean`"); TEST_ExpectTrue(testJSON.GetBoolean("some_boolean") == true); @@ -97,7 +98,7 @@ protected static function SubTest_BooleanGetSetRemove() Issue("Boolean variable isn't being properly removed"); testJSON.RemoveValue("some_boolean"); - TEST_ExpectTrue(testJSON.GetType("some_boolean") == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf("some_boolean") == JSON_Undefined); Issue( "Getters don't return default value for missing key that" @ "previously stored boolean value, that got removed"); @@ -106,14 +107,14 @@ protected static function SubTest_BooleanGetSetRemove() protected static function SubTest_StringGetSetRemove() { - local JSONObject testJSON; - testJSON = new class'JSONObject'; + local JObject testJSON; + testJSON = _().json.newObject(); testJSON.SetString("some_string", "first string"); - Context("Testing `JSONObject`'s get/set/remove functions for" @ + Context("Testing `JObject`'s get/set/remove functions for" @ "string variables"); Issue("String type isn't properly set by `SetString`"); - TEST_ExpectTrue(testJSON.GetType("some_string") == JSON_String); + TEST_ExpectTrue(testJSON.GetTypeOf("some_string") == JSON_String); Issue("Value is incorrectly assigned by `SetString`"); TEST_ExpectTrue(testJSON.GetString("some_string") == "first string"); @@ -133,7 +134,7 @@ protected static function SubTest_StringGetSetRemove() Issue("String variable isn't being properly removed"); testJSON.RemoveValue("some_string"); - TEST_ExpectTrue(testJSON.GetType("some_string") == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf("some_string") == JSON_Undefined); Issue( "Getters don't return default value for missing key that" @ "previously stored string value, but got removed"); @@ -142,14 +143,14 @@ protected static function SubTest_StringGetSetRemove() protected static function SubTest_NumberGetSetRemove() { - local JSONObject testJSON; - testJSON = new class'JSONObject'; + local JObject testJSON; + testJSON = _().json.newObject(); testJSON.SetNumber("some_number", 3.5); - Context("Testing `JSONObject`'s get/set/remove functions for" @ + Context("Testing `JObject`'s get/set/remove functions for" @ "number variables"); Issue("Number type isn't properly set by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetType("some_number") == JSON_Number); + TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Number); Issue("Value is incorrectly assigned by `SetNumber`"); TEST_ExpectTrue(testJSON.GetNumber("some_number") == 3.5); @@ -168,7 +169,7 @@ protected static function SubTest_NumberGetSetRemove() Issue("Number type isn't being properly removed"); testJSON.RemoveValue("some_number"); - TEST_ExpectTrue(testJSON.GetType("some_number") == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Undefined); Issue( "Getters don't return default value for missing key that" @ "previously stored number value, that got removed"); @@ -177,10 +178,10 @@ protected static function SubTest_NumberGetSetRemove() protected static function SubTest_NullGetSetRemove() { - local JSONObject testJSON; - testJSON = new class'JSONObject'; + local JObject testJSON; + testJSON = _().json.newObject(); - Context("Testing `JSONObject`'s get/set/remove functions for" @ + Context("Testing `JObject`'s get/set/remove functions for" @ "null values"); Issue("Undefined variable is incorrectly considered `null`"); TEST_ExpectFalse(testJSON.IsNull("some_var")); @@ -202,35 +203,35 @@ protected static function SubTest_NullGetSetRemove() TEST_ExpectTrue(testJSON.IsNull("some_var")); Issue("Null type isn't properly set by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetType("some_var") == JSON_Null); + TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Null); Issue("Null value isn't being properly removed."); testJSON.RemoveValue("some_var"); - TEST_ExpectTrue(testJSON.GetType("some_var") == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined); } protected static function SubTest_MultipleVariablesGetSet() { local int i; local bool correctValue, allValuesCorrect; - local JSONObject testJSON; - testJSON = new class'JSONObject'; - Context("Testing how `JSONObject` handles addition, change and removal" @ + local JObject testJSON; + testJSON = _().json.newObject(); + Context("Testing how `JObject` handles addition, change and removal" @ "of relatively large (hundreds) number of variables"); - for (i = 0;i < 2000; i += 1) + for (i = 0; i < 2000; i += 1) { testJSON.SetNumber("num" $ string(i), 4 * i*i - 2.6 * i + 0.75); } - for (i = 0;i < 500; i += 1) + for (i = 0; i < 500; i += 1) { testJSON.SetString("num" $ string(i), "str" $ string(Sin(i))); } - for (i = 1500;i < 2000; i += 1) + for (i = 1500; i < 2000; i += 1) { testJSON.RemoveValue("num" $ string(i)); } allValuesCorrect = true; - for (i = 0;i < 200; i += 1) + for (i = 0; i < 200; i += 1) { if (i < 500) { @@ -246,7 +247,7 @@ protected static function SubTest_MultipleVariablesGetSet() } else { - correctValue = ( testJSON.GetType("num" $ string(i)) + correctValue = ( testJSON.GetTypeOf("num" $ string(i)) == JSON_Undefined); Issue("Variables aren't removed"); } @@ -261,9 +262,9 @@ protected static function SubTest_MultipleVariablesGetSet() protected static function SubTest_Object() { - local JSONObject testObject; + local JObject testObject; Context("Testing setters and getters for folded objects"); - testObject = new class'JSONObject'; + testObject = _().json.newObject(); testObject.CreateObject("folded"); testObject.GetObject("folded").CreateObject("folded"); testObject.SetString("out", "string outside"); @@ -285,17 +286,17 @@ protected static function SubTest_Object() protected static function SubTest_ArrayUndefined() { - local JSONArray testJSON; - testJSON = new class'JSONArray'; - Context("Testing how `JSONArray` handles undefined values"); + local JArray testJSON; + testJSON = _().json.newArray(); + Context("Testing how `JArray` handles undefined values"); Issue("Undefined variable doesn't have `JSON_Undefined` type"); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); - Issue("There is a variable in an empty object after `GetType` call"); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + Issue("There is a variable in an empty object after `GetTypeOf` call"); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); Issue("Negative index refers to a defined value"); - TEST_ExpectTrue(testJSON.GetType(-1) == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf(-1) == JSON_Undefined); Issue("Getters don't return default values for undefined variables"); TEST_ExpectTrue(testJSON.GetNumber(0, 0) == 0); @@ -313,14 +314,14 @@ protected static function SubTest_ArrayUndefined() protected static function SubTest_ArrayBooleanGetSetRemove() { - local JSONArray testJSON; - testJSON = new class'JSONArray'; + local JArray testJSON; + testJSON = _().json.newArray(); testJSON.SetBoolean(0, true); - Context("Testing `JSONArray`'s get/set/remove functions for" @ + Context("Testing `JArray`'s get/set/remove functions for" @ "boolean variables"); Issue("Boolean type isn't properly set by `SetBoolean`"); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_Boolean); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Boolean); Issue("Value is incorrectly assigned by `SetBoolean`"); TEST_ExpectTrue(testJSON.GetBoolean(0) == true); @@ -335,7 +336,7 @@ protected static function SubTest_ArrayBooleanGetSetRemove() Issue("Boolean variable isn't being properly removed"); testJSON.RemoveValue(0); - TEST_ExpectTrue( testJSON.GetType(0) == JSON_Undefined); + TEST_ExpectTrue( testJSON.GetTypeOf(0) == JSON_Undefined); Issue( "Getters don't return default value for missing key that" @ "previously stored boolean value, but got removed"); @@ -344,14 +345,14 @@ protected static function SubTest_ArrayBooleanGetSetRemove() protected static function SubTest_ArrayStringGetSetRemove() { - local JSONArray testJSON; - testJSON = new class'JSONArray'; + local JArray testJSON; + testJSON = _().json.newArray(); testJSON.SetString(0, "first string"); - Context("Testing `JSONArray`'s get/set/remove functions for" @ + Context("Testing `JArray`'s get/set/remove functions for" @ "string variables"); Issue("String type isn't properly set by `SetString`"); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_String); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_String); Issue("Value is incorrectly assigned by `SetString`"); TEST_ExpectTrue(testJSON.GetString(0) == "first string"); @@ -370,7 +371,7 @@ protected static function SubTest_ArrayStringGetSetRemove() Issue("Boolean variable isn't being properly removed"); testJSON.RemoveValue(0); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); Issue( "Getters don't return default value for missing key that" @ "previously stored string value, but got removed"); @@ -379,14 +380,14 @@ protected static function SubTest_ArrayStringGetSetRemove() protected static function SubTest_ArrayNumberGetSetRemove() { - local JSONArray testJSON; - testJSON = new class'JSONArray'; + local JArray testJSON; + testJSON = _().json.newArray(); testJSON.SetNumber(0, 3.5); - Context("Testing `JSONArray`'s get/set/remove functions for" @ + Context("Testing `JArray`'s get/set/remove functions for" @ "number variables"); Issue("Number type isn't properly set by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_Number); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Number); Issue("Value is incorrectly assigned by `SetNumber`"); TEST_ExpectTrue(testJSON.GetNumber(0) == 3.5); @@ -405,7 +406,7 @@ protected static function SubTest_ArrayNumberGetSetRemove() Issue("Number type isn't being properly removed"); testJSON.RemoveValue(0); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); Issue( "Getters don't return default value for missing key that" @ "previously stored number value, but got removed"); @@ -414,10 +415,10 @@ protected static function SubTest_ArrayNumberGetSetRemove() protected static function SubTest_ArrayNullGetSetRemove() { - local JSONArray testJSON; - testJSON = new class'JSONArray'; + local JArray testJSON; + testJSON = _().json.newArray(); - Context("Testing `JSONArray`'s get/set/remove functions for" @ + Context("Testing `JArray`'s get/set/remove functions for" @ "null values"); Issue("Undefined variable is incorrectly considered `null`"); @@ -442,19 +443,19 @@ protected static function SubTest_ArrayNullGetSetRemove() TEST_ExpectTrue(testJSON.IsNull(0)); Issue("Null type isn't properly set by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_Null); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Null); Issue("Null value isn't being properly removed"); testJSON.RemoveValue(0); - TEST_ExpectTrue(testJSON.GetType(0) == JSON_Undefined); + TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); } // Returns following array: // [10.0, "test string", "another string", true, 0.0, {"var": 7.0}] -protected static function JSONArray Prepare_Array() +protected static function JArray Prepare_Array() { - local JSONArray testArray; - testArray = new class'JSONArray'; + local JArray testArray; + testArray = _().json.newArray(); testArray.AddNumber(10.0f) .AddString("test string") .AddString("another string") @@ -467,10 +468,10 @@ protected static function JSONArray Prepare_Array() protected static function SubTest_ArrayMultipleVariablesStorage() { - local JSONArray testArray; + local JArray testArray; testArray = Prepare_Array(); - Context("Testing how `JSONArray` handles adding and" @ + Context("Testing how `JArray` handles adding and" @ "changing several variables"); Issue("Stored values are compromised."); TEST_ExpectTrue(testArray.GetNumber(0) == 10.0f); @@ -490,18 +491,18 @@ protected static function SubTest_ArrayMultipleVariablesStorage() TEST_ExpectTrue(testArray.GetBoolean(3, false) == false); Issue("Type of the variable is incorrectly changed."); - TEST_ExpectTrue(testArray.GetType(3) == JSON_String); + TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_String); } protected static function SubTest_ArrayMultipleVariablesRemoval() { - local JSONArray testArray; + local JArray testArray; testArray = Prepare_Array(); // Test removing variables // After `Prepare_Array`, our array should be: // [10.0, "test string", "another string", true, 0.0, {"var": 7.0}] - Context("Testing how `JSONArray` handles adding and" @ + Context("Testing how `JArray` handles adding and" @ "removing several variables"); Issue("Values are incorrectly removed"); testArray.RemoveValue(2); @@ -511,7 +512,7 @@ protected static function SubTest_ArrayMultipleVariablesRemoval() TEST_ExpectTrue(testArray.GetString(1) == "test string"); TEST_ExpectTrue(testArray.GetBoolean(2) == true); TEST_ExpectTrue(testArray.GetNumber(3) == 0.0f); - TEST_ExpectTrue(testArray.GetType(4) == JSON_Object); + TEST_ExpectTrue(testArray.GetTypeOf(4) == JSON_Object); Issue("First element incorrectly removed"); testArray.RemoveValue(0); @@ -519,7 +520,7 @@ protected static function SubTest_ArrayMultipleVariablesRemoval() TEST_ExpectTrue(testArray.GetString(0) == "test string"); TEST_ExpectTrue(testArray.GetBoolean(1) == true); TEST_ExpectTrue(testArray.GetNumber(2) == 0.0f); - TEST_ExpectTrue(testArray.GetType(3) == JSON_Object); + TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Object); TEST_ExpectTrue(testArray.GetObject(3).GetNumber("var") == 7.0); Issue("Last element incorrectly removed"); @@ -535,19 +536,19 @@ protected static function SubTest_ArrayMultipleVariablesRemoval() testArray.RemoveValue(0); testArray.RemoveValue(0); TEST_ExpectTrue(testArray.Getlength() == 0); - TEST_ExpectTrue(testArray.GetType(0) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Undefined); } protected static function SubTest_ArrayRemovingMultipleVariablesAtOnce() { - local JSONArray testArray; - testArray = new class'JSONArray'; + local JArray testArray; + testArray = _().json.newArray(); testArray.AddNumber(10.0f) .AddString("test string") .AddString("another string") .AddNumber(7.0); - Context("Testing how `JSONArray`' handles removing" @ + Context("Testing how `JArray`' handles removing" @ "multiple elements at once"); Issue("Multiple values are incorrectly removed"); testArray.RemoveValue(1, 2); @@ -577,31 +578,31 @@ protected static function SubTest_ArrayRemovingMultipleVariablesAtOnce() Issue("Array empties incorrectly"); testArray.RemoveValue(0, testArray.GetLength()); TEST_ExpectTrue(testArray.GetLength() == 0); - TEST_ExpectTrue(testArray.GetType(0) == JSON_Undefined); - TEST_ExpectTrue(testArray.GetType(1) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Undefined); } protected static function SubTest_ArrayExpansions() { - local JSONArray testArray; - testArray = new class'JSONArray'; + local JArray testArray; + testArray = _().json.newArray(); - Context("Testing how `JSONArray`' handles expansions/shrinking " @ + Context("Testing how `JArray`' handles expansions/shrinking " @ "via `SetLength()`"); Issue("`SetLength()` doesn't properly expand empty array"); testArray.SetLength(2); TEST_ExpectTrue(testArray.GetLength() == 2); - TEST_ExpectTrue(testArray.GetType(0) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); Issue("`SetLength()` doesn't properly expand non-empty array"); testArray.AddNumber(1); testArray.SetLength(4); TEST_ExpectTrue(testArray.GetLength() == 4); - TEST_ExpectTrue(testArray.GetType(0) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(2) == JSON_Number); - TEST_ExpectTrue(testArray.GetType(3) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Number); + TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null); TEST_ExpectTrue(testArray.GetNumber(2) == 1); SubSubTest_ArraySetNumberExpansions(); SubSubTest_ArraySetStringExpansions(); @@ -610,10 +611,10 @@ protected static function SubTest_ArrayExpansions() protected static function SubSubTest_ArraySetNumberExpansions() { - local JSONArray testArray; - testArray = new class'JSONArray'; + local JArray testArray; + testArray = _().json.newArray(); - Context("Testing how `JSONArray`' handles expansions via" @ + Context("Testing how `JArray`' handles expansions via" @ "`SetNumber()` function"); Issue("Setters don't create correct first element"); testArray.SetNumber(0, 1); @@ -622,30 +623,30 @@ protected static function SubSubTest_ArraySetNumberExpansions() Issue( "`SetNumber()` doesn't properly define array when setting" @ "value out-of-bounds"); - testArray = new class'JSONArray'; + testArray = _().json.newArray(); testArray.AddNumber(1); testArray.SetNumber(4, 2); TEST_ExpectTrue(testArray.GetLength() == 5); TEST_ExpectTrue(testArray.GetNumber(0) == 1); - TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(2) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(3) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null); TEST_ExpectTrue(testArray.GetNumber(4) == 2); Issue("`SetNumber()` expands array even when it told not to"); testArray.SetNumber(6, 7, true); TEST_ExpectTrue(testArray.GetLength() == 5); TEST_ExpectTrue(testArray.GetNumber(6) == 0); - TEST_ExpectTrue(testArray.GetType(5) == JSON_Undefined); - TEST_ExpectTrue(testArray.GetType(6) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined); } protected static function SubSubTest_ArraySetStringExpansions() { - local JSONArray testArray; - testArray = new class'JSONArray'; + local JArray testArray; + testArray = _().json.newArray(); - Context("Testing how `JSONArray`' handles expansions via" @ + Context("Testing how `JArray`' handles expansions via" @ "`SetString()` function"); Issue("Setters don't create correct first element"); testArray.SetString(0, "str"); @@ -654,30 +655,30 @@ protected static function SubSubTest_ArraySetStringExpansions() Issue( "`SetString()` doesn't properly define array when setting" @ "value out-of-bounds"); - testArray = new class'JSONArray'; + testArray = _().json.newArray(); testArray.AddString("str"); testArray.SetString(4, "str2"); TEST_ExpectTrue(testArray.GetLength() == 5); TEST_ExpectTrue(testArray.GetString(0) == "str"); - TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(2) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(3) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null); TEST_ExpectTrue(testArray.GetString(4) == "str2"); Issue("`SetString()` expands array even when it told not to"); testArray.SetString(6, "new string", true); TEST_ExpectTrue(testArray.GetLength() == 5); TEST_ExpectTrue(testArray.GetString(6) == ""); - TEST_ExpectTrue(testArray.GetType(5) == JSON_Undefined); - TEST_ExpectTrue(testArray.GetType(6) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined); } protected static function SubSubTest_ArraySetBooleanExpansions() { - local JSONArray testArray; - testArray = new class'JSONArray'; + local JArray testArray; + testArray = _().json.newArray(); - Context("Testing how `JSONArray`' handles expansions via" @ + Context("Testing how `JArray`' handles expansions via" @ "`SetBoolean()` function"); Issue("Setters don't create correct first element"); testArray.SetBoolean(0, false); @@ -686,22 +687,22 @@ protected static function SubSubTest_ArraySetBooleanExpansions() Issue( "`SetBoolean()` doesn't properly define array when setting" @ "value out-of-bounds"); - testArray = new class'JSONArray'; + testArray = _().json.newArray(); testArray.AddBoolean(true); testArray.SetBoolean(4, true); TEST_ExpectTrue(testArray.GetLength() == 5); TEST_ExpectTrue(testArray.GetBoolean(0) == true); - TEST_ExpectTrue(testArray.GetType(1) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(2) == JSON_Null); - TEST_ExpectTrue(testArray.GetType(3) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null); + TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null); TEST_ExpectTrue(testArray.GetBoolean(4) == true); Issue("`SetBoolean()` expands array even when it told not to"); testArray.SetBoolean(6, true, true); TEST_ExpectTrue(testArray.GetLength() == 5); TEST_ExpectTrue(testArray.GetBoolean(6) == false); - TEST_ExpectTrue(testArray.GetType(5) == JSON_Undefined); - TEST_ExpectTrue(testArray.GetType(6) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined); + TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined); } defaultproperties diff --git a/sources/Global.uc b/sources/Global.uc index 8c46772..75c0728 100644 --- a/sources/Global.uc +++ b/sources/Global.uc @@ -21,9 +21,12 @@ */ class Global extends Singleton; -var public Acedia acedia; +var public Acedia acedia; +var public JSONAPI json; protected function OnCreated() { - acedia = class'Acedia'.static.GetInstance(); + acedia = class'Acedia'.static.GetInstance(); + Spawn(class'JSONAPI'); + json = JSONAPI(class'JSONAPI'.static.GetInstance()); } \ No newline at end of file From dd0bdc8904f88aded2b824ab77c48e541c5b8b0d Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 8 Apr 2020 01:46:57 +0700 Subject: [PATCH 06/59] Add events for a `Singleton` Add `OnCreated()` and `OnDestroyed` event functions to provide a more simple and safe way to handle these events. --- sources/Singleton.uc | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/sources/Singleton.uc b/sources/Singleton.uc index 5271cbc..2bb945d 100644 --- a/sources/Singleton.uc +++ b/sources/Singleton.uc @@ -20,10 +20,10 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class Singleton extends Actor +class Singleton extends AcediaActor abstract; -// Default value of this variable will store one and only existing version\ +// Default value of this variable will store one and only existing version // of actor of this class. var private Singleton activeInstance; @@ -44,8 +44,14 @@ public final static function bool IsSingletonCreationBlocked() return default.blockSpawning; } +protected function OnCreated(){} +protected function OnDestroyed(){} + // Make sure only one instance of 'Singleton' exists at any point in time. -// If you overload this function in any child class - +// Instead of overloading this function we suggest you overload a special +// event function `OnCreated()` that is called whenever a valid `Singleton` +// instance is spawned. +// If you absolutely must overload this function in any child class - // first call this version of the method and then check if // you are about to be deleted 'bDeleteMe == true': // ____________________________________________________________________________ @@ -57,6 +63,7 @@ public final static function bool IsSingletonCreationBlocked() // |___________________________________________________________________________ event PreBeginPlay() { + super.PreBeginPlay(); if (default.blockSpawning || GetInstance() != none) { Destroy(); @@ -64,6 +71,22 @@ event PreBeginPlay() else { default.activeInstance = self; + OnCreated(); + } +} + +// Make sure only one instance of 'Singleton' exists at any point in time. +// Instead of overloading this function we suggest you overload a special +// event function `OnDestroyed()` that is called whenever a valid `Singleton` +// instance is destroyed. +// If you absolutely must overload this function in any child class - +// first call this version of the method. +event Destroyed() +{ + super.Destroyed(); + if (self == GetInstance()) + { + OnDestroyed(); } } From 3969eb3824190900d341c73e762d9b92d115005d Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 8 Apr 2020 01:49:17 +0700 Subject: [PATCH 07/59] Refactor `Feature` Make events functions `OnEnabled()` and `OnEnabled()` protected rather than public. Move initialization and clean up logic into `OnCreated()` and `OnDestroyed()` event functions. --- sources/Features/FixAmmoSelling/FixAmmoSelling.uc | 4 ++-- sources/Features/FixDoshSpam/FixDoshSpam.uc | 4 ++-- sources/Features/FixDualiesCost/FixDualiesCost.uc | 4 ++-- sources/Features/FixFFHack/FixFFHack.uc | 4 ++-- sources/Features/FixInfiniteNades/FixInfiniteNades.uc | 4 ++-- sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc | 4 ++-- sources/Features/FixZedTimeLags/FixZedTimeLags.uc | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sources/Features/FixAmmoSelling/FixAmmoSelling.uc b/sources/Features/FixAmmoSelling/FixAmmoSelling.uc index e078a58..88f4541 100644 --- a/sources/Features/FixAmmoSelling/FixAmmoSelling.uc +++ b/sources/Features/FixAmmoSelling/FixAmmoSelling.uc @@ -133,7 +133,7 @@ struct WeaponRecord // All weapons we've detected so far. var private array registeredWeapons; -public function OnEnabled() +protected function OnEnabled() { local KFWeapon nextWeapon; local KFAmmoPickup nextPickup; @@ -149,7 +149,7 @@ public function OnEnabled() } } -public function OnDisabled() +protected function OnDisabled() { local int i; local AmmoPickupStalker nextStalker; diff --git a/sources/Features/FixDoshSpam/FixDoshSpam.uc b/sources/Features/FixDoshSpam/FixDoshSpam.uc index eecf2f3..ea298a0 100644 --- a/sources/Features/FixDoshSpam/FixDoshSpam.uc +++ b/sources/Features/FixDoshSpam/FixDoshSpam.uc @@ -89,7 +89,7 @@ var private array currentContributors; // Wads of cash that are lying around on the map. var private array wads; -public function OnEnabled() +protected function OnEnabled() { local CashPickup nextCash; // Find all wads of cash laying around on the map, @@ -100,7 +100,7 @@ public function OnEnabled() } } -public function OnDisabled() +protected function OnDisabled() { wads.length = 0; currentContributors.length = 0; diff --git a/sources/Features/FixDualiesCost/FixDualiesCost.uc b/sources/Features/FixDualiesCost/FixDualiesCost.uc index 2fd3822..639a689 100644 --- a/sources/Features/FixDualiesCost/FixDualiesCost.uc +++ b/sources/Features/FixDualiesCost/FixDualiesCost.uc @@ -145,7 +145,7 @@ var private const array storedValues; // Sell value of the last seen pickup in 'OverridePickupQuery' var private int nextSellValue; -public function OnEnabled() +protected function OnEnabled() { local KFWeapon nextWeapon; // Find all frags, that spawned when this fix wasn't running. @@ -156,7 +156,7 @@ public function OnEnabled() level.game.AddGameModifier(Spawn(class'DualiesCostRule')); } -public function OnDisabled() +protected function OnDisabled() { local GameRules rulesIter; local DualiesCostRule ruleToDestroy; diff --git a/sources/Features/FixFFHack/FixFFHack.uc b/sources/Features/FixFFHack/FixFFHack.uc index 33ad62a..ed38c9f 100644 --- a/sources/Features/FixFFHack/FixFFHack.uc +++ b/sources/Features/FixFFHack/FixFFHack.uc @@ -56,12 +56,12 @@ var private config const array< class > alwaysScale; // Damage types, for which we should never reapply friendly fire scaling. var private config const array< class > neverScale; -public function OnEnabled() +protected function OnEnabled() { level.game.AddGameModifier(Spawn(class'FFHackRule')); } -public function OnDisabled() +protected function OnDisabled() { local GameRules rulesIter; local FFHackRule ruleToDestroy; diff --git a/sources/Features/FixInfiniteNades/FixInfiniteNades.uc b/sources/Features/FixInfiniteNades/FixInfiniteNades.uc index bec784f..53e0e8a 100644 --- a/sources/Features/FixInfiniteNades/FixInfiniteNades.uc +++ b/sources/Features/FixInfiniteNades/FixInfiniteNades.uc @@ -58,7 +58,7 @@ struct FragAmmoRecord }; var private array ammoRecords; -public function OnEnabled() +protected function OnEnabled() { local Frag nextFrag; // Find all frags, that spawned when this fix wasn't running. @@ -69,7 +69,7 @@ public function OnEnabled() RecreateFrags(); } -public function OnDisabled() +protected function OnDisabled() { RecreateFrags(); ammoRecords.length = 0; diff --git a/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc b/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc index 4881386..e7146fd 100644 --- a/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc +++ b/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc @@ -44,7 +44,7 @@ struct DualiesPair // It's made configurable in case of custom dual weapons. var private config const array dualiesClasses; -public function OnEnabled() +protected function OnEnabled() { local float actualInterval; actualInterval = checkInterval; @@ -55,7 +55,7 @@ public function OnEnabled() SetTimer(actualInterval, true); } -public function OnDisabled() +protected function OnDisabled() { SetTimer(0.0f, false); } diff --git a/sources/Features/FixZedTimeLags/FixZedTimeLags.uc b/sources/Features/FixZedTimeLags/FixZedTimeLags.uc index 61e1092..5ec9fcd 100644 --- a/sources/Features/FixZedTimeLags/FixZedTimeLags.uc +++ b/sources/Features/FixZedTimeLags/FixZedTimeLags.uc @@ -65,7 +65,7 @@ var private float updateCooldown; // Recorded game type, to avoid constant conversions every tick var private KFGameType gameType; -public function OnEnabled() +protected function OnEnabled() { gameType = KFGameType(level.game); if (gameType == none) @@ -78,7 +78,7 @@ public function OnEnabled() } } -public function OnDisabled() +protected function OnDisabled() { gameType = KFGameType(level.game); if (gameType != none && disableTick) From 54e87437c27f2b080f26762fa481e19ff12ef7f0 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 8 Apr 2020 01:49:58 +0700 Subject: [PATCH 08/59] Add TODO list for a `TestCase` --- sources/TestCase.uc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/sources/TestCase.uc b/sources/TestCase.uc index 2512e30..43318a6 100644 --- a/sources/TestCase.uc +++ b/sources/TestCase.uc @@ -25,7 +25,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class TestCase extends Actor +class TestCase extends AcediaObject abstract; // Name by which this set of unit tests can be referred to. @@ -246,6 +246,17 @@ public final static function bool PerformTests() return default.currentSummary.passed; } +// TODO: Support for testing in stages to avoid infinite loop crashes. +// TODO: Add support for test scening: grabbing pawns, placing them, waiting. +// TODO: Expand scening support: triggering functions on client, moving. +// TODO: Expand scening support: zed spawning, aggro setting. +// TODO: Expand scening support: function calls (i.e. for CashToss), +// testing `FixDoshSpam` feature. +// TODO: Expand scening support: lag detection. +// TODO: Expand scening support: test `FixZedTime`. +// TODO: Expand scening support: aiming shooting, detecting damage. +// TODO: Expand scening support: testing `FixFFHack`. +// TODO: Testing infinite nade (partially), ammo selling, dualies cost. defaultproperties { caseName = "" From 97569f95682fcc2e8321ed9b27a86754475019bd Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Thu, 9 Apr 2020 14:43:45 +0700 Subject: [PATCH 09/59] Change file structure of Acedia Improves grouping of some files in project's directories. --- sources/{ => Core}/Acedia.uc | 0 sources/{ => Core}/AcediaActor.uc | 0 sources/{ => Core}/AcediaObject.uc | 0 sources/{ => Core}/AcediaReplicationInfo.uc | 0 sources/{Data => Core/Data/JSON}/JArray.uc | 0 sources/{Data => Core/Data/JSON}/JObject.uc | 0 sources/{Data => Core/Data/JSON}/JSON.uc | 0 sources/{Data => Core/Data/JSON}/JSONAPI.uc | 0 sources/{Data => Core/Data/JSON/Tests}/TEST_JSON.uc | 0 sources/{ => Core/Events/Broadcast}/BroadcastEvents.uc | 0 sources/{ => Core/Events/Broadcast}/BroadcastHandler.uc | 0 sources/{ => Core/Events/Broadcast}/BroadcastListenerBase.uc | 0 sources/{ => Core/Events}/Events.uc | 0 sources/{ => Core/Events}/Listener.uc | 0 sources/{ => Core/Events/Mutator}/MutatorEvents.uc | 0 sources/{ => Core/Events/Mutator}/MutatorListenerBase.uc | 0 sources/{ => Core}/Feature.uc | 0 sources/{ => Core}/Service.uc | 0 sources/{ => Core}/Singleton.uc | 0 sources/{ => Core}/StartUp.uc | 0 sources/{ => Core/Testing}/TestCase.uc | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename sources/{ => Core}/Acedia.uc (100%) rename sources/{ => Core}/AcediaActor.uc (100%) rename sources/{ => Core}/AcediaObject.uc (100%) rename sources/{ => Core}/AcediaReplicationInfo.uc (100%) rename sources/{Data => Core/Data/JSON}/JArray.uc (100%) rename sources/{Data => Core/Data/JSON}/JObject.uc (100%) rename sources/{Data => Core/Data/JSON}/JSON.uc (100%) rename sources/{Data => Core/Data/JSON}/JSONAPI.uc (100%) rename sources/{Data => Core/Data/JSON/Tests}/TEST_JSON.uc (100%) rename sources/{ => Core/Events/Broadcast}/BroadcastEvents.uc (100%) rename sources/{ => Core/Events/Broadcast}/BroadcastHandler.uc (100%) rename sources/{ => Core/Events/Broadcast}/BroadcastListenerBase.uc (100%) rename sources/{ => Core/Events}/Events.uc (100%) rename sources/{ => Core/Events}/Listener.uc (100%) rename sources/{ => Core/Events/Mutator}/MutatorEvents.uc (100%) rename sources/{ => Core/Events/Mutator}/MutatorListenerBase.uc (100%) rename sources/{ => Core}/Feature.uc (100%) rename sources/{ => Core}/Service.uc (100%) rename sources/{ => Core}/Singleton.uc (100%) rename sources/{ => Core}/StartUp.uc (100%) rename sources/{ => Core/Testing}/TestCase.uc (100%) diff --git a/sources/Acedia.uc b/sources/Core/Acedia.uc similarity index 100% rename from sources/Acedia.uc rename to sources/Core/Acedia.uc diff --git a/sources/AcediaActor.uc b/sources/Core/AcediaActor.uc similarity index 100% rename from sources/AcediaActor.uc rename to sources/Core/AcediaActor.uc diff --git a/sources/AcediaObject.uc b/sources/Core/AcediaObject.uc similarity index 100% rename from sources/AcediaObject.uc rename to sources/Core/AcediaObject.uc diff --git a/sources/AcediaReplicationInfo.uc b/sources/Core/AcediaReplicationInfo.uc similarity index 100% rename from sources/AcediaReplicationInfo.uc rename to sources/Core/AcediaReplicationInfo.uc diff --git a/sources/Data/JArray.uc b/sources/Core/Data/JSON/JArray.uc similarity index 100% rename from sources/Data/JArray.uc rename to sources/Core/Data/JSON/JArray.uc diff --git a/sources/Data/JObject.uc b/sources/Core/Data/JSON/JObject.uc similarity index 100% rename from sources/Data/JObject.uc rename to sources/Core/Data/JSON/JObject.uc diff --git a/sources/Data/JSON.uc b/sources/Core/Data/JSON/JSON.uc similarity index 100% rename from sources/Data/JSON.uc rename to sources/Core/Data/JSON/JSON.uc diff --git a/sources/Data/JSONAPI.uc b/sources/Core/Data/JSON/JSONAPI.uc similarity index 100% rename from sources/Data/JSONAPI.uc rename to sources/Core/Data/JSON/JSONAPI.uc diff --git a/sources/Data/TEST_JSON.uc b/sources/Core/Data/JSON/Tests/TEST_JSON.uc similarity index 100% rename from sources/Data/TEST_JSON.uc rename to sources/Core/Data/JSON/Tests/TEST_JSON.uc diff --git a/sources/BroadcastEvents.uc b/sources/Core/Events/Broadcast/BroadcastEvents.uc similarity index 100% rename from sources/BroadcastEvents.uc rename to sources/Core/Events/Broadcast/BroadcastEvents.uc diff --git a/sources/BroadcastHandler.uc b/sources/Core/Events/Broadcast/BroadcastHandler.uc similarity index 100% rename from sources/BroadcastHandler.uc rename to sources/Core/Events/Broadcast/BroadcastHandler.uc diff --git a/sources/BroadcastListenerBase.uc b/sources/Core/Events/Broadcast/BroadcastListenerBase.uc similarity index 100% rename from sources/BroadcastListenerBase.uc rename to sources/Core/Events/Broadcast/BroadcastListenerBase.uc diff --git a/sources/Events.uc b/sources/Core/Events/Events.uc similarity index 100% rename from sources/Events.uc rename to sources/Core/Events/Events.uc diff --git a/sources/Listener.uc b/sources/Core/Events/Listener.uc similarity index 100% rename from sources/Listener.uc rename to sources/Core/Events/Listener.uc diff --git a/sources/MutatorEvents.uc b/sources/Core/Events/Mutator/MutatorEvents.uc similarity index 100% rename from sources/MutatorEvents.uc rename to sources/Core/Events/Mutator/MutatorEvents.uc diff --git a/sources/MutatorListenerBase.uc b/sources/Core/Events/Mutator/MutatorListenerBase.uc similarity index 100% rename from sources/MutatorListenerBase.uc rename to sources/Core/Events/Mutator/MutatorListenerBase.uc diff --git a/sources/Feature.uc b/sources/Core/Feature.uc similarity index 100% rename from sources/Feature.uc rename to sources/Core/Feature.uc diff --git a/sources/Service.uc b/sources/Core/Service.uc similarity index 100% rename from sources/Service.uc rename to sources/Core/Service.uc diff --git a/sources/Singleton.uc b/sources/Core/Singleton.uc similarity index 100% rename from sources/Singleton.uc rename to sources/Core/Singleton.uc diff --git a/sources/StartUp.uc b/sources/Core/StartUp.uc similarity index 100% rename from sources/StartUp.uc rename to sources/Core/StartUp.uc diff --git a/sources/TestCase.uc b/sources/Core/Testing/TestCase.uc similarity index 100% rename from sources/TestCase.uc rename to sources/Core/Testing/TestCase.uc From c69057266361109c25e81fe4b9d07fdabe4c5497 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 14 Apr 2020 01:49:45 +0700 Subject: [PATCH 10/59] Add aliases functionality Add aliases functionality to acedia. Aliases allow to add "synonyms" to class names or other text values. --- config/AcediaAliases.ini | 569 +++++++++++++++++++++ sources/Core/Aliases/Aliases.uc | 140 +++++ sources/Core/Aliases/AliasesAPI.uc | 43 ++ sources/Core/Aliases/Tests/TEST_Aliases.uc | 53 ++ sources/Global.uc | 6 +- sources/Manifest.uc | 1 + 6 files changed, 810 insertions(+), 2 deletions(-) create mode 100644 config/AcediaAliases.ini create mode 100644 sources/Core/Aliases/Aliases.uc create mode 100644 sources/Core/Aliases/AliasesAPI.uc create mode 100644 sources/Core/Aliases/Tests/TEST_Aliases.uc diff --git a/config/AcediaAliases.ini b/config/AcediaAliases.ini new file mode 100644 index 0000000..4c4074f --- /dev/null +++ b/config/AcediaAliases.ini @@ -0,0 +1,569 @@ +; Field Medic weapons +[weapon/KFMod:MP7MMedicGun Aliases] +Alias="MP7M" +Alias="MP7" +[weapon/KFMod:MP5MMedicGun Aliases] +Alias="MP5M" +Alias="MP5" +Alias="MP" +Alias="M5" +[weapon/KFMod:CamoMP5MMedicGun Aliases] +Alias="CamoMP5M" +Alias="CamoMP5" +Alias="CamoMP" +Alias="CamoM5" +[weapon/KFMod:M7A3MMedicGun Aliases] +Alias="M7A3" +Alias="M7A" +Alias="M7" +[weapon/KFMod:KrissMMedicGun Aliases] +Alias="Schneidzekk" +Alias="Schneidzek" +Alias="Kriss" +Alias="Kris" +[weapon/KFMod:NeonKrissMMedicGun Aliases] +Alias="NeonSchneidzekk" +Alias="NeonSchneidzek" +Alias="NeonKriss" +Alias="NeonKris" +[weapon/KFMod:BlowerThrower Aliases] +Alias="BlowerThrower" +Alias="Blower" +Alias="Thrower" +Alias="BThrower" +Alias="PoopGun" +Alias="BileGun" +Alias="BloatGun" + +; Support Specialist weapons +[weapon/KFMod:Shotgun Aliases] +Alias="Shotgun" +[weapon/KFMod:CamoShotgun Aliases] +Alias="CamoShotgun" +[weapon/KFMod:BoomStick Aliases] +Alias="HuntingShotgun" +Alias="BoomStick" +Alias="Hunting" +[weapon/KFMod:KSGShotgun Aliases] +Alias="HSG-1Shotgun" +Alias="HSG1Shotgun" +Alias="HSGShotgun" +Alias="HSG" +Alias="KSG-1Shotgun" +Alias="KSG1Shotgun" +Alias="KSGShotgun" +Alias="KSG" +[weapon/KFMod:NeonKSGShotgun Aliases] +Alias="NeonHSG-1Shotgun" +Alias="NeonHSG1Shotgun" +Alias="NeonHSGShotgun" +Alias="NeonHSG" +Alias="NeonKSG-1Shotgun" +Alias="NeonKSG1Shotgun" +Alias="NeonKSGShotgun" +Alias="NeonKSG" +[weapon/KFMod:NailGun Aliases] +Alias="VladTheImpaler" +Alias="VladImpaler" +Alias="Vlad" +Alias="Impaler" +Alias="NailGun" +Alias="Nails" +Alias="Nail" +[weapon/KFMod:SPAutoShotgun Aliases] +Alias="MultichamberZEDThrower" +Alias="ZEDThrower" +Alias="ZThrower" +[weapon/KFMod:BenelliShotgun Aliases] +Alias="CombatShotgun" +Alias="Combat" +Alias="CShotgun" +Alias="BenelliShotgun" +Alias="BeneliShotgun" +Alias="Benelli" +Alias="Beneli" +[weapon/KFMod:GoldenBenelliShotgun Aliases] +Alias="GoldCombatShotgun" +Alias="GoldCombat" +Alias="GoldCShotgun" +Alias="GoldBenelliShotgun" +Alias="GoldBeneliShotgun" +Alias="GoldBenelli" +Alias="GoldBeneli" +[weapon/KFMod:AA12AutoShotgun Aliases] +Alias="AA12" +Alias="AA12AutoShotgun" +Alias="AA12Shotgun" +[weapon/KFMod:GoldenAA12AutoShotgun Aliases] +Alias="GoldAA12" +Alias="GoldAA12AutoShotgun" +Alias="GoldAA12Shotgun" + +; Sharpshooter weapons +[weapon/KFMod:Single Aliases] +Alias="9mmTactical" +Alias="9mmTact" +Alias="9mm" +Alias="Single" +Alias="Pistol" +[weapon/KFMod:Dualies Aliases] +Alias="Dual9mms" +Alias="Dual9mm" +Alias="9mmDual" +Alias="Dualies" +Alias="Dual" +[weapon/KFMod:Magnum44Pistol Aliases] +Alias="Magnum44Pistol" +Alias="Magnum44" +Alias="44Magnum" +Alias="Magnum" +Alias="44" +[weapon/KFMod:Dual44Magnum Aliases] +Alias="DualMagnum44Pistols" +Alias="DualMagnum44s" +Alias="DualMagnums" +Alias="DualMagnumPistols" +Alias="Dual44Magnums" +Alias="Dual44Magnum" +Alias="DualMagnum" +Alias="Dual44ss" +Alias="Dual44" +[weapon/KFMod:MK23Pistol Aliases] +Alias="MK23" +Alias="MK" +Alias="23" +[weapon/KFMod:DualMK23Pistol Aliases] +Alias="DualMK23s" +Alias="DualMK23" +Alias="DualMKs" +Alias="DualMK" +Alias="Dual23s" +Alias="Dual23" +[weapon/KFMod:Deagle Aliases] +Alias="Handcannon" +Alias="Deagle" +Alias="HC" +[weapon/KFMod:DualDeagle Aliases] +Alias="DualHandcannons" +Alias="DualHC" +Alias="DualDeagle" +[weapon/KFMod:GoldenDeagle Aliases] +Alias="GoldHandcannon" +Alias="GoldDeagle" +Alias="GoldHC" +[weapon/KFMod:GoldenDualDeagle Aliases] +Alias="GoldDualHandcannons" +Alias="GoldDualHC" +Alias="GoldDualDeagle" +[weapon/KFMod:Winchester Aliases] +Alias="Winchester" +Alias="LeverActionRifle" +Alias="LAR" +[weapon/KFMod:SPSniperRifle Aliases] +Alias="SPMusket" +Alias="Musket" +Alias="SPSniperRifle" +Alias="SPRifle" +Alias="SPSniper" +Alias="SPMauler" +Alias="Mauler" +[weapon/KFMod:M14EBRBattleRifle Aliases] +Alias="M14EBR" +Alias="M14" +Alias="EBR" +Alias="M14EBRRifle" +Alias="M14EBRBattleRifle" +[weapon/KFMod:Crossbow Aliases] +Alias="CompoundCrossbow" +Alias="CCrossbow" +Alias="Crossbow" +Alias="XBow" +[weapon/KFMod:M99SniperRifle Aliases] +Alias="M99AMR" +Alias="M99" +Alias="M99SniperRifle" +Alias="M99Sniper" +Alias="M99Rifle" +Alias="M99SR" + +; Commando weapons +[weapon/KFMod:Bullpup Aliases] +Alias="Bullpup" +Alias="Bulpup" +[weapon/KFMod:ThompsonSMG Aliases] +Alias="ThompsonSMG" +Alias="Thompson" +Alias="Thomp" +Alias="TommyGun" +Alias="TomyGun" +Alias="Tommy" +Alias="Tomy" +[weapon/KFMod:SPThompsonSMG Aliases] +Alias="SPThompsonSMG" +Alias="SPThompson" +Alias="SPThomp" +Alias="Dr.T'sLeadDeliverySystem" +Alias="Dr.TsLeadDeliverySystem" +Alias="DrT'sLeadDeliverySystem" +Alias="DrTsLeadDeliverySystem" +Alias="Dr.T'LeadDeliverySystem" +Alias="Dr.TLeadDeliverySystem" +Alias="DrT'LeadDeliverySystem" +Alias="DrTLeadDeliverySystem" +Alias="DrTDeliverySystem" +Alias="DrTLeadSystem" +Alias="DrTLeadDelivery" +Alias="DrTDelivery" +Alias="LeadDelivery" +Alias="LeadSystem" +Alias="DeliverySystem" +Alias="LeadDS" +Alias="LeadD" +[weapon/KFMod:ThompsonDrumSMG Aliases] +Alias="ThompsonDrumSMG" +Alias="ThompsonDrum" +Alias="ThompDrum" +Alias="RisingStormTommyGun" +Alias="RisingStormTommy" +Alias="RisingStormTomyGun" +Alias="RisingStormTomy" +Alias="RSTommyGun" +Alias="RSTommy" +Alias="RSTomyGun" +Alias="RSTomy" +[weapon/KFMod:AK47AssaultRifle Aliases] +Alias="AK47AssaultRifle" +Alias="AK47Assault" +Alias="AK47Rifle" +Alias="AK47AR" +Alias="AK47" +Alias="AK" +Alias="47" +[weapon/KFMod:GoldenAK47AssaultRifle Aliases] +Alias="GoldAK47AssaultRifle" +Alias="GoldAK47Assault" +Alias="GoldAK47Rifle" +Alias="GoldAK47AR" +Alias="GoldAK47" +Alias="GoldAK" +Alias="Gold47" +[weapon/KFMod:NeonAK47AssaultRifle Aliases] +Alias="NeonAK47AssaultRifle" +Alias="NeonAK47Assault" +Alias="NeonAK47Rifle" +Alias="NeonAK47AR" +Alias="NeonAK47" +Alias="NeonAK" +Alias="Neon47" +[weapon/KFMod:M4AssaultRifle Aliases] +Alias="M4AssaultRifle" +Alias="M4Assault" +Alias="M4Rifle" +Alias="M4" +[weapon/KFMod:M4AssaultRifle Aliases] +Alias="CamoM4AssaultRifle" +Alias="CamoM4Assault" +Alias="CamoM4Rifle" +Alias="CamoM4" +[weapon/KFMod:M4AssaultRifle Aliases] +Alias="CamoM4AssaultRifle" +Alias="CamoM4Assault" +Alias="CamoM4Rifle" +Alias="CamoM4" +[weapon/KFMod:MKb42AssaultRifle Aliases] +Alias="MKb42AssaultRifle" +Alias="MKb42Assault" +Alias="MKb42Rifle" +Alias="MKb42" +Alias="MK42" +Alias="MKb" +[weapon/KFMod:SCARMK17AssaultRifle Aliases] +Alias="SCARMK17AssaultRifle" +Alias="SCARMK17Assault" +Alias="SCARMK17Rifle" +Alias="SCARMKAssaultRifle" +Alias="SCARMKAssault" +Alias="SCARMKRifle" +Alias="SCAR17AssaultRifle" +Alias="SCAR17Assault" +Alias="SCAR17Rifle" +Alias="SCARAssaultRifle" +Alias="SCARAssault" +Alias="SCARRifle" +Alias="SCAR17" +Alias="SCARMK" +Alias="SCAR" +[weapon/KFMod:NeonSCARMK17AssaultRifle Aliases] +Alias="NeonSCARMK17AssaultRifle" +Alias="NeonSCARMK17Assault" +Alias="NeonSCARMK17Rifle" +Alias="NeonSCARMKAssaultRifle" +Alias="NeonSCARMKAssault" +Alias="NeonSCARMKRifle" +Alias="NeonSCAR17AssaultRifle" +Alias="NeonSCAR17Assault" +Alias="NeonSCAR17Rifle" +Alias="NeonSCARAssaultRifle" +Alias="NeonSCARAssault" +Alias="NeonSCARRifle" +Alias="NeonSCAR17" +Alias="NeonSCARMK" +Alias="NeonSCAR" +[weapon/KFMod:FNFAL_ACOG_AssaultRifle Aliases]FNFAL ACOG +Alias="FNFALACOGAssaultRifle" +Alias="FNFALACOGAssault" +Alias="FNFALACOGRifle" +Alias="FNFALAssaultRifle" +Alias="FNFALAssault" +Alias="FNFALRifle" +Alias="FALACOGAssaultRifle" +Alias="FALACOGAssault" +Alias="FALACOGRifle" +Alias="FALAssaultRifle" +Alias="FALAssault" +Alias="FALRifle" +Alias="FNFALACOG" +Alias="FNFAL" +Alias="FALACOG" +Alias="FAL" +Alias="FN" + +; Berserker weapons +[weapon/KFMod:Knife Aliases] +Alias="Knife" +[weapon/KFMod:Machete Aliases] +Alias="Machete" +Alias="Chete" +[weapon/KFMod:Axe Aliases] +Alias="Axe" +Alias="FireAxe" +[weapon/KFMod:Katana Aliases] +Alias="Katana" +[weapon/KFMod:GoldenKatana Aliases] +Alias="GoldKatana" +[weapon/KFMod:Scythe Aliases] +Alias="Scythe" +Alias="Scyte" +Alias="Sickle" +Alias="Sickl" +[weapon/KFMod:Chainsaw Aliases] +Alias="Chainsaw" +Alias="Saw" +Alias="Denji" +Alias="Pochita" +[weapon/KFMod:GoldenChainsaw Aliases] +Alias="GoldChainsaw" +Alias="GoldSaw" +Alias="GoldDenji" +Alias="GoldPochita" +[weapon/KFMod:DwarfAxe Aliases] +Alias="DwarfsAxe" +Alias="DwarfAxe" +Alias="ShitAxe" +Alias="CrapAxe" +Alias="PushAxe" +Alias="GnomeAxe" +Alias="TrollAxe" +Alias="NoobAxe" +[weapon/KFMod:ClaymoreSword Aliases] +Alias="ClaymoreSword" +Alias="ClaymoreBlade" +Alias="Claymore" +Alias="Claymor" +Alias="ClaimoreSword" +Alias="ClaimoreBlade" +Alias="Claimore" +Alias="Claimor" +Alias="Sword" +Alias="Blade" +[weapon/KFMod:Crossbuzzsaw Aliases] +Alias="Crossbuzzsaw" +Alias="Buzzsaw" +Alias="Buzz" +Alias="BuzzsawBow" +Alias="BuzzBow" +Alias="ZerkBow" + +; Firebug weapons +[weapon/KFMod:MAC10MP Aliases] +Alias="MAC10MP" +Alias="MAC10" +Alias="MAC" +[weapon/KFMod:FlareRevolver Aliases] +Alias="FlareRevolver" +Alias="FireRevolver" +Alias="FlareGun" +Alias="Flares" +Alias="Flare" +[weapon/KFMod:DualFlareRevolver Aliases] +Alias="DualFlareRevolvers" +Alias="DualFlareRevolver" +Alias="DualFireRevolvers" +Alias="DualFireRevolver" +Alias="DualFlareGuns" +Alias="DualFlareGun" +Alias="DualFlares" +Alias="DualFlare" +[weapon/KFMod:FlameThrower Aliases] +Alias="FlameThrower" +Alias="FireThrower" +Alias="FThrower" +Alias="Flamer" +Alias="FireSpam" +[weapon/KFMod:GoldenFlamethrower Aliases] +Alias="GoldFlameThrower" +Alias="GoldFireThrower" +Alias="GoldFThrower" +Alias="GoldFlamer" +Alias="GoldFireSpam" +[weapon/KFMod:Trenchgun Aliases] +Alias="DragonsBreathTrenchgun" +Alias="DragonsBreathGun" +Alias="DragonsBreath" +Alias="DragBreathTrenchgun" +Alias="DragBreathGun" +Alias="DragBreath" +Alias="Trenchgun" +Alias="FireShotgun" +Alias="Flameshotgun" +[weapon/KFMod:HuskGun Aliases] +Alias="HuskFireballLauncher" +Alias="HuskFireball" +Alias="FireballLauncher" +Alias="HuskLauncher" +Alias="HuskFirebalLauncher" +Alias="HuskFirebal" +Alias="FirebalLauncher" +Alias="HuskLauncher" +Alias="HuskGun" +Alias="Husk" + +; Demolition weapons +[weapon/KFMod:M79GrenadeLauncher Aliases] +Alias="M79GrenadeLauncher" +Alias="M79Grenade" +Alias="M79Launcher" +Alias="M79NadeLauncher" +Alias="M79Nade" +Alias="M79GrenadeLauncher" +Alias="M79" +[weapon/KFMod:GoldenM79GrenadeLauncher Aliases] +Alias="GoldM79GrenadeLauncher" +Alias="GoldM79Grenade" +Alias="GoldM79Launcher" +Alias="GoldM79NadeLauncher" +Alias="GoldM79Nade" +Alias="GoldM79GrenadeLauncher" +Alias="GoldM79" +[weapon/KFMod:SPGrenadeLauncher Aliases] +Alias="SPGrenadeLauncher" +Alias="SPNadeLauncher" +Alias="SPLauncher" +Alias="SPNade" +Alias="TheOrcaBombPropeller" +Alias="TheOrcaBombPropeler" +Alias="TheOrcaBomb" +Alias="TheOrca" +Alias="TheOrcaLauncher" +Alias="OrcaBombPropeller" +Alias="OrcaBombPropeler" +Alias="OrcaBomb" +Alias="Orca" +Alias="OrcaLauncher" +[weapon/KFMod:PipeBombExplosive Aliases] +Alias="PipeBombExplosive" +Alias="PipeExplosive" +Alias="PipeBomb" +Alias="Pipes" +Alias="Pipe" +[weapon/KFMod:SealSquealHarpoonBomber Aliases] +Alias="SealSquealHarpoonBomber" +Alias="SealSquealHarpoon" +Alias="SealSquealBomber" +Alias="SealHarpoonBomber" +Alias="SealHarpoon" +Alias="SealBomber" +Alias="SealSqueal" +Alias="HarpoonBomber" +Alias="Harpoon" +Alias="Harp" +[weapon/KFMod:SeekerSixRocketLauncher Aliases] +Alias="SeekerSixRocketLauncher" +Alias="SeekerSixLauncher" +Alias="Seeker6RocketLauncher" +Alias="Seeker6Launcher" +Alias="SeekerRocketLauncher" +Alias="SeekerLauncher" +Alias="SeekerSix" +Alias="Seeker6" +Alias="Seeker" +Alias="SuckerSix" +Alias="Sucker6" +Alias="Sucker" +[weapon/KFMod:M4203AssaultRifle Aliases] +Alias="M4203Assault" +Alias="M4203Rifle" +Alias="M4203" +Alias="M4200" +Alias="M420" +Alias="M42" +[weapon/KFMod:LAW Aliases] +Alias="LAW" +[weapon/KFMod:M32GrenadeLauncher Aliases] +Alias="M32GrenadeLauncher" +Alias="M32Grenade" +Alias="M32Launcher" +Alias="M32NadeLauncher" +Alias="M32Nade" +Alias="M32GrenadeLauncher" +Alias="M32" +[weapon/KFMod:CamoM32GrenadeLauncher Aliases] +Alias="CamoM32GrenadeLauncher" +Alias="CamoM32Grenade" +Alias="CamoM32Launcher" +Alias="CamoM32NadeLauncher" +Alias="CamoM32Nade" +Alias="CamoM32GrenadeLauncher" +Alias="CamoM32" + +; Off-perk weapons +[weapon/KFMod:ZEDGun Aliases] +Alias="ZedEradicationDevice" +Alias="ZedEradication" +Alias="ZedDevice" +Alias="ZEDGun" +Alias="ZED" +[weapon/KFMod:ZEDMKIIWeapon Aliases] +Alias="ZedEradicationDeviceMKII" +Alias="ZedEradicationMKII" +Alias="ZedDeviceMKII" +Alias="ZEDGunMKII" +Alias="ZEDMKII" +Alias="ZedEradicationDeviceMK2" +Alias="ZedEradicationMK2" +Alias="ZedDeviceMK2" +Alias="ZEDGunMK2" +Alias="ZEDMK2" +Alias="ZedEradicationDeviceMK" +Alias="ZedEradicationMK" +Alias="ZedDeviceMK" +Alias="ZEDGunMK" +Alias="ZEDMK" +Alias="ZedEradicationDevice2" +Alias="ZedEradication2" +Alias="ZedDevice2" +Alias="ZEDGun2" +Alias="ZED2" + +; For the puposes of unit-testing. +; Changing these can break unit tests. +; +; If you don't plan to run unit tests or do not know what they are, - +; feel free to remove records below. +[test/car Aliases] +Alias="Ford" +Alias="Delorean" +Alias="Audi" +[test/scifi Aliases] +Alias="Spice" +Alias="HardToBeAGod" \ No newline at end of file diff --git a/sources/Core/Aliases/Aliases.uc b/sources/Core/Aliases/Aliases.uc new file mode 100644 index 0000000..4e4ecab --- /dev/null +++ b/sources/Core/Aliases/Aliases.uc @@ -0,0 +1,140 @@ +/** + * Aliases allow users to define human-readable and easier to use + * "synonyms" to some symbol sequences (mainly names of UnrealScript classes). + * Due to how aliases are stored, there is a limitation on original + * values to which aliases refer: it must be a valid object name to store via + * `perObjectConfig`. For example it cannot contain `]` or a dot `.` + * (use `:` as a delimiter for class names: `KFMod:M14EBRBattleRifle`). + * Aliases can be grouped into categories: "weapons", "test", "maps", etc. + * Aliases can be configured in `AcediaAliases` in form: + * ________________________________________________________________________ + * | [/ Aliases] + * | Alias="" + * | Alias="" + * | ... + * |_______________________________________________________________________ + * where , , , ... can be replaced with + * desired values. + * 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 Aliases extends AcediaObject + perObjectConfig + config(AcediaAliases); + +/** + * All data is stored in config as a bunch of named `Aliases` objects + * (via `perObjectConfig`). Name of each object records both aliases group and + * value (see class description for details). + * Aliases themselves are recorded into the `alias` array. + */ + +// Stores name of the configuration file. +var private const string configName; +// Both value +// Symbol (or symbol sequence) that separates value from the group in +// `[/ Aliases]`. +var private const string delimiter; + +// Set once to prevent more than one object loading. +var private bool initialized; + +// All aliases objects, specified by the configuration file. +var private array availableRecords; + +// Data loaded from the configuration file into the `Aliases` object. +// Value to which all aliases refer to. +var private string originalValue; +// Group to which this object's aliases belong to. +var private string groupName; +// Recorded aliases ("synonyms") for the `originalValue`. +var public config array alias; + +// Initializes data that we can not directly read from the configuration file. +private final function Initialize() +{ + if (initialized) return; + + availableRecords.length = 0; + ParseObjectName(string(self.name)); + initialized = true; +} + +private final function ParseObjectName(string configName) +{ + local int i; + local array splitName; + Split(configName, "/", splitName); + groupName = splitName[0]; + originalValue = ""; + for (i = 1; i < splitName.length; i += 1) + { + originalValue $= splitName[i]; + } +} + +// This function loads all the defined aliases from the config file. +// Need to only be called once, further calls do nothing. +public static final function LoadAliases() +{ + local int i; + local array recordNames; + if (default.initialized) return; + recordNames = + GetPerObjectNames(default.configName, string(class'Aliases'.name)); + for (i = 0; i < recordNames.length; i += 1) + { + default.availableRecords[i] = new(none, recordNames[i]) class'Aliases'; + if (default.availableRecords[i] != none) + { + default.availableRecords[i].Initialize(); + } + } + default.initialized = true; +} + +// Tries to find original value for a given alias in a given group. +public static final function bool ResolveAlias +( + string group, + string alias, + out string result +) +{ + local int i, j; + if (!default.initialized) return false; + for (i = 0; i < default.availableRecords.length; i += 1) + { + if (!(default.availableRecords[i].groupName ~= group)) continue; + for (j = 0; j < default.availableRecords[i].alias.length; j += 1) + { + if (default.availableRecords[i].alias[j] ~= alias) + { + result = default.availableRecords[i].originalValue; + return true; + } + } + } + return false; +} + +defaultproperties +{ + initialized = false + configName = "AcediaAliases" + delimiter = "/" +} \ No newline at end of file diff --git a/sources/Core/Aliases/AliasesAPI.uc b/sources/Core/Aliases/AliasesAPI.uc new file mode 100644 index 0000000..7ebab8c --- /dev/null +++ b/sources/Core/Aliases/AliasesAPI.uc @@ -0,0 +1,43 @@ +/** + * Provides convenient access to Aliases-related functions. + * Copyright 2019 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 AliasesAPI extends Singleton; + +// Resolves original value for given alias and it's group. +// Returns `false` if there no such alias and `true` if there is. +public function bool Resolve(string group, string alias, out string result) +{ + return class'Aliases'.static.ResolveAlias(group, alias, result); +} + +// Tries to resolve given alias. +// If fails - returns passed `alias` value back. +public function string Try(string group, string alias) +{ + local string result; + if (class'Aliases'.static.ResolveAlias(group, alias, result)) + { + return result; + } + return alias; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Core/Aliases/Tests/TEST_Aliases.uc b/sources/Core/Aliases/Tests/TEST_Aliases.uc new file mode 100644 index 0000000..84c54ae --- /dev/null +++ b/sources/Core/Aliases/Tests/TEST_Aliases.uc @@ -0,0 +1,53 @@ +/** + * Set of tests for Aliases system. + * 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 TEST_Aliases extends TestCase + abstract; + +protected static function TESTS() +{ + local string result; + Context("Testing Aliases loading."); + Issue("`Try` cannot resolve correct alias"); + TEST_ExpectTrue(_().alias.Try("test", "Ford") == "car"); + TEST_ExpectTrue(_().alias.Try("test", "Delorean") == "car"); + TEST_ExpectTrue(_().alias.Try("test", "HardToBeAGod") == "scifi"); + + Issue("`Resolve` cannot resolve correct alias"); + _().alias.Resolve("test", "Ford", result); + TEST_ExpectTrue(result == "car"); + _().alias.Resolve("test", "Audi", result); + TEST_ExpectTrue(result == "car"); + _().alias.Resolve("test", "Spice", result); + TEST_ExpectTrue(result == "scifi"); + + Issue("`Try` does not return original alias for non-existing alias record"); + TEST_ExpectTrue(_().alias.Try("test", "AllFiction") == "AllFiction"); + + Issue("`Resolve` reports success when it failed"); + TEST_ExpectFalse(_().alias.Resolve("test", "KarmicJustice", result)); + + Issue("`Resolve` reports failure when it succeeds"); + TEST_ExpectTrue(_().alias.Resolve("test", "Delorean", result)); +} + +defaultproperties +{ + caseName = "Aliases" +} \ No newline at end of file diff --git a/sources/Global.uc b/sources/Global.uc index 75c0728..3af79dd 100644 --- a/sources/Global.uc +++ b/sources/Global.uc @@ -21,12 +21,14 @@ */ class Global extends Singleton; -var public Acedia acedia; -var public JSONAPI json; +var public Acedia acedia; +var public JSONAPI json; +var public AliasesAPI alias; protected function OnCreated() { acedia = class'Acedia'.static.GetInstance(); Spawn(class'JSONAPI'); json = JSONAPI(class'JSONAPI'.static.GetInstance()); + alias = AliasesAPI(class'AliasesAPI'.static.GetInstance()); } \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 571093e..3ccaa5b 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -46,4 +46,5 @@ defaultproperties requiredListeners(0) = class'MutatorListener_Connection' // Unit tests testCases(0) = class'TEST_JSON' + testCases(1) = class'TEST_Aliases' } \ No newline at end of file From 4e07eb6c51b00c3df677696f8658c145e9a0882f Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 14 Apr 2020 01:51:09 +0700 Subject: [PATCH 11/59] Fix deprecated `Feature` file after refactoring --- sources/Core/Feature.uc | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/sources/Core/Feature.uc b/sources/Core/Feature.uc index a59eabf..675f962 100644 --- a/sources/Core/Feature.uc +++ b/sources/Core/Feature.uc @@ -68,8 +68,8 @@ public static final function Feature EnableMe() } // Event functions that are called when -public function OnEnabled(){} -public function OnDisabled(){} +protected function OnEnabled(){} +protected function OnDisabled(){} // Set listeners' status private static function SetListenersActiveSatus(bool newStatus) @@ -82,27 +82,15 @@ private static function SetListenersActiveSatus(bool newStatus) } } -// 'OnEnabled' and 'OnDisabled' should be called from functions that -// will be called regardless of whether 'Feature' was created -// with 'ChangeEnabledState' or in some other way. -event PreBeginPlay() +protected function OnCreated() { - super.PreBeginPlay(); - // '!bDeleteMe' means that we will be a singleton instance, - // meaning that we only just got enabled. - if (!bDeleteMe && IsEnabled()) - { - // Block spawning this feature before calling any other events - default.blockSpawning = true; - SetListenersActiveSatus(true); - OnEnabled(); - return; - } + default.blockSpawning = true; + SetListenersActiveSatus(true); + OnEnabled(); } -event Destroyed() +protected function OnDestroyed() { - super.Destroyed(); SetListenersActiveSatus(false); OnDisabled(); } From fbc8abd48c2f9c67a4b302ad7e0bf2c925f13f8e Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 14 Apr 2020 01:51:49 +0700 Subject: [PATCH 12/59] Fix `JSONAPI` class description --- sources/Core/Data/JSON/JSONAPI.uc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sources/Core/Data/JSON/JSONAPI.uc b/sources/Core/Data/JSON/JSONAPI.uc index 12d4c9d..8e88fa6 100644 --- a/sources/Core/Data/JSON/JSONAPI.uc +++ b/sources/Core/Data/JSON/JSONAPI.uc @@ -1,8 +1,5 @@ /** - * Singleton is an auxiliary class, meant to be used as a base for others, - * that allows for only one instance of it to exist. - * To make sure your child class properly works, either don't overload - * 'PreBeginPlay' or make sure to call it's parent's version. + * Provides convenient access to JSON-related functions. * Copyright 2019 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. From 960e787de73bf2ab2f47b63bab5acb8009fa04b7 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:16:21 +0700 Subject: [PATCH 13/59] Refactor `Text` subsystem --- sources/Core/Text/Parser.uc | 1312 +++++++ sources/Core/Text/Tests/TEST_Parser.uc | Bin 0 -> 118744 bytes sources/Core/Text/Tests/TEST_Text.uc | Bin 0 -> 21740 bytes sources/Core/Text/Tests/TEST_TextAPI.uc | Bin 0 -> 67104 bytes sources/Core/Text/Text.uc | 290 ++ sources/Core/Text/TextAPI.uc | 1281 +++++++ sources/Core/Text/UnicodeData.uc | 4320 +++++++++++++++++++++++ 7 files changed, 7203 insertions(+) create mode 100644 sources/Core/Text/Parser.uc create mode 100644 sources/Core/Text/Tests/TEST_Parser.uc create mode 100644 sources/Core/Text/Tests/TEST_Text.uc create mode 100644 sources/Core/Text/Tests/TEST_TextAPI.uc create mode 100644 sources/Core/Text/Text.uc create mode 100644 sources/Core/Text/TextAPI.uc create mode 100644 sources/Core/Text/UnicodeData.uc diff --git a/sources/Core/Text/Parser.uc b/sources/Core/Text/Parser.uc new file mode 100644 index 0000000..0571b7b --- /dev/null +++ b/sources/Core/Text/Parser.uc @@ -0,0 +1,1312 @@ +/** + * Implements a simple `Parser` with built-in functions to parse simple + * UnrealScript's types and support for saving / restoring parser states. + * 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 Parser extends AcediaObject + dependson(Text) + dependson(UnicodeData); + +var public int BYTE_MAX; +var public int CODEPOINT_BACKSLASH; +var public int CODEPOINT_USMALL; +var public int CODEPOINT_ULARGE; + +// The sequence of Unicode code points that this `Parser` is supposed to parse. +var private array content; +// Incremented each time `Parser` is reinitialized with new `content`. +// Can be used to make `Parser` object completely independent from +// it's past, necessary since garbage collection is extra expensive in UE2 +// and we want to reuse created objects as much as possible. +var private int version; + +// Describes current state of the `Parser`, instance of this struct +// can be used to revert parser back to this state. +struct ParserState +{ + // Record to which object (and of what version) this state belongs to. + // This information is used to make sure that we apply this state + // only to same `Parser` (of the same version) that it originated from. + var private AcediaObject ownerObject; + var private int ownerVersion; + // Has parser failed at some point? + var private bool failed; + // Points at the next symbol to be used next in parsing. + var private int pointer; +}; +var private ParserState currentState; +// For convenience `Parser` will store one internal state that designates +// a state that's safe to revert to when some parsing attempt goes wrong. +// @see `Confirm()`, `R()` +var private ParserState confirmedState; + +// Describes rules for translating escaped sequences ("\r", "\n", "\t") +// into appropriate code points. +var private const array escapeCharactersMap; + +// Used to store a result of a `ParseSign()` function. +enum ParsedSign +{ + SIGN_Missing, + SIGN_Plus, + SIGN_Minus +}; + +/** + * Initializes `Parser` with new data from a raw data + * (sequence of Unicode code points). Never fails. + * + * Any data from before this call is lost, any checkpoints are invalidated. + * + * @param source Sequence of Unicode code points that represents + * a string `Parser` will need to parse. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser InitializeRaw(array source) +{ + content = source; + version += 1; + currentState.ownerObject = self; + currentState.ownerVersion = version; + currentState.failed = false; + currentState.pointer = 0; + confirmedState = currentState; + return self; +} + +/** + * Initializes `Parser` with new data from a `string`. Never fails. + * + * Any data from before this call is lost, any checkpoints are invalidated. + * + * @param source String `Parser` will need to parse. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser Initialize +( + string source, + optional Text.StringType sourceType +) +{ + InitializeRaw(_().text.StringToRaw(source, sourceType)); + return self; +} + +/** + * Initializes `Parser` with new data from a `Test`. + * + * Can fail if passed `none` as a parameter. + * + * Any data from before this call is lost, any checkpoints are invalidated. + * + * @param source `Text` object `Parser` will need to parse. + * If `none` is passed - parser won't be initialized. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser InitializeT(Text source) +{ + if (source == none) return self; + InitializeRaw(source.ToRaw()); + return self; +} + +/** + * Checks if `Parser` is in a failed state. + * + * Parser enters a failed state whenever any parsing call returns without + * completing it's job. `Parser` in a failed state will automatically fail + * any further parsing attempts until it gets reset via `R()` call. + * + * @return Returns 'false' if `Parser()` is in a failed state and + * `true` otherwise. + */ +public final function bool Ok() +{ + return (!currentState.failed); +} + +/** + * Returns copy of the current state of this parser. + * + * As long as caller `Parser` was not reinitialized, returned `ParserState` + * structure can be used to revert this `Parser` to it's current condition + * by a `RestoreState()` call. + * + * @see `RestoreState()` + * @return Copy of the current state of the caller `Parser`. + */ +public final function ParserState GetCurrentState() +{ + return currentState; +} + +/** + * Returns copy of (currently) last confirmed state of this parser. + * + * As long as caller `Parser` was not reinitialized, returned `ParserState` + * structure can be used to revert this `Parser` to it's current confirmed + * state by a `RestoreState()` call. + * + * @see `RestoreState()`, `Confirm()`, `R()` + * @return Copy of (currently) last confirmed state of this parser. + */ +public final function ParserState GetConfirmedState() +{ + return confirmedState; +} + +/** + * Checks if given `stateToCheck` is valid for the caller `Parser`, i.e.: + * 1. It is a state generated by either `GetCurrentState()` or + * `GetConfirmedState()` calls on the caller `Parser`. + * 2. Caller `Parser` was not reinitialized since a call + * that generated given `stateToCheck`. + * + * @param stateToCheck `ParserState` to check for validity for + * caller `Parser`. + * @return `true` if given `stateToCheck` is valid and `false` otherwise. + */ +public final function bool IsStateValid(ParserState stateToCheck) +{ + if (stateToCheck.ownerObject != self) return false; + if (stateToCheck.ownerVersion != version) return false; + return true; +} + +/** + * Checks if calling `RestoreState()` for passed state will return a `Parser` + * in an "Ok" state (not failed), i.e. state is valid and + * was generated when `Parser` was in a non-failed state. + * + * @param stateToCheck `ParserState` to check for corresponding to + * `Parser` being in a non-failed state. + * By definition must also be valid for the caller `Parser`. + * @return `true` if given `stateToCheck` is valid and `false` otherwise. + */ +public final function bool IsStateOk(ParserState stateToCheck) +{ + if (!IsStateValid(stateToCheck)) return false; + return (!stateToCheck.failed); +} + +/** + * Resets parser to a state, given by `stateToRestore` argument + * (so a state `Parser` was in at the moment given `stateToRestore` + * was obtained). + * + * If given `stateToRestore` is from a different `Parser` or + * the owner `Parser` was reinitialized after passed state was obtained, - + * function will simply put caller `Parser` into a failed state. + * Note that caller `Parser` being put in a failed state after this call + * doesn't mean that described issues are actually present: + * `stateToRestore` can also describe a failed state of the `Parser`. + * + * @param stateToRestore `ParserState` that this method will attempt + * to set for the caller `Parser`. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser RestoreState(ParserState stateToRestore) +{ + if (!IsStateValid(stateToRestore)) + { + currentState.failed = true; + return self; + } + currentState = stateToRestore; + return self; +} + + /** + * Remembers current state of `Parser` in an internal checkpoint variable, + * that can later be restored by an `R()` call. + * + * Can only save non-failed states and will only fail if caller `Parser` is + * in a failed state. + * + * `Confirm()` and `R()` are essentially convenience wrapper functions for + * `GetCurrentState()` and `RestoreState()` calls + + * state storage variable. + * + * @return `true` if current state is recorded in `Parser` as confirmed and + * `false` otherwise. + */ +public final function bool Confirm() +{ + if (!Ok()) return false; + + confirmedState = currentState; + return true; +} + +/** + * Resets `Parser` to a last state recorded as confirmed by a last successful + * `Confirm()` function call. If there weren't any such call - + * reverts `Parser` to it's state right after initialization. + * + * Always resets failed state of a `Parser`. Cannot fail. + * + * `Confirm()` and `R()` are essentially convenience wrapper functions for + * `GetCurrentState()` and `RestoreState()` calls + state storage variable. + * + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser R() +{ + currentState = confirmedState; + return self; +} + +/** + * Shifts parsing pointer forward. + * + * Can only shift forward. To revert to a previous state in case of failure use + * combination of `GetCurrentState()` and `RestoreState()` functions. + * + * @param shift How much to shift parsing pointer? + * Values of zero and below are discarded and `1` is used instead + * (i.e. by default this method shifts pointer by `1` position). + * @return Returns the calling object, to allow for function chaining. + */ +protected final function Parser ShiftPointer(optional int shift) +{ + shift = Max(1, shift); + currentState.pointer = Min(currentState.pointer + shift, content.length); + return self; +} + +/** + * Returns a code point from this `Parser`'s content, relative to next + * code point that caller `Parser` must handle. + * + * @param `shift` If `0` (default value) or negative value is passed - + * simply asks for the code point that caller `Parser` must handle. + * Otherwise shifts that index `shift` code points, i.e. + * `1` to return next code point or `2` to return code point after + * the next one. + * @return Returns code point at a given shift. If `shift` is too small/large + * and does not fit `Parser`'s contents, returns `-1`. + * `GetCodePoint()` with default (`0`) parameter can also return `-1` if + * contents of the caller `Parser` are empty or it has already consumed + * all input. + */ +protected final function Text.Character GetCharacter(optional int shift) +{ + local Text.Character invalidCharacter; + local int absoluteAddress; + absoluteAddress = currentState.pointer + Max(0, shift); + if (absoluteAddress < 0 || absoluteAddress >= content.length) + { + invalidCharacter.codePoint = -1; + return invalidCharacter; + } + return content[absoluteAddress]; +} + +/** + * Forces caller `Parser` to enter a failed state. + * + * @return Returns the calling object, to allow for a quick exit from + * a parsing function by `return Fail();`. + */ +protected final function Parser Fail() +{ + currentState.failed = true; + return self; +} + +/** + * Returns amount of code points that have already been parsed, + * provided that caller `Parser` is in a correct state. + * + * @return Returns how many Unicode code points have already been parsed if + * caller `Parser` is in correct state; + * otherwise return value is undefined. + */ +public final function int GetParsedLength() +{ + return Max(0, currentState.pointer); +} + +/** + * Returns amount of code points that have not yet been parsed, + * provided that caller `Parser` is in a correct state. + * + * @return Returns how many Unicode code points are still unparsed if + * caller `Parser` is in correct state; + * otherwise return value is undefined. + */ +public final function int GetRemainingLength() +{ + return Max(0, content.length - currentState.pointer); +} + +/** + * Checks if caller `Parser` has already parsed all of it's content. + * Uninitialized `Parser` has no content and, therefore, parsed it all. + * + * Should return `true` iff `GetRemainingLength() == 0`. + * + * @return `true` if caller `Parser` has no more data to parse. + */ +public final function bool HasFinished() +{ + return (currentState.pointer >= content.length); +} + +/** + * Returns still unparsed part of caller `Parser`'s source as an array of + * Unicode code points. + * + * @return Unparsed part of caller `Parser`'s source as an array of + * Unicode code points. + */ +public final function array GetRemainderRaw() +{ + local int i; + local array result; + for (i = 0; i < GetRemainingLength(); i += 1) + { + result[result.length] = GetCharacter(i); + } + return result; +} + +/** + * Returns still unparsed part of caller `Parser`'s source as a `string`. + * + * @return Unparsed part of caller `Parser`'s source as a `string`. + */ +public final function string GetRemainder() +{ + local int i; + local array rawResult; + for (i = 0; i < GetRemainingLength(); i += 1) + { + rawResult[rawResult.length] = GetCharacter(i); + } + return _().text.RawToString(rawResult, STRING_Plain); +} + +/** + * Returns still unparsed part of caller `Parser`'s source as `Text`. + * + * @return Unparsed part of caller `Parser`'s source as `Text`. + */ +public final function Text GetRemainderT() +{ + local int i; + local array rawResult; + for (i = 0; i < GetRemainingLength(); i += 1) + { + rawResult[rawResult.length] = GetCharacter(i); + } + return _().text.FromRaw(rawResult); +} + +/** + * Matches any sequence of whitespace symbols, without returning it. + * Starts from where previous parsing function finished. + * + * Can never cause parser to enter failed state. + * + * What symbols exactly are considered whitespace refer to the description of + * `TextAPI.IsWhitespace()` function. + * + * @param whitespacesAmount Returns how many whitespace symbols + * were skipped. Any given value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser Skip(optional out int whitespacesAmount) +{ + local TextAPI api; + if (!Ok()) return self; + + api = _().text; + whitespacesAmount = 0; + // Cycle will end once we either reach a non-whitespace symbol or + // there's not more code points to get + while (api.IsWhitespace(GetCharacter(whitespacesAmount))) + { + whitespacesAmount += 1; + } + ShiftPointer(whitespacesAmount); + return self; +} + +/** + * Function that tries to match given data in `Parser`'s content, + * starting from where previous parsing function finished. + * + * Does nothing if caller `Parser` was in failed state. + * + * @param data Data that must be matched to the `Parser`'s + * contents, starting from where previous parsing function finished. + * @param caseInsensitive If `false` the matching will have to be exact, + * using `true` will make this method to ignore the case, + * where it's applicable. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MatchRaw +( + array data, + optional bool caseInsensitive +) +{ + local int i; + local TextAPI api; + if (!Ok()) return self; + if (data.length > GetRemainingLength()) return Fail(); + + api = _().text; + for (i = 0; i < data.length; i += 1) + { + if (!api.AreEqual(data[i], GetCharacter(i), caseInsensitive)) + { + return Fail(); + } + } + ShiftPointer(data.length); + return self; +} + +/** + * Function that tries to match given `string`, starting from where + * previous parsing function finished. + * + * Does nothing if caller `Parser` was in failed state. + * + * @param word String that must be matched to the `Parser`'s + * contents, starting from where previous parsing function finished. + * @param caseInsensitive If `false` the matching will have to be exact, + * using `true` will make this method to ignore the case, + * where it's applicable. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser Match(string word, optional bool caseInsensitive) +{ + return MatchRaw(_().text.StringToRaw(word), caseInsensitive); +} + +/** + * Function that tries to match given `Text`, starting from where + * previous parsing function finished. + * + * Does nothing if caller `Parser` was in failed state. + * + * @param word Text that must be matched to the `Parser`'s + * contents, starting from where previous parsing function finished. + * @param caseInsensitive If `false` the matching will have to be exact, + * using `true` will make this method to ignore the case, + * where it's applicable. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MatchT(Text word, optional bool caseInsensitive) +{ + if (!Ok()) return self; + if (word == none) return Fail(); + + return MatchRaw(word.ToRaw(), caseInsensitive); +} + +/** + * Internal function for parsing unsigned integers in any base from 2 to 36. + * + * This parsing can fail, putting `Parser` into a failed state. + * + * @param result If parsing is successful, this value will contain + * parsed integer, otherwise value is undefined. + * Any passed value is discarded. + * @param base Base, in which integer in question is recorded. + * @param numberLength If this parameter is less or equal to zero, + * function will stop parsing the moment it can't recognize a character as + * belonging to a number in a given base. + * It will only fail if it couldn't parse a single character; + * If this parameter is set to be positive (`> 0`), function will + * attempt to use exactly `numberLength` character for parsing and will + * fail if they would not constitute a valid number. + * @param consumedCodePoints Amount of code point used (consumed) to parse + * this number; undefined, if parsing is unsuccessful. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MUnsignedInteger +( + out int result, + optional int base, + optional int numberLength, + optional out int consumedCodePoints +) +{ + local bool parsingFixedLength; + local int nextPosition; + numberLength = Max(0, numberLength); + parsingFixedLength = (numberLength != 0); + if (base == 0) + { + base = 10; + } + else if (base < 2 || base > 36) + { + return Fail(); + } + result = 0; + consumedCodePoints = 0; + while (!HasFinished()) + { + if (parsingFixedLength && consumedCodePoints >= numberLength) break; + nextPosition = _().text.CharacterToInt(GetCharacter(), base); + if (nextPosition < 0) break; + + result = result * base + nextPosition; + consumedCodePoints += 1; + ShiftPointer(); + } + if ( parsingFixedLength && consumedCodePoints != numberLength + || consumedCodePoints < 1) + { + return Fail(); + } + return self; +} + +/** + * Parses escaped sequence of the type that is usually used in + * string literals: backslash "\"", followed by any character + * (called escaped character later) or, in special cases, several characters. + * For most characters escaped sequence resolved into + * an escaped character's code point. + * + * Several escaped symbols: + * \n, \r, \t, \b, \f, \v + * are translated into a different code point corresponding to + * a control symbols, normally denoted by these sequences. + * + * A Unicode code point can also be directly entered with either of the two + * commands: + * \U0056 + * \u56 + * The difference is that `\U` allows you to enter two-byte code point, while + * `\u` only allows to define code points that fit into 1 byte, + * but is more compact. + * + * @param denotedCodePoint If parsing is successful, parameter will contain + * appropriate code point, denoted by a parsed escaped sequence; + * If parsing is unsuccessful, value is undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MEscapedSequence +( + out Text.Character denotedCharacter +) +{ + local int i; + if (!Ok()) return self; + // Need at least two characters to parse escaped sequence + if (GetRemainingLength() < 2) return Fail(); + if (GetCharacter().codePoint != CODEPOINT_BACKSLASH) return Fail(); + + denotedCharacter = GetCharacter(1); + ShiftPointer(2); + // Escaped character denotes some special code point + for (i = 0; i < escapeCharactersMap.length; i += 1) + { + if (escapeCharactersMap[i].from == denotedCharacter.codePoint) + { + denotedCharacter.codePoint = escapeCharactersMap[i].to; + return self; + } + } + // Escaped character denotes declaration of arbitrary Unicode code point + if (denotedCharacter.codePoint == CODEPOINT_ULARGE) + { + MUnsignedInteger(denotedCharacter.codePoint, 16, 4); + } + else if (denotedCharacter.codePoint == CODEPOINT_USMALL) + { + MUnsignedInteger(denotedCharacter.codePoint, 16, 2); + } + return self; +} + +/** + * Attempts to parse a string literal: a string enclosed in either of + * the following quotation marks: ", ', `. + * String literals can contain escaped sequences. + * String literals MUST end with closing quotation mark. + * @see `MEscapedSequence()` + * + * @param result If parsing is successful, this array will contain the + * contents of string literal with resolved escaped sequences; + * if parsing has failed, it's value is undefined. + * Any passed contents are simply discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MStringLiteralRaw(out array result) +{ + local TextAPI api; + local Text.Character nextCharacter; + local Text.Character usedQuotationMark; + local Text.Character escapedCharacter; + if (!Ok()) return self; + usedQuotationMark = GetCharacter(); + if (!_().text.IsQuotationMark(usedQuotationMark)) return Fail(); + + ShiftPointer(); // Skip opening quotation mark + api = _().text; + result.length = 0; + while (!HasFinished()) + { + nextCharacter = GetCharacter(); + // Closing quote + if (api.AreEqual(nextCharacter, usedQuotationMark)) + { + ShiftPointer(); + return self; + } + // Escaped characters + if (api.IsCodePoint(nextCharacter, CODEPOINT_BACKSLASH)) + { + if (!MEscapedSequence(escapedCharacter).Ok()) + { + return Fail(); // Backslash MUST mean valid escape sequence + } + result[result.length] = escapedCharacter; + } + // Any other code point + else + { + result[result.length] = nextCharacter; + ShiftPointer(); + } + } + // Content ended without a closing quote. + return Fail(); +} + +/** + * Attempts to parse a string literal: a string enclosed in either of + * the following quotation marks: ", ', `. + * String literals can contain escaped sequences. + * String literals MUST end with closing quotation mark. + * @see `MEscapedSequence()` + * + * @param result If parsing is successful, this `string` will contain the + * contents of string literal with resolved escaped sequences; + * if parsing has failed, it's value is undefined. + * Any passed contents are simply discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MStringLiteral(out string result) +{ + local array rawResult; + if (!Ok()) return self; + + if (MStringLiteralRaw(rawResult).Ok()) + { + result = _().text.RawToString(rawResult, STRING_Plain); + } + return self; +} + +/** + * Attempts to parse a string literal: a string enclosed in either of + * the following quotation marks: ", ', `. + * String literals can contain escaped sequences. + * String literals MUST end with closing quotation mark. + * @see `MEscapedSequence()` + * + * @param result If parsing is successful, this `Text` will contain the + * contents of string literal with resolved escaped sequences; + * if parsing has failed, it's value is undefined. + * Any passed contents are simply discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MStringLiteralT(out Text result) +{ + local array rawResult; + if (!Ok()) return self; + + if (MStringLiteralRaw(rawResult).Ok()) + { + result = _().text.FromRaw(rawResult); + } + return self; +} + +/** + * Matches everything until it finds one of the breaking symbols: + * 1. a specified code point (by default `0`); + * 2. (optionally) whitespace symbol (@see `TextAPI.IsWhitespace()`); + * 3. (optionally) quotation symbol (@see `TextAPI.IsQuotation()`). + * This method cannot fail. + * + * @param result Any content before one of the break symbols + * will be recorded into this array as a sequence of Unicode code points. + * @param codePointBreak Method will stop parsing upon encountering this + * code point (it will not be included in the `result`) + * @param whitespacesBreak `true` if you want to also treat any + * whitespace character as a break symbol + * (@see `TextAPI.IsWhitespace()` for what symbols are + * considered whitespaces) + * @param quotesBreak `true` if you want to also treat any + * quotation mark character as a break symbol + * (@see `TextAPI.IsQuotation()` for what symbols are + * considered quotation marks). + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MUntilRaw +( + out array result, + optional Text.Character characterBreak, + optional bool whitespacesBreak, + optional bool quotesBreak +) +{ + local Text.Character nextCharacter; + local TextAPI api; + if (!Ok()) return self; + + api = _().text; + result.length = 0; + while (!HasFinished()) + { + nextCharacter = GetCharacter(); + if (api.AreEqual(nextCharacter, characterBreak)) break; + if (whitespacesBreak && api.IsWhitespace(nextCharacter)) break; + if (quotesBreak && api.IsQuotationMark(nextCharacter)) break; + + result[result.length] = nextCharacter; + ShiftPointer(); + } + return self; +} + +/** + * Matches everything until it finds one of the breaking symbols: + * 1. a specified code point (by default `0`); + * 2. (optionally) whitespace symbol (@see `TextAPI.IsWhitespace()`); + * 3. (optionally) quotation symbol (@see `TextAPI.IsQuotation()`). + * This method cannot fail. + * + * @param result Any content before one of the break symbols + * will be recorded into this `string`. + * @param codePointBreak Method will stop parsing upon encountering this + * code point (it will not be included in the `result`) + * @param whitespacesBreak `true` if you want to also treat any + * whitespace character as a break symbol + * (@see `TextAPI.IsWhitespace()` for what symbols are + * considered whitespaces) + * @param quotesBreak `true` if you want to also treat any + * quotation mark character as a break symbol + * (@see `TextAPI.IsQuotation()` for what symbols are + * considered quotation marks). + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MUntil +( + out string result, + optional Text.Character characterBreak, + optional bool whitespacesBreak, + optional bool quotesBreak +) +{ + local array rawResult; + if (!Ok()) return self; + + MUntilRaw(rawResult, characterBreak, whitespacesBreak, quotesBreak); + result = _().text.RawToString(rawResult, STRING_Plain); + return self; +} + +/** + * Matches everything until it finds one of the breaking symbols: + * 1. a specified code point (by default `0`); + * 2. (optionally) whitespace symbol (@see `TextAPI.IsWhitespace()`); + * 3. (optionally) quotation symbol (@see `TextAPI.IsQuotation()`). + * This method cannot fail. + * + * @param result Any content before one of the break symbols + * will be recorded into this `Text`. + * @param codePointBreak Method will stop parsing upon encountering this + * code point (it will not be included in the `result`) + * @param whitespacesBreak `true` if you want to also treat any + * whitespace character as a break symbol + * (@see `TextAPI.IsWhitespace()` for what symbols are + * considered whitespaces) + * @param quotesBreak `true` if you want to also treat any + * quotation mark character as a break symbol + * (@see `TextAPI.IsQuotation()` for what symbols are + * considered quotation marks). + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MUntilT +( + out Text result, + optional Text.Character characterBreak, + optional bool whitespacesBreak, + optional bool quotesBreak +) +{ + local array rawResult; + if (!Ok()) return self; + + MUntilRaw(rawResult, characterBreak, whitespacesBreak, quotesBreak); + result = _().text.FromRaw(rawResult); + return self; +} + +/** + * Parses a string as either "simple" or "quoted". + * Not being able to read any symbols is not considered a failure. + * + * Reading empty string (either to lack of further data or + * instantly encountering a break symbol) is not considered a failure. + * + * Quoted string starts with quotation mark and ends either + * at the corresponding closing (un-escaped) mark + * or when `Parser`'s input has been fully consumed. + * If string started with a quotation mark, this method will act exactly + * like `MStringLiteralRaw()`. + * + * @param result If parsing is successful - string's contents will be + * recorded here; if parsing has failed - value is undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MStringRaw(out array result) +{ + if (!Ok()) return self; + + if (_().text.IsQuotationMark(GetCharacter())) + { + MStringLiteralRaw(result); + } + else + { + MUntilRaw(result,, true, true); + } + return self; +} + +/** + * Parses a string as either "simple" or "quoted". + * Not being able to read any symbols is not considered a failure. + * + * Reading empty string (either to lack of further data or + * instantly encountering a break symbol) is not considered a failure. + * + * Quoted string starts with quotation mark and ends either + * at the corresponding closing (un-escaped) mark + * or when `Parser`'s input has been fully consumed. + * If string started with a quotation mark, this method will act exactly + * like `MStringLiteral()`. + * + * @param result If parsing is successful - string's contents will be + * recorded here; if parsing has failed - value is undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MString(out string result) +{ + local array rawResult; + if (!Ok()) return self; + + MStringRaw(rawResult); + result = _().text.RawToString(rawResult, STRING_Plain); + return self; +} + +/** + * Parses a string as either "simple" or "quoted". + * Not being able to read any symbols is not considered a failure. + * + * Reading empty string (either to lack of further data or + * instantly encountering a break symbol) is not considered a failure. + * + * Quoted string starts with quotation mark and ends either + * at the corresponding closing (un-escaped) mark + * or when `Parser`'s input has been fully consumed. + * If string started with a quotation mark, this method will act exactly + * like `MStringLiteralT()`. + * + * @param result If parsing is successful - string's contents will be + * recorded here; if parsing has failed - value is undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MStringT(out Text result) +{ + local array rawResult; + if (!Ok()) return self; + + MStringRaw(rawResult); + result = _().text.FromRaw(rawResult); + return self; +} + +/** + * Matches a non-empty sequence of whitespace symbols. + * + * Cannot fail (not being able to read any input is not considered a failure). + * + * @param result If parsing was successful - whitespaces' Unicode code points + * will be recorded in this array, otherwise - undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MWhitespacesRaw(out array result) +{ + local Text.Character nextCharacter; + local TextAPI api; + if (!Ok()) return self; + + api = _().text; + result.length = 0; + while (!HasFinished()) + { + nextCharacter = GetCharacter(); + if (!api.IsWhitespace(nextCharacter)) break; + result[result.length] = nextCharacter; + ShiftPointer(); + } + return self; +} + +/** + * Matches a non-empty sequence of whitespace symbols. + * + * Cannot fail (not being able to read any input is not considered a failure). + * + * @param result If parsing was successful - whitespaces will be + * recorded here, otherwise - undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MWhitespaces(out string result) +{ + local array rawResult; + if (!Ok()) return self; + + MWhitespacesRaw(rawResult); + result = _().text.RawToString(rawResult, STRING_Plain); + return self; +} + +/** + * Matches a non-empty sequence of whitespace symbols. + * + * Cannot fail (not being able to read any input is not considered a failure). + * + * @param result If parsing was successful - whitespaces will be + * recorded here, otherwise - undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MWhitespacesT(out Text result) +{ + local array rawResult; + if (!Ok()) return self; + + MWhitespacesRaw(rawResult); + result = _().text.FromRaw(rawResult); + return self; +} + +/** + * Parses next code point as itself. + * + * Can only fail if caller `Parser` has already exhausted all available data. + * + * @param result If parsing was successful - next Unicode code point, + * otherwise - value is undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MCharacter(out Text.Character result) +{ + if (!Ok()) return self; + if (HasFinished()) return Fail(); + + result = GetCharacter(); + ShiftPointer(); + return self; +} + +/** + * Parses next code point as as byte. + * Can fail if caller `Parser` has already exhausted all available data or + * next Unicode code point cannot fit into the `byte` value range. + * + * @param result If parsing was successful - next Unicode code point as + * a byte, otherwise - value is undefined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MByte(out byte result) +{ + local Text.Character character; + if (!Ok()) return self; + + if (!MCharacter(character).Ok()) + { + return Fail(); + } + if (character.codePoint < 0 || character.codePoint > BYTE_MAX) + { + return Fail(); + } + result = character.codePoint; + return self; +} + +/** + * Tries to parse a sign: either "+" or "-". + * + * @param result Value of `ParsedSign` will be recorded here, + * depending on what sign was encountered. + * `SIGN_Missing` value is only possible if we allow sign to be missing. + * @param allowMissingSign By default `false` means that parsing will fail + * if next character is neither "+" or "-"; + * `true` means that parsing will not fail even if there is not sign, - + * method will then consume in input and will return `SIGN_Missing` + * as a result. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MSign +( + out ParsedSign result, + optional bool allowMissingSign +) +{ + local ParserState checkpoint; + if (!Ok()) return self; + + // Read sign + checkpoint = GetCurrentState(); + if (Match("-").Ok()) + { + result = SIGN_Minus; + } + else if (RestoreState(checkpoint).Match("+").Ok()) + { + result = SIGN_Plus; + } + else if (allowMissingSign) + { + result = SIGN_Missing; + RestoreState(checkpoint); + } + return self; +} + +/** + * Tries to parse a number prefix that determines a base system for denoting + * integer numbers: + * 1. `0x` means hexadecimal; + * 2. `0b` means binary; + * 3. `0o` means octal; + * 4. otherwise we use decimal system. + * + * This parsing method cannot fail. + * + * Parser consumes appropriate prefix; nothing if decimal system is determined. + * + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MBase(out int base) +{ + local ParserState checkpoint; + if (!Ok()) return self; + + checkpoint = GetCurrentState(); + if (Match("0x").Ok()) + { + base = 16; + } + else if (RestoreState(checkpoint).Match("0b").Ok()) + { + base = 2; + } + else if (RestoreState(checkpoint).Match("0o").Ok()) + { + base = 8; + } + else + { + RestoreState(checkpoint); + base = 10; + } + return self; +} + +/** + * Parses signed integer either in a directly given base (`base`) or in an + * auto-determined one (based on prefix, @see `MBase()`). + * + * Integers are expected in form: (+/-)(0x/0b/0o). + * Examples: 78, 0o34, -2, 0b0101001, -0x78aC. + * + * @param result If parsing is successful - parsed value will be + * recorded here; if parsing fails - value is undetermined. + * Any passed value is discarded. + * @param base base in which function must attempt to parse a number; + * Default value (`0`) means function must auto-determine base, + * based on the prefix, otherwise must be between 2 and 36. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MInteger(out int result, optional int base) +{ + local ParsedSign integerSign; + if (!Ok()) return self; + + MSign(integerSign, true); + if (base == 0) + { + MBase(base); + } + MUnsignedInteger(result, base); + if (integerSign == SIGN_Minus) + { + result *= -1; + } + return self; +} + +// Internal function for parsing fractional part (including the dot ".") +// of the text representation for floating point number (decimal system only). +// Cannot fail, returns `0.0` if it couldn't parse anything. +protected final function Parser MFractionalPart(out float result) +{ + local ParserState checkpoint; + local int fractionalInt; + local int digitsRead; + if (!Ok()) return self; + + result = 0.0; + checkpoint = GetCurrentState(); + if (!Match(".").Ok()) + { + RestoreState(checkpoint); + return self; + } + checkpoint = GetCurrentState(); + if (!MUnsignedInteger(fractionalInt,,, digitsRead).Ok()) + { + fractionalInt = 0.0; + RestoreState(checkpoint); + return self; + } + result = float(fractionalInt) * (0.1 ** digitsRead); + return self; +} + +// Internal function for parsing exponent part (including the symbol "e") +// of the text representation for floating point number (decimal system only). +// Can only fail if symbol "e" / "E" is present, but there is no valid +// integer right after it (whitespace symbols in-between are forbidden). +// Returns `0.0` if there was not exponent to parse. +protected final function Parser MExponentPart(out int result) +{ + local ParserState checkpoint; + local ParsedSign exponendSign; + if (!Ok()) return self; + + // Is there even an exponential part? + checkpoint = GetCurrentState(); + if (!Match("e", true).Ok()) + { + RestoreState(checkpoint); + return self; + } + // If yes - parse it: + result = 0.0; + MSign(exponendSign, true).MUnsignedInteger(result, 10); + if (exponendSign == SIGN_Minus) + { + result *= -1; + } + return self; +} + +// Internal function for parsing optional suffix of the text representation +// for floating point number ("f" or "F"). +// Cannot fail. Can only consume one Unicode code point, +// when it is either "f" or "F". +protected final function Parser MFloatSuffix() +{ + local ParserState checkpoint; + if (!Ok()) return self; + + checkpoint = GetCurrentState(); + if (!Match("f", true).Ok()) + { + RestoreState(checkpoint); + } + return self; +} + +/** + * Parses signed floating point number in JSON form + optional "f" / "F" + * suffix at the end. + * + * @param result If parsing is successful - parsed value will be + * recorded here; if parsing fails - value is undetermined. + * Any passed value is discarded. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Parser MNumber(out float result) +{ + local ParsedSign sign; + local int integerPart, exponentPart; + local float fractionalPart; + if (!Ok()) return self; + + self.MSign(sign, true) + .MUnsignedInteger(integerPart, 10) + .MFractionalPart(fractionalPart) + .MExponentPart(exponentPart) + .MFloatSuffix(); + if (!Ok()) + { + return self; + } + result = float(integerPart) + fractionalPart; + result *= 10.0 ** exponentPart; + if (sign == SIGN_Minus) + { + result *= -1; + } + return self; +} + +defaultproperties +{ + // Start with no initializations done + version = 0 + BYTE_MAX = 255 + CODEPOINT_BACKSLASH = 92 // \ + CODEPOINT_USMALL = 117 // u + CODEPOINT_ULARGE = 85 // U + escapeCharactersMap(0)=(from=110,to=10) // \n + escapeCharactersMap(1)=(from=114,to=13) // \r + escapeCharactersMap(2)=(from=116,to=9) // \t + escapeCharactersMap(3)=(from=98,to=8) // \b + escapeCharactersMap(4)=(from=102,to=12) // \f + escapeCharactersMap(5)=(from=118,to=11) // \v +} \ No newline at end of file diff --git a/sources/Core/Text/Tests/TEST_Parser.uc b/sources/Core/Text/Tests/TEST_Parser.uc new file mode 100644 index 0000000000000000000000000000000000000000..a6f0cab20faab3fa653dd31db2c181a72bf641df GIT binary patch literal 118744 zcmeHQZF3yQao$v=DrHyYQ~tn-`9LZXApoRa?AVSdh_sOsX+(gcaw3-i5Fm+&1Zf;7 zie~w*C+TMzjqaJ9*`3+l+uMUsSHJ;xyYteoPxti9{_p=)A65TCuYat5S&ceNO+)>Gz!eenOv}(ffY2QytJ>bNcR#{+m^gtLOB|oL*~QTWAljExPkn z^_H$ash(EPT>G2VdWD}^b+0<5PhQgRS+(a{Mq9`9{}1%3jO}(c-It6J<9|kTWNMud zRgP&+{QoOAGo|c3qS|qFMBg45s%cC1duihdQSjKcIVZ|IA*#J`tsYmurheYiXRqn~ zF?|MF;##y1{(2yxEgj~O!>CzO&m4Wu9fd%79mT`yKJ~Fh{c}I}sxN3fU%4@16fC6= z-2ad0+Fg2q(|@G9FjmMH#<@pVY|#w9a{uAeZ|Kt5TBGaO#tFra%^12#uGmFr z5Oi)v^MwsPaP+q}QD^%nN4XhsglYKP(Fpv5jI!>+KAgD!LB9j~c}#b~j)qd(W!yRz zXeTt}DYYwn1x2lW0@b#OUo*PyCCv?*0lmdoe?xOPr`Id`2lr^cTSVz?dVfnAbV#2*r@OiJZY_UH{DHQDM({%5Kz@sCHI8UJdhdqgf9#vh z)#5Mkt8m1^sp6bP6~D-h=%KIQm>t@N_xv5Ts51stt>o%AC9Q0_Hdt2QdF?^Vg?3C6 z(3K@iMh^c!quFTR8>FGYF_2tX{8{y0^;>V;o52!Pal6Ia@7a!u8lyO{kcnTGQao8rre@C)*L5b3)aZuVbu4XOkun} z75^k*mCN}J7!KYc&wTY8I+LB4IKkz6J3fAe_h|L3;^vq>a#FYAb;p? z-W}VnhrDYak(9wh`5fshZ|uoKd8$huEHS9TSm0K5;}Qk%eCu)QP3Keg14-Q{8v{&i zEj}>YjT(h_>8iuaUhD{WiN=g}WBg0sci-IwG+d{6`Y!MYNC4RSsOV1iS6xTTcXt~z z-(rVq(1SxUt}GNNZg_w@JR|^c~-AkDVL-hWB_v?f*pI zKzHDCfml_hfseQa&?BZB;wQc*|M?TO$>?g&jTPguG+XmDQ0Ivmt&#Qq!O67PH}FjS zNXZPrH^vsgkI0k&Ysxq<&!^5F=!}625w+AQTQ<9|-T0elX}#qQ8db>QzO*=^xxkX1 zR>E=24tzNyyMYK&C;^nLG=P+V$G6>Q_zMvXOD9LBoF^JW3gUEn<>K`d;sRn!!7?$) z5UE%+cc6z~6Ahq$Upr2UfYnvzcVzaY1M+`|FSDMmM@ZcMya8*2M*u`a&1Woi^G` z$!^v)rVoX>+FF(wpDxR`ww9wJ_0|ISq0_K!*R_`AHQq}%Z|H0FYWaXNPN}zef1&;& z&YfW_)$nbH-YTR`yvI=17KO^=Q3%H5=N6`l&3XJWk8M5m9{Fy$a#)|ut?rYDo2uWI zdDk&bJB5bw-by9kQz3f?T)Ujer|!Y~)2gc@axJk(fo0J zyT#ILWZjUpvM8$?-LO83crWx)VOChy0&_A9p)V6m*d0L{6s(`}LDY^e}B4 z;Aaw7$NwWY_wPhq&WgPtUlNOX({W|ob0c&2m|j>@=km`t-ftA5p738Lm zeyD6=HfXKci};!!%V9`6{-`V!po2Fk-=KZ>rvxe%P<6zsj9l*m`rV+uKKEA4!~)3s z4Z0I~7_4YweMzW{wPRja!0+)XZ7Ei#RwyMp`>-mHS3CUKU`-wP2k1xVoIl40bT_Z# zHfMA<29c<;NHC_6p^l_!>qpF*2Mg)V^;Jqc&Ycdgm1T2hFVa_oyYyM2?L*LbmGr#} zJ)3#hCcmMyPR}}&(S+?t%r`AIH?gY7qznG;XE*B+$?|Py7b*;6LA&={Vn>If`6}7c z?v(9nQI*Qmx2j#JGrny_)Zo{xtX9m2eFlj35Hlbad+JbbT65dbJJ2}V(Y1JPRrHc^ zI%1TtPMmA7hsW(UE0M1^@wg5v#)xJSS4rIQl%iKq5RsMLui#g~BmUwV1>Uz{TjJOl zbjpeFkonE4WN~AtW;9tB2>woGM~Kv-TK0jSdlKTPn8km7_YAz@!VT(Oj*}Ha17PpKM-|}E+X}2sTE#fMnhC$)W^g)!k>4V zFB}p1{$*NLp$xBE`w>I!Hj`?pQnw&XnG%`3wjr{z1=-ddTNwwfwJG7C@9WCBw=9ce zB~mhjzLn@o$+~Tb*{c|h7aQAdlkZ$r6xIdj*7;Coqf>FfwCr>j$_!_%l}6VVTdYWT zw9|!`UF~+=^LFE9ll{&?>WjmaB^JI5Y)Xci%C?yS8=+|8wokQEdm; zK1CSeE}B>@v`Z17-9yvfvX7Mcg9X` z-Uo#21NMzze*!W;O2shu6{A54kJlw7H|NlwexFW;e%M>hSu0eH2(3!9QDab~Oh5Zb zRSx1n`^e|vN?;1 zEy^0SOY?ZV3uQQmm&i?`rUe;XoYR26=g!`oxg0dCJvfMymg+X5jVCGJvnXzC(79NW zJ6!}$_?qYg{|c#(W1ycOT4<+94rXZfdN}VWN1D+}{q&N$bs7gvv{Obfg_YZORMFn< zRm2wUvleQ8d}#wxwDfIqd&_<4nIpSnppo{Ot~L$N$UgrZkjb+7m>&VACu`yrGti)BZkXZ_Ui2;=PX(_g|I6`#1F=Mnll-z6?U+^Q&1<1AD*ak2qUI$O-?E_Np*YGIIRf;=PIjLC$}&49a_pQ+1*(qx~mjN3VT zIvY*|#p!ldE_38Mo*5e+mqOdaQ}=GscrCBiF0xfk)bP*OV=XEFU9zjSoy$0@f+esV z=dpz><8~OMoH?0;1o)Y`JpQ*Mm6$C1eDkv)_emp>cSVM^YaZlUUWT&{L&}TamVXWr z``S`zvHHdGvP<<~sZ;x!caW~G)Wp2U*mlM4iTcK6mcVV!W9VP@Jk}fw1D}nRADmca zQFyx~$(ht=?p#Hw*e#W>7-fCD&?O7l5J@wS|xyGPZbU(yfGe;4$?m9@Mg zkE%GXAlBWEpdD8KfQI06x||D|i_w4y>*#&TYme5V42{b`jjMskd=Jj+gmtOLd_{t9 zc;}ckQMM}%%B8MvJ2>K-)7s=!LO|F20j|G6t3P%fdTNNH3zyO!Y0-LPJv^dRM1R-? zV?A?@PjBbRYWZ&8%IW&zm{|~;gSqy%a!xpE-&fn?q8{MfRRtgm6`s0~^1D25$ty^P zV;H2$ZDaS$+?sXG2_|i5F)UnEP z62m;>*$#_)_Aj63f;dhy5?2BDOkG0cj3)SW;eJ1i1=Iv2i&N|6sUcHAPg~?s`5Y=< zTg!jG#UDs6=kyE|pmSt_P#ee3iGa+8E1>uUG1BaU1~}&*cVJ&_{5cr0m8c!+Q&!i| z%bfn_XE<^8344c88+_{U4Epp(r)xP9Q|Q?>Xdy?x@Ps+rhdz^z`|_W#AUlTpWb@LL z7Wr&LHdCTooJzc#1vL!z%&_K)$}*(6?c~(w4)7!A^T(s{B4zS_D8B?t6I&bQGYvF9 z3mrM7CoovQ(^S72r*?W@eTv><4g1m^cx=&fJtKL9w#V}MaeAaG-2K(ZQW^~9e-JBi zjX6J)s_uol`h(Ek*DGFNtA8}P97N9&Apv=5o(B>vql{5LTotghLaeG!gM|R)@%pGW;Ail{63&e@SuBljJ%9<{M6H^=KECOS;<|z z<057I@U+=2T1jKu=i7vP7oT#rWs63`JDY&cl)80^?qi3(xaf1JbBr11LYI%dG5Zul zy@fyb)HoI|10uid9fgZ&-p8q(_JaOA|Eq=GG8*M~Ou7~;Ay{97&McmbuqrWa<~3!} zW@I_|=}=sU#GK+-9p0Ir7bTSAZ#81ZQ&sVNV_rdkTO$s@sM;}@h@%4+Rg*?lAI<%0HD4#WZkxvit19hoTj+V%Rj0&Loh zdDf#pmV8SB`>PTZqn>^{Jk9JlQ}QA+j@zbO?%1AkdDKi}E1mFOQcn#_yHEe|Gnk>T zl3z>HdU(q6=oU*;?xTsCi;K=;5h1MUJMYZ@R5T?rm3p_@=|@ee7Te-8#!3>uLaWt6 z_&wOZhl$HD%Q~8~&j+yMirE0Mpu8(h)^Z`w$j$O|Yk}ubr3owunc#Y=e92Cu4BN{I zoeIJ>PtM&zeiDfIi0ol3dXww;JJDJ}Bq9NE>yjE%{mD1H-p%L3#(PTn`Z#ywdA&k; zUw$kCwES8ewR7U*VQ5>=^a!u|Q4ltIsN93*#BHtUU{}3mkR(_Am6hJ7i1j$`WUkpdXf|i;V&6{2Ny$#m91FClr;~ zSym3FB#UtinIkVD|0Snlx$XPU+!`-mVwjdVuMo%$kvG?AXxtpLru8<|k2ht6nn&+z zKsR3`bV_F@F+S{PY?_0}bwA`Lt0>l+%a$TAUvS39)%Y6 z51huwqI%zp?p|^OImwdz9;7dYxa z-9`ePXzv5EFHbIPJ~iKJ^cYeN|0E{`&OHyXNIAD&BK~ZETcneIY-{DK%#!=Psm%EE zg}rA=)K}(0E%JS0-0G>;ruq?#aHr;b%11oxh&CoSqHs?pY(|&8l!}((C;<4jj-A@E zfN-p@M-Qtcb}l2b^mu*Vm$p!Lu1VW?#yO>=Z4_}Xrm?GVuZ~*nJ=OwA%?@YYHY%(K z!JlEB0DBuZYoFW+YH)0yWw}T3+tybA{X9${+c?63<=0bV{knNM-!VkVz@*33Uuce~ zFJjyr!!dSlW83!>6>fR_frSf^PH5%veM;+mUZ*gOk8drK%4@mq8QOe}Px-D{LOZ(+ zl5CD;LywFqOU@3USC)pl=V%+T?Q65BFFmbrE!tbuPaec%SI5Q7zg)Wo9I&;qn;dusl5 z{qy}+8}Ak=**6@9=kcsMe}zthMEE&kw!Ya)Pdh)iNfvbFN!Nrp>bd4Nax1cOG>kTD z-Byl1!%o2Z%Bd?{yTnf&=jviU5pPj)0C_v27t5PS1!}@DCal%PugLMN<=#Z3A4skq zX?O7x>i?{j=$EtYi_z;5ip8lrmoWo&8a2Nxy?lFYThx<*esj4eZ;f)qs651pLCEWa z;=iNbtUsSNF6Oh`;6Lm6zn>dAmUHcV?)AaQu@-^YtbY~R43T&aQjWE+C{0=(2UQ?H z5IZx`r{>rvBIe?Ct&z;7TF!hC0mpbPedNC9p8%ONUr69$=6h$7^Myv}d@VmpVwWaM z*#+~cKUs~~C1z`;flb;@uv>MG3qDzHG;_`WzEO^9d`?WG5fJ0as}tX>t|ToZ;=yW- zoOTXN_Rd2THbU`Z9vaIJRR>1(e*U6l9Crp92czwN zdE}Iacfr~I2}-N5N)T$xR8TfcY3x*_t_XV?d=^i+Tzq}a_5Rwem^lYw!wO#=RK0rg z(+^E+6;nM2e~(t9j_8Dqw=O4NS_vaw+0UQJt*?k*ex4yJFW7Zcr8Cjr7IXQ>eaw;r;frs>&37 zhG&OKJ<1CjGxHNQX8V_1jqk?QaiWeR*ZM9iTC7`*c54_~s8U|nbAYmQRoB!&dpwg# zqB}mtNofm?iZQN74V+mr@ED!tTb$BvCk#gm-AS?>Lnf zL6KDK!cPKvL2Z6cRWZEB4>sJ6)nQ4q7~^Wr|Muk@pC1F6V>@47p&dttc!tPJ+5rfTq2=(Lmo)dlKlmB!3X$38Qge|! z(fN$a73`0}lVnf>dqRJ~V!v>9L8dx92H)yw6 z%RY3OlPPkF^|@JZK&_PSoX{DysKI94Y`1L|At9eJr+Xp8c7Hos!6&ci8*nlQC&hM5 z%-p!p#>}m9r6j}m>fPmJkMa~JE^gxA=oj<5O}3s--eB25q=skz-X_`|IIV|QdO@Gy znu z%4A^qC|bttQ*QZ|Z+QOtID8o#o(dvQ@%$=sU2K$KxRgG_M}?S4=)^jGjqY{V?YQV( z_~6^9tAO!`w{jXE^tEu)i4=z6|Ia50JzitL|On?`7fdx(r?g>|JV&EVAxuiKc_8 z!Bgm)r9wFZ_&H%}WPtCxoZ~swG6CsHPN5&327<*J-}k0w(Pk;*=oyh?a~-wdEUCJf z(|Q~-rq8KAUR9JlXld<^9o?>J6(jYrV0wkYwqa{|hHFnEgXR{?=_trRYE?9!+=q^m z2>^F~a?;h8l0&vAv>c|*uSjOjX`W|pCAh29_^ntO8nQ*1dA-eY$$_3@^_!n<03FOZ z#Wbx$_r3OZgVyRJPPb&WvANYb?exHNGwu-w|40@HHG$ewKTDW5zEGcmCf{YQNQzCI zZY7=N1n9a?w%V>J&W*hdE9<&fYdj>61}uPj095q8sNQqy>Zs)LM`ZCrXzj<3{I&88 zGthPMRD{8iy+0$#?n9RA`vXw-2b#bxqn0sVTjN(O=06wB%H!(LdJH;S+o;c*1HEuU zEn}K?dDm0+xhl=){8VMRu7}O>hQ`hQ{K_O-WKr;3$lVVHGeAOTD~O`WDkBGUslD~ zamBL(mdbuTjd)~@lk>f@(51|Rjpv(Xe3m}TM?-Dvev796;(hjX=svu@oYl~GyXM3_ zzfh@6d&J(W_kMB_rBB8G%e+=g>&8O_lB?*C2}Bl}eZTZ)F@)ufpTV+i4z>2~d!OmW z%i+q&6nq5dv602ZJ`DJ5OGWH!Q_tNTchsY4yIME3czU|LC^^q-2%0b6PfwTDO>!Ue zz|9&E_rK*@AhvP3zIVDDjcLyqK35Q+uGm~>q9rqjePmB4w~$^dYtP+nQ$DGFa?B04 z%QTj+#_m(>Z{(-VUTpu+f3vts}f_?Wl)^5}q`MaFs>-OT4 z5d)>qm_T2sQ{?js<|I#2Re^6X*J0OnY<#7ZjgLSnEfHGYoc3vD7nMqwxnC)=KiZ5^ zBhWdebTp31L{Ir_Dt%Vk6PhEQip@_u4(H@Y zMD}8*F+eua8ot8{Bj}DA9eJLcoHfm71HiYUf&lxpc?>>}x_eeUk)97T1dv3apNo9s*dFks_9YmO?gm7T(#pmmj=u>`hDEcoRv5H(+Y_lBPI=ucU=IyUB zR{{MF!Rz6QVcy!x-_A1rA*2K#gVwol)b<;9&eH%k;|@l zTxi|Lvc|P7+&N-pKs2TFy?*+lU77N$OY$k>Yw>uPURFnie%u70Z>g zPB1sE=H_yAdRgV^@+P)EwMd#T9zX}+^lH$wn5E<+e{c4XAtOSmM zg#A65kYULEyDPa{uRhm((&ejyeZ$#VAF!NUogIN>MY(n{malI1_;ITn&bc{neL&BW zL5wG>bZQIytkRs$KhIe)l+{1vei4zqqA7hoZfpxVLbW3ul`WIkbH?Ri|_}m8H+_= zi=ST;<{D1-ud|g+s9a>M9f!nm`jAr6Y;#1~+jgv5J*?|e`#e>@#z$MWvY-@s34P-4 zmbSk2SiM>TU*;rV&YtCJVmFzimF>_AwCRPFfbM5FN@jct)e4aGHpC&6`?=yknSZ(D z^hbl`{ON-WbMU!T^zmt0_L;}+*n680e7v^3w3;+-zsfnJ_;Glx6kJfpp1OuCp2oK< zDLR#|uCD@>nh>ngDHSc1>&G>g9S_TPx16e%zGj3`wCGQp=Zi}I>`g44Tay+&6e|7Vp-K3#E z=532dlqY?#u4a;_gk-zXT)m~WlqG9uF^rnFv`q*ih39f|Ut`ZcHCDS`*avy4q+X-W zCYg~)sXeneh7?&$(LKx4RTJB3V5M_W`PJsraJ%6@i|FvTrL%8qY(7=+;99li_6nZS zs@h}P6T-VRxlK!Jza`s8e|#8644}q7&8O~-UJ-KTU5jTIJ1y=0b3AX>KNz5|jj_xzeZmuszOX^UU29Ao$C6lt(ttyi1GXG?>dUd_wZndPQ8 z%Ntj6mg{6`Eia+#S5aoyNhAGvo?X#-{wkQ~`4ye#XTdxVujo7<2lIS#Mdx`K%=5=9 zIZt3DYlmX^VMac7-Jz+gXo)|me9e94_`IkX$L`u{##5=Me}D(?JXy4n%Z{o4<2WV# z+u}Pvg0U@HOrLIxzp>~w-D26v7YDRY?n|20IsN!;+VNkr4H;{p&3e3L zd9{p_Y!Jb#9I>?I?}4>)4Qp9q2mPM^n=I*{DLxcCn-keVw}y}I!f|b6jLVO9&!v$`*!SJ+z9mwyeV0WGVc#ET_buq%_FabH z!@fVs?pyq_?Yj(BhkbvV-M64z+jkk#4g3BqyKh0Bw(m0Z8TS3F%)S*3YTsh1Y(}XY zA`C~hk!1@Rd#Wt3B^&~!AwkV@k_8drLQIR!%NIV)rGd7ly`nYqLRRv&QoItWmdj9+Bk+Xi!X)R-ZX=ND+VbV^)3cMJu63-=OPe?hKm8 z)erO+pCvBRj^|JvSFlpp1u5sW&S>Qk&$EEOzoB)Z6Y2?f;M_dbZphd<=@nMV_M@I} z7`-`Y;Wy?W;}~jXuC7Bq6~yf+!*kO5GTlzs>7~44n|8-Ex?Jn>=%3QKt4Q}| z?TS%-QRr2WGjMV_dQ}ZN3t}jXR>6PZK>M=T%g4F4&Hj03#sPMg`!tSxRCVY)FK|0N z0gx>8|Agv87Kc^Wsoq8;L`QBZ7LyR6Zkyk??TDx4%pdrl((b-=d`UFN8pHDusrYha zDsmQOkyOOV^*<36;e`+}zZjXKKaEV$4O$n?M|-L(sp>r-)nlR=tPxIzw5a@${)fl) zwVV+jeruGwS4?0p$Y@fCRJ}>t983Ac@rv*{$IrhZTmioX?je_l7HmcV$QMuZLmY8L-ze6& zRq&`FDmKU(RCXQSQ@~gSWbxX;n9W9 z!m}4&)0IFRoV!&F5?^o3iM~QQi`2JAVPnZWveAe&PD!%bbKL$@U68NFX_1JLVb{7# zHsjd4)yEy>8*;67WC~IYTrOHQuCL#&$@^-1|NEEu`9)@rO7)FpAJ5}_OfS?w%t;fF zEA}(aus%<-bSsXkA=!u2E=NN6JtuvIt$ObK3!k{zUGuv6le8yMZi)0aYhyZ-9ioh( z(7_P?09Ft67&$gG&es^nx_A_&xA>NlxG&Fx4k_iJKH7WY{01^x@I8Kkr_FLB;ZFHa#yOKJfzhjf&g&ch;e!H6D}RH1zO77$m!2IIUCaI;G!(Cm#;6z!@( z7(Y(IIHOU6rasEFbnOxkDfR7IUp`gJCAO=icEzMAJzgWUY;#_1auY_xLyxDARaq>~ zmi2z#e*pP@=hd&+Riz!$F0H+{n&NA=y@##9c$M^RnwVd;#3L764(-GzIeM6?Fd3(E z0q5>)5~miUi;CN=?bQqk_bs-*tGyh9OI%I$(plXxDoWPL&f^@QW$?x3%$LQJnOhC1 zS6=04l+_$t<^P(z=PQRhATeBdgq0LtMNI8yYs&^&%a%qa&@tgp+B1&=jFH!n%#QUPy!%qISdNC;-2|MMkg5xRB?fi?StM%#5O0V2y*SMkf1Q_K zS`^oH4Kr7Ea{V#vLXH<+)XdPoNVCi>ckGNMaE=4)tNxVD$B%-`Q09;pT6sJcDv6yK zaE(b6leDc52};Sac+1T$+$Q~4UVcG)7ftW#xAO7F*u0xce9N?6W`wIF$cb@>%lr+= zaf@$&Hx%704rzdH%a4w4Q?^Uv7ZbI z`5))oNSnY_w`gq|D^ge)Mhp#1?<4hHy5sP&U3=D=9p5fRkG3MMOB;yu{nhm$T7BSX z*JV7rF1N*XqI`DPW7AeYes`m4T>|WC;*QHQBhQaY1K~XW^Jj1GQSOho3d#9)D?P*4 z_36A~GHq#-Uc|dxe%ZqMh%rc@BvKWE~b#F5S)nQlHXji^u7gmAl zGUhR;o?F*91TQ`yjQS~Y1G%5Orp9^|c;b(|jWZe_`EzTo@l!hjU;2bBEk9@K#Q9}8 ziv*bt)PbS)i%*8&wGpms%E@Zz`A&az*8QbJ3*9Z-`UOw%RDgUvOWBfcw5#XF{VFRExOJak-V)i*3~K2&6pO%WvaYlGOY1!(uDN- zpIYfNvU`xGSatJV`fiqK57%f1&JWbeD;E!hm9=bBeas!Fh^|5UV;pk5qrPj*?<++Q z)QHZACOQ70nI8EPG6p>`FR`0#9&P6`qVgrL85JNh7_M#^Yr#0h714EgT4+9H%XKf0 z?`4u&J_=hK?DN2nzv|jQhG*j3!eYJ8sq_9D4zuvzn?XEp^?9g$cRsCI-rEtI#vC^Z zC(Y>Toi_+&;Wts z^CaVr+Y9s=&GL!!*(F>@#ajG&i*w>v_*%w&PeH?k9>V7_{6Q*X49dOpC~R}EJC`V9 z+IidN_$0q(K(&SIQQ;f(LZG=c%qJf}lbWJ(t&HXs7GOL%BZv_s_Uh-Iy%ft9 zgBLk|p*%)r%6&|h>^O_#^;lUXEN;`vM`U$R_OtI za5e)@OgpcBLH8op&n@#_Kg|6X)rYgcquywZWA;nB`UPE${rR)mKhX8O`w}NZ0J&Ig zz|)>^@}c%MCm-gIDfXWoXM2l?nA%>_VtZ?MTH6x-{34jGrJZo)5mD!~xfaf)?9pSi*Ym_D15XHIF1Twd1PA-0fpAEYhe)5*gUtqs-4K4JLaBwgqC;EV&AsvLlT8~0Q_6{ zHv)x)xoeMXL0yv?O6_pgW@7OSaBVA~Nb%pc{ITaoNfw2-$UCCe5mRB=tWv-U!-#rE!M!nU3k8t$?+HIg$2kj)jsllRTkMA~mF^-Dt zoDIxFx1)vLNe_7jxT;r5)Q?Y;+B)=Apxps@qi4`>N$XYc@B*JhT)BdHavo6l=>XzB z?4yGoWBfmFt$|R*=O?&w&5M2KttMP9VKk+lH_cOgCEL)_m~{wa0DNpC*=Y?DB1OWN zY`(sP94GxYo#^t_r;MKNA18K{fq;-`+4g1bjwC_#_eY6ln<@?To2I$BLq3yL`=SkFKZX6RSxivTBqgGw^W1-d#Y&W!-iQdbsu!Yu) zp2GW{y~hZ~OhLaEc#Ptdnew63kuJX_;R|+!FL8qP>vObaloUEyb2DzM^@O}h6;4P) zXyo-Q`v`uCBfKYm*tQy@Yz1~TiV}}+kL4&tIgk@(21dm4$cc0_yZ1n`(`pZNwJULN z-STw=IA_tLK#LwSW5@DBdON6wm<66AUOQOyz8+eCf?Vrk?|`?1XiI8db<{?m>w$(; zQ+jNptRtTIo)RAVq5?*wKsN#KdUia=wIc>@Dysz;%Ep0bXqvODb zunkt_V`LZ^G3p$L&VfF{9gZ@&dckpk`*(YKY~YJv^qHpd2`I|J zamie);0R}p>u@Y*;@Ga>*vsbQ!W+P`l8Ixd!iTQNik9kRbukOaGOVJkb6uTk#&IbV zN4J9Gs-1=4`~z!V`XIC1wQMb2&cb}rT&`e#oQ>ntEF70GV^Nl1zLq}A!f_dE^<^Bo zF50Z6rA+_7 zJETf{5X8yVSryAj75ay2KMp57PopwOQ&Ps1Zn+FeH_yT=#x&e=oD-;NywzHrRHWN4 zqb~QW`%seH4_3f(T>mmXGE&jl`eP8M^s26qoyB-eD}Lrjo?XS)K`(2pOq=xi>vRMX z;#O>N8uV_JqcYAXpzB*ub;e>z)3$7+WtQw1 zl|FV**0-11qTD;Y8lE-Hy`(u&8~3Aia|~lV=bk6$DLv@sG-BAu&Lp_?OUH@7dddA< z)wNmIwrowJzmvx2XTKRK z!~Efoo8E zO*#K$6}7C;I{w0%kNJ}oY4 zdW@~es&*IQh3}H2Q&uYV{jbwMb5?Q}F=xFwoz2s+VDsFx_Her(m+Ts^YOy?{+FY6R zX1h>`y@!lceWF6=Iku-`R*opSFUsw4U_V}!y${^I>h|V^?W$u;f2`b|Q4_}@QrBug zR?b8CoPXRh-$An6S?WLeBToVD;11U%NUPiLCFdVw1l+tYxNgsfSdQ)mr9|tp(Gsw+ z2FUd?uJv=qg!ztnuKp<+jc7X7aW;~|8dm9Hjq7^5oOu>h$b9tDtcp==x!G|Ee3Yf8 zP?rI<%tbC-&i|$0l3Im(vhmO+_~-twx+jCGqn#YDBxKqBE>95oPadT_HD+s}cR3B0 zy@t;n(4XkJ`+j!%5LJrXetO&ZCyZrw){=QqDzZU;|kF)ooW#uLUb?h%pL zAkS{_8h2*NGsJ_azb=kLKK;1{YHngb*lj!C)|ozkR@SfbdwFK5xt`@$Fxn?y2khbR zX!Ii}M`L}A#oS&uuFf#V)$42-J75;=Z-TM9|0teA=kYl9VZ?R2dgk}Ln}Fr-K@L~S zSFzf=hG?}8+0!0n$5*(|i`PTjJzZb?N1)lboKriWWL*0SJCcr{7xFE2WdRgk@GMh( zWpUnqeE}4%crygK6l%L{S+;&ZC#A7iqu9$wneX6?2Y1D-z&5sKQ9;`_$zI!KbJpE| z!<=0JZt8$ZH9hV zpNo5_eV3-=U}oG{&H^04tIKuTm*4p*E z+n5o{PoXnwmY+lCN}{~4g!^w~f7%f^lJ}FazLxi?PGH8D zWJY{Oj_+&6ou?Z$zQenLCfG&EcU2wWO%!|*pXXyd5B^8{6bS8>s}Of*(vrEVm={tm zbhp#N{ri_&D2M#;m9>zVhie2Uq#ZOLy+cdt@;+8qbp8<@& zk7tqE&BN&I$jz+zwlt4##k}@y_N{e9n>*8-L2GkwMLLOZeF->xVw1jn-@aLf@A-0X zHK28$mbLM{6N-HUxcM%#Dt3t^i{>_s@3gqhvvl>XMev)4(P46$cKJGlA(>lp9e4b3 z&Z(MhHB;R~E|hr585?;k4CkEXDM8| zZ~Xz+yHj{t&BbB42%=CwpF+K1p?0TGZvg7GY^Y9l3iYGb?^+*%t~R~}Xgv>F&)}@U z`G>6!BD~kHvP|K^Bf0y46$$GU<{&xil@-%gEkE_ptjFjz3Rn{po*Uy$Qzv literal 0 HcmV?d00001 diff --git a/sources/Core/Text/Tests/TEST_TextAPI.uc b/sources/Core/Text/Tests/TEST_TextAPI.uc new file mode 100644 index 0000000000000000000000000000000000000000..51aece97052af59de1987a5b8f357582f246f133 GIT binary patch literal 67104 zcmeHQZEqaMk)8ni>3{?BCHW8_hxNvZ={TZE>V>jnJEAB{mLpk;L_=rzav~*?5+jOY zxTIuRkbl7KWp+2t}87Ub;COLuj>S9NvI|NdY0arQ@i{yqD*tdCw9pKCh{O_anE!?$*_IB|58O|zv=d$_zD2#;vDaI%m z+spPbivH;Lp&OZDcP)F2tJ}D`+fyTHwkYjX(9>;h}AT&p|Tr|9Q3&Yj@+ z80Uyf-plq$Lt_3Bu9Bu~jd1X;ZE%55_BoEcdl&5(6msW@yK@JvkSm_LHrUb;D9*7e zzsQvv**Dp@IC}$k9Jsr-fQdf%L?qMu#I8G(5V@b2N)LG~xy`O-l%9G&TB=W#BCJ%ozS5IcsC1;9z%39g9` za<=yIA$}99uW{UWcM8tJeoaQK-4wM`v?D(D(1X$8se^!cAZ7$Qq1r3lx$iLl$YJ=z zVR{#s*#|bPZ{dr6_5kv{k$sij0rfV}-Wurm9gYw1?H)e-y@4}-#yPgl{)lJdW5?mv zhiebKafqJwaE(w4r&6{l?Zgl|NY;%WPXS< zUwGL0*iqs!?xHTBHrQ}<-pC%}n{Rcnaf;j#UeSV&0X3!nz+DxsXgI!~Eo2`$th{mm1$VqI zn0@H<`7vaRTuM16E;&*@Cosg%C_VTTp?U^fmfe5Djo<|Msr$s2**_g#sA;HI>OOZL zchf2ouamZM4_J5wy<#oV(>ZW5d*rxA_^NEgH*vq823G6y3lm1g>((huPnN zdvcuF+1rjgo_SP$JC?V}__GfGQJO=(6-p4Z)D#QZV9d!vbNaIMP>MKuGa)UDXeH0< z_kn>&Xgz(TV+{KX)9FLz#NkrXN=>@%jrDf~X;^On&!I3@?{WDUASlCYXCssh@ z_NLQxC%Bi`x`yY8dDZx0la4XkBS24`nb!lp^?2-hCp6R&qG1I-#XfymLQcyz2sDmr zkt?`1fF_{!<5Qt-PSL{b#h0Kubu4?|Ku39X`kvizOT|0B@7pi)ce}y8->e zR{PoK_|T8oa6Zc&cpLOX9s+AS(6L+SZy)XFd8J$UE_Vi>&jock&p2)aW2TiE=D0A{ z9c|i<`PtfUr(v2vRVKl)4oXYZLG}T#m6xYAaML=p?oC{YWs7nc%G5r1akerLj$LJ7 z36i#i&&6zc%KZM^D=fY)Y$Csmiw=;ZnoBwE{h}r=$RSgf*VbWjVH^T5ib zY4hvS|I~)%5uj1L6!YsnK&<{6?Z+$UaY(#oyE^feAWp{LnLh|hTx=+4)K&euV8 zdEKYIKGV#{eE>L_PkIiIl|CG~fd1_ezjyH={bd}>*=o&^tRkK9OdkJD2KdY`%^&{Z^a{qFwg0J^9=aJqADHlgDF- z<>_lE!(krxe{y(|%+5{Bc-;Wr*TD^zU6U-i>Q+6L$s}Xs)hWkbWh3HDC#m24s4hWD z^7`Ai<>m4a^7$KZ&=XiT<6Gfc)#y^Z-KCdQJ-W*YsqJcXzCN0beKsvxmN723#ny)EUGddu zqwQK~%ZhEev{9@%!**yX!+u16rR;K*1rA#-h9K0C%{_qy=gEdp&%*wzy-dyal-vGk z$0o(NPH9TxDf$UzT2lN2ogq5?`Ju9LruF8eVonu?WIk(ws z=yFRL>e3jKU1{lZC_+s=kG`yOb6O|IqYM4mY3t>>`ngzVU4xaM z%?y6kh|@Y|I-@?@nz}nocB8RXx1Y6Wp{|UqA>w95W=vIt$H=D)Q!68^NM;M z=20w;^A}y&`HfL@!fchSpICSEc~)~7+iiM$QhiudBgY&l>wM3R;|Q}JVI^rgXBXxX z6*8?~j%P-gC)`7iGUG^mSzeJ9FIrp8oaJ*jt3lta8{0%!Z=px2-(5goO1C=R;sAB1 zoK@-LFP{->t_KK<)d@F;<=Xtf()(4%u(Y62@6Qn|zD5eU^&2N0=b-C2k4_$7O|R5# zajx#jos-&po@WH{((xoK=~=rkYgJfb&WdKK`PP*T)V0ECjE6htk+Jh2aGSgRRx zXGhdhlQdu&~O4G?UuwMNl)f zw#PX;i;+bk`va#i_F>*2tAjIo(VU4S?z9Gol_en#XOFSdfU5{!^`y^auC2#W z7mo6^bp}td4)_w02-g5p>&N;)6g%l*{6F!hCkV4OVP2 zvfx}K_Y|N-$w&IqLc0;?`>h9ijDzYN!zR|e46$;GdqZ$!oMYI8B;?nsrX+$-(z42$ zwW5~H5{th<+=aDF%rtPd@?OT8XRb+;mGj~50IZ-C7}ME~$>Mx}`x zMf}P6fMsQl@N#~RV3Bo8^FgDFbA*>ma|Fxk9Kj-MOy`4NK8f&B?6VYE0@_PjO!{kT z1Gs9-G`qxY;SLA9lNmZ%k4Y>*b^Oyt@BTi*OIzQ$1vI^1N6@sV66YMaO9p417(3IR zpCtVkeN!3+jgIjdKF#0&7Igns!rz1rg)K+RC%tl zxLS2pd`~shOIXct7NK4{lTcqhlTfdpNvN-#64XVux`__;ccSCYbF?(F>X^Gc_ObiL zeVmp35V<=Iv&^zOMe`XS;4aR{Fw618U4IP?UcJgO)VMNJORa5@{jBuq+L_a*v;bWD zf420gK)rg#pyqx8{G1N1%VnIqY08S&dLEN6?IM3V1CsVwWIs>FQ?gGc`;wW}$BksX z%vWMYkp4th-q8i@Zc~R=)p1^Z|A;ZS^w}<2u*|j9$kVTmcgj267(w>8)EQHY4`RQN zwKd_MeWN#HIA6T>w71XrU-w0js3p~R+;UihPPwCp58*}m_|eF!?J=$|E8aJ8fR(v2>!#XEdr$Mu zwDGb#H+Z=|S9p0jH+cEadBV#mdpQSk)cY)l7hkeN+r~&qbQE)Ev7VC56>AjepY7oe z#$z@Us{Wen&MH-^QfvLn#a_=E=(%Q=h`OPfcG;dAzP&brBxR+_b>5V5)RVp5=7EQ0 z2i3GC@5-z7>&48=sq5jcJ6TYFjP;Z1FKZsU$S=)T`p~m5-$D9M`3Irw8pLt!kngL| zC;bQhWY6ucRy^eW4j-4?m00+AklB7o%*HTp&iYn*XM!c}2;}Q0<^Qgabs#ZcQF~8& z9!;b$jTc4~yU?jxPg-6{HX95zgUN8^Q_ts6;&2dEwUIMfwaN^g%!z7DM4wsTC&Bl2 z(DsOR;<|?ecSrpqnj6LQnoROR%_v^%=QO8!dp&TrsGhdQ zv(}aA+WXnaMqWpzVX>;&*+4~W8s<9Mc}UzLk}Lk@?OW>o?_gC3-&qmvl0j|E{SRN` zT`u&0ifRGAKx>?>ks6o-Jh7j%{7qWY{owAR71`mxYHtb3JuS{&E*s0t4=qm~wnene ztj!Z=t@>E=!hK5Oe#84{pQxpD?_55?kutIhvvFF(umFg3KAmqz;;+x}R}PQf`H_Q) z`#So1_)YLj<&$sM;dlD2M$>er%WqAeFT&LKfcfu;^O*IdeE4+&MOZ6-h8Rr4uW;1E zuih<^kp306q*5ewnx+Ij0LJJ8krJHYlJfDObr%6l7+t(dL{yvnmm94n{ zcQRHiFL})rW3Twx>B^eQT(RC!?q@_GZ%6T^K%cLpcSMSG2d!o}37?%UPVS82nlR6% zHD&^tKcA%-lXxedcNqqHL@}roh~GIQk%;WF)yMkeKS2A&cdbE7$(|iu(WhwFuWPU$ z78jEiv?@~J$M?bIt*DMrdE~uuPLR(D{uZ+}XOk^oqWYKPI&`t9<}32r)R!G;E!3~t ze^|h4AHI-X&Ouw%nsB>W z`}7Wyp)CpFEgskRfa~voj%^mh!T3w~`K!R<7C%vqhf7hVP*rR7@L*aY#6i8*!|D^? zs**FdC!(A|B&`n|mvMeBgs_ZC%Z;cMzSXsA{bGE_OxRx;)>j%~eZNE5m<;PxSXlr*hy&u{lM!B;;N$$M z;bXS})h;xG@*nW6Ngvco$oi zM^V$F+On4=X-2Vk5oLa_hU%>6q!DGY^flGN?_$0(Is4a+`HH#7Rv_1pc+Xy>S3G{- zo|{H|7m2a9#IJ&wG!oO<&~+Cv$$k|w8|u%GOPxo4R#Vni>8xhBo`MyGT%pB^L#{OC zoFHfY;?>98N1An;+y#jvQivo*)2b7{4lKRSME0O29zH-@eDjdi^YFJ^*Lr@!#+^O- zsH|jtr9jAhC0B+J_D=(7bf)tsT)l~_%x$v1fYqaXZvuA!*Ei2|c8V)1*b;Txu4|8d z@hMh$ZsHtkJp57lc^`?wHpg=!iW47-uSR`Jn|_}%E_vJfAs~@@Q_g6o#;Ua*oRMYK zC~Ip8H+f+Tqw{H^{p<0S8_`qTPfSP+3;Blj_rPIQW_k}CE_jb$lObL>IpoOk2=P@9 zH+-+W>VVnd#%3LbONPLZt{);77&dgBNEz?YLJS zIW&ALRNk)LMGIDoqPa!M

VKDr(*#s}cNrOxycfM9+q^d@mr0gpOK8tSI}TR+mq) zElE{OYmH*6U;1IRJyLU~?~eCZHxyNKh5kpmrx1FwVGS3nrf3n!N3tTKKAh{okj(Q@ z7KwWy`N^X;Cd%LQ(y#X3L!5Wks*;VKstz%#v5w+x!>z8c}Fbb*lQaa_2 zll3V5`2BLE(&PSAder<;e_Wjqj*~c_^+<{9Y4qviHBV3RccIdCKOIZyCiYjhsryFfw-cZa5v$h1>l2Y);XqmmsVC+)|J*e z`1&eGXQ|~h+}GigX811WCFKI6h;n3oK4}V%j5*CqhQrDDByYpi2DGc7&{*Z+Xa%(& zkI;AK4)x5%@ZC=Q#r;!K5ro{!7>aRCYR8#)HBFpZ;`-uq8P{WPq9@CwY~l?4?>scA zT`=-*__+t){1u`XYb`x4i)c8)(7sOECVQf86HlYNc4I%u+HS`Wla4gR9`h}sCq^vj zDi5Esc~33>`w;k(RrEduL+bc#H>n7cyyi8u&#Y$HroUUv0PmW#ccJC8p^QqIp$p%I zCQ?Irpob~3Id_TP@mjL6h53XOSEu)O*#KwBTcR{dRS8@i$@2#A?qiOgE9F5)OuVk{MnZE|eo@!k!VeB2OM_Z)VpzqtET8E(Rr-+mMl<0-_sVozqc z(zzq6OEbyV=M1+#^kuF2_brsryG#ZIGd2sLlq7FcH|O7I?(6GV4r~nb&ie+bk43c* z#nNzhthvGww!>Q&QsBd^JVi@oJ8!5IgnUbRi1mz*6<>nVcq^AF+qvs>tXOQXyR&4J z2lEAdtPv;0SMRvlI+?Qz<@2oCy>P6dbabQDg@2pdzL5Tmk0!N$Wn)wg(A;C_Li+1V zaM}juanyw!R4t@O&No!qMe%_{XkuF#(!rNH?MsfuJ^Ih&ZD-iK+5~O6xNl#k$zv&v ztaP$gisl^_##-6G*Pj8Exd6LLuhbt`s8>R-PGqSoyC1^PUiMR%Q~WCi+xx`pf$sM0 zpp9oSm~&3SCAy@pu2*jITM8)?LKWgtbZ7hg&H^i1S{C?s#IOiDQ&hTiRyP zF{ZxH@xG^GG|bCI?5!A-yqErp(&= zujxW9<3n_oqV{;aq2!&e2NUx{8!x76tg2*L#Zu?#+P=1s7q+#6%V03?AB9>N7`P_%j{TeuVg&UqnMbM;U1jI zE9#HLBlS7Or>Sy-KaP+Fao-``z0|weXRQaM>+muA0)Z4jtFOD?Zv>$qvYk+J!i>YjVcI#`l$3zo<)AEA0AKC+O)e*8|`8}I_ACX+C zmDTwes!dC=^rc$Mztyl9vU=_1-$>cEz5MPMZQDDav-{;#Q;TB9$?U$#ms%QaF^tht z6w*88I9dOp9OhxDN7*IlrZzCda$naIOJQYsD$vB9SUudVOa&U&GF5r0Dco>1Q1_II zcP$G&q>#r#?}+orjD>0AK5$l!eBaH?TG}dB%_~>Qz4(x^uftKQpKu&_5Md>}Qh?*6 z&XP)r6t!YMD8yuZ-Pb0{A(MNqKVRC^lWnOQ4r>Y0x-7gWMJ45mdEw4_7+_sjzGC3# zg@KiyJqEa6xd`G z_s+_ymiH4=$#5B8i#6G_=02LlqoEH_7CASGQLDX?(BCYMwyIl_{94`iVopr?O7kAA z%9y)`Qd-SN*3}eHZ=+2t5b~jVo7!?U3xQbDNYLcvM%@8IEv~x{9I^va}lW# z%vS`goIh!`W~_y&sV9wqRbsk@=CmW=m`n4X!6Y_2ZZ*YiK7E;kYVUbf>yFq~huSE< z9&?e|a+Tex*sBRXD1)=h6bKFCQMbWLvHuz4GS6-8b2=dp_zeOJ5~5)(+#0`N6y7F5cmN*8Hyag4`^h`QUqn z=I6ry>IubKrMc!YTiR}(*5=~Zk%iz6DC=IPqKg%omiMsM;@Njbx15V--b8niI>G)k zhof`)&hnHU%Flh|ci{}PFA4>-E6lz4J!>!AQ;YKi&s?U9c^c}}aE>GeNtI^Kb38Le zvPTbVHE!WgaKQKRsZ`iKH*k0AT~}C1*82rAaje2iSK(;C`J-0nH0$ecuCJz6N&Alb zYCHCuGaS40rDON4e>*Nr@%rg-;kRR{Ys`g^keP8IR#CDqrG#=~}3i^XO&799ASnoU^;p zLW;hId@}N-eBQz58ov3FFnPC24l{2eKSqP(KH#{Gy+2||CesvkTAsxi>v8%trO2?a zRP8EKrl%zN+0#>@ys5X&8Cj zc2OetKBkqRC&wMJ2`hi)qgeWYj4By5w{ZOz;F?d!HVfb?<0e*49y%W4&e;+<9KgFQ=NCp1yn6_)VjPj;;k3VU z9Z$->yte*aWoWTdUSisoanM=;K29I9|IR_yl^$DnF&)=o>s!;AiIK{f$+Z|Y9d*iG z9GN*ghQ;+;ZLY%@rErK5-D$v~WxHdQ=MGjI`zmpNUBb7NgU?oV`hHBAZkdeA z+_{Z=lye0a-vMxpk7SYSP&~c&;uW+tm}6ep%Hf*t-BBM$W68AEniaSHn1k><-Yq^~ z5N_lkT*3R#6~gIotny>1d-lMsl24P2TYcMTwDjRs`cGHdFFWjU9!q#nwbJD=4i%0n zKAb)5LV3b|-pP%^?sLHWLNAgf&|dH% zx)x=Wr9J-V%p!e(GL0+V!VWAkduF?$*I6A+OU~T1yg%tI$%2=QSY~3sp z_vxw}I$b$8Sy|3}ZaW*$`?6AmdmpE>mKDDJOZIT(F2#(jLP_Zd83d*H74Xo&Nd`R~ zk(IMyHGY?~xo3ildcQ7#+9I(q-{r?A^iHY&$Z26tNN3r;fi{&)I;%Ok3P!2|`q>(y zD9$+ZS)WtWIce$^Jm33m?_%%M-WRAt$tCvRm;g~tH162s`nefHNamVuzn0!-@~sye*dQT%f#5}@iSUG!I9j5 z8~FWUoR=T9i{H9hiMcuEwDz1up)HZPkvlyw?v?oQC0gTK;%sI`V!}njgTKG@mfT1` c!S_48)!ttnhF37F(_cDV`YVn3?EnA( literal 0 HcmV?d00001 diff --git a/sources/Core/Text/Text.uc b/sources/Core/Text/Text.uc new file mode 100644 index 0000000..a8d6e27 --- /dev/null +++ b/sources/Core/Text/Text.uc @@ -0,0 +1,290 @@ +/** + * Text object, meant as Acedia's replacement for a `string` type, + * that is supposed to provide a better (although by no means full) + * Unicode support than what is available from built-in unrealscript functions. + * Main differences with `string` are: + * 1. Text is a reference type, that doesn't copy it's contents with each + * assignment. + * 2. It's functions such as `ToUpper()` work with larger sets of + * symbols than native functions such as `Caps()` that only work with + * ASCII Latin; + * 3. Can store a wider range of characters than `string`, although + * the only way to actually add them to `Text` is via directly + * inputting Unicode code points. + * 4. Since it's functionality implemented in unrealscript, + * Text is slower that a string; + * 5. Once created, Text object won't disappear until garbage collection + * is performed, even if it is not referenced anywhere. + + * API that provides extended text handling with extended Cyrillic (Russian) + * support (native functions like `Caps` only work with Latin letters). + * 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 Text extends AcediaObject; + +// Used to store a result of a `ParseSign()` function. +enum StringType +{ + STRING_Plain, + STRING_Colored, + STRING_Formatted +}; + +enum LetterCase +{ + LCASE_Lower, + LCASE_Upper +}; + +enum StringColorType +{ + STRCOLOR_Default, + STRCOLOR_Struct, + STRCOLOR_Alias +}; + +struct Character +{ + var int codePoint; + // `false` if relevant character has a particular color, + // `true` if it does not (use context-dependent default color). + var StringColorType colorType; + // Color of the relevant character if `isDefaultColor == false`. + var Color color; + var string colorAlias; +}; +// We will store our string data in two different ways at once to make getters +// faster at the cost of doing more work in functions that change the string. +var private array contents; + +/** + * Sets new value of the `Text` object, that has called this method, + * to be equal to the given `Text`. Does not change given `Text`. + * + * @param source After this function caller `Text` will have exactly + * the same contents as given parameter. + * @return Returns the calling `Text` object, to allow for function chaining. + */ +public final function Text Copy(Text otherText) +{ + contents = otherText.contents; + return self; +} + +/** + * Replaces data of caller `Text` object with data given by the array of + * Unicode code points, preserving the order of characters where it matters + * (some modifier code points are allowed arbitrary order in Unicode standard). + * + * `Text` isn't a simple wrapper around array of Unicode code points, so + * this function call should be assumed to be more expensive than + * a simple copy. + * + * @param source New contents of the `Text`. + * @return Returns the calling object, to allow for function chaining. + */ +public final function Text CopyRaw(array rawSource) +{ + contents = rawSource; + return self; +} + +/** + * Copies contents of the given string into caller `Text`. + * + * `Text` isn't a simple wrapper around unrealscript's `string`, so + * this function call should be assumed to be more expensive than simple + * `string` copy. + * + * @param source New contents of the caller `Text`. + * @return Returns the calling `Text` object, to allow for function chaining. + */ +public final function Text CopyString(string source) +{ + CopyRaw(_().text.StringToRaw(source)); + return self; +} + +/** + * Returns data in the caller `Text` object in form of an array of + * Unicode code points, preserving the order of characters where it matters + * (some modifier code points are allowed arbitrary order in Unicode standard). + */ +public final function array ToRaw() +{ + return contents; +} + +/** + * Returns the `string` representation of contents of the caller `Text`. + * + * Unreal Engine doesn't seem to store code points higher than 2^16 in + * `string`, so some data might be lost in the process. + * (To check if it concerns you, refer to the Unicode symbol table, + * but it is not a problem for most people). + */ +public final function string ToString(optional StringType resultType) +{ + return _().text.RawToString(contents, resultType); +} + +/** + * Checks if the caller `Text` and a given `Text` have contain equal text + * content, according to Unicode standard. By default case-sensitive. + */ +public final function bool IsEqual +( + Text otherText, + optional bool caseInsensitive +) +{ + local int i; + local array otherContentsCopy; + local TextAPI api; + if (contents.length != otherText.contents.length) return false; + + api = _().text; + // There's some evidence that UnrealEngine might copy the whole + // `otherText.contents` each time we access any element, + // so just copy it once. + otherContentsCopy = otherText.contents; + for (i = 0; i < contents.length; i += 1) + { + if (!api.AreEqual(contents[i], otherContentsCopy[i], caseInsensitive)) + { + return false; + } + } + return true; +} + +/** + * Checks if the caller `Text` contains the same text content as the given + * `string`. By default case-sensitive. + * + * If text contains Unicode code points that can't be stored in + * a given `string`, equality should be considered impossible. + */ +public final function bool IsEqualToString +( + string source, + optional bool caseInsensitive, + optional StringType sourceType +) +{ + local int i; + local array rawSource; + local TextAPI api; + api = _().text; + rawSource = api.StringToRaw(source, sourceType); + if (contents.length != rawSource.length) return false; + + for (i = 0; i < contents.length; i += 1) + { + if (!api.AreEqual(contents[i], rawSource[i], caseInsensitive)) + { + return false; + } + } + return true; +} + +/** + * Returns `true` if the string has no characters, otherwise returns `false`. + */ +public final function bool IsEmpty() +{ + return (contents.length == 0); +} + +/** + * Attempts to returns Unicode code point, stored in caller `Text` at the + * given `index`. + * + * Doesn't properly work if `Text` contains characters consisting of + * multiple code points. + * + * @return For a valid index (non-negative, not exceeding the length, + * given by `GetLength()` of the `Text`) returns Unicode code point, + * stored in caller `Text` at the given `index`; otherwise - returns `-1`. + */ +public final function Character GetCharacter(optional int index) +{ + if (index < 0) return _().text.GetInvalidCharacter(); + if (index >= contents.length) return _().text.GetInvalidCharacter(); + + return contents[index]; +} + +/* + * Converts caller `Text` to lower case. + * + * Changes every symbol contained in caller `Text` to it's lower case folding + * (according to Unicode standard). Symbols without lower case folding + * (like "&" or "!") are left unchanged. + * + * @return Returns the calling object, to allow for function chaining. + */ +public final function Text ToLower() +{ + local int i; + local TextAPI api; + api = _().text; + for (i = 0; i < contents.length; i += 1) + { + contents[i] = api.ToLower(contents[i]); + } + return self; +} + +/* + * Converts caller `Text` to upper case. + * + * Changes every symbol contained in caller `Text` to it's upper case folding + * (according to Unicode standard). Symbols without upper case folding + * (like "&" or "!") are left unchanged. + * + * @return Returns the calling object, to allow for function chaining. + */ +public final function Text ToUpper() +{ + local int i; + local TextAPI api; + api = _().text; + for (i = 0; i < contents.length; i += 1) + { + contents[i] = api.ToUpper(contents[i]); + } + return self; +} + +public final function int GetHash() { + return _().text.GetHashRaw(contents); +} + +/** + * Returns amount of symbols in the caller `Text`. + */ +public final function int GetLength() +{ + return contents.length; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Core/Text/TextAPI.uc b/sources/Core/Text/TextAPI.uc new file mode 100644 index 0000000..009a138 --- /dev/null +++ b/sources/Core/Text/TextAPI.uc @@ -0,0 +1,1281 @@ +/** + * API that provides functions for working with text data, including + * standard `string` and Acedia's `Text` and raw string format + * `array`. + * 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 TextAPI extends Singleton + dependson(Text); + +// Escape code point is used to change output's color and is used in +// Unreal Engine's `string`s. +var private const int CODEPOINT_ESCAPE; +// Opening and closing symbols for colored blocks in formatted strings. +var private const int CODEPOINT_OPEN_FORMAT; +var private const int CODEPOINT_CLOSE_FORMAT; +// Symbol to escape any character in formatted strings, +// including above mentioned opening and closing symbols. +var private const int CODEPOINT_FORMAT_ESCAPE; + +// Every formatted string essentially consists of multiple differently +// formatted (colored) parts. Such strings will be more convenient for us to +// work with if we separate them from each other. +// This structure represents one such block: maximum uninterrupted +// substring, every character of which has identical formatting. +// Do note that a single block does not define text formatting, - +// it is defined by the whole sequence of blocks before it +// (if `isOpening == false` you only know that you should change previous +// formatting, but you do not know to what). +struct FormattedBlock +{ + // Did this block start by opening or closing formatted part? + // Ignored for the very first block without any formatting. + var bool isOpening; + // Full text inside the block, without any formatting + var array contents; + // Formatting tag for this block + // (ignored for `isOpening == false`) + var string tag; + // Whitespace symbol that separates tag from the `contents`; + // For the purposes of reassembling a `string` broken into blocks. + var Text.Character delimiter; +}; + +private final function FormattedBlock CreateFormattedBlock(bool isOpening) +{ + local FormattedBlock newBlock; + newBlock.isOpening = isOpening; + return newBlock; +} + +// Function that breaks formatted string into array of `FormattedBlock`s. +// Returned array is guaranteed to always have at least one block. +// First block in array always corresponds to part of the input string +// (`source`) without any formatting defined, even if it's empty. +// This is to avoid `FormattedBlock` having a third option besides two defined +// by `isOpening` variable. +private final function array DecomposeFormattedString( + string source) +{ + local Parser parser; + local Text.Character nextCharacter; + local FormattedBlock nextBlock; + local array result; + parser = ParseString(source, STRING_Plain); + while (!parser.HasFinished()) { + parser.MCharacter(nextCharacter); + // New formatted block by "{" + if (IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) + { + result[result.length] = nextBlock; + nextBlock = CreateFormattedBlock(true); + parser.MUntil(nextBlock.tag,, true).MCharacter(nextBlock.delimiter); + if (!parser.Ok()) { + break; + } + continue; + } + // New formatted block by "}" + if (IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) + { + result[result.length] = nextBlock; + nextBlock = CreateFormattedBlock(false); + continue; + } + // Escaped sequence + if (IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { + parser.MCharacter(nextCharacter); + } + if (!parser.Ok()) { + break; + } + nextBlock.contents[nextBlock.contents.length] = nextCharacter; + } + // Only put in empty block if there is nothing else. + if (nextBlock.contents.length > 0 || result.length == 0) { + result[result.length] = nextBlock; + } + _.memory.Free(parser); + return result; +} + +/** + * Converts given `string` (`source`) of specified type `sourceType` + * into the "raw data", a sequence of individually colored symbols. + * + * @param source `string` that we want to break into a raw data. + * @param sourceType Type of the `string`, plain by default. + * @return Raw data, corresponding to the given `string` if it's + * treated according to `sourceType`. + */ +public final function array StringToRaw( + string source, + optional Text.StringType sourceType +) +{ + if (sourceType == STRING_Plain) return StoR_Plain(source); + if (sourceType == STRING_Formatted) return StoR_Formatted(source); + return StoR_Colored(source); +} + +// Subroutine for converting plain string into raw data +private final function array StoR_Plain(string source) +{ + local int i; + local int sourceLength; + local Text.Character nextCharacter; + local array result; + + // Decompose `source` into integer codes + sourceLength = Len(source); + for (i = 0; i < sourceLength; i += 1) + { + nextCharacter.codePoint = Asc(Mid(source, i, 1)); + result[result.length] = nextCharacter; + } + return result; +} + +// Subroutine for converting colored string into raw data +private final function array StoR_Colored(string source) +{ + local int i; + local int sourceLength; + local array sourceAsIntegers; + local Text.Character nextCharacter; + local array result; + + // Decompose `source` into integer codes + sourceLength = Len(source); + for (i = 0; i < sourceLength; i += 1) + { + sourceAsIntegers[sourceAsIntegers.length] = Asc(Mid(source, i, 1)); + } + // Record string as array of `Character`s, parsing color tags + i = 0; + while (i < sourceLength) + { + if (sourceAsIntegers[i] == CODEPOINT_ESCAPE) + { + if (i + 3 >= sourceLength) break; + nextCharacter.colorType = STRCOLOR_Struct; + nextCharacter.color = _.color.RGB( sourceAsIntegers[i + 1], + sourceAsIntegers[i + 2], + sourceAsIntegers[i + 3]); + i += 4; + } + else + { + nextCharacter.codePoint = sourceAsIntegers[i]; + result[result.length] = nextCharacter; + i += 1; + } + } + return result; +} + +// Subroutine for converting formatted string into raw data +private final function array StoR_Formatted(string source) +{ + local int i, j; + local Parser parser; + local Text.Character nextCharacter; + local array decomposedSource; + local array blockContentsCopy; + local array colorStack; + local array result; + parser = Parser(_.memory.Borrow(class'Parser')); + nextCharacter.colorType = STRCOLOR_Default; + decomposedSource = DecomposeFormattedString(source); + // First element of `decomposedSource` is special and has + // no color information, see `DecomposeFormattedString()` for details. + result = decomposedSource[0].contents; + for (i = 1; i < decomposedSource.length; i += 1) + { + if (decomposedSource[i].isOpening) + { + parser.Initialize(decomposedSource[i].tag); + nextCharacter = PushIntoColorStack(colorStack, parser); + } + else if (colorStack.length > 0) { + nextCharacter = PopColorStack(colorStack); + } + // This whole method is mostly to decide which formatting each symbol + // should have, so we only copy code points from block's `contents`. + blockContentsCopy = decomposedSource[i].contents; + for (j = 0; j < blockContentsCopy.length; j += 1) + { + nextCharacter.codePoint = blockContentsCopy[j].codePoint; + result[result.length] = nextCharacter; + } + } + _.memory.Free(parser); + return result; +} + +// Following two functions are to maintain a "color stack" that will +// remember unclosed colors (new colors are obtained from a parser) defined in +// formatted string, on order. +// It is necessary to deal with possible folded formatting definitions in +// formatted strings. +// For storing the color information we simply use `Text.Character`, +// ignoring all information that is not related to colors. +private final function Text.Character PushIntoColorStack( + out array stack, + Parser colorDefinitionParser) +{ + local Text.Character coloredCharacter; + if (colorDefinitionParser.Match("$").Ok()) { + coloredCharacter.colorType = STRCOLOR_Alias; + colorDefinitionParser.MUntil(coloredCharacter.colorAlias,, true); + } + else { + coloredCharacter.colorType = STRCOLOR_Struct; + } + colorDefinitionParser.R(); + if (!_.color.ParseWith(colorDefinitionParser, coloredCharacter.color)) { + coloredCharacter.colorType = STRCOLOR_Default; + } + stack[stack.length] = coloredCharacter; + return coloredCharacter; +} + +private final function Text.Character PopColorStack( + out array stack) +{ + local Text.Character coloredCharacter; + stack.length = Max(0, stack.length - 1); + if (stack.length > 0) { + coloredCharacter = stack[stack.length - 1]; + } + else { + coloredCharacter.colorType = STRCOLOR_Default; + } + return coloredCharacter; +} + +/** + * Converts given "raw data" (`source`) into a `string` of a specified type + * `sourceType`. + * + * @param source Raw data that we want to assemble into a `string`. + * @param sourceType Type of the `string` we want to assemble, + * plain by default. + * @return `string`, assembled from given "raw data" in `sourceType` format. + */ +public final function string RawToString( + array source, + optional Text.StringType sourceType, + optional Color defaultColor +) +{ + if (sourceType == STRING_Plain) return RtoS_Plain(source); + if (sourceType == STRING_Formatted) return RtoS_Formatted(source); + return RtoS_Colored(source, defaultColor); +} + +// Subroutine for converting raw data into plain `string` +private final function string RtoS_Plain(array rawData) +{ + local int i; + local string result; + for (i = 0; i < rawData.length; i += 1) + { + result $= Chr(rawData[i].codePoint); + } + return result; +} + +// Subroutine for converting raw data into colored `string` +private final function string RtoS_Colored +( + array rawData, + Color defaultColor +) +{ + local int i; + local Color currentColor; + local Color nextColor; + local string result; + defaultColor = _.color.FixColor(defaultColor); + for (i = 0; i < rawData.length; i += 1) + { + // Skip any escape codepoints to avoid unnecessary colorization + if (IsCodePoint(rawData[i], CODEPOINT_ESCAPE)) continue; + // Find `nextColor` that `rawData[i]` is supposed to have + if (rawData[i].colorType != STRCOLOR_Default) + { + nextColor = _.color.FixColor(rawData[i].color); + } + else + { + nextColor = defaultColor; + } + // Add color tag (either initially or when color changes) + if (i == 0 || !_.color.AreEqual(nextColor, currentColor)) + { + currentColor = nextColor; + result $= Chr(CODEPOINT_ESCAPE); + result $= Chr(currentColor.r); + result $= Chr(currentColor.g); + result $= Chr(currentColor.b); + } + result $= Chr(rawData[i].codePoint); + } + return result; +} + +// Subroutine for converting raw data into formatted `string` +private final function string RtoS_Formatted(array rawData) +{ + local int i; + local bool isColorChange; + local Text.Character previousCharacter; + local string result; + previousCharacter.colorType = STRCOLOR_Default; + for (i = 0; i < rawData.length; i += 1) + { + isColorChange = rawData[i].colorType != previousCharacter.colorType; + if (!isColorChange && rawData[i].colorType != STRCOLOR_Default) + { + isColorChange = !_.color.AreEqual( rawData[i].color, + previousCharacter.color); + } + if (isColorChange) + { + if (previousCharacter.colorType != STRCOLOR_Default) { + result $= "}"; + } + if (rawData[i].colorType == STRCOLOR_Struct) { + result $= "{" $ _.color.ToString(rawData[i].color) $ " "; + } + if (rawData[i].colorType == STRCOLOR_Alias) { + result $= "{" $ "$" $ rawData[i].colorAlias $ " "; + } + } + if ( IsCodePoint(rawData[i], CODEPOINT_OPEN_FORMAT) + || IsCodePoint(rawData[i], CODEPOINT_CLOSE_FORMAT)) { + result $= "&"; + } + result $= Chr(rawData[i].codePoint); + previousCharacter = rawData[i]; + } + if (previousCharacter.colorType != STRCOLOR_Default) { + result $= "}"; + } + return result; +} + +/** + * Converts between three different types of `string`. + * + * @param input `string` to convers + * @param currentType Current type of the given `string`. + * @param newType Type to which given `string` must be converted to. + * @param defaultColor In case `input` is being converted into a + * `STRING_Colored` type, this color will be used for characters + * without one. Otherwise unused. + */ +public final function string ConvertString( + string input, + Text.StringType currentType, + Text.StringType newType, + optional Color defaultColor) +{ + local array rawData; + if (currentType == newType) return input; + rawData = StringToRaw(input, currentType); + return RawToString(rawData, newType, defaultColor); +} + +/** + * Checks if given character is lower case. + * + * Result of this method describes whether character is + * precisely "lower case", instead of just "not being upper of title case". + * That is, this method will return `true` for characters that aren't + * considered either lowercase or uppercase (like "#", "@" or "&"). + * + * @param character Character to test for lower case. + * @return `true` if given character is lower case. + */ +public final function bool IsLower(Text.Character character) +{ + // Small Latin letters + if (character.codePoint >= 97 && character.codePoint <= 122) { + return true; + } + // Small Cyrillic (Russian) letters + if (character.codePoint >= 1072 && character.codePoint <= 1103) { + return true; + } + // `ё` + if (character.codePoint == 1105) { + return true; + } + return false; +} + +/** + * Checks if given `string` is in lower case. + * + * This function returns `true` as long as it's equal to it's own + * `ToLowerString()` folding. + * This means that it can contain symbols that neither lower or upper case, or + * upper case symbols that don't have a lower case folding. + * + * To check whether a symbol is lower cased, use a combination of + * `GetCharacter()` and `IsLower()`. + * + * @param source `string` to check for being in lower case. + * @param sourceType Type of the `string` to check; default is plain string. + * @return `true` if `string` is equal to it's own lower folding, + * (per character given by `ToLower()` method). + */ +public final function bool IsLowerString +( + string source, + optional Text.StringType sourceType +) +{ + local int i; + local array rawData; + rawData = StringToRaw(source, sourceType); + for (i = 0; i < rawData.length; i += 1) + { + if (rawData[i] != ToLower(rawData[i])) { + return false; + } + } + return true; +} + +/** + * Checks if given character is upper case. + * + * Result of this method describes whether character is + * precisely "upper case", instead of just "not being upper of title case". + * That is, this method will return `true` for characters that aren't + * considered either uppercase or uppercase (like "#", "@" or "&"). + * + * @param character Character to test for upper case. + * @return `true` if given character is upper case. + */ +public final function bool IsUpper(Text.Character character) +{ + // Capital Latin letters + if (character.codePoint >= 65 && character.codePoint <= 90) { + return true; + } + // Capital Cyrillic (Russian) letters + if (character.codePoint >= 1040 && character.codePoint <= 1071) { + return true; + } + // `Ё` + if (character.codePoint == 1025) { + return true; + } + return false; +} + +/** + * Checks if given `string` is in upper case. + * + * This function returns `true` as long as it's equal to it's own + * `ToUpperString()` folding. + * This means that it can contain symbols that neither lower or upper case, or + * lower case symbols that don't have an upper case folding. + * + * To check whether a symbol is upper cased, use a combination of + * `GetCharacter()` and `IsUpper()`. + * + * @param source `string` to check for being in upper case. + * @param sourceType Type of the `string` to check; default is plain string. + * @return `true` if `string` is equal to it's own upper folding, + * (per character given by `ToUpper()` method). + */ +public final function bool IsUpperString +( + string source, + optional Text.StringType sourceType +) +{ + local int i; + local array rawData; + rawData = StringToRaw(source, sourceType); + for (i = 0; i < rawData.length; i += 1) + { + if (rawData[i] != ToUpper(rawData[i])) { + return false; + } + } + return true; +} + +/** + * Checks if given character corresponds to a digit. + * + * @param codePoint Unicode code point to check for being a digit. + * @return `true` if given Unicode code point is a digit, `false` otherwise. + */ +public final function bool IsDigit(Text.Character character) +{ + if (character.codePoint >= 48 && character.codePoint <= 57) { + return true; + } + return false; +} + +/** + * Checks if given character is an ASCII character. + * + * @param character Character to check for being a digit. + * @return `true` if given character is a digit, `false` otherwise. + */ +public final function bool IsASCII(Text.Character character) +{ + if (character.codePoint >= 0 && character.codePoint <= 127) { + return true; + } + return false; +} + +/** + * Checks if given `string` consists only from ASCII characters + * (ignoring characters in 4-byte color change sequences in colored strings). + * + * @param source `string` to test for being ASCII-only. + * @param sourceType Type of the passed `string`. + * @return `true` if passed `string` contains only ASCII characters. + */ +public final function bool IsASCIIString +( + string source, + optional Text.StringType sourceType +) +{ + local int i; + local array rawData; + rawData = StringToRaw(source, sourceType); + for (i = 0; i < rawData.length; i += 1) + { + if (!IsASCII(rawData[i])) { + return false; + } + } + return true; +} + +/** + * Checks if given character represents some kind of white space + * symbol (like space ~ 0x0020, tab ~ 0x0009, etc.), + * according to either Unicode or a more classic space symbol definition, + * that includes: + * whitespace, tab, line feed, line tabulation, form feed, carriage return. + * + * @param character Character to check for being a whitespace. + * @return `true` if given character is a whitespace, `false` otherwise. + */ +public final function bool IsWhitespace(Text.Character character) +{ + switch (character.codePoint) + { + // Classic whitespaces + case 0x0020: // Whitespace + case 0x0009: // Tab + case 0x000A: // Line feed + case 0x000B: // Line tabulation + case 0x000C: // Form feed + case 0x000D: // Carriage return + // Unicode Characters in the 'Separator, Space' Category + case 0x00A0: // No-break space + case 0x1680: // Ogham space mark + case 0x2000: // En quad + case 0x2001: // Em quad + case 0x2002: // En space + case 0x2003: // Em space + case 0x2004: // Three-per-em space + case 0x2005: // Four-per-em space + case 0x2006: // Six-per-em space + case 0x2007: // Figure space + case 0x2008: // Punctuation space + case 0x2009: // Thin space + case 0x200A: // Hair space + case 0x202F: // Narrow no-break space + case 0x205F: // Medium mathematical space + case 0x3000: // Ideographic space + return true; + default: + return false; + } + return false; +} + +/** + * Checks if passed character is one of the following quotation mark symbols: + * `"`, `'`, `\``. + * + * @param character Character to check for being a quotation mark. + * @return `true` if given Unicode code point denotes one of the recognized + * quote symbols, `false` otherwise. + */ +public final function bool IsQuotationMark(Text.Character character) +{ + if (character.codePoint == 0x0022) return true; + if (character.codePoint == 0x0027) return true; + if (character.codePoint == 0x0060) return true; + return false; +} + +/** + * Converts given character into a number it represents in some base + * (from 2 to 36), i.e.: + * 1 -> 1 + * 7 -> 7 + * a -> 10 + * e -> 14 + * z -> 35 + * + * @param character Character to convert into integer. + * Case does not matter, i.e. "a" and "A" will be treated the same. + * @param base Base to use for conversion. + * Valid values are from `2` to `36` (inclusive); + * If invalid value was specified (such as default `0`), + * the base of `36` is assumed, since that would allow for all possible + * characters to be converted. + * @return Positive integer value that is denoted by + * given character in given base; + * `-1` if given character does not represent anything in the given base. + */ +public final function int CharacterToInt +( + Text.Character character, + optional int base +) +{ + local int number; + if (base < 2 || base > 36) { + base = 36; + } + character = ToLower(character); + // digits + if (character.codePoint >= 0x0030 && character.codePoint <= 0x0039) { + number = character.codePoint - 0x0030; + } + // a-z + else if (character.codePoint >= 0x0061 && character.codePoint <= 0x007a) { + number = character.codePoint - 0x0061 + 10; + } + else { + return -1; + } + if (number >= base) { + return -1; + } + return number; +} + +/** + * Checks if given `character` can be represented by a given `codePoint` in + * Unicode standard. + * + * @param character Character to check. + * @param codePoint Code point to check. + * @return `true` if given character can be represented by a given code point + * and `false` otherwise. + */ +public final function bool IsCodePoint(Text.Character character, int codePoint) +{ + return (character.codePoint == codePoint); +} + +/** + * Returns a particular character from a given `string`, of a given type, + * with preserved color information. + * + * @param source String, from which to fetch the character. + * @param position Which, in order, character to fetch + * (starting counting from '0'). + * By default returns first (`0`th) character. + * @param sourceType Type of the given `source` `string`. + * @return Character from a `source` at a given position `position`. + * If given position is out-of-bounds for a given `string` + * (it is either negative or at least the same as a total character count), + * - returns invalid character. + */ +public final function Text.Character GetCharacter +( + string source, + optional int position, + optional Text.StringType sourceType +) +{ + local Text.Character resultCharacter; + local array rawData; + if (position < 0) return GetInvalidCharacter(); + + // `STRING_Plain` is the only type where we do not need to do any parsing + // and get just fetch a character, so handle it separately. + if (sourceType == STRING_Plain) + { + if (position >= Len(source)) { + return GetInvalidCharacter(); + } + resultCharacter.codePoint = Asc(Mid(source, position, 1)); + return resultCharacter; + } + rawData = StringToRaw(source, sourceType); + if (position >= rawData.length) { + return GetInvalidCharacter(); + } + return rawData[position]; +} + +/** + * Returns color of a given `Character` with set default color. + * + * `Character`s can have their color set to "default", meaning they would use + * whatever considered default color in the context. + * + * @param character `Character`, which color to return. + * @param defaultColor Color, considered default. + * @return Supposed color of a given `Character`, assuming default color is + * `defaultColor`. + */ +public final function Color GetCharacterColor( + Text.Character character, + Color defaultColor) +{ + if (character.colorType == STRCOLOR_Default) { + return defaultColor; + } + return character.color; +} + +/** + * Returns character that is considered invalid. + * + * It is not unique, there can be different invalid characters. + * + * @return Invalid character instance. + */ +public final function Text.Character GetInvalidCharacter() +{ + local Text.Character result; + result.codePoint = -1; + return result; +} + +/** + * Checks if given character is invalid. + * + * @param character Character to check. + * @return `true` if passed character is valid and `false` otherwise. + */ +public final function bool IsValidCharacter(Text.Character character) +{ + return (character.codePoint >= 0); +} + +/** + * Checks if given characters are equal, with or without accounting + * for their case. + * + * @param codePoint1 Character to compare. + * @param codePoint2 Character to compare. + * @param caseInsensitive Optional parameter, + * if `false` we will require characters to be exactly the same, + * if `true` we will also consider characters equal if they + * only differ by case. + * @return `true` if given characters are considered equal, + * `false` otherwise. + */ +public final function bool AreEqual( + Text.Character character1, + Text.Character character2, + optional bool caseInsensitive +) +{ + if (character1.codePoint == character2.codePoint) return true; + if (character1.codePoint < 0 && character2.codePoint < 0) return true; + + if (caseInsensitive) + { + character1 = ToLower(character1); + character2 = ToLower(character2); + } + return (character1.codePoint == character2.codePoint); +} + +/** + * Checks if given `string`s are equal to each other, with or without + * accounting for their case. + * + * @param string1 `string` to compare. + * @param string2 `string` to compare. + * @param caseInsensitive Optional parameter, + * if `false` we will require `string`s to be exactly the same, + * if `true` we will also consider `string`s equal if their corresponding + * characters only differ by case. + * @return `true` if given `string`s are considered equal, `false` otherwise. + */ +public final function bool AreEqualStrings( + string string1, + string string2, + optional bool caseInsensitive +) +{ + local int i; + local array rawData1, rawData2; + rawData1 = StringToRaw(string1); + rawData2 = StringToRaw(string2); + if (rawData1.length != rawData2.length) return false; + + for (i = 0; i < rawData1.length; i += 1) + { + if (!AreEqual(rawData1[i], rawData2[i], caseInsensitive)) return false; + } + return true; +} + +/** + * Converts Unicode code point into it's lower case folding, + * as defined by Unicode standard. + * + * @param codePoint Code point to convert into lower case. + * @return Lower case folding of the given code point. If Unicode standard does + * not define any lower case folding (like "&" or "!") for given code point, - + * function returns given code point unchanged. + */ +public final function Text.Character ToLower(Text.Character character) +{ + local int newCodePoint; + newCodePoint = + class'UnicodeData'.static.ToLowerCodePoint(character.codePoint); + if (newCodePoint >= 0) { + character.codePoint = newCodePoint; + } + return character; +} + +/** + * Converts Unicode code point into it's upper case version, + * as defined by Unicode standard. + * + * @param codePoint Code point to convert into upper case. + * @return Upper case version of the given code point. If Unicode standard does + * not define any upper case version (like "&" or "!") for given code point, - + * function returns given code point unchanged. + */ +public final function Text.Character ToUpper(Text.Character character) +{ + local int newCodePoint; + newCodePoint = + class'UnicodeData'.static.ToUpperCodePoint(character.codePoint); + if (newCodePoint >= 0) { + character.codePoint = newCodePoint; + } + return character; +} + +/** + * Converts `string` to lower case. + * + * Changes every symbol in the `string` to their lower case folding. + * Characters without lower case folding (like "&" or "!") are left unchanged. + * + * @param source `string` that will be converted into a lower case. + * @return Lower case folding of a given `string`. + */ +public final function string ToLowerString( + string source, + optional Text.StringType sourceType +) +{ + if (sourceType == STRING_Plain) { + return ConvertCaseForString_Plain(source, LCASE_Lower); + } + if (sourceType == STRING_Formatted) { + return ConvertCaseForString_Formatted(source, LCASE_Lower); + } + return ConvertCaseForString_Colored(source, LCASE_Lower); +} + +/** + * Converts `string` to upper case. + * + * Changes every symbol in the `string` to their upper case folding. + * Characters without upper case folding (like "&" or "!") are left unchanged. + * + * @param source `string` that will be converted into an upper case. + * @return Upper case folding of a given `string`. + */ +public final function string ToUpperString( + string source, + optional Text.StringType sourceType +) +{ + if (sourceType == STRING_Plain) { + return ConvertCaseForString_Plain(source, LCASE_Upper); + } + if (sourceType == STRING_Formatted) { + return ConvertCaseForString_Formatted(source, LCASE_Upper); + } + return ConvertCaseForString_Colored(source, LCASE_Upper); +} + +private final function string ConvertCaseForString_Plain +( + string source, + Text.LetterCase targetCase +) +{ + local int i; + local array rawData; + rawData = StringToRaw(source, STRING_Plain); + for (i = 0; i < rawData.length; i += 1) + { + if (targetCase == LCASE_Lower) { + rawData[i] = ToLower(rawData[i]); + } + else { + rawData[i] = ToUpper(rawData[i]); + } + } + return RawToString(rawData, STRING_Plain); +} + +private final function string ConvertCaseForString_Colored +( + string source, + Text.LetterCase targetCase +) +{ + local int i; + local string result; + local array rawData; + rawData = StringToRaw(source, STRING_Colored); + for (i = 0; i < rawData.length; i += 1) + { + if (targetCase == LCASE_Lower) { + rawData[i] = ToLower(rawData[i]); + } + else { + rawData[i] = ToUpper(rawData[i]); + } + } + result = RawToString(rawData, STRING_Colored); + if (rawData.length > 0 && rawData[0].colorType == STRCOLOR_Default) { + result = Mid(result, 4); + } + return result; +} + +private final function string ConvertCaseForString_Formatted +( + string source, + Text.LetterCase targetCase +) +{ + // TODO: finish it later, no one needs it right now, + // no idea wtf I even bothered with these functions + return source; +} + +/** + * Returns hash for a raw `string` data. + * + * Uses djb2 algorithm, somewhat adapted to make use of formatting + * (color) information. Hopefully it did not broke horribly. + * + * @param rawData Data to calculate hash of. + * @return Hash of the given data. + */ +public final function int GetHashRaw(array rawData) { + local int i; + local int colorInt; + local int hash; + hash = 5381; + for (i = 0; i < rawData.length; i += 1) { + // hash * 33 + rawData[i].codePoint + hash = ((hash << 5) + hash) + rawData[i].codePoint; + if (rawData[i].colorType != STRCOLOR_Default) { + colorInt = rawData[i].color.r + + rawData[i].color.g * 0x00ff + + rawData[i].color.b * 0xffff; + hash = ((hash << 5) + hash) + colorInt; + } + } + return hash; +} + +/** + * Returns hash for a `string` data. + * + * Uses djb2 algorithm, somewhat adapted to make use of formatting + * (color) information. Hopefully it did not broke horribly. + * + * @param rawData `string` to calculate hash of. + * @param sourceType Type of the `string`, in case you want has to be more + * formatting-independent. Leaving default value (`STRING_Plain`) should be + * fine for almost any use case. + * @return Hash of the given data. + */ +public final function int GetHash( + string source, + optional Text.StringType sourceType) { + return GetHashRaw(StringToRaw(source, sourceType)); +} + +/** + * Creates a new, empty `Text`. + * + * This is a shortcut, same result cam be achieved by `new class'Text'`. + * + * @return Brand new, empty instance of `Text`. + */ +public final function Text Empty() +{ + local Text newText; + newText = new class'Text'; + return newText; +} + +/** + * Creates a `Text` that will contain a given `string`. Parameter made optional + * to enable easier way of creating empty `Text`. + * + * @param source `string` that will be copied into returned `Text`. + * @return New instance (not taken from the object pool) of `Text` that + * will contain passed `string`. + */ +public final function Text FromString(optional string source) +{ + local Text newText; + newText = new class'Text'; + newText.CopyString(source); + return newText; +} + +/** + * Creates a `Text` that will contain `string` with characters recorded in the + * given array. Parameter made optional to enable easier way of + * creating empty `Text`. + * + * @param rawData Sequence of characters that will be copied into + * returned `Text`. + * @return New instance (not taken from the object pool) of `Text` that + * will contain passed sequence of Unicode code points. + */ +public final function Text FromRaw(array rawData) +{ + local Text newText; + newText = new class'Text'; + newText.CopyRaw(rawData); + return newText; +} + +/** + * Method for creating a new, uninitialized parser object. + * + * Always creates a new parser. This method should be used when you plan to + * store created `Parser` and reuse later. + * To parse something once it's advised to use + * `Parse()`, `ParseString()` or `ParseRaw()` instead. + * + * It is a good practice to free created `Parser` once you don't need it. + * + * @see `Parser` + * @return Guaranteed to be new, uninitialized `Parser`. + */ +public final function Parser NewParser() +{ + return (new class'Parser'); +} + +/** + * Method for creating a new parser, initialized with contents of given `Text`. + * + * Always creates a new parser. This method should be used when you plan to + * store created `Parser` and reuse later. + * To parse something once it's advised to use `Parse()` instead. + * + * It is a good practice to free created `Parser` once you don't need it. + * + * @see `Parser` + * @param source Returned `Parser` will be setup to parse the contents of + * the passed `Text`. + * If `none` value is passed, - parser won't be initialized. + * @return Guaranteed to be new `Parser`, + * initialized with contents of `source`. + */ +public final function Parser NewParserFromText(Text source) +{ + local Parser parser; + parser = new class'Parser'; + parser.InitializeT(source); + return parser; +} + +/** + * Method for creating a new parser, initialized with a given `string`. + * + * Always creates a new parser. This method should be used when you plan to + * store created `Parser` and reuse later. + * To parse something once it's advised to use `ParseString()` instead. + * + * It is a good practice to free created `Parser` once you don't need it. + * + * @see `Parser` + * @param source Returned `Parser` will be setup to parse the `source`. + * @return Guaranteed to be new `Parser`, initialized with given `string`. + */ +public final function Parser NewParserFromString(string source) +{ + local Parser parser; + parser = new class'Parser'; + parser.Initialize(source); + return parser; +} + +/** + * Method for creating a new parser, initialized with a given sequence of + * characters. + * + * Always creates a new parser. This method should be used when you plan to + * store created `Parser` and reuse later. + * To parse something once it's advised to use `ParseRaw()` instead. + * + * It is a good practice to free created `Parser` once you don't need it. + * + * @see `Parser` + * @param source Returned `Parser` will be setup to parse passed + * characters sequence. + * @return Guaranteed to be new `Parser`, initialized with given + * characters sequence. + */ +public final function Parser NewParserFromRaw(array source) +{ + local Parser parser; + parser = new class'Parser'; + parser.InitializeRaw(source); + return parser; +} + +/** + * Returns "temporary" `Parser` that can be used for one-time parsing, + * initialized with a given sequence of characters. + * It will be automatically freed to be reused again after + * current tick ends. + * + * Returned `Parser` does not have to be a new object and + * it is possible that it is still referenced by some buggy or malicious code. + * To ensure that no problem arises: + * 1. Re-initialize returned `Parser` after executing any piece of + * code that you do not trust to misuse `Parser`s; + * 2. Do not use obtained reference after current tick ends or + * calling `FreeParser()` on it. + * For more details @see `Parser`. + * + * @param source Returned `Parser` will be setup to parse passed + * characters sequence. + * @return Temporary `Parser`, initialized with given + * characters sequence. + */ +public final function Parser ParseRaw(array source) +{ + local Parser parser; + parser = Parser(_.memory.Borrow(class'Parser')); + if (parser != none) + { + parser.InitializeRaw(source); + return parser; + } + return none; +} + +/** + * Returns "temporary" `Parser` that can be used for one-time parsing, + * initialized with contents of given `Text`. + * It will be automatically freed to be reused again after + * current tick ends. + * + * Returned `Parser` does not have to be a new object and + * it is possible that it is still referenced by some buggy or malicious code. + * To ensure that no problem arises: + * 1. Re-initialize returned `Parser` after executing any piece of + * code that you do not trust to misuse `Parser`s; + * 2. Do not use obtained reference after current tick ends or + * calling `FreeParser()` on it. + * For more details @see `Parser`. + * + * @param source Returned `Parser` will be setup to parse the contents of + * the passed `Text`. + * @return Temporary `Parser`, initialized with contents of the given `Text`. + */ +public final function Parser Parse(Text source) +{ + local Parser parser; + if (source == none) return NewParser(); + + parser = Parser(_.memory.Borrow(class'Parser')); + if (parser != none) + { + parser.InitializeT(source); + return parser; + } + return none; +} + +/** + * Returns "temporary" `Parser` that can be used for one-time parsing, + * initialized `string`. + * It will be automatically freed to be reused again after + * current tick ends. + * + * Returned `Parser` does not have to be a new object and + * it is possible that it is still referenced by some buggy or malicious code. + * To ensure that no problem arises: + * 1. Re-initialize returned `Parser` after executing any piece of + * code that you do not trust to misuse `Parser`s; + * 2. Do not use obtained reference after current tick ends or + * calling `FreeParser()` on it. + * For more details @see `Parser`. + * + * @param source Returned `Parser` will be setup to parse `source`. + * @return Temporary `Parser`, initialized with the given `string`. + */ +public final function Parser ParseString ( + string source, +optional Text.StringType sourceType) { + local Parser parser; + parser = Parser(_.memory.Borrow(class'Parser')); + if (parser != none) { + parser.Initialize(source, sourceType); + return parser; + } + return none; +} + +defaultproperties +{ + CODEPOINT_ESCAPE = 27 // ANSI escape code + CODEPOINT_OPEN_FORMAT = 123 // '{' + CODEPOINT_CLOSE_FORMAT = 125 // '}' + CODEPOINT_FORMAT_ESCAPE = 38 // '&' +} \ No newline at end of file diff --git a/sources/Core/Text/UnicodeData.uc b/sources/Core/Text/UnicodeData.uc new file mode 100644 index 0000000..f405d5b --- /dev/null +++ b/sources/Core/Text/UnicodeData.uc @@ -0,0 +1,4320 @@ +/** + * 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 . + */ + +// !!! THIS FILE IS AUTOGENERATED AND SHOULD NOT BE MODIFIED !!! + +class UnicodeData extends AcediaObject + abstract; + +struct CodePointMapping +{ + var public int from; + var public int to; +}; + +var public const array to_lower; +var public const array to_upper; +var public const array to_title; + +public static function int ToLowerCodePoint(int codePoint) +{ + local int midPoint; + local int lowerIndex, higherIndex; + lowerIndex = 0; + higherIndex = default.to_lower.length - 1; + while (lowerIndex < higherIndex) + { + // Border case: lower and higher indices are right next to each other + if (lowerIndex + 1 >= higherIndex) + { + if (default.to_lower[lowerIndex].from == codePoint) { + return default.to_lower[lowerIndex].to; + } + if (default.to_lower[higherIndex].from == codePoint) { + return default.to_lower[higherIndex].to; + } + return -1; + } + // Choose half + midPoint = (lowerIndex + higherIndex) / 2; + if (default.to_lower[midPoint].from == codePoint) { + return default.to_lower[midPoint].to; + } + if (default.to_lower[midPoint].from < codePoint) { + lowerIndex = midPoint; + } + else {//if (default.to_lower[midPoint].from > codePoint) + higherIndex = midPoint; + } + } + return -1; +} + +public static function int ToUpperCodePoint(int codePoint) +{ + local int midPoint; + local int lowerIndex, higherIndex; + lowerIndex = 0; + higherIndex = default.to_upper.length - 1; + while (lowerIndex < higherIndex) + { + // Border case: lower and higher indices are right next to each other + if (lowerIndex + 1 >= higherIndex) + { + if (default.to_upper[lowerIndex].from == codePoint) { + return default.to_upper[lowerIndex].to; + } + if (default.to_upper[higherIndex].from == codePoint) { + return default.to_upper[higherIndex].to; + } + return -1; + } + // Choose half + midPoint = (lowerIndex + higherIndex) / 2; + if (default.to_upper[midPoint].from == codePoint) { + return default.to_upper[midPoint].to; + } + if (default.to_upper[midPoint].from < codePoint) { + lowerIndex = midPoint; + } + else {//if (default.to_upper[midPoint].from > codePoint) + higherIndex = midPoint; + } + } + return -1; +} + +defaultproperties +{ + to_lower(0)=(from=65,to=97) + to_lower(1)=(from=66,to=98) + to_lower(2)=(from=67,to=99) + to_lower(3)=(from=68,to=100) + to_lower(4)=(from=69,to=101) + to_lower(5)=(from=70,to=102) + to_lower(6)=(from=71,to=103) + to_lower(7)=(from=72,to=104) + to_lower(8)=(from=73,to=105) + to_lower(9)=(from=74,to=106) + to_lower(10)=(from=75,to=107) + to_lower(11)=(from=76,to=108) + to_lower(12)=(from=77,to=109) + to_lower(13)=(from=78,to=110) + to_lower(14)=(from=79,to=111) + to_lower(15)=(from=80,to=112) + to_lower(16)=(from=81,to=113) + to_lower(17)=(from=82,to=114) + to_lower(18)=(from=83,to=115) + to_lower(19)=(from=84,to=116) + to_lower(20)=(from=85,to=117) + to_lower(21)=(from=86,to=118) + to_lower(22)=(from=87,to=119) + to_lower(23)=(from=88,to=120) + to_lower(24)=(from=89,to=121) + to_lower(25)=(from=90,to=122) + to_lower(26)=(from=192,to=224) + to_lower(27)=(from=193,to=225) + to_lower(28)=(from=194,to=226) + to_lower(29)=(from=195,to=227) + to_lower(30)=(from=196,to=228) + to_lower(31)=(from=197,to=229) + to_lower(32)=(from=198,to=230) + to_lower(33)=(from=199,to=231) + to_lower(34)=(from=200,to=232) + to_lower(35)=(from=201,to=233) + to_lower(36)=(from=202,to=234) + to_lower(37)=(from=203,to=235) + to_lower(38)=(from=204,to=236) + to_lower(39)=(from=205,to=237) + to_lower(40)=(from=206,to=238) + to_lower(41)=(from=207,to=239) + to_lower(42)=(from=208,to=240) + to_lower(43)=(from=209,to=241) + to_lower(44)=(from=210,to=242) + to_lower(45)=(from=211,to=243) + to_lower(46)=(from=212,to=244) + to_lower(47)=(from=213,to=245) + to_lower(48)=(from=214,to=246) + to_lower(49)=(from=216,to=248) + to_lower(50)=(from=217,to=249) + to_lower(51)=(from=218,to=250) + to_lower(52)=(from=219,to=251) + to_lower(53)=(from=220,to=252) + to_lower(54)=(from=221,to=253) + to_lower(55)=(from=222,to=254) + to_lower(56)=(from=256,to=257) + to_lower(57)=(from=258,to=259) + to_lower(58)=(from=260,to=261) + to_lower(59)=(from=262,to=263) + to_lower(60)=(from=264,to=265) + to_lower(61)=(from=266,to=267) + to_lower(62)=(from=268,to=269) + to_lower(63)=(from=270,to=271) + to_lower(64)=(from=272,to=273) + to_lower(65)=(from=274,to=275) + to_lower(66)=(from=276,to=277) + to_lower(67)=(from=278,to=279) + to_lower(68)=(from=280,to=281) + to_lower(69)=(from=282,to=283) + to_lower(70)=(from=284,to=285) + to_lower(71)=(from=286,to=287) + to_lower(72)=(from=288,to=289) + to_lower(73)=(from=290,to=291) + to_lower(74)=(from=292,to=293) + to_lower(75)=(from=294,to=295) + to_lower(76)=(from=296,to=297) + to_lower(77)=(from=298,to=299) + to_lower(78)=(from=300,to=301) + to_lower(79)=(from=302,to=303) + to_lower(80)=(from=304,to=105) + to_lower(81)=(from=306,to=307) + to_lower(82)=(from=308,to=309) + to_lower(83)=(from=310,to=311) + to_lower(84)=(from=313,to=314) + to_lower(85)=(from=315,to=316) + to_lower(86)=(from=317,to=318) + to_lower(87)=(from=319,to=320) + to_lower(88)=(from=321,to=322) + to_lower(89)=(from=323,to=324) + to_lower(90)=(from=325,to=326) + to_lower(91)=(from=327,to=328) + to_lower(92)=(from=330,to=331) + to_lower(93)=(from=332,to=333) + to_lower(94)=(from=334,to=335) + to_lower(95)=(from=336,to=337) + to_lower(96)=(from=338,to=339) + to_lower(97)=(from=340,to=341) + to_lower(98)=(from=342,to=343) + to_lower(99)=(from=344,to=345) + to_lower(100)=(from=346,to=347) + to_lower(101)=(from=348,to=349) + to_lower(102)=(from=350,to=351) + to_lower(103)=(from=352,to=353) + to_lower(104)=(from=354,to=355) + to_lower(105)=(from=356,to=357) + to_lower(106)=(from=358,to=359) + to_lower(107)=(from=360,to=361) + to_lower(108)=(from=362,to=363) + to_lower(109)=(from=364,to=365) + to_lower(110)=(from=366,to=367) + to_lower(111)=(from=368,to=369) + to_lower(112)=(from=370,to=371) + to_lower(113)=(from=372,to=373) + to_lower(114)=(from=374,to=375) + to_lower(115)=(from=376,to=255) + to_lower(116)=(from=377,to=378) + to_lower(117)=(from=379,to=380) + to_lower(118)=(from=381,to=382) + to_lower(119)=(from=385,to=595) + to_lower(120)=(from=386,to=387) + to_lower(121)=(from=388,to=389) + to_lower(122)=(from=390,to=596) + to_lower(123)=(from=391,to=392) + to_lower(124)=(from=393,to=598) + to_lower(125)=(from=394,to=599) + to_lower(126)=(from=395,to=396) + to_lower(127)=(from=398,to=477) + to_lower(128)=(from=399,to=601) + to_lower(129)=(from=400,to=603) + to_lower(130)=(from=401,to=402) + to_lower(131)=(from=403,to=608) + to_lower(132)=(from=404,to=611) + to_lower(133)=(from=406,to=617) + to_lower(134)=(from=407,to=616) + to_lower(135)=(from=408,to=409) + to_lower(136)=(from=412,to=623) + to_lower(137)=(from=413,to=626) + to_lower(138)=(from=415,to=629) + to_lower(139)=(from=416,to=417) + to_lower(140)=(from=418,to=419) + to_lower(141)=(from=420,to=421) + to_lower(142)=(from=422,to=640) + to_lower(143)=(from=423,to=424) + to_lower(144)=(from=425,to=643) + to_lower(145)=(from=428,to=429) + to_lower(146)=(from=430,to=648) + to_lower(147)=(from=431,to=432) + to_lower(148)=(from=433,to=650) + to_lower(149)=(from=434,to=651) + to_lower(150)=(from=435,to=436) + to_lower(151)=(from=437,to=438) + to_lower(152)=(from=439,to=658) + to_lower(153)=(from=440,to=441) + to_lower(154)=(from=444,to=445) + to_lower(155)=(from=452,to=454) + to_lower(156)=(from=453,to=454) + to_lower(157)=(from=455,to=457) + to_lower(158)=(from=456,to=457) + to_lower(159)=(from=458,to=460) + to_lower(160)=(from=459,to=460) + to_lower(161)=(from=461,to=462) + to_lower(162)=(from=463,to=464) + to_lower(163)=(from=465,to=466) + to_lower(164)=(from=467,to=468) + to_lower(165)=(from=469,to=470) + to_lower(166)=(from=471,to=472) + to_lower(167)=(from=473,to=474) + to_lower(168)=(from=475,to=476) + to_lower(169)=(from=478,to=479) + to_lower(170)=(from=480,to=481) + to_lower(171)=(from=482,to=483) + to_lower(172)=(from=484,to=485) + to_lower(173)=(from=486,to=487) + to_lower(174)=(from=488,to=489) + to_lower(175)=(from=490,to=491) + to_lower(176)=(from=492,to=493) + to_lower(177)=(from=494,to=495) + to_lower(178)=(from=497,to=499) + to_lower(179)=(from=498,to=499) + to_lower(180)=(from=500,to=501) + to_lower(181)=(from=502,to=405) + to_lower(182)=(from=503,to=447) + to_lower(183)=(from=504,to=505) + to_lower(184)=(from=506,to=507) + to_lower(185)=(from=508,to=509) + to_lower(186)=(from=510,to=511) + to_lower(187)=(from=512,to=513) + to_lower(188)=(from=514,to=515) + to_lower(189)=(from=516,to=517) + to_lower(190)=(from=518,to=519) + to_lower(191)=(from=520,to=521) + to_lower(192)=(from=522,to=523) + to_lower(193)=(from=524,to=525) + to_lower(194)=(from=526,to=527) + to_lower(195)=(from=528,to=529) + to_lower(196)=(from=530,to=531) + to_lower(197)=(from=532,to=533) + to_lower(198)=(from=534,to=535) + to_lower(199)=(from=536,to=537) + to_lower(200)=(from=538,to=539) + to_lower(201)=(from=540,to=541) + to_lower(202)=(from=542,to=543) + to_lower(203)=(from=544,to=414) + to_lower(204)=(from=546,to=547) + to_lower(205)=(from=548,to=549) + to_lower(206)=(from=550,to=551) + to_lower(207)=(from=552,to=553) + to_lower(208)=(from=554,to=555) + to_lower(209)=(from=556,to=557) + to_lower(210)=(from=558,to=559) + to_lower(211)=(from=560,to=561) + to_lower(212)=(from=562,to=563) + to_lower(213)=(from=570,to=11365) + to_lower(214)=(from=571,to=572) + to_lower(215)=(from=573,to=410) + to_lower(216)=(from=574,to=11366) + to_lower(217)=(from=577,to=578) + to_lower(218)=(from=579,to=384) + to_lower(219)=(from=580,to=649) + to_lower(220)=(from=581,to=652) + to_lower(221)=(from=582,to=583) + to_lower(222)=(from=584,to=585) + to_lower(223)=(from=586,to=587) + to_lower(224)=(from=588,to=589) + to_lower(225)=(from=590,to=591) + to_lower(226)=(from=880,to=881) + to_lower(227)=(from=882,to=883) + to_lower(228)=(from=886,to=887) + to_lower(229)=(from=895,to=1011) + to_lower(230)=(from=902,to=940) + to_lower(231)=(from=904,to=941) + to_lower(232)=(from=905,to=942) + to_lower(233)=(from=906,to=943) + to_lower(234)=(from=908,to=972) + to_lower(235)=(from=910,to=973) + to_lower(236)=(from=911,to=974) + to_lower(237)=(from=913,to=945) + to_lower(238)=(from=914,to=946) + to_lower(239)=(from=915,to=947) + to_lower(240)=(from=916,to=948) + to_lower(241)=(from=917,to=949) + to_lower(242)=(from=918,to=950) + to_lower(243)=(from=919,to=951) + to_lower(244)=(from=920,to=952) + to_lower(245)=(from=921,to=953) + to_lower(246)=(from=922,to=954) + to_lower(247)=(from=923,to=955) + to_lower(248)=(from=924,to=956) + to_lower(249)=(from=925,to=957) + to_lower(250)=(from=926,to=958) + to_lower(251)=(from=927,to=959) + to_lower(252)=(from=928,to=960) + to_lower(253)=(from=929,to=961) + to_lower(254)=(from=931,to=963) + to_lower(255)=(from=932,to=964) + to_lower(256)=(from=933,to=965) + to_lower(257)=(from=934,to=966) + to_lower(258)=(from=935,to=967) + to_lower(259)=(from=936,to=968) + to_lower(260)=(from=937,to=969) + to_lower(261)=(from=938,to=970) + to_lower(262)=(from=939,to=971) + to_lower(263)=(from=975,to=983) + to_lower(264)=(from=984,to=985) + to_lower(265)=(from=986,to=987) + to_lower(266)=(from=988,to=989) + to_lower(267)=(from=990,to=991) + to_lower(268)=(from=992,to=993) + to_lower(269)=(from=994,to=995) + to_lower(270)=(from=996,to=997) + to_lower(271)=(from=998,to=999) + to_lower(272)=(from=1000,to=1001) + to_lower(273)=(from=1002,to=1003) + to_lower(274)=(from=1004,to=1005) + to_lower(275)=(from=1006,to=1007) + to_lower(276)=(from=1012,to=952) + to_lower(277)=(from=1015,to=1016) + to_lower(278)=(from=1017,to=1010) + to_lower(279)=(from=1018,to=1019) + to_lower(280)=(from=1021,to=891) + to_lower(281)=(from=1022,to=892) + to_lower(282)=(from=1023,to=893) + to_lower(283)=(from=1024,to=1104) + to_lower(284)=(from=1025,to=1105) + to_lower(285)=(from=1026,to=1106) + to_lower(286)=(from=1027,to=1107) + to_lower(287)=(from=1028,to=1108) + to_lower(288)=(from=1029,to=1109) + to_lower(289)=(from=1030,to=1110) + to_lower(290)=(from=1031,to=1111) + to_lower(291)=(from=1032,to=1112) + to_lower(292)=(from=1033,to=1113) + to_lower(293)=(from=1034,to=1114) + to_lower(294)=(from=1035,to=1115) + to_lower(295)=(from=1036,to=1116) + to_lower(296)=(from=1037,to=1117) + to_lower(297)=(from=1038,to=1118) + to_lower(298)=(from=1039,to=1119) + to_lower(299)=(from=1040,to=1072) + to_lower(300)=(from=1041,to=1073) + to_lower(301)=(from=1042,to=1074) + to_lower(302)=(from=1043,to=1075) + to_lower(303)=(from=1044,to=1076) + to_lower(304)=(from=1045,to=1077) + to_lower(305)=(from=1046,to=1078) + to_lower(306)=(from=1047,to=1079) + to_lower(307)=(from=1048,to=1080) + to_lower(308)=(from=1049,to=1081) + to_lower(309)=(from=1050,to=1082) + to_lower(310)=(from=1051,to=1083) + to_lower(311)=(from=1052,to=1084) + to_lower(312)=(from=1053,to=1085) + to_lower(313)=(from=1054,to=1086) + to_lower(314)=(from=1055,to=1087) + to_lower(315)=(from=1056,to=1088) + to_lower(316)=(from=1057,to=1089) + to_lower(317)=(from=1058,to=1090) + to_lower(318)=(from=1059,to=1091) + to_lower(319)=(from=1060,to=1092) + to_lower(320)=(from=1061,to=1093) + to_lower(321)=(from=1062,to=1094) + to_lower(322)=(from=1063,to=1095) + to_lower(323)=(from=1064,to=1096) + to_lower(324)=(from=1065,to=1097) + to_lower(325)=(from=1066,to=1098) + to_lower(326)=(from=1067,to=1099) + to_lower(327)=(from=1068,to=1100) + to_lower(328)=(from=1069,to=1101) + to_lower(329)=(from=1070,to=1102) + to_lower(330)=(from=1071,to=1103) + to_lower(331)=(from=1120,to=1121) + to_lower(332)=(from=1122,to=1123) + to_lower(333)=(from=1124,to=1125) + to_lower(334)=(from=1126,to=1127) + to_lower(335)=(from=1128,to=1129) + to_lower(336)=(from=1130,to=1131) + to_lower(337)=(from=1132,to=1133) + to_lower(338)=(from=1134,to=1135) + to_lower(339)=(from=1136,to=1137) + to_lower(340)=(from=1138,to=1139) + to_lower(341)=(from=1140,to=1141) + to_lower(342)=(from=1142,to=1143) + to_lower(343)=(from=1144,to=1145) + to_lower(344)=(from=1146,to=1147) + to_lower(345)=(from=1148,to=1149) + to_lower(346)=(from=1150,to=1151) + to_lower(347)=(from=1152,to=1153) + to_lower(348)=(from=1162,to=1163) + to_lower(349)=(from=1164,to=1165) + to_lower(350)=(from=1166,to=1167) + to_lower(351)=(from=1168,to=1169) + to_lower(352)=(from=1170,to=1171) + to_lower(353)=(from=1172,to=1173) + to_lower(354)=(from=1174,to=1175) + to_lower(355)=(from=1176,to=1177) + to_lower(356)=(from=1178,to=1179) + to_lower(357)=(from=1180,to=1181) + to_lower(358)=(from=1182,to=1183) + to_lower(359)=(from=1184,to=1185) + to_lower(360)=(from=1186,to=1187) + to_lower(361)=(from=1188,to=1189) + to_lower(362)=(from=1190,to=1191) + to_lower(363)=(from=1192,to=1193) + to_lower(364)=(from=1194,to=1195) + to_lower(365)=(from=1196,to=1197) + to_lower(366)=(from=1198,to=1199) + to_lower(367)=(from=1200,to=1201) + to_lower(368)=(from=1202,to=1203) + to_lower(369)=(from=1204,to=1205) + to_lower(370)=(from=1206,to=1207) + to_lower(371)=(from=1208,to=1209) + to_lower(372)=(from=1210,to=1211) + to_lower(373)=(from=1212,to=1213) + to_lower(374)=(from=1214,to=1215) + to_lower(375)=(from=1216,to=1231) + to_lower(376)=(from=1217,to=1218) + to_lower(377)=(from=1219,to=1220) + to_lower(378)=(from=1221,to=1222) + to_lower(379)=(from=1223,to=1224) + to_lower(380)=(from=1225,to=1226) + to_lower(381)=(from=1227,to=1228) + to_lower(382)=(from=1229,to=1230) + to_lower(383)=(from=1232,to=1233) + to_lower(384)=(from=1234,to=1235) + to_lower(385)=(from=1236,to=1237) + to_lower(386)=(from=1238,to=1239) + to_lower(387)=(from=1240,to=1241) + to_lower(388)=(from=1242,to=1243) + to_lower(389)=(from=1244,to=1245) + to_lower(390)=(from=1246,to=1247) + to_lower(391)=(from=1248,to=1249) + to_lower(392)=(from=1250,to=1251) + to_lower(393)=(from=1252,to=1253) + to_lower(394)=(from=1254,to=1255) + to_lower(395)=(from=1256,to=1257) + to_lower(396)=(from=1258,to=1259) + to_lower(397)=(from=1260,to=1261) + to_lower(398)=(from=1262,to=1263) + to_lower(399)=(from=1264,to=1265) + to_lower(400)=(from=1266,to=1267) + to_lower(401)=(from=1268,to=1269) + to_lower(402)=(from=1270,to=1271) + to_lower(403)=(from=1272,to=1273) + to_lower(404)=(from=1274,to=1275) + to_lower(405)=(from=1276,to=1277) + to_lower(406)=(from=1278,to=1279) + to_lower(407)=(from=1280,to=1281) + to_lower(408)=(from=1282,to=1283) + to_lower(409)=(from=1284,to=1285) + to_lower(410)=(from=1286,to=1287) + to_lower(411)=(from=1288,to=1289) + to_lower(412)=(from=1290,to=1291) + to_lower(413)=(from=1292,to=1293) + to_lower(414)=(from=1294,to=1295) + to_lower(415)=(from=1296,to=1297) + to_lower(416)=(from=1298,to=1299) + to_lower(417)=(from=1300,to=1301) + to_lower(418)=(from=1302,to=1303) + to_lower(419)=(from=1304,to=1305) + to_lower(420)=(from=1306,to=1307) + to_lower(421)=(from=1308,to=1309) + to_lower(422)=(from=1310,to=1311) + to_lower(423)=(from=1312,to=1313) + to_lower(424)=(from=1314,to=1315) + to_lower(425)=(from=1316,to=1317) + to_lower(426)=(from=1318,to=1319) + to_lower(427)=(from=1320,to=1321) + to_lower(428)=(from=1322,to=1323) + to_lower(429)=(from=1324,to=1325) + to_lower(430)=(from=1326,to=1327) + to_lower(431)=(from=1329,to=1377) + to_lower(432)=(from=1330,to=1378) + to_lower(433)=(from=1331,to=1379) + to_lower(434)=(from=1332,to=1380) + to_lower(435)=(from=1333,to=1381) + to_lower(436)=(from=1334,to=1382) + to_lower(437)=(from=1335,to=1383) + to_lower(438)=(from=1336,to=1384) + to_lower(439)=(from=1337,to=1385) + to_lower(440)=(from=1338,to=1386) + to_lower(441)=(from=1339,to=1387) + to_lower(442)=(from=1340,to=1388) + to_lower(443)=(from=1341,to=1389) + to_lower(444)=(from=1342,to=1390) + to_lower(445)=(from=1343,to=1391) + to_lower(446)=(from=1344,to=1392) + to_lower(447)=(from=1345,to=1393) + to_lower(448)=(from=1346,to=1394) + to_lower(449)=(from=1347,to=1395) + to_lower(450)=(from=1348,to=1396) + to_lower(451)=(from=1349,to=1397) + to_lower(452)=(from=1350,to=1398) + to_lower(453)=(from=1351,to=1399) + to_lower(454)=(from=1352,to=1400) + to_lower(455)=(from=1353,to=1401) + to_lower(456)=(from=1354,to=1402) + to_lower(457)=(from=1355,to=1403) + to_lower(458)=(from=1356,to=1404) + to_lower(459)=(from=1357,to=1405) + to_lower(460)=(from=1358,to=1406) + to_lower(461)=(from=1359,to=1407) + to_lower(462)=(from=1360,to=1408) + to_lower(463)=(from=1361,to=1409) + to_lower(464)=(from=1362,to=1410) + to_lower(465)=(from=1363,to=1411) + to_lower(466)=(from=1364,to=1412) + to_lower(467)=(from=1365,to=1413) + to_lower(468)=(from=1366,to=1414) + to_lower(469)=(from=4256,to=11520) + to_lower(470)=(from=4257,to=11521) + to_lower(471)=(from=4258,to=11522) + to_lower(472)=(from=4259,to=11523) + to_lower(473)=(from=4260,to=11524) + to_lower(474)=(from=4261,to=11525) + to_lower(475)=(from=4262,to=11526) + to_lower(476)=(from=4263,to=11527) + to_lower(477)=(from=4264,to=11528) + to_lower(478)=(from=4265,to=11529) + to_lower(479)=(from=4266,to=11530) + to_lower(480)=(from=4267,to=11531) + to_lower(481)=(from=4268,to=11532) + to_lower(482)=(from=4269,to=11533) + to_lower(483)=(from=4270,to=11534) + to_lower(484)=(from=4271,to=11535) + to_lower(485)=(from=4272,to=11536) + to_lower(486)=(from=4273,to=11537) + to_lower(487)=(from=4274,to=11538) + to_lower(488)=(from=4275,to=11539) + to_lower(489)=(from=4276,to=11540) + to_lower(490)=(from=4277,to=11541) + to_lower(491)=(from=4278,to=11542) + to_lower(492)=(from=4279,to=11543) + to_lower(493)=(from=4280,to=11544) + to_lower(494)=(from=4281,to=11545) + to_lower(495)=(from=4282,to=11546) + to_lower(496)=(from=4283,to=11547) + to_lower(497)=(from=4284,to=11548) + to_lower(498)=(from=4285,to=11549) + to_lower(499)=(from=4286,to=11550) + to_lower(500)=(from=4287,to=11551) + to_lower(501)=(from=4288,to=11552) + to_lower(502)=(from=4289,to=11553) + to_lower(503)=(from=4290,to=11554) + to_lower(504)=(from=4291,to=11555) + to_lower(505)=(from=4292,to=11556) + to_lower(506)=(from=4293,to=11557) + to_lower(507)=(from=4295,to=11559) + to_lower(508)=(from=4301,to=11565) + to_lower(509)=(from=5024,to=43888) + to_lower(510)=(from=5025,to=43889) + to_lower(511)=(from=5026,to=43890) + to_lower(512)=(from=5027,to=43891) + to_lower(513)=(from=5028,to=43892) + to_lower(514)=(from=5029,to=43893) + to_lower(515)=(from=5030,to=43894) + to_lower(516)=(from=5031,to=43895) + to_lower(517)=(from=5032,to=43896) + to_lower(518)=(from=5033,to=43897) + to_lower(519)=(from=5034,to=43898) + to_lower(520)=(from=5035,to=43899) + to_lower(521)=(from=5036,to=43900) + to_lower(522)=(from=5037,to=43901) + to_lower(523)=(from=5038,to=43902) + to_lower(524)=(from=5039,to=43903) + to_lower(525)=(from=5040,to=43904) + to_lower(526)=(from=5041,to=43905) + to_lower(527)=(from=5042,to=43906) + to_lower(528)=(from=5043,to=43907) + to_lower(529)=(from=5044,to=43908) + to_lower(530)=(from=5045,to=43909) + to_lower(531)=(from=5046,to=43910) + to_lower(532)=(from=5047,to=43911) + to_lower(533)=(from=5048,to=43912) + to_lower(534)=(from=5049,to=43913) + to_lower(535)=(from=5050,to=43914) + to_lower(536)=(from=5051,to=43915) + to_lower(537)=(from=5052,to=43916) + to_lower(538)=(from=5053,to=43917) + to_lower(539)=(from=5054,to=43918) + to_lower(540)=(from=5055,to=43919) + to_lower(541)=(from=5056,to=43920) + to_lower(542)=(from=5057,to=43921) + to_lower(543)=(from=5058,to=43922) + to_lower(544)=(from=5059,to=43923) + to_lower(545)=(from=5060,to=43924) + to_lower(546)=(from=5061,to=43925) + to_lower(547)=(from=5062,to=43926) + to_lower(548)=(from=5063,to=43927) + to_lower(549)=(from=5064,to=43928) + to_lower(550)=(from=5065,to=43929) + to_lower(551)=(from=5066,to=43930) + to_lower(552)=(from=5067,to=43931) + to_lower(553)=(from=5068,to=43932) + to_lower(554)=(from=5069,to=43933) + to_lower(555)=(from=5070,to=43934) + to_lower(556)=(from=5071,to=43935) + to_lower(557)=(from=5072,to=43936) + to_lower(558)=(from=5073,to=43937) + to_lower(559)=(from=5074,to=43938) + to_lower(560)=(from=5075,to=43939) + to_lower(561)=(from=5076,to=43940) + to_lower(562)=(from=5077,to=43941) + to_lower(563)=(from=5078,to=43942) + to_lower(564)=(from=5079,to=43943) + to_lower(565)=(from=5080,to=43944) + to_lower(566)=(from=5081,to=43945) + to_lower(567)=(from=5082,to=43946) + to_lower(568)=(from=5083,to=43947) + to_lower(569)=(from=5084,to=43948) + to_lower(570)=(from=5085,to=43949) + to_lower(571)=(from=5086,to=43950) + to_lower(572)=(from=5087,to=43951) + to_lower(573)=(from=5088,to=43952) + to_lower(574)=(from=5089,to=43953) + to_lower(575)=(from=5090,to=43954) + to_lower(576)=(from=5091,to=43955) + to_lower(577)=(from=5092,to=43956) + to_lower(578)=(from=5093,to=43957) + to_lower(579)=(from=5094,to=43958) + to_lower(580)=(from=5095,to=43959) + to_lower(581)=(from=5096,to=43960) + to_lower(582)=(from=5097,to=43961) + to_lower(583)=(from=5098,to=43962) + to_lower(584)=(from=5099,to=43963) + to_lower(585)=(from=5100,to=43964) + to_lower(586)=(from=5101,to=43965) + to_lower(587)=(from=5102,to=43966) + to_lower(588)=(from=5103,to=43967) + to_lower(589)=(from=5104,to=5112) + to_lower(590)=(from=5105,to=5113) + to_lower(591)=(from=5106,to=5114) + to_lower(592)=(from=5107,to=5115) + to_lower(593)=(from=5108,to=5116) + to_lower(594)=(from=5109,to=5117) + to_lower(595)=(from=7312,to=4304) + to_lower(596)=(from=7313,to=4305) + to_lower(597)=(from=7314,to=4306) + to_lower(598)=(from=7315,to=4307) + to_lower(599)=(from=7316,to=4308) + to_lower(600)=(from=7317,to=4309) + to_lower(601)=(from=7318,to=4310) + to_lower(602)=(from=7319,to=4311) + to_lower(603)=(from=7320,to=4312) + to_lower(604)=(from=7321,to=4313) + to_lower(605)=(from=7322,to=4314) + to_lower(606)=(from=7323,to=4315) + to_lower(607)=(from=7324,to=4316) + to_lower(608)=(from=7325,to=4317) + to_lower(609)=(from=7326,to=4318) + to_lower(610)=(from=7327,to=4319) + to_lower(611)=(from=7328,to=4320) + to_lower(612)=(from=7329,to=4321) + to_lower(613)=(from=7330,to=4322) + to_lower(614)=(from=7331,to=4323) + to_lower(615)=(from=7332,to=4324) + to_lower(616)=(from=7333,to=4325) + to_lower(617)=(from=7334,to=4326) + to_lower(618)=(from=7335,to=4327) + to_lower(619)=(from=7336,to=4328) + to_lower(620)=(from=7337,to=4329) + to_lower(621)=(from=7338,to=4330) + to_lower(622)=(from=7339,to=4331) + to_lower(623)=(from=7340,to=4332) + to_lower(624)=(from=7341,to=4333) + to_lower(625)=(from=7342,to=4334) + to_lower(626)=(from=7343,to=4335) + to_lower(627)=(from=7344,to=4336) + to_lower(628)=(from=7345,to=4337) + to_lower(629)=(from=7346,to=4338) + to_lower(630)=(from=7347,to=4339) + to_lower(631)=(from=7348,to=4340) + to_lower(632)=(from=7349,to=4341) + to_lower(633)=(from=7350,to=4342) + to_lower(634)=(from=7351,to=4343) + to_lower(635)=(from=7352,to=4344) + to_lower(636)=(from=7353,to=4345) + to_lower(637)=(from=7354,to=4346) + to_lower(638)=(from=7357,to=4349) + to_lower(639)=(from=7358,to=4350) + to_lower(640)=(from=7359,to=4351) + to_lower(641)=(from=7680,to=7681) + to_lower(642)=(from=7682,to=7683) + to_lower(643)=(from=7684,to=7685) + to_lower(644)=(from=7686,to=7687) + to_lower(645)=(from=7688,to=7689) + to_lower(646)=(from=7690,to=7691) + to_lower(647)=(from=7692,to=7693) + to_lower(648)=(from=7694,to=7695) + to_lower(649)=(from=7696,to=7697) + to_lower(650)=(from=7698,to=7699) + to_lower(651)=(from=7700,to=7701) + to_lower(652)=(from=7702,to=7703) + to_lower(653)=(from=7704,to=7705) + to_lower(654)=(from=7706,to=7707) + to_lower(655)=(from=7708,to=7709) + to_lower(656)=(from=7710,to=7711) + to_lower(657)=(from=7712,to=7713) + to_lower(658)=(from=7714,to=7715) + to_lower(659)=(from=7716,to=7717) + to_lower(660)=(from=7718,to=7719) + to_lower(661)=(from=7720,to=7721) + to_lower(662)=(from=7722,to=7723) + to_lower(663)=(from=7724,to=7725) + to_lower(664)=(from=7726,to=7727) + to_lower(665)=(from=7728,to=7729) + to_lower(666)=(from=7730,to=7731) + to_lower(667)=(from=7732,to=7733) + to_lower(668)=(from=7734,to=7735) + to_lower(669)=(from=7736,to=7737) + to_lower(670)=(from=7738,to=7739) + to_lower(671)=(from=7740,to=7741) + to_lower(672)=(from=7742,to=7743) + to_lower(673)=(from=7744,to=7745) + to_lower(674)=(from=7746,to=7747) + to_lower(675)=(from=7748,to=7749) + to_lower(676)=(from=7750,to=7751) + to_lower(677)=(from=7752,to=7753) + to_lower(678)=(from=7754,to=7755) + to_lower(679)=(from=7756,to=7757) + to_lower(680)=(from=7758,to=7759) + to_lower(681)=(from=7760,to=7761) + to_lower(682)=(from=7762,to=7763) + to_lower(683)=(from=7764,to=7765) + to_lower(684)=(from=7766,to=7767) + to_lower(685)=(from=7768,to=7769) + to_lower(686)=(from=7770,to=7771) + to_lower(687)=(from=7772,to=7773) + to_lower(688)=(from=7774,to=7775) + to_lower(689)=(from=7776,to=7777) + to_lower(690)=(from=7778,to=7779) + to_lower(691)=(from=7780,to=7781) + to_lower(692)=(from=7782,to=7783) + to_lower(693)=(from=7784,to=7785) + to_lower(694)=(from=7786,to=7787) + to_lower(695)=(from=7788,to=7789) + to_lower(696)=(from=7790,to=7791) + to_lower(697)=(from=7792,to=7793) + to_lower(698)=(from=7794,to=7795) + to_lower(699)=(from=7796,to=7797) + to_lower(700)=(from=7798,to=7799) + to_lower(701)=(from=7800,to=7801) + to_lower(702)=(from=7802,to=7803) + to_lower(703)=(from=7804,to=7805) + to_lower(704)=(from=7806,to=7807) + to_lower(705)=(from=7808,to=7809) + to_lower(706)=(from=7810,to=7811) + to_lower(707)=(from=7812,to=7813) + to_lower(708)=(from=7814,to=7815) + to_lower(709)=(from=7816,to=7817) + to_lower(710)=(from=7818,to=7819) + to_lower(711)=(from=7820,to=7821) + to_lower(712)=(from=7822,to=7823) + to_lower(713)=(from=7824,to=7825) + to_lower(714)=(from=7826,to=7827) + to_lower(715)=(from=7828,to=7829) + to_lower(716)=(from=7838,to=223) + to_lower(717)=(from=7840,to=7841) + to_lower(718)=(from=7842,to=7843) + to_lower(719)=(from=7844,to=7845) + to_lower(720)=(from=7846,to=7847) + to_lower(721)=(from=7848,to=7849) + to_lower(722)=(from=7850,to=7851) + to_lower(723)=(from=7852,to=7853) + to_lower(724)=(from=7854,to=7855) + to_lower(725)=(from=7856,to=7857) + to_lower(726)=(from=7858,to=7859) + to_lower(727)=(from=7860,to=7861) + to_lower(728)=(from=7862,to=7863) + to_lower(729)=(from=7864,to=7865) + to_lower(730)=(from=7866,to=7867) + to_lower(731)=(from=7868,to=7869) + to_lower(732)=(from=7870,to=7871) + to_lower(733)=(from=7872,to=7873) + to_lower(734)=(from=7874,to=7875) + to_lower(735)=(from=7876,to=7877) + to_lower(736)=(from=7878,to=7879) + to_lower(737)=(from=7880,to=7881) + to_lower(738)=(from=7882,to=7883) + to_lower(739)=(from=7884,to=7885) + to_lower(740)=(from=7886,to=7887) + to_lower(741)=(from=7888,to=7889) + to_lower(742)=(from=7890,to=7891) + to_lower(743)=(from=7892,to=7893) + to_lower(744)=(from=7894,to=7895) + to_lower(745)=(from=7896,to=7897) + to_lower(746)=(from=7898,to=7899) + to_lower(747)=(from=7900,to=7901) + to_lower(748)=(from=7902,to=7903) + to_lower(749)=(from=7904,to=7905) + to_lower(750)=(from=7906,to=7907) + to_lower(751)=(from=7908,to=7909) + to_lower(752)=(from=7910,to=7911) + to_lower(753)=(from=7912,to=7913) + to_lower(754)=(from=7914,to=7915) + to_lower(755)=(from=7916,to=7917) + to_lower(756)=(from=7918,to=7919) + to_lower(757)=(from=7920,to=7921) + to_lower(758)=(from=7922,to=7923) + to_lower(759)=(from=7924,to=7925) + to_lower(760)=(from=7926,to=7927) + to_lower(761)=(from=7928,to=7929) + to_lower(762)=(from=7930,to=7931) + to_lower(763)=(from=7932,to=7933) + to_lower(764)=(from=7934,to=7935) + to_lower(765)=(from=7944,to=7936) + to_lower(766)=(from=7945,to=7937) + to_lower(767)=(from=7946,to=7938) + to_lower(768)=(from=7947,to=7939) + to_lower(769)=(from=7948,to=7940) + to_lower(770)=(from=7949,to=7941) + to_lower(771)=(from=7950,to=7942) + to_lower(772)=(from=7951,to=7943) + to_lower(773)=(from=7960,to=7952) + to_lower(774)=(from=7961,to=7953) + to_lower(775)=(from=7962,to=7954) + to_lower(776)=(from=7963,to=7955) + to_lower(777)=(from=7964,to=7956) + to_lower(778)=(from=7965,to=7957) + to_lower(779)=(from=7976,to=7968) + to_lower(780)=(from=7977,to=7969) + to_lower(781)=(from=7978,to=7970) + to_lower(782)=(from=7979,to=7971) + to_lower(783)=(from=7980,to=7972) + to_lower(784)=(from=7981,to=7973) + to_lower(785)=(from=7982,to=7974) + to_lower(786)=(from=7983,to=7975) + to_lower(787)=(from=7992,to=7984) + to_lower(788)=(from=7993,to=7985) + to_lower(789)=(from=7994,to=7986) + to_lower(790)=(from=7995,to=7987) + to_lower(791)=(from=7996,to=7988) + to_lower(792)=(from=7997,to=7989) + to_lower(793)=(from=7998,to=7990) + to_lower(794)=(from=7999,to=7991) + to_lower(795)=(from=8008,to=8000) + to_lower(796)=(from=8009,to=8001) + to_lower(797)=(from=8010,to=8002) + to_lower(798)=(from=8011,to=8003) + to_lower(799)=(from=8012,to=8004) + to_lower(800)=(from=8013,to=8005) + to_lower(801)=(from=8025,to=8017) + to_lower(802)=(from=8027,to=8019) + to_lower(803)=(from=8029,to=8021) + to_lower(804)=(from=8031,to=8023) + to_lower(805)=(from=8040,to=8032) + to_lower(806)=(from=8041,to=8033) + to_lower(807)=(from=8042,to=8034) + to_lower(808)=(from=8043,to=8035) + to_lower(809)=(from=8044,to=8036) + to_lower(810)=(from=8045,to=8037) + to_lower(811)=(from=8046,to=8038) + to_lower(812)=(from=8047,to=8039) + to_lower(813)=(from=8072,to=8064) + to_lower(814)=(from=8073,to=8065) + to_lower(815)=(from=8074,to=8066) + to_lower(816)=(from=8075,to=8067) + to_lower(817)=(from=8076,to=8068) + to_lower(818)=(from=8077,to=8069) + to_lower(819)=(from=8078,to=8070) + to_lower(820)=(from=8079,to=8071) + to_lower(821)=(from=8088,to=8080) + to_lower(822)=(from=8089,to=8081) + to_lower(823)=(from=8090,to=8082) + to_lower(824)=(from=8091,to=8083) + to_lower(825)=(from=8092,to=8084) + to_lower(826)=(from=8093,to=8085) + to_lower(827)=(from=8094,to=8086) + to_lower(828)=(from=8095,to=8087) + to_lower(829)=(from=8104,to=8096) + to_lower(830)=(from=8105,to=8097) + to_lower(831)=(from=8106,to=8098) + to_lower(832)=(from=8107,to=8099) + to_lower(833)=(from=8108,to=8100) + to_lower(834)=(from=8109,to=8101) + to_lower(835)=(from=8110,to=8102) + to_lower(836)=(from=8111,to=8103) + to_lower(837)=(from=8120,to=8112) + to_lower(838)=(from=8121,to=8113) + to_lower(839)=(from=8122,to=8048) + to_lower(840)=(from=8123,to=8049) + to_lower(841)=(from=8124,to=8115) + to_lower(842)=(from=8136,to=8050) + to_lower(843)=(from=8137,to=8051) + to_lower(844)=(from=8138,to=8052) + to_lower(845)=(from=8139,to=8053) + to_lower(846)=(from=8140,to=8131) + to_lower(847)=(from=8152,to=8144) + to_lower(848)=(from=8153,to=8145) + to_lower(849)=(from=8154,to=8054) + to_lower(850)=(from=8155,to=8055) + to_lower(851)=(from=8168,to=8160) + to_lower(852)=(from=8169,to=8161) + to_lower(853)=(from=8170,to=8058) + to_lower(854)=(from=8171,to=8059) + to_lower(855)=(from=8172,to=8165) + to_lower(856)=(from=8184,to=8056) + to_lower(857)=(from=8185,to=8057) + to_lower(858)=(from=8186,to=8060) + to_lower(859)=(from=8187,to=8061) + to_lower(860)=(from=8188,to=8179) + to_lower(861)=(from=8486,to=969) + to_lower(862)=(from=8490,to=107) + to_lower(863)=(from=8491,to=229) + to_lower(864)=(from=8498,to=8526) + to_lower(865)=(from=8544,to=8560) + to_lower(866)=(from=8545,to=8561) + to_lower(867)=(from=8546,to=8562) + to_lower(868)=(from=8547,to=8563) + to_lower(869)=(from=8548,to=8564) + to_lower(870)=(from=8549,to=8565) + to_lower(871)=(from=8550,to=8566) + to_lower(872)=(from=8551,to=8567) + to_lower(873)=(from=8552,to=8568) + to_lower(874)=(from=8553,to=8569) + to_lower(875)=(from=8554,to=8570) + to_lower(876)=(from=8555,to=8571) + to_lower(877)=(from=8556,to=8572) + to_lower(878)=(from=8557,to=8573) + to_lower(879)=(from=8558,to=8574) + to_lower(880)=(from=8559,to=8575) + to_lower(881)=(from=8579,to=8580) + to_lower(882)=(from=9398,to=9424) + to_lower(883)=(from=9399,to=9425) + to_lower(884)=(from=9400,to=9426) + to_lower(885)=(from=9401,to=9427) + to_lower(886)=(from=9402,to=9428) + to_lower(887)=(from=9403,to=9429) + to_lower(888)=(from=9404,to=9430) + to_lower(889)=(from=9405,to=9431) + to_lower(890)=(from=9406,to=9432) + to_lower(891)=(from=9407,to=9433) + to_lower(892)=(from=9408,to=9434) + to_lower(893)=(from=9409,to=9435) + to_lower(894)=(from=9410,to=9436) + to_lower(895)=(from=9411,to=9437) + to_lower(896)=(from=9412,to=9438) + to_lower(897)=(from=9413,to=9439) + to_lower(898)=(from=9414,to=9440) + to_lower(899)=(from=9415,to=9441) + to_lower(900)=(from=9416,to=9442) + to_lower(901)=(from=9417,to=9443) + to_lower(902)=(from=9418,to=9444) + to_lower(903)=(from=9419,to=9445) + to_lower(904)=(from=9420,to=9446) + to_lower(905)=(from=9421,to=9447) + to_lower(906)=(from=9422,to=9448) + to_lower(907)=(from=9423,to=9449) + to_lower(908)=(from=11264,to=11312) + to_lower(909)=(from=11265,to=11313) + to_lower(910)=(from=11266,to=11314) + to_lower(911)=(from=11267,to=11315) + to_lower(912)=(from=11268,to=11316) + to_lower(913)=(from=11269,to=11317) + to_lower(914)=(from=11270,to=11318) + to_lower(915)=(from=11271,to=11319) + to_lower(916)=(from=11272,to=11320) + to_lower(917)=(from=11273,to=11321) + to_lower(918)=(from=11274,to=11322) + to_lower(919)=(from=11275,to=11323) + to_lower(920)=(from=11276,to=11324) + to_lower(921)=(from=11277,to=11325) + to_lower(922)=(from=11278,to=11326) + to_lower(923)=(from=11279,to=11327) + to_lower(924)=(from=11280,to=11328) + to_lower(925)=(from=11281,to=11329) + to_lower(926)=(from=11282,to=11330) + to_lower(927)=(from=11283,to=11331) + to_lower(928)=(from=11284,to=11332) + to_lower(929)=(from=11285,to=11333) + to_lower(930)=(from=11286,to=11334) + to_lower(931)=(from=11287,to=11335) + to_lower(932)=(from=11288,to=11336) + to_lower(933)=(from=11289,to=11337) + to_lower(934)=(from=11290,to=11338) + to_lower(935)=(from=11291,to=11339) + to_lower(936)=(from=11292,to=11340) + to_lower(937)=(from=11293,to=11341) + to_lower(938)=(from=11294,to=11342) + to_lower(939)=(from=11295,to=11343) + to_lower(940)=(from=11296,to=11344) + to_lower(941)=(from=11297,to=11345) + to_lower(942)=(from=11298,to=11346) + to_lower(943)=(from=11299,to=11347) + to_lower(944)=(from=11300,to=11348) + to_lower(945)=(from=11301,to=11349) + to_lower(946)=(from=11302,to=11350) + to_lower(947)=(from=11303,to=11351) + to_lower(948)=(from=11304,to=11352) + to_lower(949)=(from=11305,to=11353) + to_lower(950)=(from=11306,to=11354) + to_lower(951)=(from=11307,to=11355) + to_lower(952)=(from=11308,to=11356) + to_lower(953)=(from=11309,to=11357) + to_lower(954)=(from=11310,to=11358) + to_lower(955)=(from=11360,to=11361) + to_lower(956)=(from=11362,to=619) + to_lower(957)=(from=11363,to=7549) + to_lower(958)=(from=11364,to=637) + to_lower(959)=(from=11367,to=11368) + to_lower(960)=(from=11369,to=11370) + to_lower(961)=(from=11371,to=11372) + to_lower(962)=(from=11373,to=593) + to_lower(963)=(from=11374,to=625) + to_lower(964)=(from=11375,to=592) + to_lower(965)=(from=11376,to=594) + to_lower(966)=(from=11378,to=11379) + to_lower(967)=(from=11381,to=11382) + to_lower(968)=(from=11390,to=575) + to_lower(969)=(from=11391,to=576) + to_lower(970)=(from=11392,to=11393) + to_lower(971)=(from=11394,to=11395) + to_lower(972)=(from=11396,to=11397) + to_lower(973)=(from=11398,to=11399) + to_lower(974)=(from=11400,to=11401) + to_lower(975)=(from=11402,to=11403) + to_lower(976)=(from=11404,to=11405) + to_lower(977)=(from=11406,to=11407) + to_lower(978)=(from=11408,to=11409) + to_lower(979)=(from=11410,to=11411) + to_lower(980)=(from=11412,to=11413) + to_lower(981)=(from=11414,to=11415) + to_lower(982)=(from=11416,to=11417) + to_lower(983)=(from=11418,to=11419) + to_lower(984)=(from=11420,to=11421) + to_lower(985)=(from=11422,to=11423) + to_lower(986)=(from=11424,to=11425) + to_lower(987)=(from=11426,to=11427) + to_lower(988)=(from=11428,to=11429) + to_lower(989)=(from=11430,to=11431) + to_lower(990)=(from=11432,to=11433) + to_lower(991)=(from=11434,to=11435) + to_lower(992)=(from=11436,to=11437) + to_lower(993)=(from=11438,to=11439) + to_lower(994)=(from=11440,to=11441) + to_lower(995)=(from=11442,to=11443) + to_lower(996)=(from=11444,to=11445) + to_lower(997)=(from=11446,to=11447) + to_lower(998)=(from=11448,to=11449) + to_lower(999)=(from=11450,to=11451) + to_lower(1000)=(from=11452,to=11453) + to_lower(1001)=(from=11454,to=11455) + to_lower(1002)=(from=11456,to=11457) + to_lower(1003)=(from=11458,to=11459) + to_lower(1004)=(from=11460,to=11461) + to_lower(1005)=(from=11462,to=11463) + to_lower(1006)=(from=11464,to=11465) + to_lower(1007)=(from=11466,to=11467) + to_lower(1008)=(from=11468,to=11469) + to_lower(1009)=(from=11470,to=11471) + to_lower(1010)=(from=11472,to=11473) + to_lower(1011)=(from=11474,to=11475) + to_lower(1012)=(from=11476,to=11477) + to_lower(1013)=(from=11478,to=11479) + to_lower(1014)=(from=11480,to=11481) + to_lower(1015)=(from=11482,to=11483) + to_lower(1016)=(from=11484,to=11485) + to_lower(1017)=(from=11486,to=11487) + to_lower(1018)=(from=11488,to=11489) + to_lower(1019)=(from=11490,to=11491) + to_lower(1020)=(from=11499,to=11500) + to_lower(1021)=(from=11501,to=11502) + to_lower(1022)=(from=11506,to=11507) + to_lower(1023)=(from=42560,to=42561) + to_lower(1024)=(from=42562,to=42563) + to_lower(1025)=(from=42564,to=42565) + to_lower(1026)=(from=42566,to=42567) + to_lower(1027)=(from=42568,to=42569) + to_lower(1028)=(from=42570,to=42571) + to_lower(1029)=(from=42572,to=42573) + to_lower(1030)=(from=42574,to=42575) + to_lower(1031)=(from=42576,to=42577) + to_lower(1032)=(from=42578,to=42579) + to_lower(1033)=(from=42580,to=42581) + to_lower(1034)=(from=42582,to=42583) + to_lower(1035)=(from=42584,to=42585) + to_lower(1036)=(from=42586,to=42587) + to_lower(1037)=(from=42588,to=42589) + to_lower(1038)=(from=42590,to=42591) + to_lower(1039)=(from=42592,to=42593) + to_lower(1040)=(from=42594,to=42595) + to_lower(1041)=(from=42596,to=42597) + to_lower(1042)=(from=42598,to=42599) + to_lower(1043)=(from=42600,to=42601) + to_lower(1044)=(from=42602,to=42603) + to_lower(1045)=(from=42604,to=42605) + to_lower(1046)=(from=42624,to=42625) + to_lower(1047)=(from=42626,to=42627) + to_lower(1048)=(from=42628,to=42629) + to_lower(1049)=(from=42630,to=42631) + to_lower(1050)=(from=42632,to=42633) + to_lower(1051)=(from=42634,to=42635) + to_lower(1052)=(from=42636,to=42637) + to_lower(1053)=(from=42638,to=42639) + to_lower(1054)=(from=42640,to=42641) + to_lower(1055)=(from=42642,to=42643) + to_lower(1056)=(from=42644,to=42645) + to_lower(1057)=(from=42646,to=42647) + to_lower(1058)=(from=42648,to=42649) + to_lower(1059)=(from=42650,to=42651) + to_lower(1060)=(from=42786,to=42787) + to_lower(1061)=(from=42788,to=42789) + to_lower(1062)=(from=42790,to=42791) + to_lower(1063)=(from=42792,to=42793) + to_lower(1064)=(from=42794,to=42795) + to_lower(1065)=(from=42796,to=42797) + to_lower(1066)=(from=42798,to=42799) + to_lower(1067)=(from=42802,to=42803) + to_lower(1068)=(from=42804,to=42805) + to_lower(1069)=(from=42806,to=42807) + to_lower(1070)=(from=42808,to=42809) + to_lower(1071)=(from=42810,to=42811) + to_lower(1072)=(from=42812,to=42813) + to_lower(1073)=(from=42814,to=42815) + to_lower(1074)=(from=42816,to=42817) + to_lower(1075)=(from=42818,to=42819) + to_lower(1076)=(from=42820,to=42821) + to_lower(1077)=(from=42822,to=42823) + to_lower(1078)=(from=42824,to=42825) + to_lower(1079)=(from=42826,to=42827) + to_lower(1080)=(from=42828,to=42829) + to_lower(1081)=(from=42830,to=42831) + to_lower(1082)=(from=42832,to=42833) + to_lower(1083)=(from=42834,to=42835) + to_lower(1084)=(from=42836,to=42837) + to_lower(1085)=(from=42838,to=42839) + to_lower(1086)=(from=42840,to=42841) + to_lower(1087)=(from=42842,to=42843) + to_lower(1088)=(from=42844,to=42845) + to_lower(1089)=(from=42846,to=42847) + to_lower(1090)=(from=42848,to=42849) + to_lower(1091)=(from=42850,to=42851) + to_lower(1092)=(from=42852,to=42853) + to_lower(1093)=(from=42854,to=42855) + to_lower(1094)=(from=42856,to=42857) + to_lower(1095)=(from=42858,to=42859) + to_lower(1096)=(from=42860,to=42861) + to_lower(1097)=(from=42862,to=42863) + to_lower(1098)=(from=42873,to=42874) + to_lower(1099)=(from=42875,to=42876) + to_lower(1100)=(from=42877,to=7545) + to_lower(1101)=(from=42878,to=42879) + to_lower(1102)=(from=42880,to=42881) + to_lower(1103)=(from=42882,to=42883) + to_lower(1104)=(from=42884,to=42885) + to_lower(1105)=(from=42886,to=42887) + to_lower(1106)=(from=42891,to=42892) + to_lower(1107)=(from=42893,to=613) + to_lower(1108)=(from=42896,to=42897) + to_lower(1109)=(from=42898,to=42899) + to_lower(1110)=(from=42902,to=42903) + to_lower(1111)=(from=42904,to=42905) + to_lower(1112)=(from=42906,to=42907) + to_lower(1113)=(from=42908,to=42909) + to_lower(1114)=(from=42910,to=42911) + to_lower(1115)=(from=42912,to=42913) + to_lower(1116)=(from=42914,to=42915) + to_lower(1117)=(from=42916,to=42917) + to_lower(1118)=(from=42918,to=42919) + to_lower(1119)=(from=42920,to=42921) + to_lower(1120)=(from=42922,to=614) + to_lower(1121)=(from=42923,to=604) + to_lower(1122)=(from=42924,to=609) + to_lower(1123)=(from=42925,to=620) + to_lower(1124)=(from=42926,to=618) + to_lower(1125)=(from=42928,to=670) + to_lower(1126)=(from=42929,to=647) + to_lower(1127)=(from=42930,to=669) + to_lower(1128)=(from=42931,to=43859) + to_lower(1129)=(from=42932,to=42933) + to_lower(1130)=(from=42934,to=42935) + to_lower(1131)=(from=42936,to=42937) + to_lower(1132)=(from=42938,to=42939) + to_lower(1133)=(from=42940,to=42941) + to_lower(1134)=(from=42942,to=42943) + to_lower(1135)=(from=42946,to=42947) + to_lower(1136)=(from=42948,to=42900) + to_lower(1137)=(from=42949,to=642) + to_lower(1138)=(from=42950,to=7566) + to_lower(1139)=(from=42951,to=42952) + to_lower(1140)=(from=42953,to=42954) + to_lower(1141)=(from=42997,to=42998) + to_lower(1142)=(from=65313,to=65345) + to_lower(1143)=(from=65314,to=65346) + to_lower(1144)=(from=65315,to=65347) + to_lower(1145)=(from=65316,to=65348) + to_lower(1146)=(from=65317,to=65349) + to_lower(1147)=(from=65318,to=65350) + to_lower(1148)=(from=65319,to=65351) + to_lower(1149)=(from=65320,to=65352) + to_lower(1150)=(from=65321,to=65353) + to_lower(1151)=(from=65322,to=65354) + to_lower(1152)=(from=65323,to=65355) + to_lower(1153)=(from=65324,to=65356) + to_lower(1154)=(from=65325,to=65357) + to_lower(1155)=(from=65326,to=65358) + to_lower(1156)=(from=65327,to=65359) + to_lower(1157)=(from=65328,to=65360) + to_lower(1158)=(from=65329,to=65361) + to_lower(1159)=(from=65330,to=65362) + to_lower(1160)=(from=65331,to=65363) + to_lower(1161)=(from=65332,to=65364) + to_lower(1162)=(from=65333,to=65365) + to_lower(1163)=(from=65334,to=65366) + to_lower(1164)=(from=65335,to=65367) + to_lower(1165)=(from=65336,to=65368) + to_lower(1166)=(from=65337,to=65369) + to_lower(1167)=(from=65338,to=65370) + to_lower(1168)=(from=66560,to=66600) + to_lower(1169)=(from=66561,to=66601) + to_lower(1170)=(from=66562,to=66602) + to_lower(1171)=(from=66563,to=66603) + to_lower(1172)=(from=66564,to=66604) + to_lower(1173)=(from=66565,to=66605) + to_lower(1174)=(from=66566,to=66606) + to_lower(1175)=(from=66567,to=66607) + to_lower(1176)=(from=66568,to=66608) + to_lower(1177)=(from=66569,to=66609) + to_lower(1178)=(from=66570,to=66610) + to_lower(1179)=(from=66571,to=66611) + to_lower(1180)=(from=66572,to=66612) + to_lower(1181)=(from=66573,to=66613) + to_lower(1182)=(from=66574,to=66614) + to_lower(1183)=(from=66575,to=66615) + to_lower(1184)=(from=66576,to=66616) + to_lower(1185)=(from=66577,to=66617) + to_lower(1186)=(from=66578,to=66618) + to_lower(1187)=(from=66579,to=66619) + to_lower(1188)=(from=66580,to=66620) + to_lower(1189)=(from=66581,to=66621) + to_lower(1190)=(from=66582,to=66622) + to_lower(1191)=(from=66583,to=66623) + to_lower(1192)=(from=66584,to=66624) + to_lower(1193)=(from=66585,to=66625) + to_lower(1194)=(from=66586,to=66626) + to_lower(1195)=(from=66587,to=66627) + to_lower(1196)=(from=66588,to=66628) + to_lower(1197)=(from=66589,to=66629) + to_lower(1198)=(from=66590,to=66630) + to_lower(1199)=(from=66591,to=66631) + to_lower(1200)=(from=66592,to=66632) + to_lower(1201)=(from=66593,to=66633) + to_lower(1202)=(from=66594,to=66634) + to_lower(1203)=(from=66595,to=66635) + to_lower(1204)=(from=66596,to=66636) + to_lower(1205)=(from=66597,to=66637) + to_lower(1206)=(from=66598,to=66638) + to_lower(1207)=(from=66599,to=66639) + to_lower(1208)=(from=66736,to=66776) + to_lower(1209)=(from=66737,to=66777) + to_lower(1210)=(from=66738,to=66778) + to_lower(1211)=(from=66739,to=66779) + to_lower(1212)=(from=66740,to=66780) + to_lower(1213)=(from=66741,to=66781) + to_lower(1214)=(from=66742,to=66782) + to_lower(1215)=(from=66743,to=66783) + to_lower(1216)=(from=66744,to=66784) + to_lower(1217)=(from=66745,to=66785) + to_lower(1218)=(from=66746,to=66786) + to_lower(1219)=(from=66747,to=66787) + to_lower(1220)=(from=66748,to=66788) + to_lower(1221)=(from=66749,to=66789) + to_lower(1222)=(from=66750,to=66790) + to_lower(1223)=(from=66751,to=66791) + to_lower(1224)=(from=66752,to=66792) + to_lower(1225)=(from=66753,to=66793) + to_lower(1226)=(from=66754,to=66794) + to_lower(1227)=(from=66755,to=66795) + to_lower(1228)=(from=66756,to=66796) + to_lower(1229)=(from=66757,to=66797) + to_lower(1230)=(from=66758,to=66798) + to_lower(1231)=(from=66759,to=66799) + to_lower(1232)=(from=66760,to=66800) + to_lower(1233)=(from=66761,to=66801) + to_lower(1234)=(from=66762,to=66802) + to_lower(1235)=(from=66763,to=66803) + to_lower(1236)=(from=66764,to=66804) + to_lower(1237)=(from=66765,to=66805) + to_lower(1238)=(from=66766,to=66806) + to_lower(1239)=(from=66767,to=66807) + to_lower(1240)=(from=66768,to=66808) + to_lower(1241)=(from=66769,to=66809) + to_lower(1242)=(from=66770,to=66810) + to_lower(1243)=(from=66771,to=66811) + to_lower(1244)=(from=68736,to=68800) + to_lower(1245)=(from=68737,to=68801) + to_lower(1246)=(from=68738,to=68802) + to_lower(1247)=(from=68739,to=68803) + to_lower(1248)=(from=68740,to=68804) + to_lower(1249)=(from=68741,to=68805) + to_lower(1250)=(from=68742,to=68806) + to_lower(1251)=(from=68743,to=68807) + to_lower(1252)=(from=68744,to=68808) + to_lower(1253)=(from=68745,to=68809) + to_lower(1254)=(from=68746,to=68810) + to_lower(1255)=(from=68747,to=68811) + to_lower(1256)=(from=68748,to=68812) + to_lower(1257)=(from=68749,to=68813) + to_lower(1258)=(from=68750,to=68814) + to_lower(1259)=(from=68751,to=68815) + to_lower(1260)=(from=68752,to=68816) + to_lower(1261)=(from=68753,to=68817) + to_lower(1262)=(from=68754,to=68818) + to_lower(1263)=(from=68755,to=68819) + to_lower(1264)=(from=68756,to=68820) + to_lower(1265)=(from=68757,to=68821) + to_lower(1266)=(from=68758,to=68822) + to_lower(1267)=(from=68759,to=68823) + to_lower(1268)=(from=68760,to=68824) + to_lower(1269)=(from=68761,to=68825) + to_lower(1270)=(from=68762,to=68826) + to_lower(1271)=(from=68763,to=68827) + to_lower(1272)=(from=68764,to=68828) + to_lower(1273)=(from=68765,to=68829) + to_lower(1274)=(from=68766,to=68830) + to_lower(1275)=(from=68767,to=68831) + to_lower(1276)=(from=68768,to=68832) + to_lower(1277)=(from=68769,to=68833) + to_lower(1278)=(from=68770,to=68834) + to_lower(1279)=(from=68771,to=68835) + to_lower(1280)=(from=68772,to=68836) + to_lower(1281)=(from=68773,to=68837) + to_lower(1282)=(from=68774,to=68838) + to_lower(1283)=(from=68775,to=68839) + to_lower(1284)=(from=68776,to=68840) + to_lower(1285)=(from=68777,to=68841) + to_lower(1286)=(from=68778,to=68842) + to_lower(1287)=(from=68779,to=68843) + to_lower(1288)=(from=68780,to=68844) + to_lower(1289)=(from=68781,to=68845) + to_lower(1290)=(from=68782,to=68846) + to_lower(1291)=(from=68783,to=68847) + to_lower(1292)=(from=68784,to=68848) + to_lower(1293)=(from=68785,to=68849) + to_lower(1294)=(from=68786,to=68850) + to_lower(1295)=(from=71840,to=71872) + to_lower(1296)=(from=71841,to=71873) + to_lower(1297)=(from=71842,to=71874) + to_lower(1298)=(from=71843,to=71875) + to_lower(1299)=(from=71844,to=71876) + to_lower(1300)=(from=71845,to=71877) + to_lower(1301)=(from=71846,to=71878) + to_lower(1302)=(from=71847,to=71879) + to_lower(1303)=(from=71848,to=71880) + to_lower(1304)=(from=71849,to=71881) + to_lower(1305)=(from=71850,to=71882) + to_lower(1306)=(from=71851,to=71883) + to_lower(1307)=(from=71852,to=71884) + to_lower(1308)=(from=71853,to=71885) + to_lower(1309)=(from=71854,to=71886) + to_lower(1310)=(from=71855,to=71887) + to_lower(1311)=(from=71856,to=71888) + to_lower(1312)=(from=71857,to=71889) + to_lower(1313)=(from=71858,to=71890) + to_lower(1314)=(from=71859,to=71891) + to_lower(1315)=(from=71860,to=71892) + to_lower(1316)=(from=71861,to=71893) + to_lower(1317)=(from=71862,to=71894) + to_lower(1318)=(from=71863,to=71895) + to_lower(1319)=(from=71864,to=71896) + to_lower(1320)=(from=71865,to=71897) + to_lower(1321)=(from=71866,to=71898) + to_lower(1322)=(from=71867,to=71899) + to_lower(1323)=(from=71868,to=71900) + to_lower(1324)=(from=71869,to=71901) + to_lower(1325)=(from=71870,to=71902) + to_lower(1326)=(from=71871,to=71903) + to_lower(1327)=(from=93760,to=93792) + to_lower(1328)=(from=93761,to=93793) + to_lower(1329)=(from=93762,to=93794) + to_lower(1330)=(from=93763,to=93795) + to_lower(1331)=(from=93764,to=93796) + to_lower(1332)=(from=93765,to=93797) + to_lower(1333)=(from=93766,to=93798) + to_lower(1334)=(from=93767,to=93799) + to_lower(1335)=(from=93768,to=93800) + to_lower(1336)=(from=93769,to=93801) + to_lower(1337)=(from=93770,to=93802) + to_lower(1338)=(from=93771,to=93803) + to_lower(1339)=(from=93772,to=93804) + to_lower(1340)=(from=93773,to=93805) + to_lower(1341)=(from=93774,to=93806) + to_lower(1342)=(from=93775,to=93807) + to_lower(1343)=(from=93776,to=93808) + to_lower(1344)=(from=93777,to=93809) + to_lower(1345)=(from=93778,to=93810) + to_lower(1346)=(from=93779,to=93811) + to_lower(1347)=(from=93780,to=93812) + to_lower(1348)=(from=93781,to=93813) + to_lower(1349)=(from=93782,to=93814) + to_lower(1350)=(from=93783,to=93815) + to_lower(1351)=(from=93784,to=93816) + to_lower(1352)=(from=93785,to=93817) + to_lower(1353)=(from=93786,to=93818) + to_lower(1354)=(from=93787,to=93819) + to_lower(1355)=(from=93788,to=93820) + to_lower(1356)=(from=93789,to=93821) + to_lower(1357)=(from=93790,to=93822) + to_lower(1358)=(from=93791,to=93823) + to_lower(1359)=(from=125184,to=125218) + to_lower(1360)=(from=125185,to=125219) + to_lower(1361)=(from=125186,to=125220) + to_lower(1362)=(from=125187,to=125221) + to_lower(1363)=(from=125188,to=125222) + to_lower(1364)=(from=125189,to=125223) + to_lower(1365)=(from=125190,to=125224) + to_lower(1366)=(from=125191,to=125225) + to_lower(1367)=(from=125192,to=125226) + to_lower(1368)=(from=125193,to=125227) + to_lower(1369)=(from=125194,to=125228) + to_lower(1370)=(from=125195,to=125229) + to_lower(1371)=(from=125196,to=125230) + to_lower(1372)=(from=125197,to=125231) + to_lower(1373)=(from=125198,to=125232) + to_lower(1374)=(from=125199,to=125233) + to_lower(1375)=(from=125200,to=125234) + to_lower(1376)=(from=125201,to=125235) + to_lower(1377)=(from=125202,to=125236) + to_lower(1378)=(from=125203,to=125237) + to_lower(1379)=(from=125204,to=125238) + to_lower(1380)=(from=125205,to=125239) + to_lower(1381)=(from=125206,to=125240) + to_lower(1382)=(from=125207,to=125241) + to_lower(1383)=(from=125208,to=125242) + to_lower(1384)=(from=125209,to=125243) + to_lower(1385)=(from=125210,to=125244) + to_lower(1386)=(from=125211,to=125245) + to_lower(1387)=(from=125212,to=125246) + to_lower(1388)=(from=125213,to=125247) + to_lower(1389)=(from=125214,to=125248) + to_lower(1390)=(from=125215,to=125249) + to_lower(1391)=(from=125216,to=125250) + to_lower(1392)=(from=125217,to=125251) + to_upper(0)=(from=97,to=65) + to_upper(1)=(from=98,to=66) + to_upper(2)=(from=99,to=67) + to_upper(3)=(from=100,to=68) + to_upper(4)=(from=101,to=69) + to_upper(5)=(from=102,to=70) + to_upper(6)=(from=103,to=71) + to_upper(7)=(from=104,to=72) + to_upper(8)=(from=105,to=73) + to_upper(9)=(from=106,to=74) + to_upper(10)=(from=107,to=75) + to_upper(11)=(from=108,to=76) + to_upper(12)=(from=109,to=77) + to_upper(13)=(from=110,to=78) + to_upper(14)=(from=111,to=79) + to_upper(15)=(from=112,to=80) + to_upper(16)=(from=113,to=81) + to_upper(17)=(from=114,to=82) + to_upper(18)=(from=115,to=83) + to_upper(19)=(from=116,to=84) + to_upper(20)=(from=117,to=85) + to_upper(21)=(from=118,to=86) + to_upper(22)=(from=119,to=87) + to_upper(23)=(from=120,to=88) + to_upper(24)=(from=121,to=89) + to_upper(25)=(from=122,to=90) + to_upper(26)=(from=181,to=924) + to_upper(27)=(from=224,to=192) + to_upper(28)=(from=225,to=193) + to_upper(29)=(from=226,to=194) + to_upper(30)=(from=227,to=195) + to_upper(31)=(from=228,to=196) + to_upper(32)=(from=229,to=197) + to_upper(33)=(from=230,to=198) + to_upper(34)=(from=231,to=199) + to_upper(35)=(from=232,to=200) + to_upper(36)=(from=233,to=201) + to_upper(37)=(from=234,to=202) + to_upper(38)=(from=235,to=203) + to_upper(39)=(from=236,to=204) + to_upper(40)=(from=237,to=205) + to_upper(41)=(from=238,to=206) + to_upper(42)=(from=239,to=207) + to_upper(43)=(from=240,to=208) + to_upper(44)=(from=241,to=209) + to_upper(45)=(from=242,to=210) + to_upper(46)=(from=243,to=211) + to_upper(47)=(from=244,to=212) + to_upper(48)=(from=245,to=213) + to_upper(49)=(from=246,to=214) + to_upper(50)=(from=248,to=216) + to_upper(51)=(from=249,to=217) + to_upper(52)=(from=250,to=218) + to_upper(53)=(from=251,to=219) + to_upper(54)=(from=252,to=220) + to_upper(55)=(from=253,to=221) + to_upper(56)=(from=254,to=222) + to_upper(57)=(from=255,to=376) + to_upper(58)=(from=257,to=256) + to_upper(59)=(from=259,to=258) + to_upper(60)=(from=261,to=260) + to_upper(61)=(from=263,to=262) + to_upper(62)=(from=265,to=264) + to_upper(63)=(from=267,to=266) + to_upper(64)=(from=269,to=268) + to_upper(65)=(from=271,to=270) + to_upper(66)=(from=273,to=272) + to_upper(67)=(from=275,to=274) + to_upper(68)=(from=277,to=276) + to_upper(69)=(from=279,to=278) + to_upper(70)=(from=281,to=280) + to_upper(71)=(from=283,to=282) + to_upper(72)=(from=285,to=284) + to_upper(73)=(from=287,to=286) + to_upper(74)=(from=289,to=288) + to_upper(75)=(from=291,to=290) + to_upper(76)=(from=293,to=292) + to_upper(77)=(from=295,to=294) + to_upper(78)=(from=297,to=296) + to_upper(79)=(from=299,to=298) + to_upper(80)=(from=301,to=300) + to_upper(81)=(from=303,to=302) + to_upper(82)=(from=305,to=73) + to_upper(83)=(from=307,to=306) + to_upper(84)=(from=309,to=308) + to_upper(85)=(from=311,to=310) + to_upper(86)=(from=314,to=313) + to_upper(87)=(from=316,to=315) + to_upper(88)=(from=318,to=317) + to_upper(89)=(from=320,to=319) + to_upper(90)=(from=322,to=321) + to_upper(91)=(from=324,to=323) + to_upper(92)=(from=326,to=325) + to_upper(93)=(from=328,to=327) + to_upper(94)=(from=331,to=330) + to_upper(95)=(from=333,to=332) + to_upper(96)=(from=335,to=334) + to_upper(97)=(from=337,to=336) + to_upper(98)=(from=339,to=338) + to_upper(99)=(from=341,to=340) + to_upper(100)=(from=343,to=342) + to_upper(101)=(from=345,to=344) + to_upper(102)=(from=347,to=346) + to_upper(103)=(from=349,to=348) + to_upper(104)=(from=351,to=350) + to_upper(105)=(from=353,to=352) + to_upper(106)=(from=355,to=354) + to_upper(107)=(from=357,to=356) + to_upper(108)=(from=359,to=358) + to_upper(109)=(from=361,to=360) + to_upper(110)=(from=363,to=362) + to_upper(111)=(from=365,to=364) + to_upper(112)=(from=367,to=366) + to_upper(113)=(from=369,to=368) + to_upper(114)=(from=371,to=370) + to_upper(115)=(from=373,to=372) + to_upper(116)=(from=375,to=374) + to_upper(117)=(from=378,to=377) + to_upper(118)=(from=380,to=379) + to_upper(119)=(from=382,to=381) + to_upper(120)=(from=383,to=83) + to_upper(121)=(from=384,to=579) + to_upper(122)=(from=387,to=386) + to_upper(123)=(from=389,to=388) + to_upper(124)=(from=392,to=391) + to_upper(125)=(from=396,to=395) + to_upper(126)=(from=402,to=401) + to_upper(127)=(from=405,to=502) + to_upper(128)=(from=409,to=408) + to_upper(129)=(from=410,to=573) + to_upper(130)=(from=414,to=544) + to_upper(131)=(from=417,to=416) + to_upper(132)=(from=419,to=418) + to_upper(133)=(from=421,to=420) + to_upper(134)=(from=424,to=423) + to_upper(135)=(from=429,to=428) + to_upper(136)=(from=432,to=431) + to_upper(137)=(from=436,to=435) + to_upper(138)=(from=438,to=437) + to_upper(139)=(from=441,to=440) + to_upper(140)=(from=445,to=444) + to_upper(141)=(from=447,to=503) + to_upper(142)=(from=453,to=452) + to_upper(143)=(from=454,to=452) + to_upper(144)=(from=456,to=455) + to_upper(145)=(from=457,to=455) + to_upper(146)=(from=459,to=458) + to_upper(147)=(from=460,to=458) + to_upper(148)=(from=462,to=461) + to_upper(149)=(from=464,to=463) + to_upper(150)=(from=466,to=465) + to_upper(151)=(from=468,to=467) + to_upper(152)=(from=470,to=469) + to_upper(153)=(from=472,to=471) + to_upper(154)=(from=474,to=473) + to_upper(155)=(from=476,to=475) + to_upper(156)=(from=477,to=398) + to_upper(157)=(from=479,to=478) + to_upper(158)=(from=481,to=480) + to_upper(159)=(from=483,to=482) + to_upper(160)=(from=485,to=484) + to_upper(161)=(from=487,to=486) + to_upper(162)=(from=489,to=488) + to_upper(163)=(from=491,to=490) + to_upper(164)=(from=493,to=492) + to_upper(165)=(from=495,to=494) + to_upper(166)=(from=498,to=497) + to_upper(167)=(from=499,to=497) + to_upper(168)=(from=501,to=500) + to_upper(169)=(from=505,to=504) + to_upper(170)=(from=507,to=506) + to_upper(171)=(from=509,to=508) + to_upper(172)=(from=511,to=510) + to_upper(173)=(from=513,to=512) + to_upper(174)=(from=515,to=514) + to_upper(175)=(from=517,to=516) + to_upper(176)=(from=519,to=518) + to_upper(177)=(from=521,to=520) + to_upper(178)=(from=523,to=522) + to_upper(179)=(from=525,to=524) + to_upper(180)=(from=527,to=526) + to_upper(181)=(from=529,to=528) + to_upper(182)=(from=531,to=530) + to_upper(183)=(from=533,to=532) + to_upper(184)=(from=535,to=534) + to_upper(185)=(from=537,to=536) + to_upper(186)=(from=539,to=538) + to_upper(187)=(from=541,to=540) + to_upper(188)=(from=543,to=542) + to_upper(189)=(from=547,to=546) + to_upper(190)=(from=549,to=548) + to_upper(191)=(from=551,to=550) + to_upper(192)=(from=553,to=552) + to_upper(193)=(from=555,to=554) + to_upper(194)=(from=557,to=556) + to_upper(195)=(from=559,to=558) + to_upper(196)=(from=561,to=560) + to_upper(197)=(from=563,to=562) + to_upper(198)=(from=572,to=571) + to_upper(199)=(from=575,to=11390) + to_upper(200)=(from=576,to=11391) + to_upper(201)=(from=578,to=577) + to_upper(202)=(from=583,to=582) + to_upper(203)=(from=585,to=584) + to_upper(204)=(from=587,to=586) + to_upper(205)=(from=589,to=588) + to_upper(206)=(from=591,to=590) + to_upper(207)=(from=592,to=11375) + to_upper(208)=(from=593,to=11373) + to_upper(209)=(from=594,to=11376) + to_upper(210)=(from=595,to=385) + to_upper(211)=(from=596,to=390) + to_upper(212)=(from=598,to=393) + to_upper(213)=(from=599,to=394) + to_upper(214)=(from=601,to=399) + to_upper(215)=(from=603,to=400) + to_upper(216)=(from=604,to=42923) + to_upper(217)=(from=608,to=403) + to_upper(218)=(from=609,to=42924) + to_upper(219)=(from=611,to=404) + to_upper(220)=(from=613,to=42893) + to_upper(221)=(from=614,to=42922) + to_upper(222)=(from=616,to=407) + to_upper(223)=(from=617,to=406) + to_upper(224)=(from=618,to=42926) + to_upper(225)=(from=619,to=11362) + to_upper(226)=(from=620,to=42925) + to_upper(227)=(from=623,to=412) + to_upper(228)=(from=625,to=11374) + to_upper(229)=(from=626,to=413) + to_upper(230)=(from=629,to=415) + to_upper(231)=(from=637,to=11364) + to_upper(232)=(from=640,to=422) + to_upper(233)=(from=642,to=42949) + to_upper(234)=(from=643,to=425) + to_upper(235)=(from=647,to=42929) + to_upper(236)=(from=648,to=430) + to_upper(237)=(from=649,to=580) + to_upper(238)=(from=650,to=433) + to_upper(239)=(from=651,to=434) + to_upper(240)=(from=652,to=581) + to_upper(241)=(from=658,to=439) + to_upper(242)=(from=669,to=42930) + to_upper(243)=(from=670,to=42928) + to_upper(244)=(from=837,to=921) + to_upper(245)=(from=881,to=880) + to_upper(246)=(from=883,to=882) + to_upper(247)=(from=887,to=886) + to_upper(248)=(from=891,to=1021) + to_upper(249)=(from=892,to=1022) + to_upper(250)=(from=893,to=1023) + to_upper(251)=(from=940,to=902) + to_upper(252)=(from=941,to=904) + to_upper(253)=(from=942,to=905) + to_upper(254)=(from=943,to=906) + to_upper(255)=(from=945,to=913) + to_upper(256)=(from=946,to=914) + to_upper(257)=(from=947,to=915) + to_upper(258)=(from=948,to=916) + to_upper(259)=(from=949,to=917) + to_upper(260)=(from=950,to=918) + to_upper(261)=(from=951,to=919) + to_upper(262)=(from=952,to=920) + to_upper(263)=(from=953,to=921) + to_upper(264)=(from=954,to=922) + to_upper(265)=(from=955,to=923) + to_upper(266)=(from=956,to=924) + to_upper(267)=(from=957,to=925) + to_upper(268)=(from=958,to=926) + to_upper(269)=(from=959,to=927) + to_upper(270)=(from=960,to=928) + to_upper(271)=(from=961,to=929) + to_upper(272)=(from=962,to=931) + to_upper(273)=(from=963,to=931) + to_upper(274)=(from=964,to=932) + to_upper(275)=(from=965,to=933) + to_upper(276)=(from=966,to=934) + to_upper(277)=(from=967,to=935) + to_upper(278)=(from=968,to=936) + to_upper(279)=(from=969,to=937) + to_upper(280)=(from=970,to=938) + to_upper(281)=(from=971,to=939) + to_upper(282)=(from=972,to=908) + to_upper(283)=(from=973,to=910) + to_upper(284)=(from=974,to=911) + to_upper(285)=(from=976,to=914) + to_upper(286)=(from=977,to=920) + to_upper(287)=(from=981,to=934) + to_upper(288)=(from=982,to=928) + to_upper(289)=(from=983,to=975) + to_upper(290)=(from=985,to=984) + to_upper(291)=(from=987,to=986) + to_upper(292)=(from=989,to=988) + to_upper(293)=(from=991,to=990) + to_upper(294)=(from=993,to=992) + to_upper(295)=(from=995,to=994) + to_upper(296)=(from=997,to=996) + to_upper(297)=(from=999,to=998) + to_upper(298)=(from=1001,to=1000) + to_upper(299)=(from=1003,to=1002) + to_upper(300)=(from=1005,to=1004) + to_upper(301)=(from=1007,to=1006) + to_upper(302)=(from=1008,to=922) + to_upper(303)=(from=1009,to=929) + to_upper(304)=(from=1010,to=1017) + to_upper(305)=(from=1011,to=895) + to_upper(306)=(from=1013,to=917) + to_upper(307)=(from=1016,to=1015) + to_upper(308)=(from=1019,to=1018) + to_upper(309)=(from=1072,to=1040) + to_upper(310)=(from=1073,to=1041) + to_upper(311)=(from=1074,to=1042) + to_upper(312)=(from=1075,to=1043) + to_upper(313)=(from=1076,to=1044) + to_upper(314)=(from=1077,to=1045) + to_upper(315)=(from=1078,to=1046) + to_upper(316)=(from=1079,to=1047) + to_upper(317)=(from=1080,to=1048) + to_upper(318)=(from=1081,to=1049) + to_upper(319)=(from=1082,to=1050) + to_upper(320)=(from=1083,to=1051) + to_upper(321)=(from=1084,to=1052) + to_upper(322)=(from=1085,to=1053) + to_upper(323)=(from=1086,to=1054) + to_upper(324)=(from=1087,to=1055) + to_upper(325)=(from=1088,to=1056) + to_upper(326)=(from=1089,to=1057) + to_upper(327)=(from=1090,to=1058) + to_upper(328)=(from=1091,to=1059) + to_upper(329)=(from=1092,to=1060) + to_upper(330)=(from=1093,to=1061) + to_upper(331)=(from=1094,to=1062) + to_upper(332)=(from=1095,to=1063) + to_upper(333)=(from=1096,to=1064) + to_upper(334)=(from=1097,to=1065) + to_upper(335)=(from=1098,to=1066) + to_upper(336)=(from=1099,to=1067) + to_upper(337)=(from=1100,to=1068) + to_upper(338)=(from=1101,to=1069) + to_upper(339)=(from=1102,to=1070) + to_upper(340)=(from=1103,to=1071) + to_upper(341)=(from=1104,to=1024) + to_upper(342)=(from=1105,to=1025) + to_upper(343)=(from=1106,to=1026) + to_upper(344)=(from=1107,to=1027) + to_upper(345)=(from=1108,to=1028) + to_upper(346)=(from=1109,to=1029) + to_upper(347)=(from=1110,to=1030) + to_upper(348)=(from=1111,to=1031) + to_upper(349)=(from=1112,to=1032) + to_upper(350)=(from=1113,to=1033) + to_upper(351)=(from=1114,to=1034) + to_upper(352)=(from=1115,to=1035) + to_upper(353)=(from=1116,to=1036) + to_upper(354)=(from=1117,to=1037) + to_upper(355)=(from=1118,to=1038) + to_upper(356)=(from=1119,to=1039) + to_upper(357)=(from=1121,to=1120) + to_upper(358)=(from=1123,to=1122) + to_upper(359)=(from=1125,to=1124) + to_upper(360)=(from=1127,to=1126) + to_upper(361)=(from=1129,to=1128) + to_upper(362)=(from=1131,to=1130) + to_upper(363)=(from=1133,to=1132) + to_upper(364)=(from=1135,to=1134) + to_upper(365)=(from=1137,to=1136) + to_upper(366)=(from=1139,to=1138) + to_upper(367)=(from=1141,to=1140) + to_upper(368)=(from=1143,to=1142) + to_upper(369)=(from=1145,to=1144) + to_upper(370)=(from=1147,to=1146) + to_upper(371)=(from=1149,to=1148) + to_upper(372)=(from=1151,to=1150) + to_upper(373)=(from=1153,to=1152) + to_upper(374)=(from=1163,to=1162) + to_upper(375)=(from=1165,to=1164) + to_upper(376)=(from=1167,to=1166) + to_upper(377)=(from=1169,to=1168) + to_upper(378)=(from=1171,to=1170) + to_upper(379)=(from=1173,to=1172) + to_upper(380)=(from=1175,to=1174) + to_upper(381)=(from=1177,to=1176) + to_upper(382)=(from=1179,to=1178) + to_upper(383)=(from=1181,to=1180) + to_upper(384)=(from=1183,to=1182) + to_upper(385)=(from=1185,to=1184) + to_upper(386)=(from=1187,to=1186) + to_upper(387)=(from=1189,to=1188) + to_upper(388)=(from=1191,to=1190) + to_upper(389)=(from=1193,to=1192) + to_upper(390)=(from=1195,to=1194) + to_upper(391)=(from=1197,to=1196) + to_upper(392)=(from=1199,to=1198) + to_upper(393)=(from=1201,to=1200) + to_upper(394)=(from=1203,to=1202) + to_upper(395)=(from=1205,to=1204) + to_upper(396)=(from=1207,to=1206) + to_upper(397)=(from=1209,to=1208) + to_upper(398)=(from=1211,to=1210) + to_upper(399)=(from=1213,to=1212) + to_upper(400)=(from=1215,to=1214) + to_upper(401)=(from=1218,to=1217) + to_upper(402)=(from=1220,to=1219) + to_upper(403)=(from=1222,to=1221) + to_upper(404)=(from=1224,to=1223) + to_upper(405)=(from=1226,to=1225) + to_upper(406)=(from=1228,to=1227) + to_upper(407)=(from=1230,to=1229) + to_upper(408)=(from=1231,to=1216) + to_upper(409)=(from=1233,to=1232) + to_upper(410)=(from=1235,to=1234) + to_upper(411)=(from=1237,to=1236) + to_upper(412)=(from=1239,to=1238) + to_upper(413)=(from=1241,to=1240) + to_upper(414)=(from=1243,to=1242) + to_upper(415)=(from=1245,to=1244) + to_upper(416)=(from=1247,to=1246) + to_upper(417)=(from=1249,to=1248) + to_upper(418)=(from=1251,to=1250) + to_upper(419)=(from=1253,to=1252) + to_upper(420)=(from=1255,to=1254) + to_upper(421)=(from=1257,to=1256) + to_upper(422)=(from=1259,to=1258) + to_upper(423)=(from=1261,to=1260) + to_upper(424)=(from=1263,to=1262) + to_upper(425)=(from=1265,to=1264) + to_upper(426)=(from=1267,to=1266) + to_upper(427)=(from=1269,to=1268) + to_upper(428)=(from=1271,to=1270) + to_upper(429)=(from=1273,to=1272) + to_upper(430)=(from=1275,to=1274) + to_upper(431)=(from=1277,to=1276) + to_upper(432)=(from=1279,to=1278) + to_upper(433)=(from=1281,to=1280) + to_upper(434)=(from=1283,to=1282) + to_upper(435)=(from=1285,to=1284) + to_upper(436)=(from=1287,to=1286) + to_upper(437)=(from=1289,to=1288) + to_upper(438)=(from=1291,to=1290) + to_upper(439)=(from=1293,to=1292) + to_upper(440)=(from=1295,to=1294) + to_upper(441)=(from=1297,to=1296) + to_upper(442)=(from=1299,to=1298) + to_upper(443)=(from=1301,to=1300) + to_upper(444)=(from=1303,to=1302) + to_upper(445)=(from=1305,to=1304) + to_upper(446)=(from=1307,to=1306) + to_upper(447)=(from=1309,to=1308) + to_upper(448)=(from=1311,to=1310) + to_upper(449)=(from=1313,to=1312) + to_upper(450)=(from=1315,to=1314) + to_upper(451)=(from=1317,to=1316) + to_upper(452)=(from=1319,to=1318) + to_upper(453)=(from=1321,to=1320) + to_upper(454)=(from=1323,to=1322) + to_upper(455)=(from=1325,to=1324) + to_upper(456)=(from=1327,to=1326) + to_upper(457)=(from=1377,to=1329) + to_upper(458)=(from=1378,to=1330) + to_upper(459)=(from=1379,to=1331) + to_upper(460)=(from=1380,to=1332) + to_upper(461)=(from=1381,to=1333) + to_upper(462)=(from=1382,to=1334) + to_upper(463)=(from=1383,to=1335) + to_upper(464)=(from=1384,to=1336) + to_upper(465)=(from=1385,to=1337) + to_upper(466)=(from=1386,to=1338) + to_upper(467)=(from=1387,to=1339) + to_upper(468)=(from=1388,to=1340) + to_upper(469)=(from=1389,to=1341) + to_upper(470)=(from=1390,to=1342) + to_upper(471)=(from=1391,to=1343) + to_upper(472)=(from=1392,to=1344) + to_upper(473)=(from=1393,to=1345) + to_upper(474)=(from=1394,to=1346) + to_upper(475)=(from=1395,to=1347) + to_upper(476)=(from=1396,to=1348) + to_upper(477)=(from=1397,to=1349) + to_upper(478)=(from=1398,to=1350) + to_upper(479)=(from=1399,to=1351) + to_upper(480)=(from=1400,to=1352) + to_upper(481)=(from=1401,to=1353) + to_upper(482)=(from=1402,to=1354) + to_upper(483)=(from=1403,to=1355) + to_upper(484)=(from=1404,to=1356) + to_upper(485)=(from=1405,to=1357) + to_upper(486)=(from=1406,to=1358) + to_upper(487)=(from=1407,to=1359) + to_upper(488)=(from=1408,to=1360) + to_upper(489)=(from=1409,to=1361) + to_upper(490)=(from=1410,to=1362) + to_upper(491)=(from=1411,to=1363) + to_upper(492)=(from=1412,to=1364) + to_upper(493)=(from=1413,to=1365) + to_upper(494)=(from=1414,to=1366) + to_upper(495)=(from=4304,to=7312) + to_upper(496)=(from=4305,to=7313) + to_upper(497)=(from=4306,to=7314) + to_upper(498)=(from=4307,to=7315) + to_upper(499)=(from=4308,to=7316) + to_upper(500)=(from=4309,to=7317) + to_upper(501)=(from=4310,to=7318) + to_upper(502)=(from=4311,to=7319) + to_upper(503)=(from=4312,to=7320) + to_upper(504)=(from=4313,to=7321) + to_upper(505)=(from=4314,to=7322) + to_upper(506)=(from=4315,to=7323) + to_upper(507)=(from=4316,to=7324) + to_upper(508)=(from=4317,to=7325) + to_upper(509)=(from=4318,to=7326) + to_upper(510)=(from=4319,to=7327) + to_upper(511)=(from=4320,to=7328) + to_upper(512)=(from=4321,to=7329) + to_upper(513)=(from=4322,to=7330) + to_upper(514)=(from=4323,to=7331) + to_upper(515)=(from=4324,to=7332) + to_upper(516)=(from=4325,to=7333) + to_upper(517)=(from=4326,to=7334) + to_upper(518)=(from=4327,to=7335) + to_upper(519)=(from=4328,to=7336) + to_upper(520)=(from=4329,to=7337) + to_upper(521)=(from=4330,to=7338) + to_upper(522)=(from=4331,to=7339) + to_upper(523)=(from=4332,to=7340) + to_upper(524)=(from=4333,to=7341) + to_upper(525)=(from=4334,to=7342) + to_upper(526)=(from=4335,to=7343) + to_upper(527)=(from=4336,to=7344) + to_upper(528)=(from=4337,to=7345) + to_upper(529)=(from=4338,to=7346) + to_upper(530)=(from=4339,to=7347) + to_upper(531)=(from=4340,to=7348) + to_upper(532)=(from=4341,to=7349) + to_upper(533)=(from=4342,to=7350) + to_upper(534)=(from=4343,to=7351) + to_upper(535)=(from=4344,to=7352) + to_upper(536)=(from=4345,to=7353) + to_upper(537)=(from=4346,to=7354) + to_upper(538)=(from=4349,to=7357) + to_upper(539)=(from=4350,to=7358) + to_upper(540)=(from=4351,to=7359) + to_upper(541)=(from=5112,to=5104) + to_upper(542)=(from=5113,to=5105) + to_upper(543)=(from=5114,to=5106) + to_upper(544)=(from=5115,to=5107) + to_upper(545)=(from=5116,to=5108) + to_upper(546)=(from=5117,to=5109) + to_upper(547)=(from=7296,to=1042) + to_upper(548)=(from=7297,to=1044) + to_upper(549)=(from=7298,to=1054) + to_upper(550)=(from=7299,to=1057) + to_upper(551)=(from=7300,to=1058) + to_upper(552)=(from=7301,to=1058) + to_upper(553)=(from=7302,to=1066) + to_upper(554)=(from=7303,to=1122) + to_upper(555)=(from=7304,to=42570) + to_upper(556)=(from=7545,to=42877) + to_upper(557)=(from=7549,to=11363) + to_upper(558)=(from=7566,to=42950) + to_upper(559)=(from=7681,to=7680) + to_upper(560)=(from=7683,to=7682) + to_upper(561)=(from=7685,to=7684) + to_upper(562)=(from=7687,to=7686) + to_upper(563)=(from=7689,to=7688) + to_upper(564)=(from=7691,to=7690) + to_upper(565)=(from=7693,to=7692) + to_upper(566)=(from=7695,to=7694) + to_upper(567)=(from=7697,to=7696) + to_upper(568)=(from=7699,to=7698) + to_upper(569)=(from=7701,to=7700) + to_upper(570)=(from=7703,to=7702) + to_upper(571)=(from=7705,to=7704) + to_upper(572)=(from=7707,to=7706) + to_upper(573)=(from=7709,to=7708) + to_upper(574)=(from=7711,to=7710) + to_upper(575)=(from=7713,to=7712) + to_upper(576)=(from=7715,to=7714) + to_upper(577)=(from=7717,to=7716) + to_upper(578)=(from=7719,to=7718) + to_upper(579)=(from=7721,to=7720) + to_upper(580)=(from=7723,to=7722) + to_upper(581)=(from=7725,to=7724) + to_upper(582)=(from=7727,to=7726) + to_upper(583)=(from=7729,to=7728) + to_upper(584)=(from=7731,to=7730) + to_upper(585)=(from=7733,to=7732) + to_upper(586)=(from=7735,to=7734) + to_upper(587)=(from=7737,to=7736) + to_upper(588)=(from=7739,to=7738) + to_upper(589)=(from=7741,to=7740) + to_upper(590)=(from=7743,to=7742) + to_upper(591)=(from=7745,to=7744) + to_upper(592)=(from=7747,to=7746) + to_upper(593)=(from=7749,to=7748) + to_upper(594)=(from=7751,to=7750) + to_upper(595)=(from=7753,to=7752) + to_upper(596)=(from=7755,to=7754) + to_upper(597)=(from=7757,to=7756) + to_upper(598)=(from=7759,to=7758) + to_upper(599)=(from=7761,to=7760) + to_upper(600)=(from=7763,to=7762) + to_upper(601)=(from=7765,to=7764) + to_upper(602)=(from=7767,to=7766) + to_upper(603)=(from=7769,to=7768) + to_upper(604)=(from=7771,to=7770) + to_upper(605)=(from=7773,to=7772) + to_upper(606)=(from=7775,to=7774) + to_upper(607)=(from=7777,to=7776) + to_upper(608)=(from=7779,to=7778) + to_upper(609)=(from=7781,to=7780) + to_upper(610)=(from=7783,to=7782) + to_upper(611)=(from=7785,to=7784) + to_upper(612)=(from=7787,to=7786) + to_upper(613)=(from=7789,to=7788) + to_upper(614)=(from=7791,to=7790) + to_upper(615)=(from=7793,to=7792) + to_upper(616)=(from=7795,to=7794) + to_upper(617)=(from=7797,to=7796) + to_upper(618)=(from=7799,to=7798) + to_upper(619)=(from=7801,to=7800) + to_upper(620)=(from=7803,to=7802) + to_upper(621)=(from=7805,to=7804) + to_upper(622)=(from=7807,to=7806) + to_upper(623)=(from=7809,to=7808) + to_upper(624)=(from=7811,to=7810) + to_upper(625)=(from=7813,to=7812) + to_upper(626)=(from=7815,to=7814) + to_upper(627)=(from=7817,to=7816) + to_upper(628)=(from=7819,to=7818) + to_upper(629)=(from=7821,to=7820) + to_upper(630)=(from=7823,to=7822) + to_upper(631)=(from=7825,to=7824) + to_upper(632)=(from=7827,to=7826) + to_upper(633)=(from=7829,to=7828) + to_upper(634)=(from=7835,to=7776) + to_upper(635)=(from=7841,to=7840) + to_upper(636)=(from=7843,to=7842) + to_upper(637)=(from=7845,to=7844) + to_upper(638)=(from=7847,to=7846) + to_upper(639)=(from=7849,to=7848) + to_upper(640)=(from=7851,to=7850) + to_upper(641)=(from=7853,to=7852) + to_upper(642)=(from=7855,to=7854) + to_upper(643)=(from=7857,to=7856) + to_upper(644)=(from=7859,to=7858) + to_upper(645)=(from=7861,to=7860) + to_upper(646)=(from=7863,to=7862) + to_upper(647)=(from=7865,to=7864) + to_upper(648)=(from=7867,to=7866) + to_upper(649)=(from=7869,to=7868) + to_upper(650)=(from=7871,to=7870) + to_upper(651)=(from=7873,to=7872) + to_upper(652)=(from=7875,to=7874) + to_upper(653)=(from=7877,to=7876) + to_upper(654)=(from=7879,to=7878) + to_upper(655)=(from=7881,to=7880) + to_upper(656)=(from=7883,to=7882) + to_upper(657)=(from=7885,to=7884) + to_upper(658)=(from=7887,to=7886) + to_upper(659)=(from=7889,to=7888) + to_upper(660)=(from=7891,to=7890) + to_upper(661)=(from=7893,to=7892) + to_upper(662)=(from=7895,to=7894) + to_upper(663)=(from=7897,to=7896) + to_upper(664)=(from=7899,to=7898) + to_upper(665)=(from=7901,to=7900) + to_upper(666)=(from=7903,to=7902) + to_upper(667)=(from=7905,to=7904) + to_upper(668)=(from=7907,to=7906) + to_upper(669)=(from=7909,to=7908) + to_upper(670)=(from=7911,to=7910) + to_upper(671)=(from=7913,to=7912) + to_upper(672)=(from=7915,to=7914) + to_upper(673)=(from=7917,to=7916) + to_upper(674)=(from=7919,to=7918) + to_upper(675)=(from=7921,to=7920) + to_upper(676)=(from=7923,to=7922) + to_upper(677)=(from=7925,to=7924) + to_upper(678)=(from=7927,to=7926) + to_upper(679)=(from=7929,to=7928) + to_upper(680)=(from=7931,to=7930) + to_upper(681)=(from=7933,to=7932) + to_upper(682)=(from=7935,to=7934) + to_upper(683)=(from=7936,to=7944) + to_upper(684)=(from=7937,to=7945) + to_upper(685)=(from=7938,to=7946) + to_upper(686)=(from=7939,to=7947) + to_upper(687)=(from=7940,to=7948) + to_upper(688)=(from=7941,to=7949) + to_upper(689)=(from=7942,to=7950) + to_upper(690)=(from=7943,to=7951) + to_upper(691)=(from=7952,to=7960) + to_upper(692)=(from=7953,to=7961) + to_upper(693)=(from=7954,to=7962) + to_upper(694)=(from=7955,to=7963) + to_upper(695)=(from=7956,to=7964) + to_upper(696)=(from=7957,to=7965) + to_upper(697)=(from=7968,to=7976) + to_upper(698)=(from=7969,to=7977) + to_upper(699)=(from=7970,to=7978) + to_upper(700)=(from=7971,to=7979) + to_upper(701)=(from=7972,to=7980) + to_upper(702)=(from=7973,to=7981) + to_upper(703)=(from=7974,to=7982) + to_upper(704)=(from=7975,to=7983) + to_upper(705)=(from=7984,to=7992) + to_upper(706)=(from=7985,to=7993) + to_upper(707)=(from=7986,to=7994) + to_upper(708)=(from=7987,to=7995) + to_upper(709)=(from=7988,to=7996) + to_upper(710)=(from=7989,to=7997) + to_upper(711)=(from=7990,to=7998) + to_upper(712)=(from=7991,to=7999) + to_upper(713)=(from=8000,to=8008) + to_upper(714)=(from=8001,to=8009) + to_upper(715)=(from=8002,to=8010) + to_upper(716)=(from=8003,to=8011) + to_upper(717)=(from=8004,to=8012) + to_upper(718)=(from=8005,to=8013) + to_upper(719)=(from=8017,to=8025) + to_upper(720)=(from=8019,to=8027) + to_upper(721)=(from=8021,to=8029) + to_upper(722)=(from=8023,to=8031) + to_upper(723)=(from=8032,to=8040) + to_upper(724)=(from=8033,to=8041) + to_upper(725)=(from=8034,to=8042) + to_upper(726)=(from=8035,to=8043) + to_upper(727)=(from=8036,to=8044) + to_upper(728)=(from=8037,to=8045) + to_upper(729)=(from=8038,to=8046) + to_upper(730)=(from=8039,to=8047) + to_upper(731)=(from=8048,to=8122) + to_upper(732)=(from=8049,to=8123) + to_upper(733)=(from=8050,to=8136) + to_upper(734)=(from=8051,to=8137) + to_upper(735)=(from=8052,to=8138) + to_upper(736)=(from=8053,to=8139) + to_upper(737)=(from=8054,to=8154) + to_upper(738)=(from=8055,to=8155) + to_upper(739)=(from=8056,to=8184) + to_upper(740)=(from=8057,to=8185) + to_upper(741)=(from=8058,to=8170) + to_upper(742)=(from=8059,to=8171) + to_upper(743)=(from=8060,to=8186) + to_upper(744)=(from=8061,to=8187) + to_upper(745)=(from=8064,to=8072) + to_upper(746)=(from=8065,to=8073) + to_upper(747)=(from=8066,to=8074) + to_upper(748)=(from=8067,to=8075) + to_upper(749)=(from=8068,to=8076) + to_upper(750)=(from=8069,to=8077) + to_upper(751)=(from=8070,to=8078) + to_upper(752)=(from=8071,to=8079) + to_upper(753)=(from=8080,to=8088) + to_upper(754)=(from=8081,to=8089) + to_upper(755)=(from=8082,to=8090) + to_upper(756)=(from=8083,to=8091) + to_upper(757)=(from=8084,to=8092) + to_upper(758)=(from=8085,to=8093) + to_upper(759)=(from=8086,to=8094) + to_upper(760)=(from=8087,to=8095) + to_upper(761)=(from=8096,to=8104) + to_upper(762)=(from=8097,to=8105) + to_upper(763)=(from=8098,to=8106) + to_upper(764)=(from=8099,to=8107) + to_upper(765)=(from=8100,to=8108) + to_upper(766)=(from=8101,to=8109) + to_upper(767)=(from=8102,to=8110) + to_upper(768)=(from=8103,to=8111) + to_upper(769)=(from=8112,to=8120) + to_upper(770)=(from=8113,to=8121) + to_upper(771)=(from=8115,to=8124) + to_upper(772)=(from=8126,to=921) + to_upper(773)=(from=8131,to=8140) + to_upper(774)=(from=8144,to=8152) + to_upper(775)=(from=8145,to=8153) + to_upper(776)=(from=8160,to=8168) + to_upper(777)=(from=8161,to=8169) + to_upper(778)=(from=8165,to=8172) + to_upper(779)=(from=8179,to=8188) + to_upper(780)=(from=8526,to=8498) + to_upper(781)=(from=8560,to=8544) + to_upper(782)=(from=8561,to=8545) + to_upper(783)=(from=8562,to=8546) + to_upper(784)=(from=8563,to=8547) + to_upper(785)=(from=8564,to=8548) + to_upper(786)=(from=8565,to=8549) + to_upper(787)=(from=8566,to=8550) + to_upper(788)=(from=8567,to=8551) + to_upper(789)=(from=8568,to=8552) + to_upper(790)=(from=8569,to=8553) + to_upper(791)=(from=8570,to=8554) + to_upper(792)=(from=8571,to=8555) + to_upper(793)=(from=8572,to=8556) + to_upper(794)=(from=8573,to=8557) + to_upper(795)=(from=8574,to=8558) + to_upper(796)=(from=8575,to=8559) + to_upper(797)=(from=8580,to=8579) + to_upper(798)=(from=9424,to=9398) + to_upper(799)=(from=9425,to=9399) + to_upper(800)=(from=9426,to=9400) + to_upper(801)=(from=9427,to=9401) + to_upper(802)=(from=9428,to=9402) + to_upper(803)=(from=9429,to=9403) + to_upper(804)=(from=9430,to=9404) + to_upper(805)=(from=9431,to=9405) + to_upper(806)=(from=9432,to=9406) + to_upper(807)=(from=9433,to=9407) + to_upper(808)=(from=9434,to=9408) + to_upper(809)=(from=9435,to=9409) + to_upper(810)=(from=9436,to=9410) + to_upper(811)=(from=9437,to=9411) + to_upper(812)=(from=9438,to=9412) + to_upper(813)=(from=9439,to=9413) + to_upper(814)=(from=9440,to=9414) + to_upper(815)=(from=9441,to=9415) + to_upper(816)=(from=9442,to=9416) + to_upper(817)=(from=9443,to=9417) + to_upper(818)=(from=9444,to=9418) + to_upper(819)=(from=9445,to=9419) + to_upper(820)=(from=9446,to=9420) + to_upper(821)=(from=9447,to=9421) + to_upper(822)=(from=9448,to=9422) + to_upper(823)=(from=9449,to=9423) + to_upper(824)=(from=11312,to=11264) + to_upper(825)=(from=11313,to=11265) + to_upper(826)=(from=11314,to=11266) + to_upper(827)=(from=11315,to=11267) + to_upper(828)=(from=11316,to=11268) + to_upper(829)=(from=11317,to=11269) + to_upper(830)=(from=11318,to=11270) + to_upper(831)=(from=11319,to=11271) + to_upper(832)=(from=11320,to=11272) + to_upper(833)=(from=11321,to=11273) + to_upper(834)=(from=11322,to=11274) + to_upper(835)=(from=11323,to=11275) + to_upper(836)=(from=11324,to=11276) + to_upper(837)=(from=11325,to=11277) + to_upper(838)=(from=11326,to=11278) + to_upper(839)=(from=11327,to=11279) + to_upper(840)=(from=11328,to=11280) + to_upper(841)=(from=11329,to=11281) + to_upper(842)=(from=11330,to=11282) + to_upper(843)=(from=11331,to=11283) + to_upper(844)=(from=11332,to=11284) + to_upper(845)=(from=11333,to=11285) + to_upper(846)=(from=11334,to=11286) + to_upper(847)=(from=11335,to=11287) + to_upper(848)=(from=11336,to=11288) + to_upper(849)=(from=11337,to=11289) + to_upper(850)=(from=11338,to=11290) + to_upper(851)=(from=11339,to=11291) + to_upper(852)=(from=11340,to=11292) + to_upper(853)=(from=11341,to=11293) + to_upper(854)=(from=11342,to=11294) + to_upper(855)=(from=11343,to=11295) + to_upper(856)=(from=11344,to=11296) + to_upper(857)=(from=11345,to=11297) + to_upper(858)=(from=11346,to=11298) + to_upper(859)=(from=11347,to=11299) + to_upper(860)=(from=11348,to=11300) + to_upper(861)=(from=11349,to=11301) + to_upper(862)=(from=11350,to=11302) + to_upper(863)=(from=11351,to=11303) + to_upper(864)=(from=11352,to=11304) + to_upper(865)=(from=11353,to=11305) + to_upper(866)=(from=11354,to=11306) + to_upper(867)=(from=11355,to=11307) + to_upper(868)=(from=11356,to=11308) + to_upper(869)=(from=11357,to=11309) + to_upper(870)=(from=11358,to=11310) + to_upper(871)=(from=11361,to=11360) + to_upper(872)=(from=11365,to=570) + to_upper(873)=(from=11366,to=574) + to_upper(874)=(from=11368,to=11367) + to_upper(875)=(from=11370,to=11369) + to_upper(876)=(from=11372,to=11371) + to_upper(877)=(from=11379,to=11378) + to_upper(878)=(from=11382,to=11381) + to_upper(879)=(from=11393,to=11392) + to_upper(880)=(from=11395,to=11394) + to_upper(881)=(from=11397,to=11396) + to_upper(882)=(from=11399,to=11398) + to_upper(883)=(from=11401,to=11400) + to_upper(884)=(from=11403,to=11402) + to_upper(885)=(from=11405,to=11404) + to_upper(886)=(from=11407,to=11406) + to_upper(887)=(from=11409,to=11408) + to_upper(888)=(from=11411,to=11410) + to_upper(889)=(from=11413,to=11412) + to_upper(890)=(from=11415,to=11414) + to_upper(891)=(from=11417,to=11416) + to_upper(892)=(from=11419,to=11418) + to_upper(893)=(from=11421,to=11420) + to_upper(894)=(from=11423,to=11422) + to_upper(895)=(from=11425,to=11424) + to_upper(896)=(from=11427,to=11426) + to_upper(897)=(from=11429,to=11428) + to_upper(898)=(from=11431,to=11430) + to_upper(899)=(from=11433,to=11432) + to_upper(900)=(from=11435,to=11434) + to_upper(901)=(from=11437,to=11436) + to_upper(902)=(from=11439,to=11438) + to_upper(903)=(from=11441,to=11440) + to_upper(904)=(from=11443,to=11442) + to_upper(905)=(from=11445,to=11444) + to_upper(906)=(from=11447,to=11446) + to_upper(907)=(from=11449,to=11448) + to_upper(908)=(from=11451,to=11450) + to_upper(909)=(from=11453,to=11452) + to_upper(910)=(from=11455,to=11454) + to_upper(911)=(from=11457,to=11456) + to_upper(912)=(from=11459,to=11458) + to_upper(913)=(from=11461,to=11460) + to_upper(914)=(from=11463,to=11462) + to_upper(915)=(from=11465,to=11464) + to_upper(916)=(from=11467,to=11466) + to_upper(917)=(from=11469,to=11468) + to_upper(918)=(from=11471,to=11470) + to_upper(919)=(from=11473,to=11472) + to_upper(920)=(from=11475,to=11474) + to_upper(921)=(from=11477,to=11476) + to_upper(922)=(from=11479,to=11478) + to_upper(923)=(from=11481,to=11480) + to_upper(924)=(from=11483,to=11482) + to_upper(925)=(from=11485,to=11484) + to_upper(926)=(from=11487,to=11486) + to_upper(927)=(from=11489,to=11488) + to_upper(928)=(from=11491,to=11490) + to_upper(929)=(from=11500,to=11499) + to_upper(930)=(from=11502,to=11501) + to_upper(931)=(from=11507,to=11506) + to_upper(932)=(from=11520,to=4256) + to_upper(933)=(from=11521,to=4257) + to_upper(934)=(from=11522,to=4258) + to_upper(935)=(from=11523,to=4259) + to_upper(936)=(from=11524,to=4260) + to_upper(937)=(from=11525,to=4261) + to_upper(938)=(from=11526,to=4262) + to_upper(939)=(from=11527,to=4263) + to_upper(940)=(from=11528,to=4264) + to_upper(941)=(from=11529,to=4265) + to_upper(942)=(from=11530,to=4266) + to_upper(943)=(from=11531,to=4267) + to_upper(944)=(from=11532,to=4268) + to_upper(945)=(from=11533,to=4269) + to_upper(946)=(from=11534,to=4270) + to_upper(947)=(from=11535,to=4271) + to_upper(948)=(from=11536,to=4272) + to_upper(949)=(from=11537,to=4273) + to_upper(950)=(from=11538,to=4274) + to_upper(951)=(from=11539,to=4275) + to_upper(952)=(from=11540,to=4276) + to_upper(953)=(from=11541,to=4277) + to_upper(954)=(from=11542,to=4278) + to_upper(955)=(from=11543,to=4279) + to_upper(956)=(from=11544,to=4280) + to_upper(957)=(from=11545,to=4281) + to_upper(958)=(from=11546,to=4282) + to_upper(959)=(from=11547,to=4283) + to_upper(960)=(from=11548,to=4284) + to_upper(961)=(from=11549,to=4285) + to_upper(962)=(from=11550,to=4286) + to_upper(963)=(from=11551,to=4287) + to_upper(964)=(from=11552,to=4288) + to_upper(965)=(from=11553,to=4289) + to_upper(966)=(from=11554,to=4290) + to_upper(967)=(from=11555,to=4291) + to_upper(968)=(from=11556,to=4292) + to_upper(969)=(from=11557,to=4293) + to_upper(970)=(from=11559,to=4295) + to_upper(971)=(from=11565,to=4301) + to_upper(972)=(from=42561,to=42560) + to_upper(973)=(from=42563,to=42562) + to_upper(974)=(from=42565,to=42564) + to_upper(975)=(from=42567,to=42566) + to_upper(976)=(from=42569,to=42568) + to_upper(977)=(from=42571,to=42570) + to_upper(978)=(from=42573,to=42572) + to_upper(979)=(from=42575,to=42574) + to_upper(980)=(from=42577,to=42576) + to_upper(981)=(from=42579,to=42578) + to_upper(982)=(from=42581,to=42580) + to_upper(983)=(from=42583,to=42582) + to_upper(984)=(from=42585,to=42584) + to_upper(985)=(from=42587,to=42586) + to_upper(986)=(from=42589,to=42588) + to_upper(987)=(from=42591,to=42590) + to_upper(988)=(from=42593,to=42592) + to_upper(989)=(from=42595,to=42594) + to_upper(990)=(from=42597,to=42596) + to_upper(991)=(from=42599,to=42598) + to_upper(992)=(from=42601,to=42600) + to_upper(993)=(from=42603,to=42602) + to_upper(994)=(from=42605,to=42604) + to_upper(995)=(from=42625,to=42624) + to_upper(996)=(from=42627,to=42626) + to_upper(997)=(from=42629,to=42628) + to_upper(998)=(from=42631,to=42630) + to_upper(999)=(from=42633,to=42632) + to_upper(1000)=(from=42635,to=42634) + to_upper(1001)=(from=42637,to=42636) + to_upper(1002)=(from=42639,to=42638) + to_upper(1003)=(from=42641,to=42640) + to_upper(1004)=(from=42643,to=42642) + to_upper(1005)=(from=42645,to=42644) + to_upper(1006)=(from=42647,to=42646) + to_upper(1007)=(from=42649,to=42648) + to_upper(1008)=(from=42651,to=42650) + to_upper(1009)=(from=42787,to=42786) + to_upper(1010)=(from=42789,to=42788) + to_upper(1011)=(from=42791,to=42790) + to_upper(1012)=(from=42793,to=42792) + to_upper(1013)=(from=42795,to=42794) + to_upper(1014)=(from=42797,to=42796) + to_upper(1015)=(from=42799,to=42798) + to_upper(1016)=(from=42803,to=42802) + to_upper(1017)=(from=42805,to=42804) + to_upper(1018)=(from=42807,to=42806) + to_upper(1019)=(from=42809,to=42808) + to_upper(1020)=(from=42811,to=42810) + to_upper(1021)=(from=42813,to=42812) + to_upper(1022)=(from=42815,to=42814) + to_upper(1023)=(from=42817,to=42816) + to_upper(1024)=(from=42819,to=42818) + to_upper(1025)=(from=42821,to=42820) + to_upper(1026)=(from=42823,to=42822) + to_upper(1027)=(from=42825,to=42824) + to_upper(1028)=(from=42827,to=42826) + to_upper(1029)=(from=42829,to=42828) + to_upper(1030)=(from=42831,to=42830) + to_upper(1031)=(from=42833,to=42832) + to_upper(1032)=(from=42835,to=42834) + to_upper(1033)=(from=42837,to=42836) + to_upper(1034)=(from=42839,to=42838) + to_upper(1035)=(from=42841,to=42840) + to_upper(1036)=(from=42843,to=42842) + to_upper(1037)=(from=42845,to=42844) + to_upper(1038)=(from=42847,to=42846) + to_upper(1039)=(from=42849,to=42848) + to_upper(1040)=(from=42851,to=42850) + to_upper(1041)=(from=42853,to=42852) + to_upper(1042)=(from=42855,to=42854) + to_upper(1043)=(from=42857,to=42856) + to_upper(1044)=(from=42859,to=42858) + to_upper(1045)=(from=42861,to=42860) + to_upper(1046)=(from=42863,to=42862) + to_upper(1047)=(from=42874,to=42873) + to_upper(1048)=(from=42876,to=42875) + to_upper(1049)=(from=42879,to=42878) + to_upper(1050)=(from=42881,to=42880) + to_upper(1051)=(from=42883,to=42882) + to_upper(1052)=(from=42885,to=42884) + to_upper(1053)=(from=42887,to=42886) + to_upper(1054)=(from=42892,to=42891) + to_upper(1055)=(from=42897,to=42896) + to_upper(1056)=(from=42899,to=42898) + to_upper(1057)=(from=42900,to=42948) + to_upper(1058)=(from=42903,to=42902) + to_upper(1059)=(from=42905,to=42904) + to_upper(1060)=(from=42907,to=42906) + to_upper(1061)=(from=42909,to=42908) + to_upper(1062)=(from=42911,to=42910) + to_upper(1063)=(from=42913,to=42912) + to_upper(1064)=(from=42915,to=42914) + to_upper(1065)=(from=42917,to=42916) + to_upper(1066)=(from=42919,to=42918) + to_upper(1067)=(from=42921,to=42920) + to_upper(1068)=(from=42933,to=42932) + to_upper(1069)=(from=42935,to=42934) + to_upper(1070)=(from=42937,to=42936) + to_upper(1071)=(from=42939,to=42938) + to_upper(1072)=(from=42941,to=42940) + to_upper(1073)=(from=42943,to=42942) + to_upper(1074)=(from=42947,to=42946) + to_upper(1075)=(from=42952,to=42951) + to_upper(1076)=(from=42954,to=42953) + to_upper(1077)=(from=42998,to=42997) + to_upper(1078)=(from=43859,to=42931) + to_upper(1079)=(from=43888,to=5024) + to_upper(1080)=(from=43889,to=5025) + to_upper(1081)=(from=43890,to=5026) + to_upper(1082)=(from=43891,to=5027) + to_upper(1083)=(from=43892,to=5028) + to_upper(1084)=(from=43893,to=5029) + to_upper(1085)=(from=43894,to=5030) + to_upper(1086)=(from=43895,to=5031) + to_upper(1087)=(from=43896,to=5032) + to_upper(1088)=(from=43897,to=5033) + to_upper(1089)=(from=43898,to=5034) + to_upper(1090)=(from=43899,to=5035) + to_upper(1091)=(from=43900,to=5036) + to_upper(1092)=(from=43901,to=5037) + to_upper(1093)=(from=43902,to=5038) + to_upper(1094)=(from=43903,to=5039) + to_upper(1095)=(from=43904,to=5040) + to_upper(1096)=(from=43905,to=5041) + to_upper(1097)=(from=43906,to=5042) + to_upper(1098)=(from=43907,to=5043) + to_upper(1099)=(from=43908,to=5044) + to_upper(1100)=(from=43909,to=5045) + to_upper(1101)=(from=43910,to=5046) + to_upper(1102)=(from=43911,to=5047) + to_upper(1103)=(from=43912,to=5048) + to_upper(1104)=(from=43913,to=5049) + to_upper(1105)=(from=43914,to=5050) + to_upper(1106)=(from=43915,to=5051) + to_upper(1107)=(from=43916,to=5052) + to_upper(1108)=(from=43917,to=5053) + to_upper(1109)=(from=43918,to=5054) + to_upper(1110)=(from=43919,to=5055) + to_upper(1111)=(from=43920,to=5056) + to_upper(1112)=(from=43921,to=5057) + to_upper(1113)=(from=43922,to=5058) + to_upper(1114)=(from=43923,to=5059) + to_upper(1115)=(from=43924,to=5060) + to_upper(1116)=(from=43925,to=5061) + to_upper(1117)=(from=43926,to=5062) + to_upper(1118)=(from=43927,to=5063) + to_upper(1119)=(from=43928,to=5064) + to_upper(1120)=(from=43929,to=5065) + to_upper(1121)=(from=43930,to=5066) + to_upper(1122)=(from=43931,to=5067) + to_upper(1123)=(from=43932,to=5068) + to_upper(1124)=(from=43933,to=5069) + to_upper(1125)=(from=43934,to=5070) + to_upper(1126)=(from=43935,to=5071) + to_upper(1127)=(from=43936,to=5072) + to_upper(1128)=(from=43937,to=5073) + to_upper(1129)=(from=43938,to=5074) + to_upper(1130)=(from=43939,to=5075) + to_upper(1131)=(from=43940,to=5076) + to_upper(1132)=(from=43941,to=5077) + to_upper(1133)=(from=43942,to=5078) + to_upper(1134)=(from=43943,to=5079) + to_upper(1135)=(from=43944,to=5080) + to_upper(1136)=(from=43945,to=5081) + to_upper(1137)=(from=43946,to=5082) + to_upper(1138)=(from=43947,to=5083) + to_upper(1139)=(from=43948,to=5084) + to_upper(1140)=(from=43949,to=5085) + to_upper(1141)=(from=43950,to=5086) + to_upper(1142)=(from=43951,to=5087) + to_upper(1143)=(from=43952,to=5088) + to_upper(1144)=(from=43953,to=5089) + to_upper(1145)=(from=43954,to=5090) + to_upper(1146)=(from=43955,to=5091) + to_upper(1147)=(from=43956,to=5092) + to_upper(1148)=(from=43957,to=5093) + to_upper(1149)=(from=43958,to=5094) + to_upper(1150)=(from=43959,to=5095) + to_upper(1151)=(from=43960,to=5096) + to_upper(1152)=(from=43961,to=5097) + to_upper(1153)=(from=43962,to=5098) + to_upper(1154)=(from=43963,to=5099) + to_upper(1155)=(from=43964,to=5100) + to_upper(1156)=(from=43965,to=5101) + to_upper(1157)=(from=43966,to=5102) + to_upper(1158)=(from=43967,to=5103) + to_upper(1159)=(from=65345,to=65313) + to_upper(1160)=(from=65346,to=65314) + to_upper(1161)=(from=65347,to=65315) + to_upper(1162)=(from=65348,to=65316) + to_upper(1163)=(from=65349,to=65317) + to_upper(1164)=(from=65350,to=65318) + to_upper(1165)=(from=65351,to=65319) + to_upper(1166)=(from=65352,to=65320) + to_upper(1167)=(from=65353,to=65321) + to_upper(1168)=(from=65354,to=65322) + to_upper(1169)=(from=65355,to=65323) + to_upper(1170)=(from=65356,to=65324) + to_upper(1171)=(from=65357,to=65325) + to_upper(1172)=(from=65358,to=65326) + to_upper(1173)=(from=65359,to=65327) + to_upper(1174)=(from=65360,to=65328) + to_upper(1175)=(from=65361,to=65329) + to_upper(1176)=(from=65362,to=65330) + to_upper(1177)=(from=65363,to=65331) + to_upper(1178)=(from=65364,to=65332) + to_upper(1179)=(from=65365,to=65333) + to_upper(1180)=(from=65366,to=65334) + to_upper(1181)=(from=65367,to=65335) + to_upper(1182)=(from=65368,to=65336) + to_upper(1183)=(from=65369,to=65337) + to_upper(1184)=(from=65370,to=65338) + to_upper(1185)=(from=66600,to=66560) + to_upper(1186)=(from=66601,to=66561) + to_upper(1187)=(from=66602,to=66562) + to_upper(1188)=(from=66603,to=66563) + to_upper(1189)=(from=66604,to=66564) + to_upper(1190)=(from=66605,to=66565) + to_upper(1191)=(from=66606,to=66566) + to_upper(1192)=(from=66607,to=66567) + to_upper(1193)=(from=66608,to=66568) + to_upper(1194)=(from=66609,to=66569) + to_upper(1195)=(from=66610,to=66570) + to_upper(1196)=(from=66611,to=66571) + to_upper(1197)=(from=66612,to=66572) + to_upper(1198)=(from=66613,to=66573) + to_upper(1199)=(from=66614,to=66574) + to_upper(1200)=(from=66615,to=66575) + to_upper(1201)=(from=66616,to=66576) + to_upper(1202)=(from=66617,to=66577) + to_upper(1203)=(from=66618,to=66578) + to_upper(1204)=(from=66619,to=66579) + to_upper(1205)=(from=66620,to=66580) + to_upper(1206)=(from=66621,to=66581) + to_upper(1207)=(from=66622,to=66582) + to_upper(1208)=(from=66623,to=66583) + to_upper(1209)=(from=66624,to=66584) + to_upper(1210)=(from=66625,to=66585) + to_upper(1211)=(from=66626,to=66586) + to_upper(1212)=(from=66627,to=66587) + to_upper(1213)=(from=66628,to=66588) + to_upper(1214)=(from=66629,to=66589) + to_upper(1215)=(from=66630,to=66590) + to_upper(1216)=(from=66631,to=66591) + to_upper(1217)=(from=66632,to=66592) + to_upper(1218)=(from=66633,to=66593) + to_upper(1219)=(from=66634,to=66594) + to_upper(1220)=(from=66635,to=66595) + to_upper(1221)=(from=66636,to=66596) + to_upper(1222)=(from=66637,to=66597) + to_upper(1223)=(from=66638,to=66598) + to_upper(1224)=(from=66639,to=66599) + to_upper(1225)=(from=66776,to=66736) + to_upper(1226)=(from=66777,to=66737) + to_upper(1227)=(from=66778,to=66738) + to_upper(1228)=(from=66779,to=66739) + to_upper(1229)=(from=66780,to=66740) + to_upper(1230)=(from=66781,to=66741) + to_upper(1231)=(from=66782,to=66742) + to_upper(1232)=(from=66783,to=66743) + to_upper(1233)=(from=66784,to=66744) + to_upper(1234)=(from=66785,to=66745) + to_upper(1235)=(from=66786,to=66746) + to_upper(1236)=(from=66787,to=66747) + to_upper(1237)=(from=66788,to=66748) + to_upper(1238)=(from=66789,to=66749) + to_upper(1239)=(from=66790,to=66750) + to_upper(1240)=(from=66791,to=66751) + to_upper(1241)=(from=66792,to=66752) + to_upper(1242)=(from=66793,to=66753) + to_upper(1243)=(from=66794,to=66754) + to_upper(1244)=(from=66795,to=66755) + to_upper(1245)=(from=66796,to=66756) + to_upper(1246)=(from=66797,to=66757) + to_upper(1247)=(from=66798,to=66758) + to_upper(1248)=(from=66799,to=66759) + to_upper(1249)=(from=66800,to=66760) + to_upper(1250)=(from=66801,to=66761) + to_upper(1251)=(from=66802,to=66762) + to_upper(1252)=(from=66803,to=66763) + to_upper(1253)=(from=66804,to=66764) + to_upper(1254)=(from=66805,to=66765) + to_upper(1255)=(from=66806,to=66766) + to_upper(1256)=(from=66807,to=66767) + to_upper(1257)=(from=66808,to=66768) + to_upper(1258)=(from=66809,to=66769) + to_upper(1259)=(from=66810,to=66770) + to_upper(1260)=(from=66811,to=66771) + to_upper(1261)=(from=68800,to=68736) + to_upper(1262)=(from=68801,to=68737) + to_upper(1263)=(from=68802,to=68738) + to_upper(1264)=(from=68803,to=68739) + to_upper(1265)=(from=68804,to=68740) + to_upper(1266)=(from=68805,to=68741) + to_upper(1267)=(from=68806,to=68742) + to_upper(1268)=(from=68807,to=68743) + to_upper(1269)=(from=68808,to=68744) + to_upper(1270)=(from=68809,to=68745) + to_upper(1271)=(from=68810,to=68746) + to_upper(1272)=(from=68811,to=68747) + to_upper(1273)=(from=68812,to=68748) + to_upper(1274)=(from=68813,to=68749) + to_upper(1275)=(from=68814,to=68750) + to_upper(1276)=(from=68815,to=68751) + to_upper(1277)=(from=68816,to=68752) + to_upper(1278)=(from=68817,to=68753) + to_upper(1279)=(from=68818,to=68754) + to_upper(1280)=(from=68819,to=68755) + to_upper(1281)=(from=68820,to=68756) + to_upper(1282)=(from=68821,to=68757) + to_upper(1283)=(from=68822,to=68758) + to_upper(1284)=(from=68823,to=68759) + to_upper(1285)=(from=68824,to=68760) + to_upper(1286)=(from=68825,to=68761) + to_upper(1287)=(from=68826,to=68762) + to_upper(1288)=(from=68827,to=68763) + to_upper(1289)=(from=68828,to=68764) + to_upper(1290)=(from=68829,to=68765) + to_upper(1291)=(from=68830,to=68766) + to_upper(1292)=(from=68831,to=68767) + to_upper(1293)=(from=68832,to=68768) + to_upper(1294)=(from=68833,to=68769) + to_upper(1295)=(from=68834,to=68770) + to_upper(1296)=(from=68835,to=68771) + to_upper(1297)=(from=68836,to=68772) + to_upper(1298)=(from=68837,to=68773) + to_upper(1299)=(from=68838,to=68774) + to_upper(1300)=(from=68839,to=68775) + to_upper(1301)=(from=68840,to=68776) + to_upper(1302)=(from=68841,to=68777) + to_upper(1303)=(from=68842,to=68778) + to_upper(1304)=(from=68843,to=68779) + to_upper(1305)=(from=68844,to=68780) + to_upper(1306)=(from=68845,to=68781) + to_upper(1307)=(from=68846,to=68782) + to_upper(1308)=(from=68847,to=68783) + to_upper(1309)=(from=68848,to=68784) + to_upper(1310)=(from=68849,to=68785) + to_upper(1311)=(from=68850,to=68786) + to_upper(1312)=(from=71872,to=71840) + to_upper(1313)=(from=71873,to=71841) + to_upper(1314)=(from=71874,to=71842) + to_upper(1315)=(from=71875,to=71843) + to_upper(1316)=(from=71876,to=71844) + to_upper(1317)=(from=71877,to=71845) + to_upper(1318)=(from=71878,to=71846) + to_upper(1319)=(from=71879,to=71847) + to_upper(1320)=(from=71880,to=71848) + to_upper(1321)=(from=71881,to=71849) + to_upper(1322)=(from=71882,to=71850) + to_upper(1323)=(from=71883,to=71851) + to_upper(1324)=(from=71884,to=71852) + to_upper(1325)=(from=71885,to=71853) + to_upper(1326)=(from=71886,to=71854) + to_upper(1327)=(from=71887,to=71855) + to_upper(1328)=(from=71888,to=71856) + to_upper(1329)=(from=71889,to=71857) + to_upper(1330)=(from=71890,to=71858) + to_upper(1331)=(from=71891,to=71859) + to_upper(1332)=(from=71892,to=71860) + to_upper(1333)=(from=71893,to=71861) + to_upper(1334)=(from=71894,to=71862) + to_upper(1335)=(from=71895,to=71863) + to_upper(1336)=(from=71896,to=71864) + to_upper(1337)=(from=71897,to=71865) + to_upper(1338)=(from=71898,to=71866) + to_upper(1339)=(from=71899,to=71867) + to_upper(1340)=(from=71900,to=71868) + to_upper(1341)=(from=71901,to=71869) + to_upper(1342)=(from=71902,to=71870) + to_upper(1343)=(from=71903,to=71871) + to_upper(1344)=(from=93792,to=93760) + to_upper(1345)=(from=93793,to=93761) + to_upper(1346)=(from=93794,to=93762) + to_upper(1347)=(from=93795,to=93763) + to_upper(1348)=(from=93796,to=93764) + to_upper(1349)=(from=93797,to=93765) + to_upper(1350)=(from=93798,to=93766) + to_upper(1351)=(from=93799,to=93767) + to_upper(1352)=(from=93800,to=93768) + to_upper(1353)=(from=93801,to=93769) + to_upper(1354)=(from=93802,to=93770) + to_upper(1355)=(from=93803,to=93771) + to_upper(1356)=(from=93804,to=93772) + to_upper(1357)=(from=93805,to=93773) + to_upper(1358)=(from=93806,to=93774) + to_upper(1359)=(from=93807,to=93775) + to_upper(1360)=(from=93808,to=93776) + to_upper(1361)=(from=93809,to=93777) + to_upper(1362)=(from=93810,to=93778) + to_upper(1363)=(from=93811,to=93779) + to_upper(1364)=(from=93812,to=93780) + to_upper(1365)=(from=93813,to=93781) + to_upper(1366)=(from=93814,to=93782) + to_upper(1367)=(from=93815,to=93783) + to_upper(1368)=(from=93816,to=93784) + to_upper(1369)=(from=93817,to=93785) + to_upper(1370)=(from=93818,to=93786) + to_upper(1371)=(from=93819,to=93787) + to_upper(1372)=(from=93820,to=93788) + to_upper(1373)=(from=93821,to=93789) + to_upper(1374)=(from=93822,to=93790) + to_upper(1375)=(from=93823,to=93791) + to_upper(1376)=(from=125218,to=125184) + to_upper(1377)=(from=125219,to=125185) + to_upper(1378)=(from=125220,to=125186) + to_upper(1379)=(from=125221,to=125187) + to_upper(1380)=(from=125222,to=125188) + to_upper(1381)=(from=125223,to=125189) + to_upper(1382)=(from=125224,to=125190) + to_upper(1383)=(from=125225,to=125191) + to_upper(1384)=(from=125226,to=125192) + to_upper(1385)=(from=125227,to=125193) + to_upper(1386)=(from=125228,to=125194) + to_upper(1387)=(from=125229,to=125195) + to_upper(1388)=(from=125230,to=125196) + to_upper(1389)=(from=125231,to=125197) + to_upper(1390)=(from=125232,to=125198) + to_upper(1391)=(from=125233,to=125199) + to_upper(1392)=(from=125234,to=125200) + to_upper(1393)=(from=125235,to=125201) + to_upper(1394)=(from=125236,to=125202) + to_upper(1395)=(from=125237,to=125203) + to_upper(1396)=(from=125238,to=125204) + to_upper(1397)=(from=125239,to=125205) + to_upper(1398)=(from=125240,to=125206) + to_upper(1399)=(from=125241,to=125207) + to_upper(1400)=(from=125242,to=125208) + to_upper(1401)=(from=125243,to=125209) + to_upper(1402)=(from=125244,to=125210) + to_upper(1403)=(from=125245,to=125211) + to_upper(1404)=(from=125246,to=125212) + to_upper(1405)=(from=125247,to=125213) + to_upper(1406)=(from=125248,to=125214) + to_upper(1407)=(from=125249,to=125215) + to_upper(1408)=(from=125250,to=125216) + to_upper(1409)=(from=125251,to=125217) + to_title(0)=(from=97,to=65) + to_title(1)=(from=98,to=66) + to_title(2)=(from=99,to=67) + to_title(3)=(from=100,to=68) + to_title(4)=(from=101,to=69) + to_title(5)=(from=102,to=70) + to_title(6)=(from=103,to=71) + to_title(7)=(from=104,to=72) + to_title(8)=(from=105,to=73) + to_title(9)=(from=106,to=74) + to_title(10)=(from=107,to=75) + to_title(11)=(from=108,to=76) + to_title(12)=(from=109,to=77) + to_title(13)=(from=110,to=78) + to_title(14)=(from=111,to=79) + to_title(15)=(from=112,to=80) + to_title(16)=(from=113,to=81) + to_title(17)=(from=114,to=82) + to_title(18)=(from=115,to=83) + to_title(19)=(from=116,to=84) + to_title(20)=(from=117,to=85) + to_title(21)=(from=118,to=86) + to_title(22)=(from=119,to=87) + to_title(23)=(from=120,to=88) + to_title(24)=(from=121,to=89) + to_title(25)=(from=122,to=90) + to_title(26)=(from=181,to=924) + to_title(27)=(from=224,to=192) + to_title(28)=(from=225,to=193) + to_title(29)=(from=226,to=194) + to_title(30)=(from=227,to=195) + to_title(31)=(from=228,to=196) + to_title(32)=(from=229,to=197) + to_title(33)=(from=230,to=198) + to_title(34)=(from=231,to=199) + to_title(35)=(from=232,to=200) + to_title(36)=(from=233,to=201) + to_title(37)=(from=234,to=202) + to_title(38)=(from=235,to=203) + to_title(39)=(from=236,to=204) + to_title(40)=(from=237,to=205) + to_title(41)=(from=238,to=206) + to_title(42)=(from=239,to=207) + to_title(43)=(from=240,to=208) + to_title(44)=(from=241,to=209) + to_title(45)=(from=242,to=210) + to_title(46)=(from=243,to=211) + to_title(47)=(from=244,to=212) + to_title(48)=(from=245,to=213) + to_title(49)=(from=246,to=214) + to_title(50)=(from=248,to=216) + to_title(51)=(from=249,to=217) + to_title(52)=(from=250,to=218) + to_title(53)=(from=251,to=219) + to_title(54)=(from=252,to=220) + to_title(55)=(from=253,to=221) + to_title(56)=(from=254,to=222) + to_title(57)=(from=255,to=376) + to_title(58)=(from=257,to=256) + to_title(59)=(from=259,to=258) + to_title(60)=(from=261,to=260) + to_title(61)=(from=263,to=262) + to_title(62)=(from=265,to=264) + to_title(63)=(from=267,to=266) + to_title(64)=(from=269,to=268) + to_title(65)=(from=271,to=270) + to_title(66)=(from=273,to=272) + to_title(67)=(from=275,to=274) + to_title(68)=(from=277,to=276) + to_title(69)=(from=279,to=278) + to_title(70)=(from=281,to=280) + to_title(71)=(from=283,to=282) + to_title(72)=(from=285,to=284) + to_title(73)=(from=287,to=286) + to_title(74)=(from=289,to=288) + to_title(75)=(from=291,to=290) + to_title(76)=(from=293,to=292) + to_title(77)=(from=295,to=294) + to_title(78)=(from=297,to=296) + to_title(79)=(from=299,to=298) + to_title(80)=(from=301,to=300) + to_title(81)=(from=303,to=302) + to_title(82)=(from=305,to=73) + to_title(83)=(from=307,to=306) + to_title(84)=(from=309,to=308) + to_title(85)=(from=311,to=310) + to_title(86)=(from=314,to=313) + to_title(87)=(from=316,to=315) + to_title(88)=(from=318,to=317) + to_title(89)=(from=320,to=319) + to_title(90)=(from=322,to=321) + to_title(91)=(from=324,to=323) + to_title(92)=(from=326,to=325) + to_title(93)=(from=328,to=327) + to_title(94)=(from=331,to=330) + to_title(95)=(from=333,to=332) + to_title(96)=(from=335,to=334) + to_title(97)=(from=337,to=336) + to_title(98)=(from=339,to=338) + to_title(99)=(from=341,to=340) + to_title(100)=(from=343,to=342) + to_title(101)=(from=345,to=344) + to_title(102)=(from=347,to=346) + to_title(103)=(from=349,to=348) + to_title(104)=(from=351,to=350) + to_title(105)=(from=353,to=352) + to_title(106)=(from=355,to=354) + to_title(107)=(from=357,to=356) + to_title(108)=(from=359,to=358) + to_title(109)=(from=361,to=360) + to_title(110)=(from=363,to=362) + to_title(111)=(from=365,to=364) + to_title(112)=(from=367,to=366) + to_title(113)=(from=369,to=368) + to_title(114)=(from=371,to=370) + to_title(115)=(from=373,to=372) + to_title(116)=(from=375,to=374) + to_title(117)=(from=378,to=377) + to_title(118)=(from=380,to=379) + to_title(119)=(from=382,to=381) + to_title(120)=(from=383,to=83) + to_title(121)=(from=384,to=579) + to_title(122)=(from=387,to=386) + to_title(123)=(from=389,to=388) + to_title(124)=(from=392,to=391) + to_title(125)=(from=396,to=395) + to_title(126)=(from=402,to=401) + to_title(127)=(from=405,to=502) + to_title(128)=(from=409,to=408) + to_title(129)=(from=410,to=573) + to_title(130)=(from=414,to=544) + to_title(131)=(from=417,to=416) + to_title(132)=(from=419,to=418) + to_title(133)=(from=421,to=420) + to_title(134)=(from=424,to=423) + to_title(135)=(from=429,to=428) + to_title(136)=(from=432,to=431) + to_title(137)=(from=436,to=435) + to_title(138)=(from=438,to=437) + to_title(139)=(from=441,to=440) + to_title(140)=(from=445,to=444) + to_title(141)=(from=447,to=503) + to_title(142)=(from=452,to=453) + to_title(143)=(from=453,to=453) + to_title(144)=(from=454,to=453) + to_title(145)=(from=455,to=456) + to_title(146)=(from=456,to=456) + to_title(147)=(from=457,to=456) + to_title(148)=(from=458,to=459) + to_title(149)=(from=459,to=459) + to_title(150)=(from=460,to=459) + to_title(151)=(from=462,to=461) + to_title(152)=(from=464,to=463) + to_title(153)=(from=466,to=465) + to_title(154)=(from=468,to=467) + to_title(155)=(from=470,to=469) + to_title(156)=(from=472,to=471) + to_title(157)=(from=474,to=473) + to_title(158)=(from=476,to=475) + to_title(159)=(from=477,to=398) + to_title(160)=(from=479,to=478) + to_title(161)=(from=481,to=480) + to_title(162)=(from=483,to=482) + to_title(163)=(from=485,to=484) + to_title(164)=(from=487,to=486) + to_title(165)=(from=489,to=488) + to_title(166)=(from=491,to=490) + to_title(167)=(from=493,to=492) + to_title(168)=(from=495,to=494) + to_title(169)=(from=497,to=498) + to_title(170)=(from=498,to=498) + to_title(171)=(from=499,to=498) + to_title(172)=(from=501,to=500) + to_title(173)=(from=505,to=504) + to_title(174)=(from=507,to=506) + to_title(175)=(from=509,to=508) + to_title(176)=(from=511,to=510) + to_title(177)=(from=513,to=512) + to_title(178)=(from=515,to=514) + to_title(179)=(from=517,to=516) + to_title(180)=(from=519,to=518) + to_title(181)=(from=521,to=520) + to_title(182)=(from=523,to=522) + to_title(183)=(from=525,to=524) + to_title(184)=(from=527,to=526) + to_title(185)=(from=529,to=528) + to_title(186)=(from=531,to=530) + to_title(187)=(from=533,to=532) + to_title(188)=(from=535,to=534) + to_title(189)=(from=537,to=536) + to_title(190)=(from=539,to=538) + to_title(191)=(from=541,to=540) + to_title(192)=(from=543,to=542) + to_title(193)=(from=547,to=546) + to_title(194)=(from=549,to=548) + to_title(195)=(from=551,to=550) + to_title(196)=(from=553,to=552) + to_title(197)=(from=555,to=554) + to_title(198)=(from=557,to=556) + to_title(199)=(from=559,to=558) + to_title(200)=(from=561,to=560) + to_title(201)=(from=563,to=562) + to_title(202)=(from=572,to=571) + to_title(203)=(from=575,to=11390) + to_title(204)=(from=576,to=11391) + to_title(205)=(from=578,to=577) + to_title(206)=(from=583,to=582) + to_title(207)=(from=585,to=584) + to_title(208)=(from=587,to=586) + to_title(209)=(from=589,to=588) + to_title(210)=(from=591,to=590) + to_title(211)=(from=592,to=11375) + to_title(212)=(from=593,to=11373) + to_title(213)=(from=594,to=11376) + to_title(214)=(from=595,to=385) + to_title(215)=(from=596,to=390) + to_title(216)=(from=598,to=393) + to_title(217)=(from=599,to=394) + to_title(218)=(from=601,to=399) + to_title(219)=(from=603,to=400) + to_title(220)=(from=604,to=42923) + to_title(221)=(from=608,to=403) + to_title(222)=(from=609,to=42924) + to_title(223)=(from=611,to=404) + to_title(224)=(from=613,to=42893) + to_title(225)=(from=614,to=42922) + to_title(226)=(from=616,to=407) + to_title(227)=(from=617,to=406) + to_title(228)=(from=618,to=42926) + to_title(229)=(from=619,to=11362) + to_title(230)=(from=620,to=42925) + to_title(231)=(from=623,to=412) + to_title(232)=(from=625,to=11374) + to_title(233)=(from=626,to=413) + to_title(234)=(from=629,to=415) + to_title(235)=(from=637,to=11364) + to_title(236)=(from=640,to=422) + to_title(237)=(from=642,to=42949) + to_title(238)=(from=643,to=425) + to_title(239)=(from=647,to=42929) + to_title(240)=(from=648,to=430) + to_title(241)=(from=649,to=580) + to_title(242)=(from=650,to=433) + to_title(243)=(from=651,to=434) + to_title(244)=(from=652,to=581) + to_title(245)=(from=658,to=439) + to_title(246)=(from=669,to=42930) + to_title(247)=(from=670,to=42928) + to_title(248)=(from=837,to=921) + to_title(249)=(from=881,to=880) + to_title(250)=(from=883,to=882) + to_title(251)=(from=887,to=886) + to_title(252)=(from=891,to=1021) + to_title(253)=(from=892,to=1022) + to_title(254)=(from=893,to=1023) + to_title(255)=(from=940,to=902) + to_title(256)=(from=941,to=904) + to_title(257)=(from=942,to=905) + to_title(258)=(from=943,to=906) + to_title(259)=(from=945,to=913) + to_title(260)=(from=946,to=914) + to_title(261)=(from=947,to=915) + to_title(262)=(from=948,to=916) + to_title(263)=(from=949,to=917) + to_title(264)=(from=950,to=918) + to_title(265)=(from=951,to=919) + to_title(266)=(from=952,to=920) + to_title(267)=(from=953,to=921) + to_title(268)=(from=954,to=922) + to_title(269)=(from=955,to=923) + to_title(270)=(from=956,to=924) + to_title(271)=(from=957,to=925) + to_title(272)=(from=958,to=926) + to_title(273)=(from=959,to=927) + to_title(274)=(from=960,to=928) + to_title(275)=(from=961,to=929) + to_title(276)=(from=962,to=931) + to_title(277)=(from=963,to=931) + to_title(278)=(from=964,to=932) + to_title(279)=(from=965,to=933) + to_title(280)=(from=966,to=934) + to_title(281)=(from=967,to=935) + to_title(282)=(from=968,to=936) + to_title(283)=(from=969,to=937) + to_title(284)=(from=970,to=938) + to_title(285)=(from=971,to=939) + to_title(286)=(from=972,to=908) + to_title(287)=(from=973,to=910) + to_title(288)=(from=974,to=911) + to_title(289)=(from=976,to=914) + to_title(290)=(from=977,to=920) + to_title(291)=(from=981,to=934) + to_title(292)=(from=982,to=928) + to_title(293)=(from=983,to=975) + to_title(294)=(from=985,to=984) + to_title(295)=(from=987,to=986) + to_title(296)=(from=989,to=988) + to_title(297)=(from=991,to=990) + to_title(298)=(from=993,to=992) + to_title(299)=(from=995,to=994) + to_title(300)=(from=997,to=996) + to_title(301)=(from=999,to=998) + to_title(302)=(from=1001,to=1000) + to_title(303)=(from=1003,to=1002) + to_title(304)=(from=1005,to=1004) + to_title(305)=(from=1007,to=1006) + to_title(306)=(from=1008,to=922) + to_title(307)=(from=1009,to=929) + to_title(308)=(from=1010,to=1017) + to_title(309)=(from=1011,to=895) + to_title(310)=(from=1013,to=917) + to_title(311)=(from=1016,to=1015) + to_title(312)=(from=1019,to=1018) + to_title(313)=(from=1072,to=1040) + to_title(314)=(from=1073,to=1041) + to_title(315)=(from=1074,to=1042) + to_title(316)=(from=1075,to=1043) + to_title(317)=(from=1076,to=1044) + to_title(318)=(from=1077,to=1045) + to_title(319)=(from=1078,to=1046) + to_title(320)=(from=1079,to=1047) + to_title(321)=(from=1080,to=1048) + to_title(322)=(from=1081,to=1049) + to_title(323)=(from=1082,to=1050) + to_title(324)=(from=1083,to=1051) + to_title(325)=(from=1084,to=1052) + to_title(326)=(from=1085,to=1053) + to_title(327)=(from=1086,to=1054) + to_title(328)=(from=1087,to=1055) + to_title(329)=(from=1088,to=1056) + to_title(330)=(from=1089,to=1057) + to_title(331)=(from=1090,to=1058) + to_title(332)=(from=1091,to=1059) + to_title(333)=(from=1092,to=1060) + to_title(334)=(from=1093,to=1061) + to_title(335)=(from=1094,to=1062) + to_title(336)=(from=1095,to=1063) + to_title(337)=(from=1096,to=1064) + to_title(338)=(from=1097,to=1065) + to_title(339)=(from=1098,to=1066) + to_title(340)=(from=1099,to=1067) + to_title(341)=(from=1100,to=1068) + to_title(342)=(from=1101,to=1069) + to_title(343)=(from=1102,to=1070) + to_title(344)=(from=1103,to=1071) + to_title(345)=(from=1104,to=1024) + to_title(346)=(from=1105,to=1025) + to_title(347)=(from=1106,to=1026) + to_title(348)=(from=1107,to=1027) + to_title(349)=(from=1108,to=1028) + to_title(350)=(from=1109,to=1029) + to_title(351)=(from=1110,to=1030) + to_title(352)=(from=1111,to=1031) + to_title(353)=(from=1112,to=1032) + to_title(354)=(from=1113,to=1033) + to_title(355)=(from=1114,to=1034) + to_title(356)=(from=1115,to=1035) + to_title(357)=(from=1116,to=1036) + to_title(358)=(from=1117,to=1037) + to_title(359)=(from=1118,to=1038) + to_title(360)=(from=1119,to=1039) + to_title(361)=(from=1121,to=1120) + to_title(362)=(from=1123,to=1122) + to_title(363)=(from=1125,to=1124) + to_title(364)=(from=1127,to=1126) + to_title(365)=(from=1129,to=1128) + to_title(366)=(from=1131,to=1130) + to_title(367)=(from=1133,to=1132) + to_title(368)=(from=1135,to=1134) + to_title(369)=(from=1137,to=1136) + to_title(370)=(from=1139,to=1138) + to_title(371)=(from=1141,to=1140) + to_title(372)=(from=1143,to=1142) + to_title(373)=(from=1145,to=1144) + to_title(374)=(from=1147,to=1146) + to_title(375)=(from=1149,to=1148) + to_title(376)=(from=1151,to=1150) + to_title(377)=(from=1153,to=1152) + to_title(378)=(from=1163,to=1162) + to_title(379)=(from=1165,to=1164) + to_title(380)=(from=1167,to=1166) + to_title(381)=(from=1169,to=1168) + to_title(382)=(from=1171,to=1170) + to_title(383)=(from=1173,to=1172) + to_title(384)=(from=1175,to=1174) + to_title(385)=(from=1177,to=1176) + to_title(386)=(from=1179,to=1178) + to_title(387)=(from=1181,to=1180) + to_title(388)=(from=1183,to=1182) + to_title(389)=(from=1185,to=1184) + to_title(390)=(from=1187,to=1186) + to_title(391)=(from=1189,to=1188) + to_title(392)=(from=1191,to=1190) + to_title(393)=(from=1193,to=1192) + to_title(394)=(from=1195,to=1194) + to_title(395)=(from=1197,to=1196) + to_title(396)=(from=1199,to=1198) + to_title(397)=(from=1201,to=1200) + to_title(398)=(from=1203,to=1202) + to_title(399)=(from=1205,to=1204) + to_title(400)=(from=1207,to=1206) + to_title(401)=(from=1209,to=1208) + to_title(402)=(from=1211,to=1210) + to_title(403)=(from=1213,to=1212) + to_title(404)=(from=1215,to=1214) + to_title(405)=(from=1218,to=1217) + to_title(406)=(from=1220,to=1219) + to_title(407)=(from=1222,to=1221) + to_title(408)=(from=1224,to=1223) + to_title(409)=(from=1226,to=1225) + to_title(410)=(from=1228,to=1227) + to_title(411)=(from=1230,to=1229) + to_title(412)=(from=1231,to=1216) + to_title(413)=(from=1233,to=1232) + to_title(414)=(from=1235,to=1234) + to_title(415)=(from=1237,to=1236) + to_title(416)=(from=1239,to=1238) + to_title(417)=(from=1241,to=1240) + to_title(418)=(from=1243,to=1242) + to_title(419)=(from=1245,to=1244) + to_title(420)=(from=1247,to=1246) + to_title(421)=(from=1249,to=1248) + to_title(422)=(from=1251,to=1250) + to_title(423)=(from=1253,to=1252) + to_title(424)=(from=1255,to=1254) + to_title(425)=(from=1257,to=1256) + to_title(426)=(from=1259,to=1258) + to_title(427)=(from=1261,to=1260) + to_title(428)=(from=1263,to=1262) + to_title(429)=(from=1265,to=1264) + to_title(430)=(from=1267,to=1266) + to_title(431)=(from=1269,to=1268) + to_title(432)=(from=1271,to=1270) + to_title(433)=(from=1273,to=1272) + to_title(434)=(from=1275,to=1274) + to_title(435)=(from=1277,to=1276) + to_title(436)=(from=1279,to=1278) + to_title(437)=(from=1281,to=1280) + to_title(438)=(from=1283,to=1282) + to_title(439)=(from=1285,to=1284) + to_title(440)=(from=1287,to=1286) + to_title(441)=(from=1289,to=1288) + to_title(442)=(from=1291,to=1290) + to_title(443)=(from=1293,to=1292) + to_title(444)=(from=1295,to=1294) + to_title(445)=(from=1297,to=1296) + to_title(446)=(from=1299,to=1298) + to_title(447)=(from=1301,to=1300) + to_title(448)=(from=1303,to=1302) + to_title(449)=(from=1305,to=1304) + to_title(450)=(from=1307,to=1306) + to_title(451)=(from=1309,to=1308) + to_title(452)=(from=1311,to=1310) + to_title(453)=(from=1313,to=1312) + to_title(454)=(from=1315,to=1314) + to_title(455)=(from=1317,to=1316) + to_title(456)=(from=1319,to=1318) + to_title(457)=(from=1321,to=1320) + to_title(458)=(from=1323,to=1322) + to_title(459)=(from=1325,to=1324) + to_title(460)=(from=1327,to=1326) + to_title(461)=(from=1377,to=1329) + to_title(462)=(from=1378,to=1330) + to_title(463)=(from=1379,to=1331) + to_title(464)=(from=1380,to=1332) + to_title(465)=(from=1381,to=1333) + to_title(466)=(from=1382,to=1334) + to_title(467)=(from=1383,to=1335) + to_title(468)=(from=1384,to=1336) + to_title(469)=(from=1385,to=1337) + to_title(470)=(from=1386,to=1338) + to_title(471)=(from=1387,to=1339) + to_title(472)=(from=1388,to=1340) + to_title(473)=(from=1389,to=1341) + to_title(474)=(from=1390,to=1342) + to_title(475)=(from=1391,to=1343) + to_title(476)=(from=1392,to=1344) + to_title(477)=(from=1393,to=1345) + to_title(478)=(from=1394,to=1346) + to_title(479)=(from=1395,to=1347) + to_title(480)=(from=1396,to=1348) + to_title(481)=(from=1397,to=1349) + to_title(482)=(from=1398,to=1350) + to_title(483)=(from=1399,to=1351) + to_title(484)=(from=1400,to=1352) + to_title(485)=(from=1401,to=1353) + to_title(486)=(from=1402,to=1354) + to_title(487)=(from=1403,to=1355) + to_title(488)=(from=1404,to=1356) + to_title(489)=(from=1405,to=1357) + to_title(490)=(from=1406,to=1358) + to_title(491)=(from=1407,to=1359) + to_title(492)=(from=1408,to=1360) + to_title(493)=(from=1409,to=1361) + to_title(494)=(from=1410,to=1362) + to_title(495)=(from=1411,to=1363) + to_title(496)=(from=1412,to=1364) + to_title(497)=(from=1413,to=1365) + to_title(498)=(from=1414,to=1366) + to_title(499)=(from=4304,to=4304) + to_title(500)=(from=4305,to=4305) + to_title(501)=(from=4306,to=4306) + to_title(502)=(from=4307,to=4307) + to_title(503)=(from=4308,to=4308) + to_title(504)=(from=4309,to=4309) + to_title(505)=(from=4310,to=4310) + to_title(506)=(from=4311,to=4311) + to_title(507)=(from=4312,to=4312) + to_title(508)=(from=4313,to=4313) + to_title(509)=(from=4314,to=4314) + to_title(510)=(from=4315,to=4315) + to_title(511)=(from=4316,to=4316) + to_title(512)=(from=4317,to=4317) + to_title(513)=(from=4318,to=4318) + to_title(514)=(from=4319,to=4319) + to_title(515)=(from=4320,to=4320) + to_title(516)=(from=4321,to=4321) + to_title(517)=(from=4322,to=4322) + to_title(518)=(from=4323,to=4323) + to_title(519)=(from=4324,to=4324) + to_title(520)=(from=4325,to=4325) + to_title(521)=(from=4326,to=4326) + to_title(522)=(from=4327,to=4327) + to_title(523)=(from=4328,to=4328) + to_title(524)=(from=4329,to=4329) + to_title(525)=(from=4330,to=4330) + to_title(526)=(from=4331,to=4331) + to_title(527)=(from=4332,to=4332) + to_title(528)=(from=4333,to=4333) + to_title(529)=(from=4334,to=4334) + to_title(530)=(from=4335,to=4335) + to_title(531)=(from=4336,to=4336) + to_title(532)=(from=4337,to=4337) + to_title(533)=(from=4338,to=4338) + to_title(534)=(from=4339,to=4339) + to_title(535)=(from=4340,to=4340) + to_title(536)=(from=4341,to=4341) + to_title(537)=(from=4342,to=4342) + to_title(538)=(from=4343,to=4343) + to_title(539)=(from=4344,to=4344) + to_title(540)=(from=4345,to=4345) + to_title(541)=(from=4346,to=4346) + to_title(542)=(from=4349,to=4349) + to_title(543)=(from=4350,to=4350) + to_title(544)=(from=4351,to=4351) + to_title(545)=(from=5112,to=5104) + to_title(546)=(from=5113,to=5105) + to_title(547)=(from=5114,to=5106) + to_title(548)=(from=5115,to=5107) + to_title(549)=(from=5116,to=5108) + to_title(550)=(from=5117,to=5109) + to_title(551)=(from=7296,to=1042) + to_title(552)=(from=7297,to=1044) + to_title(553)=(from=7298,to=1054) + to_title(554)=(from=7299,to=1057) + to_title(555)=(from=7300,to=1058) + to_title(556)=(from=7301,to=1058) + to_title(557)=(from=7302,to=1066) + to_title(558)=(from=7303,to=1122) + to_title(559)=(from=7304,to=42570) + to_title(560)=(from=7545,to=42877) + to_title(561)=(from=7549,to=11363) + to_title(562)=(from=7566,to=42950) + to_title(563)=(from=7681,to=7680) + to_title(564)=(from=7683,to=7682) + to_title(565)=(from=7685,to=7684) + to_title(566)=(from=7687,to=7686) + to_title(567)=(from=7689,to=7688) + to_title(568)=(from=7691,to=7690) + to_title(569)=(from=7693,to=7692) + to_title(570)=(from=7695,to=7694) + to_title(571)=(from=7697,to=7696) + to_title(572)=(from=7699,to=7698) + to_title(573)=(from=7701,to=7700) + to_title(574)=(from=7703,to=7702) + to_title(575)=(from=7705,to=7704) + to_title(576)=(from=7707,to=7706) + to_title(577)=(from=7709,to=7708) + to_title(578)=(from=7711,to=7710) + to_title(579)=(from=7713,to=7712) + to_title(580)=(from=7715,to=7714) + to_title(581)=(from=7717,to=7716) + to_title(582)=(from=7719,to=7718) + to_title(583)=(from=7721,to=7720) + to_title(584)=(from=7723,to=7722) + to_title(585)=(from=7725,to=7724) + to_title(586)=(from=7727,to=7726) + to_title(587)=(from=7729,to=7728) + to_title(588)=(from=7731,to=7730) + to_title(589)=(from=7733,to=7732) + to_title(590)=(from=7735,to=7734) + to_title(591)=(from=7737,to=7736) + to_title(592)=(from=7739,to=7738) + to_title(593)=(from=7741,to=7740) + to_title(594)=(from=7743,to=7742) + to_title(595)=(from=7745,to=7744) + to_title(596)=(from=7747,to=7746) + to_title(597)=(from=7749,to=7748) + to_title(598)=(from=7751,to=7750) + to_title(599)=(from=7753,to=7752) + to_title(600)=(from=7755,to=7754) + to_title(601)=(from=7757,to=7756) + to_title(602)=(from=7759,to=7758) + to_title(603)=(from=7761,to=7760) + to_title(604)=(from=7763,to=7762) + to_title(605)=(from=7765,to=7764) + to_title(606)=(from=7767,to=7766) + to_title(607)=(from=7769,to=7768) + to_title(608)=(from=7771,to=7770) + to_title(609)=(from=7773,to=7772) + to_title(610)=(from=7775,to=7774) + to_title(611)=(from=7777,to=7776) + to_title(612)=(from=7779,to=7778) + to_title(613)=(from=7781,to=7780) + to_title(614)=(from=7783,to=7782) + to_title(615)=(from=7785,to=7784) + to_title(616)=(from=7787,to=7786) + to_title(617)=(from=7789,to=7788) + to_title(618)=(from=7791,to=7790) + to_title(619)=(from=7793,to=7792) + to_title(620)=(from=7795,to=7794) + to_title(621)=(from=7797,to=7796) + to_title(622)=(from=7799,to=7798) + to_title(623)=(from=7801,to=7800) + to_title(624)=(from=7803,to=7802) + to_title(625)=(from=7805,to=7804) + to_title(626)=(from=7807,to=7806) + to_title(627)=(from=7809,to=7808) + to_title(628)=(from=7811,to=7810) + to_title(629)=(from=7813,to=7812) + to_title(630)=(from=7815,to=7814) + to_title(631)=(from=7817,to=7816) + to_title(632)=(from=7819,to=7818) + to_title(633)=(from=7821,to=7820) + to_title(634)=(from=7823,to=7822) + to_title(635)=(from=7825,to=7824) + to_title(636)=(from=7827,to=7826) + to_title(637)=(from=7829,to=7828) + to_title(638)=(from=7835,to=7776) + to_title(639)=(from=7841,to=7840) + to_title(640)=(from=7843,to=7842) + to_title(641)=(from=7845,to=7844) + to_title(642)=(from=7847,to=7846) + to_title(643)=(from=7849,to=7848) + to_title(644)=(from=7851,to=7850) + to_title(645)=(from=7853,to=7852) + to_title(646)=(from=7855,to=7854) + to_title(647)=(from=7857,to=7856) + to_title(648)=(from=7859,to=7858) + to_title(649)=(from=7861,to=7860) + to_title(650)=(from=7863,to=7862) + to_title(651)=(from=7865,to=7864) + to_title(652)=(from=7867,to=7866) + to_title(653)=(from=7869,to=7868) + to_title(654)=(from=7871,to=7870) + to_title(655)=(from=7873,to=7872) + to_title(656)=(from=7875,to=7874) + to_title(657)=(from=7877,to=7876) + to_title(658)=(from=7879,to=7878) + to_title(659)=(from=7881,to=7880) + to_title(660)=(from=7883,to=7882) + to_title(661)=(from=7885,to=7884) + to_title(662)=(from=7887,to=7886) + to_title(663)=(from=7889,to=7888) + to_title(664)=(from=7891,to=7890) + to_title(665)=(from=7893,to=7892) + to_title(666)=(from=7895,to=7894) + to_title(667)=(from=7897,to=7896) + to_title(668)=(from=7899,to=7898) + to_title(669)=(from=7901,to=7900) + to_title(670)=(from=7903,to=7902) + to_title(671)=(from=7905,to=7904) + to_title(672)=(from=7907,to=7906) + to_title(673)=(from=7909,to=7908) + to_title(674)=(from=7911,to=7910) + to_title(675)=(from=7913,to=7912) + to_title(676)=(from=7915,to=7914) + to_title(677)=(from=7917,to=7916) + to_title(678)=(from=7919,to=7918) + to_title(679)=(from=7921,to=7920) + to_title(680)=(from=7923,to=7922) + to_title(681)=(from=7925,to=7924) + to_title(682)=(from=7927,to=7926) + to_title(683)=(from=7929,to=7928) + to_title(684)=(from=7931,to=7930) + to_title(685)=(from=7933,to=7932) + to_title(686)=(from=7935,to=7934) + to_title(687)=(from=7936,to=7944) + to_title(688)=(from=7937,to=7945) + to_title(689)=(from=7938,to=7946) + to_title(690)=(from=7939,to=7947) + to_title(691)=(from=7940,to=7948) + to_title(692)=(from=7941,to=7949) + to_title(693)=(from=7942,to=7950) + to_title(694)=(from=7943,to=7951) + to_title(695)=(from=7952,to=7960) + to_title(696)=(from=7953,to=7961) + to_title(697)=(from=7954,to=7962) + to_title(698)=(from=7955,to=7963) + to_title(699)=(from=7956,to=7964) + to_title(700)=(from=7957,to=7965) + to_title(701)=(from=7968,to=7976) + to_title(702)=(from=7969,to=7977) + to_title(703)=(from=7970,to=7978) + to_title(704)=(from=7971,to=7979) + to_title(705)=(from=7972,to=7980) + to_title(706)=(from=7973,to=7981) + to_title(707)=(from=7974,to=7982) + to_title(708)=(from=7975,to=7983) + to_title(709)=(from=7984,to=7992) + to_title(710)=(from=7985,to=7993) + to_title(711)=(from=7986,to=7994) + to_title(712)=(from=7987,to=7995) + to_title(713)=(from=7988,to=7996) + to_title(714)=(from=7989,to=7997) + to_title(715)=(from=7990,to=7998) + to_title(716)=(from=7991,to=7999) + to_title(717)=(from=8000,to=8008) + to_title(718)=(from=8001,to=8009) + to_title(719)=(from=8002,to=8010) + to_title(720)=(from=8003,to=8011) + to_title(721)=(from=8004,to=8012) + to_title(722)=(from=8005,to=8013) + to_title(723)=(from=8017,to=8025) + to_title(724)=(from=8019,to=8027) + to_title(725)=(from=8021,to=8029) + to_title(726)=(from=8023,to=8031) + to_title(727)=(from=8032,to=8040) + to_title(728)=(from=8033,to=8041) + to_title(729)=(from=8034,to=8042) + to_title(730)=(from=8035,to=8043) + to_title(731)=(from=8036,to=8044) + to_title(732)=(from=8037,to=8045) + to_title(733)=(from=8038,to=8046) + to_title(734)=(from=8039,to=8047) + to_title(735)=(from=8048,to=8122) + to_title(736)=(from=8049,to=8123) + to_title(737)=(from=8050,to=8136) + to_title(738)=(from=8051,to=8137) + to_title(739)=(from=8052,to=8138) + to_title(740)=(from=8053,to=8139) + to_title(741)=(from=8054,to=8154) + to_title(742)=(from=8055,to=8155) + to_title(743)=(from=8056,to=8184) + to_title(744)=(from=8057,to=8185) + to_title(745)=(from=8058,to=8170) + to_title(746)=(from=8059,to=8171) + to_title(747)=(from=8060,to=8186) + to_title(748)=(from=8061,to=8187) + to_title(749)=(from=8064,to=8072) + to_title(750)=(from=8065,to=8073) + to_title(751)=(from=8066,to=8074) + to_title(752)=(from=8067,to=8075) + to_title(753)=(from=8068,to=8076) + to_title(754)=(from=8069,to=8077) + to_title(755)=(from=8070,to=8078) + to_title(756)=(from=8071,to=8079) + to_title(757)=(from=8080,to=8088) + to_title(758)=(from=8081,to=8089) + to_title(759)=(from=8082,to=8090) + to_title(760)=(from=8083,to=8091) + to_title(761)=(from=8084,to=8092) + to_title(762)=(from=8085,to=8093) + to_title(763)=(from=8086,to=8094) + to_title(764)=(from=8087,to=8095) + to_title(765)=(from=8096,to=8104) + to_title(766)=(from=8097,to=8105) + to_title(767)=(from=8098,to=8106) + to_title(768)=(from=8099,to=8107) + to_title(769)=(from=8100,to=8108) + to_title(770)=(from=8101,to=8109) + to_title(771)=(from=8102,to=8110) + to_title(772)=(from=8103,to=8111) + to_title(773)=(from=8112,to=8120) + to_title(774)=(from=8113,to=8121) + to_title(775)=(from=8115,to=8124) + to_title(776)=(from=8126,to=921) + to_title(777)=(from=8131,to=8140) + to_title(778)=(from=8144,to=8152) + to_title(779)=(from=8145,to=8153) + to_title(780)=(from=8160,to=8168) + to_title(781)=(from=8161,to=8169) + to_title(782)=(from=8165,to=8172) + to_title(783)=(from=8179,to=8188) + to_title(784)=(from=8526,to=8498) + to_title(785)=(from=8560,to=8544) + to_title(786)=(from=8561,to=8545) + to_title(787)=(from=8562,to=8546) + to_title(788)=(from=8563,to=8547) + to_title(789)=(from=8564,to=8548) + to_title(790)=(from=8565,to=8549) + to_title(791)=(from=8566,to=8550) + to_title(792)=(from=8567,to=8551) + to_title(793)=(from=8568,to=8552) + to_title(794)=(from=8569,to=8553) + to_title(795)=(from=8570,to=8554) + to_title(796)=(from=8571,to=8555) + to_title(797)=(from=8572,to=8556) + to_title(798)=(from=8573,to=8557) + to_title(799)=(from=8574,to=8558) + to_title(800)=(from=8575,to=8559) + to_title(801)=(from=8580,to=8579) + to_title(802)=(from=9424,to=9398) + to_title(803)=(from=9425,to=9399) + to_title(804)=(from=9426,to=9400) + to_title(805)=(from=9427,to=9401) + to_title(806)=(from=9428,to=9402) + to_title(807)=(from=9429,to=9403) + to_title(808)=(from=9430,to=9404) + to_title(809)=(from=9431,to=9405) + to_title(810)=(from=9432,to=9406) + to_title(811)=(from=9433,to=9407) + to_title(812)=(from=9434,to=9408) + to_title(813)=(from=9435,to=9409) + to_title(814)=(from=9436,to=9410) + to_title(815)=(from=9437,to=9411) + to_title(816)=(from=9438,to=9412) + to_title(817)=(from=9439,to=9413) + to_title(818)=(from=9440,to=9414) + to_title(819)=(from=9441,to=9415) + to_title(820)=(from=9442,to=9416) + to_title(821)=(from=9443,to=9417) + to_title(822)=(from=9444,to=9418) + to_title(823)=(from=9445,to=9419) + to_title(824)=(from=9446,to=9420) + to_title(825)=(from=9447,to=9421) + to_title(826)=(from=9448,to=9422) + to_title(827)=(from=9449,to=9423) + to_title(828)=(from=11312,to=11264) + to_title(829)=(from=11313,to=11265) + to_title(830)=(from=11314,to=11266) + to_title(831)=(from=11315,to=11267) + to_title(832)=(from=11316,to=11268) + to_title(833)=(from=11317,to=11269) + to_title(834)=(from=11318,to=11270) + to_title(835)=(from=11319,to=11271) + to_title(836)=(from=11320,to=11272) + to_title(837)=(from=11321,to=11273) + to_title(838)=(from=11322,to=11274) + to_title(839)=(from=11323,to=11275) + to_title(840)=(from=11324,to=11276) + to_title(841)=(from=11325,to=11277) + to_title(842)=(from=11326,to=11278) + to_title(843)=(from=11327,to=11279) + to_title(844)=(from=11328,to=11280) + to_title(845)=(from=11329,to=11281) + to_title(846)=(from=11330,to=11282) + to_title(847)=(from=11331,to=11283) + to_title(848)=(from=11332,to=11284) + to_title(849)=(from=11333,to=11285) + to_title(850)=(from=11334,to=11286) + to_title(851)=(from=11335,to=11287) + to_title(852)=(from=11336,to=11288) + to_title(853)=(from=11337,to=11289) + to_title(854)=(from=11338,to=11290) + to_title(855)=(from=11339,to=11291) + to_title(856)=(from=11340,to=11292) + to_title(857)=(from=11341,to=11293) + to_title(858)=(from=11342,to=11294) + to_title(859)=(from=11343,to=11295) + to_title(860)=(from=11344,to=11296) + to_title(861)=(from=11345,to=11297) + to_title(862)=(from=11346,to=11298) + to_title(863)=(from=11347,to=11299) + to_title(864)=(from=11348,to=11300) + to_title(865)=(from=11349,to=11301) + to_title(866)=(from=11350,to=11302) + to_title(867)=(from=11351,to=11303) + to_title(868)=(from=11352,to=11304) + to_title(869)=(from=11353,to=11305) + to_title(870)=(from=11354,to=11306) + to_title(871)=(from=11355,to=11307) + to_title(872)=(from=11356,to=11308) + to_title(873)=(from=11357,to=11309) + to_title(874)=(from=11358,to=11310) + to_title(875)=(from=11361,to=11360) + to_title(876)=(from=11365,to=570) + to_title(877)=(from=11366,to=574) + to_title(878)=(from=11368,to=11367) + to_title(879)=(from=11370,to=11369) + to_title(880)=(from=11372,to=11371) + to_title(881)=(from=11379,to=11378) + to_title(882)=(from=11382,to=11381) + to_title(883)=(from=11393,to=11392) + to_title(884)=(from=11395,to=11394) + to_title(885)=(from=11397,to=11396) + to_title(886)=(from=11399,to=11398) + to_title(887)=(from=11401,to=11400) + to_title(888)=(from=11403,to=11402) + to_title(889)=(from=11405,to=11404) + to_title(890)=(from=11407,to=11406) + to_title(891)=(from=11409,to=11408) + to_title(892)=(from=11411,to=11410) + to_title(893)=(from=11413,to=11412) + to_title(894)=(from=11415,to=11414) + to_title(895)=(from=11417,to=11416) + to_title(896)=(from=11419,to=11418) + to_title(897)=(from=11421,to=11420) + to_title(898)=(from=11423,to=11422) + to_title(899)=(from=11425,to=11424) + to_title(900)=(from=11427,to=11426) + to_title(901)=(from=11429,to=11428) + to_title(902)=(from=11431,to=11430) + to_title(903)=(from=11433,to=11432) + to_title(904)=(from=11435,to=11434) + to_title(905)=(from=11437,to=11436) + to_title(906)=(from=11439,to=11438) + to_title(907)=(from=11441,to=11440) + to_title(908)=(from=11443,to=11442) + to_title(909)=(from=11445,to=11444) + to_title(910)=(from=11447,to=11446) + to_title(911)=(from=11449,to=11448) + to_title(912)=(from=11451,to=11450) + to_title(913)=(from=11453,to=11452) + to_title(914)=(from=11455,to=11454) + to_title(915)=(from=11457,to=11456) + to_title(916)=(from=11459,to=11458) + to_title(917)=(from=11461,to=11460) + to_title(918)=(from=11463,to=11462) + to_title(919)=(from=11465,to=11464) + to_title(920)=(from=11467,to=11466) + to_title(921)=(from=11469,to=11468) + to_title(922)=(from=11471,to=11470) + to_title(923)=(from=11473,to=11472) + to_title(924)=(from=11475,to=11474) + to_title(925)=(from=11477,to=11476) + to_title(926)=(from=11479,to=11478) + to_title(927)=(from=11481,to=11480) + to_title(928)=(from=11483,to=11482) + to_title(929)=(from=11485,to=11484) + to_title(930)=(from=11487,to=11486) + to_title(931)=(from=11489,to=11488) + to_title(932)=(from=11491,to=11490) + to_title(933)=(from=11500,to=11499) + to_title(934)=(from=11502,to=11501) + to_title(935)=(from=11507,to=11506) + to_title(936)=(from=11520,to=4256) + to_title(937)=(from=11521,to=4257) + to_title(938)=(from=11522,to=4258) + to_title(939)=(from=11523,to=4259) + to_title(940)=(from=11524,to=4260) + to_title(941)=(from=11525,to=4261) + to_title(942)=(from=11526,to=4262) + to_title(943)=(from=11527,to=4263) + to_title(944)=(from=11528,to=4264) + to_title(945)=(from=11529,to=4265) + to_title(946)=(from=11530,to=4266) + to_title(947)=(from=11531,to=4267) + to_title(948)=(from=11532,to=4268) + to_title(949)=(from=11533,to=4269) + to_title(950)=(from=11534,to=4270) + to_title(951)=(from=11535,to=4271) + to_title(952)=(from=11536,to=4272) + to_title(953)=(from=11537,to=4273) + to_title(954)=(from=11538,to=4274) + to_title(955)=(from=11539,to=4275) + to_title(956)=(from=11540,to=4276) + to_title(957)=(from=11541,to=4277) + to_title(958)=(from=11542,to=4278) + to_title(959)=(from=11543,to=4279) + to_title(960)=(from=11544,to=4280) + to_title(961)=(from=11545,to=4281) + to_title(962)=(from=11546,to=4282) + to_title(963)=(from=11547,to=4283) + to_title(964)=(from=11548,to=4284) + to_title(965)=(from=11549,to=4285) + to_title(966)=(from=11550,to=4286) + to_title(967)=(from=11551,to=4287) + to_title(968)=(from=11552,to=4288) + to_title(969)=(from=11553,to=4289) + to_title(970)=(from=11554,to=4290) + to_title(971)=(from=11555,to=4291) + to_title(972)=(from=11556,to=4292) + to_title(973)=(from=11557,to=4293) + to_title(974)=(from=11559,to=4295) + to_title(975)=(from=11565,to=4301) + to_title(976)=(from=42561,to=42560) + to_title(977)=(from=42563,to=42562) + to_title(978)=(from=42565,to=42564) + to_title(979)=(from=42567,to=42566) + to_title(980)=(from=42569,to=42568) + to_title(981)=(from=42571,to=42570) + to_title(982)=(from=42573,to=42572) + to_title(983)=(from=42575,to=42574) + to_title(984)=(from=42577,to=42576) + to_title(985)=(from=42579,to=42578) + to_title(986)=(from=42581,to=42580) + to_title(987)=(from=42583,to=42582) + to_title(988)=(from=42585,to=42584) + to_title(989)=(from=42587,to=42586) + to_title(990)=(from=42589,to=42588) + to_title(991)=(from=42591,to=42590) + to_title(992)=(from=42593,to=42592) + to_title(993)=(from=42595,to=42594) + to_title(994)=(from=42597,to=42596) + to_title(995)=(from=42599,to=42598) + to_title(996)=(from=42601,to=42600) + to_title(997)=(from=42603,to=42602) + to_title(998)=(from=42605,to=42604) + to_title(999)=(from=42625,to=42624) + to_title(1000)=(from=42627,to=42626) + to_title(1001)=(from=42629,to=42628) + to_title(1002)=(from=42631,to=42630) + to_title(1003)=(from=42633,to=42632) + to_title(1004)=(from=42635,to=42634) + to_title(1005)=(from=42637,to=42636) + to_title(1006)=(from=42639,to=42638) + to_title(1007)=(from=42641,to=42640) + to_title(1008)=(from=42643,to=42642) + to_title(1009)=(from=42645,to=42644) + to_title(1010)=(from=42647,to=42646) + to_title(1011)=(from=42649,to=42648) + to_title(1012)=(from=42651,to=42650) + to_title(1013)=(from=42787,to=42786) + to_title(1014)=(from=42789,to=42788) + to_title(1015)=(from=42791,to=42790) + to_title(1016)=(from=42793,to=42792) + to_title(1017)=(from=42795,to=42794) + to_title(1018)=(from=42797,to=42796) + to_title(1019)=(from=42799,to=42798) + to_title(1020)=(from=42803,to=42802) + to_title(1021)=(from=42805,to=42804) + to_title(1022)=(from=42807,to=42806) + to_title(1023)=(from=42809,to=42808) + to_title(1024)=(from=42811,to=42810) + to_title(1025)=(from=42813,to=42812) + to_title(1026)=(from=42815,to=42814) + to_title(1027)=(from=42817,to=42816) + to_title(1028)=(from=42819,to=42818) + to_title(1029)=(from=42821,to=42820) + to_title(1030)=(from=42823,to=42822) + to_title(1031)=(from=42825,to=42824) + to_title(1032)=(from=42827,to=42826) + to_title(1033)=(from=42829,to=42828) + to_title(1034)=(from=42831,to=42830) + to_title(1035)=(from=42833,to=42832) + to_title(1036)=(from=42835,to=42834) + to_title(1037)=(from=42837,to=42836) + to_title(1038)=(from=42839,to=42838) + to_title(1039)=(from=42841,to=42840) + to_title(1040)=(from=42843,to=42842) + to_title(1041)=(from=42845,to=42844) + to_title(1042)=(from=42847,to=42846) + to_title(1043)=(from=42849,to=42848) + to_title(1044)=(from=42851,to=42850) + to_title(1045)=(from=42853,to=42852) + to_title(1046)=(from=42855,to=42854) + to_title(1047)=(from=42857,to=42856) + to_title(1048)=(from=42859,to=42858) + to_title(1049)=(from=42861,to=42860) + to_title(1050)=(from=42863,to=42862) + to_title(1051)=(from=42874,to=42873) + to_title(1052)=(from=42876,to=42875) + to_title(1053)=(from=42879,to=42878) + to_title(1054)=(from=42881,to=42880) + to_title(1055)=(from=42883,to=42882) + to_title(1056)=(from=42885,to=42884) + to_title(1057)=(from=42887,to=42886) + to_title(1058)=(from=42892,to=42891) + to_title(1059)=(from=42897,to=42896) + to_title(1060)=(from=42899,to=42898) + to_title(1061)=(from=42900,to=42948) + to_title(1062)=(from=42903,to=42902) + to_title(1063)=(from=42905,to=42904) + to_title(1064)=(from=42907,to=42906) + to_title(1065)=(from=42909,to=42908) + to_title(1066)=(from=42911,to=42910) + to_title(1067)=(from=42913,to=42912) + to_title(1068)=(from=42915,to=42914) + to_title(1069)=(from=42917,to=42916) + to_title(1070)=(from=42919,to=42918) + to_title(1071)=(from=42921,to=42920) + to_title(1072)=(from=42933,to=42932) + to_title(1073)=(from=42935,to=42934) + to_title(1074)=(from=42937,to=42936) + to_title(1075)=(from=42939,to=42938) + to_title(1076)=(from=42941,to=42940) + to_title(1077)=(from=42943,to=42942) + to_title(1078)=(from=42947,to=42946) + to_title(1079)=(from=42952,to=42951) + to_title(1080)=(from=42954,to=42953) + to_title(1081)=(from=42998,to=42997) + to_title(1082)=(from=43859,to=42931) + to_title(1083)=(from=43888,to=5024) + to_title(1084)=(from=43889,to=5025) + to_title(1085)=(from=43890,to=5026) + to_title(1086)=(from=43891,to=5027) + to_title(1087)=(from=43892,to=5028) + to_title(1088)=(from=43893,to=5029) + to_title(1089)=(from=43894,to=5030) + to_title(1090)=(from=43895,to=5031) + to_title(1091)=(from=43896,to=5032) + to_title(1092)=(from=43897,to=5033) + to_title(1093)=(from=43898,to=5034) + to_title(1094)=(from=43899,to=5035) + to_title(1095)=(from=43900,to=5036) + to_title(1096)=(from=43901,to=5037) + to_title(1097)=(from=43902,to=5038) + to_title(1098)=(from=43903,to=5039) + to_title(1099)=(from=43904,to=5040) + to_title(1100)=(from=43905,to=5041) + to_title(1101)=(from=43906,to=5042) + to_title(1102)=(from=43907,to=5043) + to_title(1103)=(from=43908,to=5044) + to_title(1104)=(from=43909,to=5045) + to_title(1105)=(from=43910,to=5046) + to_title(1106)=(from=43911,to=5047) + to_title(1107)=(from=43912,to=5048) + to_title(1108)=(from=43913,to=5049) + to_title(1109)=(from=43914,to=5050) + to_title(1110)=(from=43915,to=5051) + to_title(1111)=(from=43916,to=5052) + to_title(1112)=(from=43917,to=5053) + to_title(1113)=(from=43918,to=5054) + to_title(1114)=(from=43919,to=5055) + to_title(1115)=(from=43920,to=5056) + to_title(1116)=(from=43921,to=5057) + to_title(1117)=(from=43922,to=5058) + to_title(1118)=(from=43923,to=5059) + to_title(1119)=(from=43924,to=5060) + to_title(1120)=(from=43925,to=5061) + to_title(1121)=(from=43926,to=5062) + to_title(1122)=(from=43927,to=5063) + to_title(1123)=(from=43928,to=5064) + to_title(1124)=(from=43929,to=5065) + to_title(1125)=(from=43930,to=5066) + to_title(1126)=(from=43931,to=5067) + to_title(1127)=(from=43932,to=5068) + to_title(1128)=(from=43933,to=5069) + to_title(1129)=(from=43934,to=5070) + to_title(1130)=(from=43935,to=5071) + to_title(1131)=(from=43936,to=5072) + to_title(1132)=(from=43937,to=5073) + to_title(1133)=(from=43938,to=5074) + to_title(1134)=(from=43939,to=5075) + to_title(1135)=(from=43940,to=5076) + to_title(1136)=(from=43941,to=5077) + to_title(1137)=(from=43942,to=5078) + to_title(1138)=(from=43943,to=5079) + to_title(1139)=(from=43944,to=5080) + to_title(1140)=(from=43945,to=5081) + to_title(1141)=(from=43946,to=5082) + to_title(1142)=(from=43947,to=5083) + to_title(1143)=(from=43948,to=5084) + to_title(1144)=(from=43949,to=5085) + to_title(1145)=(from=43950,to=5086) + to_title(1146)=(from=43951,to=5087) + to_title(1147)=(from=43952,to=5088) + to_title(1148)=(from=43953,to=5089) + to_title(1149)=(from=43954,to=5090) + to_title(1150)=(from=43955,to=5091) + to_title(1151)=(from=43956,to=5092) + to_title(1152)=(from=43957,to=5093) + to_title(1153)=(from=43958,to=5094) + to_title(1154)=(from=43959,to=5095) + to_title(1155)=(from=43960,to=5096) + to_title(1156)=(from=43961,to=5097) + to_title(1157)=(from=43962,to=5098) + to_title(1158)=(from=43963,to=5099) + to_title(1159)=(from=43964,to=5100) + to_title(1160)=(from=43965,to=5101) + to_title(1161)=(from=43966,to=5102) + to_title(1162)=(from=43967,to=5103) + to_title(1163)=(from=65345,to=65313) + to_title(1164)=(from=65346,to=65314) + to_title(1165)=(from=65347,to=65315) + to_title(1166)=(from=65348,to=65316) + to_title(1167)=(from=65349,to=65317) + to_title(1168)=(from=65350,to=65318) + to_title(1169)=(from=65351,to=65319) + to_title(1170)=(from=65352,to=65320) + to_title(1171)=(from=65353,to=65321) + to_title(1172)=(from=65354,to=65322) + to_title(1173)=(from=65355,to=65323) + to_title(1174)=(from=65356,to=65324) + to_title(1175)=(from=65357,to=65325) + to_title(1176)=(from=65358,to=65326) + to_title(1177)=(from=65359,to=65327) + to_title(1178)=(from=65360,to=65328) + to_title(1179)=(from=65361,to=65329) + to_title(1180)=(from=65362,to=65330) + to_title(1181)=(from=65363,to=65331) + to_title(1182)=(from=65364,to=65332) + to_title(1183)=(from=65365,to=65333) + to_title(1184)=(from=65366,to=65334) + to_title(1185)=(from=65367,to=65335) + to_title(1186)=(from=65368,to=65336) + to_title(1187)=(from=65369,to=65337) + to_title(1188)=(from=65370,to=65338) + to_title(1189)=(from=66600,to=66560) + to_title(1190)=(from=66601,to=66561) + to_title(1191)=(from=66602,to=66562) + to_title(1192)=(from=66603,to=66563) + to_title(1193)=(from=66604,to=66564) + to_title(1194)=(from=66605,to=66565) + to_title(1195)=(from=66606,to=66566) + to_title(1196)=(from=66607,to=66567) + to_title(1197)=(from=66608,to=66568) + to_title(1198)=(from=66609,to=66569) + to_title(1199)=(from=66610,to=66570) + to_title(1200)=(from=66611,to=66571) + to_title(1201)=(from=66612,to=66572) + to_title(1202)=(from=66613,to=66573) + to_title(1203)=(from=66614,to=66574) + to_title(1204)=(from=66615,to=66575) + to_title(1205)=(from=66616,to=66576) + to_title(1206)=(from=66617,to=66577) + to_title(1207)=(from=66618,to=66578) + to_title(1208)=(from=66619,to=66579) + to_title(1209)=(from=66620,to=66580) + to_title(1210)=(from=66621,to=66581) + to_title(1211)=(from=66622,to=66582) + to_title(1212)=(from=66623,to=66583) + to_title(1213)=(from=66624,to=66584) + to_title(1214)=(from=66625,to=66585) + to_title(1215)=(from=66626,to=66586) + to_title(1216)=(from=66627,to=66587) + to_title(1217)=(from=66628,to=66588) + to_title(1218)=(from=66629,to=66589) + to_title(1219)=(from=66630,to=66590) + to_title(1220)=(from=66631,to=66591) + to_title(1221)=(from=66632,to=66592) + to_title(1222)=(from=66633,to=66593) + to_title(1223)=(from=66634,to=66594) + to_title(1224)=(from=66635,to=66595) + to_title(1225)=(from=66636,to=66596) + to_title(1226)=(from=66637,to=66597) + to_title(1227)=(from=66638,to=66598) + to_title(1228)=(from=66639,to=66599) + to_title(1229)=(from=66776,to=66736) + to_title(1230)=(from=66777,to=66737) + to_title(1231)=(from=66778,to=66738) + to_title(1232)=(from=66779,to=66739) + to_title(1233)=(from=66780,to=66740) + to_title(1234)=(from=66781,to=66741) + to_title(1235)=(from=66782,to=66742) + to_title(1236)=(from=66783,to=66743) + to_title(1237)=(from=66784,to=66744) + to_title(1238)=(from=66785,to=66745) + to_title(1239)=(from=66786,to=66746) + to_title(1240)=(from=66787,to=66747) + to_title(1241)=(from=66788,to=66748) + to_title(1242)=(from=66789,to=66749) + to_title(1243)=(from=66790,to=66750) + to_title(1244)=(from=66791,to=66751) + to_title(1245)=(from=66792,to=66752) + to_title(1246)=(from=66793,to=66753) + to_title(1247)=(from=66794,to=66754) + to_title(1248)=(from=66795,to=66755) + to_title(1249)=(from=66796,to=66756) + to_title(1250)=(from=66797,to=66757) + to_title(1251)=(from=66798,to=66758) + to_title(1252)=(from=66799,to=66759) + to_title(1253)=(from=66800,to=66760) + to_title(1254)=(from=66801,to=66761) + to_title(1255)=(from=66802,to=66762) + to_title(1256)=(from=66803,to=66763) + to_title(1257)=(from=66804,to=66764) + to_title(1258)=(from=66805,to=66765) + to_title(1259)=(from=66806,to=66766) + to_title(1260)=(from=66807,to=66767) + to_title(1261)=(from=66808,to=66768) + to_title(1262)=(from=66809,to=66769) + to_title(1263)=(from=66810,to=66770) + to_title(1264)=(from=66811,to=66771) + to_title(1265)=(from=68800,to=68736) + to_title(1266)=(from=68801,to=68737) + to_title(1267)=(from=68802,to=68738) + to_title(1268)=(from=68803,to=68739) + to_title(1269)=(from=68804,to=68740) + to_title(1270)=(from=68805,to=68741) + to_title(1271)=(from=68806,to=68742) + to_title(1272)=(from=68807,to=68743) + to_title(1273)=(from=68808,to=68744) + to_title(1274)=(from=68809,to=68745) + to_title(1275)=(from=68810,to=68746) + to_title(1276)=(from=68811,to=68747) + to_title(1277)=(from=68812,to=68748) + to_title(1278)=(from=68813,to=68749) + to_title(1279)=(from=68814,to=68750) + to_title(1280)=(from=68815,to=68751) + to_title(1281)=(from=68816,to=68752) + to_title(1282)=(from=68817,to=68753) + to_title(1283)=(from=68818,to=68754) + to_title(1284)=(from=68819,to=68755) + to_title(1285)=(from=68820,to=68756) + to_title(1286)=(from=68821,to=68757) + to_title(1287)=(from=68822,to=68758) + to_title(1288)=(from=68823,to=68759) + to_title(1289)=(from=68824,to=68760) + to_title(1290)=(from=68825,to=68761) + to_title(1291)=(from=68826,to=68762) + to_title(1292)=(from=68827,to=68763) + to_title(1293)=(from=68828,to=68764) + to_title(1294)=(from=68829,to=68765) + to_title(1295)=(from=68830,to=68766) + to_title(1296)=(from=68831,to=68767) + to_title(1297)=(from=68832,to=68768) + to_title(1298)=(from=68833,to=68769) + to_title(1299)=(from=68834,to=68770) + to_title(1300)=(from=68835,to=68771) + to_title(1301)=(from=68836,to=68772) + to_title(1302)=(from=68837,to=68773) + to_title(1303)=(from=68838,to=68774) + to_title(1304)=(from=68839,to=68775) + to_title(1305)=(from=68840,to=68776) + to_title(1306)=(from=68841,to=68777) + to_title(1307)=(from=68842,to=68778) + to_title(1308)=(from=68843,to=68779) + to_title(1309)=(from=68844,to=68780) + to_title(1310)=(from=68845,to=68781) + to_title(1311)=(from=68846,to=68782) + to_title(1312)=(from=68847,to=68783) + to_title(1313)=(from=68848,to=68784) + to_title(1314)=(from=68849,to=68785) + to_title(1315)=(from=68850,to=68786) + to_title(1316)=(from=71872,to=71840) + to_title(1317)=(from=71873,to=71841) + to_title(1318)=(from=71874,to=71842) + to_title(1319)=(from=71875,to=71843) + to_title(1320)=(from=71876,to=71844) + to_title(1321)=(from=71877,to=71845) + to_title(1322)=(from=71878,to=71846) + to_title(1323)=(from=71879,to=71847) + to_title(1324)=(from=71880,to=71848) + to_title(1325)=(from=71881,to=71849) + to_title(1326)=(from=71882,to=71850) + to_title(1327)=(from=71883,to=71851) + to_title(1328)=(from=71884,to=71852) + to_title(1329)=(from=71885,to=71853) + to_title(1330)=(from=71886,to=71854) + to_title(1331)=(from=71887,to=71855) + to_title(1332)=(from=71888,to=71856) + to_title(1333)=(from=71889,to=71857) + to_title(1334)=(from=71890,to=71858) + to_title(1335)=(from=71891,to=71859) + to_title(1336)=(from=71892,to=71860) + to_title(1337)=(from=71893,to=71861) + to_title(1338)=(from=71894,to=71862) + to_title(1339)=(from=71895,to=71863) + to_title(1340)=(from=71896,to=71864) + to_title(1341)=(from=71897,to=71865) + to_title(1342)=(from=71898,to=71866) + to_title(1343)=(from=71899,to=71867) + to_title(1344)=(from=71900,to=71868) + to_title(1345)=(from=71901,to=71869) + to_title(1346)=(from=71902,to=71870) + to_title(1347)=(from=71903,to=71871) + to_title(1348)=(from=93792,to=93760) + to_title(1349)=(from=93793,to=93761) + to_title(1350)=(from=93794,to=93762) + to_title(1351)=(from=93795,to=93763) + to_title(1352)=(from=93796,to=93764) + to_title(1353)=(from=93797,to=93765) + to_title(1354)=(from=93798,to=93766) + to_title(1355)=(from=93799,to=93767) + to_title(1356)=(from=93800,to=93768) + to_title(1357)=(from=93801,to=93769) + to_title(1358)=(from=93802,to=93770) + to_title(1359)=(from=93803,to=93771) + to_title(1360)=(from=93804,to=93772) + to_title(1361)=(from=93805,to=93773) + to_title(1362)=(from=93806,to=93774) + to_title(1363)=(from=93807,to=93775) + to_title(1364)=(from=93808,to=93776) + to_title(1365)=(from=93809,to=93777) + to_title(1366)=(from=93810,to=93778) + to_title(1367)=(from=93811,to=93779) + to_title(1368)=(from=93812,to=93780) + to_title(1369)=(from=93813,to=93781) + to_title(1370)=(from=93814,to=93782) + to_title(1371)=(from=93815,to=93783) + to_title(1372)=(from=93816,to=93784) + to_title(1373)=(from=93817,to=93785) + to_title(1374)=(from=93818,to=93786) + to_title(1375)=(from=93819,to=93787) + to_title(1376)=(from=93820,to=93788) + to_title(1377)=(from=93821,to=93789) + to_title(1378)=(from=93822,to=93790) + to_title(1379)=(from=93823,to=93791) + to_title(1380)=(from=125218,to=125184) + to_title(1381)=(from=125219,to=125185) + to_title(1382)=(from=125220,to=125186) + to_title(1383)=(from=125221,to=125187) + to_title(1384)=(from=125222,to=125188) + to_title(1385)=(from=125223,to=125189) + to_title(1386)=(from=125224,to=125190) + to_title(1387)=(from=125225,to=125191) + to_title(1388)=(from=125226,to=125192) + to_title(1389)=(from=125227,to=125193) + to_title(1390)=(from=125228,to=125194) + to_title(1391)=(from=125229,to=125195) + to_title(1392)=(from=125230,to=125196) + to_title(1393)=(from=125231,to=125197) + to_title(1394)=(from=125232,to=125198) + to_title(1395)=(from=125233,to=125199) + to_title(1396)=(from=125234,to=125200) + to_title(1397)=(from=125235,to=125201) + to_title(1398)=(from=125236,to=125202) + to_title(1399)=(from=125237,to=125203) + to_title(1400)=(from=125238,to=125204) + to_title(1401)=(from=125239,to=125205) + to_title(1402)=(from=125240,to=125206) + to_title(1403)=(from=125241,to=125207) + to_title(1404)=(from=125242,to=125208) + to_title(1405)=(from=125243,to=125209) + to_title(1406)=(from=125244,to=125210) + to_title(1407)=(from=125245,to=125211) + to_title(1408)=(from=125246,to=125212) + to_title(1409)=(from=125247,to=125213) + to_title(1410)=(from=125248,to=125214) + to_title(1411)=(from=125249,to=125215) + to_title(1412)=(from=125250,to=125216) + to_title(1413)=(from=125251,to=125217) +} \ No newline at end of file From 3849fd5c9d724f7459ed7fe9fd28c76370608223 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:16:41 +0700 Subject: [PATCH 14/59] Refactor `Testing` subsystem --- sources/Core/Testing/IssueSummary.uc | 294 ++++++++++ sources/Core/Testing/Service/TestingEvents.uc | 66 +++ .../Testing/Service/TestingListenerBase.uc | 34 ++ .../Core/Testing/Service/TestingService.uc | 253 ++++++++ sources/Core/Testing/TestCase.uc | 323 +++++------ sources/Core/Testing/TestCaseSummary.uc | 540 ++++++++++++++++++ 6 files changed, 1330 insertions(+), 180 deletions(-) create mode 100644 sources/Core/Testing/IssueSummary.uc create mode 100644 sources/Core/Testing/Service/TestingEvents.uc create mode 100644 sources/Core/Testing/Service/TestingListenerBase.uc create mode 100644 sources/Core/Testing/Service/TestingService.uc create mode 100644 sources/Core/Testing/TestCaseSummary.uc diff --git a/sources/Core/Testing/IssueSummary.uc b/sources/Core/Testing/IssueSummary.uc new file mode 100644 index 0000000..27e20d7 --- /dev/null +++ b/sources/Core/Testing/IssueSummary.uc @@ -0,0 +1,294 @@ +/** + * Class for storing and processing the information about how well testing + * against a certain issue went. + * 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 IssueSummary extends AcediaObject; + +// Each issue is uniquely identified by these values. +var private class ownerCase; +var private string context; +var private string description; + +// Records, in chronological order, results of the tests that were +// run to test this issue. +var private array successRecords; + +private final function byte BoolToByte(bool boolToConvert) +{ + if (boolToConvert) return 1; + return 0; +} + +/** + * Sets `TestCase`, context and description for the issue, + * tracked in this summary. + * + * Can only be successfully called once, but will fail if passed a `none` + * class reference to `TestCase`. + * + * @param targetCase `TestCase`, in which issue, + * relevant to this summary, is defined. + * @param targetContext Context, in which this issue, + * relevant to this summary, is defined. + * @param targetDescription Description of the issue relevant to + * this summary. + * @return `true` if `TestCase`, context and description were successfully set, + * `false` otherwise. + */ +public final function bool SetIssue( + class targetCase, + string targetContext, + string targetDescription +) +{ + if (ownerCase != none) return false; + if (initCase == none) return false; + ownerCase = targetCase; + context = targetContext; + description = targetDescription; + return true; +} + +/** + * Returns context for the issue in question. + * + * `TestCase` can be important for both displaying information about testing to + * the user and distinguishing between two different issues with the same + * description and context. + * @see `TestCase` for more information. + * + * @return Test case that tested for relevant issue. + */ +public final function class GetTestCase() +{ + return ownerCase; +} + +/** + * Returns context for the issue in question. + * + * Context can be important for both displaying information about testing to + * the user and distinguishing between two different issues with + * the same description and in the same `TestCase`. + * @see `TestCase` for more information. + * + * @return Context for relevant issue. + */ +public final function string GetContext() +{ + if (ownerCase == none) return ""; + return context; +} + +/** + * Returns description for the issue in question. + * + * Description of an issue is the main way to distinguish between + * different possibly arising problems. + * Two different issues can have the same description if they are defined + * in different `TestCase`s and/or in different context. + * @see `TestCase` for more information. + * + * @return Description for the issue in question. + */ +public final function string GetDescription() +{ + if (ownerCase == none) return ""; + return description; +} + +/** + * Adds result of another test (success or not) to the records of this summary. + * + * @param success `true` if test was successful and had passed, + * `false` otherwise. + */ +public final function AddTestResult(bool success) +{ + successRecords[successRecords.length] = BoolToByte(success); +} + +/** + * Returns total amount of test results recorded in caller summary. + * Never a negative value. + * + * @return Amount of tests that were run. + */ +public final function int GetTotalTestsAmount() +{ + return successRecords.length; +} + +/** + * Returns total amount of recorded successful test results in caller summary. + * Never a negative value. + * + * @return Amount of recorded successfully performed tests for + * the relevant issue. + */ +public final function int GetSuccessfulTestsAmount() +{ + local int i; + local int counter; + counter = 0; + for (i = 0; i < successRecords.length; i += 1) + { + if (successRecords[i] > 0) { + counter += 1; + } + } + return counter; +} + +/** + * Returns total amount of recorded failed test results in caller summary. + * Never a negative value. + * + * @return Amount of recorded failed tests for the relevant issue. + */ +public final function int GetFailedTestsAmount() +{ + return GetTotalTestsAmount() - GetSuccessfulTestsAmount(); +} + +/** + * Returns total success rate ("amount of successes" / "total amount of tests") + * of recorded test results for relevant issue + * (value between 0 and 1, including boundaries). + * + * If there are no test results recorded - returns `-1`. + * + * @return Success rate of recorded test results for the relevant issue + * Returns values outside [0; 1] segment (specifically, negative values) + * iff no test results at all were recorded. + */ +public final function float GetSuccessRate() +{ + local int totalTestsAmount; + totalTestsAmount = GetTotalTestsAmount(); + if (totalTestsAmount <= 0) { + return -1; + } + return GetSuccessfulTestsAmount() / totalTestsAmount; +} + +/** + * Checks whether all tests recorded in this summary have passed. + * + * @return `true` if all tests for relevant issue have passed, + * `false` otherwise. + */ +public final function bool HasPassedAllTests() +{ + return (GetFailedTestsAmount() <= 0); +} + +/** + * Returns boolean array of test results: each element recording whether test + * was a success (`>0`) or a failure (`0`). + * + * All results in the array are in a chronological order of arrival. + * + * @return Returns copy of boolean array of recorded test results. + */ +public final function array GetTestRecords() +{ + return successRecords; +} + +/** + * Returns index numbers (starting from 1, not 0) of tests that ended in + * a success, while performed for the same test case, context and issue. + * So if tests went: [success, success, failure, success, failure], + * method will return: [1, 2, 4]. + * + * All results in the array are in a chronological order of arrival. + * + * @return index numbers of successful tests. + */ +public final function array GetSuccessfulTests() +{ + local int i; + local array result; + for (i = 0; i < successRecords.length; i += 1) + { + if (successRecords[i] > 0) { + result[result.length] = i + 1; + } + } + return result; +} + +/** + * Returns index numbers (starting from 1, not 0) of tests that ended in + * a failure, while performed for the same test case, context and issue. + * So if tests went: [success, success, failure, success, failure], + * method will return: [3, 5]. + * + * All results in the array are in a chronological order of arrival. + * + * @return index numbers of successful tests. + */ +public final function array GetFailedTests() +{ + local int i; + local array result; + for (i = 0; i < successRecords.length; i += 1) + { + if (successRecords[i] == 0) { + result[result.length] = i + 1; + } + } + return result; +} + +/** + * Returns a formatted text representation of the caller `IssueSummary` + * in a following format: + * "{$text_default } {$text_subtle []}" + * + * @return Formatted string with text representation of the + * caller `IssueSummary`. + */ +public final function string ToString() +{ + local int i; + local string result; + local array failedTests; + result = "{$text_default" @ GetDescription() $ "}"; + if (GetFailedTestsAmount() <= 0) { + return result; + } + result @= "{$text_subtle ["; + failedTests = GetFailedTests(); + for (i = 0; i < failedTests.length; i += 1) + { + if (i < failedTests.length - 1) { + result $= string(failedTests[i]) $ ", "; + } + else { + result $= string(failedTests[i]); + } + } + return (result $ "]"); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Core/Testing/Service/TestingEvents.uc b/sources/Core/Testing/Service/TestingEvents.uc new file mode 100644 index 0000000..167f14b --- /dev/null +++ b/sources/Core/Testing/Service/TestingEvents.uc @@ -0,0 +1,66 @@ +/** + * Event generator for events related to testing. + * 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 TestingEvents extends Events + abstract; + +static function CallTestingBegan(array< class > testQueue) +{ + local int i; + local array< class > listeners; + listeners = GetListeners(); + for (i = 0; i < listeners.length; i += 1) + { + class(listeners[i]) + .static.TestingBegan(testQueue); + } +} + +static function CallCaseTested( + class testedCase, + TestCaseSummary result) +{ + local int i; + local array< class > listeners; + listeners = GetListeners(); + for (i = 0; i < listeners.length; i += 1) + { + class(listeners[i]) + .static.CaseTested(testedCase, result); + } +} + +static function CallTestingEnded( + array< class > testQueue, + array results) +{ + local int i; + local array< class > listeners; + listeners = GetListeners(); + for (i = 0; i < listeners.length; i += 1) + { + class(listeners[i]) + .static.TestingEnded(testQueue, results); + } +} + +defaultproperties +{ + relatedListener = class'TestingListenerBase' +} \ No newline at end of file diff --git a/sources/Core/Testing/Service/TestingListenerBase.uc b/sources/Core/Testing/Service/TestingListenerBase.uc new file mode 100644 index 0000000..92272bb --- /dev/null +++ b/sources/Core/Testing/Service/TestingListenerBase.uc @@ -0,0 +1,34 @@ +/** + * Listener for events related to testing. + * 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 TestingListenerBase extends Listener + abstract; + +static function TestingBegan(array< class > testQueue) {} + +static function CaseTested(class testQueue, TestCaseSummary result) {} + +static function TestingEnded( + array< class > testedCase, + array results) {} + +defaultproperties +{ + relatedEvents = class'TestingEvents' +} \ No newline at end of file diff --git a/sources/Core/Testing/Service/TestingService.uc b/sources/Core/Testing/Service/TestingService.uc new file mode 100644 index 0000000..896a932 --- /dev/null +++ b/sources/Core/Testing/Service/TestingService.uc @@ -0,0 +1,253 @@ +/** + * This service allows to separate running separate `TestCase`s in separate + * ticks, which helps to avoid hang ups or false infinite loop detection. + * 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 TestingService extends Service + config(AcediaSystem); + +// All test cases, loaded from all available packages. +// Always use `default` copy of this array. +var private array< class > registeredTestCases; + +// Will be `true` if we have yet more tests to run +// (either during current or following ticks) +var private bool runningTests; +// Queue with all test cases for the current/next testing +var private array< class > testCasesToRun; +// Track which test case we need to execute during next tick +var private int nextTestCase; + +// Record test results during the last test run here. +// After testing has finished - copy them into it's default value +// `default.summarizedResults` to be available even after `TestingService` +// shuts down. +var private array summarizedResults; + +// Configuration variables that tell Acedia what tests to run +// (and whether to run any at all) on start up. +var public config const bool runTestsOnStartUp; +var public config const bool filterTestsByName; +var public config const bool filterTestsByGroup; +var public config const string requiredName; +var public config const string requiredGroup; + +// Shortcut to `TestingEvents`, so that we don't have to write +// class'TestingEvents' every time. +var const class events; + +/** + * Registers another `TestCase` class for later testing. + * + * @return `true` if registration was successful. + */ +public final static function bool RegisterTestCase(class newTestCase) +{ + local int i; + if (newTestCase == none) return false; + + for (i = 0; i < default.registeredTestCases.length; i += 1) + { + if (default.registeredTestCases[i] == newTestCase) { + return false; + } + // Warn if there are test cases with the same name and group + if ( !(default.registeredTestCases[i].static.GetGroup() + ~= newTestCase.static.GetGroup())) { + continue; + } + if ( !(default.registeredTestCases[i].static.GetName() + ~= 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."); + } + default.registeredTestCases[default.registeredTestCases.length] = + newTestCase; + return true; +} + +/** + * Checks whether service is still in the process of running tests. + * + * @return `true` if there are still some tests that are scheduled, but + * were not yet ran and `false` otherwise. + */ +public final static function bool IsRunningTests() +{ + local TestingService myInstance; + myInstance = TestingService(class'TestingService'.static.GetInstance()); + if (myInstance == none) return false; + + return myInstance.runningTests; +} + +/** + * Returns the results of the last tests run. + * + * If no tests were run - returns an empty array. + * + * @return Results of the last tests run. + */ +public final static function array GetLastResults() +{ + return default.summarizedResults; +} + +/** + * Adds all tests to the testing queue. + * + * To actually run them use `Run()`. + * To only run certain tests, - filter them by `FilterByName()` + * and `FilterByGroup()` + * + * Will do nothing if service is already in the process of testing + * (`IsRunningTests() == true`). + * + * @return Caller `TestService` to allow for method chaining. + */ +public final function TestingService PrepareTests() +{ + if (runningTests) { + return self; + } + testCasesToRun = default.registeredTestCases; + return self; +} + +/** + * Filters tests in current queue to only those that have a specific name. + * Should be used after `PrepareTests()` call, but before `Run()`. + * + * Will do nothing if service is already in the process of testing + * (`IsRunningTests() == true`). + * + * @return Caller `TestService` to allow for method chaining. + */ +public final function TestingService FilterByName(string caseName) +{ + local int i; + local array< class > preFiltered; + if (runningTests) { + return self; + } + preFiltered = testCasesToRun; + testCasesToRun.length = 0; + for (i = 0; i < preFiltered.length; i += 1) + { + if (preFiltered[i].static.GetName() ~= caseName) { + testCasesToRun[testCasesToRun.length] = preFiltered[i]; + } + } + return self; +} + +/** + * Filters tests in current queue to only those that belong to + * a specific group. Should be used after `PrepareTests()` call, + * but before `Run()`. + * + * Will do nothing if service is already in the process of testing + * (`IsRunningTests() == true`). + * + * @return Caller `TestService` to allow for method chaining. + */ +public final function TestingService FilterByGroup(string caseGroup) +{ + local int i; + local array< class > preFiltered; + if (runningTests) { + return self; + } + preFiltered = testCasesToRun; + testCasesToRun.length = 0; + for (i = 0; i < preFiltered.length; i += 1) + { + if (preFiltered[i].static.GetGroup() ~= caseGroup) { + testCasesToRun[testCasesToRun.length] = preFiltered[i]; + } + } + return self; +} + +/** + * Makes `TestingService` run all tests in a current queue. + * + * Queue musty be build before hand: start with `PrepareTests()` call and + * optionally use `FilterByName()` / `FilterByGroup()` before + * `Run()` method call. + * + * @return `false` if service is already performing the testing + * and `true` otherwise. Note that `TestingService` might be inactive even + * after `Run()` call that returns `true`, if the testing queue was empty. + */ +public final function bool Run() +{ + if (runningTests) { + return false; + } + nextTestCase = 0; + runningTests = true; + summarizedResults.length = 0; + events.static.CallTestingBegan(testCasesToRun); + if (testCasesToRun.length <= 0) { + runningTests = false; + events.static.CallTestingEnded(testCasesToRun, summarizedResults); + } + return true; +} + +private final function DoTestingStep() +{ + local TestCaseSummary newResult; + if (nextTestCase >= testCasesToRun.length) + { + runningTests = false; + default.summarizedResults = summarizedResults; + events.static.CallTestingEnded(testCasesToRun, summarizedResults); + return; + } + testCasesToRun[nextTestCase].static.PerformTests(); + newResult = testCasesToRun[nextTestCase].static.GetSummary(); + events.static.CallCaseTested(testCasesToRun[nextTestCase], newResult); + summarizedResults[summarizedResults.length] = newResult; + nextTestCase += 1; +} + +event Tick(float delta) +{ + // This will destroy us on the next tick after we were + // either created or finished performing tests + if (!runningTests) { + Destroy(); + return; + } + DoTestingStep(); +} + +defaultproperties +{ + runTestsOnStartUp = false + events = class'TestingEvents' +} \ No newline at end of file diff --git a/sources/Core/Testing/TestCase.uc b/sources/Core/Testing/TestCase.uc index 43318a6..7f69117 100644 --- a/sources/Core/Testing/TestCase.uc +++ b/sources/Core/Testing/TestCase.uc @@ -1,13 +1,8 @@ /** - * Base class aimed to contain sets of unit tests for various components of + * Base class aimed to contain sets of tests for various components of * Acedia and it's features. - * Currently provides bare-bones testing functions that check boolean - * variables for true/false and objects for whether they're `none` or not. - * Tests: - * ~ can be grouped by their "context", - * describing what they are testing; - * ~ test (or several tests) can be assigned an error message, - * describing what exactly went wrong. + * Neither this class, nor it's children aren't supposed to + * be instantiated. * Copyright 2020 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -30,202 +25,165 @@ class TestCase extends AcediaObject // Name by which this set of unit tests can be referred to. var protected const string caseName; +// Name of group to which this set of unit tests belong. +var protected const string caseGroup; -// Information about how well testing went for a particular context, -// i.e. subsets of tests for a particular functionality. -struct ContextSummary -{ - // Text, human-readable description of the purpose of - // tests in this context. - var string description; - // `false` if at least one test failed, `true` otherwise. - var bool passed; - // How many test were performed. - var int testsPerformed; - // How many tests failed. - var int testsFailed; - // Error messages generated by failed tests. - var array errors; -}; - -// Collection of summaries for all contexts defined by the user so far. -struct Summary -{ - var bool passed; - var array contextSummaries; -}; - -// Has function for defining context (`Context()`) been called. -var private bool userDefinedContext; // Were all tests performed? -var private bool finishedTests; +var private bool finishedTests; +// Context under which we are currently performing our tests. +var private string currentContext; // Error message that will be generated if some test will fail now. -var private string currentErrorMessage; +var private string currentIssue; -// Store complete summary here. -var private Summary currentSummary; -// For quick access store current context's summary here and update it in -// `currentSummary` once done. -var private ContextSummary activeContextSummary; +// Summary where we are recording results of all our tests. +var private TestCaseSummary currentSummary; -// Call this function to define a context for subsequent test -// (until another call). -public final static function Context(string description) +/** + * Sets context for any tests that will follow this call (but before the next + * `Context()` call). + * + * Context is supposed to be a short description about what + * exactly you are testing. When reporting failed tests, - failures will be + * grouped up by a context. + * + * Changing current context will also reset current issue, to set it up + * use `Issue()` method. + * + * @param context Context for the following tests. + */ +public final static function Context(string context) { - if ( default.userDefinedContext - || default.activeContextSummary.testsPerformed > 0) - { - UpdateContextSummary(default.activeContextSummary); - } - default.userDefinedContext = true; - default.activeContextSummary = GetContextSummary(description); - default.currentErrorMessage = ""; + default.currentContext = context; + default.currentIssue = ""; // Reset issue. } // Call this function to define an error message for tests that // would fail after it. // Message is reset by another call of `Issue()` or // by changing the context via `Context()`. -public final static function Issue(string errorMessage) +/** + * Changes an issue that any following tests (but before the next `Issue()` or + * `Context()` call) will test for. + * + * Issue is the message that will be displayed to the user if any relevant + * tests have failed. + * + * NOTE: Current issue will be reset by any `Context()` call. + * + * @param issue Issue that following tests will test for. + */ +public final static function Issue(string issue) { - default.currentErrorMessage = errorMessage; + default.currentIssue = issue; } -// All tests to be performed can be placed in this function, -// along with appropriate calls to `Context()` and `Issue()`. -// For an example see class `TEST_JSON`. -protected static function TESTS(){} +// Following functions provide simple test primitives -// Following functions provide simple test primitives, +/** + * This call will record either one success or one failure for the caller + * `TestCase` class, depending on passed `bool` argument. + * + * @param result Your test's result as a `bool` value: `true` will record a + * success and `false` a failure. + */ public final static function TEST_ExpectTrue(bool result) { - RecordTestResult(result, default.currentErrorMessage); + RecordTestResult(result); } +/** + * This call will record either one success or one failure for the caller + * `TestCase` class, depending on passed `bool` argument. + * + * @param result Your test's result as a `bool` value: `false` will result in + * recording a success and `true` in a failure. + */ public final static function TEST_ExpectFalse(bool result) { - RecordTestResult(!result, default.currentErrorMessage); + RecordTestResult(!result); } +/** + * This call will record either one success or one failure for the caller + * `TestCase` class, depending on passed `Object` argument. + * + * @param result Your test's result as an `Object` value: `none` will result + * in recording success and any non-`none` value in failure. + */ public final static function TEST_ExpectNone(Object object) { - RecordTestResult(object == none, default.currentErrorMessage); + RecordTestResult(object == none); } +/** + * This call will record either one success or one failure for the caller + * `TestCase` class, depending on passed `Object` argument. + * + * @param result Your test's result as an `Object` value: any non-`none` + * value will result in recording success and `none` in failure. + */ public final static function TEST_ExpectNotNone(Object object) { - RecordTestResult(object != none, default.currentErrorMessage); + RecordTestResult(object != none); } -// Returns the summary of how testing went. -public final static function Summary GetSummary() +// Records (in current context summary) that another test was performed and +// succeeded/failed, along with given error message. +private final static function RecordTestResult(bool isSuccessful) { + if (default.finishedTests) return; + if (default.currentSummary == none) return; + default.currentSummary.AddTestResult( default.currentContext, + default.currentIssue, + isSuccessful); +} + +/** + * Once testing has finished returns compiled results as a + * `TestCaseSummary` object. + * + * @return `TestCaseSummary` with compiled results if the testing has finished + * and `none` otherwise. + */ +public final static function TestCaseSummary GetSummary() +{ + if (!default.finishedTests) { + return none; + } return default.currentSummary; } -// Name by which this set of unit tests can be referred to. +/** + * Checks whether this `TestCase` has already finished running all it's tests. + * Finished testing means a prepared `TestCaseSummary` is available + * (by `GetSummary()` method). + * + * @return `true` if this test case already did the testing + * and `false` otherwise. + */ +public final static function bool HasFinishedTesting() +{ + return default.finishedTests; +} + +/** + * Returns name of this `TestCase`. + * + * @return Name of this `TestCase`. + */ public final static function string GetName() { return default.caseName; } -// Creates brand new summary for context with a given description, -// marked as "passed" and zero tests done. -private final static function ContextSummary NewContextSummary -( - string description -) +/** + * Returns group name of this `TestCase`. + * + * @return Group name of this `TestCase`. + */ +public final static function string GetGroup() { - local ContextSummary newSummary; - newSummary.passed = true; - newSummary.description = description; - newSummary.testsPerformed = 0; - newSummary.testsFailed = 0; - newSummary.errors.length = 0; - return newSummary; -} - -// Returns index of summary with given description -// in our records (`currentSummary`). -// Return `-1` if there is no context with such description. -private final static function int GetContextSummaryIndex(string description) -{ - local int i; - for (i = 0; i < default.currentSummary.contextSummaries.length; i += 1) - { - if ( default.currentSummary.contextSummaries[i].description - ~= description) - { - return i; - } - } - return -1; -} -// Returns index summary with given description -// in our records. -// Return new context summary if there is no context with such description. -private final static function ContextSummary GetContextSummary -( - string description -) -{ - local int index; - if (default.activeContextSummary.description ~= description) - { - return default.activeContextSummary; - } - index = GetContextSummaryIndex(description); - if (index < 0) - { - return NewContextSummary(description); - } - - return default.currentSummary.contextSummaries[index]; -} - -// Rewrites summary (with the same name as a given summary) -// in `currentSummary` records. -// If there's no such record - adds a new one. -private final static function UpdateContextSummary -( - ContextSummary relevantSummary -) -{ - local int index; - index = GetContextSummaryIndex(relevantSummary.description); - if (index < 0) - { - index = default.currentSummary.contextSummaries.length; - } - default.currentSummary.contextSummaries[index] = relevantSummary; -} - -// Records (in current context summary) that another test was performed and -// succeeded/failed, along with given error message. -private final static function RecordTestResult -( - bool isSuccessful, - string errorMessage -) -{ - local int i; - local int errorsAmount; - if (default.finishedTests) return; - default.activeContextSummary.testsPerformed += 1; - if (isSuccessful) return; - - default.currentSummary.passed = false; - default.activeContextSummary.passed = false; - default.activeContextSummary.testsFailed += 1; - errorsAmount = default.activeContextSummary.errors.length; - for (i = 0; i < errorsAmount; i += 1) - { - if (default.activeContextSummary.errors[i] ~= errorMessage) - { - return; - } - } - default.activeContextSummary.errors[errorsAmount] = errorMessage; + return default.caseGroup; } // Calling this function will perform unit tests defined in `TESTS()` @@ -233,31 +191,36 @@ private final static function RecordTestResult // obtainable through `GetSummary()` function. // Returns `true` if all tests have successfully passed // and `false` otherwise. +/** + * Performs all tests for this `TestCase`. + * Guaranteed to be done after this finishes. + * + * @return `true` if all tests have finished successfully + * and `false` otherwise. + */ public final static function bool PerformTests() { - default.finishedTests = false; - default.userDefinedContext = false; - default.currentSummary.passed = true; - default.currentSummary.contextSummaries.length = 0; - default.activeContextSummary = NewContextSummary(""); + default.finishedTests = false; + _().memory.Free(default.currentSummary); + default.currentSummary = new class'TestCaseSummary'; + default.currentSummary.Initialize(default.class); TESTS(); - UpdateContextSummary(default.activeContextSummary); default.finishedTests = true; - return default.currentSummary.passed; + return default.currentSummary.HasPassedAllTests(); } -// TODO: Support for testing in stages to avoid infinite loop crashes. -// TODO: Add support for test scening: grabbing pawns, placing them, waiting. -// TODO: Expand scening support: triggering functions on client, moving. -// TODO: Expand scening support: zed spawning, aggro setting. -// TODO: Expand scening support: function calls (i.e. for CashToss), -// testing `FixDoshSpam` feature. -// TODO: Expand scening support: lag detection. -// TODO: Expand scening support: test `FixZedTime`. -// TODO: Expand scening support: aiming shooting, detecting damage. -// TODO: Expand scening support: testing `FixFFHack`. -// TODO: Testing infinite nade (partially), ammo selling, dualies cost. +/** + * Any tests that your `TestCase` class needs to perform should be put in + * this function. + * To separate tests into groups it's recommended (as a style + * consideration) to put them in separate function calls and give these + * functions names starting with "Test_". They can have further folded + * functions with prefix "SubTest_", which can contain "SubSubTest_", etc.. + */ +protected static function TESTS(){} + defaultproperties { caseName = "" + caseGroup = "" } \ No newline at end of file diff --git a/sources/Core/Testing/TestCaseSummary.uc b/sources/Core/Testing/TestCaseSummary.uc new file mode 100644 index 0000000..b6b8719 --- /dev/null +++ b/sources/Core/Testing/TestCaseSummary.uc @@ -0,0 +1,540 @@ +/** + * Class for storing and processing the information about how well testing + * for a certain `TestCase` went. That information is stored as + * a collection of `IssueSummary`s, that can be accessed all at once + * or by their context. + * `TestCaseSummary` must be initialized for some `TestCase` before it can + * be used for anything (unlike `IssueSummary`). + * 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 TestCaseSummary extends AcediaObject; + +// Case for which this summary was initialized. +// `none` if it was not. +var private class ownerCase; + +/** + * + * We will store issue summaries for different contexts separately. + * INVARIANT: any function that adds records to `contextRecords` + * must guarantee that: + * 1. No two distinct records will have the same `context`; + * 2. All the `IssueSummary`s in `issueSummaries` array have different + * issue descriptions. + * Comparisons of `string`s for two above conditions are case-insensitive. + */ +struct ContextRecord +{ + var string context; + var array issueSummaries; +}; +var private array contextRecords; + +// String literals used for displaying array of test case summaries +var private const string indent; +var private const string reportHeader; +var private const string reportSuccessfulEnding; +var private const string reportUnsuccessfulEnding; + +/** + * Initializes caller summary for given `TestCase` class. + * Can only be successfully done once, but will fail if + * passed a `none` reference. + * + * @param targetCase `TestCase` class for which this summary will be + * recording test results. + * @return `true` if initialization was successful and `false otherwise + * (either summary already initialized or passed reference is `none`). + */ +public final function bool Initialize(class targetCase) +{ + if (ownerCase != none) return false; + if (targetCase == none) return false; + ownerCase = targetCase; + return true; +} + +/** + * Returns index of a context record with a given description + * (`context`) in `contextRecords`. + * Creates one if missing. Never fails. + * + * @param context Context that desired record must match. + * @return Index of the context record that matches `context`. + * Returned index is always valid. + */ +private final function int TouchContext(string context) +{ + local int i; + local ContextRecord newRecord; + // Try to find existing record with given context description + for (i = 0; i < contextRecords.length; i += 1) + { + if (context ~= contextRecords[i].context) { + return i; + } + } + // If there is none - make a new one + newRecord.context = context; + contextRecords[contextRecords.length] = newRecord; + return (contextRecords.length - 1); +} + +/** + * Finds indices of a context record and an `IssueSummary` in + * a nested array that have matching `context` + * and `issueDescription`. + * Creates records and/or `IssueSummary` if missing. Never fails. + * + * @param context Context description that + * desired record must match. + * @param issueDescription Issue description that + * desired `IssueSummary`must match. + * @param recordIndex Index of the context record that matches + * `context` description will be recorded here. + * Returned value is always valid. Passed value is discarded. + * @param recordIndex Index of the `IssueSummary` that matches + * `issueDescription` description will be recorded here. + * Returned value is always valid. Passed value is discarded. + */ +private final function TouchIssue( + string context, + string issueDescription, + out int recordIndex, + out int issueIndex +) +{ + local int i; + local array issueSummaries; + recordIndex = TouchContext(context); + issueSummaries = contextRecords[recordIndex].issueSummaries; + // Try to find existing issue summary with a given description + for (i = 0; i < issueSummaries.length; i += 1) + { + if (issueSummaries[i] == none) continue; + if (issueDescription ~= issueSummaries[i].GetDescription()) + { + issueIndex = i; + return; + } + } + // If there is none - add a new one + issueIndex = issueSummaries.length; + issueSummaries[issueIndex] = new class'IssueSummary'; + issueSummaries[issueIndex].SetIssue(ownerCase, context, issueDescription); + contextRecords[recordIndex].issueSummaries = issueSummaries; +} + +/** + * Checks if caller summary was correctly initialized. + * + * @return `true` if summary was correctly initialized and `false` otherwise. + */ +public final function bool IsInitialized() +{ + return (ownerCase != none); +} + +/** + * Adds result of another test (success or not) to the records of this summary. + * + * @param context Context under which test was performed. + * @param issueDescription Description of issue, + * for which test was performed. + * @param success `true` if test was successful and had passed, + * `false` otherwise. + */ +public final function AddTestResult( + string context, + string issueDescription, + bool success +) +{ + local int recordIndex, issueIndex; + TouchIssue(context, issueDescription, recordIndex, issueIndex); + contextRecords[recordIndex] + .issueSummaries[issueIndex] + .AddTestResult(success); +} + +/** + * Returns all contexts, for which caller summary has any records of tests + * being performed. + * + * To check if particular context exists you can use `DoesContextExists()`. + * + * @return Array of `string`s, each representing one of the contexts, + * used in tests. + * Guarantees no duplicates (equality without accounting for case). + */ +public final function array GetContexts() +{ + local int i; + local array result; + for (i = 0; i < contextRecords.length; i += 1) { + result[result.length] = contextRecords[i].context; + } + return result; +} + +/** + * Checks if given context has any records about performing tests + * (whether they ended in success or a failure) under it. + * + * To get an array of all existing contexts use `GetContexts()`. + * + * @param context A context to check for existing in records. + * @return `true` if there was a record about a test being performed under + * a given context and `false` otherwise. + */ +public final function bool DoesContextExists(string context) +{ + local int i; + for (i = 0; i < contextRecords.length; i += 1) + { + if (contextRecords[i].context ~= context) { + return true; + } + } + return false; +} + +/** + * `IssueSummary`s for every issue that was tested and recorded in + * the caller `TestCaseSummary`. + * + * @return Array of `IssueSummary`s for every tested and recorded issue. + */ +public final function array GetIssueSummaries() +{ + local int i, j; + local array recordedSummaries; + local array result; + for (i = 0; i < contextRecords.length; i += 1) + { + recordedSummaries = contextRecords[i].issueSummaries; + for (j = 0; j < recordedSummaries.length; j += 1) { + result[result.length] = recordedSummaries[j]; + } + } + return result; +} + +/** + * Returns `IssueSummary`s for every issue that was tested under + * a given context and recorded in caller `TestCaseSummary`. + * + * @param context Context under which issues of interest were tested. + * @return Array of `IssueSummary`s for every issue that was tested under + * given context. + */ +public final function array GetIssueSummariesForContext( + string context +) +{ + local int i; + local array emptyResult; + for (i = 0; i < contextRecords.length; i += 1) + { + if (contextRecords[i].context ~= context) { + return contextRecords[i].issueSummaries; + } + } + return emptyResult; +} + +// Counts total amount of tests performed under the contexts +// corresponding to `contextRecords[recordIndex]` record. +private final function int GetTotalTestsAmountForRecord(int recordIndex) +{ + local int i; + local int result; + local array issueSummaries; + issueSummaries = contextRecords[recordIndex].issueSummaries; + result = 0; + for (i = 0; i < issueSummaries.length; i += 1) + { + if (issueSummaries[i] == none) continue; + result += issueSummaries[i].GetTotalTestsAmount(); + } + return result; +} + +/** + * Total amount of performed tests, recorded in caller `TestCaseSummary`. + * + * If you are interested in amount of test under a specific context, - + * use `GetTotalTestsAmountForContext()` instead. + * + * @return Total amount of performed tests. + */ +public final function int GetTotalTestsAmount() +{ + local int i; + local int result; + for (i = 0; i < contextRecords.length; i += 1) + { + result += GetTotalTestsAmountForRecord(i); + } + return result; +} + +/** + * Total amount of tests, performed under a context `context` and + * recorded in caller `TestCaseSummary`. + * + * If you are interested in total amount of test under all contexts, - + * use `GetTotalTestsAmount()` instead. + * + * @param context Context for which method must count amount of + * performed tests. + * @return Total amount of tests, performed under given context. + * If given context does not exist in records, - returns `-1`. + */ +public final function int GetTotalTestsAmountForContext(string context) +{ + local int i; + for (i = 0; i < contextRecords.length; i += 1) + { + if (context ~= contextRecords[i].context) { + return GetTotalTestsAmountForRecord(i); + } + } + return -1; +} + +// Counts total amount of successful tests performed under the contexts +// corresponding to `contextRecords[recordIndex]` record. +private final function int GetSuccessfulTestsAmountForRecord(int recordIndex) +{ + local int i; + local int result; + local array issueSummaries; + issueSummaries = contextRecords[recordIndex].issueSummaries; + result = 0; + for (i = 0; i < issueSummaries.length; i += 1) + { + if (issueSummaries[i] == none) continue; + result += issueSummaries[i].GetSuccessfulTestsAmount(); + } + return result; +} + +/** + * Total amount of successfully performed tests, + * recorded in caller `TestCaseSummary`. + * + * If you are interested in amount of successful test under a specific context, + * - use `GetSuccessfulTestsAmountForContext()` instead. + * + * @return Total amount of successfully performed tests. + */ +public final function int GetSuccessfulTestsAmount() +{ + local int i; + local int result; + for (i = 0; i < contextRecords.length; i += 1) + { + result += GetSuccessfulTestsAmountForRecord(i); + } + return result; +} + +/** + * Total amount of tests, performed under a context `context` and + * recorded in caller `TestCaseSummary`. + * + * If you are interested in total amount of successful test under all contexts, + * - use `GetSuccessfulTestsAmount()` instead. + * + * @param context Context for which we method must count amount of + * successful tests. + * @return Total amount of successful tests, performed under given context. + * If given context does not exist in records, - returns `-1`. + */ +public final function int GetSuccessfulTestsAmountForContext(string context) +{ + local int i; + for (i = 0; i < contextRecords.length; i += 1) + { + if (context ~= contextRecords[i].context) { + return GetSuccessfulTestsAmountForRecord(i); + } + } + return -1; +} + +// Counts total amount of tests, failed under the contexts +// corresponding to `contextRecords[recordIndex]` record. +private final function int GetFailedTestsAmountForRecord(int recordIndex) +{ + local int i; + local int result; + local array issueSummaries; + issueSummaries = contextRecords[recordIndex].issueSummaries; + result = 0; + for (i = 0; i < issueSummaries.length; i += 1) + { + if (issueSummaries[i] == none) continue; + result += issueSummaries[i].GetFailedTestsAmount(); + } + return result; +} + +/** + * Total amount of failed tests, recorded in caller `TestCaseSummary`. + * + * If you are interested in amount of failed test under a specific context, - + * use `GetFailedTestsAmountForContext()` instead. + * + * @return Total amount of failed tests. + */ +public final function int GetFailedTestsAmount() +{ + local int i; + local int result; + for (i = 0; i < contextRecords.length; i += 1) + { + result += GetFailedTestsAmountForRecord(i); + } + return result; +} + +/** + * Total amount of failed tests, performed under a context `context` and + * recorded in caller `TestCaseSummary`. + * + * If you are interested in total amount of failed test under all contexts, - + * use `GetFailedTestsAmount()` instead. + * + * @param context Context for which method must count amount of + * failed tests. + * @return Total amount of failed tests, performed under given context. + * If given context does not exist in records, - returns `-1`. + */ +public final function int GetFailedTestsAmountForContext(string context) +{ + local int i; + for (i = 0; i < contextRecords.length; i += 1) + { + if (context ~= contextRecords[i].context) { + return GetFailedTestsAmountForRecord(i); + } + } + return -1; +} + +/** + * Checks whether all tests recorded in this summary have passed. + * + * @return `true` if all tests have passed, `false` otherwise. + */ +public final function bool HasPassedAllTests() +{ + return (GetFailedTestsAmount() <= 0); +} + +/** + * Checks whether all tests, performed under given context and + * recorded in this summary, have passed. + * + * @return `true` if all tests under given context have passed, + * `false` otherwise. + * If given context does not exists - it did not fail any tests. + */ +public final function bool HasPassedAllTestsForContext(string context) +{ + return (GetFailedTestsAmountForContext(context) <= 0); +} + +/** + * Generates a text summary for a set of results, given as array of + * `TestCaseSummary`s (exactly how results are returned by `TestingService`). + * + * @param summaries `TestCase` summaries (obtained as a result of testing) + * that we want to display. + * @return Test representation of `summaries` as an array of + * formatted strings, where each string corresponds to it's own line. + */ +public final static function array GenerateStringSummary( + array summaries) +{ + local int i; + local bool allTestsPassed; + local array result; + allTestsPassed = true; + result[0] = default.reportHeader; + for (i = 0; i < summaries.length; i += 1) + { + if (summaries[i] == none) continue; + summaries[i].AppendCaseSummary(result); + allTestsPassed = allTestsPassed && summaries[i].HasPassedAllTests(); + } + if (allTestsPassed) { + result[result.length] = default.reportSuccessfulEnding; + } + else { + result[result.length] = default.reportUnsuccessfulEnding; + } + return result; +} + +// Add text representation of caller `TestCase` to the existing array `result`. +private final function AppendCaseSummary(out array result) +{ + local int i, j; + local array contexts; + local string testCaseAnnouncement; + local array issues; + if (ownerCase == none) return; + // Announce case + testCaseAnnouncement = "{$text_default Test case {$text_emphasis"; + if (ownerCase.static.GetGroup() != "") { + testCaseAnnouncement @= "[" $ ownerCase.static.GetGroup() $ "]"; + } + testCaseAnnouncement @= ownerCase.static.GetName() $ "}:}"; + if (GetFailedTestsAmount() > 0) { + testCaseAnnouncement @= "{$text_failure failed}!"; + } + else { + testCaseAnnouncement @= "{$text_ok passed}!"; + } + result[result.length] = testCaseAnnouncement; + // Report failed tests + contexts = GetContexts(); + for (i = 0;i < contexts.length; i += 1) + { + if (GetFailedTestsAmountForContext(contexts[i]) <= 0) continue; + result[result.length] = "{$text_warning " $ contexts[i] $ "}"; + issues = GetIssueSummariesForContext(contexts[i]); + for (j = 0; j < issues.length; j += 1) + { + if (issues[j] == none) continue; + if (issues[j].GetFailedTestsAmount() <= 0) continue; + result[result.length] = indent $ issues[j].ToString(); + } + } +} + +defaultproperties +{ + indent = " " + reportHeader = "{$text_default ############################## {$text_emphasis Test summary} ###############################}" + reportSuccessfulEnding = "{$text_default ########################### {$text_ok All tests have passed!} ############################}" + reportUnsuccessfulEnding = "{$text_default ########################## {$text_failure Some tests have failed :(} ###########################}" +} \ No newline at end of file From 27c88b8707aef783f75c4781b4f12e3ae01ea39c Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:17:22 +0700 Subject: [PATCH 15/59] Refactor Aliases subsystem --- sources/Core/Aliases/AliasHash.uc | 218 ++++++++++ sources/Core/Aliases/AliasService.uc | 135 +++++++ sources/Core/Aliases/AliasSource.uc | 379 ++++++++++++++++++ sources/Core/Aliases/Aliases.uc | 192 ++++----- sources/Core/Aliases/AliasesAPI.uc | 212 +++++++++- .../BuiltInSources/ColorAliasSource.uc | 27 ++ .../Aliases/BuiltInSources/ColorAliases.uc | 27 ++ .../BuiltInSources/WeaponAliasSource.uc | 27 ++ .../Aliases/BuiltInSources/WeaponAliases.uc | 27 ++ 9 files changed, 1138 insertions(+), 106 deletions(-) create mode 100644 sources/Core/Aliases/AliasHash.uc create mode 100644 sources/Core/Aliases/AliasService.uc create mode 100644 sources/Core/Aliases/AliasSource.uc create mode 100644 sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc create mode 100644 sources/Core/Aliases/BuiltInSources/ColorAliases.uc create mode 100644 sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc create mode 100644 sources/Core/Aliases/BuiltInSources/WeaponAliases.uc diff --git a/sources/Core/Aliases/AliasHash.uc b/sources/Core/Aliases/AliasHash.uc new file mode 100644 index 0000000..43da820 --- /dev/null +++ b/sources/Core/Aliases/AliasHash.uc @@ -0,0 +1,218 @@ +/** + * A class, implementing a hash-table-based dictionary for quick access to + * aliases' values. + * It does not support dynamic hash table capacity change and + * requires to set the size upfront. + * 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 AliasHash extends AcediaObject + dependson(AliasSource) + config(AcediaSystem); + +// Reasonable lower and upper limits on hash table capacity, +// that will be enforced if user requires something outside those bounds +var private config const int MINIMUM_CAPACITY; +var private config const int MAXIMUM_CAPACITY; + +// Bucket of alias-value pairs, with the same alias hash. +struct PairBucket +{ + var array pairs; +}; +var private array hashTable; + +/** + * Initializes caller `AliasHash`. + * + * Calling this function again will clear all existing data and will create + * a brand new hash table. + * + * @param desiredCapacity Desired capacity of the underlying hash table. + * Will be clamped between `MINIMUM_CAPACITY` and `MAXIMUM_CAPACITY`. + * Not specifying anything as this parameter creates a hash table of + * size `MINIMUM_CAPACITY`. + * @return A reference to a caller object to allow for function chaining. + */ +public final function AliasHash Initialize(optional int desiredCapacity) +{ + desiredCapacity = Clamp(desiredCapacity, MINIMUM_CAPACITY, + MAXIMUM_CAPACITY); + hashTable.length = 0; + hashTable.length = desiredCapacity; + return self; +} + +// Helper method that is needed as a replacement for `%`, since it is +// an operation on `float`s in UnrealScript and does not have enough precision +// to work with hashes. +// Assumes positive input. +private function int Remainder(int number, int divisor) +{ + local int quotient; + quotient = number / divisor; + return (number - quotient * divisor); +} + +// Finds indices for: +// 1. Bucked that contains specified alias (`bucketIndex`); +// 2. Pair for specified alias in the bucket's collection (`pairIndex`). +// `bucketIndex` is always found, +// `pairIndex` is valid iff method returns `true`. +private final function bool FindPairIndices( + string alias, + out int bucketIndex, + out int pairIndex) +{ + local int i; + local array bucketPairs; + // `Locs()` is used because aliases are case-insensitive. + bucketIndex = _().text.GetHash(Locs(alias)); + if (bucketIndex < 0) { + bucketIndex *= -1; + } + bucketIndex = Remainder(bucketIndex, hashTable.length); + // Check if bucket actually has given alias. + bucketPairs = hashTable[bucketIndex].pairs; + for (i = 0; i < bucketPairs.length; i += 1) + { + if (bucketPairs[i].alias ~= alias) + { + pairIndex = i; + return true; + } + } + return false; +} + +/** + * Finds a value for a given alias. + * + * @param alias Alias for which we need to find a value. + * Aliases are case-insensitive. + * @param value If given alias is present in caller `AliasHash`, - + * it's value will be written in this variable. + * Otherwise value is undefined. + * @return `true` if we found value, `false` otherwise. + */ +public final function bool Find(string alias, out string value) +{ + local int bucketIndex; + local int pairIndex; + if (FindPairIndices(alias, bucketIndex, pairIndex)) + { + value = hashTable[bucketIndex].pairs[pairIndex].value; + return true; + } + return false; +} + +/** + * Checks if caller `AliasHash` contains given alias. + * + * @param alias Alias to check for belonging to caller `AliasHash`. + * Aliases are case-insensitive. + * @return `true` if caller `AliasHash` contains the value for a given alias + * and `false` otherwise. + */ +public final function bool Contains(string alias) +{ + local int bucketIndex; + local int pairIndex; + return FindPairIndices(alias, bucketIndex, pairIndex); +} + +/** + * Inserts new record for alias `alias` for value of `value`. + * + * If there is already a value for a given `alias` - it will be overwritten. + * + * @param alias Alias to insert. Aliases are case-insensitive. + * @param value Value for a given alias to store. + * @return A reference to a caller object to allow for function chaining. + */ +public final function AliasHash Insert(string alias, string value) +{ + local int bucketIndex; + local int pairIndex; + local AliasSource.AliasValuePair newRecord; + newRecord.value = value; + newRecord.alias = alias; + if (!FindPairIndices(alias, bucketIndex, pairIndex)) { + pairIndex = hashTable[bucketIndex].pairs.length; + } + hashTable[bucketIndex].pairs[pairIndex] = newRecord; + return self; +} + +/** + * Inserts new record for alias `alias` for value of `value`. + * + * If there is already a value for a given `alias`, - new value will be + * discarded and `AliasHash` will not be changed. + * + * @param alias Alias to insert. Aliases are case-insensitive. + * @param value Value for a given alias to store. + * @param existingValue Value that will correspond to a given alias after + * this method's execution. If insertion was successful - given `value`, + * otherwise (if there already was a record for an `alias`) + * it will return value that already existed in caller `AliasHash`. + * @return `true` if given alias-value pair was inserted and `false` otherwise. + */ +public final function bool InsertIfMissing( + string alias, + string value, + out string existingValue) +{ + local int bucketIndex; + local int pairIndex; + local AliasSource.AliasValuePair newRecord; + newRecord.value = value; + newRecord.alias = alias; + existingValue = value; + if (FindPairIndices(alias, bucketIndex, pairIndex)) { + existingValue = hashTable[bucketIndex].pairs[pairIndex].value; + return false; + } + pairIndex = hashTable[bucketIndex].pairs.length; + hashTable[bucketIndex].pairs[pairIndex] = newRecord; + return true; +} + +/** + * Removes record, corresponding to a given alias `alias`. + * + * @param alias Alias for which all records must be removed. + * @return `true` if record was removed, `false` if id did not + * (can only happen when `AliasHash` did not have any records for `alias`). + */ +public final function bool Remove(string alias) +{ + local int bucketIndex; + local int pairIndex; + if (FindPairIndices(alias, bucketIndex, pairIndex)) { + hashTable[bucketIndex].pairs.Remove(pairIndex, 1); + return true; + } + return false; +} + +defaultproperties +{ + MINIMUM_CAPACITY = 10 + MAXIMUM_CAPACITY = 100000 +} \ No newline at end of file diff --git a/sources/Core/Aliases/AliasService.uc b/sources/Core/Aliases/AliasService.uc new file mode 100644 index 0000000..ac9cd94 --- /dev/null +++ b/sources/Core/Aliases/AliasService.uc @@ -0,0 +1,135 @@ +/** + * Service that handles pending saving of aliases data into configs. + * Adding aliases into `AliasSource`s causes corresponding configs to update. + * This service allows to delay and spread config rewrites over time, + * which should help in case someone dynamically adds a lot of + * different aliases. + * 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 AliasService extends Service + config(AcediaSystem); + +// Objects for which we are yet to write configs +var private array sourcesPendingToSave; +var private array aliasesPendingToSave; +// How often should we do it. +// Negative or zero values would be reset to `0.05`. +var public config const float saveInterval; + +// To avoid creating yet another object for aliases system we will +// keep config variable pointing to weapon, color, etc. `AliasSource` +// subclasses here. It's not the best regarding separation of responsibility, +// but should make config files less fragmented. +// Changing these allows you to change in what sources `AliasesAPI` +// looks for weapon and color aliases. +var public config const class weaponAliasesSource; +var public config const class colorAliasesSource; + +protected function OnLaunch() +{ + local float actualInterval; + actualInterval = saveInterval; + if (actualInterval <= 0) + { + actualInterval = 0.05; + } + SetTimer(actualInterval, true); +} + +protected function OnShutdown() +{ + SaveAllPendingObjects(); +} + +public final function PendingSaveSource(AliasSource sourceToSave) +{ + local int i; + if (sourceToSave == none) return; + // Starting searching from the end of an array will make situations when + // we add several aliases to a single source in a row more efficient. + for (i = sourcesPendingToSave.length - 1;i >= 0; i -= 1) { + if (sourcesPendingToSave[i] == sourceToSave) return; + } + sourcesPendingToSave[sourcesPendingToSave.length] = sourceToSave; +} + +public final function PendingSaveObject(Aliases objectToSave) +{ + local int i; + if (objectToSave == none) return; + // Starting searching from the end of an array will make situations when + // we add several aliases to a single `Aliases` object in a row + // more efficient. + for (i = aliasesPendingToSave.length - 1;i >= 0; i -= 1) { + if (aliasesPendingToSave[i] == objectToSave) return; + } + aliasesPendingToSave[aliasesPendingToSave.length] = objectToSave; +} + +/** + * Forces saving of the next object (either `AliasSource` or `Aliases`) + * in queue to the config file. + * + * Does not reset the timer until next saving. + */ +private final function DoSaveNextPendingObject() +{ + if (sourcesPendingToSave.length > 0) + { + if (sourcesPendingToSave[0] != none) { + sourcesPendingToSave[0].SaveConfig(); + } + sourcesPendingToSave.Remove(0, 1); + return; + } + if (aliasesPendingToSave.length > 0) + { + aliasesPendingToSave[0].SaveOrClear(); + aliasesPendingToSave.Remove(0, 1); + } +} + +/** + * Forces saving of all objects (both `AliasSource`s or `Aliases`s) in queue + * to their config files. + */ +private final function SaveAllPendingObjects() +{ + local int i; + for (i = 0; i < sourcesPendingToSave.length; i += 1) { + if (sourcesPendingToSave[i] == none) continue; + sourcesPendingToSave[i].SaveConfig(); + } + for (i = 0; i < aliasesPendingToSave.length; i += 1) { + aliasesPendingToSave[i].SaveOrClear(); + } + sourcesPendingToSave.length = 0; + aliasesPendingToSave.length = 0; +} + +event Timer() +{ + DoSaveNextPendingObject(); +} + +defaultproperties +{ + saveInterval = 0.05 + weaponAliasesSource = class'WeaponAliasSource' + colorAliasesSource = class'ColorAliasSource' +} \ No newline at end of file diff --git a/sources/Core/Aliases/AliasSource.uc b/sources/Core/Aliases/AliasSource.uc new file mode 100644 index 0000000..6f863ea --- /dev/null +++ b/sources/Core/Aliases/AliasSource.uc @@ -0,0 +1,379 @@ +/** + * Aliases allow users to define human-readable and easier to use + * "synonyms" to some symbol sequences (mainly names of UnrealScript classes). + * This class implements an alias database that stores aliases inside + * standard config ini-files. + * Several `AliasSource`s are supposed to exist separately, each storing + * aliases of particular kind: for weapon, zeds, colors, etc.. + * 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 AliasSource extends Singleton + config(AcediaAliases); + +// Name of the configurational file (without extension) where +// this `AliasSource`'s data will be stored. +var private const string configName; + +// (Sub-)class of `Aliases` objects that this `AliasSource` uses to store +// aliases in per-object-config manner. +// Leaving this variable `none` will produce an `AliasSource` that can +// only store aliases in form of `record=(alias="...",value="...")`. +var public const class aliasesClass; +// Storage for all objects of `aliasesClass` class in the config. +// Exists after `OnCreated()` event and is maintained up-to-date at all times. +var private array loadedAliasObjects; + +// Links alias to a value. +// An array of these structures (without duplicate `alias` records) defines +// a function from the space of aliases to the space of values. +struct AliasValuePair +{ + var string alias; + var string value; +}; +// Aliases data for saving and loading on a disk (ini-file). +// Name is chosen to make configurational files more readable. +var private config array record; +// Hash table for a faster access to value by alias' name. +// It contains same records as `record` array + aliases from +// `loadedAliasObjects` objects when there are no duplicate aliases. +// Otherwise only stores first loaded alias. +var private AliasHash hash; + + +// How many times bigger capacity of `hash` should be, compared to amount of +// initially loaded data from a config. +var private const float HASH_TABLE_SCALE; + +// Load and hash all the data `AliasSource` creation. +protected function OnCreated() +{ + local int entriesAmount; + if (!AssertAliasesClassIsOwnedByMe()) { + return; + } + // Load and hash + entriesAmount = LoadData(); + hash = AliasHash(_.memory.Allocate(class'AliasHash')); + hash.Initialize(int(entriesAmount * HASH_TABLE_SCALE)); + HashValidAliases(); +} + +// Ensures invariant of our `Aliases` class only belonging to us by +// itself ourselves otherwise. +private final function bool AssertAliasesClassIsOwnedByMe() +{ + 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."); + Destroy(); + return false; +} + +// This method loads all the defined aliases from the config file and +// returns how many entries are there are total. +// Does not change data, including fixing duplicates. +private final function int LoadData() +{ + local int i; + local int entriesAmount; + local array objectNames; + entriesAmount = record.length; + if (aliasesClass == none) { + return entriesAmount; + } + objectNames = + GetPerObjectNames(configName, string(aliasesClass.name), MaxInt); + loadedAliasObjects.length = objectNames.length; + for (i = 0; i < objectNames.length; i += 1) + { + loadedAliasObjects[i] = new(none, objectNames[i]) aliasesClass; + entriesAmount += loadedAliasObjects[i].GetAliases().length; + } + return entriesAmount; +} + +/** + * Simply checks if given alias is present in caller `AliasSource`. + * + * @param alias Alias to check, case-insensitive. + * @return `true` if present, `false` otherwise. + */ +public function bool ContainsAlias(string alias) +{ + return hash.Contains(alias); +} + +/** + * Tries to look up a value, stored for given alias in caller `AliasSource` and + * reports error upon failure. + * + * Also see `Try()` method. + * + * @param alias Alias, for which method will attempt to look up a value. + * Case-insensitive. + * @param value If passed `alias` was recorded in caller `AliasSource`, + * it's corresponding value will be written in this variable. + * Otherwise value is undefined. + * @return `true` if lookup was successful (alias present in 'AliasSource`) + * and correct value was written into `value`, `false` otherwise. + */ +public function bool Resolve(string alias, out string value) +{ + return hash.Find(alias, value); +} + +/** + * Tries to look up a value, stored for given alias in caller `AliasSource` and + * silently returns given `alias` value upon failure. + * + * Also see `Resolve()` method. + * + * @param alias Alias, for which method will attempt to look up a value. + * Case-insensitive. + * @return Value corresponding to a given alias, if it was present in + * caller `AliasSource` and value of `alias` parameter instead. + */ +public function string Try(string alias) +{ + local string result; + if (hash.Find(alias, result)) { + return result; + } + return alias; +} + +/** + * Adds another alias to the caller `AliasSource`. + * If alias with the same name as `aliasToAdd` already exists, - + * method overwrites it. + * + * Can fail iff `aliasToAdd` is an invalid alias. + * + * When adding alias to an object (`saveInObject == true`) alias `aliasToAdd` + * will be altered by changing any ':' inside it into a '.'. + * This is a necessary measure to allow storing class names in + * config files via per-object-config. + * + * NOTE: This call will cause update of an ini-file. That update can be + * slightly delayed, so do not make assumptions about it's immediacy. + * + * NOTE #2: Removing alias would require this method to go through the + * whole `AliasSource` to remove possible duplicates. + * This means that unless you can guarantee that there is no duplicates, - + * performing a lot of alias additions during run-time can be costly. + * + * @param aliasToAdd Alias that you want to add to caller source. + * Alias names are case-insensitive. + * @param aliasValue Intended value of this alias. + * @param saveInObject Setting this to `true` will make `AliasSource` save + * given alias in per-object-config storage, while keeping it at default + * `false` will just add alias to the `record=` storage. + * If caller `AliasSource` does not support per-object-config storage, - + * this flag will be ignores. + * @return `true` if alias was added and `false` otherwise (alias was invalid). + */ +public final function bool AddAlias( + string aliasToAdd, + string aliasValue, + optional bool saveInObject) +{ + local AliasValuePair newPair; + if (_.alias.IsAliasValid(aliasToAdd)) { + return false; + } + if (hash.Contains(aliasToAdd)) { + RemoveAlias(aliasToAdd); + } + // We might not be able to use per-object-config storage + if (saveInObject && aliasesClass == none) { + saveInObject = false; + _.logger.Warning("Cannot save alias in object for source `" + $ string(class) + $ "`, because it does not have appropriate `Aliases` class setup."); + } + // Save + if (saveInObject) { + GetAliasesObjectWithValue(aliasValue).AddAlias(aliasToAdd); + } + else + { + newPair.alias = aliasToAdd; + newPair.value = aliasValue; + record[record.length] = newPair; + } + hash.Insert(aliasToAdd, aliasValue); + AliasService(class'AliasService'.static.Require()).PendingSaveSource(self); + return true; +} + +/** + * Removes alias (all records with it, in case of duplicates) from + * the caller `AliasSource`. + * + * Cannot fail. + * + * NOTE: This call will cause update of an ini-file. That update can be + * slightly delayed, so do not make assumptions about it's immediacy. + * + * NOTE #2: removing alias requires this method to go through the + * whole `AliasSource` to remove possible duplicates, which can make + * performing a lot of alias removal during run-time costly. + * + * @param aliasToRemove Alias that you want to remove from caller source. + */ +public final function RemoveAlias(string aliasToRemove) +{ + local int i; + local bool removedAliasFromRecord; + hash.Remove(aliasToRemove); + while (i < record.length) + { + if (record[i].alias ~= aliasToRemove) + { + record.Remove(i, 1); + removedAliasFromRecord = true; + } + else { + i += 1; + } + } + for (i = 0; i < loadedAliasObjects.length; i += 1) { + loadedAliasObjects[i].RemoveAlias(aliasToRemove); + } + if (removedAliasFromRecord) + { + AliasService(class'AliasService'.static.Require()) + .PendingSaveSource(self); + } +} + +// Performs initial hashing of every record with valid alias. +// In case of duplicate or invalid aliases - method will skip them +// and log warnings. +private final function HashValidAliases() +{ + if (hash == none) { + _.logger.Warning("Alias source `" $ string(class) $ "` called" + $ "`HashValidAliases()` function without creating an `AliasHasher`" + $ "instance first. This should not have happened."); + return; + } + HashValidAliasesFromRecord(); + HashValidAliasesFromPerObjectConfig(); +} + +private final function LogDuplicateAliasWarning( + string alias, + string existingValue) +{ + _.logger.Warning("Alias source `" $ string(class) + $ "` has duplicate record for alias \"" $ alias + $ "\". This is likely due to an erroneous config. \"" $ existingValue + $ "\" value will be used."); +} + +private final function LogInvalidAliasWarning(string invalidAlias) +{ + _.logger.Warning("Alias source `" $ string(class) + $ "` contains invalid alias name \"" $ invalidAlias + $ "\". This alias will not be loaded."); +} + +private final function HashValidAliasesFromRecord() +{ + local int i; + local bool isDuplicate; + local string existingValue; + for (i = 0; i < record.length; i += 1) + { + if (!_.alias.IsAliasValid(record[i].alias)) + { + LogInvalidAliasWarning(record[i].alias); + continue; + } + isDuplicate = !hash.InsertIfMissing(record[i].alias, record[i].value, + existingValue); + if (isDuplicate) { + LogDuplicateAliasWarning(record[i].alias, existingValue); + } + } +} + +private final function HashValidAliasesFromPerObjectConfig() +{ + local int i, j; + local bool isDuplicate; + local string existingValue; + local string objectValue; + local array objectAliases; + for (i = 0; i < loadedAliasObjects.length; i += 1) + { + objectValue = loadedAliasObjects[i].GetValue(); + objectAliases = loadedAliasObjects[i].GetAliases(); + for (j = 0; j < objectAliases.length; j += 1) + { + if (!_.alias.IsAliasValid(objectAliases[j])) + { + LogInvalidAliasWarning(objectAliases[j]); + continue; + } + isDuplicate = !hash.InsertIfMissing(objectAliases[j], objectValue, + existingValue); + if (isDuplicate) { + LogDuplicateAliasWarning(objectAliases[j], existingValue); + } + } + } +} + +// Tries to find a loaded `Aliases` config object that stores aliases for +// the given value. If such object does not exists - creates a new one. +private final function Aliases GetAliasesObjectWithValue(string value) +{ + local int i; + local Aliases newAliasesObject; + // This method only makes sense if this `AliasSource` supports + // per-object-config storage. + if (aliasesClass == none) + { + _.logger.Warning("`GetAliasesObjectForValue()` function was called for " + $ "alias source with `aliasesClass == none`." + $ "This should not happen."); + return none; + } + for (i = 0; i < loadedAliasObjects.length; i += 1) + { + if (loadedAliasObjects[i].GetValue() ~= value) { + return loadedAliasObjects[i]; + } + } + newAliasesObject = new(none, value) aliasesClass; + loadedAliasObjects[loadedAliasObjects.length] = newAliasesObject; + return newAliasesObject; +} + +defaultproperties +{ + // Source main parameters + configName = "AcediaAliases" + aliasesClass = class'Aliases' + // HashTable twice the size of data entries should do it + HASH_TABLE_SCALE = 2.0 +} \ No newline at end of file diff --git a/sources/Core/Aliases/Aliases.uc b/sources/Core/Aliases/Aliases.uc index 4e4ecab..8b30683 100644 --- a/sources/Core/Aliases/Aliases.uc +++ b/sources/Core/Aliases/Aliases.uc @@ -1,20 +1,14 @@ /** - * Aliases allow users to define human-readable and easier to use - * "synonyms" to some symbol sequences (mainly names of UnrealScript classes). - * Due to how aliases are stored, there is a limitation on original - * values to which aliases refer: it must be a valid object name to store via - * `perObjectConfig`. For example it cannot contain `]` or a dot `.` - * (use `:` as a delimiter for class names: `KFMod:M14EBRBattleRifle`). - * Aliases can be grouped into categories: "weapons", "test", "maps", etc. - * Aliases can be configured in `AcediaAliases` in form: - * ________________________________________________________________________ - * | [/ Aliases] - * | Alias="" - * | Alias="" - * | ... - * |_______________________________________________________________________ - * where , , , ... can be replaced with - * desired values. + * This is a simple helper object for `AliasSource` that can store + * an array of aliases in config files in a per-object-config manner. + * One `Aliases` object can store several aliases for a single value. + * It is recommended that you do not try to access these objects directly. + * Class name `Aliases` is chosen to make configuration files + * more readable. + * It's only interesting function is storing '.'s as ':' in it's config, + * which is necessary to allow storing aliases for class names via + * these objects (since UnrealScript's cannot handle '.'s in object's names + * in it's configs). * Copyright 2020 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -36,105 +30,113 @@ class Aliases extends AcediaObject perObjectConfig config(AcediaAliases); +// Link to the `AliasSource` that uses `Aliases` objects of this class. +// To ensure that any `Aliases` sub-class only belongs to one `AliasSource`. +var public const class sourceClass; + +// Aliases, recorded by this `Aliases` object that all mean the same value, +// defined by this object's name `string(self.name)`. +var protected config array alias; + +// Since '.'s in values are converted into ':' for storage purposes, +// we need methods to convert between "storage" and "actual" value version. +// `ToStorageVersion()` and `ToActualVersion()` do that. +private final function string ToStorageVersion(string actualValue) +{ + return Repl(actualValue, ".", ":"); +} + +private final function string ToActualVersion(string storageValue) +{ + return Repl(storageValue, ":", "."); +} + /** - * All data is stored in config as a bunch of named `Aliases` objects - * (via `perObjectConfig`). Name of each object records both aliases group and - * value (see class description for details). - * Aliases themselves are recorded into the `alias` array. + * Returns value that caller's `Aliases` object's aliases point to. + * + * @return Value, stored by this object. */ - -// Stores name of the configuration file. -var private const string configName; -// Both value -// Symbol (or symbol sequence) that separates value from the group in -// `[/ Aliases]`. -var private const string delimiter; - -// Set once to prevent more than one object loading. -var private bool initialized; - -// All aliases objects, specified by the configuration file. -var private array availableRecords; - -// Data loaded from the configuration file into the `Aliases` object. -// Value to which all aliases refer to. -var private string originalValue; -// Group to which this object's aliases belong to. -var private string groupName; -// Recorded aliases ("synonyms") for the `originalValue`. -var public config array alias; - -// Initializes data that we can not directly read from the configuration file. -private final function Initialize() +public final function string GetValue() { - if (initialized) return; - - availableRecords.length = 0; - ParseObjectName(string(self.name)); - initialized = true; + return ToActualVersion(string(self.name)); } -private final function ParseObjectName(string configName) +/** + * Returns array of aliases that caller `Aliases` tells us point to it's value. + * + * @return Array of all aliases, stored by caller `Aliases` object. + */ +public final function array GetAliases() { - local int i; - local array splitName; - Split(configName, "/", splitName); - groupName = splitName[0]; - originalValue = ""; - for (i = 1; i < splitName.length; i += 1) - { - originalValue $= splitName[i]; + return alias; +} + +/** + * [For inner use by `AliasSource`] Adds new alias to this object. + * + * Does no duplicates checks through for it's `AliasSource` and + * neither it updates relevant `AliasHash`, + * but will prevent adding duplicate records inside it's own storage. + * + * @param aliasToAdd Alias to add to caller `Aliases` object. + */ +public final function AddAlias(string aliasToAdd) +{ + local int i; + for (i = 0; i < alias.length; i += 1) { + if (alias[i] ~= aliasToAdd) return; } + alias[alias.length] = ToStorageVersion(aliasToAdd); + AliasService(class'AliasService'.static.Require()) + .PendingSaveObject(self); } -// This function loads all the defined aliases from the config file. -// Need to only be called once, further calls do nothing. -public static final function LoadAliases() +/** + * [For inner use by `AliasSource`] Removes alias from this object. + * + * Does not update relevant `AliasHash`. + * + * Will prevent adding duplicate records inside it's own storage. + * + * @param aliasToRemove Alias to remove from caller `Aliases` object. + */ +public final function RemoveAlias(string aliasToRemove) { - local int i; - local array recordNames; - if (default.initialized) return; - recordNames = - GetPerObjectNames(default.configName, string(class'Aliases'.name)); - for (i = 0; i < recordNames.length; i += 1) + local int i; + local bool removedAlias; + while (i < alias.length) { - default.availableRecords[i] = new(none, recordNames[i]) class'Aliases'; - if (default.availableRecords[i] != none) + if (alias[i] ~= aliasToRemove) { - default.availableRecords[i].Initialize(); + alias.Remove(i, 1); + removedAlias = true; + } + else { + i += 1; } } - default.initialized = true; + if (removedAlias) + { + AliasService(class'AliasService'.static.Require()) + .PendingSaveObject(self); + } } -// Tries to find original value for a given alias in a given group. -public static final function bool ResolveAlias -( - string group, - string alias, - out string result -) +/** + * If this object still has any alias records, - forces a rewrite of it's data + * into the config file, otherwise - removes it's record entirely. + */ +public final function SaveOrClear() { - local int i, j; - if (!default.initialized) return false; - for (i = 0; i < default.availableRecords.length; i += 1) - { - if (!(default.availableRecords[i].groupName ~= group)) continue; - for (j = 0; j < default.availableRecords[i].alias.length; j += 1) - { - if (default.availableRecords[i].alias[j] ~= alias) - { - result = default.availableRecords[i].originalValue; - return true; - } - } + if (alias.length <= 0) { + ClearConfig(); + } + else { + SaveConfig(); } - return false; } defaultproperties { - initialized = false - configName = "AcediaAliases" - delimiter = "/" + sourceClass = class'AliasSource' } \ No newline at end of file diff --git a/sources/Core/Aliases/AliasesAPI.uc b/sources/Core/Aliases/AliasesAPI.uc index 7ebab8c..d231640 100644 --- a/sources/Core/Aliases/AliasesAPI.uc +++ b/sources/Core/Aliases/AliasesAPI.uc @@ -19,21 +19,211 @@ */ class AliasesAPI extends Singleton; -// Resolves original value for given alias and it's group. -// Returns `false` if there no such alias and `true` if there is. -public function bool Resolve(string group, string alias, out string result) +/** + * Checks that passed value is a valid alias name. + * + * A valid name is any name consisting out of 128 ASCII symbols. + * + * @param aliasToCheck Alias to check for validity. + * @return `true` if `aliasToCheck` is a valid alias and `false` otherwise. + */ +public final function bool IsAliasValid(string aliasToCheck) { - return class'Aliases'.static.ResolveAlias(group, alias, result); + return _.text.IsASCIIString(aliasToCheck); } -// Tries to resolve given alias. -// If fails - returns passed `alias` value back. -public function string Try(string group, string alias) +/** + * Provides an easier access to the instance of the `AliasSource` of + * the given class. + * + * Can fail if `customSourceClass` is incorrectly defined. + * + * @param customSourceClass Class of the source we want. + * @return Instance of the requested `AliasSource`, + * `none` if `customSourceClass` is incorrectly defined. + */ +public final function AliasSource GetCustomSource( + class customSourceClass) { - local string result; - if (class'Aliases'.static.ResolveAlias(group, alias, result)) - { - return result; + return AliasSource(customSourceClass.static.GetInstance(true)); +} + +/** + * Returns `AliasSource` that is designated in configuration files as + * a source for weapon aliases. + * + * NOTE: while by default weapon aliases source will contain only weapon + * aliases, you should not assume that. Acedia allows admins to store all + * the aliases in the same config. + * + * @return Reference to the `AliasSource` that contains weapon aliases. + * Can return `none` if no source for weapons was configured or + * the configured source is incorrectly defined. + */ +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."); + 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`."); + return none; + } + return weaponSource; +} + +/** + * Returns `AliasSource` that is designated in configuration files as + * a source for color aliases. + * + * NOTE: while by default color aliases source will contain only color aliases, + * you should not assume that. Acedia allows admins to store all the aliases + * in the same config. + * + * @return Reference to the `AliasSource` that contains color aliases. + * Can return `none` if no source for colors was configured or + * the configured source is incorrectly defined. + */ +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."); + 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`."); + return none; + } + return colorSource; +} + +/** + * Tries to look up a value, stored for given alias in an `AliasSource` + * configured to store weapon aliases. Reports error on failure. + * + * Lookup of alias can fail if either alias does not exist in weapon alias + * source or weapon alias source itself does not exist + * (due to either faulty configuration or incorrect definition). + * To determine if weapon alias source exists you can check + * `_.alias.GetWeaponSource()` value. + * + * Also see `TryWeapon()` method. + * + * @param alias Alias, for which method will attempt to look up a value. + * Case-insensitive. + * @param value If passed `alias` was recorded as a weapon alias, + * it's corresponding value will be written in this variable. + * Otherwise value is undefined. + * @return `true` if lookup was successful and `false` otherwise. + */ +public final function bool ResolveWeapon(string alias, out string result) +{ + local AliasSource source; + source = GetWeaponSource(); + if (source != none) { + return source.Resolve(alias, result); + } + return false; +} + +/** + * Tries to look up a value, stored for given alias in an `AliasSource` + * configured to store weapon aliases and silently returns given `alias` + * value upon failure. + * + * Lookup of alias can fail if either alias does not exist in weapon alias + * source or weapon alias source itself does not exist + * (due to either faulty configuration or incorrect definition). + * To determine if weapon alias source exists you can check + * `_.alias.GetWeaponSource()` value. + * + * Also see `ResolveWeapon()` method. + * + * @param alias Alias, for which method will attempt to look up a value. + * Case-insensitive. + * @return Weapon value corresponding to a given alias, if it was present in + * the weapon alias source and value of `alias` parameter instead. + */ +public function string TryWeapon(string alias) +{ + local AliasSource source; + source = GetWeaponSource(); + if (source != none) { + return source.Try(alias); + } + return alias; +} + +/** + * Tries to look up a value, stored for given alias in an `AliasSource` + * configured to store color aliases. Reports error on failure. + * + * Lookup of alias can fail if either alias does not exist in color alias + * source or color alias source itself does not exist + * (due to either faulty configuration or incorrect definition). + * To determine if color alias source exists you can check + * `_.alias.GetColorSource()` value. + * + * Also see `TryColor()` method. + * + * @param alias Alias, for which method will attempt to look up a value. + * Case-insensitive. + * @param value If passed `alias` was recorded as a color alias, + * it's corresponding value will be written in this variable. + * Otherwise value is undefined. + * @return `true` if lookup was successful and `false` otherwise. + */ +public final function bool ResolveColor(string alias, out string result) +{ + local AliasSource source; + source = GetColorSource(); + if (source != none) { + return source.Resolve(alias, result); + } + return false; +} + +/** + * Tries to look up a value, stored for given alias in an `AliasSource` + * configured to store color aliases and silently returns given `alias` + * value upon failure. + * + * Lookup of alias can fail if either alias does not exist in color alias + * source or color alias source itself does not exist + * (due to either faulty configuration or incorrect definition). + * To determine if color alias source exists you can check + * `_.alias.GetColorSource()` value. + * + * Also see `ResolveColor()` method. + * + * @param alias Alias, for which method will attempt to look up a value. + * Case-insensitive. + * @return Color value corresponding to a given alias, if it was present in + * the color alias source and value of `alias` parameter instead. + */ +public function string TryColor(string alias) +{ + local AliasSource source; + source = GetColorSource(); + if (source != none) { + return source.Try(alias); } return alias; } diff --git a/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc b/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc new file mode 100644 index 0000000..5cd75cf --- /dev/null +++ b/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc @@ -0,0 +1,27 @@ +/** + * Source intended for color aliases. + * 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 ColorAliasSource extends AliasSource + config(AcediaAliases_Colors); + +defaultproperties +{ + configName = "AcediaAliases_Colors" + aliasesClass = class'ColorAliases' +} \ No newline at end of file diff --git a/sources/Core/Aliases/BuiltInSources/ColorAliases.uc b/sources/Core/Aliases/BuiltInSources/ColorAliases.uc new file mode 100644 index 0000000..d0998b6 --- /dev/null +++ b/sources/Core/Aliases/BuiltInSources/ColorAliases.uc @@ -0,0 +1,27 @@ +/** + * Per-object-configuration intended for color aliases. + * 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 ColorAliases extends Aliases + perObjectConfig + config(AcediaAliases_Colors); + +defaultproperties +{ + sourceClass = class'ColorAliasSource' +} \ No newline at end of file diff --git a/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc b/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc new file mode 100644 index 0000000..0cf1bc4 --- /dev/null +++ b/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc @@ -0,0 +1,27 @@ +/** + * Source intended for weapon aliases. + * 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 WeaponAliasSource extends AliasSource + config(AcediaAliases_Weapons); + +defaultproperties +{ + configName = "AcediaAliases_Weapons" + aliasesClass = class'WeaponAliases' +} \ No newline at end of file diff --git a/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc b/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc new file mode 100644 index 0000000..82acd45 --- /dev/null +++ b/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc @@ -0,0 +1,27 @@ +/** + * Per-object-configuration intended for weapon aliases. + * 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 WeaponAliases extends Aliases + perObjectConfig + config(AcediaAliases_Weapons); + +defaultproperties +{ + sourceClass = class'WeaponAliasSource' +} \ No newline at end of file From 5a14bb6d2c9f3c4facfdcb6647328a7816378b47 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:17:47 +0700 Subject: [PATCH 16/59] Add testing to Aliases subsystem --- sources/Core/Aliases/Tests/MockAliasSource.uc | 27 ++++ sources/Core/Aliases/Tests/MockAliases.uc | 27 ++++ sources/Core/Aliases/Tests/TEST_Aliases.uc | 118 +++++++++++++++--- 3 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 sources/Core/Aliases/Tests/MockAliasSource.uc create mode 100644 sources/Core/Aliases/Tests/MockAliases.uc diff --git a/sources/Core/Aliases/Tests/MockAliasSource.uc b/sources/Core/Aliases/Tests/MockAliasSource.uc new file mode 100644 index 0000000..d724501 --- /dev/null +++ b/sources/Core/Aliases/Tests/MockAliasSource.uc @@ -0,0 +1,27 @@ +/** + * Source intended for testing aliases. + * 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 MockAliasSource extends AliasSource + config(AcediaAliases_Tests); + +defaultproperties +{ + configName = "AcediaAliases_Tests" + aliasesClass = class'MockAliases' +} \ No newline at end of file diff --git a/sources/Core/Aliases/Tests/MockAliases.uc b/sources/Core/Aliases/Tests/MockAliases.uc new file mode 100644 index 0000000..83eeef3 --- /dev/null +++ b/sources/Core/Aliases/Tests/MockAliases.uc @@ -0,0 +1,27 @@ +/** + * Per-object-configuration intended for testing aliases. + * 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 MockAliases extends Aliases + perObjectConfig + config(AcediaAliases_Tests); + +defaultproperties +{ + sourceClass = class'MockAliasSource' +} \ No newline at end of file diff --git a/sources/Core/Aliases/Tests/TEST_Aliases.uc b/sources/Core/Aliases/Tests/TEST_Aliases.uc index 84c54ae..be51505 100644 --- a/sources/Core/Aliases/Tests/TEST_Aliases.uc +++ b/sources/Core/Aliases/Tests/TEST_Aliases.uc @@ -22,29 +22,109 @@ class TEST_Aliases extends TestCase protected static function TESTS() { - local string result; - Context("Testing Aliases loading."); - Issue("`Try` cannot resolve correct alias"); - TEST_ExpectTrue(_().alias.Try("test", "Ford") == "car"); - TEST_ExpectTrue(_().alias.Try("test", "Delorean") == "car"); - TEST_ExpectTrue(_().alias.Try("test", "HardToBeAGod") == "scifi"); + Test_AliasHash(); + Test_AliasLoading(); +} - Issue("`Resolve` cannot resolve correct alias"); - _().alias.Resolve("test", "Ford", result); - TEST_ExpectTrue(result == "car"); - _().alias.Resolve("test", "Audi", result); - TEST_ExpectTrue(result == "car"); - _().alias.Resolve("test", "Spice", result); - TEST_ExpectTrue(result == "scifi"); +protected static function Test_AliasLoading() +{ + Context("Testing loading aliases from a mock object `MockAliasSource`."); + SubTest_AliasLoadingCorrect(); + SubTest_AliasLoadingIncorrect(); +} - Issue("`Try` does not return original alias for non-existing alias record"); - TEST_ExpectTrue(_().alias.Try("test", "AllFiction") == "AllFiction"); +protected static function SubTest_AliasLoadingCorrect() +{ + local AliasSource source; + local string outValue; - Issue("`Resolve` reports success when it failed"); - TEST_ExpectFalse(_().alias.Resolve("test", "KarmicJustice", result)); + Issue("`Resolve()` fails to return alias that should be loaded."); + source = _().alias.GetCustomSource(class'MockAliasSource'); + TEST_ExpectTrue(source.Resolve("Global", outValue)); + TEST_ExpectTrue(outValue == "value"); + TEST_ExpectTrue(source.Resolve("ford", outValue)); + TEST_ExpectTrue(outValue == "car"); - Issue("`Resolve` reports failure when it succeeds"); - TEST_ExpectTrue(_().alias.Resolve("test", "Delorean", result)); + Issue("`Try()` fails to return alias that should be loaded."); + TEST_ExpectTrue(source.Try("question") == "response"); + TEST_ExpectTrue(source.Try("delorean") == "car"); + + Issue("`ContainsAlias()` reports alias, that should be present," + @ "as missing."); + TEST_ExpectTrue(source.ContainsAlias("Global")); + TEST_ExpectTrue(source.ContainsAlias("audi")); + + Issue("Aliases in per-object-configs incorrectly handle ':'."); + TEST_ExpectTrue(source.Try("HardToBeAGod") == "sci.fi"); + + Issue("Aliases with empty values in alias name or their value are handled" + @ "incorrectly."); + TEST_ExpectTrue(source.Try("") == "empty"); + TEST_ExpectTrue(source.Try("also") == ""); +} + +protected static function SubTest_AliasLoadingIncorrect() +{ + local AliasSource source; + local string outValue; + Context("Testing loading aliases from a mock object `MockAliasSource`."); + Issue("`AliasAPI` cannot return value custom source."); + source = _().alias.GetCustomSource(class'MockAliasSource'); + TEST_ExpectNotNone(source); + + Issue("`Resolve()` reports success of finding inexistent alias."); + source = _().alias.GetCustomSource(class'MockAliasSource'); + TEST_ExpectFalse(source.Resolve("noSuchThing", outValue)); + + Issue("`Try()` does not return given value for non-existent alias."); + TEST_ExpectTrue(source.Try("TheHellIsThis") == "TheHellIsThis"); + + Issue("`ContainsAlias()` reports inexistent alias as present."); + TEST_ExpectFalse(source.ContainsAlias("FordК")); +} + +protected static function Test_AliasHash() +{ + Context("Testing `AliasHasher`."); + SubTest_AliasHashInsertingRemoval(); +} + +protected static function SubTest_AliasHashInsertingRemoval() +{ + local AliasHash hasher; + local string outValue; + hasher = new class'AliasHash'; + hasher.Initialize(); + Issue("`AliasHash` cannot properly store added aliases."); + hasher.Insert("alias", "value").Insert("one", "more"); + TEST_ExpectTrue(hasher.Contains("alias")); + TEST_ExpectTrue(hasher.Contains("one")); + TEST_ExpectTrue(hasher.Find("alias", outValue)); + TEST_ExpectTrue(outValue == "value"); + TEST_ExpectTrue(hasher.Find("one", outValue)); + TEST_ExpectTrue(outValue == "more"); + + Issue("`AliasHash` reports hashing aliases that never were hashed."); + TEST_ExpectFalse(hasher.Contains("alia")); + + Issue("`AliasHash` cannot properly remove stored aliases."); + hasher.Remove("alias"); + TEST_ExpectFalse(hasher.Contains("alias")); + TEST_ExpectTrue(hasher.Contains("one")); + TEST_ExpectFalse(hasher.Find("alias", outValue)); + outValue = "wrong"; + TEST_ExpectTrue(hasher.Find("one", outValue)); + TEST_ExpectTrue(outValue == "more"); + + Issue("`InsertIfMissing()` function cannot properly store added aliases."); + TEST_ExpectTrue(hasher.InsertIfMissing("another", "var", outValue)); + TEST_ExpectTrue(hasher.Find("another", outValue)); + TEST_ExpectTrue(outValue == "var"); + + Issue("`InsertIfMissing()` function incorrectly resolves a conflict with" + @ "an existing value."); + TEST_ExpectFalse(hasher.InsertIfMissing("one", "something", outValue)); + TEST_ExpectTrue(outValue == "more"); } defaultproperties From 612528904034a23f1b5f5fefc59fb97294e39654 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:21:23 +0700 Subject: [PATCH 17/59] Add events ability to start/shutdown `Service` Events might rely on a particular `Service` to generate them, this patch allows them to auto-launch/shutdown a service, depending on whether anybody is listening to it's events. --- sources/Core/Events/Events.uc | 36 ++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/sources/Core/Events/Events.uc b/sources/Core/Events/Events.uc index 86cf877..4f6a0a0 100644 --- a/sources/Core/Events/Events.uc +++ b/sources/Core/Events/Events.uc @@ -70,6 +70,13 @@ var private array< class > listeners; var public const class relatedListener; +// Even class can also auto-spawn a `Service`, +// in case it's require to generate events +var public const class connectedServiceClass; +// Set this to `true`if you want `connectedServiceClass` service to also +// auto-shutdown whenever no-one listens to the events. +var public const bool shutDownServiceWithoutListeners; + static public final function array< class > GetListeners() { return default.listeners; @@ -83,10 +90,15 @@ static public final function bool ActivateListener(class newListener) if (newListener == none) return false; if (!ClassIsChildOf(newListener, default.relatedListener)) return false; + // Spawn service, if absent + if ( default.listeners.length == 0 + && default.connectedServiceClass != none) { + default.connectedServiceClass.static.Require(); + } + // Add listener for (i = 0;i < default.listeners.length;i += 1) { - if (default.listeners[i] == newListener) - { + if (default.listeners[i] == newListener) { return false; } } @@ -99,17 +111,31 @@ static public final function bool ActivateListener(class newListener) static public final function bool DeactivateListener(class listener) { local int i; + local bool removedListener; + local Service service; if (listener == none) return false; - + + // Remove listener for (i = 0; i < default.listeners.length; i += 1) { if (default.listeners[i] == listener) { default.listeners.Remove(i, 1); - return true; + removedListener = true; + break; } } - return false; + // Remove unneeded service + if ( default.shutDownServiceWithoutListeners + && default.listeners.length == 0 + && default.connectedServiceClass != none) + { + service = Service(default.connectedServiceClass.static.GetInstance()); + if (service != none) { + service.Destroy(); + } + } + return removedListener; } static public final function bool IsActiveListener(class listener) From b954c718aa56b3fd332c0a68371d590c00da9eb5 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:25:02 +0700 Subject: [PATCH 18/59] Change `ConnectionService` to be auto-started To avoid launching `ConnectionService` all the time - only do so when it is needed by someone --- sources/Services/Connection/ConnectionEvents.uc | 1 + 1 file changed, 1 insertion(+) diff --git a/sources/Services/Connection/ConnectionEvents.uc b/sources/Services/Connection/ConnectionEvents.uc index 8b204d3..aad9d42 100644 --- a/sources/Services/Connection/ConnectionEvents.uc +++ b/sources/Services/Connection/ConnectionEvents.uc @@ -48,4 +48,5 @@ static function CallPlayerDisconnected(ConnectionService.Connection connection) defaultproperties { relatedListener = class'ConnectionListenerBase' + connectedServiceClass = class'ConnectionService' } \ No newline at end of file From 341c4aaf89766f628151a6c2e6c78dc53ebf5fdd Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:28:40 +0700 Subject: [PATCH 19/59] Add functionality to `Service` Add ability to auto-launch and obtain instance of a `Service` with a single command. Add `Service`-specific events `Launch()` / `ShutDown()` Add ability to auto-register required listeners on launch. --- sources/Core/Service.uc | 59 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/sources/Core/Service.uc b/sources/Core/Service.uc index 6296aff..ed435e1 100644 --- a/sources/Core/Service.uc +++ b/sources/Core/Service.uc @@ -1,7 +1,7 @@ /** * Parent class for all services used in Acedia. * Currently simply makes itself server-only. - * Copyright 2019 Anton Tarasenko + * Copyright 2020 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -21,8 +21,61 @@ class Service extends Singleton abstract; +// Listeners listed here will be automatically activated. +var public const array< class > requiredListeners; + +// Enables feature of given class. +public static final function Service Require() +{ + local Service newInstance; + if (IsRunning()) + { + return Service(GetInstance()); + } + default.blockSpawning = false; + newInstance = class'Acedia'.static.GetInstance().Spawn(default.class); + default.blockSpawning = true; + return newInstance; +} + +// Whether service is currently running is determined by +public static final function bool IsRunning() +{ + return (GetInstance() != none); +} + +protected function OnLaunch(){} +protected function OnShutdown(){} + +protected function OnCreated() +{ + default.blockSpawning = true; + SetListenersActiveSatus(true); + OnLaunch(); +} + +protected function OnDestroyed() +{ + SetListenersActiveSatus(false); + OnShutdown(); +} + +// Set listeners' status +private static function SetListenersActiveSatus(bool newStatus) +{ + local int i; + for (i = 0; i < default.requiredListeners.length; i += 1) + { + if (default.requiredListeners[i] == none) continue; + default.requiredListeners[i].static.SetActive(newStatus); + } +} + defaultproperties { - remoteRole = ROLE_None - DrawType = DT_None + DrawType = DT_None + // Prevent spawning this feature by any other means than 'Launch()'. + blockSpawning = true + // Features are server-only actors + remoteRole = ROLE_None } \ No newline at end of file From e3f3296c2d78ee6dbaad0dc6c54dfcf99bed756f Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:30:55 +0700 Subject: [PATCH 20/59] Add ability to auto-create `Singleton` Add an optional parameter that allows to auto-spawn `Singleton` with `GetInstance()` call. --- sources/Core/Singleton.uc | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/sources/Core/Singleton.uc b/sources/Core/Singleton.uc index 2bb945d..c4d61c4 100644 --- a/sources/Core/Singleton.uc +++ b/sources/Core/Singleton.uc @@ -32,11 +32,18 @@ var private Singleton activeInstance; // Only a default value is ever used. var protected bool blockSpawning; -public final static function Singleton GetInstance() +public final static function Singleton GetInstance(optional bool spawnIfMissing) { - if (default.activeInstance != none && default.activeInstance.bPendingDelete) - return none; - return default.activeInstance; + local bool instanceExists; + instanceExists = default.activeInstance != none + && !default.activeInstance.bPendingDelete; + if (instanceExists) { + return default.activeInstance; + } + if (spawnIfMissing) { + return class'Acedia'.static.GetInstance().Spawn(default.class); + } + return none; } public final static function bool IsSingletonCreationBlocked() From 070211158e1b51cd5b26e50887842352a53edcf6 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:31:11 +0700 Subject: [PATCH 21/59] Add `ColorAPI` --- sources/Core/Color/ColorAPI.uc | 812 ++++++++++++++++++++++ sources/Core/Color/Tests/TEST_ColorAPI.uc | 507 ++++++++++++++ 2 files changed, 1319 insertions(+) create mode 100644 sources/Core/Color/ColorAPI.uc create mode 100644 sources/Core/Color/Tests/TEST_ColorAPI.uc diff --git a/sources/Core/Color/ColorAPI.uc b/sources/Core/Color/ColorAPI.uc new file mode 100644 index 0000000..6a72cb0 --- /dev/null +++ b/sources/Core/Color/ColorAPI.uc @@ -0,0 +1,812 @@ +/** + * API that provides functions for working with color. + * It has a wide range of pre-defined colors and some functions that + * allow to quickly assemble color from rgb(a) values, parse it from + * a `Text`/string or load it from an alias. + * 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 ColorAPI extends Singleton + dependson(Parser) + config(AcediaSystem); + +/** + * Enumeration for ways to represent `Color` as a `string`. + */ +enum ColorDisplayType +{ + // Hex format; for pink: #ffc0cb + CLRDISPLAY_HEX, + // RGB format; for pink: rgb(255,192,203) + CLRDISPLAY_RGB, + // RGBA format; for opaque pink: rgb(255,192,203,255) + CLRDISPLAY_RGBA, + // RGB format with tags; for pink: rgb(r=255,g=192,b=203) + CLRDISPLAY_RGB_TAG, + // RGBA format with tags; for pink: rgb(r=255,g=192,b=203,a=255) + CLRDISPLAY_RGBA_TAG +}; + +// Some useful predefined color values. +// They are marked as `config` to allow server admins to mess about with +// colors if they want to. +// Pink colors +var public config const Color Pink; +var public config const Color LightPink; +var public config const Color HotPink; +var public config const Color DeepPink; +var public config const Color PaleVioletRed; +var public config const Color MediumVioletRed; +// Red colors +var public config const Color LightSalmon; +var public config const Color Salmon; +var public config const Color DarkSalmon; +var public config const Color LightCoral; +var public config const Color IndianRed; +var public config const Color Crimson; +var public config const Color Firebrick; +var public config const Color DarkRed; +var public config const Color Red; +// Orange colors +var public config const Color OrangeRed; +var public config const Color Tomato; +var public config const Color Coral; +var public config const Color DarkOrange; +var public config const Color Orange; +// Yellow colors +var public config const Color Yellow; +var public config const Color LightYellow; +var public config const Color LemonChiffon; +var public config const Color LightGoldenrodYellow; +var public config const Color PapayaWhip; +var public config const Color Moccasin; +var public config const Color PeachPuff; +var public config const Color PaleGoldenrod; +var public config const Color Khaki; +var public config const Color DarkKhaki; +var public config const Color Gold; +// Brown colors +var public config const Color Cornsilk; +var public config const Color BlanchedAlmond; +var public config const Color Bisque; +var public config const Color NavajoWhite; +var public config const Color Wheat; +var public config const Color Burlywood; +var public config const Color TanColor; // `Tan()` already taken by a function +var public config const Color RosyBrown; +var public config const Color SandyBrown; +var public config const Color Goldenrod; +var public config const Color DarkGoldenrod; +var public config const Color Peru; +var public config const Color Chocolate; +var public config const Color SaddleBrown; +var public config const Color Sienna; +var public config const Color Brown; +var public config const Color Maroon; +// Green colors +var public config const Color DarkOliveGreen; +var public config const Color Olive; +var public config const Color OliveDrab; +var public config const Color YellowGreen; +var public config const Color LimeGreen; +var public config const Color Lime; +var public config const Color LawnGreen; +var public config const Color Chartreuse; +var public config const Color GreenYellow; +var public config const Color SpringGreen; +var public config const Color MediumSpringGreen; +var public config const Color LightGreen; +var public config const Color PaleGreen; +var public config const Color DarkSeaGreen; +var public config const Color MediumAquamarine; +var public config const Color MediumSeaGreen; +var public config const Color SeaGreen; +var public config const Color ForestGreen; +var public config const Color Green; +var public config const Color DarkGreen; +// Cyan colors +var public config const Color Aqua; +var public config const Color Cyan; +var public config const Color LightCyan; +var public config const Color PaleTurquoise; +var public config const Color Aquamarine; +var public config const Color Turquoise; +var public config const Color MediumTurquoise; +var public config const Color DarkTurquoise; +var public config const Color LightSeaGreen; +var public config const Color CadetBlue; +var public config const Color DarkCyan; +var public config const Color Teal; +// Blue colors +var public config const Color LightSteelBlue; +var public config const Color PowderBlue; +var public config const Color LightBlue; +var public config const Color SkyBlue; +var public config const Color LightSkyBlue; +var public config const Color DeepSkyBlue; +var public config const Color DodgerBlue; +var public config const Color CornflowerBlue; +var public config const Color SteelBlue; +var public config const Color RoyalBlue; +var public config const Color Blue; +var public config const Color MediumBlue; +var public config const Color DarkBlue; +var public config const Color Navy; +var public config const Color MidnightBlue; +// Purple, violet, and magenta colors +var public config const Color Lavender; +var public config const Color Thistle; +var public config const Color Plum; +var public config const Color Violet; +var public config const Color Orchid; +var public config const Color Fuchsia; +var public config const Color Magenta; +var public config const Color MediumOrchid; +var public config const Color MediumPurple; +var public config const Color BlueViolet; +var public config const Color DarkViolet; +var public config const Color DarkOrchid; +var public config const Color DarkMagenta; +var public config const Color Purple; +var public config const Color Indigo; +var public config const Color DarkSlateBlue; +var public config const Color SlateBlue; +var public config const Color MediumSlateBlue; +// White colors +var public config const Color White; +var public config const Color Snow; +var public config const Color Honeydew; +var public config const Color MintCream; +var public config const Color Azure; +var public config const Color AliceBlue; +var public config const Color GhostWhite; +var public config const Color WhiteSmoke; +var public config const Color Seashell; +var public config const Color Beige; +var public config const Color OldLace; +var public config const Color FloralWhite; +var public config const Color Ivory; +var public config const Color AntiqueWhite; +var public config const Color Linen; +var public config const Color LavenderBlush; +var public config const Color MistyRose; +// Gray and black colors +var public config const Color Gainsboro; +var public config const Color LightGray; +var public config const Color Silver; +var public config const Color DarkGray; +var public config const Color Gray; +var public config const Color DimGray; +var public config const Color LightSlateGray; +var public config const Color SlateGray; +var public config const Color DarkSlateGray; +var public config const Color Eigengrau; +var public config const Color Black; + +// Escape code point is used to change output's color and is used in +// Unreal Engine's `string`s. +var private const int CODEPOINT_ESCAPE; +var private const int CODEPOINT_SMALL_A; + +/** + * Creates opaque color from (red, green, blue) triplet. + * + * @param red Red component, range from 0 to 255. + * @param green Green component, range from 0 to 255. + * @param blue Blue component, range from 0 to 255. + * @return `Color` with specified red, green and blue component and + * alpha component of `255`. + */ +public final function Color RGB(byte red, byte green, byte blue) +{ + local Color result; + result.r = red; + result.g = green; + result.b = blue; + result.a = 255; + return result; +} + +/** + * Creates color from (red, green, blue, alpha) quadruplet. + * + * @param red Red component, range from 0 to 255. + * @param green Green component, range from 0 to 255. + * @param blue Blue component, range from 0 to 255. + * @param alpha Alpha component, range from 0 to 255. + * @return `Color` with specified red, green, blue and alpha component. + */ +public final function Color RGBA(byte red, byte green, byte blue, byte alpha) +{ + local Color result; + result.r = red; + result.g = green; + result.b = blue; + result.a = alpha; + return result; +} + +/** + * Compares two colors for exact equality of red, green and blue components. + * Alpha component is ignored. + * + * @param color1 Color to compare + * @param color2 Color to compare + * @return `true` if colors' red, green and blue components are equal + * and `false` otherwise. + */ +public final function bool AreEqual(Color color1, Color color2, optional bool fixColors) +{ + if (fixColors) { + color1 = FixColor(color1); + color2 = FixColor(color2); + } + if (color1.r != color2.r) return false; + if (color1.g != color2.g) return false; + if (color1.b != color2.b) return false; + return true; +} + +/** + * Compares two colors for exact equality of red, green, blue + * and alpha components. + * + * @param color1 Color to compare + * @param color2 Color to compare + * @return `true` if colors' red, green, blue and alpha components are equal + * and `false` otherwise. + */ +public final function bool AreEqualWithAlpha(Color color1, Color color2, optional bool fixColors) +{ + if (fixColors) { + color1 = FixColor(color1); + color2 = FixColor(color2); + } + if (color1.r != color2.r) return false; + if (color1.g != color2.g) return false; + if (color1.b != color2.b) return false; + if (color1.a != color2.a) return false; + return true; +} + +/** + * Killing floor's standard methods of rendering colored `string`s + * make use of inserting 4-byte sequence into them: first bytes denotes + * the start of the sequence, 3 following bytes denote rgb color components. + * Unfortunately these methods also have issues with rendering `string`s + * if you specify certain values (`0` and `10`) of rgb color components. + * + * This function "fixes" components by replacing them with close and valid + * color component values (adds `1` to the component). + */ +public final function byte FixColorComponent(byte colorComponent) +{ + if (colorComponent == 0 || colorComponent == 10) + { + return colorComponent + 1; + } + return colorComponent; +} + +/** + * Killing floor's standard methods of rendering colored `string`s + * make use of inserting 4-byte sequence into them: first bytes denotes + * the start of the sequence, 3 following bytes denote rgb color components. + * Unfortunately these methods also have issues with rendering `string`s + * if you specify certain values (`0` and `10`) as rgb color components. + * + * This function "fixes" given `Color`'s components by replacing them with + * close and valid color values (using `FixColorComponent()` method), + * resulting in a `Color` that looks almost the same, but is suitable to be + * included into 4-byte color change sequence. + * + * Since alpha component is never used in color-change sequences, + * it is never affected. + */ +public final function Color FixColor(Color colorToFix) +{ + colorToFix.r = FixColorComponent(colorToFix.r); + colorToFix.g = FixColorComponent(colorToFix.g); + colorToFix.b = FixColorComponent(colorToFix.b); + return colorToFix; +} + +/** + * Returns 4-gyte sequence for color change to a given color. + * + * To make returned tag work in most sequences, the value of given color is + * auto "fixed" (see `FixColor()` for details). + * There is an option to skip color fixing, but method will still change + * `0` components to `1`, since they cannot otherwise be used in a tag at all. + * + * Also see `GetColorTagRGB()`. + * + * @param colorToUse Color to which tag must change the text. + * It's alpha value (`colorToUse.a`) is discarded. + * @param doNotFixComponents Minimizes changes to color components + * (only allows to change `0` components to `1` before creating a tag). + * @return `string` containing 4-byte sequence that will swap text's color to + * a given one in standard Unreal Engine's UI. + */ +public final function string GetColorTag( + Color colorToUse, + optional bool doNotFixComponents) +{ + if (!doNotFixComponents) { + colorToUse = FixColor(colorToUse); + } + colorToUse.r = Max(1, colorToUse.r); + colorToUse.g = Max(1, colorToUse.g); + colorToUse.b = Max(1, colorToUse.b); + return Chr(CODEPOINT_ESCAPE) + $ Chr(colorToUse.r) + $ Chr(colorToUse.g) + $ Chr(colorToUse.b); +} + +/** + * Returns 4-gyte sequence for color change to a given color. + * + * To make returned tag work in most sequences, the value of given color is + * auto "fixed" (see `FixColor()` for details). + * There is an option to skip color fixing, but method will still change + * `0` components to `1`, since they cannot otherwise be used in a tag at all. + * + * Also see `GetColorTag()`. + * + * @param red Red component of color to which tag must + * change the text. + * @param green Green component of color to which tag must + * change the text. + * @param blue Blue component of color to which tag must + * change the text. + * @param doNotFixComponents Minimizes changes to color components + * (only allows to change `0` components to `1` before creating a tag). + * @return `string` containing 4-byte sequence that will swap text's color to + * a given one in standard Unreal Engine's UI. + */ +public final function string GetColorTagRGB( + int red, + int green, + int blue, + optional bool doNotFixComponents) +{ + if (!doNotFixComponents) + { + red = FixColorComponent(red); + green = FixColorComponent(green); + blue = FixColorComponent(blue); + } + red = Max(1, red); + green = Max(1, green); + blue = Max(1, blue); + return Chr(CODEPOINT_ESCAPE) $ Chr(red) $ Chr(green) $ Chr(blue); +} + +// Helper function that converts `byte` with values between 0 and 15 into +// a corresponding hex letter +private final function string ByteToHexCharacter(byte component) +{ + component = Clamp(component, 0, 15); + if (component < 10) { + return string(component); + } + return Chr(component - 10 + CODEPOINT_SMALL_A); +} + +// `byte` to `string` in hex +private final function string ComponentToHex(byte component) +{ + local byte high4Bits, low4Bits; + low4Bits = component % 16; + if (component >= 16) { + high4Bits = (component - low4Bits) / 16; + } + else { + high4Bits = 0; + } + return ByteToHexCharacter(high4Bits) $ ByteToHexCharacter(low4Bits); +} + +/** + * Displays given color as a string in a given style + * (hex color representation by default). + * + * @param colorToConvert Color to display as a `string`. + * @param displayType `enum` value, describing how should color + * be displayed. + * @return `string` representation of a given color in a given style. + */ +public final function string ToStringType( + Color colorToConvert, + optional ColorDisplayType displayType) +{ + if (displayType == CLRDISPLAY_HEX) { + return "#" $ ComponentToHex(colorToConvert.r) + $ ComponentToHex(colorToConvert.g) + $ ComponentToHex(colorToConvert.b); + } + else if (displayType == CLRDISPLAY_RGB) + { + return "rgb(" $ string(colorToConvert.r) $ "," + $ string(colorToConvert.g) $ "," + $ string(colorToConvert.b) $ ")"; + } + else if (displayType == CLRDISPLAY_RGBA) + { + return "rgba(" $ string(colorToConvert.r) $ "," + $ string(colorToConvert.g) $ "," + $ string(colorToConvert.b) $ "," + $ string(colorToConvert.a) $ ")"; + } + else if (displayType == CLRDISPLAY_RGB_TAG) + { + return "rgb(r=" $ string(colorToConvert.r) $ "," + $ "g=" $ string(colorToConvert.g) $ "," + $ "b=" $ string(colorToConvert.b) $ ")"; + } + //else if (displayType == CLRDISPLAY_RGBA_TAG) + return "rgba(r=" $ string(colorToConvert.r) $ "," + $ "g=" $ string(colorToConvert.g) $ "," + $ "b=" $ string(colorToConvert.b) $ "," + $ "a=" $ string(colorToConvert.a) $ ")"; +} + +/** + * Displays given color as a string in RGB or RGBA format, depending on + * whether color is opaque. + * + * @param colorToConvert Color to display as a `string` in `CLRDISPLAY_RGB` + * style if `colorToConvert.a == 255` and `CLRDISPLAY_RGBA` otherwise. + * @return `string` representation of a given color in a given style. + */ +public final function string ToString(Color colorToConvert) +{ + if (colorToConvert.a < 255) { + return ToStringType(colorToConvert, CLRDISPLAY_RGBA); + } + return ToStringType(colorToConvert, CLRDISPLAY_RGB); +} + +// Parses color in `CLRDISPLAY_RGB` and `CLRDISPLAY_RGB_TAG` representations. +private final function Color ParseRGB(Parser parser) +{ + local int redComponent; + local int greenComponent; + local int blueComponent; + local Parser.ParserState initialParserState; + initialParserState = parser.GetCurrentState(); + parser.Match("rgb(", true) + .MInteger(redComponent).Match(",") + .MInteger(greenComponent).Match(",") + .MInteger(blueComponent).Match(")"); + if (!parser.Ok()) + { + parser.RestoreState(initialParserState).Match("rgb(", true) + .Match("r=", true).MInteger(redComponent).Match(",") + .Match("g=", true).MInteger(greenComponent).Match(",") + .Match("b=", true).MInteger(blueComponent).Match(")"); + } + return RGB(redComponent, greenComponent, blueComponent); +} + +// Parses color in `CLRDISPLAY_RGBA` and `CLRDISPLAY_RGBA_TAG` representations. +private final function Color ParseRGBA(Parser parser) +{ + local int redComponent; + local int greenComponent; + local int blueComponent; + local int alphaComponent; + local Parser.ParserState initialParserState; + initialParserState = parser.GetCurrentState(); + parser.Match("rgba(", true) + .MInteger(redComponent).Match(",") + .MInteger(greenComponent).Match(",") + .MInteger(blueComponent).Match(",") + .MInteger(alphaComponent).Match(")"); + if (!parser.Ok()) + { + parser.RestoreState(initialParserState).Match("rgba(", true) + .Match("r=", true).MInteger(redComponent).Match(",") + .Match("g=", true).MInteger(greenComponent).Match(",") + .Match("b=", true).MInteger(blueComponent).Match(",") + .Match("a=", true).MInteger(alphaComponent).Match(")"); + } + return RGBA(redComponent, greenComponent, blueComponent, alphaComponent); +} + +// Parses color in `CLRDISPLAY_HEX` representation. +private final function Color ParseHexColor(Parser parser) +{ + local int redComponent; + local int greenComponent; + local int blueComponent; + parser.Match("#") + .MUnsignedInteger(redComponent, 16, 2) + .MUnsignedInteger(greenComponent, 16, 2) + .MUnsignedInteger(blueComponent, 16, 2); + return RGB(redComponent, greenComponent, blueComponent); +} + +/** + * Uses given parser to try and parse a color in any of the + * `ColorDisplayType` representations. + * + * @param parser Parser that method would use to parse color from + * wherever it left. It's confirmed state will not be changed. + * Do not treat `parser` bein in a non-failed state as a confirmation of + * successful parsing: color parsing might fail regardless. + * Check return value for that. + * @param resultingColor Parsed color will be written here if parsing is + * successful, otherwise value is undefined. + * If parsed color did not specify alpha component - 255 will be used. + * @return `true` if parsing was successful and false otherwise. + */ +public final function bool ParseWith(Parser parser, out Color resultingColor) +{ + local bool successfullyParsed; + local string colorAlias; + local Parser colorParser; + local Parser.ParserState initialParserState; + if (parser == none) return false; + resultingColor.a = 0xff; + colorParser = parser; + initialParserState = parser.GetCurrentState(); + if (parser.Match("$").MUntil(colorAlias,, true).Ok()) + { + colorParser = _.text.ParseString(_.alias.TryColor(colorAlias)); + initialParserState = colorParser.GetCurrentState(); + } + else { + parser.RestoreState(initialParserState); + } + resultingColor = ParseRGB(colorParser); + if (!colorParser.Ok()) + { + colorParser.RestoreState(initialParserState); + resultingColor = ParseRGBA(colorParser); + } + if (!colorParser.Ok()) + { + colorParser.RestoreState(initialParserState); + resultingColor = ParseHexColor(colorParser); + } + successfullyParsed = colorParser.Ok(); + if (colorParser != parser) { + _.memory.Free(colorParser); + } + return successfullyParsed; +} + +/** + * Parses a color in any of the `ColorDisplayType` representations from the + * beginning of a given `string`. + * + * @param stringWithColor String, that contains color definition at + * the beginning. Anything after color definition is not used. + * @param resultingColor Parsed color will be written here if parsing is + * successful, otherwise value is undefined. + * If parsed color did not specify alpha component - 255 will be used. + * @param stringType How to treat given `string`, + * see `StringType` for more details. + * @return `true` if parsing was successful and false otherwise. + */ +public final function bool ParseString( + string stringWithColor, + out Color resultingColor, + optional Text.StringType stringType) +{ + local bool successfullyParsed; + local Parser colorParser; + colorParser = _.text.ParseString(stringWithColor, stringType); + successfullyParsed = ParseWith(colorParser, resultingColor); + _.memory.Free(colorParser); + return successfullyParsed; +} + +/** + * Parses a color in any of the `ColorDisplayType` representations from the + * beginning of a given `Text`. + * + * @param textWithColor `Text`, that contains color definition at + * the beginning. Anything after color definition is not used. + * @param resultingColor Parsed color will be written here if parsing is + * successful, otherwise value is undefined. + * If parsed color did not specify alpha component - 255 will be used. + * @return `true` if parsing was successful and false otherwise. + */ +public final function bool ParseText( + Text textWithColor, + out Color resultingColor) +{ + local bool successfullyParsed; + local Parser colorParser; + colorParser = _.text.Parse(textWithColor); + successfullyParsed = ParseWith(colorParser, resultingColor); + _.memory.Free(colorParser); + return successfullyParsed; +} + +/** + * Parses a color in any of the `ColorDisplayType` representations from the + * beginning of a given raw data. + * + * @param rawDataWithColor Raw data, that contains color definition at + * the beginning. Anything after color definition is not used. + * @param resultingColor Parsed color will be written here if parsing is + * successful, otherwise value is undefined. + * If parsed color did not specify alpha component - 255 will be used. + * @return `true` if parsing was successful and false otherwise. + */ +public final function bool ParseRaw( + array rawDataWithColor, + out Color resultingColor) +{ + local bool successfullyParsed; + local Parser colorParser; + colorParser = _.text.ParseRaw(rawDataWithColor); + successfullyParsed = ParseWith(colorParser, resultingColor); + _.memory.Free(colorParser); + return successfullyParsed; +} + +defaultproperties +{ + Pink=(R=255,G=192,B=203,A=255) + LightPink=(R=255,G=182,B=193,A=255) + HotPink=(R=255,G=105,B=180,A=255) + DeepPink=(R=255,G=20,B=147,A=255) + PaleVioletRed=(R=219,G=112,B=147,A=255) + MediumVioletRed=(R=199,G=21,B=133,A=255) + LightSalmon=(R=255,G=160,B=122,A=255) + Salmon=(R=250,G=128,B=114,A=255) + DarkSalmon=(R=233,G=150,B=122,A=255) + LightCoral=(R=240,G=128,B=128,A=255) + IndianRed=(R=205,G=92,B=92,A=255) + Crimson=(R=220,G=20,B=60,A=255) + Firebrick=(R=178,G=34,B=34,A=255) + DarkRed=(R=139,G=0,B=0,A=255) + Red=(R=255,G=0,B=0,A=255) + OrangeRed=(R=255,G=69,B=0,A=255) + Tomato=(R=255,G=99,B=71,A=255) + Coral=(R=255,G=127,B=80,A=255) + DarkOrange=(R=255,G=140,B=0,A=255) + Orange=(R=255,G=165,B=0,A=255) + Yellow=(R=255,G=255,B=0,A=255) + LightYellow=(R=255,G=255,B=224,A=255) + LemonChiffon=(R=255,G=250,B=205,A=255) + LightGoldenrodYellow=(R=250,G=250,B=210,A=255) + PapayaWhip=(R=255,G=239,B=213,A=255) + Moccasin=(R=255,G=228,B=181,A=255) + PeachPuff=(R=255,G=218,B=185,A=255) + PaleGoldenrod=(R=238,G=232,B=170,A=255) + Khaki=(R=240,G=230,B=140,A=255) + DarkKhaki=(R=189,G=183,B=107,A=255) + Gold=(R=255,G=215,B=0,A=255) + Cornsilk=(R=255,G=248,B=220,A=255) + BlanchedAlmond=(R=255,G=235,B=205,A=255) + Bisque=(R=255,G=228,B=196,A=255) + NavajoWhite=(R=255,G=222,B=173,A=255) + Wheat=(R=245,G=222,B=179,A=255) + Burlywood=(R=222,G=184,B=135,A=255) + TanColor=(R=210,G=180,B=140,A=255) + RosyBrown=(R=188,G=143,B=143,A=255) + SandyBrown=(R=244,G=164,B=96,A=255) + Goldenrod=(R=218,G=165,B=32,A=255) + DarkGoldenrod=(R=184,G=134,B=11,A=255) + Peru=(R=205,G=133,B=63,A=255) + Chocolate=(R=210,G=105,B=30,A=255) + SaddleBrown=(R=139,G=69,B=19,A=255) + Sienna=(R=160,G=82,B=45,A=255) + Brown=(R=165,G=42,B=42,A=255) + Maroon=(R=128,G=0,B=0,A=255) + DarkOliveGreen=(R=85,G=107,B=47,A=255) + Olive=(R=128,G=128,B=0,A=255) + OliveDrab=(R=107,G=142,B=35,A=255) + YellowGreen=(R=154,G=205,B=50,A=255) + LimeGreen=(R=50,G=205,B=50,A=255) + Lime=(R=0,G=255,B=0,A=255) + LawnGreen=(R=124,G=252,B=0,A=255) + Chartreuse=(R=127,G=255,B=0,A=255) + GreenYellow=(R=173,G=255,B=47,A=255) + SpringGreen=(R=0,G=255,B=127,A=255) + MediumSpringGreen=(R=0,G=250,B=154,A=255) + LightGreen=(R=144,G=238,B=144,A=255) + PaleGreen=(R=152,G=251,B=152,A=255) + DarkSeaGreen=(R=143,G=188,B=143,A=255) + MediumAquamarine=(R=102,G=205,B=170,A=255) + MediumSeaGreen=(R=60,G=179,B=113,A=255) + SeaGreen=(R=46,G=139,B=87,A=255) + ForestGreen=(R=34,G=139,B=34,A=255) + Green=(R=0,G=128,B=0,A=255) + DarkGreen=(R=0,G=100,B=0,A=255) + Aqua=(R=0,G=255,B=255,A=255) + Cyan=(R=0,G=255,B=255,A=255) + LightCyan=(R=224,G=255,B=255,A=255) + PaleTurquoise=(R=175,G=238,B=238,A=255) + Aquamarine=(R=127,G=255,B=212,A=255) + Turquoise=(R=64,G=224,B=208,A=255) + MediumTurquoise=(R=72,G=209,B=204,A=255) + DarkTurquoise=(R=0,G=206,B=209,A=255) + LightSeaGreen=(R=32,G=178,B=170,A=255) + CadetBlue=(R=95,G=158,B=160,A=255) + DarkCyan=(R=0,G=139,B=139,A=255) + Teal=(R=0,G=128,B=128,A=255) + LightSteelBlue=(R=176,G=196,B=222,A=255) + PowderBlue=(R=176,G=224,B=230,A=255) + LightBlue=(R=173,G=216,B=230,A=255) + SkyBlue=(R=135,G=206,B=235,A=255) + LightSkyBlue=(R=135,G=206,B=250,A=255) + DeepSkyBlue=(R=0,G=191,B=255,A=255) + DodgerBlue=(R=30,G=144,B=255,A=255) + CornflowerBlue=(R=100,G=149,B=237,A=255) + SteelBlue=(R=70,G=130,B=180,A=255) + RoyalBlue=(R=65,G=105,B=225,A=255) + Blue=(R=0,G=0,B=255,A=255) + MediumBlue=(R=0,G=0,B=205,A=255) + DarkBlue=(R=0,G=0,B=139,A=255) + Navy=(R=0,G=0,B=128,A=255) + MidnightBlue=(R=25,G=25,B=112,A=255) + Lavender=(R=230,G=230,B=250,A=255) + Thistle=(R=216,G=191,B=216,A=255) + Plum=(R=221,G=160,B=221,A=255) + Violet=(R=238,G=130,B=238,A=255) + Orchid=(R=218,G=112,B=214,A=255) + Fuchsia=(R=255,G=0,B=255,A=255) + Magenta=(R=255,G=0,B=255,A=255) + MediumOrchid=(R=186,G=85,B=211,A=255) + MediumPurple=(R=147,G=112,B=219,A=255) + BlueViolet=(R=138,G=43,B=226,A=255) + DarkViolet=(R=148,G=0,B=211,A=255) + DarkOrchid=(R=153,G=50,B=204,A=255) + DarkMagenta=(R=139,G=0,B=139,A=255) + Purple=(R=128,G=0,B=128,A=255) + Indigo=(R=75,G=0,B=130,A=255) + DarkSlateBlue=(R=72,G=61,B=139,A=255) + SlateBlue=(R=106,G=90,B=205,A=255) + MediumSlateBlue=(R=123,G=104,B=238,A=255) + White=(R=255,G=255,B=255,A=255) + Snow=(R=255,G=250,B=250,A=255) + Honeydew=(R=240,G=255,B=240,A=255) + MintCream=(R=245,G=255,B=250,A=255) + Azure=(R=240,G=255,B=255,A=255) + AliceBlue=(R=240,G=248,B=255,A=255) + GhostWhite=(R=248,G=248,B=255,A=255) + WhiteSmoke=(R=245,G=245,B=245,A=255) + Seashell=(R=255,G=245,B=238,A=255) + Beige=(R=245,G=245,B=220,A=255) + OldLace=(R=253,G=245,B=230,A=255) + FloralWhite=(R=255,G=250,B=240,A=255) + Ivory=(R=255,G=255,B=240,A=255) + AntiqueWhite=(R=250,G=235,B=215,A=255) + Linen=(R=250,G=240,B=230,A=255) + LavenderBlush=(R=255,G=240,B=245,A=255) + MistyRose=(R=255,G=228,B=225,A=255) + Gainsboro=(R=220,G=220,B=220,A=255) + LightGray=(R=211,G=211,B=211,A=255) + Silver=(R=192,G=192,B=192,A=255) + Gray=(R=169,G=169,B=169,A=255) + DimGray=(R=128,G=128,B=128,A=255) + DarkGray=(R=105,G=105,B=105,A=255) + LightSlateGray=(R=119,G=136,B=153,A=255) + SlateGray=(R=112,G=128,B=144,A=255) + DarkSlateGray=(R=47,G=79,B=79,A=255) + Eigengrau=(R=22,G=22,B=29,A=255) + Black=(R=0,G=0,B=0,A=255) + CODEPOINT_SMALL_A = 97 + CODEPOINT_ESCAPE = 27 +} \ No newline at end of file diff --git a/sources/Core/Color/Tests/TEST_ColorAPI.uc b/sources/Core/Color/Tests/TEST_ColorAPI.uc new file mode 100644 index 0000000..fc21a3d --- /dev/null +++ b/sources/Core/Color/Tests/TEST_ColorAPI.uc @@ -0,0 +1,507 @@ +/** + * Set of tests for Color API. + * 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 TEST_ColorAPI extends TestCase + abstract; + +protected static function TESTS() +{ + Test_ColorCreation(); + Test_EqualityCheck(); + Test_ColorFixing(); + Test_ToString(); + Test_Parse(); + Test_GetTag(); +} + +protected static function Test_ColorCreation() +{ + Context("Testing `ColorAPI`'s functions for creating color structures."); + SubTest_ColorCreationRGB(); + SubTest_ColorCreationRGBA(); +} + +protected static function SubTest_ColorCreationRGB() +{ + local Color createdColor; + Issue("`RGB() function does not set red, green and blue components" + @ "correctly."); + createdColor = _().color.RGB(145, 67, 237); + TEST_ExpectTrue(createdColor.r == 145); + TEST_ExpectTrue(createdColor.g == 67); + TEST_ExpectTrue(createdColor.b == 237); + + Issue("`RGB() function does not set alpha component to 255."); + TEST_ExpectTrue(createdColor.a == 255); + + Issue("`RGB() function does not set special values (border values" + @ "`0`, `255` and value `10`, incorrect for coloring a `string`) for" + @"red, green and blue components correctly."); + createdColor = _().color.RGB(0, 10, 255); + TEST_ExpectTrue(createdColor.r == 0); + TEST_ExpectTrue(createdColor.g == 10); + TEST_ExpectTrue(createdColor.b == 255); + + Issue("`RGB() function does not set alpha value to 255."); + TEST_ExpectTrue(createdColor.a == 255); +} + +protected static function SubTest_ColorCreationRGBA() +{ + local Color createdColor; + Issue("`RGBA() function does not set red, green, blue, alpha" + @ "components correctly."); + createdColor = _().color.RGBA(93, 245, 1, 67); + TEST_ExpectTrue(createdColor.r == 93); + TEST_ExpectTrue(createdColor.g == 245); + TEST_ExpectTrue(createdColor.b == 1); + TEST_ExpectTrue(createdColor.a == 67); + + Issue("`RGBA() function does not set special values (border values" + @ "`0`, `255` and value `10`, incorrect for coloring a `string`) for" + @"red, green, blue components correctly."); + createdColor = _().color.RGBA(0, 10, 10, 255); + TEST_ExpectTrue(createdColor.r == 0); + TEST_ExpectTrue(createdColor.g == 10); + TEST_ExpectTrue(createdColor.b == 10); + TEST_ExpectTrue(createdColor.a == 255); +} + +protected static function Test_EqualityCheck() +{ + Context("Testing `ColorAPI`'s functions for color equality check."); + SubTest_EqualityCheckNotFixed(); + SubTest_EqualityCheckFixed(); +} + +protected static function SubTest_EqualityCheckNotFixed() +{ + local Color color1, color2, color3; + color1 = _().color.RGB(45, 10, 19); + color2 = _().color.RGB(45, 11, 19); + color3 = _().color.RGBA(45, 10, 19, 178); + Issue("`AreEqual()` does not recognized equal colors as such."); + TEST_ExpectTrue(_().color.AreEqual(color1, color1)); + + Issue("`AreEqual()` does not recognized colors that differ only in alpha" + @ "channel as equal."); + TEST_ExpectTrue(_().color.AreEqual(color1, color3)); + + Issue("`AreEqual()` does not recognized different colors as such."); + TEST_ExpectFalse(_().color.AreEqual(color1, color2)); + + Issue("`AreEqualWithAlpha()` does not recognized equal colors as such."); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(color1, color1)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(color3, color3)); + + Issue("`AreEqualWithAlpha()` does not recognized different colors" + @ "as such."); + TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color2)); + TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color3)); +} + +protected static function SubTest_EqualityCheckFixed() +{ + local Color color1, color2, color3; + color1 = _().color.RGB(45, 10, 0); + color2 = _().color.RGB(45, 239, 19); + color3 = _().color.RGBA(45, 11, 1, 178); + Issue("`AreEqual()` does not recognized equal colors as such (with color" + @ "auto-fix)."); + TEST_ExpectTrue(_().color.AreEqual(color1, color1, true)); + + Issue("`AreEqual()` does not recognized colors that differ only in alpha" + @ "channel as equal (with color auto-fix)."); + TEST_ExpectTrue(_().color.AreEqual(color1, color3, true)); + + Issue("`AreEqual()` does not recognized different colors as such" + @ "(with color auto-fix)."); + TEST_ExpectFalse(_().color.AreEqual(color1, color2, true)); + + Issue("`AreEqualWithAlpha()` does not recognized equal colors as such" + @ "(with color auto-fix)."); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(color1, color1, true)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(color3, color3, true)); + + Issue("`AreEqualWithAlpha()` does not recognized different colors as such" + @ "(with color auto-fix)."); + TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color2, true)); + TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color3, true)); +} + +protected static function Test_ColorFixing() +{ + local Color validColor, brokenColor; + validColor = _().color.RGB(23, 179, 244); + brokenColor = _().color.RGB(10, 35, 0); + Context("Testing `ColorAPI`'s functions for fixing color components for" + @ "game's native render functions."); + Issue("`FixColorComponent()` does not \"fix\" values it is expected to," + @ "the way it is expected to."); + TEST_ExpectTrue(_().color.FixColorComponent(0) == 1); + TEST_ExpectTrue(_().color.FixColorComponent(10) == 11); + + Issue("`FixColorComponent()` changes values it should not."); + TEST_ExpectTrue(_().color.FixColorComponent(9) == 9); + TEST_ExpectTrue(_().color.FixColorComponent(255) == 255); + TEST_ExpectTrue(_().color.FixColorComponent(87) == 87); + + Issue("`FixColor()` changes colors it should not."); + TEST_ExpectTrue( + _().color.AreEqualWithAlpha(validColor, + _().color.FixColor(validColor))); + + Issue("`FixColor()` doesn't fix color it should fix in an expected way."); + TEST_ExpectTrue( + _().color.AreEqualWithAlpha(_().color.RGB(11, 35, 1), + _().color.FixColor(brokenColor))); + + Issue("`FixColor()` affects alpha channel."); + TEST_ExpectTrue(_().color.FixColor(validColor).a == 255); + validColor.a = 0; + TEST_ExpectTrue(_().color.FixColor(validColor).a == 0); + validColor.a = 10; + TEST_ExpectTrue(_().color.FixColor(validColor).a == 10); +} + +protected static function Test_ToString() +{ + Context("Testing `ColorAPI`'s `ToString()` function."); + SubTest_ToStringType(); + SubTest_ToString(); +} + +protected static function SubTest_ToStringType() +{ + local Color normalColor, borderValueColor; + normalColor = _().color.RGBA(24, 232, 187, 34); + borderValueColor = _().color.RGBA(0, 255, 255, 0); + Issue("`ToStringType()` improperly works with `CLRDISPLAY_HEX` option."); + TEST_ExpectTrue(_().color.ToStringType(normalColor) ~= "#18e8bb"); + TEST_ExpectTrue(_().color.ToStringType(borderValueColor) ~= "#00ffff"); + + Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGB` option."); + TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGB) + ~= "rgb(24,232,187)"); + TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGB) + ~= "rgb(0,255,255)"); + + Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGBA` option."); + TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGBA) + ~= "rgba(24,232,187,34)"); + TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA) + ~= "rgba(0,255,255,0)"); + + Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGB_TAG`" + @ "option."); + TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGB_TAG) + ~= "rgb(r=24,g=232,b=187)"); + TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGB_TAG) + ~= "rgb(r=0,g=255,b=255)"); + + Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGBA_TAG`" + @ "option."); + TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGBA_TAG) + ~= "rgba(r=24,g=232,b=187,a=34)"); + TEST_ExpectTrue( + _().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA_TAG) + ~= "rgba(r=0,g=255,b=255,a=0)"); +} + +protected static function SubTest_ToString() +{ + local Color opaqueColor, transparentColor; + opaqueColor = _().color.RGBA(143, 211, 43, 255); + transparentColor = _().color.RGBA(234, 32, 145, 13); + Issue("`ToString()` improperly converts color with opaque color."); + TEST_ExpectTrue(_().color.ToString(opaqueColor) ~= "rgb(143,211,43)"); + Issue("`ToString()` improperly converts color with transparent color."); + TEST_ExpectTrue(_().color.ToString(transparentColor) + ~= "rgba(234,32,145,13)"); +} + +protected static function Test_GetTag() +{ + Context("Testing `ColorAPI`'s functionality of creating 4-byte color" + @ "change sequences."); + SubTest_GetTagColor(); + SubTest_GetTagRGB(); +} + +protected static function SubTest_GetTagColor() +{ + local Color normalColor, borderColor; + normalColor = _().color.RGB(143, 211, 43); + borderColor = _().color.RGB(10, 0, 255); + Issue("`GetColorTag()` does not properly convert colors."); + TEST_ExpectTrue(_().color.GetColorTag(normalColor) + == (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43))); + TEST_ExpectTrue(_().color.GetColorTag(borderColor) + == (Chr(27) $ Chr(11) $ Chr(1) $ Chr(255))); + + Issue("`GetColorTag()` does not properly convert colors when asked not to" + @ "fix components."); + TEST_ExpectTrue(_().color.GetColorTag(normalColor, true) + == (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43))); + TEST_ExpectTrue(_().color.GetColorTag(borderColor, true) + == (Chr(27) $ Chr(10) $ Chr(1) $ Chr(255))); +} + +protected static function SubTest_GetTagRGB() +{ + Issue("`GetColorTagRGB()` does not properly convert colors."); + TEST_ExpectTrue(_().color.GetColorTagRGB(143, 211, 43) + == (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43))); + TEST_ExpectTrue(_().color.GetColorTagRGB(10, 0, 255) + == (Chr(27) $ Chr(11) $ Chr(1) $ Chr(255))); + + Issue("`GetColorTagRGB()` does not properly convert colors when asked" + @ "not to fix components."); + TEST_ExpectTrue(_().color.GetColorTagRGB(143, 211, 43, true) + == (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43))); + TEST_ExpectTrue(_().color.GetColorTagRGB(10, 0, 255, true) + == (Chr(27) $ Chr(10) $ Chr(1) $ Chr(255))); +} + +protected static function Test_Parse() +{ + Context("Testing `ColorAPI`'s parsing functionality."); + SubTest_ParseWithParser(); + SubTest_ParseStringPlain(); + SubTest_ParseStringColored(); + SubTest_ParseStringFormatted(); + SubTest_ParseText(); + SubTest_ParseRaw(); +} + +protected static function SubTest_ParseWithParser() +{ + local Color expectedColor, resultColor; + expectedColor = _().color.RGBA(154, 255, 0, 187); + Issue("`ParseWith()` cannot parse hex colors."); + TEST_ExpectTrue(_().color.ParseWith(_().text.ParseString("#9aff00"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseWith()` cannot parse rgb colors."); + TEST_ExpectTrue(_().color.ParseWith(_().text.ParseString("rgb(154,255,0)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseWith()` cannot parse rgba colors."); + TEST_ExpectTrue(_().color.ParseWith( + _().text.ParseString("rgba(154,255,0,187)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseWith()` cannot parse rgb colors with tags."); + TEST_ExpectTrue(_().color.ParseWith( + _().text.ParseString("rgb(r=154,g=255,b=0)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseWith()` cannot parse rgba colors with tags."); + TEST_ExpectTrue(_().color.ParseWith( + _().text.ParseString("rgba(r=154,g=255,b=0,a=187)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseWith()` reports success when parsing invalid color string."); + TEST_ExpectFalse(_().color.ParseWith( _().text.ParseString("#9aff0g"), + resultColor)); +} + +protected static function SubTest_ParseStringPlain() +{ + local Color expectedColor, resultColor; + expectedColor = _().color.RGBA(154, 255, 0, 187); + Issue("`ParseString()` cannot parse hex colors."); + TEST_ExpectTrue(_().color.ParseString("#9aff00", resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString()` cannot parse rgb colors."); + TEST_ExpectTrue(_().color.ParseString("rgb(154,255,0)", resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString()` cannot parse rgba colors."); + TEST_ExpectTrue(_().color.ParseString("rgba(154,255,0,187)", resultColor)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseString()` cannot parse rgb colors with tags."); + TEST_ExpectTrue(_().color.ParseString("rgb(r=154,g=255,b=0)", resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString()` cannot parse rgba colors with tags."); + TEST_ExpectTrue(_().color.ParseString( "rgba(r=154,g=255,b=0,a=187)", + resultColor)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseString()` reports success when parsing invalid color string."); + TEST_ExpectFalse(_().color.ParseString("#9aff0g", resultColor)); +} + +protected static function SubTest_ParseStringColored() +{ + local Color expectedColor, resultColor; + expectedColor = _().color.RGBA(154, 255, 0, 187); + Issue("`ParseString(STRING_Colored)` cannot parse hex colors."); + TEST_ExpectTrue(_().color.ParseString( + "#9af" $ Chr(27) $ Chr(45) $ Chr(234) $ Chr(24) $ "f00", + resultColor, STRING_Colored)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString(STRING_Colored)` cannot parse rgb colors."); + TEST_ExpectTrue(_().color.ParseString( + "rgb(154,2" $ Chr(27) $ Chr(23) $ Chr(32) $ Chr(53) $ "55,0)", + resultColor, STRING_Colored)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString(STRING_Colored)` cannot parse rgba colors."); + TEST_ExpectTrue(_().color.ParseString( + "rgba(154,255,0,187" $ Chr(27) $ Chr(133) $ Chr(234) $ Chr(10) $ ")", + resultColor, STRING_Colored)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseString(STRING_Colored)` cannot parse rgb colors with tags."); + TEST_ExpectTrue(_().color.ParseString( + "rg" $ Chr(27) $ Chr(26) $ Chr(234) $ Chr(125) $ "b(r=154,g=255,b=0)", + resultColor, STRING_Colored)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString(STRING_Colored)` cannot parse rgba colors with tags."); + TEST_ExpectTrue(_().color.ParseString( + "rgba(r=154,g=255,b" $ Chr(27) $ Chr(1) $ Chr(4) $ Chr(7) $ "=0,a=187)", + resultColor, STRING_Colored)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); +} + +protected static function SubTest_ParseStringFormatted() +{ + local Color expectedColor, resultColor; + expectedColor = _().color.RGBA(154, 255, 0, 187); + Issue("`ParseString(STRING_Formatted)` cannot parse hex colors."); + TEST_ExpectTrue(_().color.ParseString( + "#9a{#4753d5 ff0}0", + resultColor, STRING_Formatted)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString(STRING_Formatted)` cannot parse rgb colors."); + TEST_ExpectTrue(_().color.ParseString( + "rg{rgb(45,67,123) b(154,25}5,0)", + resultColor, STRING_Formatted)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString(STRING_Formatted)` cannot parse rgba colors."); + TEST_ExpectTrue(_().color.ParseString( + "rgba(154,2{#34d1a7 }55,0,187)", + resultColor, STRING_Formatted)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseString(STRING_Formatted)` cannot parse rgb colors with tags."); + TEST_ExpectTrue(_().color.ParseString( + "rgb(r{#34d1a7 }=154,g=255,b=0)", + resultColor, STRING_Formatted)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseString(STRING_Formatted)` cannot parse rgba colors with" + @ "tags."); + TEST_ExpectTrue(_().color.ParseString( + "r{rgb(12,12,253) gba(r=154,g=255,b=0,a=187)}", + resultColor, STRING_Formatted)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); +} + +protected static function SubTest_ParseText() +{ + local Color expectedColor, resultColor; + expectedColor = _().color.RGBA(154, 255, 0, 187); + Issue("`ParseText()` cannot parse hex colors."); + TEST_ExpectTrue(_().color.ParseText(_().text.FromString("#9aff00"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseText()` cannot parse rgb colors."); + TEST_ExpectTrue(_().color.ParseText(_().text.FromString("rgb(154,255,0)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseText()` cannot parse rgba colors."); + TEST_ExpectTrue(_().color.ParseText( + _().text.FromString("rgba(154,255,0,187)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseText()` cannot parse rgb colors with tags."); + TEST_ExpectTrue(_().color.ParseText( + _().text.FromString("rgb(r=154,g=255,b=0)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseText()` cannot parse rgba colors with tags."); + TEST_ExpectTrue(_().color.ParseText( + _().text.FromString("rgba(r=154,g=255,b=0,a=187)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseText()` reports success when parsing invalid color string."); + TEST_ExpectFalse(_().color.ParseText( _().text.FromString("#9aff0g"), + resultColor)); +} + +protected static function SubTest_ParseRaw() +{ + local Color expectedColor, resultColor; + expectedColor = _().color.RGBA(154, 255, 0, 187); + Issue("`ParseRaw()` cannot parse hex colors."); + TEST_ExpectTrue(_().color.ParseRaw( _().text.StringToRaw("#9aff00"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseRaw()` cannot parse rgb colors."); + TEST_ExpectTrue(_().color.ParseRaw( _().text.StringToRaw("rgb(154,255,0)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseRaw()` cannot parse rgba colors."); + TEST_ExpectTrue(_().color.ParseRaw( + _().text.StringToRaw("rgba(154,255,0,187)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseRaw()` cannot parse rgb colors with tags."); + TEST_ExpectTrue(_().color.ParseRaw( + _().text.StringToRaw("rgb(r=154,g=255,b=0)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); + + Issue("`ParseRaw()` cannot parse rgba colors with tags."); + TEST_ExpectTrue(_().color.ParseRaw( + _().text.StringToRaw("rgba(r=154,g=255,b=0,a=187)"), + resultColor)); + TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); + + Issue("`ParseRaw()` reports success when parsing invalid color string."); + TEST_ExpectFalse(_().color.ParseRaw(_().text.StringToRaw("#9aff0g"), + resultColor)); +} + +defaultproperties +{ + caseName = "Colors" +} \ No newline at end of file From 8845d69b1d1cee6533f63ae2cb63c45d55207455 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:31:39 +0700 Subject: [PATCH 22/59] Add `ConsoleAPI` --- sources/Core/Console/ConsoleAPI.uc | 280 ++++++++++++++++++ sources/Core/Console/ConsoleBuffer.uc | 393 ++++++++++++++++++++++++++ sources/Core/Console/ConsoleWriter.uc | 373 ++++++++++++++++++++++++ 3 files changed, 1046 insertions(+) create mode 100644 sources/Core/Console/ConsoleAPI.uc create mode 100644 sources/Core/Console/ConsoleBuffer.uc create mode 100644 sources/Core/Console/ConsoleWriter.uc diff --git a/sources/Core/Console/ConsoleAPI.uc b/sources/Core/Console/ConsoleAPI.uc new file mode 100644 index 0000000..5365626 --- /dev/null +++ b/sources/Core/Console/ConsoleAPI.uc @@ -0,0 +1,280 @@ +/** + * API that provides functions for outputting text in + * Killing Floor's console. It takes care of coloring output and breaking up + * long lines (since allowing game to handle line breaking completely + * messes up console output). + * + * Actual output is taken care of by `ConsoleWriter` objects that this + * API generates. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class ConsoleAPI extends Singleton + config(AcediaSystem); + +/** + * Main issue with console output in Killing Floor is + * automatic line breaking of long enough messages: + * it breaks formatting and can lead to an ugly text overlapping. + * To fix this we will try to break up user's output into lines ourselves, + * before game does it for us. + * + * We are not 100% sure how Killing Floor decides when to break the line, + * but it seems to calculate how much text can actually fit in a certain + * area on screen. + * There are two issues: + * 1. We do not know for sure what this limit value is. + * Even if we knew how to compute it, we cannot do that in server mode, + * since it depends on a screen resolution and font, which + * can vary for different players. + * 2. Even invisible characters, such as color change sequences, + * that do not take any space on the screen, contribute towards + * that limit. So for a heavily colored text we will have to + * break line much sooner than for the plain text. + * Both issues are solved by introducing two limits that users themselves + * are allowed to change: visible character limit and total character limit. + * ~ Total character limit will be a hard limit on a character amount in + * a line (including hidden ones used for color change sequences) that + * will be used to prevent Killing Floor's native line breaks. + * ~ Visible character limit will be a lower limit on amount of actually + * visible character. It introduction basically reserves some space that can be + * used only for color change sequences. Without this limit lines with + * colored lines will appear to be shorter that mono-colored ones. + * Visible limit will help to alleviate this problem. + * + * For example, if we set total limit to `120` and visible limit to `80`: + * 1. Line not formatted with color will all break at + * around length of `80`. + * 2. Since color change sequence consists of 4 characters: + * we can fit up to `(120 - 80) / 4 = 10` color swaps into each line, + * while still breaking them at a around the same length of `80`. + * ~ To differentiate our line breaks from line breaks intended by + * the user, we will also add 2 symbols worth of padding in front of all our + * output: + * 1. Before intended new line they will be just two spaces. + * 2. After our line break we will replace first space with "|" to indicate + * that we had to break a long line. + * + * Described measures are not perfect: + * 1. Since Killing Floor's console doe not use monospaced font, + * the same amount of characters on the line does not mean lines of + * visually the same length; + * 2. Heavily enough colored lines are still going to be shorter; + * 3. Depending on a resolution, default limits may appear to either use + * too little space (for high resolutions) or, on the contrary, + * not prevent native line breaks (low resolutions). + * In these cases user might be required to manually set limits; + * 4. There are probably more. + * But if seems to provide good enough results for the average use case. + */ + +/** + * Configures how text will be rendered in target console(s). + */ +struct ConsoleDisplaySettings +{ + // What color to use for text by default + var Color defaultColor; + // How many visible characters in be displayed in a line? + var int maxVisibleLineWidth; + // How many total characters can be output at once? + var int maxTotalLineWidth; +}; +// We will store data for `ConsoleDisplaySettings` separately for the ease of +// configuration. +var private config Color defaultColor; +var private config int maxVisibleLineWidth; +var private config int maxTotalLineWidth; + +/** + * Return current global visible limit that describes how many (at most) + * visible characters can be output in the console line. + * + * Instances of `ConsoleWriter` are initialized with this value, + * but can later change this value independently. + * Changes to global values do not affect already created `ConsoleWriters`. + * + * @return Current global visible limit. + */ +public final function int GetVisibleLineLength() +{ + return maxVisibleLineWidth; +} + +/** + * Sets current global visible limit that describes how many (at most) visible + * characters can be output in the console line. + * + * Instances of `ConsoleWriter` are initialized with this value, + * but can later change this value independently. + * Changes to global values do not affect already created `ConsoleWriters`. + * + * @param newMaxVisibleLineWidth New global visible character limit. + */ +public final function SetVisibleLineLength(int newMaxVisibleLineWidth) +{ + maxVisibleLineWidth = newMaxVisibleLineWidth; +} + +/** + * Return current global total limit that describes how many (at most) + * characters can be output in the console line. + * + * Instances of `ConsoleWriter` are initialized with this value, + * but can later change this value independently. + * Changes to global values do not affect already created `ConsoleWriters`. + * + * @return Current global total limit. + */ +public final function int GetTotalLineLength() +{ + return maxTotalLineWidth; +} + +/** + * Sets current global total limit that describes how many (at most) + * characters can be output in the console line, counting both visible symbols + * and color change sequences. + * + * Instances of `ConsoleWriter` are initialized with this value, + * but can later change this value independently. + * Changes to global values do not affect already created `ConsoleWriters`. + * + * @param newMaxTotalLineWidth New global total character limit. + */ +public final function SetTotalLineLength(int newMaxTotalLineWidth) +{ + maxTotalLineWidth = newMaxTotalLineWidth; +} + +/** + * Return current global total limit that describes how many (at most) + * characters can be output in the console line. + * + * Instances of `ConsoleWriter` are initialized with this value, + * but can later change this value independently. + * Changes to global values do not affect already created `ConsoleWriters`. + * + * @return Current default output color. + */ +public final function Color GetDefaultColor(int newMaxTotalLineWidth) +{ + return defaultColor; +} + +/** + * Sets current global default color for console output., + * + * Instances of `ConsoleWriter` are initialized with this value, + * but can later change this value independently. + * Changes to global values do not affect already created `ConsoleWriters`. + * + * @param newMaxTotalLineWidth New global default output color. + */ +public final function SetDefaultColor(Color newDefaultColor) +{ + defaultColor = newDefaultColor; +} + +/** + * Returns borrowed `ConsoleWriter` instance that will write into + * consoles of all players. + * + * @return ConsoleWriter Borrowed `ConsoleWriter` instance, configured to + * write into consoles of all players. + * Never `none`. + */ +public final function ConsoleWriter ForAll() +{ + local ConsoleDisplaySettings globalSettings; + globalSettings.defaultColor = defaultColor; + globalSettings.maxTotalLineWidth = maxTotalLineWidth; + globalSettings.maxVisibleLineWidth = maxVisibleLineWidth; + return ConsoleWriter(_.memory.Claim(class'ConsoleWriter')) + .Initialize(globalSettings).ForAll(); +} + +/** + * Returns borrowed `ConsoleWriter` instance that will write into + * console of the player with a given controller. + * + * @param targetController Player, to whom console we want to write. + * If `none` - returned `ConsoleWriter` would be configured to + * throw messages away. + * @return Borrowed `ConsoleWriter` instance, configured to + * write into consoles of all players. + * Never `none`. + */ +public final function ConsoleWriter For(PlayerController targetController) +{ + local ConsoleDisplaySettings globalSettings; + globalSettings.defaultColor = defaultColor; + globalSettings.maxTotalLineWidth = maxTotalLineWidth; + globalSettings.maxVisibleLineWidth = maxVisibleLineWidth; + return ConsoleWriter(_.memory.Claim(class'ConsoleWriter')) + .Initialize(globalSettings).ForController(targetController); +} + +/** + * Returns new `ConsoleWriter` instance that will write into + * consoles of all players. + * Should be freed after use. + * + * @return ConsoleWriter New `ConsoleWriter` instance, configured to + * write into consoles of all players. + * Never `none`. + */ +public final function ConsoleWriter MakeForAll() +{ + local ConsoleDisplaySettings globalSettings; + globalSettings.defaultColor = defaultColor; + globalSettings.maxTotalLineWidth = maxTotalLineWidth; + globalSettings.maxVisibleLineWidth = maxVisibleLineWidth; + return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter')) + .Initialize(globalSettings).ForAll(); +} + +/** + * Returns new `ConsoleWriter` instance that will write into + * console of the player with a given controller. + * Should be freed after use. + * + * @param targetController Player, to whom console we want to write. + * If `none` - returned `ConsoleWriter` would be configured to + * throw messages away. + * @return New `ConsoleWriter` instance, configured to + * write into consoles of all players. + * Never `none`. + */ +public final function ConsoleWriter MakeFor(PlayerController targetController) +{ + local ConsoleDisplaySettings globalSettings; + globalSettings.defaultColor = defaultColor; + globalSettings.maxTotalLineWidth = maxTotalLineWidth; + globalSettings.maxVisibleLineWidth = maxVisibleLineWidth; + return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter')) + .Initialize(globalSettings).ForController(targetController); +} + +defaultproperties +{ + defaultColor = (R=255,G=255,B=255,A=255) + // These should guarantee decent text output even at + // 640x480 shit resolution + maxVisibleLineWidth = 80 + maxTotalLineWidth = 108 +} \ No newline at end of file diff --git a/sources/Core/Console/ConsoleBuffer.uc b/sources/Core/Console/ConsoleBuffer.uc new file mode 100644 index 0000000..a7f477a --- /dev/null +++ b/sources/Core/Console/ConsoleBuffer.uc @@ -0,0 +1,393 @@ +/** + * Object that provides a buffer functionality for Killing Floor's (in-game) + * console output: it accepts content that user want to output and breaks it + * into lines that will be well-rendered according to the given + * `ConsoleDisplaySettings`. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class ConsoleBuffer extends AcediaObject + dependson(Text) + dependson(ConsoleAPI); + +/** + * `ConsoleBuffer` works by breaking it's input into words, counting how much + * space they take up and only then deciding to which line to append them + * (new or the next, new one). + */ + +var private int CODEPOINT_ESCAPE; +var private int CODEPOINT_NEWLINE; +var private int COLOR_SEQUENCE_LENGTH; + +// Display settings according to which to format our output +var private ConsoleAPI.ConsoleDisplaySettings displaySettings; + +/** + * This structure is used to both share results of our work and for tracking + * information about the line we are currently filling. + */ +struct LineRecord +{ + // Contents of the line, in `STRING_Colored` format + var string contents; + // Is this a wrapped line? + // `true` means that this line was supposed to be part part of another, + // singular line of text, that had to be broken into smaller pieces. + // Such lines will start with "|" in front of them in Acedia's + // `ConsoleWriter`. + var bool wrappedLine; + // Information variables that describe how many visible and total symbols + // (visible + color change sequences) are stored int the `line` + var int visibleSymbolsStored; + var int totalSymbolsStored; + // Does `contents` contain a color change sequence? + // Non-empty line can have no such sequence if they consist of whitespaces. + var private bool colorInserted; + // If `colorInserted == true`, stores the last inserted color. + var private Color endColor; +}; +// Lines that are ready to be output to the console +var private array completedLines; + +// Line we are currently building +var private LineRecord currentLine; +// Word we are currently building, colors of it's characters will be +// automatically converted into `STRCOLOR_Struct`, according to the default +// color setting at the time of their addition. +var private array wordBuffer; +// Amount of color swaps inside `wordBuffer` +var private int colorSwapsInWordBuffer; + +/** + * Returns current setting used by this buffer to break up it's input into + * lines fit to be output in console. + * + * @return Currently used `ConsoleDisplaySettings`. + */ +public final function ConsoleAPI.ConsoleDisplaySettings GetSettings() +{ + return displaySettings; +} + +/** + * Sets new setting to be used by this buffer to break up it's input into + * lines fit to be output in console. + * + * It is recommended (although not required) to call `Flush()` before + * changing settings. Not doing so would not lead to any errors or warnings, + * but can lead to some wonky results and is considered an undefined behavior. + * + * @param newSettings New `ConsoleDisplaySettings` to be used. + * @return Returns caller `ConsoleBuffer` to allow for method chaining. + */ +public final function ConsoleBuffer SetSettings( + ConsoleAPI.ConsoleDisplaySettings newSettings) +{ + displaySettings = newSettings; + return self; +} + +/** + * Does caller `ConsoleBuffer` has any completed lines that can be output? + * + * "Completed line" means that nothing else will be added to it. + * So negative (`false`) response does not mean that the buffer is empty, - + * it can still contain an uncompleted and non-empty line that can still be + * expanded with `InsertString()`. If you want to completely empty the buffer - + * call the `Flush()` method. + * Also see `IsEmpty()`. + * + * @return `true` if caller `ConsoleBuffer` has no completed lines and + * `false` otherwise. + */ +public final function bool HasCompletedLines() +{ + return (completedLines.length > 0); +} + +/** + * Does caller `ConsoleBuffer` has any unprocessed input? + * + * Note that `ConsoleBuffer` can be non-empty, but no completed line if it + * currently builds one. + * See `Flush()` and `HasCompletedLines()` methods. + * + * @return `true` if `ConsoleBuffer` is completely empty + * (either did not receive or already returned all processed input) and + * `false` otherwise. + */ +public final function bool IsEmpty() +{ + if (HasCompletedLines()) return false; + if (currentLine.totalSymbolsStored > 0) return false; + if (wordBuffer.length > 0) return false; + return true; +} + +/** + * Clears the buffer of all data, but leaving current settings intact. + * After this calling method `IsEmpty()` should return `true`. + * + * @return Returns caller `ConsoleBuffer` to allow method chaining. + */ +public final function ConsoleBuffer Clear() +{ + local LineRecord newLineRecord; + currentLine = newLineRecord; + completedLines.length = 0; + return self; +} + +/** + * Inserts a string into the buffer. This method does not automatically break + * the line after the `input`, call `Flush()` or add line feed symbol "\n" + * at the end of the `input` if you want that. + * + * @param input `string` to be added to the current line in caller + * `ConsoleBuffer`. + * @param inputType How to treat given `string` regarding coloring. + * @return Returns caller `ConsoleBuffer` to allow method chaining. + */ +public final function ConsoleBuffer InsertString( + string input, + Text.StringType inputType) +{ + local int inputConsumed; + local array rawInput; + rawInput = _().text.StringToRaw(input, inputType); + while (rawInput.length > 0) + { + // Fill word buffer, remove consumed input from `rawInput` + inputConsumed = 0; + while (inputConsumed < rawInput.length) + { + if (_().text.IsWhitespace(rawInput[inputConsumed])) break; + InsertIntoWordBuffer(rawInput[inputConsumed]); + inputConsumed += 1; + } + rawInput.Remove(0, inputConsumed); + // If we didn't encounter any whitespace symbols - bail + if (rawInput.length <= 0) { + return self; + } + FlushWordBuffer(); + // Dump whitespaces into lines + inputConsumed = 0; + while (inputConsumed < rawInput.length) + { + if (!_().text.IsWhitespace(rawInput[inputConsumed])) break; + AppendWhitespaceToCurrentLine(rawInput[inputConsumed]); + inputConsumed += 1; + } + rawInput.Remove(0, inputConsumed); + } + return self; +} + +/** + * Returns (and makes caller `ConsoleBuffer` forget) next completed line that + * can be output to console in `STRING_Colored` format. + * + * If there are no completed line to return - returns an empty one. + * + * @return Next completed line that can be output, in `STRING_Colored` format. + */ +public final function LineRecord PopNextLine() +{ + local LineRecord result; + if (completedLines.length <= 0) return result; + result = completedLines[0]; + completedLines.Remove(0, 1); + return result; +} + +/** + * Forces all buffered data into "completed line" array, making it retrievable + * by `PopNextLine()`. + * + * @return Next completed line that can be output, in `STRING_Colored` format. + */ +public final function ConsoleBuffer Flush() +{ + FlushWordBuffer(); + BreakLine(false); + return self; +} + +// It is assumed that passed characters are not whitespace, - +// responsibility to check is on the one calling this method. +private final function InsertIntoWordBuffer(Text.Character newCharacter) +{ + local int newCharacterIndex; + local Color oldColor, newColor; + newCharacterIndex = wordBuffer.length; + // Fix text color in the buffer to remember default color, if we use it. + newCharacter.color = + _().text.GetCharacterColor(newCharacter, displaySettings.defaultColor); + newCharacter.colorType = STRCOLOR_Struct; + wordBuffer[newCharacterIndex] = newCharacter; + if (newCharacterIndex <= 0) { + return; + } + oldColor = wordBuffer[newCharacterIndex].color; + newColor = wordBuffer[newCharacterIndex - 1].color; + if (!_().color.AreEqual(oldColor, newColor, true)) { + colorSwapsInWordBuffer += 1; + } +} + +// Pushes whole `wordBuffer` into lines +private final function FlushWordBuffer() +{ + local int i; + local Color newColor; + if (!WordCanFitInCurrentLine() && WordCanFitInNewLine()) { + BreakLine(true); + } + for (i = 0; i < wordBuffer.length; i += 1) + { + if (!CanAppendNonWhitespaceIntoLine(wordBuffer[i])) { + BreakLine(true); + } + newColor = wordBuffer[i].color; + if (MustSwapColorsFor(newColor)) + { + currentLine.contents $= _().color.GetColorTag(newColor); + currentLine.totalSymbolsStored += COLOR_SEQUENCE_LENGTH; + currentLine.colorInserted = true; + currentLine.endColor = newColor; + } + currentLine.contents $= Chr(wordBuffer[i].codePoint); + currentLine.totalSymbolsStored += 1; + currentLine.visibleSymbolsStored += 1; + } + wordBuffer.length = 0; + colorSwapsInWordBuffer = 0; +} + +private final function BreakLine(bool makeWrapped) +{ + local LineRecord newLineRecord; + if (currentLine.visibleSymbolsStored > 0) { + completedLines[completedLines.length] = currentLine; + } + currentLine = newLineRecord; + currentLine.wrappedLine = makeWrapped; +} + +private final function bool MustSwapColorsFor(Color newColor) +{ + if (!currentLine.colorInserted) return true; + return !_().color.AreEqual(currentLine.endColor, newColor, true); +} + +private final function bool CanAppendWhitespaceIntoLine() +{ + // We always allow to append at least something into empty line, + // otherwise we can never insert it anywhere + if (currentLine.totalSymbolsStored <= 0) return true; + if (currentLine.totalSymbolsStored >= displaySettings.maxTotalLineWidth) + { + return false; + } + if (currentLine.visibleSymbolsStored >= displaySettings.maxVisibleLineWidth) + { + return false; + } + return true; +} + +private final function bool CanAppendNonWhitespaceIntoLine( + Text.Character nextCharacter) +{ + // We always allow to insert at least something into empty line, + // otherwise we can never insert it anywhere + if (currentLine.totalSymbolsStored <= 0) { + return true; + } + // Check if we can fit a single character by fitting a whitespace symbol. + if (!CanAppendWhitespaceIntoLine()) { + return false; + } + if (!MustSwapColorsFor(nextCharacter.color)) { + return true; + } + // Can we fit character + color swap sequence? + return ( currentLine.totalSymbolsStored + COLOR_SEQUENCE_LENGTH + 1 + <= displaySettings.maxTotalLineWidth); +} + +// For performance reasons assumes that passed character is a whitespace, +// the burden of checking is on the caller. +private final function AppendWhitespaceToCurrentLine(Text.Character whitespace) +{ + if (_().text.IsCodePoint(whitespace, CODEPOINT_NEWLINE)) { + BreakLine(true); + return; + } + if (!CanAppendWhitespaceIntoLine()) { + BreakLine(true); + } + currentLine.contents $= Chr(whitespace.codePoint); + currentLine.totalSymbolsStored += 1; + currentLine.visibleSymbolsStored += 1; +} + +private final function bool WordCanFitInNewLine() +{ + local int totalCharactersInWord; + if (wordBuffer.length <= 0) return true; + if (wordBuffer.length > displaySettings.maxVisibleLineWidth) { + return false; + } + // `(colorSwapsInWordBuffer + 1)` counts how many times we must + // switch color inside a word + 1 for setting initial color + totalCharactersInWord = wordBuffer.length + + (colorSwapsInWordBuffer + 1) * COLOR_SEQUENCE_LENGTH; + return (totalCharactersInWord <= displaySettings.maxTotalLineWidth); +} + +private final function bool WordCanFitInCurrentLine() +{ + local int totalLimit, visibleLimit; + local int totalCharactersInWord; + if (wordBuffer.length <= 0) return true; + totalLimit = + displaySettings.maxTotalLineWidth - currentLine.totalSymbolsStored; + visibleLimit = + displaySettings.maxVisibleLineWidth - currentLine.visibleSymbolsStored; + // Visible symbols check + if (wordBuffer.length > visibleLimit) { + return false; + } + // Total symbols check + totalCharactersInWord = wordBuffer.length + + colorSwapsInWordBuffer * COLOR_SEQUENCE_LENGTH; + if (MustSwapColorsFor(wordBuffer[0].color)) { + totalCharactersInWord += COLOR_SEQUENCE_LENGTH; + } + return (totalCharactersInWord <= totalLimit); +} + +defaultproperties +{ + CODEPOINT_ESCAPE = 27 + CODEPOINT_NEWLINE = 10 + // CODEPOINT_ESCAPE + + + + COLOR_SEQUENCE_LENGTH = 4 +} \ No newline at end of file diff --git a/sources/Core/Console/ConsoleWriter.uc b/sources/Core/Console/ConsoleWriter.uc new file mode 100644 index 0000000..7408530 --- /dev/null +++ b/sources/Core/Console/ConsoleWriter.uc @@ -0,0 +1,373 @@ +/** + * Object that provides simple access to console output. + * Can either write to a certain player's console or to all consoles at once. + * Supports "fancy" and "raw" output (for more details @see `ConsoleAPI`). + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class ConsoleWriter extends AcediaObject + dependson(ConsoleAPI) + dependson(ConnectionService); + +// Prefixes we output before every line to signify whether they were broken +// or not +var private string NEWLINE_PREFIX; +var private string BROKENLINE_PREFIX; + +/** + * Describes current output target of the `ConsoleWriter`. + */ +enum ConsoleWriterTarget +{ + // No one. Can happed if our target disconnects. + CWTARGET_None, + // A certain player. + CWTARGET_Player, + // All players. + CWTARGET_All +}; +var private ConsoleWriterTarget targetType; +// Controller of the player that will receive output passed +// to this `ConsoleWriter`. +// Only used when `targetType == CWTARGET_Player` +var private PlayerController outputTarget; +var private ConsoleBuffer outputBuffer; + +var private ConsoleAPI.ConsoleDisplaySettings displaySettings; + +public final function ConsoleWriter Initialize( + ConsoleAPI.ConsoleDisplaySettings newDisplaySettings) +{ + displaySettings = newDisplaySettings; + if (outputBuffer == none) { + outputBuffer = ConsoleBuffer(_().memory.Allocate(class'ConsoleBuffer')); + } + else { + outputBuffer.Clear(); + } + outputBuffer.SetSettings(displaySettings); + return self; +} + +/** + * Return current default color for caller `ConsoleWriter`. + * + * This method returns default color, i.e. color that will be used if no other + * is specified by text you're outputting. + * If color is specified, this value will be ignored. + * + * This value is not synchronized with the global value from `ConsoleAPI` + * (or such value from any other `ConsoleWriter`) and affects only + * output produced by this `ConsoleWriter`. + * + * @return Current default color. + */ +public final function Color GetColor() +{ + return displaySettings.defaultColor; +} + +/** + * Sets default color for caller 'ConsoleWriter`'s output. + * + * This only changes default color, i.e. color that will be used if no other is + * specified by text you're outputting. + * If color is specified, this value will be ignored. + * + * This value is not synchronized with the global value from `ConsoleAPI` + * (or such value from any other `ConsoleWriter`) and affects only + * output produced by this `ConsoleWriter`. + * + * @param newDefaultColor New color to use when none specified by text itself. + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter SetColor(Color newDefaultColor) +{ + displaySettings.defaultColor = newDefaultColor; + if (outputBuffer != none) { + outputBuffer.SetSettings(displaySettings); + } + return self; +} + +/** + * Return current visible limit that describes how many (at most) + * visible characters can be output in the console line. + * + * This value is not synchronized with the global value from `ConsoleAPI` + * (or such value from any other `ConsoleWriter`) and affects only + * output produced by this `ConsoleWriter`. + * + * @return Current global visible limit. + */ +public final function int GetVisibleLineLength() +{ + return displaySettings.maxVisibleLineWidth; +} + +/** + * Sets current visible limit that describes how many (at most) visible + * characters can be output in the console line. + * + * This value is not synchronized with the global value from `ConsoleAPI` + * (or such value from any other `ConsoleWriter`) and affects only + * output produced by this `ConsoleWriter`. + * + * @param newVisibleLimit New global visible limit. + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter SetVisibleLineLength( + int newMaxVisibleLineWidth +) +{ + displaySettings.maxVisibleLineWidth = newMaxVisibleLineWidth; + if (outputBuffer != none) { + outputBuffer.SetSettings(displaySettings); + } + return self; +} + +/** + * Return current total limit that describes how many (at most) + * characters can be output in the console line. + * + * This value is not synchronized with the global value from `ConsoleAPI` + * (or such value from any other `ConsoleWriter`) and affects only + * output produced by this `ConsoleWriter`. + * + * @return Current global total limit. + */ +public final function int GetTotalLineLength() +{ + return displaySettings.maxTotalLineWidth; +} + +/** + * Sets current total limit that describes how many (at most) + * characters can be output in the console line. + * + * This value is not synchronized with the global value from `ConsoleAPI` + * (or such value from any other `ConsoleWriter`) and affects only + * output produced by this `ConsoleWriter`. + * + * @param newTotalLimit New global total limit. + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter SetTotalLineLength(int newMaxTotalLineWidth) +{ + displaySettings.maxTotalLineWidth = newMaxTotalLineWidth; + if (outputBuffer != none) { + outputBuffer.SetSettings(displaySettings); + } + return self; +} + +/** + * Configures caller `ConsoleWriter` to output to all players. + * `Flush()` will be automatically called between target change. + * + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter ForAll() +{ + Flush(); + targetType = CWTARGET_All; + return self; +} + +/** + * Configures caller `ConsoleWriter` to output only to a player, + * given by a passed `PlayerController`. + * `Flush()` will be automatically called between target change. + * + * @param targetController Player, to whom console we want to write. + * If `none` - caller `ConsoleWriter` would be configured to + * throw messages away. + * @return ConsoleWriter Returns caller `ConsoleWriter` to allow for + * method chaining. + */ +public final function ConsoleWriter ForController( + PlayerController targetController +) +{ + Flush(); + if (targetController != none) + { + targetType = CWTARGET_Player; + outputTarget = targetController; + } + else { + targetType = CWTARGET_None; + } + return self; +} + +/** + * Returns type of current target for the caller `ConsoleWriter`. + * + * @return `ConsoleWriterTarget` value, describing current target of + * the caller `ConsoleWriter`. + */ +public final function ConsoleWriterTarget CurrentTarget() +{ + if (targetType == CWTARGET_Player && outputTarget == none) { + targetType = CWTARGET_None; + } + return targetType; +} + +/** + * Returns `PlayerController` of the player to whom console caller + * `ConsoleWriter` is outputting messages. + * + * @return `PlayerController` of the player to whom console caller + * `ConsoleWriter` is outputting messages. + * Returns `none` iff it currently outputs to every player or to no one. + */ +public final function PlayerController GetTargetPlayerController() +{ + if (targetType == CWTARGET_All) return none; + return outputTarget; +} + +/** + * Outputs all buffered input and moves further output onto a new line. + * + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter Flush() +{ + outputBuffer.Flush(); + SendBuffer(); + return self; +} + +/** + * Writes a formatted string into console. + * + * Does not trigger console output, for that use `WriteLine()` or `Flush()`. + * + * To output a different type of string into a console, use `WriteT()`. + * + * @param message Formatted string to output. + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter Write(string message) +{ + outputBuffer.InsertString(message, STRING_Formatted); + return self; +} + +/** + * Writes a formatted string into console. + * Result will be output immediately, starts a new line. + * + * To output a different type of string into a console, use `WriteLineT()`. + * + * @param message Formatted string to output. + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter WriteLine(string message) +{ + outputBuffer.InsertString(message, STRING_Formatted); + Flush(); + return self; +} + +/** + * Writes a `string` of specified type into console. + * + * Does not trigger console output, for that use `WriteLineT()` or `Flush()`. + * + * To output a formatted string you might want to simply use `Write()`. + * + * @param message String of a given type to output. + * @param inputType Type of the string method should output. + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter WriteT( + string message, + Text.StringType inputType) +{ + outputBuffer.InsertString(message, inputType); + return self; +} + +/** + * Writes a `string` of specified type into console. + * Result will be output immediately, starts a new line. + * + * To output a formatted string you might want to simply use `WriteLine()`. + * + * @param message String of a given type to output. + * @param inputType Type of the string method should output. + * @return Returns caller `ConsoleWriter` to allow for method chaining. + */ +public final function ConsoleWriter WriteLineT( + string message, + Text.StringType inputType) +{ + outputBuffer.InsertString(message, inputType); + Flush(); + return self; +} + +// Send all completed lines from an `outputBuffer` +private final function SendBuffer() +{ + local string prefix; + local ConnectionService service; + local ConsoleBuffer.LineRecord nextLineRecord; + while (outputBuffer.HasCompletedLines()) + { + nextLineRecord = outputBuffer.PopNextLine(); + if (nextLineRecord.wrappedLine) { + prefix = NEWLINE_PREFIX; + } + else { + prefix = BROKENLINE_PREFIX; + } + service = ConnectionService(class'ConnectionService'.static.Require()); + SendConsoleMessage(service, prefix $ nextLineRecord.contents); + } +} + +// Assumes `service != none`, caller function must ensure that. +private final function SendConsoleMessage( + ConnectionService service, + string message) +{ + local int i; + local array connections; + if (targetType != CWTARGET_All) + { + if (outputTarget != none) { + outputTarget.ClientMessage(message); + } + return; + } + connections = service.GetActiveConnections(); + for (i = 0; i < connections.length; i += 1) { + connections[i].controllerReference.ClientMessage(message); + } +} + +defaultproperties +{ + NEWLINE_PREFIX = "| " + BROKENLINE_PREFIX = " " +} \ No newline at end of file From 2c1a43f9d42dd3f2079e63b791d30ea37b3099d2 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:31:58 +0700 Subject: [PATCH 23/59] Add basic version of `LoggerAPI` --- sources/Core/Logger/LoggerAPI.uc | 92 +++++++++++++++ sources/Core/Logger/LoggerService.uc | 166 +++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 sources/Core/Logger/LoggerAPI.uc create mode 100644 sources/Core/Logger/LoggerService.uc diff --git a/sources/Core/Logger/LoggerAPI.uc b/sources/Core/Logger/LoggerAPI.uc new file mode 100644 index 0000000..8b2f5d8 --- /dev/null +++ b/sources/Core/Logger/LoggerAPI.uc @@ -0,0 +1,92 @@ +/** + * API that provides functions quick access to Acedia's + * logging functionality. + * 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 LoggerAPI extends Singleton; + +var private LoggerService logService; + +protected function OnCreated() +{ + logService = LoggerService(class'LoggerService'.static.Require()); +} + +public final function Track(string message) +{ + if (logService == none) + { + class'LoggerService'.static.LogMessageToKFLog(LOG_Track, message); + return; + } + logService.LogMessage(LOG_Track, message); +} + +public final function Debug(string message) +{ + if (logService == none) + { + class'LoggerService'.static.LogMessageToKFLog(LOG_Debug, message); + return; + } + logService.LogMessage(LOG_Debug, message); +} + +public final function Info(string message) +{ + if (logService == none) + { + class'LoggerService'.static.LogMessageToKFLog(LOG_Info, message); + return; + } + logService.LogMessage(LOG_Info, message); +} + +public final function Warning(string message) +{ + if (logService == none) + { + class'LoggerService'.static.LogMessageToKFLog(LOG_Warning, message); + return; + } + logService.LogMessage(LOG_Warning, message); +} + +public final function Failure(string message) +{ + if (logService == none) + { + class'LoggerService'.static.LogMessageToKFLog(LOG_Failure, message); + return; + } + logService.LogMessage(LOG_Failure, message); +} + +public final function Fatal(string message) +{ + if (logService == none) + { + class'LoggerService'.static.LogMessageToKFLog(LOG_Fatal, message); + return; + } + logService.LogMessage(LOG_Fatal, message); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Core/Logger/LoggerService.uc b/sources/Core/Logger/LoggerService.uc new file mode 100644 index 0000000..3dd8d85 --- /dev/null +++ b/sources/Core/Logger/LoggerService.uc @@ -0,0 +1,166 @@ +/** + * 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_Trace && 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_Error && 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_Trace: + 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_Error: + 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_Trace: + 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_Error: + 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 From f6364229edfda25f64814f5b9b819cfbfc6edf49 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:32:11 +0700 Subject: [PATCH 24/59] Add `MemoryAPI` --- sources/Core/Memory/MemoryAPI.uc | 290 +++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 sources/Core/Memory/MemoryAPI.uc diff --git a/sources/Core/Memory/MemoryAPI.uc b/sources/Core/Memory/MemoryAPI.uc new file mode 100644 index 0000000..810e72a --- /dev/null +++ b/sources/Core/Memory/MemoryAPI.uc @@ -0,0 +1,290 @@ +/** + * API that provides functions for managing objects and actors by providing + * easy and general means to create and destroy them, that allow to make use of + * temporary `Object`s in a more efficient way. + * This is a low-level API that most users of Acedia, most likely, + * would not have to use, since creation of most objects would use their own + * wrapper functions around this API. + * 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 MemoryAPI extends Singleton; + +// This variable counts ticks and should be different each new tick. +var private int currentTick; + +// Stores instance of an `Object` that can be borrowed from the pool. +struct BorrowableRecord +{ + // Borrowable instance + var Object instance; + // Was this object borrowed? + // This flag will persist unless object was explicitly freed, + // even if borrowed reference timed out. + var bool borrowed; + // When was this object borrowed? + // Used to automatically free borrowed objects after the tick has passed. + var int borrowTick; +}; + +// Available object pools +var private array borrowPool; + +// Checks if instance in the given `record` is borrowed. +private final function bool IsBorrowed(BorrowableRecord record) +{ + // `record.borrowed` means instance was borrowed, + // but not explicitly freed; + // `record.borrowTick >= currentTick` means that rights to the borrowed + // instance hasn't yet ran out. + return (record.borrowed && record.borrowTick >= currentTick); +} + +// Loads a reference to class instance from it's string representation. +private final function class LoadClass(string classReference) +{ + return class(DynamicLoadObject(classReference, class'Class', true)); +} + +/** + * Creates a new `Object` / `Actor` of a given class. + * + * If uses a proper spawning mechanism for both objects (`new`) + * and actors (`Spawn`). + * + * @param classToAllocate Class of the `Object` / `Actor` that this method + * must create. + * @return Newly created object, might be `none` if creation has failed. + */ +public final function Object Allocate(class classToAllocate) +{ + local class actorClassToSpawn; + if (classToAllocate == none) return none; + + actorClassToSpawn = class(classToAllocate); + if (actorClassToSpawn != none) + { + return Spawn(actorClassToSpawn); + } + return (new classToAllocate); +} + +/** + * Creates a new `Object` / `Actor` of a given class. + * + * If uses a proper spawning mechanism for both objects (`new`) + * and actors (`Spawn`). + * + * @param classToAllocate Text representation (name) of the class of the + * `Object` / `Actor` that this method must create. + * Should contain full package-path. + * @return Newly created object, might be `none` if creation has failed. + */ +public final function Object AllocateByReference(string refToClassToAllocate) +{ + return Allocate(LoadClass(refToClassToAllocate)); +} + +/** + * Borrows an instance of an `Object` / `Actor` of the given class + * from the pool. + * Borrowed instance will be auto-freed during next tick. + * + * @param classToBorrow Class of an `Object` / `Actor` we want to borrow. + * @return Borrowed object, might be `none` if borrow pool is empty and + * creation of a new `Object` / `Actor` has failed. + */ +public final function Object Borrow(class classToBorrow) +{ + local int i; + local BorrowableRecord newRecord; + for (i = 0; i < borrowPool.length; i += 1) + { + if (IsBorrowed(borrowPool[i])) continue; + if (borrowPool[i].instance == none) continue; + if (borrowPool[i].instance.class != classToBorrow) continue; + + borrowPool[i].borrowed = true; + borrowPool[i].borrowTick = currentTick; + return borrowPool[i].instance; + } + // Create a new instance to borrow, if there isn't any available for + // the given class. + newRecord.borrowed = false; + newRecord.instance = Allocate(classToBorrow); + if (newRecord.instance != none) + { + borrowPool[borrowPool.length] = newRecord; + } + return newRecord.instance; +} + +/** + * Borrows an instance of an `Object` / `Actor` of the given class + * from the pool. + * Borrowed instance will be auto-freed during next tick. + * + * @param classToBorrow Text representation (name) of the class of + * an `Object` / `Actor` we want to borrow. + * @return Borrowed object, might be `none` if borrow pool is empty and + * creation of a new `Object` / `Actor` has failed. + */ +public final function Object BorrowByReference(string refToClassToBorrow) +{ + return Borrow(LoadClass(refToClassToBorrow)); +} + +/** + * Claims an instance of an `Object` / `Actor` of the given class + * from the pool. + * Claimed instances are removed from the borrow pool and + * will not be automatically freed. + * + * @param classToClaim Class of an `Object` / `Actor` we wish to borrow. + * @return Borrowed object, might be `none` if borrow pool is empty and + * creation of a new `Object` / `Actor` has failed. + */ +public final function Object Claim(class classToClaim) +{ + local int i; + local Object instance; + for (i = 0; i < borrowPool.length; i += 1) + { + if (IsBorrowed(borrowPool[i])) continue; + if (borrowPool[i].instance == none) continue; + if (borrowPool[i].instance.class != classToClaim) continue; + + instance = borrowPool[i].instance; + borrowPool.Remove(i, 1); + return instance; + } + // Create a new instance to borrow, if there isn't any available for + // the given class. + return Allocate(classToClaim); +} + +/** + * Claims an instance of an `Object` / `Actor` of the given class + * from the pool. + * Claimed instances are removed from the borrow pool and + * will not be automatically freed. + * + * @param classToClaim Text representation (name) of the class of + * an `Object` / `Actor` we wish to claim. + * @return Borrowed object, might be `none` if borrow pool is empty and + * creation of a new `Object` / `Actor` has failed. + */ +public final function Object ClaimByReference(string refToClassToClaim) +{ + return Claim(LoadClass(refToClassToClaim)); +} + +/** + * Frees given `Object` / `Actor` resource. + * + * By default `Actor`s are destroyed. + * Due to limitations of the engine objects cannot be outright destroyed. + * Instead, they are put into a "borrow pool", from where they can later be + * taken for a reuse. + * + * @param objectToDelete `Object` / `Actor` that must be freed. + * @param forceMakeBorrowable Only has an effect if `objectToDelete` + * is an `Actor`, in which case it forces it to be added + * to the borrow pool, instead of being destroyed. + */ +public final function Free +( + Object objectToDelete, + optional bool forceMakeBorrowable +) +{ + local int i; + local Actor actorToDelete; + local BorrowableRecord newRecord; + if (objectToDelete == none) return; + + actorToDelete = Actor(objectToDelete); + if (actorToDelete != none && !forceMakeBorrowable) + { + actorToDelete.Destroy(); + return; + } + // Check if `objectToDelete` is already in our records. + for (i = 0; i < borrowPool.length; i += 1) + { + if (borrowPool[i].instance == objectToDelete) + { + borrowPool[i].borrowed = false; + return; + } + } + // If not - add it + newRecord.instance = objectToDelete; + newRecord.borrowed = false; + borrowPool[borrowPool.length] = newRecord; +} + +/** + * Forces Unreal Engine to do garbage collection. + * By default also cleans up all the objects in the borrow object pool. + * + * Process of garbage collection causes significant lag spike during the game + * and should be used carefully. + * + * NOTE: method does not guarantee that borrow pool will be empty after + * this call (even with `keepBorrowedObjectPool = true`), + * since some of the borrowable objects might be currently in use and, + * therefore, cannot be garbage collected. + * + * @param keepBorrowedObjectPool Set this to `true` to NOT garbage collect + * objects in a borrow pool. Otherwise keep it `false`. + */ +public final function CollectGarbage(optional bool keepBorrowedObjectPool) +{ + local int i; + if (!keepBorrowedObjectPool) + { + // Dereference all non-borrowed objects from borrow pool, + // so that they can be garbage collected. + i = 0; + while (i < borrowPool.length) + { + if ( borrowPool[i].instance == none + || !IsBorrowed(borrowPool[i]) ) + { + borrowPool.Remove(i, 1); + } + else + { + i += 1; + } + } + } + // This makes Unreal Engine do garbage collection + ConsoleCommand("obj garbage"); +} + +event Tick(float delta) +{ + currentTick += 1; +} + +// TODO: add cleaning on cooldown +defaultproperties +{ + currentTick = 0 +} \ No newline at end of file From cef551b66008125fcd9f26030b3c768d156888e8 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:32:52 +0700 Subject: [PATCH 25/59] Add global `string` to `Text` conversion --- sources/Core/AcediaActor.uc | 5 +++++ sources/Core/AcediaObject.uc | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/sources/Core/AcediaActor.uc b/sources/Core/AcediaActor.uc index 1950db2..ce51fee 100644 --- a/sources/Core/AcediaActor.uc +++ b/sources/Core/AcediaActor.uc @@ -25,6 +25,11 @@ class AcediaActor extends Actor var protected Global _; +public final function Text T(string string) +{ + return _.text.FromString(string); +} + event PreBeginPlay() { super.PreBeginPlay(); diff --git a/sources/Core/AcediaObject.uc b/sources/Core/AcediaObject.uc index ec6cb4d..2d451cf 100644 --- a/sources/Core/AcediaObject.uc +++ b/sources/Core/AcediaObject.uc @@ -25,6 +25,11 @@ class AcediaObject extends Object abstract; +public final function Text T(string string) +{ + return _().text.FromString(string); +} + public static final function Global _() { return Global(class'Global'.static.GetInstance()); From b6e75a44b019a5074e4ad38b38386204c238d277 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:34:11 +0700 Subject: [PATCH 26/59] Change `ConnectionService` to add it's listeners --- sources/Services/Connection/ConnectionService.uc | 1 + 1 file changed, 1 insertion(+) diff --git a/sources/Services/Connection/ConnectionService.uc b/sources/Services/Connection/ConnectionService.uc index 89375fa..bc128cb 100644 --- a/sources/Services/Connection/ConnectionService.uc +++ b/sources/Services/Connection/ConnectionService.uc @@ -139,4 +139,5 @@ event Tick(float delta) defaultproperties { events = class'ConnectionEvents' + requiredListeners(0) = class'MutatorListener_Connection' } \ No newline at end of file From 826e6272a4481e10e834dfab585905c33da4a704 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:35:25 +0700 Subject: [PATCH 27/59] Add proper method to disable a `Feature` --- sources/Core/Feature.uc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sources/Core/Feature.uc b/sources/Core/Feature.uc index 675f962..fc29a7b 100644 --- a/sources/Core/Feature.uc +++ b/sources/Core/Feature.uc @@ -53,7 +53,6 @@ public static final function bool IsEnabled() } // Enables feature of given class. -// To disable a feature simply use 'Destroy'. public static final function Feature EnableMe() { local Feature newInstance; @@ -67,6 +66,18 @@ public static final function Feature EnableMe() return newInstance; } +public static final function bool DisableMe() +{ + local Feature myself; + myself = Feature(GetInstance()); + if (myself != none) + { + myself.Destroy(); + return true; + } + return false; +} + // Event functions that are called when protected function OnEnabled(){} protected function OnDisabled(){} @@ -98,6 +109,7 @@ protected function OnDestroyed() defaultproperties { autoEnable = false + DrawType = DT_None // Prevent spawning this feature by any other means than 'EnableMe()'. blockSpawning = true // Features are server-only actors From 740f98edbd0eea50ac04871c6e797854addca550 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:36:01 +0700 Subject: [PATCH 28/59] Add new APIs to a `Global` object --- sources/Global.uc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sources/Global.uc b/sources/Global.uc index 3af79dd..5cbb741 100644 --- a/sources/Global.uc +++ b/sources/Global.uc @@ -22,13 +22,30 @@ class Global extends Singleton; var public Acedia acedia; +var public LoggerAPI logger; var public JSONAPI json; var public AliasesAPI alias; +var public TextAPI text; +var public MemoryAPI memory; +var public ConsoleAPI console; +var public ColorAPI color; +// TODO: APIs must be `remoteRole = ROLE_None` protected function OnCreated() { acedia = class'Acedia'.static.GetInstance(); + Spawn(class'LoggerAPI'); + logger = LoggerAPI(class'LoggerAPI'.static.GetInstance()); Spawn(class'JSONAPI'); json = JSONAPI(class'JSONAPI'.static.GetInstance()); + Spawn(class'AliasesAPI'); alias = AliasesAPI(class'AliasesAPI'.static.GetInstance()); + Spawn(class'TextAPI'); + text = TextAPI(class'TextAPI'.static.GetInstance()); + Spawn(class'MemoryAPI'); + memory = MemoryAPI(class'MemoryAPI'.static.GetInstance()); + Spawn(class'ConsoleAPI'); + console = ConsoleAPI(class'ConsoleAPI'.static.GetInstance()); + Spawn(class'ColorAPI'); + color = ColorAPI(class'ColorAPI'.static.GetInstance()); } \ No newline at end of file From 507c7ba12ce1bec31d6f3236dd4081c187072baa Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:37:20 +0700 Subject: [PATCH 29/59] Fix `Singleton` not calling `OnDestroyed()` event --- sources/Core/Singleton.uc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sources/Core/Singleton.uc b/sources/Core/Singleton.uc index c4d61c4..bd83fa7 100644 --- a/sources/Core/Singleton.uc +++ b/sources/Core/Singleton.uc @@ -91,9 +91,10 @@ event PreBeginPlay() event Destroyed() { super.Destroyed(); - if (self == GetInstance()) + if (self == default.activeInstance) { OnDestroyed(); + default.activeInstance = none; } } From ba85300315e2c1ca57c8fcde18bb298af2ffa113 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:38:39 +0700 Subject: [PATCH 30/59] Change Acedia's loading process --- sources/Core/Acedia.uc | 77 ++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/sources/Core/Acedia.uc b/sources/Core/Acedia.uc index fbe7764..96707ce 100644 --- a/sources/Core/Acedia.uc +++ b/sources/Core/Acedia.uc @@ -1,5 +1,5 @@ /** - * Main and only Acedia mutator used for initialization of necessary services + * Main and only Acedia mutator used for loading Acedia packages * and providing access to mutator events' calls. * Copyright 2020 Anton Tarasenko *------------------------------------------------------------------------------ @@ -29,10 +29,10 @@ class Acedia extends Mutator var private Acedia selfReference; // Array of predefined services that must be started along with Acedia mutator. -var private array< class > systemServices; +var private config array< class > registeredManifests; -// All unit tests loaded from all packages. -var private array< class > testCases; +// Array of predefined services that must be started along with Acedia mutator. +var private array< class > systemServices; static public final function Acedia GetInstance() { @@ -48,46 +48,58 @@ event PreBeginPlay() return; } default.selfReference = self; - // Boot up Acedia + BootUp(); + if (class'TestingService'.default.runTestsOnStartUp) { + RunStartUpTests(); + } +} + +private final function BootUp() +{ + local int i; Spawn(class'Global'); - LoadManifest(class'Manifest'); - LaunchServices(); + for (i = 0; i < registeredManifests.length; i += 1) { + LoadManifest(registeredManifests[i]); + } InjectBroadcastHandler(); // TODO: move this to 'SideEffect' mechanic } +private final function RunStartUpTests() +{ + local TestingService testService; + testService = TestingService(class'TestingService'.static.Require()); + testService.PrepareTests(); + if (testService.filterTestsByName) { + testService.FilterByName(testService.requiredName); + } + if (testService.filterTestsByGroup) { + testService.FilterByName(testService.requiredGroup); + } + testService.Run(); +} + private final function LoadManifest(class manifestClass) { local int i; - // Activate manifest's listeners - for (i = 0; i < manifestClass.default.requiredListeners.length; i += 1) + // Load alias sources + for (i = 0; i < manifestClass.default.aliasSources.length; i += 1) { - if (manifestClass.default.requiredListeners[i] == none) continue; - manifestClass.default.requiredListeners[i].static.SetActive(true); + if (manifestClass.default.aliasSources[i] == none) continue; + Spawn(manifestClass.default.aliasSources[i]); } // Enable features for (i = 0; i < manifestClass.default.features.length; i += 1) { if (manifestClass.default.features[i] == none) continue; - if (manifestClass.default.features[i].static.IsAutoEnabled()) - { + if (manifestClass.default.features[i].static.IsAutoEnabled()) { manifestClass.default.features[i].static.EnableMe(); } } - // Load unit tests + // Load tests cases for (i = 0; i < manifestClass.default.testCases.length; i += 1) { - if (manifestClass.default.testCases[i] == none) continue; - testCases[testCases.length] = manifestClass.default.testCases[i]; - } -} - -private final function LaunchServices() -{ - local int i; - for (i = 0; i < systemServices.length; i += 1) - { - if (systemServices[i] == none) continue; - Spawn(systemServices[i]); + class'TestingService'.static + .RegisterTestCase(manifestClass.default.testCases[i]); } } @@ -119,23 +131,22 @@ function bool CheckReplacement(Actor other, out byte isSuperRelevant) CallCheckReplacement(other, isSuperRelevant); } -function Mutate(string command, PlayerController sendingPlayer) +function Mutate(string command, PlayerController sendingController) { - if (class'MutatorEvents'.static.CallMutate(command, sendingPlayer)) - { - super.Mutate(command, sendingPlayer); + if (class'MutatorEvents'.static.CallMutate(command, sendingController)) { + super.Mutate(command, sendingController); } } defaultproperties { - // List built-in services - systemServices(0) = class'ConnectionService' + // Add Acedia's own manifest + registeredManifests(0) = class'Manifest' // This is a server-only mutator remoteRole = ROLE_None bAlwaysRelevant = true // Mutator description GroupName = "Core mutator" FriendlyName = "Acedia" - Description = "Mutator for all your degenerate needs" + Description = "Launcher for Acedia modules" } \ No newline at end of file From d62e4d8e67eadc4c1d3926707560c99402d5d53f Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:39:05 +0700 Subject: [PATCH 31/59] Update Acedia's `Manifest` --- sources/Manifest.uc | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 3ccaa5b..0652b72 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -24,16 +24,19 @@ abstract; // List of features in this manifest's package. -var public const array< class > features; +var public const array< class > aliasSources; // List of features in this manifest's package. -var public const array< class > testCases; +var public const array< class > features; -// Listeners listed here will be automatically activated. -var public const array< class > requiredListeners; +// List of features in this manifest's package. +var public const array< class > testCases; defaultproperties { + aliasSources(0) = class'AliasSource' + aliasSources(1) = class'WeaponAliasSource' + aliasSources(2) = class'ColorAliasSource' features(0) = class'FixZedTimeLags' features(1) = class'FixDoshSpam' features(2) = class'FixFFHack' @@ -42,9 +45,11 @@ defaultproperties features(5) = class'FixSpectatorCrash' features(6) = class'FixDualiesCost' features(7) = class'FixInventoryAbuse' - // Listeners - requiredListeners(0) = class'MutatorListener_Connection' // Unit tests - testCases(0) = class'TEST_JSON' - testCases(1) = class'TEST_Aliases' + testCases(0) = class'TEST_Aliases' + testCases(1) = class'TEST_ColorAPI' + testCases(2) = class'TEST_JSON' + testCases(3) = class'TEST_Text' + testCases(4) = class'TEST_TextAPI' + testCases(5) = class'TEST_Parser' } \ No newline at end of file From 35ca9d6cb8e209a02be07ee191623ca057106ca6 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:39:22 +0700 Subject: [PATCH 32/59] Update configs --- config/AcediaAliases_Colors.ini | 165 ++++++++++++++++ config/AcediaAliases_Tests.ini | 17 ++ ...aAliases.ini => AcediaAliases_Weapons.ini} | 178 ++++++++--------- config/AcediaSystem.ini | 180 ++++++++++++++++++ 4 files changed, 440 insertions(+), 100 deletions(-) create mode 100644 config/AcediaAliases_Colors.ini create mode 100644 config/AcediaAliases_Tests.ini rename config/{AcediaAliases.ini => AcediaAliases_Weapons.ini} (72%) create mode 100644 config/AcediaSystem.ini diff --git a/config/AcediaAliases_Colors.ini b/config/AcediaAliases_Colors.ini new file mode 100644 index 0000000..27c0935 --- /dev/null +++ b/config/AcediaAliases_Colors.ini @@ -0,0 +1,165 @@ +[Acedia.ColorAliasSource] +; System colors +record=(alias="text_default",value="rgb(255,255,255)") +record=(alias="text_subtle",value="rgb(128,128,128)") +record=(alias="text_emphasis",value="rgb(0,128,255)") +record=(alias="text_ok",value="rgb(0,255,0)") +record=(alias="text_warning",value="rgb(255,128,0)") +record=(alias="text_failure",value="rgb(255,0,0)") +record=(alias="type_number",value="rgb(181,137,0)") +record=(alias="type_boolean",value="rgb(38,139,210)") +record=(alias="type_string",value="rgb(211,54,130)") +record=(alias="type_literal",value="rgb(42,161,152)") +record=(alias="type_class",value="rgb(108,113,196)") +; Pink colors +record=(alias="Pink",value="rgb(255,192,203)") +record=(alias="LightPink",value="rgb(255,182,193)") +record=(alias="HotPink",value="rgb(255,105,180)") +record=(alias="DeepPink",value="rgb(255,20,147)") +record=(alias="PaleVioletRed",value="rgb(219,112,147)") +record=(alias="MediumVioletRed",value="rgb(199,21,133)") +; Red colors +record=(alias="LightSalmon",value="rgb(255,160,122)") +record=(alias="Salmon",value="rgb(250,128,114)") +record=(alias="DarkSalmon",value="rgb(233,150,122)") +record=(alias="LightCoral",value="rgb(240,128,128)") +record=(alias="IndianRed",value="rgb(205,92,92)") +record=(alias="Crimson",value="rgb(220,20,60)") +record=(alias="Firebrick",value="rgb(178,34,34)") +record=(alias="DarkRed",value="rgb(139,0,0)") +record=(alias="Red",value="rgb(255,0,0)") +; Orange colors +record=(alias="OrangeRed",value="rgb(255,69,0)") +record=(alias="Tomato",value="rgb(255,99,71)") +record=(alias="Coral",value="rgb(255,127,80)") +record=(alias="DarkOrange",value="rgb(255,140,0)") +record=(alias="Orange",value="rgb(255,165,0)") +; Yellow colors +record=(alias="Yellow",value="rgb(255,255,0)") +record=(alias="LightYellow",value="rgb(255,255,224)") +record=(alias="LemonChiffon",value="rgb(255,250,205)") +record=(alias="LightGoldenrodYellow",value="rgb(250,250,210)") +record=(alias="PapayaWhip",value="rgb(255,239,213)") +record=(alias="Moccasin",value="rgb(255,228,181)") +record=(alias="PeachPuff",value="rgb(255,218,185)") +record=(alias="PaleGoldenrod",value="rgb(238,232,170)") +record=(alias="Khaki",value="rgb(240,230,140)") +record=(alias="DarkKhaki",value="rgb(189,183,107)") +record=(alias="Gold",value="rgb(255,215,0)") +; Brown colors +record=(alias="Cornsilk",value="rgb(255,248,220)") +record=(alias="BlanchedAlmond",value="rgb(255,235,205)") +record=(alias="Bisque",value="rgb(255,228,196)") +record=(alias="NavajoWhite",value="rgb(255,222,173)") +record=(alias="Wheat",value="rgb(245,222,179)") +record=(alias="Burlywood",value="rgb(222,184,135)") +record=(alias="Tan",value="rgb(210,180,140)") +record=(alias="RosyBrown",value="rgb(188,143,143)") +record=(alias="SandyBrown",value="rgb(244,164,96)") +record=(alias="Goldenrod",value="rgb(218,165,32)") +record=(alias="DarkGoldenrod",value="rgb(184,134,11)") +record=(alias="Peru",value="rgb(205,133,63)") +record=(alias="Chocolate",value="rgb(210,105,30)") +record=(alias="SaddleBrown",value="rgb(139,69,19)") +record=(alias="Sienna",value="rgb(160,82,45)") +record=(alias="Brown",value="rgb(165,42,42)") +record=(alias="Maroon",value="rgb(128,0,0)") +; Green colors +record=(alias="DarkOliveGreen",value="rgb(85,107,47)") +record=(alias="Olive",value="rgb(128,128,0)") +record=(alias="OliveDrab",value="rgb(107,142,35)") +record=(alias="YellowGreen",value="rgb(154,205,50)") +record=(alias="LimeGreen",value="rgb(50,205,50)") +record=(alias="Lime",value="rgb(0,255,0)") +record=(alias="LawnGreen",value="rgb(124,252,0)") +record=(alias="Chartreuse",value="rgb(127,255,0)") +record=(alias="GreenYellow",value="rgb(173,255,47)") +record=(alias="SpringGreen",value="rgb(0,255,127)") +record=(alias="MediumSpringGreen",value="rgb(0,250,154)") +record=(alias="LightGreen",value="rgb(144,238,144)") +record=(alias="PaleGreen",value="rgb(152,251,152)") +record=(alias="DarkSeaGreen",value="rgb(143,188,143)") +record=(alias="MediumAquamarine",value="rgb(102,205,170)") +record=(alias="MediumSeaGreen",value="rgb(60,179,113)") +record=(alias="SeaGreen",value="rgb(46,139,87)") +record=(alias="ForestGreen",value="rgb(34,139,34)") +record=(alias="Green",value="rgb(0,128,0)") +record=(alias="DarkGreen",value="rgb(0,100,0)") +; Cyan colors +record=(alias="Aqua",value="rgb(0,255,255)") +record=(alias="Cyan",value="rgb(0,255,255)") +record=(alias="LightCyan",value="rgb(224,255,255)") +record=(alias="PaleTurquoise",value="rgb(175,238,238)") +record=(alias="Aquamarine",value="rgb(127,255,212)") +record=(alias="Turquoise",value="rgb(64,224,208)") +record=(alias="MediumTurquoise",value="rgb(72,209,204)") +record=(alias="DarkTurquoise",value="rgb(0,206,209)") +record=(alias="LightSeaGreen",value="rgb(32,178,170)") +record=(alias="CadetBlue",value="rgb(95,158,160)") +record=(alias="DarkCyan",value="rgb(0,139,139)") +record=(alias="Teal",value="rgb(0,128,128)") +; Blue colors +record=(alias="LightSteelBlue",value="rgb(176,196,222)") +record=(alias="PowderBlue",value="rgb(176,224,230)") +record=(alias="LightBlue",value="rgb(173,216,230)") +record=(alias="SkyBlue",value="rgb(135,206,235)") +record=(alias="LightSkyBlue",value="rgb(135,206,250)") +record=(alias="DeepSkyBlue",value="rgb(0,191,255)") +record=(alias="DodgerBlue",value="rgb(30,144,255)") +record=(alias="CornflowerBlue",value="rgb(100,149,237)") +record=(alias="SteelBlue",value="rgb(70,130,180)") +record=(alias="RoyalBlue",value="rgb(65,105,225)") +record=(alias="Blue",value="rgb(0,0,255)") +record=(alias="MediumBlue",value="rgb(0,0,205)") +record=(alias="DarkBlue",value="rgb(0,0,139)") +record=(alias="Navy",value="rgb(0,0,128)") +record=(alias="MidnightBlue",value="rgb(25,25,112)") +; Purple, violet, and magenta colors +record=(alias="Lavender",value="rgb(230,230,250)") +record=(alias="Thistle",value="rgb(216,191,216)") +record=(alias="Plum",value="rgb(221,160,221)") +record=(alias="Violet",value="rgb(238,130,238)") +record=(alias="Orchid",value="rgb(218,112,214)") +record=(alias="Fuchsia",value="rgb(255,0,255)") +record=(alias="Magenta",value="rgb(255,0,255)") +record=(alias="MediumOrchid",value="rgb(186,85,211)") +record=(alias="MediumPurple",value="rgb(147,112,219)") +record=(alias="BlueViolet",value="rgb(138,43,226)") +record=(alias="DarkViolet",value="rgb(148,0,211)") +record=(alias="DarkOrchid",value="rgb(153,50,204)") +record=(alias="DarkMagenta",value="rgb(139,0,139)") +record=(alias="Purple",value="rgb(128,0,128)") +record=(alias="Indigo",value="rgb(75,0,130)") +record=(alias="DarkSlateBlue",value="rgb(72,61,139)") +record=(alias="SlateBlue",value="rgb(106,90,205)") +record=(alias="MediumSlateBlue",value="rgb(123,104,238)") +; White colors +record=(alias="White",value="rgb(255,255,255)") +record=(alias="Snow",value="rgb(255,250,250)") +record=(alias="Honeydew",value="rgb(240,255,240)") +record=(alias="MintCream",value="rgb(245,255,250)") +record=(alias="Azure",value="rgb(240,255,255)") +record=(alias="AliceBlue",value="rgb(240,248,255)") +record=(alias="GhostWhite",value="rgb(248,248,255)") +record=(alias="WhiteSmoke",value="rgb(245,245,245)") +record=(alias="Seashell",value="rgb(255,245,238)") +record=(alias="Beige",value="rgb(245,245,220)") +record=(alias="OldLace",value="rgb(253,245,230)") +record=(alias="FloralWhite",value="rgb(255,250,240)") +record=(alias="Ivory",value="rgb(255,255,240)") +record=(alias="AntiqueWhite",value="rgb(250,235,215)") +record=(alias="Linen",value="rgb(250,240,230)") +record=(alias="LavenderBlush",value="rgb(255,240,245)") +record=(alias="MistyRose",value="rgb(255,228,225)") +; Gray and black colors +record=(alias="Gainsboro",value="rgb(220,220,220)") +record=(alias="LightGray",value="rgb(211,211,211)") +record=(alias="Silver",value="rgb(192,192,192)") +record=(alias="Gray",value="rgb(169,169,169)") +record=(alias="DimGray",value="rgb(128,128,128)") +record=(alias="DarkGray",value="rgb(105,105,105)") +record=(alias="LightSlateGray",value="rgb(119,136,153)") +record=(alias="SlateGray",value="rgb(112,128,144)") +record=(alias="DarkSlateGray",value="rgb(47,79,79)") +record=(alias="Eigengrau",value="rgb(22,22,29)") +record=(alias="Black",value="rgb(0,0,0)") \ No newline at end of file diff --git a/config/AcediaAliases_Tests.ini b/config/AcediaAliases_Tests.ini new file mode 100644 index 0000000..850b0b4 --- /dev/null +++ b/config/AcediaAliases_Tests.ini @@ -0,0 +1,17 @@ +; For the puposes of testing alias functionality. +; Changing these can break tests. +; +; If you don't plan to run tests or do not know what they are, - +; feel free to remove this file. +[Acedia.MockAliasSource] +record=(alias="global",value="value") +record=(alias="question",value="response") +record=(alias="",value="empty") +record=(alias="also",value="") +[car MockAliases] +Alias="Ford" +Alias="Delorean" +Alias="Audi" +[sci:fi MockAliases] +Alias="Spice" +Alias="HardToBeAGod" \ No newline at end of file diff --git a/config/AcediaAliases.ini b/config/AcediaAliases_Weapons.ini similarity index 72% rename from config/AcediaAliases.ini rename to config/AcediaAliases_Weapons.ini index 4c4074f..067af09 100644 --- a/config/AcediaAliases.ini +++ b/config/AcediaAliases_Weapons.ini @@ -1,32 +1,33 @@ +[Acedia.WeaponAliasSource] ; Field Medic weapons -[weapon/KFMod:MP7MMedicGun Aliases] +[KFMod:MP7MMedicGun WeaponAliases] Alias="MP7M" Alias="MP7" -[weapon/KFMod:MP5MMedicGun Aliases] +[KFMod:MP5MMedicGun WeaponAliases] Alias="MP5M" Alias="MP5" Alias="MP" Alias="M5" -[weapon/KFMod:CamoMP5MMedicGun Aliases] +[KFMod:CamoMP5MMedicGun WeaponAliases] Alias="CamoMP5M" Alias="CamoMP5" Alias="CamoMP" Alias="CamoM5" -[weapon/KFMod:M7A3MMedicGun Aliases] +[KFMod:M7A3MMedicGun WeaponAliases] Alias="M7A3" Alias="M7A" Alias="M7" -[weapon/KFMod:KrissMMedicGun Aliases] +[KFMod:KrissMMedicGun WeaponAliases] Alias="Schneidzekk" Alias="Schneidzek" Alias="Kriss" Alias="Kris" -[weapon/KFMod:NeonKrissMMedicGun Aliases] +[KFMod:NeonKrissMMedicGun WeaponAliases] Alias="NeonSchneidzekk" Alias="NeonSchneidzek" Alias="NeonKriss" Alias="NeonKris" -[weapon/KFMod:BlowerThrower Aliases] +[KFMod:BlowerThrower WeaponAliases] Alias="BlowerThrower" Alias="Blower" Alias="Thrower" @@ -36,15 +37,15 @@ Alias="BileGun" Alias="BloatGun" ; Support Specialist weapons -[weapon/KFMod:Shotgun Aliases] +[KFMod:Shotgun WeaponAliases] Alias="Shotgun" -[weapon/KFMod:CamoShotgun Aliases] +[KFMod:CamoShotgun WeaponAliases] Alias="CamoShotgun" -[weapon/KFMod:BoomStick Aliases] +[KFMod:BoomStick WeaponAliases] Alias="HuntingShotgun" Alias="BoomStick" Alias="Hunting" -[weapon/KFMod:KSGShotgun Aliases] +[KFMod:KSGShotgun WeaponAliases] Alias="HSG-1Shotgun" Alias="HSG1Shotgun" Alias="HSGShotgun" @@ -53,7 +54,7 @@ Alias="KSG-1Shotgun" Alias="KSG1Shotgun" Alias="KSGShotgun" Alias="KSG" -[weapon/KFMod:NeonKSGShotgun Aliases] +[KFMod:NeonKSGShotgun WeaponAliases] Alias="NeonHSG-1Shotgun" Alias="NeonHSG1Shotgun" Alias="NeonHSGShotgun" @@ -62,7 +63,7 @@ Alias="NeonKSG-1Shotgun" Alias="NeonKSG1Shotgun" Alias="NeonKSGShotgun" Alias="NeonKSG" -[weapon/KFMod:NailGun Aliases] +[KFMod:NailGun WeaponAliases] Alias="VladTheImpaler" Alias="VladImpaler" Alias="Vlad" @@ -70,11 +71,11 @@ Alias="Impaler" Alias="NailGun" Alias="Nails" Alias="Nail" -[weapon/KFMod:SPAutoShotgun Aliases] +[KFMod:SPAutoShotgun WeaponAliases] Alias="MultichamberZEDThrower" Alias="ZEDThrower" Alias="ZThrower" -[weapon/KFMod:BenelliShotgun Aliases] +[KFMod:BenelliShotgun WeaponAliases] Alias="CombatShotgun" Alias="Combat" Alias="CShotgun" @@ -82,7 +83,7 @@ Alias="BenelliShotgun" Alias="BeneliShotgun" Alias="Benelli" Alias="Beneli" -[weapon/KFMod:GoldenBenelliShotgun Aliases] +[KFMod:GoldenBenelliShotgun WeaponAliases] Alias="GoldCombatShotgun" Alias="GoldCombat" Alias="GoldCShotgun" @@ -90,35 +91,35 @@ Alias="GoldBenelliShotgun" Alias="GoldBeneliShotgun" Alias="GoldBenelli" Alias="GoldBeneli" -[weapon/KFMod:AA12AutoShotgun Aliases] +[KFMod:AA12AutoShotgun WeaponAliases] Alias="AA12" Alias="AA12AutoShotgun" Alias="AA12Shotgun" -[weapon/KFMod:GoldenAA12AutoShotgun Aliases] +[KFMod:GoldenAA12AutoShotgun WeaponAliases] Alias="GoldAA12" Alias="GoldAA12AutoShotgun" Alias="GoldAA12Shotgun" ; Sharpshooter weapons -[weapon/KFMod:Single Aliases] +[KFMod:Single WeaponAliases] Alias="9mmTactical" Alias="9mmTact" Alias="9mm" Alias="Single" Alias="Pistol" -[weapon/KFMod:Dualies Aliases] +[KFMod:Dualies WeaponAliases] Alias="Dual9mms" Alias="Dual9mm" Alias="9mmDual" Alias="Dualies" Alias="Dual" -[weapon/KFMod:Magnum44Pistol Aliases] +[KFMod:Magnum44Pistol WeaponAliases] Alias="Magnum44Pistol" Alias="Magnum44" Alias="44Magnum" Alias="Magnum" Alias="44" -[weapon/KFMod:Dual44Magnum Aliases] +[KFMod:Dual44Magnum WeaponAliases] Alias="DualMagnum44Pistols" Alias="DualMagnum44s" Alias="DualMagnums" @@ -128,38 +129,38 @@ Alias="Dual44Magnum" Alias="DualMagnum" Alias="Dual44ss" Alias="Dual44" -[weapon/KFMod:MK23Pistol Aliases] +[KFMod:MK23Pistol WeaponAliases] Alias="MK23" Alias="MK" Alias="23" -[weapon/KFMod:DualMK23Pistol Aliases] +[KFMod:DualMK23Pistol WeaponAliases] Alias="DualMK23s" Alias="DualMK23" Alias="DualMKs" Alias="DualMK" Alias="Dual23s" Alias="Dual23" -[weapon/KFMod:Deagle Aliases] +[KFMod:Deagle WeaponAliases] Alias="Handcannon" Alias="Deagle" Alias="HC" -[weapon/KFMod:DualDeagle Aliases] +[KFMod:DualDeagle WeaponAliases] Alias="DualHandcannons" Alias="DualHC" Alias="DualDeagle" -[weapon/KFMod:GoldenDeagle Aliases] +[KFMod:GoldenDeagle WeaponAliases] Alias="GoldHandcannon" Alias="GoldDeagle" Alias="GoldHC" -[weapon/KFMod:GoldenDualDeagle Aliases] +[KFMod:GoldenDualDeagle WeaponAliases] Alias="GoldDualHandcannons" Alias="GoldDualHC" Alias="GoldDualDeagle" -[weapon/KFMod:Winchester Aliases] +[KFMod:Winchester WeaponAliases] Alias="Winchester" Alias="LeverActionRifle" Alias="LAR" -[weapon/KFMod:SPSniperRifle Aliases] +[KFMod:SPSniperRifle WeaponAliases] Alias="SPMusket" Alias="Musket" Alias="SPSniperRifle" @@ -167,18 +168,18 @@ Alias="SPRifle" Alias="SPSniper" Alias="SPMauler" Alias="Mauler" -[weapon/KFMod:M14EBRBattleRifle Aliases] +[KFMod:M14EBRBattleRifle WeaponAliases] Alias="M14EBR" Alias="M14" Alias="EBR" Alias="M14EBRRifle" Alias="M14EBRBattleRifle" -[weapon/KFMod:Crossbow Aliases] +[KFMod:Crossbow WeaponAliases] Alias="CompoundCrossbow" Alias="CCrossbow" Alias="Crossbow" Alias="XBow" -[weapon/KFMod:M99SniperRifle Aliases] +[KFMod:M99SniperRifle WeaponAliases] Alias="M99AMR" Alias="M99" Alias="M99SniperRifle" @@ -187,10 +188,10 @@ Alias="M99Rifle" Alias="M99SR" ; Commando weapons -[weapon/KFMod:Bullpup Aliases] +[KFMod:Bullpup WeaponAliases] Alias="Bullpup" Alias="Bulpup" -[weapon/KFMod:ThompsonSMG Aliases] +[KFMod:ThompsonSMG WeaponAliases] Alias="ThompsonSMG" Alias="Thompson" Alias="Thomp" @@ -198,7 +199,7 @@ Alias="TommyGun" Alias="TomyGun" Alias="Tommy" Alias="Tomy" -[weapon/KFMod:SPThompsonSMG Aliases] +[KFMod:SPThompsonSMG WeaponAliases] Alias="SPThompsonSMG" Alias="SPThompson" Alias="SPThomp" @@ -219,7 +220,7 @@ Alias="LeadSystem" Alias="DeliverySystem" Alias="LeadDS" Alias="LeadD" -[weapon/KFMod:ThompsonDrumSMG Aliases] +[KFMod:ThompsonDrumSMG WeaponAliases] Alias="ThompsonDrumSMG" Alias="ThompsonDrum" Alias="ThompDrum" @@ -231,7 +232,7 @@ Alias="RSTommyGun" Alias="RSTommy" Alias="RSTomyGun" Alias="RSTomy" -[weapon/KFMod:AK47AssaultRifle Aliases] +[KFMod:AK47AssaultRifle WeaponAliases] Alias="AK47AssaultRifle" Alias="AK47Assault" Alias="AK47Rifle" @@ -239,7 +240,7 @@ Alias="AK47AR" Alias="AK47" Alias="AK" Alias="47" -[weapon/KFMod:GoldenAK47AssaultRifle Aliases] +[KFMod:GoldenAK47AssaultRifle WeaponAliases] Alias="GoldAK47AssaultRifle" Alias="GoldAK47Assault" Alias="GoldAK47Rifle" @@ -247,7 +248,7 @@ Alias="GoldAK47AR" Alias="GoldAK47" Alias="GoldAK" Alias="Gold47" -[weapon/KFMod:NeonAK47AssaultRifle Aliases] +[KFMod:NeonAK47AssaultRifle WeaponAliases] Alias="NeonAK47AssaultRifle" Alias="NeonAK47Assault" Alias="NeonAK47Rifle" @@ -255,29 +256,24 @@ Alias="NeonAK47AR" Alias="NeonAK47" Alias="NeonAK" Alias="Neon47" -[weapon/KFMod:M4AssaultRifle Aliases] +[KFMod:M4AssaultRifle WeaponAliases] Alias="M4AssaultRifle" Alias="M4Assault" Alias="M4Rifle" Alias="M4" -[weapon/KFMod:M4AssaultRifle Aliases] +[KFMod:CamoM4AssaultRifle WeaponAliases] Alias="CamoM4AssaultRifle" Alias="CamoM4Assault" Alias="CamoM4Rifle" Alias="CamoM4" -[weapon/KFMod:M4AssaultRifle Aliases] -Alias="CamoM4AssaultRifle" -Alias="CamoM4Assault" -Alias="CamoM4Rifle" -Alias="CamoM4" -[weapon/KFMod:MKb42AssaultRifle Aliases] +[KFMod:MKb42AssaultRifle WeaponAliases] Alias="MKb42AssaultRifle" Alias="MKb42Assault" Alias="MKb42Rifle" Alias="MKb42" Alias="MK42" Alias="MKb" -[weapon/KFMod:SCARMK17AssaultRifle Aliases] +[KFMod:SCARMK17AssaultRifle WeaponAliases] Alias="SCARMK17AssaultRifle" Alias="SCARMK17Assault" Alias="SCARMK17Rifle" @@ -293,7 +289,7 @@ Alias="SCARRifle" Alias="SCAR17" Alias="SCARMK" Alias="SCAR" -[weapon/KFMod:NeonSCARMK17AssaultRifle Aliases] +[KFMod:NeonSCARMK17AssaultRifle WeaponAliases] Alias="NeonSCARMK17AssaultRifle" Alias="NeonSCARMK17Assault" Alias="NeonSCARMK17Rifle" @@ -309,7 +305,7 @@ Alias="NeonSCARRifle" Alias="NeonSCAR17" Alias="NeonSCARMK" Alias="NeonSCAR" -[weapon/KFMod:FNFAL_ACOG_AssaultRifle Aliases]FNFAL ACOG +[KFMod:FNFAL_ACOG_AssaultRifle WeaponAliases]FNFAL ACOG Alias="FNFALACOGAssaultRifle" Alias="FNFALACOGAssault" Alias="FNFALACOGRifle" @@ -329,34 +325,34 @@ Alias="FAL" Alias="FN" ; Berserker weapons -[weapon/KFMod:Knife Aliases] +[KFMod:Knife WeaponAliases] Alias="Knife" -[weapon/KFMod:Machete Aliases] +[KFMod:Machete WeaponAliases] Alias="Machete" Alias="Chete" -[weapon/KFMod:Axe Aliases] +[KFMod:Axe WeaponAliases] Alias="Axe" Alias="FireAxe" -[weapon/KFMod:Katana Aliases] +[KFMod:Katana WeaponAliases] Alias="Katana" -[weapon/KFMod:GoldenKatana Aliases] +[KFMod:GoldenKatana WeaponAliases] Alias="GoldKatana" -[weapon/KFMod:Scythe Aliases] +[KFMod:Scythe WeaponAliases] Alias="Scythe" Alias="Scyte" Alias="Sickle" Alias="Sickl" -[weapon/KFMod:Chainsaw Aliases] +[KFMod:Chainsaw WeaponAliases] Alias="Chainsaw" Alias="Saw" Alias="Denji" Alias="Pochita" -[weapon/KFMod:GoldenChainsaw Aliases] +[KFMod:GoldenChainsaw WeaponAliases] Alias="GoldChainsaw" Alias="GoldSaw" Alias="GoldDenji" Alias="GoldPochita" -[weapon/KFMod:DwarfAxe Aliases] +[KFMod:DwarfAxe WeaponAliases] Alias="DwarfsAxe" Alias="DwarfAxe" Alias="ShitAxe" @@ -365,7 +361,7 @@ Alias="PushAxe" Alias="GnomeAxe" Alias="TrollAxe" Alias="NoobAxe" -[weapon/KFMod:ClaymoreSword Aliases] +[KFMod:ClaymoreSword WeaponAliases] Alias="ClaymoreSword" Alias="ClaymoreBlade" Alias="Claymore" @@ -376,7 +372,7 @@ Alias="Claimore" Alias="Claimor" Alias="Sword" Alias="Blade" -[weapon/KFMod:Crossbuzzsaw Aliases] +[KFMod:Crossbuzzsaw WeaponAliases] Alias="Crossbuzzsaw" Alias="Buzzsaw" Alias="Buzz" @@ -385,17 +381,17 @@ Alias="BuzzBow" Alias="ZerkBow" ; Firebug weapons -[weapon/KFMod:MAC10MP Aliases] +[KFMod:MAC10MP WeaponAliases] Alias="MAC10MP" Alias="MAC10" Alias="MAC" -[weapon/KFMod:FlareRevolver Aliases] +[KFMod:FlareRevolver WeaponAliases] Alias="FlareRevolver" Alias="FireRevolver" Alias="FlareGun" Alias="Flares" Alias="Flare" -[weapon/KFMod:DualFlareRevolver Aliases] +[KFMod:DualFlareRevolver WeaponAliases] Alias="DualFlareRevolvers" Alias="DualFlareRevolver" Alias="DualFireRevolvers" @@ -404,19 +400,19 @@ Alias="DualFlareGuns" Alias="DualFlareGun" Alias="DualFlares" Alias="DualFlare" -[weapon/KFMod:FlameThrower Aliases] +[KFMod:FlameThrower WeaponAliases] Alias="FlameThrower" Alias="FireThrower" Alias="FThrower" Alias="Flamer" Alias="FireSpam" -[weapon/KFMod:GoldenFlamethrower Aliases] +[KFMod:GoldenFlamethrower WeaponAliases] Alias="GoldFlameThrower" Alias="GoldFireThrower" Alias="GoldFThrower" Alias="GoldFlamer" Alias="GoldFireSpam" -[weapon/KFMod:Trenchgun Aliases] +[KFMod:Trenchgun WeaponAliases] Alias="DragonsBreathTrenchgun" Alias="DragonsBreathGun" Alias="DragonsBreath" @@ -426,7 +422,7 @@ Alias="DragBreath" Alias="Trenchgun" Alias="FireShotgun" Alias="Flameshotgun" -[weapon/KFMod:HuskGun Aliases] +[KFMod:HuskGun WeaponAliases] Alias="HuskFireballLauncher" Alias="HuskFireball" Alias="FireballLauncher" @@ -434,28 +430,25 @@ Alias="HuskLauncher" Alias="HuskFirebalLauncher" Alias="HuskFirebal" Alias="FirebalLauncher" -Alias="HuskLauncher" Alias="HuskGun" Alias="Husk" ; Demolition weapons -[weapon/KFMod:M79GrenadeLauncher Aliases] +[KFMod:M79GrenadeLauncher WeaponAliases] Alias="M79GrenadeLauncher" Alias="M79Grenade" Alias="M79Launcher" Alias="M79NadeLauncher" Alias="M79Nade" -Alias="M79GrenadeLauncher" Alias="M79" -[weapon/KFMod:GoldenM79GrenadeLauncher Aliases] +[KFMod:GoldenM79GrenadeLauncher WeaponAliases] Alias="GoldM79GrenadeLauncher" Alias="GoldM79Grenade" Alias="GoldM79Launcher" Alias="GoldM79NadeLauncher" Alias="GoldM79Nade" -Alias="GoldM79GrenadeLauncher" Alias="GoldM79" -[weapon/KFMod:SPGrenadeLauncher Aliases] +[KFMod:SPGrenadeLauncher WeaponAliases] Alias="SPGrenadeLauncher" Alias="SPNadeLauncher" Alias="SPLauncher" @@ -470,13 +463,13 @@ Alias="OrcaBombPropeler" Alias="OrcaBomb" Alias="Orca" Alias="OrcaLauncher" -[weapon/KFMod:PipeBombExplosive Aliases] +[KFMod:PipeBombExplosive WeaponAliases] Alias="PipeBombExplosive" Alias="PipeExplosive" Alias="PipeBomb" Alias="Pipes" Alias="Pipe" -[weapon/KFMod:SealSquealHarpoonBomber Aliases] +[KFMod:SealSquealHarpoonBomber WeaponAliases] Alias="SealSquealHarpoonBomber" Alias="SealSquealHarpoon" Alias="SealSquealBomber" @@ -487,7 +480,7 @@ Alias="SealSqueal" Alias="HarpoonBomber" Alias="Harpoon" Alias="Harp" -[weapon/KFMod:SeekerSixRocketLauncher Aliases] +[KFMod:SeekerSixRocketLauncher WeaponAliases] Alias="SeekerSixRocketLauncher" Alias="SeekerSixLauncher" Alias="Seeker6RocketLauncher" @@ -500,40 +493,38 @@ Alias="Seeker" Alias="SuckerSix" Alias="Sucker6" Alias="Sucker" -[weapon/KFMod:M4203AssaultRifle Aliases] +[KFMod:M4203AssaultRifle WeaponAliases] Alias="M4203Assault" Alias="M4203Rifle" Alias="M4203" Alias="M4200" Alias="M420" Alias="M42" -[weapon/KFMod:LAW Aliases] +[KFMod:LAW WeaponAliases] Alias="LAW" -[weapon/KFMod:M32GrenadeLauncher Aliases] +[KFMod:M32GrenadeLauncher WeaponAliases] Alias="M32GrenadeLauncher" Alias="M32Grenade" Alias="M32Launcher" Alias="M32NadeLauncher" Alias="M32Nade" -Alias="M32GrenadeLauncher" Alias="M32" -[weapon/KFMod:CamoM32GrenadeLauncher Aliases] +[KFMod:CamoM32GrenadeLauncher WeaponAliases] Alias="CamoM32GrenadeLauncher" Alias="CamoM32Grenade" Alias="CamoM32Launcher" Alias="CamoM32NadeLauncher" Alias="CamoM32Nade" -Alias="CamoM32GrenadeLauncher" Alias="CamoM32" ; Off-perk weapons -[weapon/KFMod:ZEDGun Aliases] +[KFMod:ZEDGun WeaponAliases] Alias="ZedEradicationDevice" Alias="ZedEradication" Alias="ZedDevice" Alias="ZEDGun" Alias="ZED" -[weapon/KFMod:ZEDMKIIWeapon Aliases] +[KFMod:ZEDMKIIWeapon WeaponAliases] Alias="ZedEradicationDeviceMKII" Alias="ZedEradicationMKII" Alias="ZedDeviceMKII" @@ -553,17 +544,4 @@ Alias="ZedEradicationDevice2" Alias="ZedEradication2" Alias="ZedDevice2" Alias="ZEDGun2" -Alias="ZED2" - -; For the puposes of unit-testing. -; Changing these can break unit tests. -; -; If you don't plan to run unit tests or do not know what they are, - -; feel free to remove records below. -[test/car Aliases] -Alias="Ford" -Alias="Delorean" -Alias="Audi" -[test/scifi Aliases] -Alias="Spice" -Alias="HardToBeAGod" \ No newline at end of file +Alias="ZED2" \ No newline at end of file diff --git a/config/AcediaSystem.ini b/config/AcediaSystem.ini new file mode 100644 index 0000000..ee08db7 --- /dev/null +++ b/config/AcediaSystem.ini @@ -0,0 +1,180 @@ +; Every single option in this config should be considered [ADVANCED] +[Acedia.AliasService] +; Changing these allows you to change in what sources `AliasesAPI` +; looks for weapon and color aliases. +weaponAliasesSource=Class'WeaponAliasSource' +colorAliasesSource=Class'ColorAliasSource' +; How often are different alias-storing objects are allowed to record +; their updated data into a config. +; Negative or zero values would be reset to `0.05`. +saveInterval=0.05 + +[Acedia.AliasHash] +; Reasonable lower and upper limits on hash table capacity for +; aliases' storage, that will be enforced if user requires something outside +; those bounds. +MINIMUM_CAPACITY=10 +MAXIMUM_CAPACITY=100000 + +[Acedia.TestingService] +; Allows you to run tests on server's start up. This option is to help run +; tests quicker during development and should not be used for servers that are +; setup for actually playing the game. +runTestsOnStartUp=false +; Use these flags to only run tests from particular test cases +filterTestsByName=false +filterTestsByGroup=false +requiredName="" +requiredGroup="" + +[Acedia.ConsoleAPI] +; These should guarantee decent text output in console even at +; 640x480 shit resolution +; (and it look fine at normal resolutions as well) +maxVisibleLineWidth=80 +maxTotalLineWidth=108 + +[Acedia.ColorAPI] +; Changing these values will alter color's definitions in `ColorAPI`, +; changing how Acedia behaves +Pink=(R=255,G=192,B=203,A=255) +LightPink=(R=255,G=182,B=193,A=255) +HotPink=(R=255,G=105,B=180,A=255) +DeepPink=(R=255,G=20,B=147,A=255) +PaleVioletRed=(R=219,G=112,B=147,A=255) +MediumVioletRed=(R=199,G=21,B=133,A=255) +LightSalmon=(R=255,G=160,B=122,A=255) +Salmon=(R=250,G=128,B=114,A=255) +DarkSalmon=(R=233,G=150,B=122,A=255) +LightCoral=(R=240,G=128,B=128,A=255) +IndianRed=(R=205,G=92,B=92,A=255) +Crimson=(R=220,G=20,B=60,A=255) +Firebrick=(R=178,G=34,B=34,A=255) +DarkRed=(R=139,G=0,B=0,A=255) +Red=(R=255,G=0,B=0,A=255) +OrangeRed=(R=255,G=69,B=0,A=255) +Tomato=(R=255,G=99,B=71,A=255) +Coral=(R=255,G=127,B=80,A=255) +DarkOrange=(R=255,G=140,B=0,A=255) +Orange=(R=255,G=165,B=0,A=255) +Yellow=(R=255,G=255,B=0,A=255) +LightYellow=(R=255,G=255,B=224,A=255) +LemonChiffon=(R=255,G=250,B=205,A=255) +LightGoldenrodYellow=(R=250,G=250,B=210,A=255) +PapayaWhip=(R=255,G=239,B=213,A=255) +Moccasin=(R=255,G=228,B=181,A=255) +PeachPuff=(R=255,G=218,B=185,A=255) +PaleGoldenrod=(R=238,G=232,B=170,A=255) +Khaki=(R=240,G=230,B=140,A=255) +DarkKhaki=(R=189,G=183,B=107,A=255) +Gold=(R=255,G=215,B=0,A=255) +Cornsilk=(R=255,G=248,B=220,A=255) +BlanchedAlmond=(R=255,G=235,B=205,A=255) +Bisque=(R=255,G=228,B=196,A=255) +NavajoWhite=(R=255,G=222,B=173,A=255) +Wheat=(R=245,G=222,B=179,A=255) +Burlywood=(R=222,G=184,B=135,A=255) +TanColor=(R=210,G=180,B=140,A=255) +RosyBrown=(R=188,G=143,B=143,A=255) +SandyBrown=(R=244,G=164,B=96,A=255) +Goldenrod=(R=218,G=165,B=32,A=255) +DarkGoldenrod=(R=184,G=134,B=11,A=255) +Peru=(R=205,G=133,B=63,A=255) +Chocolate=(R=210,G=105,B=30,A=255) +SaddleBrown=(R=139,G=69,B=19,A=255) +Sienna=(R=160,G=82,B=45,A=255) +Brown=(R=165,G=42,B=42,A=255) +Maroon=(R=128,G=0,B=0,A=255) +DarkOliveGreen=(R=85,G=107,B=47,A=255) +Olive=(R=128,G=128,B=0,A=255) +OliveDrab=(R=107,G=142,B=35,A=255) +YellowGreen=(R=154,G=205,B=50,A=255) +LimeGreen=(R=50,G=205,B=50,A=255) +Lime=(R=0,G=255,B=0,A=255) +LawnGreen=(R=124,G=252,B=0,A=255) +Chartreuse=(R=127,G=255,B=0,A=255) +GreenYellow=(R=173,G=255,B=47,A=255) +SpringGreen=(R=0,G=255,B=127,A=255) +MediumSpringGreen=(R=0,G=250,B=154,A=255) +LightGreen=(R=144,G=238,B=144,A=255) +PaleGreen=(R=152,G=251,B=152,A=255) +DarkSeaGreen=(R=143,G=188,B=143,A=255) +MediumAquamarine=(R=102,G=205,B=170,A=255) +MediumSeaGreen=(R=60,G=179,B=113,A=255) +SeaGreen=(R=46,G=139,B=87,A=255) +ForestGreen=(R=34,G=139,B=34,A=255) +Green=(R=0,G=128,B=0,A=255) +DarkGreen=(R=0,G=100,B=0,A=255) +Aqua=(R=0,G=255,B=255,A=255) +Cyan=(R=0,G=255,B=255,A=255) +LightCyan=(R=224,G=255,B=255,A=255) +PaleTurquoise=(R=175,G=238,B=238,A=255) +Aquamarine=(R=127,G=255,B=212,A=255) +Turquoise=(R=64,G=224,B=208,A=255) +MediumTurquoise=(R=72,G=209,B=204,A=255) +DarkTurquoise=(R=0,G=206,B=209,A=255) +LightSeaGreen=(R=32,G=178,B=170,A=255) +CadetBlue=(R=95,G=158,B=160,A=255) +DarkCyan=(R=0,G=139,B=139,A=255) +Teal=(R=0,G=128,B=128,A=255) +LightSteelBlue=(R=176,G=196,B=222,A=255) +PowderBlue=(R=176,G=224,B=230,A=255) +LightBlue=(R=173,G=216,B=230,A=255) +SkyBlue=(R=135,G=206,B=235,A=255) +LightSkyBlue=(R=135,G=206,B=250,A=255) +DeepSkyBlue=(R=0,G=191,B=255,A=255) +DodgerBlue=(R=30,G=144,B=255,A=255) +CornflowerBlue=(R=100,G=149,B=237,A=255) +SteelBlue=(R=70,G=130,B=180,A=255) +RoyalBlue=(R=65,G=105,B=225,A=255) +Blue=(R=0,G=0,B=255,A=255) +MediumBlue=(R=0,G=0,B=205,A=255) +DarkBlue=(R=0,G=0,B=139,A=255) +Navy=(R=0,G=0,B=128,A=255) +MidnightBlue=(R=25,G=25,B=112,A=255) +Lavender=(R=230,G=230,B=250,A=255) +Thistle=(R=216,G=191,B=216,A=255) +Plum=(R=221,G=160,B=221,A=255) +Violet=(R=238,G=130,B=238,A=255) +Orchid=(R=218,G=112,B=214,A=255) +Fuchsia=(R=255,G=0,B=255,A=255) +Magenta=(R=255,G=0,B=255,A=255) +MediumOrchid=(R=186,G=85,B=211,A=255) +MediumPurple=(R=147,G=112,B=219,A=255) +BlueViolet=(R=138,G=43,B=226,A=255) +DarkViolet=(R=148,G=0,B=211,A=255) +DarkOrchid=(R=153,G=50,B=204,A=255) +DarkMagenta=(R=139,G=0,B=139,A=255) +Purple=(R=128,G=0,B=128,A=255) +Indigo=(R=75,G=0,B=130,A=255) +DarkSlateBlue=(R=72,G=61,B=139,A=255) +SlateBlue=(R=106,G=90,B=205,A=255) +MediumSlateBlue=(R=123,G=104,B=238,A=255) +White=(R=255,G=255,B=255,A=255) +Snow=(R=255,G=250,B=250,A=255) +Honeydew=(R=240,G=255,B=240,A=255) +MintCream=(R=245,G=255,B=250,A=255) +Azure=(R=240,G=255,B=255,A=255) +AliceBlue=(R=240,G=248,B=255,A=255) +GhostWhite=(R=248,G=248,B=255,A=255) +WhiteSmoke=(R=245,G=245,B=245,A=255) +Seashell=(R=255,G=245,B=238,A=255) +Beige=(R=245,G=245,B=220,A=255) +OldLace=(R=253,G=245,B=230,A=255) +FloralWhite=(R=255,G=250,B=240,A=255) +Ivory=(R=255,G=255,B=240,A=255) +AntiqueWhite=(R=250,G=235,B=215,A=255) +Linen=(R=250,G=240,B=230,A=255) +LavenderBlush=(R=255,G=240,B=245,A=255) +MistyRose=(R=255,G=228,B=225,A=255) +Gainsboro=(R=220,G=220,B=220,A=255) +LightGray=(R=211,G=211,B=211,A=255) +Silver=(R=192,G=192,B=192,A=255) +DarkGray=(R=169,G=169,B=169,A=255) +Gray=(R=128,G=128,B=128,A=255) +DimGray=(R=105,G=105,B=105,A=255) +LightSlateGray=(R=119,G=136,B=153,A=255) +SlateGray=(R=112,G=128,B=144,A=255) +DarkSlateGray=(R=47,G=79,B=79,A=255) +Eigengrau=(R=22,G=22,B=29,A=255) +Black=(R=0,G=0,B=0,A=255) \ No newline at end of file From 04bcbacc3e90ae78d2ae9ec6949bf7c9f576a000 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:39:39 +0700 Subject: [PATCH 33/59] Add aliases docs --- docs/Aliases.md | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 docs/Aliases.md diff --git a/docs/Aliases.md b/docs/Aliases.md new file mode 100644 index 0000000..8bbdc63 --- /dev/null +++ b/docs/Aliases.md @@ -0,0 +1,138 @@ +# Aliases + +Aliases are `string` values that act as human-readable synonyms to some other `string` values. + +Often, when using some console commands, users are forced to type into exact class names of objects in **UnrealScript** (e.g., commands to give someone an M14EBR take form similar to `mutate give KFmod.M14EBRBattleRifle`), but such names can be cumbersome to remember and type. + +Aliases solve this problem by allowing players to instead type `mutate give $ebr`, where `$` denotes that following word `ebr` is an alias that will be automatically resolved into `KFmod.M14EBRBattleRifle`. + +## Alias names + +Alias can be any `string` consisting of ASCII character, although for practical reasons it is better to use only letters, digits and `_` character. Otherwise using them might become more difficult, partially defeating their purpose. + +Aliases are case-insensitive, so `EBR`, `Ebr` and `ebr` are all considered the same alias. + +## Alias sources + +Sources essentially act as aliases databases: matching each alias to some value. They can be used to separate aliases that describe different categories of objects: weapons, zeds, colors, etc.. + +Inside each source aliases and their values are expected to be in many-to-one relationship: many aliases can mean the same value, but each alias can only mean one value. However, two different sources can each contain the same alias and make it point to different values. So it's important for the game to know what source contains what type of aliases. + +In case there are several aliases with the same name in the database, - **Acedia** will warn you about it, but won't actually remove duplicates, instead letting the source use the first it finds. + +By default **Acedia** offers 4 different alias sources: + +* `WeaponAliasSource` (*AcediaAliases_Weapons.ini*) - source filled with aliases for weapons (by default contains aliases to every vanilla weapon); +* `ColorAliasSource` (*AcediaAliases_Colors.ini*) - source filled with aliases for colors (by default contains a decent amount of pre-defined colors); +* `AliasSource` (*AcediaAliases.ini*) - unused source that can, nevertheless, be utilized by server admins or other packages (by default empty); +* `MockAliasSource` (*AcediaAliases_Tests.ini*) - source that is used for testing whether aliases functionality works correctly, avoid changing it if you intend to run tests for **Acedia**'s functionality. + +### [Advanced] Changing meaning of alias sources + +Even though some of the above sources have rather specific names, only use of `MockAliasSource` is hardcoded: admins can, in theory, move all aliases into any source they like. They'll just have to tell **Acedia** where to look for them by changing *AcediaSystem.ini*'s section *Acedia.AliasService* to point at appropriate source: + +```ini +weaponAliasesSource=Class'Acedia.WeaponAliasSource' +colorAliasesSource=Class'Acedia.ColorAliasSource' +``` + +Specifically, you can move all aliases to a single source (for example `AliasSource`) and tell **Acedia** to look for weapon and color aliases there: + +```ini +weaponAliasesSource=Class'Acedia.AliasSource' +colorAliasesSource=Class'Acedia.AliasSource' +``` + +## How sources are stored + +Alias sources are stored in appropriate *ini*-files in two ways that can be mixed with each other however you like. + +### 1. Flat array `record` + +First way is to define a set alias-value pairs in section of the alias source. Example from the color alias source: + +```ini +[Acedia.ColorAliasSource] +; Pink colors +record=(alias="Pink",value="rgb(255,192,203)") +record=(alias="LightPink",value="rgb(255,182,193)") +record=(alias="HotPink",value="rgb(255,105,180)") +record=(alias="DeepPink",value="rgb(255,20,147)") +record=(alias="PaleVioletRed",value="rgb(219,112,147)") +record=(alias="MediumVioletRed",value="rgb(199,21,133)") +``` + +If you want several different aliases to point to the same value, just add a record for each of them: + +```ini +record=(alias="Pink",value="rgb(255,192,203)") +record=(alias="Punk",value="rgb(255,192,203)") +record=(alias="Bunk",value="rgb(255,192,203)") +``` + +Just avoid having several records for the same alias in one source. + +### 2. Per-object-config + +If you need to define several aliases for one value it might be better to use per-object-configuration with named objects: each of them stores an array of aliases, while the corresponding value is recorded as object's name. Example from weapons alias source: + +```ini +[KFMod:MP5MMedicGun WeaponAliases] +Alias="MP5M" +Alias="MP5" +Alias="MP" +Alias="M5" +``` + +Here aliases are defined in every line that starts with `Alias=`. Their value `KFMod:MP5MMedicGun` is defined as a first part of the config section (`:` is going to be translated to `.`, more on that below) and the second part `WeaponAliases` indicates that this is a record for `WeaponAliasSource`. + +Each source has it's own identification for per-object-config records: + +* For `WeaponAliasSource` it is `WeaponAliases`; +* For `ColorAliasSource` it is `ColorAliases`; +* For `MockAliasSource` it is `MockAliases`; +* For `AliasSource` it is just `Aliases`. + +#### Limitations of the second way + +Because alias' value must be a part of the *ini*-file section there are certain limitations imposed on what that value can be (for example having `.` or `]` inside value's name will confuse **Unreal Engine**'s config parser, so you can't use them). There is not official, complete list of forbidden characters, but it is suggested you keep them limited to sequence of letters, numbers and `_` character. + +If you do need to store some weird string as a value, - first test that it does load correctly and, if not, use the first way to define it's aliases. + +But `.` being a forbidden symbol is too harsh of a limitation, since we mainly want to store class names via per-object-configs. Because of that any alias values defined the second way will load `:` as `.` from a config. This change allows us to define classes as values at the cost of preventing the use of `:`. + +## [Technical] Defining new alias sources + +If you make a module using **Acedia** and want to add another alias source you simply need to decide on the names of your: + +* Alias source (suppose it's `NewSource`); +* Helper class for second way (*per-object-config*) of defining aliases (suppose it's `NewAliases`) +* Config file, where their data will be stored (suppose it's `MyNewAliases.ini`); + +then create two classes, like that: + +```java +class NewSource extends AliasSource + config(MyNewAliases); + +defaultproperties +{ + configName = "MyNewAliases" + aliasesClass = class'NewAliases' +} +``` + +```java +class NewAliases extends Aliases + perObjectConfig + config(MyNewAliases); + +defaultproperties +{ + sourceClass = class'NewSource' +} +``` + +and put them in your manifest. + +For more examples check out source code for `ColorAliasSource`, `WeaponAliasSource`, `MockAliasSource`. From e5137491195130d9efc9ce5bf4e9585d36557329 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:40:07 +0700 Subject: [PATCH 34/59] Add stub for color docs --- docs/Colors.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/Colors.md diff --git a/docs/Colors.md b/docs/Colors.md new file mode 100644 index 0000000..94b1481 --- /dev/null +++ b/docs/Colors.md @@ -0,0 +1,16 @@ +# Colors + +The main, and possibly only, notable thing abotu **Acedia**'s colors is it's support for parsing their text representation. To be precise, **Acedia** understands: + +1. Hex color definitions in format of `#ffc0cb`; +2. RGB color definitions that look like either `rgb(255,192,203)` or `rgb(r=255,g=192,b=203)`; +3. RGBA color definitions that look like either `rgb(255,192,203,13)` or `rgb(r=255,g=192,b=203,a=13)`; +4. Alias color definitions that **Acedia** looks up from color-specific alias source and look like any other alias reference: `$pink`. + +You should be able to use any form you like while working with **Acedia**. + +## [Technical] Color fixing + +Killing floor's standard methods of rendering colored `string`s make use of inserting 4-byte sequence into them: first bytes denotes the start of the sequence, 3 following bytes denote rgb color components. Unfortunately these methods also have issues with rendering `string`s if you specify certain values (`0` and `10`) as red-green-blue color components. + +You can freely use colors with these components, since **Acedia** automatically should fix them for you (by replacing them with indistinguishably close, but valid color) whenever it matters. From c42edfb88ff9bd79cf9b5f2dfb979e86a509784b Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 19:21:03 +0700 Subject: [PATCH 35/59] Move most files to other Acedia packages --- config/Acedia.ini | 258 +- config/AcediaAliases_Colors.ini | 165 - config/AcediaAliases_Tests.ini | 17 - config/AcediaAliases_Weapons.ini | 547 --- config/AcediaSystem.ini | 180 - docs/Aliases.md | 138 - docs/Colors.md | 16 - sources/Core/AcediaActor.uc | 45 - sources/Core/AcediaObject.uc | 40 - sources/Core/AcediaReplicationInfo.uc | 32 - sources/Core/Aliases/AliasHash.uc | 218 - sources/Core/Aliases/AliasService.uc | 135 - sources/Core/Aliases/AliasSource.uc | 379 -- sources/Core/Aliases/Aliases.uc | 142 - sources/Core/Aliases/AliasesAPI.uc | 233 - .../BuiltInSources/ColorAliasSource.uc | 27 - .../Aliases/BuiltInSources/ColorAliases.uc | 27 - .../BuiltInSources/WeaponAliasSource.uc | 27 - .../Aliases/BuiltInSources/WeaponAliases.uc | 27 - sources/Core/Aliases/Tests/MockAliasSource.uc | 27 - sources/Core/Aliases/Tests/MockAliases.uc | 27 - sources/Core/Aliases/Tests/TEST_Aliases.uc | 133 - sources/Core/Color/ColorAPI.uc | 812 ---- sources/Core/Color/Tests/TEST_ColorAPI.uc | 507 -- sources/Core/Console/ConsoleAPI.uc | 280 -- sources/Core/Console/ConsoleBuffer.uc | 393 -- sources/Core/Console/ConsoleWriter.uc | 373 -- sources/Core/Data/JSON/JArray.uc | 351 -- sources/Core/Data/JSON/JObject.uc | 265 - sources/Core/Data/JSON/JSON.uc | 84 - sources/Core/Data/JSON/JSONAPI.uc | 38 - sources/Core/Data/JSON/Tests/TEST_JSON.uc | 711 --- .../Core/Events/Broadcast/BroadcastEvents.uc | 142 - .../Core/Events/Broadcast/BroadcastHandler.uc | 197 - .../Events/Broadcast/BroadcastListenerBase.uc | 120 - sources/Core/Events/Events.uc | 159 - sources/Core/Events/Listener.uc | 59 - sources/Core/Events/Mutator/MutatorEvents.uc | 56 - .../Events/Mutator/MutatorListenerBase.uc | 47 - sources/Core/Feature.uc | 117 - sources/Core/Logger/LoggerAPI.uc | 92 - sources/Core/Logger/LoggerService.uc | 166 - sources/Core/Memory/MemoryAPI.uc | 290 -- sources/Core/Service.uc | 81 - sources/Core/Singleton.uc | 104 - sources/Core/Testing/IssueSummary.uc | 294 -- sources/Core/Testing/Service/TestingEvents.uc | 66 - .../Core/Testing/Service/TestingService.uc | 253 - sources/Core/Testing/TestCase.uc | 226 - sources/Core/Testing/TestCaseSummary.uc | 540 --- sources/Core/Text/Parser.uc | 1312 ----- sources/Core/Text/Tests/TEST_Parser.uc | Bin 118744 -> 0 bytes sources/Core/Text/Tests/TEST_Text.uc | Bin 21740 -> 0 bytes sources/Core/Text/Tests/TEST_TextAPI.uc | Bin 67104 -> 0 bytes sources/Core/Text/Text.uc | 290 -- sources/Core/Text/TextAPI.uc | 1281 ----- sources/Core/Text/UnicodeData.uc | 4320 ----------------- .../FixAmmoSelling/AmmoPickupStalker.uc | 85 - .../Features/FixAmmoSelling/FixAmmoSelling.uc | 395 -- .../FixAmmoSellingClass_CamoM32Pickup.uc | 26 - .../FixAmmoSellingClass_CrossbowPickup.uc | 26 - .../FixAmmoSellingClass_GoldenM79Pickup.uc | 26 - .../FixAmmoSellingClass_LAWPickup.uc | 26 - .../FixAmmoSellingClass_M32Pickup.uc | 26 - .../FixAmmoSellingClass_M79Pickup.uc | 26 - .../FixAmmoSellingClass_PipeBombPickup.uc | 26 - .../FixAmmoSellingClass_SPGrenadePickup.uc | 27 - .../FixAmmoSellingClass_SealSquealPickup.uc | 26 - .../FixAmmoSellingClass_SeekerSixPickup.uc | 26 - .../MutatorListener_FixAmmoSelling.uc | 97 - sources/Features/FixDoshSpam/FixDoshSpam.uc | 252 - .../MutatorListener_FixDoshSpam.uc | 51 - .../FixDualiesCost/DualiesCostRule.uc | 45 - .../Features/FixDualiesCost/FixDualiesCost.uc | 454 -- .../MutatorListener_FixDualiesCost.uc | 43 - sources/Features/FixFFHack/FFHackRule.uc | 74 - sources/Features/FixFFHack/FixFFHack.uc | 152 - .../FixInfiniteNades/FixInfiniteNades.uc | 233 - .../FixInfiniteNades/FixedFragFire.uc | 36 - .../MutatorListener_FixInfiniteNades.uc | 44 - .../FixInventoryAbuse/FixInventoryAbuse.uc | 225 - .../BroadcastListener_FixSpectatorCrash.uc | 51 - .../FixSpectatorCrash/FixSpectatorCrash.uc | 291 -- .../Features/FixZedTimeLags/FixZedTimeLags.uc | 188 - sources/Global.uc | 51 - sources/Manifest.uc | 55 - sources/{Core/Acedia.uc => Packages.uc} | 80 +- .../Services/Connection/ConnectionEvents.uc | 52 - .../Connection/ConnectionListenerBase.uc | 34 - .../Services/Connection/ConnectionService.uc | 143 - .../Connection/MutatorListener_Connection.uc | 53 - sources/{Core => }/StartUp.uc | 2 +- ...e.uc => TestingListener_AcediaLauncher.uc} | 27 +- 93 files changed, 85 insertions(+), 20255 deletions(-) delete mode 100644 config/AcediaAliases_Colors.ini delete mode 100644 config/AcediaAliases_Tests.ini delete mode 100644 config/AcediaAliases_Weapons.ini delete mode 100644 config/AcediaSystem.ini delete mode 100644 docs/Aliases.md delete mode 100644 docs/Colors.md delete mode 100644 sources/Core/AcediaActor.uc delete mode 100644 sources/Core/AcediaObject.uc delete mode 100644 sources/Core/AcediaReplicationInfo.uc delete mode 100644 sources/Core/Aliases/AliasHash.uc delete mode 100644 sources/Core/Aliases/AliasService.uc delete mode 100644 sources/Core/Aliases/AliasSource.uc delete mode 100644 sources/Core/Aliases/Aliases.uc delete mode 100644 sources/Core/Aliases/AliasesAPI.uc delete mode 100644 sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc delete mode 100644 sources/Core/Aliases/BuiltInSources/ColorAliases.uc delete mode 100644 sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc delete mode 100644 sources/Core/Aliases/BuiltInSources/WeaponAliases.uc delete mode 100644 sources/Core/Aliases/Tests/MockAliasSource.uc delete mode 100644 sources/Core/Aliases/Tests/MockAliases.uc delete mode 100644 sources/Core/Aliases/Tests/TEST_Aliases.uc delete mode 100644 sources/Core/Color/ColorAPI.uc delete mode 100644 sources/Core/Color/Tests/TEST_ColorAPI.uc delete mode 100644 sources/Core/Console/ConsoleAPI.uc delete mode 100644 sources/Core/Console/ConsoleBuffer.uc delete mode 100644 sources/Core/Console/ConsoleWriter.uc delete mode 100644 sources/Core/Data/JSON/JArray.uc delete mode 100644 sources/Core/Data/JSON/JObject.uc delete mode 100644 sources/Core/Data/JSON/JSON.uc delete mode 100644 sources/Core/Data/JSON/JSONAPI.uc delete mode 100644 sources/Core/Data/JSON/Tests/TEST_JSON.uc delete mode 100644 sources/Core/Events/Broadcast/BroadcastEvents.uc delete mode 100644 sources/Core/Events/Broadcast/BroadcastHandler.uc delete mode 100644 sources/Core/Events/Broadcast/BroadcastListenerBase.uc delete mode 100644 sources/Core/Events/Events.uc delete mode 100644 sources/Core/Events/Listener.uc delete mode 100644 sources/Core/Events/Mutator/MutatorEvents.uc delete mode 100644 sources/Core/Events/Mutator/MutatorListenerBase.uc delete mode 100644 sources/Core/Feature.uc delete mode 100644 sources/Core/Logger/LoggerAPI.uc delete mode 100644 sources/Core/Logger/LoggerService.uc delete mode 100644 sources/Core/Memory/MemoryAPI.uc delete mode 100644 sources/Core/Service.uc delete mode 100644 sources/Core/Singleton.uc delete mode 100644 sources/Core/Testing/IssueSummary.uc delete mode 100644 sources/Core/Testing/Service/TestingEvents.uc delete mode 100644 sources/Core/Testing/Service/TestingService.uc delete mode 100644 sources/Core/Testing/TestCase.uc delete mode 100644 sources/Core/Testing/TestCaseSummary.uc delete mode 100644 sources/Core/Text/Parser.uc delete mode 100644 sources/Core/Text/Tests/TEST_Parser.uc delete mode 100644 sources/Core/Text/Tests/TEST_Text.uc delete mode 100644 sources/Core/Text/Tests/TEST_TextAPI.uc delete mode 100644 sources/Core/Text/Text.uc delete mode 100644 sources/Core/Text/TextAPI.uc delete mode 100644 sources/Core/Text/UnicodeData.uc delete mode 100644 sources/Features/FixAmmoSelling/AmmoPickupStalker.uc delete mode 100644 sources/Features/FixAmmoSelling/FixAmmoSelling.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc delete mode 100644 sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc delete mode 100644 sources/Features/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc delete mode 100644 sources/Features/FixDoshSpam/FixDoshSpam.uc delete mode 100644 sources/Features/FixDoshSpam/MutatorListener_FixDoshSpam.uc delete mode 100644 sources/Features/FixDualiesCost/DualiesCostRule.uc delete mode 100644 sources/Features/FixDualiesCost/FixDualiesCost.uc delete mode 100644 sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc delete mode 100644 sources/Features/FixFFHack/FFHackRule.uc delete mode 100644 sources/Features/FixFFHack/FixFFHack.uc delete mode 100644 sources/Features/FixInfiniteNades/FixInfiniteNades.uc delete mode 100644 sources/Features/FixInfiniteNades/FixedFragFire.uc delete mode 100644 sources/Features/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc delete mode 100644 sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc delete mode 100644 sources/Features/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc delete mode 100644 sources/Features/FixSpectatorCrash/FixSpectatorCrash.uc delete mode 100644 sources/Features/FixZedTimeLags/FixZedTimeLags.uc delete mode 100644 sources/Global.uc delete mode 100644 sources/Manifest.uc rename sources/{Core/Acedia.uc => Packages.uc} (64%) delete mode 100644 sources/Services/Connection/ConnectionEvents.uc delete mode 100644 sources/Services/Connection/ConnectionListenerBase.uc delete mode 100644 sources/Services/Connection/ConnectionService.uc delete mode 100644 sources/Services/Connection/MutatorListener_Connection.uc rename sources/{Core => }/StartUp.uc (95%) rename sources/{Core/Testing/Service/TestingListenerBase.uc => TestingListener_AcediaLauncher.uc} (55%) diff --git a/config/Acedia.ini b/config/Acedia.ini index 80b4ec9..fecbb06 100644 --- a/config/Acedia.ini +++ b/config/Acedia.ini @@ -1,255 +1,3 @@ -[Acedia.FixDualiesCost] -; This feature fixes several issues, related to the selling price of both -; single and dual pistols, all originating from the existence of dual weapons. -; Most notable issue is the ability to "print" money by buying and -; selling pistols in a certain way. -; -; Fix only works with vanilla pistols, as it's unpredictable what -; custom ones can do and they can handle these issues on their own -; in a better way. -autoEnable=true -; Some issues involve possible decrease in pistols' price and -; don't lead to the exploit, but are still bugs and require fixing. -; If you have a Deagle in your inventory and then get another one -; (by either buying or picking it off the ground) - the price of resulting -; dual pistols will be set to the price of the last deagle, -; like the first one wasn't worth anything at all. -; In particular this means that (prices are off-perk for more clarity): -; 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of -; the cost (+750 do$h), you lose 250 do$h; -; 2. If you first buy a deagle (-500 do$h), then buy -; the second one (-500 do$h) and then sell them, you'll only get -; 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h; -; 3. So if you already have bought a deagle (-500 do$h), -; you can get a more expensive weapon by doing a stupid thing -; and first selling your Deagle (+375 do$h), -; then buying dual deagles (-1000 do$h). -; If you sell them after that, you'll gain 75% of the cost of -; dual deagles (+750 do$h), leaving you with losing only 375 do$h. -; Of course, situations described above are only relevant if you're planning -; to sell your weapons at some point and most players won't even -; notice these issues. -; But such an oversight still shouldn't exist in a game and we fix it by -; setting sell value of dualies as a sum of values of each pistol. -; Yet, fixing this issue leads to players having more expensive -; (while fairly priced) weapons than on vanilla, technically making -; the game easier. And some people might object to having that in -; a whitelisted bug-fixing feature. -; These people are, without a question, complete degenerates. -; But making mods for only non-mentally challenged isn't inclusive. -; So we add this option. -; Set it to 'false' if you only want to fix ammo printing -; and leave the rest of the bullshit as-is. -allowSellValueIncrease=true - - -[Acedia.FixAmmoSelling] -; This feature addressed an oversight in vanilla code that -; allows clients to sell weapon's ammunition. -; Due to the implementation of ammo selling, this allows cheaters to -; "print money" by buying and selling ammo over and over again. -autoEnable=true -; Due to how this fix works, players with level below 6 get charged less -; than necessary by the shop and this fix must take the rest of -; the cost by itself. -; The problem is, due to how ammo purchase is coded, low-level (<6 lvl) -; players can actually buy more ammo for "fixed" weapons than they can afford -; by filling ammo for one or all weapons. -; Setting this flag to 'true' will allow us to still take full cost -; from them, putting them in "debt" (having negative dosh amount). -; If you don't want to have players with negative dosh values on your server -; as a side-effect of this fix, then leave this flag as 'false', -; letting low level players buy ammo cheaper -; (but not cheaper than lvl6 could). -; NOTE: this issue doesn't affect level 6 players. -; NOTE #2: this fix does give players below level 6 some -; technical advantage compared to vanilla game, but this advantage -; cannot exceed benefits of having level 6. -allowNegativeDosh=false - - -[Acedia.FixInventoryAbuse] -; This feature addressed two issues with the inventory: -; 1. Players carrying amount of weapons that shouldn't be allowed by the -; weight limit. -; 2. Players carrying two variants of the same gun. -; For example carrying both M32 and camo M32. -; Single and dual version of the same weapon are also considered -; the same type of gun, so you shouldn't be able to carry -; both MK23 and dual MK23 or dual handcannons and golden handcannon. -; But cheaters do. But not with this fix. -autoEnable=true -; How often (in seconds) should we do inventory validation checks? -; You shouldn't really worry about performance, but there's also no need to -; do this check too often. -checkInterval=0.25 -; For this fix to properly work, this array must contain an entry for -; every dual weapon in the game (like pistols, with single and dual versions). -; It's made configurable in case of custom dual weapons. -dualiesClasses=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup') -dualiesClasses=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup') -dualiesClasses=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup') -dualiesClasses=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup') -dualiesClasses=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup') -dualiesClasses=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup') - - -[Acedia.FixInfiniteNades] -; This feature fixes a vulnerability in a code of 'Frag' that can allow -; player to throw grenades even when he no longer has any. -; There's also no cooldowns on the throw, which can lead to a server crash. -autoEnable=true -; Setting this flag to 'true' will allow to throw grenades by calling -; 'ServerThrow' directly, as long as player has necessary ammo. -; This can allow some players to throw grenades much quicker than intended, -; therefore it's suggested to keep this flag set to 'false'. -ignoreTossFlags=false - - -[Acedia.FixDoshSpam] -; This feature addressed two dosh-related issues: -; 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash'; -; 2. Breaking collision detection logic by stacking large amount of -; 'CashPickup' actors in one place, which allows one to either -; reach unintended locations or even instantly kill zeds. -; -; It fixes them by limiting speed, with which dosh can spawn, and -; allowing this limit to decrease when there's already too much dosh -; present on the map. -autoEnable=true -; Highest and lowest speed with which players can throw dosh wads. -; It'll be evenly spread between all players. -; For example, if speed is set to 6 and only one player will be spamming dosh, -; - he'll be able to throw 6 wads of dosh per second; -; but if all 6 players are spamming it, - each will throw only 1 per second. -; NOTE: these speed values can be exceeded, since a player is guaranteed -; to be able to throw at least one wad of dosh, if he didn't do so in awhile. -; NOTE #2: if maximum value is less than minimum one, -; the lowest (maximum one) will be used. -doshPerSecondLimitMax=50 -doshPerSecondLimitMin=5 -; Amount of dosh pickups on the map at which we must set dosh per second -; to 'doshPerSecondLimitMin'. -; We use 'doshPerSecondLimitMax' when there's no dosh on the map and -; scale linearly between them as it's amount grows. -criticalDoshAmount=25 - - -[Acedia.FixSpectatorCrash] -; This feature attempts to prevent server crashes caused by someone -; quickly switching between being spectator and an active player. -autoEnable=true -; This fix will try to kick any player that switches between active player -; and cooldown faster than time (in seconds) in this value. -; NOTE: raising this value past default value of '0.25' -; won't actually improve crash prevention, but might cause regular players to -; get accidentally kicked. -spectatorChangeTimeout=0.25 -; [ADVANCED] Don't change this setting unless you know what you're doing. -; Allows you to turn off server blocking. -; Players that don't respect timeout will still be kicked. -; This might be needed if this fix conflicts with another mutator -; that also changes 'numPlayers'. -; This option is necessary to block aggressive enough server crash -; attempts, but can cause compatibility issues with some mutators. -; It's highly recommended to rewrite such a mutator to be compatible instead. -; NOTE: fix should be compatible with most faked players-type mutators, -; since this it remembers the difference between amount of -; real players and 'numPlayers'. -; After unblocking, it sets 'numPlayers' to -; the current amount of real players + that difference. -; So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes -; 3 players + 3 (=6 numPlayers). -allowServerBlock=true - - -[Acedia.FixFFHack] -; This feature fixes a bug that can allow players to bypass server's -; friendly fire limitations and teamkill. -; Usual fixes apply friendly fire scale to suspicious damage themselves, which -; also disables some of the environmental damage. -; In oder to avoid that, this fix allows server owner to define precisely -; to what damage types to apply the friendly fire scaling. -; It should be all damage types related to projectiles. -autoEnable=true -; Defines a general rule for chosing whether or not to apply -; friendly fire scaling. -; This can be overwritten by exceptions ('alwaysScale' or 'neverScale'). -; Enabling scaling by default without any exceptions in 'neverScale' will -; make this fix behave almost identically to Mutant's 'Explosives Fix Mutator'. -scaleByDefault=false -; Damage types, for which we should always reaaply friendly fire scaling. -alwaysScale=Class'KFMod.DamTypeCrossbuzzsawHeadShot' -alwaysScale=Class'KFMod.DamTypeCrossbuzzsaw' -alwaysScale=Class'KFMod.DamTypeFrag' -alwaysScale=Class'KFMod.DamTypePipeBomb' -alwaysScale=Class'KFMod.DamTypeM203Grenade' -alwaysScale=Class'KFMod.DamTypeM79Grenade' -alwaysScale=Class'KFMod.DamTypeM79GrenadeImpact' -alwaysScale=Class'KFMod.DamTypeM32Grenade' -alwaysScale=Class'KFMod.DamTypeLAW' -alwaysScale=Class'KFMod.DamTypeLawRocketImpact' -alwaysScale=Class'KFMod.DamTypeFlameNade' -alwaysScale=Class'KFMod.DamTypeFlareRevolver' -alwaysScale=Class'KFMod.DamTypeFlareProjectileImpact' -alwaysScale=Class'KFMod.DamTypeBurned' -alwaysScale=Class'KFMod.DamTypeTrenchgun' -alwaysScale=Class'KFMod.DamTypeHuskGun' -alwaysScale=Class'KFMod.DamTypeCrossbow' -alwaysScale=Class'KFMod.DamTypeCrossbowHeadShot' -alwaysScale=Class'KFMod.DamTypeM99SniperRifle' -alwaysScale=Class'KFMod.DamTypeM99HeadShot' -alwaysScale=Class'KFMod.DamTypeShotgun' -alwaysScale=Class'KFMod.DamTypeNailGun' -alwaysScale=Class'KFMod.DamTypeDBShotgun' -alwaysScale=Class'KFMod.DamTypeKSGShotgun' -alwaysScale=Class'KFMod.DamTypeBenelli' -alwaysScale=Class'KFMod.DamTypeSPGrenade' -alwaysScale=Class'KFMod.DamTypeSPGrenadeImpact' -alwaysScale=Class'KFMod.DamTypeSeekerSixRocket' -alwaysScale=Class'KFMod.DamTypeSeekerRocketImpact' -alwaysScale=Class'KFMod.DamTypeSealSquealExplosion' -alwaysScale=Class'KFMod.DamTypeRocketImpact' -alwaysScale=Class'KFMod.DamTypeBlowerThrower' -alwaysScale=Class'KFMod.DamTypeSPShotgun' -alwaysScale=Class'KFMod.DamTypeZEDGun' -alwaysScale=Class'KFMod.DamTypeZEDGunMKII' -alwaysScale=Class'KFMod.DamTypeZEDGunMKII' -; Damage types, for which we should never reaply friendly fire scaling. -;neverScale=Class'KFMod.???' - - -[Acedia.FixZedTimeLags] -; When zed time activates, game speed is immediately set to -; 'zedTimeSlomoScale' (0.2 by default), defined, like all other variables, -; in 'KFGameType'. Zed time lasts 'zedTimeDuration' seconds (3.0 by default), -; but during last 'zedTimeDuration * 0.166' seconds (by default 0.498) -; it starts to speed back up, causing game speed to update every tick. -; This makes animations look more smooth when exiting zed-time. -; However, updating speed every tick for that purpose seems like -; an overkill and, combined with things like -; increased tick rate, certain open maps and increased zed limit, -; it can lead to noticable lags at the end of the zed time. -; This fix limits amount of actual game speed updates, alleviating the issue. -; -; As a side effect it also fixes an issue where during zed time speed up -; 'zedTimeSlomoScale' was assumed to be default value of '0.2'. -; Now zed time will behave correctly with mods that change 'zedTimeSlomoScale'. -autoEnable=true -; Maximum amount of game speed updates upon leaving zed time. -; 2 or 3 seem to provide a good enough result that, -; i.e. it should be hard to notice difference with vanilla game behavior. -; 1 is a smallest possible value, resulting in effectively removing any -; smooting via speed up, simply changing speed from -; the slowest (0.2) to the highest. -; For the reference: on servers with default 30 tick rate there's usually -; about 13 updates total (without this fix). -maxGameSpeedUpdatesAmount=3 -; [ADVANCED] Don't change this setting unless you know what you're doing. -; Compatibility setting that allows to keep 'GameInfo' 's 'Tick' event -; from being disabled. -; Useful when running Acedia along with custom 'GameInfo' -; (that isn't 'KFGameType') that relies on 'Tick' event. -; Note, however, that in order to keep this fix working properly, -; it's on you to make sure 'KFGameType.Tick()' logic isn't executed. -disableTick=true \ No newline at end of file +[Acedia.Packages] +corePackage="AcediaCore_0_2" +package="AcediaFixes" \ No newline at end of file diff --git a/config/AcediaAliases_Colors.ini b/config/AcediaAliases_Colors.ini deleted file mode 100644 index 27c0935..0000000 --- a/config/AcediaAliases_Colors.ini +++ /dev/null @@ -1,165 +0,0 @@ -[Acedia.ColorAliasSource] -; System colors -record=(alias="text_default",value="rgb(255,255,255)") -record=(alias="text_subtle",value="rgb(128,128,128)") -record=(alias="text_emphasis",value="rgb(0,128,255)") -record=(alias="text_ok",value="rgb(0,255,0)") -record=(alias="text_warning",value="rgb(255,128,0)") -record=(alias="text_failure",value="rgb(255,0,0)") -record=(alias="type_number",value="rgb(181,137,0)") -record=(alias="type_boolean",value="rgb(38,139,210)") -record=(alias="type_string",value="rgb(211,54,130)") -record=(alias="type_literal",value="rgb(42,161,152)") -record=(alias="type_class",value="rgb(108,113,196)") -; Pink colors -record=(alias="Pink",value="rgb(255,192,203)") -record=(alias="LightPink",value="rgb(255,182,193)") -record=(alias="HotPink",value="rgb(255,105,180)") -record=(alias="DeepPink",value="rgb(255,20,147)") -record=(alias="PaleVioletRed",value="rgb(219,112,147)") -record=(alias="MediumVioletRed",value="rgb(199,21,133)") -; Red colors -record=(alias="LightSalmon",value="rgb(255,160,122)") -record=(alias="Salmon",value="rgb(250,128,114)") -record=(alias="DarkSalmon",value="rgb(233,150,122)") -record=(alias="LightCoral",value="rgb(240,128,128)") -record=(alias="IndianRed",value="rgb(205,92,92)") -record=(alias="Crimson",value="rgb(220,20,60)") -record=(alias="Firebrick",value="rgb(178,34,34)") -record=(alias="DarkRed",value="rgb(139,0,0)") -record=(alias="Red",value="rgb(255,0,0)") -; Orange colors -record=(alias="OrangeRed",value="rgb(255,69,0)") -record=(alias="Tomato",value="rgb(255,99,71)") -record=(alias="Coral",value="rgb(255,127,80)") -record=(alias="DarkOrange",value="rgb(255,140,0)") -record=(alias="Orange",value="rgb(255,165,0)") -; Yellow colors -record=(alias="Yellow",value="rgb(255,255,0)") -record=(alias="LightYellow",value="rgb(255,255,224)") -record=(alias="LemonChiffon",value="rgb(255,250,205)") -record=(alias="LightGoldenrodYellow",value="rgb(250,250,210)") -record=(alias="PapayaWhip",value="rgb(255,239,213)") -record=(alias="Moccasin",value="rgb(255,228,181)") -record=(alias="PeachPuff",value="rgb(255,218,185)") -record=(alias="PaleGoldenrod",value="rgb(238,232,170)") -record=(alias="Khaki",value="rgb(240,230,140)") -record=(alias="DarkKhaki",value="rgb(189,183,107)") -record=(alias="Gold",value="rgb(255,215,0)") -; Brown colors -record=(alias="Cornsilk",value="rgb(255,248,220)") -record=(alias="BlanchedAlmond",value="rgb(255,235,205)") -record=(alias="Bisque",value="rgb(255,228,196)") -record=(alias="NavajoWhite",value="rgb(255,222,173)") -record=(alias="Wheat",value="rgb(245,222,179)") -record=(alias="Burlywood",value="rgb(222,184,135)") -record=(alias="Tan",value="rgb(210,180,140)") -record=(alias="RosyBrown",value="rgb(188,143,143)") -record=(alias="SandyBrown",value="rgb(244,164,96)") -record=(alias="Goldenrod",value="rgb(218,165,32)") -record=(alias="DarkGoldenrod",value="rgb(184,134,11)") -record=(alias="Peru",value="rgb(205,133,63)") -record=(alias="Chocolate",value="rgb(210,105,30)") -record=(alias="SaddleBrown",value="rgb(139,69,19)") -record=(alias="Sienna",value="rgb(160,82,45)") -record=(alias="Brown",value="rgb(165,42,42)") -record=(alias="Maroon",value="rgb(128,0,0)") -; Green colors -record=(alias="DarkOliveGreen",value="rgb(85,107,47)") -record=(alias="Olive",value="rgb(128,128,0)") -record=(alias="OliveDrab",value="rgb(107,142,35)") -record=(alias="YellowGreen",value="rgb(154,205,50)") -record=(alias="LimeGreen",value="rgb(50,205,50)") -record=(alias="Lime",value="rgb(0,255,0)") -record=(alias="LawnGreen",value="rgb(124,252,0)") -record=(alias="Chartreuse",value="rgb(127,255,0)") -record=(alias="GreenYellow",value="rgb(173,255,47)") -record=(alias="SpringGreen",value="rgb(0,255,127)") -record=(alias="MediumSpringGreen",value="rgb(0,250,154)") -record=(alias="LightGreen",value="rgb(144,238,144)") -record=(alias="PaleGreen",value="rgb(152,251,152)") -record=(alias="DarkSeaGreen",value="rgb(143,188,143)") -record=(alias="MediumAquamarine",value="rgb(102,205,170)") -record=(alias="MediumSeaGreen",value="rgb(60,179,113)") -record=(alias="SeaGreen",value="rgb(46,139,87)") -record=(alias="ForestGreen",value="rgb(34,139,34)") -record=(alias="Green",value="rgb(0,128,0)") -record=(alias="DarkGreen",value="rgb(0,100,0)") -; Cyan colors -record=(alias="Aqua",value="rgb(0,255,255)") -record=(alias="Cyan",value="rgb(0,255,255)") -record=(alias="LightCyan",value="rgb(224,255,255)") -record=(alias="PaleTurquoise",value="rgb(175,238,238)") -record=(alias="Aquamarine",value="rgb(127,255,212)") -record=(alias="Turquoise",value="rgb(64,224,208)") -record=(alias="MediumTurquoise",value="rgb(72,209,204)") -record=(alias="DarkTurquoise",value="rgb(0,206,209)") -record=(alias="LightSeaGreen",value="rgb(32,178,170)") -record=(alias="CadetBlue",value="rgb(95,158,160)") -record=(alias="DarkCyan",value="rgb(0,139,139)") -record=(alias="Teal",value="rgb(0,128,128)") -; Blue colors -record=(alias="LightSteelBlue",value="rgb(176,196,222)") -record=(alias="PowderBlue",value="rgb(176,224,230)") -record=(alias="LightBlue",value="rgb(173,216,230)") -record=(alias="SkyBlue",value="rgb(135,206,235)") -record=(alias="LightSkyBlue",value="rgb(135,206,250)") -record=(alias="DeepSkyBlue",value="rgb(0,191,255)") -record=(alias="DodgerBlue",value="rgb(30,144,255)") -record=(alias="CornflowerBlue",value="rgb(100,149,237)") -record=(alias="SteelBlue",value="rgb(70,130,180)") -record=(alias="RoyalBlue",value="rgb(65,105,225)") -record=(alias="Blue",value="rgb(0,0,255)") -record=(alias="MediumBlue",value="rgb(0,0,205)") -record=(alias="DarkBlue",value="rgb(0,0,139)") -record=(alias="Navy",value="rgb(0,0,128)") -record=(alias="MidnightBlue",value="rgb(25,25,112)") -; Purple, violet, and magenta colors -record=(alias="Lavender",value="rgb(230,230,250)") -record=(alias="Thistle",value="rgb(216,191,216)") -record=(alias="Plum",value="rgb(221,160,221)") -record=(alias="Violet",value="rgb(238,130,238)") -record=(alias="Orchid",value="rgb(218,112,214)") -record=(alias="Fuchsia",value="rgb(255,0,255)") -record=(alias="Magenta",value="rgb(255,0,255)") -record=(alias="MediumOrchid",value="rgb(186,85,211)") -record=(alias="MediumPurple",value="rgb(147,112,219)") -record=(alias="BlueViolet",value="rgb(138,43,226)") -record=(alias="DarkViolet",value="rgb(148,0,211)") -record=(alias="DarkOrchid",value="rgb(153,50,204)") -record=(alias="DarkMagenta",value="rgb(139,0,139)") -record=(alias="Purple",value="rgb(128,0,128)") -record=(alias="Indigo",value="rgb(75,0,130)") -record=(alias="DarkSlateBlue",value="rgb(72,61,139)") -record=(alias="SlateBlue",value="rgb(106,90,205)") -record=(alias="MediumSlateBlue",value="rgb(123,104,238)") -; White colors -record=(alias="White",value="rgb(255,255,255)") -record=(alias="Snow",value="rgb(255,250,250)") -record=(alias="Honeydew",value="rgb(240,255,240)") -record=(alias="MintCream",value="rgb(245,255,250)") -record=(alias="Azure",value="rgb(240,255,255)") -record=(alias="AliceBlue",value="rgb(240,248,255)") -record=(alias="GhostWhite",value="rgb(248,248,255)") -record=(alias="WhiteSmoke",value="rgb(245,245,245)") -record=(alias="Seashell",value="rgb(255,245,238)") -record=(alias="Beige",value="rgb(245,245,220)") -record=(alias="OldLace",value="rgb(253,245,230)") -record=(alias="FloralWhite",value="rgb(255,250,240)") -record=(alias="Ivory",value="rgb(255,255,240)") -record=(alias="AntiqueWhite",value="rgb(250,235,215)") -record=(alias="Linen",value="rgb(250,240,230)") -record=(alias="LavenderBlush",value="rgb(255,240,245)") -record=(alias="MistyRose",value="rgb(255,228,225)") -; Gray and black colors -record=(alias="Gainsboro",value="rgb(220,220,220)") -record=(alias="LightGray",value="rgb(211,211,211)") -record=(alias="Silver",value="rgb(192,192,192)") -record=(alias="Gray",value="rgb(169,169,169)") -record=(alias="DimGray",value="rgb(128,128,128)") -record=(alias="DarkGray",value="rgb(105,105,105)") -record=(alias="LightSlateGray",value="rgb(119,136,153)") -record=(alias="SlateGray",value="rgb(112,128,144)") -record=(alias="DarkSlateGray",value="rgb(47,79,79)") -record=(alias="Eigengrau",value="rgb(22,22,29)") -record=(alias="Black",value="rgb(0,0,0)") \ No newline at end of file diff --git a/config/AcediaAliases_Tests.ini b/config/AcediaAliases_Tests.ini deleted file mode 100644 index 850b0b4..0000000 --- a/config/AcediaAliases_Tests.ini +++ /dev/null @@ -1,17 +0,0 @@ -; For the puposes of testing alias functionality. -; Changing these can break tests. -; -; If you don't plan to run tests or do not know what they are, - -; feel free to remove this file. -[Acedia.MockAliasSource] -record=(alias="global",value="value") -record=(alias="question",value="response") -record=(alias="",value="empty") -record=(alias="also",value="") -[car MockAliases] -Alias="Ford" -Alias="Delorean" -Alias="Audi" -[sci:fi MockAliases] -Alias="Spice" -Alias="HardToBeAGod" \ No newline at end of file diff --git a/config/AcediaAliases_Weapons.ini b/config/AcediaAliases_Weapons.ini deleted file mode 100644 index 067af09..0000000 --- a/config/AcediaAliases_Weapons.ini +++ /dev/null @@ -1,547 +0,0 @@ -[Acedia.WeaponAliasSource] -; Field Medic weapons -[KFMod:MP7MMedicGun WeaponAliases] -Alias="MP7M" -Alias="MP7" -[KFMod:MP5MMedicGun WeaponAliases] -Alias="MP5M" -Alias="MP5" -Alias="MP" -Alias="M5" -[KFMod:CamoMP5MMedicGun WeaponAliases] -Alias="CamoMP5M" -Alias="CamoMP5" -Alias="CamoMP" -Alias="CamoM5" -[KFMod:M7A3MMedicGun WeaponAliases] -Alias="M7A3" -Alias="M7A" -Alias="M7" -[KFMod:KrissMMedicGun WeaponAliases] -Alias="Schneidzekk" -Alias="Schneidzek" -Alias="Kriss" -Alias="Kris" -[KFMod:NeonKrissMMedicGun WeaponAliases] -Alias="NeonSchneidzekk" -Alias="NeonSchneidzek" -Alias="NeonKriss" -Alias="NeonKris" -[KFMod:BlowerThrower WeaponAliases] -Alias="BlowerThrower" -Alias="Blower" -Alias="Thrower" -Alias="BThrower" -Alias="PoopGun" -Alias="BileGun" -Alias="BloatGun" - -; Support Specialist weapons -[KFMod:Shotgun WeaponAliases] -Alias="Shotgun" -[KFMod:CamoShotgun WeaponAliases] -Alias="CamoShotgun" -[KFMod:BoomStick WeaponAliases] -Alias="HuntingShotgun" -Alias="BoomStick" -Alias="Hunting" -[KFMod:KSGShotgun WeaponAliases] -Alias="HSG-1Shotgun" -Alias="HSG1Shotgun" -Alias="HSGShotgun" -Alias="HSG" -Alias="KSG-1Shotgun" -Alias="KSG1Shotgun" -Alias="KSGShotgun" -Alias="KSG" -[KFMod:NeonKSGShotgun WeaponAliases] -Alias="NeonHSG-1Shotgun" -Alias="NeonHSG1Shotgun" -Alias="NeonHSGShotgun" -Alias="NeonHSG" -Alias="NeonKSG-1Shotgun" -Alias="NeonKSG1Shotgun" -Alias="NeonKSGShotgun" -Alias="NeonKSG" -[KFMod:NailGun WeaponAliases] -Alias="VladTheImpaler" -Alias="VladImpaler" -Alias="Vlad" -Alias="Impaler" -Alias="NailGun" -Alias="Nails" -Alias="Nail" -[KFMod:SPAutoShotgun WeaponAliases] -Alias="MultichamberZEDThrower" -Alias="ZEDThrower" -Alias="ZThrower" -[KFMod:BenelliShotgun WeaponAliases] -Alias="CombatShotgun" -Alias="Combat" -Alias="CShotgun" -Alias="BenelliShotgun" -Alias="BeneliShotgun" -Alias="Benelli" -Alias="Beneli" -[KFMod:GoldenBenelliShotgun WeaponAliases] -Alias="GoldCombatShotgun" -Alias="GoldCombat" -Alias="GoldCShotgun" -Alias="GoldBenelliShotgun" -Alias="GoldBeneliShotgun" -Alias="GoldBenelli" -Alias="GoldBeneli" -[KFMod:AA12AutoShotgun WeaponAliases] -Alias="AA12" -Alias="AA12AutoShotgun" -Alias="AA12Shotgun" -[KFMod:GoldenAA12AutoShotgun WeaponAliases] -Alias="GoldAA12" -Alias="GoldAA12AutoShotgun" -Alias="GoldAA12Shotgun" - -; Sharpshooter weapons -[KFMod:Single WeaponAliases] -Alias="9mmTactical" -Alias="9mmTact" -Alias="9mm" -Alias="Single" -Alias="Pistol" -[KFMod:Dualies WeaponAliases] -Alias="Dual9mms" -Alias="Dual9mm" -Alias="9mmDual" -Alias="Dualies" -Alias="Dual" -[KFMod:Magnum44Pistol WeaponAliases] -Alias="Magnum44Pistol" -Alias="Magnum44" -Alias="44Magnum" -Alias="Magnum" -Alias="44" -[KFMod:Dual44Magnum WeaponAliases] -Alias="DualMagnum44Pistols" -Alias="DualMagnum44s" -Alias="DualMagnums" -Alias="DualMagnumPistols" -Alias="Dual44Magnums" -Alias="Dual44Magnum" -Alias="DualMagnum" -Alias="Dual44ss" -Alias="Dual44" -[KFMod:MK23Pistol WeaponAliases] -Alias="MK23" -Alias="MK" -Alias="23" -[KFMod:DualMK23Pistol WeaponAliases] -Alias="DualMK23s" -Alias="DualMK23" -Alias="DualMKs" -Alias="DualMK" -Alias="Dual23s" -Alias="Dual23" -[KFMod:Deagle WeaponAliases] -Alias="Handcannon" -Alias="Deagle" -Alias="HC" -[KFMod:DualDeagle WeaponAliases] -Alias="DualHandcannons" -Alias="DualHC" -Alias="DualDeagle" -[KFMod:GoldenDeagle WeaponAliases] -Alias="GoldHandcannon" -Alias="GoldDeagle" -Alias="GoldHC" -[KFMod:GoldenDualDeagle WeaponAliases] -Alias="GoldDualHandcannons" -Alias="GoldDualHC" -Alias="GoldDualDeagle" -[KFMod:Winchester WeaponAliases] -Alias="Winchester" -Alias="LeverActionRifle" -Alias="LAR" -[KFMod:SPSniperRifle WeaponAliases] -Alias="SPMusket" -Alias="Musket" -Alias="SPSniperRifle" -Alias="SPRifle" -Alias="SPSniper" -Alias="SPMauler" -Alias="Mauler" -[KFMod:M14EBRBattleRifle WeaponAliases] -Alias="M14EBR" -Alias="M14" -Alias="EBR" -Alias="M14EBRRifle" -Alias="M14EBRBattleRifle" -[KFMod:Crossbow WeaponAliases] -Alias="CompoundCrossbow" -Alias="CCrossbow" -Alias="Crossbow" -Alias="XBow" -[KFMod:M99SniperRifle WeaponAliases] -Alias="M99AMR" -Alias="M99" -Alias="M99SniperRifle" -Alias="M99Sniper" -Alias="M99Rifle" -Alias="M99SR" - -; Commando weapons -[KFMod:Bullpup WeaponAliases] -Alias="Bullpup" -Alias="Bulpup" -[KFMod:ThompsonSMG WeaponAliases] -Alias="ThompsonSMG" -Alias="Thompson" -Alias="Thomp" -Alias="TommyGun" -Alias="TomyGun" -Alias="Tommy" -Alias="Tomy" -[KFMod:SPThompsonSMG WeaponAliases] -Alias="SPThompsonSMG" -Alias="SPThompson" -Alias="SPThomp" -Alias="Dr.T'sLeadDeliverySystem" -Alias="Dr.TsLeadDeliverySystem" -Alias="DrT'sLeadDeliverySystem" -Alias="DrTsLeadDeliverySystem" -Alias="Dr.T'LeadDeliverySystem" -Alias="Dr.TLeadDeliverySystem" -Alias="DrT'LeadDeliverySystem" -Alias="DrTLeadDeliverySystem" -Alias="DrTDeliverySystem" -Alias="DrTLeadSystem" -Alias="DrTLeadDelivery" -Alias="DrTDelivery" -Alias="LeadDelivery" -Alias="LeadSystem" -Alias="DeliverySystem" -Alias="LeadDS" -Alias="LeadD" -[KFMod:ThompsonDrumSMG WeaponAliases] -Alias="ThompsonDrumSMG" -Alias="ThompsonDrum" -Alias="ThompDrum" -Alias="RisingStormTommyGun" -Alias="RisingStormTommy" -Alias="RisingStormTomyGun" -Alias="RisingStormTomy" -Alias="RSTommyGun" -Alias="RSTommy" -Alias="RSTomyGun" -Alias="RSTomy" -[KFMod:AK47AssaultRifle WeaponAliases] -Alias="AK47AssaultRifle" -Alias="AK47Assault" -Alias="AK47Rifle" -Alias="AK47AR" -Alias="AK47" -Alias="AK" -Alias="47" -[KFMod:GoldenAK47AssaultRifle WeaponAliases] -Alias="GoldAK47AssaultRifle" -Alias="GoldAK47Assault" -Alias="GoldAK47Rifle" -Alias="GoldAK47AR" -Alias="GoldAK47" -Alias="GoldAK" -Alias="Gold47" -[KFMod:NeonAK47AssaultRifle WeaponAliases] -Alias="NeonAK47AssaultRifle" -Alias="NeonAK47Assault" -Alias="NeonAK47Rifle" -Alias="NeonAK47AR" -Alias="NeonAK47" -Alias="NeonAK" -Alias="Neon47" -[KFMod:M4AssaultRifle WeaponAliases] -Alias="M4AssaultRifle" -Alias="M4Assault" -Alias="M4Rifle" -Alias="M4" -[KFMod:CamoM4AssaultRifle WeaponAliases] -Alias="CamoM4AssaultRifle" -Alias="CamoM4Assault" -Alias="CamoM4Rifle" -Alias="CamoM4" -[KFMod:MKb42AssaultRifle WeaponAliases] -Alias="MKb42AssaultRifle" -Alias="MKb42Assault" -Alias="MKb42Rifle" -Alias="MKb42" -Alias="MK42" -Alias="MKb" -[KFMod:SCARMK17AssaultRifle WeaponAliases] -Alias="SCARMK17AssaultRifle" -Alias="SCARMK17Assault" -Alias="SCARMK17Rifle" -Alias="SCARMKAssaultRifle" -Alias="SCARMKAssault" -Alias="SCARMKRifle" -Alias="SCAR17AssaultRifle" -Alias="SCAR17Assault" -Alias="SCAR17Rifle" -Alias="SCARAssaultRifle" -Alias="SCARAssault" -Alias="SCARRifle" -Alias="SCAR17" -Alias="SCARMK" -Alias="SCAR" -[KFMod:NeonSCARMK17AssaultRifle WeaponAliases] -Alias="NeonSCARMK17AssaultRifle" -Alias="NeonSCARMK17Assault" -Alias="NeonSCARMK17Rifle" -Alias="NeonSCARMKAssaultRifle" -Alias="NeonSCARMKAssault" -Alias="NeonSCARMKRifle" -Alias="NeonSCAR17AssaultRifle" -Alias="NeonSCAR17Assault" -Alias="NeonSCAR17Rifle" -Alias="NeonSCARAssaultRifle" -Alias="NeonSCARAssault" -Alias="NeonSCARRifle" -Alias="NeonSCAR17" -Alias="NeonSCARMK" -Alias="NeonSCAR" -[KFMod:FNFAL_ACOG_AssaultRifle WeaponAliases]FNFAL ACOG -Alias="FNFALACOGAssaultRifle" -Alias="FNFALACOGAssault" -Alias="FNFALACOGRifle" -Alias="FNFALAssaultRifle" -Alias="FNFALAssault" -Alias="FNFALRifle" -Alias="FALACOGAssaultRifle" -Alias="FALACOGAssault" -Alias="FALACOGRifle" -Alias="FALAssaultRifle" -Alias="FALAssault" -Alias="FALRifle" -Alias="FNFALACOG" -Alias="FNFAL" -Alias="FALACOG" -Alias="FAL" -Alias="FN" - -; Berserker weapons -[KFMod:Knife WeaponAliases] -Alias="Knife" -[KFMod:Machete WeaponAliases] -Alias="Machete" -Alias="Chete" -[KFMod:Axe WeaponAliases] -Alias="Axe" -Alias="FireAxe" -[KFMod:Katana WeaponAliases] -Alias="Katana" -[KFMod:GoldenKatana WeaponAliases] -Alias="GoldKatana" -[KFMod:Scythe WeaponAliases] -Alias="Scythe" -Alias="Scyte" -Alias="Sickle" -Alias="Sickl" -[KFMod:Chainsaw WeaponAliases] -Alias="Chainsaw" -Alias="Saw" -Alias="Denji" -Alias="Pochita" -[KFMod:GoldenChainsaw WeaponAliases] -Alias="GoldChainsaw" -Alias="GoldSaw" -Alias="GoldDenji" -Alias="GoldPochita" -[KFMod:DwarfAxe WeaponAliases] -Alias="DwarfsAxe" -Alias="DwarfAxe" -Alias="ShitAxe" -Alias="CrapAxe" -Alias="PushAxe" -Alias="GnomeAxe" -Alias="TrollAxe" -Alias="NoobAxe" -[KFMod:ClaymoreSword WeaponAliases] -Alias="ClaymoreSword" -Alias="ClaymoreBlade" -Alias="Claymore" -Alias="Claymor" -Alias="ClaimoreSword" -Alias="ClaimoreBlade" -Alias="Claimore" -Alias="Claimor" -Alias="Sword" -Alias="Blade" -[KFMod:Crossbuzzsaw WeaponAliases] -Alias="Crossbuzzsaw" -Alias="Buzzsaw" -Alias="Buzz" -Alias="BuzzsawBow" -Alias="BuzzBow" -Alias="ZerkBow" - -; Firebug weapons -[KFMod:MAC10MP WeaponAliases] -Alias="MAC10MP" -Alias="MAC10" -Alias="MAC" -[KFMod:FlareRevolver WeaponAliases] -Alias="FlareRevolver" -Alias="FireRevolver" -Alias="FlareGun" -Alias="Flares" -Alias="Flare" -[KFMod:DualFlareRevolver WeaponAliases] -Alias="DualFlareRevolvers" -Alias="DualFlareRevolver" -Alias="DualFireRevolvers" -Alias="DualFireRevolver" -Alias="DualFlareGuns" -Alias="DualFlareGun" -Alias="DualFlares" -Alias="DualFlare" -[KFMod:FlameThrower WeaponAliases] -Alias="FlameThrower" -Alias="FireThrower" -Alias="FThrower" -Alias="Flamer" -Alias="FireSpam" -[KFMod:GoldenFlamethrower WeaponAliases] -Alias="GoldFlameThrower" -Alias="GoldFireThrower" -Alias="GoldFThrower" -Alias="GoldFlamer" -Alias="GoldFireSpam" -[KFMod:Trenchgun WeaponAliases] -Alias="DragonsBreathTrenchgun" -Alias="DragonsBreathGun" -Alias="DragonsBreath" -Alias="DragBreathTrenchgun" -Alias="DragBreathGun" -Alias="DragBreath" -Alias="Trenchgun" -Alias="FireShotgun" -Alias="Flameshotgun" -[KFMod:HuskGun WeaponAliases] -Alias="HuskFireballLauncher" -Alias="HuskFireball" -Alias="FireballLauncher" -Alias="HuskLauncher" -Alias="HuskFirebalLauncher" -Alias="HuskFirebal" -Alias="FirebalLauncher" -Alias="HuskGun" -Alias="Husk" - -; Demolition weapons -[KFMod:M79GrenadeLauncher WeaponAliases] -Alias="M79GrenadeLauncher" -Alias="M79Grenade" -Alias="M79Launcher" -Alias="M79NadeLauncher" -Alias="M79Nade" -Alias="M79" -[KFMod:GoldenM79GrenadeLauncher WeaponAliases] -Alias="GoldM79GrenadeLauncher" -Alias="GoldM79Grenade" -Alias="GoldM79Launcher" -Alias="GoldM79NadeLauncher" -Alias="GoldM79Nade" -Alias="GoldM79" -[KFMod:SPGrenadeLauncher WeaponAliases] -Alias="SPGrenadeLauncher" -Alias="SPNadeLauncher" -Alias="SPLauncher" -Alias="SPNade" -Alias="TheOrcaBombPropeller" -Alias="TheOrcaBombPropeler" -Alias="TheOrcaBomb" -Alias="TheOrca" -Alias="TheOrcaLauncher" -Alias="OrcaBombPropeller" -Alias="OrcaBombPropeler" -Alias="OrcaBomb" -Alias="Orca" -Alias="OrcaLauncher" -[KFMod:PipeBombExplosive WeaponAliases] -Alias="PipeBombExplosive" -Alias="PipeExplosive" -Alias="PipeBomb" -Alias="Pipes" -Alias="Pipe" -[KFMod:SealSquealHarpoonBomber WeaponAliases] -Alias="SealSquealHarpoonBomber" -Alias="SealSquealHarpoon" -Alias="SealSquealBomber" -Alias="SealHarpoonBomber" -Alias="SealHarpoon" -Alias="SealBomber" -Alias="SealSqueal" -Alias="HarpoonBomber" -Alias="Harpoon" -Alias="Harp" -[KFMod:SeekerSixRocketLauncher WeaponAliases] -Alias="SeekerSixRocketLauncher" -Alias="SeekerSixLauncher" -Alias="Seeker6RocketLauncher" -Alias="Seeker6Launcher" -Alias="SeekerRocketLauncher" -Alias="SeekerLauncher" -Alias="SeekerSix" -Alias="Seeker6" -Alias="Seeker" -Alias="SuckerSix" -Alias="Sucker6" -Alias="Sucker" -[KFMod:M4203AssaultRifle WeaponAliases] -Alias="M4203Assault" -Alias="M4203Rifle" -Alias="M4203" -Alias="M4200" -Alias="M420" -Alias="M42" -[KFMod:LAW WeaponAliases] -Alias="LAW" -[KFMod:M32GrenadeLauncher WeaponAliases] -Alias="M32GrenadeLauncher" -Alias="M32Grenade" -Alias="M32Launcher" -Alias="M32NadeLauncher" -Alias="M32Nade" -Alias="M32" -[KFMod:CamoM32GrenadeLauncher WeaponAliases] -Alias="CamoM32GrenadeLauncher" -Alias="CamoM32Grenade" -Alias="CamoM32Launcher" -Alias="CamoM32NadeLauncher" -Alias="CamoM32Nade" -Alias="CamoM32" - -; Off-perk weapons -[KFMod:ZEDGun WeaponAliases] -Alias="ZedEradicationDevice" -Alias="ZedEradication" -Alias="ZedDevice" -Alias="ZEDGun" -Alias="ZED" -[KFMod:ZEDMKIIWeapon WeaponAliases] -Alias="ZedEradicationDeviceMKII" -Alias="ZedEradicationMKII" -Alias="ZedDeviceMKII" -Alias="ZEDGunMKII" -Alias="ZEDMKII" -Alias="ZedEradicationDeviceMK2" -Alias="ZedEradicationMK2" -Alias="ZedDeviceMK2" -Alias="ZEDGunMK2" -Alias="ZEDMK2" -Alias="ZedEradicationDeviceMK" -Alias="ZedEradicationMK" -Alias="ZedDeviceMK" -Alias="ZEDGunMK" -Alias="ZEDMK" -Alias="ZedEradicationDevice2" -Alias="ZedEradication2" -Alias="ZedDevice2" -Alias="ZEDGun2" -Alias="ZED2" \ No newline at end of file diff --git a/config/AcediaSystem.ini b/config/AcediaSystem.ini deleted file mode 100644 index ee08db7..0000000 --- a/config/AcediaSystem.ini +++ /dev/null @@ -1,180 +0,0 @@ -; Every single option in this config should be considered [ADVANCED] -[Acedia.AliasService] -; Changing these allows you to change in what sources `AliasesAPI` -; looks for weapon and color aliases. -weaponAliasesSource=Class'WeaponAliasSource' -colorAliasesSource=Class'ColorAliasSource' -; How often are different alias-storing objects are allowed to record -; their updated data into a config. -; Negative or zero values would be reset to `0.05`. -saveInterval=0.05 - -[Acedia.AliasHash] -; Reasonable lower and upper limits on hash table capacity for -; aliases' storage, that will be enforced if user requires something outside -; those bounds. -MINIMUM_CAPACITY=10 -MAXIMUM_CAPACITY=100000 - -[Acedia.TestingService] -; Allows you to run tests on server's start up. This option is to help run -; tests quicker during development and should not be used for servers that are -; setup for actually playing the game. -runTestsOnStartUp=false -; Use these flags to only run tests from particular test cases -filterTestsByName=false -filterTestsByGroup=false -requiredName="" -requiredGroup="" - -[Acedia.ConsoleAPI] -; These should guarantee decent text output in console even at -; 640x480 shit resolution -; (and it look fine at normal resolutions as well) -maxVisibleLineWidth=80 -maxTotalLineWidth=108 - -[Acedia.ColorAPI] -; Changing these values will alter color's definitions in `ColorAPI`, -; changing how Acedia behaves -Pink=(R=255,G=192,B=203,A=255) -LightPink=(R=255,G=182,B=193,A=255) -HotPink=(R=255,G=105,B=180,A=255) -DeepPink=(R=255,G=20,B=147,A=255) -PaleVioletRed=(R=219,G=112,B=147,A=255) -MediumVioletRed=(R=199,G=21,B=133,A=255) -LightSalmon=(R=255,G=160,B=122,A=255) -Salmon=(R=250,G=128,B=114,A=255) -DarkSalmon=(R=233,G=150,B=122,A=255) -LightCoral=(R=240,G=128,B=128,A=255) -IndianRed=(R=205,G=92,B=92,A=255) -Crimson=(R=220,G=20,B=60,A=255) -Firebrick=(R=178,G=34,B=34,A=255) -DarkRed=(R=139,G=0,B=0,A=255) -Red=(R=255,G=0,B=0,A=255) -OrangeRed=(R=255,G=69,B=0,A=255) -Tomato=(R=255,G=99,B=71,A=255) -Coral=(R=255,G=127,B=80,A=255) -DarkOrange=(R=255,G=140,B=0,A=255) -Orange=(R=255,G=165,B=0,A=255) -Yellow=(R=255,G=255,B=0,A=255) -LightYellow=(R=255,G=255,B=224,A=255) -LemonChiffon=(R=255,G=250,B=205,A=255) -LightGoldenrodYellow=(R=250,G=250,B=210,A=255) -PapayaWhip=(R=255,G=239,B=213,A=255) -Moccasin=(R=255,G=228,B=181,A=255) -PeachPuff=(R=255,G=218,B=185,A=255) -PaleGoldenrod=(R=238,G=232,B=170,A=255) -Khaki=(R=240,G=230,B=140,A=255) -DarkKhaki=(R=189,G=183,B=107,A=255) -Gold=(R=255,G=215,B=0,A=255) -Cornsilk=(R=255,G=248,B=220,A=255) -BlanchedAlmond=(R=255,G=235,B=205,A=255) -Bisque=(R=255,G=228,B=196,A=255) -NavajoWhite=(R=255,G=222,B=173,A=255) -Wheat=(R=245,G=222,B=179,A=255) -Burlywood=(R=222,G=184,B=135,A=255) -TanColor=(R=210,G=180,B=140,A=255) -RosyBrown=(R=188,G=143,B=143,A=255) -SandyBrown=(R=244,G=164,B=96,A=255) -Goldenrod=(R=218,G=165,B=32,A=255) -DarkGoldenrod=(R=184,G=134,B=11,A=255) -Peru=(R=205,G=133,B=63,A=255) -Chocolate=(R=210,G=105,B=30,A=255) -SaddleBrown=(R=139,G=69,B=19,A=255) -Sienna=(R=160,G=82,B=45,A=255) -Brown=(R=165,G=42,B=42,A=255) -Maroon=(R=128,G=0,B=0,A=255) -DarkOliveGreen=(R=85,G=107,B=47,A=255) -Olive=(R=128,G=128,B=0,A=255) -OliveDrab=(R=107,G=142,B=35,A=255) -YellowGreen=(R=154,G=205,B=50,A=255) -LimeGreen=(R=50,G=205,B=50,A=255) -Lime=(R=0,G=255,B=0,A=255) -LawnGreen=(R=124,G=252,B=0,A=255) -Chartreuse=(R=127,G=255,B=0,A=255) -GreenYellow=(R=173,G=255,B=47,A=255) -SpringGreen=(R=0,G=255,B=127,A=255) -MediumSpringGreen=(R=0,G=250,B=154,A=255) -LightGreen=(R=144,G=238,B=144,A=255) -PaleGreen=(R=152,G=251,B=152,A=255) -DarkSeaGreen=(R=143,G=188,B=143,A=255) -MediumAquamarine=(R=102,G=205,B=170,A=255) -MediumSeaGreen=(R=60,G=179,B=113,A=255) -SeaGreen=(R=46,G=139,B=87,A=255) -ForestGreen=(R=34,G=139,B=34,A=255) -Green=(R=0,G=128,B=0,A=255) -DarkGreen=(R=0,G=100,B=0,A=255) -Aqua=(R=0,G=255,B=255,A=255) -Cyan=(R=0,G=255,B=255,A=255) -LightCyan=(R=224,G=255,B=255,A=255) -PaleTurquoise=(R=175,G=238,B=238,A=255) -Aquamarine=(R=127,G=255,B=212,A=255) -Turquoise=(R=64,G=224,B=208,A=255) -MediumTurquoise=(R=72,G=209,B=204,A=255) -DarkTurquoise=(R=0,G=206,B=209,A=255) -LightSeaGreen=(R=32,G=178,B=170,A=255) -CadetBlue=(R=95,G=158,B=160,A=255) -DarkCyan=(R=0,G=139,B=139,A=255) -Teal=(R=0,G=128,B=128,A=255) -LightSteelBlue=(R=176,G=196,B=222,A=255) -PowderBlue=(R=176,G=224,B=230,A=255) -LightBlue=(R=173,G=216,B=230,A=255) -SkyBlue=(R=135,G=206,B=235,A=255) -LightSkyBlue=(R=135,G=206,B=250,A=255) -DeepSkyBlue=(R=0,G=191,B=255,A=255) -DodgerBlue=(R=30,G=144,B=255,A=255) -CornflowerBlue=(R=100,G=149,B=237,A=255) -SteelBlue=(R=70,G=130,B=180,A=255) -RoyalBlue=(R=65,G=105,B=225,A=255) -Blue=(R=0,G=0,B=255,A=255) -MediumBlue=(R=0,G=0,B=205,A=255) -DarkBlue=(R=0,G=0,B=139,A=255) -Navy=(R=0,G=0,B=128,A=255) -MidnightBlue=(R=25,G=25,B=112,A=255) -Lavender=(R=230,G=230,B=250,A=255) -Thistle=(R=216,G=191,B=216,A=255) -Plum=(R=221,G=160,B=221,A=255) -Violet=(R=238,G=130,B=238,A=255) -Orchid=(R=218,G=112,B=214,A=255) -Fuchsia=(R=255,G=0,B=255,A=255) -Magenta=(R=255,G=0,B=255,A=255) -MediumOrchid=(R=186,G=85,B=211,A=255) -MediumPurple=(R=147,G=112,B=219,A=255) -BlueViolet=(R=138,G=43,B=226,A=255) -DarkViolet=(R=148,G=0,B=211,A=255) -DarkOrchid=(R=153,G=50,B=204,A=255) -DarkMagenta=(R=139,G=0,B=139,A=255) -Purple=(R=128,G=0,B=128,A=255) -Indigo=(R=75,G=0,B=130,A=255) -DarkSlateBlue=(R=72,G=61,B=139,A=255) -SlateBlue=(R=106,G=90,B=205,A=255) -MediumSlateBlue=(R=123,G=104,B=238,A=255) -White=(R=255,G=255,B=255,A=255) -Snow=(R=255,G=250,B=250,A=255) -Honeydew=(R=240,G=255,B=240,A=255) -MintCream=(R=245,G=255,B=250,A=255) -Azure=(R=240,G=255,B=255,A=255) -AliceBlue=(R=240,G=248,B=255,A=255) -GhostWhite=(R=248,G=248,B=255,A=255) -WhiteSmoke=(R=245,G=245,B=245,A=255) -Seashell=(R=255,G=245,B=238,A=255) -Beige=(R=245,G=245,B=220,A=255) -OldLace=(R=253,G=245,B=230,A=255) -FloralWhite=(R=255,G=250,B=240,A=255) -Ivory=(R=255,G=255,B=240,A=255) -AntiqueWhite=(R=250,G=235,B=215,A=255) -Linen=(R=250,G=240,B=230,A=255) -LavenderBlush=(R=255,G=240,B=245,A=255) -MistyRose=(R=255,G=228,B=225,A=255) -Gainsboro=(R=220,G=220,B=220,A=255) -LightGray=(R=211,G=211,B=211,A=255) -Silver=(R=192,G=192,B=192,A=255) -DarkGray=(R=169,G=169,B=169,A=255) -Gray=(R=128,G=128,B=128,A=255) -DimGray=(R=105,G=105,B=105,A=255) -LightSlateGray=(R=119,G=136,B=153,A=255) -SlateGray=(R=112,G=128,B=144,A=255) -DarkSlateGray=(R=47,G=79,B=79,A=255) -Eigengrau=(R=22,G=22,B=29,A=255) -Black=(R=0,G=0,B=0,A=255) \ No newline at end of file diff --git a/docs/Aliases.md b/docs/Aliases.md deleted file mode 100644 index 8bbdc63..0000000 --- a/docs/Aliases.md +++ /dev/null @@ -1,138 +0,0 @@ -# Aliases - -Aliases are `string` values that act as human-readable synonyms to some other `string` values. - -Often, when using some console commands, users are forced to type into exact class names of objects in **UnrealScript** (e.g., commands to give someone an M14EBR take form similar to `mutate give KFmod.M14EBRBattleRifle`), but such names can be cumbersome to remember and type. - -Aliases solve this problem by allowing players to instead type `mutate give $ebr`, where `$` denotes that following word `ebr` is an alias that will be automatically resolved into `KFmod.M14EBRBattleRifle`. - -## Alias names - -Alias can be any `string` consisting of ASCII character, although for practical reasons it is better to use only letters, digits and `_` character. Otherwise using them might become more difficult, partially defeating their purpose. - -Aliases are case-insensitive, so `EBR`, `Ebr` and `ebr` are all considered the same alias. - -## Alias sources - -Sources essentially act as aliases databases: matching each alias to some value. They can be used to separate aliases that describe different categories of objects: weapons, zeds, colors, etc.. - -Inside each source aliases and their values are expected to be in many-to-one relationship: many aliases can mean the same value, but each alias can only mean one value. However, two different sources can each contain the same alias and make it point to different values. So it's important for the game to know what source contains what type of aliases. - -In case there are several aliases with the same name in the database, - **Acedia** will warn you about it, but won't actually remove duplicates, instead letting the source use the first it finds. - -By default **Acedia** offers 4 different alias sources: - -* `WeaponAliasSource` (*AcediaAliases_Weapons.ini*) - source filled with aliases for weapons (by default contains aliases to every vanilla weapon); -* `ColorAliasSource` (*AcediaAliases_Colors.ini*) - source filled with aliases for colors (by default contains a decent amount of pre-defined colors); -* `AliasSource` (*AcediaAliases.ini*) - unused source that can, nevertheless, be utilized by server admins or other packages (by default empty); -* `MockAliasSource` (*AcediaAliases_Tests.ini*) - source that is used for testing whether aliases functionality works correctly, avoid changing it if you intend to run tests for **Acedia**'s functionality. - -### [Advanced] Changing meaning of alias sources - -Even though some of the above sources have rather specific names, only use of `MockAliasSource` is hardcoded: admins can, in theory, move all aliases into any source they like. They'll just have to tell **Acedia** where to look for them by changing *AcediaSystem.ini*'s section *Acedia.AliasService* to point at appropriate source: - -```ini -weaponAliasesSource=Class'Acedia.WeaponAliasSource' -colorAliasesSource=Class'Acedia.ColorAliasSource' -``` - -Specifically, you can move all aliases to a single source (for example `AliasSource`) and tell **Acedia** to look for weapon and color aliases there: - -```ini -weaponAliasesSource=Class'Acedia.AliasSource' -colorAliasesSource=Class'Acedia.AliasSource' -``` - -## How sources are stored - -Alias sources are stored in appropriate *ini*-files in two ways that can be mixed with each other however you like. - -### 1. Flat array `record` - -First way is to define a set alias-value pairs in section of the alias source. Example from the color alias source: - -```ini -[Acedia.ColorAliasSource] -; Pink colors -record=(alias="Pink",value="rgb(255,192,203)") -record=(alias="LightPink",value="rgb(255,182,193)") -record=(alias="HotPink",value="rgb(255,105,180)") -record=(alias="DeepPink",value="rgb(255,20,147)") -record=(alias="PaleVioletRed",value="rgb(219,112,147)") -record=(alias="MediumVioletRed",value="rgb(199,21,133)") -``` - -If you want several different aliases to point to the same value, just add a record for each of them: - -```ini -record=(alias="Pink",value="rgb(255,192,203)") -record=(alias="Punk",value="rgb(255,192,203)") -record=(alias="Bunk",value="rgb(255,192,203)") -``` - -Just avoid having several records for the same alias in one source. - -### 2. Per-object-config - -If you need to define several aliases for one value it might be better to use per-object-configuration with named objects: each of them stores an array of aliases, while the corresponding value is recorded as object's name. Example from weapons alias source: - -```ini -[KFMod:MP5MMedicGun WeaponAliases] -Alias="MP5M" -Alias="MP5" -Alias="MP" -Alias="M5" -``` - -Here aliases are defined in every line that starts with `Alias=`. Their value `KFMod:MP5MMedicGun` is defined as a first part of the config section (`:` is going to be translated to `.`, more on that below) and the second part `WeaponAliases` indicates that this is a record for `WeaponAliasSource`. - -Each source has it's own identification for per-object-config records: - -* For `WeaponAliasSource` it is `WeaponAliases`; -* For `ColorAliasSource` it is `ColorAliases`; -* For `MockAliasSource` it is `MockAliases`; -* For `AliasSource` it is just `Aliases`. - -#### Limitations of the second way - -Because alias' value must be a part of the *ini*-file section there are certain limitations imposed on what that value can be (for example having `.` or `]` inside value's name will confuse **Unreal Engine**'s config parser, so you can't use them). There is not official, complete list of forbidden characters, but it is suggested you keep them limited to sequence of letters, numbers and `_` character. - -If you do need to store some weird string as a value, - first test that it does load correctly and, if not, use the first way to define it's aliases. - -But `.` being a forbidden symbol is too harsh of a limitation, since we mainly want to store class names via per-object-configs. Because of that any alias values defined the second way will load `:` as `.` from a config. This change allows us to define classes as values at the cost of preventing the use of `:`. - -## [Technical] Defining new alias sources - -If you make a module using **Acedia** and want to add another alias source you simply need to decide on the names of your: - -* Alias source (suppose it's `NewSource`); -* Helper class for second way (*per-object-config*) of defining aliases (suppose it's `NewAliases`) -* Config file, where their data will be stored (suppose it's `MyNewAliases.ini`); - -then create two classes, like that: - -```java -class NewSource extends AliasSource - config(MyNewAliases); - -defaultproperties -{ - configName = "MyNewAliases" - aliasesClass = class'NewAliases' -} -``` - -```java -class NewAliases extends Aliases - perObjectConfig - config(MyNewAliases); - -defaultproperties -{ - sourceClass = class'NewSource' -} -``` - -and put them in your manifest. - -For more examples check out source code for `ColorAliasSource`, `WeaponAliasSource`, `MockAliasSource`. diff --git a/docs/Colors.md b/docs/Colors.md deleted file mode 100644 index 94b1481..0000000 --- a/docs/Colors.md +++ /dev/null @@ -1,16 +0,0 @@ -# Colors - -The main, and possibly only, notable thing abotu **Acedia**'s colors is it's support for parsing their text representation. To be precise, **Acedia** understands: - -1. Hex color definitions in format of `#ffc0cb`; -2. RGB color definitions that look like either `rgb(255,192,203)` or `rgb(r=255,g=192,b=203)`; -3. RGBA color definitions that look like either `rgb(255,192,203,13)` or `rgb(r=255,g=192,b=203,a=13)`; -4. Alias color definitions that **Acedia** looks up from color-specific alias source and look like any other alias reference: `$pink`. - -You should be able to use any form you like while working with **Acedia**. - -## [Technical] Color fixing - -Killing floor's standard methods of rendering colored `string`s make use of inserting 4-byte sequence into them: first bytes denotes the start of the sequence, 3 following bytes denote rgb color components. Unfortunately these methods also have issues with rendering `string`s if you specify certain values (`0` and `10`) as red-green-blue color components. - -You can freely use colors with these components, since **Acedia** automatically should fix them for you (by replacing them with indistinguishably close, but valid color) whenever it matters. diff --git a/sources/Core/AcediaActor.uc b/sources/Core/AcediaActor.uc deleted file mode 100644 index ce51fee..0000000 --- a/sources/Core/AcediaActor.uc +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Actor base class to be used to Acedia instead of an `Actor`. - * The only difference is defined `_` member that provides convenient access to - * Acedia's API. - * It isn't guaranteed that `default._` will be defined for `AcediaActor`s. - * 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 AcediaActor extends Actor - abstract; - -var protected Global _; - -public final function Text T(string string) -{ - return _.text.FromString(string); -} - -event PreBeginPlay() -{ - super.PreBeginPlay(); - if (_ == none) - { - _ = Global(class'Global'.static.GetInstance()); - default._ = _; - } -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/AcediaObject.uc b/sources/Core/AcediaObject.uc deleted file mode 100644 index 2d451cf..0000000 --- a/sources/Core/AcediaObject.uc +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Object base class to be used to Acedia instead of an `Object`. - * The only difference is defined `_` member that provides convenient access to - * Acedia's API. - * Since `Global` is an actor, we wish to avoid storing it's instance in - * the object because it can mess with garbage collection on level change. - * So we provide an accessor function `_()` instead. - * 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 AcediaObject extends Object - abstract; - -public final function Text T(string string) -{ - return _().text.FromString(string); -} - -public static final function Global _() -{ - return Global(class'Global'.static.GetInstance()); -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/AcediaReplicationInfo.uc b/sources/Core/AcediaReplicationInfo.uc deleted file mode 100644 index 74141ed..0000000 --- a/sources/Core/AcediaReplicationInfo.uc +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Facilitates some core replicated functions between client and server. - * Copyright 2019 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 AcediaReplicationInfo extends ReplicationInfo; - -var public PlayerController linkOwner; - -replication -{ - reliable if (role == ROLE_Authority) - linkOwner; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Aliases/AliasHash.uc b/sources/Core/Aliases/AliasHash.uc deleted file mode 100644 index 43da820..0000000 --- a/sources/Core/Aliases/AliasHash.uc +++ /dev/null @@ -1,218 +0,0 @@ -/** - * A class, implementing a hash-table-based dictionary for quick access to - * aliases' values. - * It does not support dynamic hash table capacity change and - * requires to set the size upfront. - * 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 AliasHash extends AcediaObject - dependson(AliasSource) - config(AcediaSystem); - -// Reasonable lower and upper limits on hash table capacity, -// that will be enforced if user requires something outside those bounds -var private config const int MINIMUM_CAPACITY; -var private config const int MAXIMUM_CAPACITY; - -// Bucket of alias-value pairs, with the same alias hash. -struct PairBucket -{ - var array pairs; -}; -var private array hashTable; - -/** - * Initializes caller `AliasHash`. - * - * Calling this function again will clear all existing data and will create - * a brand new hash table. - * - * @param desiredCapacity Desired capacity of the underlying hash table. - * Will be clamped between `MINIMUM_CAPACITY` and `MAXIMUM_CAPACITY`. - * Not specifying anything as this parameter creates a hash table of - * size `MINIMUM_CAPACITY`. - * @return A reference to a caller object to allow for function chaining. - */ -public final function AliasHash Initialize(optional int desiredCapacity) -{ - desiredCapacity = Clamp(desiredCapacity, MINIMUM_CAPACITY, - MAXIMUM_CAPACITY); - hashTable.length = 0; - hashTable.length = desiredCapacity; - return self; -} - -// Helper method that is needed as a replacement for `%`, since it is -// an operation on `float`s in UnrealScript and does not have enough precision -// to work with hashes. -// Assumes positive input. -private function int Remainder(int number, int divisor) -{ - local int quotient; - quotient = number / divisor; - return (number - quotient * divisor); -} - -// Finds indices for: -// 1. Bucked that contains specified alias (`bucketIndex`); -// 2. Pair for specified alias in the bucket's collection (`pairIndex`). -// `bucketIndex` is always found, -// `pairIndex` is valid iff method returns `true`. -private final function bool FindPairIndices( - string alias, - out int bucketIndex, - out int pairIndex) -{ - local int i; - local array bucketPairs; - // `Locs()` is used because aliases are case-insensitive. - bucketIndex = _().text.GetHash(Locs(alias)); - if (bucketIndex < 0) { - bucketIndex *= -1; - } - bucketIndex = Remainder(bucketIndex, hashTable.length); - // Check if bucket actually has given alias. - bucketPairs = hashTable[bucketIndex].pairs; - for (i = 0; i < bucketPairs.length; i += 1) - { - if (bucketPairs[i].alias ~= alias) - { - pairIndex = i; - return true; - } - } - return false; -} - -/** - * Finds a value for a given alias. - * - * @param alias Alias for which we need to find a value. - * Aliases are case-insensitive. - * @param value If given alias is present in caller `AliasHash`, - - * it's value will be written in this variable. - * Otherwise value is undefined. - * @return `true` if we found value, `false` otherwise. - */ -public final function bool Find(string alias, out string value) -{ - local int bucketIndex; - local int pairIndex; - if (FindPairIndices(alias, bucketIndex, pairIndex)) - { - value = hashTable[bucketIndex].pairs[pairIndex].value; - return true; - } - return false; -} - -/** - * Checks if caller `AliasHash` contains given alias. - * - * @param alias Alias to check for belonging to caller `AliasHash`. - * Aliases are case-insensitive. - * @return `true` if caller `AliasHash` contains the value for a given alias - * and `false` otherwise. - */ -public final function bool Contains(string alias) -{ - local int bucketIndex; - local int pairIndex; - return FindPairIndices(alias, bucketIndex, pairIndex); -} - -/** - * Inserts new record for alias `alias` for value of `value`. - * - * If there is already a value for a given `alias` - it will be overwritten. - * - * @param alias Alias to insert. Aliases are case-insensitive. - * @param value Value for a given alias to store. - * @return A reference to a caller object to allow for function chaining. - */ -public final function AliasHash Insert(string alias, string value) -{ - local int bucketIndex; - local int pairIndex; - local AliasSource.AliasValuePair newRecord; - newRecord.value = value; - newRecord.alias = alias; - if (!FindPairIndices(alias, bucketIndex, pairIndex)) { - pairIndex = hashTable[bucketIndex].pairs.length; - } - hashTable[bucketIndex].pairs[pairIndex] = newRecord; - return self; -} - -/** - * Inserts new record for alias `alias` for value of `value`. - * - * If there is already a value for a given `alias`, - new value will be - * discarded and `AliasHash` will not be changed. - * - * @param alias Alias to insert. Aliases are case-insensitive. - * @param value Value for a given alias to store. - * @param existingValue Value that will correspond to a given alias after - * this method's execution. If insertion was successful - given `value`, - * otherwise (if there already was a record for an `alias`) - * it will return value that already existed in caller `AliasHash`. - * @return `true` if given alias-value pair was inserted and `false` otherwise. - */ -public final function bool InsertIfMissing( - string alias, - string value, - out string existingValue) -{ - local int bucketIndex; - local int pairIndex; - local AliasSource.AliasValuePair newRecord; - newRecord.value = value; - newRecord.alias = alias; - existingValue = value; - if (FindPairIndices(alias, bucketIndex, pairIndex)) { - existingValue = hashTable[bucketIndex].pairs[pairIndex].value; - return false; - } - pairIndex = hashTable[bucketIndex].pairs.length; - hashTable[bucketIndex].pairs[pairIndex] = newRecord; - return true; -} - -/** - * Removes record, corresponding to a given alias `alias`. - * - * @param alias Alias for which all records must be removed. - * @return `true` if record was removed, `false` if id did not - * (can only happen when `AliasHash` did not have any records for `alias`). - */ -public final function bool Remove(string alias) -{ - local int bucketIndex; - local int pairIndex; - if (FindPairIndices(alias, bucketIndex, pairIndex)) { - hashTable[bucketIndex].pairs.Remove(pairIndex, 1); - return true; - } - return false; -} - -defaultproperties -{ - MINIMUM_CAPACITY = 10 - MAXIMUM_CAPACITY = 100000 -} \ No newline at end of file diff --git a/sources/Core/Aliases/AliasService.uc b/sources/Core/Aliases/AliasService.uc deleted file mode 100644 index ac9cd94..0000000 --- a/sources/Core/Aliases/AliasService.uc +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Service that handles pending saving of aliases data into configs. - * Adding aliases into `AliasSource`s causes corresponding configs to update. - * This service allows to delay and spread config rewrites over time, - * which should help in case someone dynamically adds a lot of - * different aliases. - * 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 AliasService extends Service - config(AcediaSystem); - -// Objects for which we are yet to write configs -var private array sourcesPendingToSave; -var private array aliasesPendingToSave; -// How often should we do it. -// Negative or zero values would be reset to `0.05`. -var public config const float saveInterval; - -// To avoid creating yet another object for aliases system we will -// keep config variable pointing to weapon, color, etc. `AliasSource` -// subclasses here. It's not the best regarding separation of responsibility, -// but should make config files less fragmented. -// Changing these allows you to change in what sources `AliasesAPI` -// looks for weapon and color aliases. -var public config const class weaponAliasesSource; -var public config const class colorAliasesSource; - -protected function OnLaunch() -{ - local float actualInterval; - actualInterval = saveInterval; - if (actualInterval <= 0) - { - actualInterval = 0.05; - } - SetTimer(actualInterval, true); -} - -protected function OnShutdown() -{ - SaveAllPendingObjects(); -} - -public final function PendingSaveSource(AliasSource sourceToSave) -{ - local int i; - if (sourceToSave == none) return; - // Starting searching from the end of an array will make situations when - // we add several aliases to a single source in a row more efficient. - for (i = sourcesPendingToSave.length - 1;i >= 0; i -= 1) { - if (sourcesPendingToSave[i] == sourceToSave) return; - } - sourcesPendingToSave[sourcesPendingToSave.length] = sourceToSave; -} - -public final function PendingSaveObject(Aliases objectToSave) -{ - local int i; - if (objectToSave == none) return; - // Starting searching from the end of an array will make situations when - // we add several aliases to a single `Aliases` object in a row - // more efficient. - for (i = aliasesPendingToSave.length - 1;i >= 0; i -= 1) { - if (aliasesPendingToSave[i] == objectToSave) return; - } - aliasesPendingToSave[aliasesPendingToSave.length] = objectToSave; -} - -/** - * Forces saving of the next object (either `AliasSource` or `Aliases`) - * in queue to the config file. - * - * Does not reset the timer until next saving. - */ -private final function DoSaveNextPendingObject() -{ - if (sourcesPendingToSave.length > 0) - { - if (sourcesPendingToSave[0] != none) { - sourcesPendingToSave[0].SaveConfig(); - } - sourcesPendingToSave.Remove(0, 1); - return; - } - if (aliasesPendingToSave.length > 0) - { - aliasesPendingToSave[0].SaveOrClear(); - aliasesPendingToSave.Remove(0, 1); - } -} - -/** - * Forces saving of all objects (both `AliasSource`s or `Aliases`s) in queue - * to their config files. - */ -private final function SaveAllPendingObjects() -{ - local int i; - for (i = 0; i < sourcesPendingToSave.length; i += 1) { - if (sourcesPendingToSave[i] == none) continue; - sourcesPendingToSave[i].SaveConfig(); - } - for (i = 0; i < aliasesPendingToSave.length; i += 1) { - aliasesPendingToSave[i].SaveOrClear(); - } - sourcesPendingToSave.length = 0; - aliasesPendingToSave.length = 0; -} - -event Timer() -{ - DoSaveNextPendingObject(); -} - -defaultproperties -{ - saveInterval = 0.05 - weaponAliasesSource = class'WeaponAliasSource' - colorAliasesSource = class'ColorAliasSource' -} \ No newline at end of file diff --git a/sources/Core/Aliases/AliasSource.uc b/sources/Core/Aliases/AliasSource.uc deleted file mode 100644 index 6f863ea..0000000 --- a/sources/Core/Aliases/AliasSource.uc +++ /dev/null @@ -1,379 +0,0 @@ -/** - * Aliases allow users to define human-readable and easier to use - * "synonyms" to some symbol sequences (mainly names of UnrealScript classes). - * This class implements an alias database that stores aliases inside - * standard config ini-files. - * Several `AliasSource`s are supposed to exist separately, each storing - * aliases of particular kind: for weapon, zeds, colors, etc.. - * 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 AliasSource extends Singleton - config(AcediaAliases); - -// Name of the configurational file (without extension) where -// this `AliasSource`'s data will be stored. -var private const string configName; - -// (Sub-)class of `Aliases` objects that this `AliasSource` uses to store -// aliases in per-object-config manner. -// Leaving this variable `none` will produce an `AliasSource` that can -// only store aliases in form of `record=(alias="...",value="...")`. -var public const class aliasesClass; -// Storage for all objects of `aliasesClass` class in the config. -// Exists after `OnCreated()` event and is maintained up-to-date at all times. -var private array loadedAliasObjects; - -// Links alias to a value. -// An array of these structures (without duplicate `alias` records) defines -// a function from the space of aliases to the space of values. -struct AliasValuePair -{ - var string alias; - var string value; -}; -// Aliases data for saving and loading on a disk (ini-file). -// Name is chosen to make configurational files more readable. -var private config array record; -// Hash table for a faster access to value by alias' name. -// It contains same records as `record` array + aliases from -// `loadedAliasObjects` objects when there are no duplicate aliases. -// Otherwise only stores first loaded alias. -var private AliasHash hash; - - -// How many times bigger capacity of `hash` should be, compared to amount of -// initially loaded data from a config. -var private const float HASH_TABLE_SCALE; - -// Load and hash all the data `AliasSource` creation. -protected function OnCreated() -{ - local int entriesAmount; - if (!AssertAliasesClassIsOwnedByMe()) { - return; - } - // Load and hash - entriesAmount = LoadData(); - hash = AliasHash(_.memory.Allocate(class'AliasHash')); - hash.Initialize(int(entriesAmount * HASH_TABLE_SCALE)); - HashValidAliases(); -} - -// Ensures invariant of our `Aliases` class only belonging to us by -// itself ourselves otherwise. -private final function bool AssertAliasesClassIsOwnedByMe() -{ - 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."); - Destroy(); - return false; -} - -// This method loads all the defined aliases from the config file and -// returns how many entries are there are total. -// Does not change data, including fixing duplicates. -private final function int LoadData() -{ - local int i; - local int entriesAmount; - local array objectNames; - entriesAmount = record.length; - if (aliasesClass == none) { - return entriesAmount; - } - objectNames = - GetPerObjectNames(configName, string(aliasesClass.name), MaxInt); - loadedAliasObjects.length = objectNames.length; - for (i = 0; i < objectNames.length; i += 1) - { - loadedAliasObjects[i] = new(none, objectNames[i]) aliasesClass; - entriesAmount += loadedAliasObjects[i].GetAliases().length; - } - return entriesAmount; -} - -/** - * Simply checks if given alias is present in caller `AliasSource`. - * - * @param alias Alias to check, case-insensitive. - * @return `true` if present, `false` otherwise. - */ -public function bool ContainsAlias(string alias) -{ - return hash.Contains(alias); -} - -/** - * Tries to look up a value, stored for given alias in caller `AliasSource` and - * reports error upon failure. - * - * Also see `Try()` method. - * - * @param alias Alias, for which method will attempt to look up a value. - * Case-insensitive. - * @param value If passed `alias` was recorded in caller `AliasSource`, - * it's corresponding value will be written in this variable. - * Otherwise value is undefined. - * @return `true` if lookup was successful (alias present in 'AliasSource`) - * and correct value was written into `value`, `false` otherwise. - */ -public function bool Resolve(string alias, out string value) -{ - return hash.Find(alias, value); -} - -/** - * Tries to look up a value, stored for given alias in caller `AliasSource` and - * silently returns given `alias` value upon failure. - * - * Also see `Resolve()` method. - * - * @param alias Alias, for which method will attempt to look up a value. - * Case-insensitive. - * @return Value corresponding to a given alias, if it was present in - * caller `AliasSource` and value of `alias` parameter instead. - */ -public function string Try(string alias) -{ - local string result; - if (hash.Find(alias, result)) { - return result; - } - return alias; -} - -/** - * Adds another alias to the caller `AliasSource`. - * If alias with the same name as `aliasToAdd` already exists, - - * method overwrites it. - * - * Can fail iff `aliasToAdd` is an invalid alias. - * - * When adding alias to an object (`saveInObject == true`) alias `aliasToAdd` - * will be altered by changing any ':' inside it into a '.'. - * This is a necessary measure to allow storing class names in - * config files via per-object-config. - * - * NOTE: This call will cause update of an ini-file. That update can be - * slightly delayed, so do not make assumptions about it's immediacy. - * - * NOTE #2: Removing alias would require this method to go through the - * whole `AliasSource` to remove possible duplicates. - * This means that unless you can guarantee that there is no duplicates, - - * performing a lot of alias additions during run-time can be costly. - * - * @param aliasToAdd Alias that you want to add to caller source. - * Alias names are case-insensitive. - * @param aliasValue Intended value of this alias. - * @param saveInObject Setting this to `true` will make `AliasSource` save - * given alias in per-object-config storage, while keeping it at default - * `false` will just add alias to the `record=` storage. - * If caller `AliasSource` does not support per-object-config storage, - - * this flag will be ignores. - * @return `true` if alias was added and `false` otherwise (alias was invalid). - */ -public final function bool AddAlias( - string aliasToAdd, - string aliasValue, - optional bool saveInObject) -{ - local AliasValuePair newPair; - if (_.alias.IsAliasValid(aliasToAdd)) { - return false; - } - if (hash.Contains(aliasToAdd)) { - RemoveAlias(aliasToAdd); - } - // We might not be able to use per-object-config storage - if (saveInObject && aliasesClass == none) { - saveInObject = false; - _.logger.Warning("Cannot save alias in object for source `" - $ string(class) - $ "`, because it does not have appropriate `Aliases` class setup."); - } - // Save - if (saveInObject) { - GetAliasesObjectWithValue(aliasValue).AddAlias(aliasToAdd); - } - else - { - newPair.alias = aliasToAdd; - newPair.value = aliasValue; - record[record.length] = newPair; - } - hash.Insert(aliasToAdd, aliasValue); - AliasService(class'AliasService'.static.Require()).PendingSaveSource(self); - return true; -} - -/** - * Removes alias (all records with it, in case of duplicates) from - * the caller `AliasSource`. - * - * Cannot fail. - * - * NOTE: This call will cause update of an ini-file. That update can be - * slightly delayed, so do not make assumptions about it's immediacy. - * - * NOTE #2: removing alias requires this method to go through the - * whole `AliasSource` to remove possible duplicates, which can make - * performing a lot of alias removal during run-time costly. - * - * @param aliasToRemove Alias that you want to remove from caller source. - */ -public final function RemoveAlias(string aliasToRemove) -{ - local int i; - local bool removedAliasFromRecord; - hash.Remove(aliasToRemove); - while (i < record.length) - { - if (record[i].alias ~= aliasToRemove) - { - record.Remove(i, 1); - removedAliasFromRecord = true; - } - else { - i += 1; - } - } - for (i = 0; i < loadedAliasObjects.length; i += 1) { - loadedAliasObjects[i].RemoveAlias(aliasToRemove); - } - if (removedAliasFromRecord) - { - AliasService(class'AliasService'.static.Require()) - .PendingSaveSource(self); - } -} - -// Performs initial hashing of every record with valid alias. -// In case of duplicate or invalid aliases - method will skip them -// and log warnings. -private final function HashValidAliases() -{ - if (hash == none) { - _.logger.Warning("Alias source `" $ string(class) $ "` called" - $ "`HashValidAliases()` function without creating an `AliasHasher`" - $ "instance first. This should not have happened."); - return; - } - HashValidAliasesFromRecord(); - HashValidAliasesFromPerObjectConfig(); -} - -private final function LogDuplicateAliasWarning( - string alias, - string existingValue) -{ - _.logger.Warning("Alias source `" $ string(class) - $ "` has duplicate record for alias \"" $ alias - $ "\". This is likely due to an erroneous config. \"" $ existingValue - $ "\" value will be used."); -} - -private final function LogInvalidAliasWarning(string invalidAlias) -{ - _.logger.Warning("Alias source `" $ string(class) - $ "` contains invalid alias name \"" $ invalidAlias - $ "\". This alias will not be loaded."); -} - -private final function HashValidAliasesFromRecord() -{ - local int i; - local bool isDuplicate; - local string existingValue; - for (i = 0; i < record.length; i += 1) - { - if (!_.alias.IsAliasValid(record[i].alias)) - { - LogInvalidAliasWarning(record[i].alias); - continue; - } - isDuplicate = !hash.InsertIfMissing(record[i].alias, record[i].value, - existingValue); - if (isDuplicate) { - LogDuplicateAliasWarning(record[i].alias, existingValue); - } - } -} - -private final function HashValidAliasesFromPerObjectConfig() -{ - local int i, j; - local bool isDuplicate; - local string existingValue; - local string objectValue; - local array objectAliases; - for (i = 0; i < loadedAliasObjects.length; i += 1) - { - objectValue = loadedAliasObjects[i].GetValue(); - objectAliases = loadedAliasObjects[i].GetAliases(); - for (j = 0; j < objectAliases.length; j += 1) - { - if (!_.alias.IsAliasValid(objectAliases[j])) - { - LogInvalidAliasWarning(objectAliases[j]); - continue; - } - isDuplicate = !hash.InsertIfMissing(objectAliases[j], objectValue, - existingValue); - if (isDuplicate) { - LogDuplicateAliasWarning(objectAliases[j], existingValue); - } - } - } -} - -// Tries to find a loaded `Aliases` config object that stores aliases for -// the given value. If such object does not exists - creates a new one. -private final function Aliases GetAliasesObjectWithValue(string value) -{ - local int i; - local Aliases newAliasesObject; - // This method only makes sense if this `AliasSource` supports - // per-object-config storage. - if (aliasesClass == none) - { - _.logger.Warning("`GetAliasesObjectForValue()` function was called for " - $ "alias source with `aliasesClass == none`." - $ "This should not happen."); - return none; - } - for (i = 0; i < loadedAliasObjects.length; i += 1) - { - if (loadedAliasObjects[i].GetValue() ~= value) { - return loadedAliasObjects[i]; - } - } - newAliasesObject = new(none, value) aliasesClass; - loadedAliasObjects[loadedAliasObjects.length] = newAliasesObject; - return newAliasesObject; -} - -defaultproperties -{ - // Source main parameters - configName = "AcediaAliases" - aliasesClass = class'Aliases' - // HashTable twice the size of data entries should do it - HASH_TABLE_SCALE = 2.0 -} \ No newline at end of file diff --git a/sources/Core/Aliases/Aliases.uc b/sources/Core/Aliases/Aliases.uc deleted file mode 100644 index 8b30683..0000000 --- a/sources/Core/Aliases/Aliases.uc +++ /dev/null @@ -1,142 +0,0 @@ -/** - * This is a simple helper object for `AliasSource` that can store - * an array of aliases in config files in a per-object-config manner. - * One `Aliases` object can store several aliases for a single value. - * It is recommended that you do not try to access these objects directly. - * Class name `Aliases` is chosen to make configuration files - * more readable. - * It's only interesting function is storing '.'s as ':' in it's config, - * which is necessary to allow storing aliases for class names via - * these objects (since UnrealScript's cannot handle '.'s in object's names - * in it's configs). - * 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 Aliases extends AcediaObject - perObjectConfig - config(AcediaAliases); - -// Link to the `AliasSource` that uses `Aliases` objects of this class. -// To ensure that any `Aliases` sub-class only belongs to one `AliasSource`. -var public const class sourceClass; - -// Aliases, recorded by this `Aliases` object that all mean the same value, -// defined by this object's name `string(self.name)`. -var protected config array alias; - -// Since '.'s in values are converted into ':' for storage purposes, -// we need methods to convert between "storage" and "actual" value version. -// `ToStorageVersion()` and `ToActualVersion()` do that. -private final function string ToStorageVersion(string actualValue) -{ - return Repl(actualValue, ".", ":"); -} - -private final function string ToActualVersion(string storageValue) -{ - return Repl(storageValue, ":", "."); -} - -/** - * Returns value that caller's `Aliases` object's aliases point to. - * - * @return Value, stored by this object. - */ -public final function string GetValue() -{ - return ToActualVersion(string(self.name)); -} - -/** - * Returns array of aliases that caller `Aliases` tells us point to it's value. - * - * @return Array of all aliases, stored by caller `Aliases` object. - */ -public final function array GetAliases() -{ - return alias; -} - -/** - * [For inner use by `AliasSource`] Adds new alias to this object. - * - * Does no duplicates checks through for it's `AliasSource` and - * neither it updates relevant `AliasHash`, - * but will prevent adding duplicate records inside it's own storage. - * - * @param aliasToAdd Alias to add to caller `Aliases` object. - */ -public final function AddAlias(string aliasToAdd) -{ - local int i; - for (i = 0; i < alias.length; i += 1) { - if (alias[i] ~= aliasToAdd) return; - } - alias[alias.length] = ToStorageVersion(aliasToAdd); - AliasService(class'AliasService'.static.Require()) - .PendingSaveObject(self); -} - -/** - * [For inner use by `AliasSource`] Removes alias from this object. - * - * Does not update relevant `AliasHash`. - * - * Will prevent adding duplicate records inside it's own storage. - * - * @param aliasToRemove Alias to remove from caller `Aliases` object. - */ -public final function RemoveAlias(string aliasToRemove) -{ - local int i; - local bool removedAlias; - while (i < alias.length) - { - if (alias[i] ~= aliasToRemove) - { - alias.Remove(i, 1); - removedAlias = true; - } - else { - i += 1; - } - } - if (removedAlias) - { - AliasService(class'AliasService'.static.Require()) - .PendingSaveObject(self); - } -} - -/** - * If this object still has any alias records, - forces a rewrite of it's data - * into the config file, otherwise - removes it's record entirely. - */ -public final function SaveOrClear() -{ - if (alias.length <= 0) { - ClearConfig(); - } - else { - SaveConfig(); - } -} - -defaultproperties -{ - sourceClass = class'AliasSource' -} \ No newline at end of file diff --git a/sources/Core/Aliases/AliasesAPI.uc b/sources/Core/Aliases/AliasesAPI.uc deleted file mode 100644 index d231640..0000000 --- a/sources/Core/Aliases/AliasesAPI.uc +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Provides convenient access to Aliases-related functions. - * Copyright 2019 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 AliasesAPI extends Singleton; - -/** - * Checks that passed value is a valid alias name. - * - * A valid name is any name consisting out of 128 ASCII symbols. - * - * @param aliasToCheck Alias to check for validity. - * @return `true` if `aliasToCheck` is a valid alias and `false` otherwise. - */ -public final function bool IsAliasValid(string aliasToCheck) -{ - return _.text.IsASCIIString(aliasToCheck); -} - -/** - * Provides an easier access to the instance of the `AliasSource` of - * the given class. - * - * Can fail if `customSourceClass` is incorrectly defined. - * - * @param customSourceClass Class of the source we want. - * @return Instance of the requested `AliasSource`, - * `none` if `customSourceClass` is incorrectly defined. - */ -public final function AliasSource GetCustomSource( - class customSourceClass) -{ - return AliasSource(customSourceClass.static.GetInstance(true)); -} - -/** - * Returns `AliasSource` that is designated in configuration files as - * a source for weapon aliases. - * - * NOTE: while by default weapon aliases source will contain only weapon - * aliases, you should not assume that. Acedia allows admins to store all - * the aliases in the same config. - * - * @return Reference to the `AliasSource` that contains weapon aliases. - * Can return `none` if no source for weapons was configured or - * the configured source is incorrectly defined. - */ -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."); - 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`."); - return none; - } - return weaponSource; -} - -/** - * Returns `AliasSource` that is designated in configuration files as - * a source for color aliases. - * - * NOTE: while by default color aliases source will contain only color aliases, - * you should not assume that. Acedia allows admins to store all the aliases - * in the same config. - * - * @return Reference to the `AliasSource` that contains color aliases. - * Can return `none` if no source for colors was configured or - * the configured source is incorrectly defined. - */ -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."); - 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`."); - return none; - } - return colorSource; -} - -/** - * Tries to look up a value, stored for given alias in an `AliasSource` - * configured to store weapon aliases. Reports error on failure. - * - * Lookup of alias can fail if either alias does not exist in weapon alias - * source or weapon alias source itself does not exist - * (due to either faulty configuration or incorrect definition). - * To determine if weapon alias source exists you can check - * `_.alias.GetWeaponSource()` value. - * - * Also see `TryWeapon()` method. - * - * @param alias Alias, for which method will attempt to look up a value. - * Case-insensitive. - * @param value If passed `alias` was recorded as a weapon alias, - * it's corresponding value will be written in this variable. - * Otherwise value is undefined. - * @return `true` if lookup was successful and `false` otherwise. - */ -public final function bool ResolveWeapon(string alias, out string result) -{ - local AliasSource source; - source = GetWeaponSource(); - if (source != none) { - return source.Resolve(alias, result); - } - return false; -} - -/** - * Tries to look up a value, stored for given alias in an `AliasSource` - * configured to store weapon aliases and silently returns given `alias` - * value upon failure. - * - * Lookup of alias can fail if either alias does not exist in weapon alias - * source or weapon alias source itself does not exist - * (due to either faulty configuration or incorrect definition). - * To determine if weapon alias source exists you can check - * `_.alias.GetWeaponSource()` value. - * - * Also see `ResolveWeapon()` method. - * - * @param alias Alias, for which method will attempt to look up a value. - * Case-insensitive. - * @return Weapon value corresponding to a given alias, if it was present in - * the weapon alias source and value of `alias` parameter instead. - */ -public function string TryWeapon(string alias) -{ - local AliasSource source; - source = GetWeaponSource(); - if (source != none) { - return source.Try(alias); - } - return alias; -} - -/** - * Tries to look up a value, stored for given alias in an `AliasSource` - * configured to store color aliases. Reports error on failure. - * - * Lookup of alias can fail if either alias does not exist in color alias - * source or color alias source itself does not exist - * (due to either faulty configuration or incorrect definition). - * To determine if color alias source exists you can check - * `_.alias.GetColorSource()` value. - * - * Also see `TryColor()` method. - * - * @param alias Alias, for which method will attempt to look up a value. - * Case-insensitive. - * @param value If passed `alias` was recorded as a color alias, - * it's corresponding value will be written in this variable. - * Otherwise value is undefined. - * @return `true` if lookup was successful and `false` otherwise. - */ -public final function bool ResolveColor(string alias, out string result) -{ - local AliasSource source; - source = GetColorSource(); - if (source != none) { - return source.Resolve(alias, result); - } - return false; -} - -/** - * Tries to look up a value, stored for given alias in an `AliasSource` - * configured to store color aliases and silently returns given `alias` - * value upon failure. - * - * Lookup of alias can fail if either alias does not exist in color alias - * source or color alias source itself does not exist - * (due to either faulty configuration or incorrect definition). - * To determine if color alias source exists you can check - * `_.alias.GetColorSource()` value. - * - * Also see `ResolveColor()` method. - * - * @param alias Alias, for which method will attempt to look up a value. - * Case-insensitive. - * @return Color value corresponding to a given alias, if it was present in - * the color alias source and value of `alias` parameter instead. - */ -public function string TryColor(string alias) -{ - local AliasSource source; - source = GetColorSource(); - if (source != none) { - return source.Try(alias); - } - return alias; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc b/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc deleted file mode 100644 index 5cd75cf..0000000 --- a/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Source intended for color aliases. - * 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 ColorAliasSource extends AliasSource - config(AcediaAliases_Colors); - -defaultproperties -{ - configName = "AcediaAliases_Colors" - aliasesClass = class'ColorAliases' -} \ No newline at end of file diff --git a/sources/Core/Aliases/BuiltInSources/ColorAliases.uc b/sources/Core/Aliases/BuiltInSources/ColorAliases.uc deleted file mode 100644 index d0998b6..0000000 --- a/sources/Core/Aliases/BuiltInSources/ColorAliases.uc +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Per-object-configuration intended for color aliases. - * 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 ColorAliases extends Aliases - perObjectConfig - config(AcediaAliases_Colors); - -defaultproperties -{ - sourceClass = class'ColorAliasSource' -} \ No newline at end of file diff --git a/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc b/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc deleted file mode 100644 index 0cf1bc4..0000000 --- a/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Source intended for weapon aliases. - * 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 WeaponAliasSource extends AliasSource - config(AcediaAliases_Weapons); - -defaultproperties -{ - configName = "AcediaAliases_Weapons" - aliasesClass = class'WeaponAliases' -} \ No newline at end of file diff --git a/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc b/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc deleted file mode 100644 index 82acd45..0000000 --- a/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Per-object-configuration intended for weapon aliases. - * 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 WeaponAliases extends Aliases - perObjectConfig - config(AcediaAliases_Weapons); - -defaultproperties -{ - sourceClass = class'WeaponAliasSource' -} \ No newline at end of file diff --git a/sources/Core/Aliases/Tests/MockAliasSource.uc b/sources/Core/Aliases/Tests/MockAliasSource.uc deleted file mode 100644 index d724501..0000000 --- a/sources/Core/Aliases/Tests/MockAliasSource.uc +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Source intended for testing aliases. - * 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 MockAliasSource extends AliasSource - config(AcediaAliases_Tests); - -defaultproperties -{ - configName = "AcediaAliases_Tests" - aliasesClass = class'MockAliases' -} \ No newline at end of file diff --git a/sources/Core/Aliases/Tests/MockAliases.uc b/sources/Core/Aliases/Tests/MockAliases.uc deleted file mode 100644 index 83eeef3..0000000 --- a/sources/Core/Aliases/Tests/MockAliases.uc +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Per-object-configuration intended for testing aliases. - * 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 MockAliases extends Aliases - perObjectConfig - config(AcediaAliases_Tests); - -defaultproperties -{ - sourceClass = class'MockAliasSource' -} \ No newline at end of file diff --git a/sources/Core/Aliases/Tests/TEST_Aliases.uc b/sources/Core/Aliases/Tests/TEST_Aliases.uc deleted file mode 100644 index be51505..0000000 --- a/sources/Core/Aliases/Tests/TEST_Aliases.uc +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Set of tests for Aliases system. - * 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 TEST_Aliases extends TestCase - abstract; - -protected static function TESTS() -{ - Test_AliasHash(); - Test_AliasLoading(); -} - -protected static function Test_AliasLoading() -{ - Context("Testing loading aliases from a mock object `MockAliasSource`."); - SubTest_AliasLoadingCorrect(); - SubTest_AliasLoadingIncorrect(); -} - -protected static function SubTest_AliasLoadingCorrect() -{ - local AliasSource source; - local string outValue; - - Issue("`Resolve()` fails to return alias that should be loaded."); - source = _().alias.GetCustomSource(class'MockAliasSource'); - TEST_ExpectTrue(source.Resolve("Global", outValue)); - TEST_ExpectTrue(outValue == "value"); - TEST_ExpectTrue(source.Resolve("ford", outValue)); - TEST_ExpectTrue(outValue == "car"); - - Issue("`Try()` fails to return alias that should be loaded."); - TEST_ExpectTrue(source.Try("question") == "response"); - TEST_ExpectTrue(source.Try("delorean") == "car"); - - Issue("`ContainsAlias()` reports alias, that should be present," - @ "as missing."); - TEST_ExpectTrue(source.ContainsAlias("Global")); - TEST_ExpectTrue(source.ContainsAlias("audi")); - - Issue("Aliases in per-object-configs incorrectly handle ':'."); - TEST_ExpectTrue(source.Try("HardToBeAGod") == "sci.fi"); - - Issue("Aliases with empty values in alias name or their value are handled" - @ "incorrectly."); - TEST_ExpectTrue(source.Try("") == "empty"); - TEST_ExpectTrue(source.Try("also") == ""); -} - -protected static function SubTest_AliasLoadingIncorrect() -{ - local AliasSource source; - local string outValue; - Context("Testing loading aliases from a mock object `MockAliasSource`."); - Issue("`AliasAPI` cannot return value custom source."); - source = _().alias.GetCustomSource(class'MockAliasSource'); - TEST_ExpectNotNone(source); - - Issue("`Resolve()` reports success of finding inexistent alias."); - source = _().alias.GetCustomSource(class'MockAliasSource'); - TEST_ExpectFalse(source.Resolve("noSuchThing", outValue)); - - Issue("`Try()` does not return given value for non-existent alias."); - TEST_ExpectTrue(source.Try("TheHellIsThis") == "TheHellIsThis"); - - Issue("`ContainsAlias()` reports inexistent alias as present."); - TEST_ExpectFalse(source.ContainsAlias("FordК")); -} - -protected static function Test_AliasHash() -{ - Context("Testing `AliasHasher`."); - SubTest_AliasHashInsertingRemoval(); -} - -protected static function SubTest_AliasHashInsertingRemoval() -{ - local AliasHash hasher; - local string outValue; - hasher = new class'AliasHash'; - hasher.Initialize(); - Issue("`AliasHash` cannot properly store added aliases."); - hasher.Insert("alias", "value").Insert("one", "more"); - TEST_ExpectTrue(hasher.Contains("alias")); - TEST_ExpectTrue(hasher.Contains("one")); - TEST_ExpectTrue(hasher.Find("alias", outValue)); - TEST_ExpectTrue(outValue == "value"); - TEST_ExpectTrue(hasher.Find("one", outValue)); - TEST_ExpectTrue(outValue == "more"); - - Issue("`AliasHash` reports hashing aliases that never were hashed."); - TEST_ExpectFalse(hasher.Contains("alia")); - - Issue("`AliasHash` cannot properly remove stored aliases."); - hasher.Remove("alias"); - TEST_ExpectFalse(hasher.Contains("alias")); - TEST_ExpectTrue(hasher.Contains("one")); - TEST_ExpectFalse(hasher.Find("alias", outValue)); - outValue = "wrong"; - TEST_ExpectTrue(hasher.Find("one", outValue)); - TEST_ExpectTrue(outValue == "more"); - - Issue("`InsertIfMissing()` function cannot properly store added aliases."); - TEST_ExpectTrue(hasher.InsertIfMissing("another", "var", outValue)); - TEST_ExpectTrue(hasher.Find("another", outValue)); - TEST_ExpectTrue(outValue == "var"); - - Issue("`InsertIfMissing()` function incorrectly resolves a conflict with" - @ "an existing value."); - TEST_ExpectFalse(hasher.InsertIfMissing("one", "something", outValue)); - TEST_ExpectTrue(outValue == "more"); -} - -defaultproperties -{ - caseName = "Aliases" -} \ No newline at end of file diff --git a/sources/Core/Color/ColorAPI.uc b/sources/Core/Color/ColorAPI.uc deleted file mode 100644 index 6a72cb0..0000000 --- a/sources/Core/Color/ColorAPI.uc +++ /dev/null @@ -1,812 +0,0 @@ -/** - * API that provides functions for working with color. - * It has a wide range of pre-defined colors and some functions that - * allow to quickly assemble color from rgb(a) values, parse it from - * a `Text`/string or load it from an alias. - * 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 ColorAPI extends Singleton - dependson(Parser) - config(AcediaSystem); - -/** - * Enumeration for ways to represent `Color` as a `string`. - */ -enum ColorDisplayType -{ - // Hex format; for pink: #ffc0cb - CLRDISPLAY_HEX, - // RGB format; for pink: rgb(255,192,203) - CLRDISPLAY_RGB, - // RGBA format; for opaque pink: rgb(255,192,203,255) - CLRDISPLAY_RGBA, - // RGB format with tags; for pink: rgb(r=255,g=192,b=203) - CLRDISPLAY_RGB_TAG, - // RGBA format with tags; for pink: rgb(r=255,g=192,b=203,a=255) - CLRDISPLAY_RGBA_TAG -}; - -// Some useful predefined color values. -// They are marked as `config` to allow server admins to mess about with -// colors if they want to. -// Pink colors -var public config const Color Pink; -var public config const Color LightPink; -var public config const Color HotPink; -var public config const Color DeepPink; -var public config const Color PaleVioletRed; -var public config const Color MediumVioletRed; -// Red colors -var public config const Color LightSalmon; -var public config const Color Salmon; -var public config const Color DarkSalmon; -var public config const Color LightCoral; -var public config const Color IndianRed; -var public config const Color Crimson; -var public config const Color Firebrick; -var public config const Color DarkRed; -var public config const Color Red; -// Orange colors -var public config const Color OrangeRed; -var public config const Color Tomato; -var public config const Color Coral; -var public config const Color DarkOrange; -var public config const Color Orange; -// Yellow colors -var public config const Color Yellow; -var public config const Color LightYellow; -var public config const Color LemonChiffon; -var public config const Color LightGoldenrodYellow; -var public config const Color PapayaWhip; -var public config const Color Moccasin; -var public config const Color PeachPuff; -var public config const Color PaleGoldenrod; -var public config const Color Khaki; -var public config const Color DarkKhaki; -var public config const Color Gold; -// Brown colors -var public config const Color Cornsilk; -var public config const Color BlanchedAlmond; -var public config const Color Bisque; -var public config const Color NavajoWhite; -var public config const Color Wheat; -var public config const Color Burlywood; -var public config const Color TanColor; // `Tan()` already taken by a function -var public config const Color RosyBrown; -var public config const Color SandyBrown; -var public config const Color Goldenrod; -var public config const Color DarkGoldenrod; -var public config const Color Peru; -var public config const Color Chocolate; -var public config const Color SaddleBrown; -var public config const Color Sienna; -var public config const Color Brown; -var public config const Color Maroon; -// Green colors -var public config const Color DarkOliveGreen; -var public config const Color Olive; -var public config const Color OliveDrab; -var public config const Color YellowGreen; -var public config const Color LimeGreen; -var public config const Color Lime; -var public config const Color LawnGreen; -var public config const Color Chartreuse; -var public config const Color GreenYellow; -var public config const Color SpringGreen; -var public config const Color MediumSpringGreen; -var public config const Color LightGreen; -var public config const Color PaleGreen; -var public config const Color DarkSeaGreen; -var public config const Color MediumAquamarine; -var public config const Color MediumSeaGreen; -var public config const Color SeaGreen; -var public config const Color ForestGreen; -var public config const Color Green; -var public config const Color DarkGreen; -// Cyan colors -var public config const Color Aqua; -var public config const Color Cyan; -var public config const Color LightCyan; -var public config const Color PaleTurquoise; -var public config const Color Aquamarine; -var public config const Color Turquoise; -var public config const Color MediumTurquoise; -var public config const Color DarkTurquoise; -var public config const Color LightSeaGreen; -var public config const Color CadetBlue; -var public config const Color DarkCyan; -var public config const Color Teal; -// Blue colors -var public config const Color LightSteelBlue; -var public config const Color PowderBlue; -var public config const Color LightBlue; -var public config const Color SkyBlue; -var public config const Color LightSkyBlue; -var public config const Color DeepSkyBlue; -var public config const Color DodgerBlue; -var public config const Color CornflowerBlue; -var public config const Color SteelBlue; -var public config const Color RoyalBlue; -var public config const Color Blue; -var public config const Color MediumBlue; -var public config const Color DarkBlue; -var public config const Color Navy; -var public config const Color MidnightBlue; -// Purple, violet, and magenta colors -var public config const Color Lavender; -var public config const Color Thistle; -var public config const Color Plum; -var public config const Color Violet; -var public config const Color Orchid; -var public config const Color Fuchsia; -var public config const Color Magenta; -var public config const Color MediumOrchid; -var public config const Color MediumPurple; -var public config const Color BlueViolet; -var public config const Color DarkViolet; -var public config const Color DarkOrchid; -var public config const Color DarkMagenta; -var public config const Color Purple; -var public config const Color Indigo; -var public config const Color DarkSlateBlue; -var public config const Color SlateBlue; -var public config const Color MediumSlateBlue; -// White colors -var public config const Color White; -var public config const Color Snow; -var public config const Color Honeydew; -var public config const Color MintCream; -var public config const Color Azure; -var public config const Color AliceBlue; -var public config const Color GhostWhite; -var public config const Color WhiteSmoke; -var public config const Color Seashell; -var public config const Color Beige; -var public config const Color OldLace; -var public config const Color FloralWhite; -var public config const Color Ivory; -var public config const Color AntiqueWhite; -var public config const Color Linen; -var public config const Color LavenderBlush; -var public config const Color MistyRose; -// Gray and black colors -var public config const Color Gainsboro; -var public config const Color LightGray; -var public config const Color Silver; -var public config const Color DarkGray; -var public config const Color Gray; -var public config const Color DimGray; -var public config const Color LightSlateGray; -var public config const Color SlateGray; -var public config const Color DarkSlateGray; -var public config const Color Eigengrau; -var public config const Color Black; - -// Escape code point is used to change output's color and is used in -// Unreal Engine's `string`s. -var private const int CODEPOINT_ESCAPE; -var private const int CODEPOINT_SMALL_A; - -/** - * Creates opaque color from (red, green, blue) triplet. - * - * @param red Red component, range from 0 to 255. - * @param green Green component, range from 0 to 255. - * @param blue Blue component, range from 0 to 255. - * @return `Color` with specified red, green and blue component and - * alpha component of `255`. - */ -public final function Color RGB(byte red, byte green, byte blue) -{ - local Color result; - result.r = red; - result.g = green; - result.b = blue; - result.a = 255; - return result; -} - -/** - * Creates color from (red, green, blue, alpha) quadruplet. - * - * @param red Red component, range from 0 to 255. - * @param green Green component, range from 0 to 255. - * @param blue Blue component, range from 0 to 255. - * @param alpha Alpha component, range from 0 to 255. - * @return `Color` with specified red, green, blue and alpha component. - */ -public final function Color RGBA(byte red, byte green, byte blue, byte alpha) -{ - local Color result; - result.r = red; - result.g = green; - result.b = blue; - result.a = alpha; - return result; -} - -/** - * Compares two colors for exact equality of red, green and blue components. - * Alpha component is ignored. - * - * @param color1 Color to compare - * @param color2 Color to compare - * @return `true` if colors' red, green and blue components are equal - * and `false` otherwise. - */ -public final function bool AreEqual(Color color1, Color color2, optional bool fixColors) -{ - if (fixColors) { - color1 = FixColor(color1); - color2 = FixColor(color2); - } - if (color1.r != color2.r) return false; - if (color1.g != color2.g) return false; - if (color1.b != color2.b) return false; - return true; -} - -/** - * Compares two colors for exact equality of red, green, blue - * and alpha components. - * - * @param color1 Color to compare - * @param color2 Color to compare - * @return `true` if colors' red, green, blue and alpha components are equal - * and `false` otherwise. - */ -public final function bool AreEqualWithAlpha(Color color1, Color color2, optional bool fixColors) -{ - if (fixColors) { - color1 = FixColor(color1); - color2 = FixColor(color2); - } - if (color1.r != color2.r) return false; - if (color1.g != color2.g) return false; - if (color1.b != color2.b) return false; - if (color1.a != color2.a) return false; - return true; -} - -/** - * Killing floor's standard methods of rendering colored `string`s - * make use of inserting 4-byte sequence into them: first bytes denotes - * the start of the sequence, 3 following bytes denote rgb color components. - * Unfortunately these methods also have issues with rendering `string`s - * if you specify certain values (`0` and `10`) of rgb color components. - * - * This function "fixes" components by replacing them with close and valid - * color component values (adds `1` to the component). - */ -public final function byte FixColorComponent(byte colorComponent) -{ - if (colorComponent == 0 || colorComponent == 10) - { - return colorComponent + 1; - } - return colorComponent; -} - -/** - * Killing floor's standard methods of rendering colored `string`s - * make use of inserting 4-byte sequence into them: first bytes denotes - * the start of the sequence, 3 following bytes denote rgb color components. - * Unfortunately these methods also have issues with rendering `string`s - * if you specify certain values (`0` and `10`) as rgb color components. - * - * This function "fixes" given `Color`'s components by replacing them with - * close and valid color values (using `FixColorComponent()` method), - * resulting in a `Color` that looks almost the same, but is suitable to be - * included into 4-byte color change sequence. - * - * Since alpha component is never used in color-change sequences, - * it is never affected. - */ -public final function Color FixColor(Color colorToFix) -{ - colorToFix.r = FixColorComponent(colorToFix.r); - colorToFix.g = FixColorComponent(colorToFix.g); - colorToFix.b = FixColorComponent(colorToFix.b); - return colorToFix; -} - -/** - * Returns 4-gyte sequence for color change to a given color. - * - * To make returned tag work in most sequences, the value of given color is - * auto "fixed" (see `FixColor()` for details). - * There is an option to skip color fixing, but method will still change - * `0` components to `1`, since they cannot otherwise be used in a tag at all. - * - * Also see `GetColorTagRGB()`. - * - * @param colorToUse Color to which tag must change the text. - * It's alpha value (`colorToUse.a`) is discarded. - * @param doNotFixComponents Minimizes changes to color components - * (only allows to change `0` components to `1` before creating a tag). - * @return `string` containing 4-byte sequence that will swap text's color to - * a given one in standard Unreal Engine's UI. - */ -public final function string GetColorTag( - Color colorToUse, - optional bool doNotFixComponents) -{ - if (!doNotFixComponents) { - colorToUse = FixColor(colorToUse); - } - colorToUse.r = Max(1, colorToUse.r); - colorToUse.g = Max(1, colorToUse.g); - colorToUse.b = Max(1, colorToUse.b); - return Chr(CODEPOINT_ESCAPE) - $ Chr(colorToUse.r) - $ Chr(colorToUse.g) - $ Chr(colorToUse.b); -} - -/** - * Returns 4-gyte sequence for color change to a given color. - * - * To make returned tag work in most sequences, the value of given color is - * auto "fixed" (see `FixColor()` for details). - * There is an option to skip color fixing, but method will still change - * `0` components to `1`, since they cannot otherwise be used in a tag at all. - * - * Also see `GetColorTag()`. - * - * @param red Red component of color to which tag must - * change the text. - * @param green Green component of color to which tag must - * change the text. - * @param blue Blue component of color to which tag must - * change the text. - * @param doNotFixComponents Minimizes changes to color components - * (only allows to change `0` components to `1` before creating a tag). - * @return `string` containing 4-byte sequence that will swap text's color to - * a given one in standard Unreal Engine's UI. - */ -public final function string GetColorTagRGB( - int red, - int green, - int blue, - optional bool doNotFixComponents) -{ - if (!doNotFixComponents) - { - red = FixColorComponent(red); - green = FixColorComponent(green); - blue = FixColorComponent(blue); - } - red = Max(1, red); - green = Max(1, green); - blue = Max(1, blue); - return Chr(CODEPOINT_ESCAPE) $ Chr(red) $ Chr(green) $ Chr(blue); -} - -// Helper function that converts `byte` with values between 0 and 15 into -// a corresponding hex letter -private final function string ByteToHexCharacter(byte component) -{ - component = Clamp(component, 0, 15); - if (component < 10) { - return string(component); - } - return Chr(component - 10 + CODEPOINT_SMALL_A); -} - -// `byte` to `string` in hex -private final function string ComponentToHex(byte component) -{ - local byte high4Bits, low4Bits; - low4Bits = component % 16; - if (component >= 16) { - high4Bits = (component - low4Bits) / 16; - } - else { - high4Bits = 0; - } - return ByteToHexCharacter(high4Bits) $ ByteToHexCharacter(low4Bits); -} - -/** - * Displays given color as a string in a given style - * (hex color representation by default). - * - * @param colorToConvert Color to display as a `string`. - * @param displayType `enum` value, describing how should color - * be displayed. - * @return `string` representation of a given color in a given style. - */ -public final function string ToStringType( - Color colorToConvert, - optional ColorDisplayType displayType) -{ - if (displayType == CLRDISPLAY_HEX) { - return "#" $ ComponentToHex(colorToConvert.r) - $ ComponentToHex(colorToConvert.g) - $ ComponentToHex(colorToConvert.b); - } - else if (displayType == CLRDISPLAY_RGB) - { - return "rgb(" $ string(colorToConvert.r) $ "," - $ string(colorToConvert.g) $ "," - $ string(colorToConvert.b) $ ")"; - } - else if (displayType == CLRDISPLAY_RGBA) - { - return "rgba(" $ string(colorToConvert.r) $ "," - $ string(colorToConvert.g) $ "," - $ string(colorToConvert.b) $ "," - $ string(colorToConvert.a) $ ")"; - } - else if (displayType == CLRDISPLAY_RGB_TAG) - { - return "rgb(r=" $ string(colorToConvert.r) $ "," - $ "g=" $ string(colorToConvert.g) $ "," - $ "b=" $ string(colorToConvert.b) $ ")"; - } - //else if (displayType == CLRDISPLAY_RGBA_TAG) - return "rgba(r=" $ string(colorToConvert.r) $ "," - $ "g=" $ string(colorToConvert.g) $ "," - $ "b=" $ string(colorToConvert.b) $ "," - $ "a=" $ string(colorToConvert.a) $ ")"; -} - -/** - * Displays given color as a string in RGB or RGBA format, depending on - * whether color is opaque. - * - * @param colorToConvert Color to display as a `string` in `CLRDISPLAY_RGB` - * style if `colorToConvert.a == 255` and `CLRDISPLAY_RGBA` otherwise. - * @return `string` representation of a given color in a given style. - */ -public final function string ToString(Color colorToConvert) -{ - if (colorToConvert.a < 255) { - return ToStringType(colorToConvert, CLRDISPLAY_RGBA); - } - return ToStringType(colorToConvert, CLRDISPLAY_RGB); -} - -// Parses color in `CLRDISPLAY_RGB` and `CLRDISPLAY_RGB_TAG` representations. -private final function Color ParseRGB(Parser parser) -{ - local int redComponent; - local int greenComponent; - local int blueComponent; - local Parser.ParserState initialParserState; - initialParserState = parser.GetCurrentState(); - parser.Match("rgb(", true) - .MInteger(redComponent).Match(",") - .MInteger(greenComponent).Match(",") - .MInteger(blueComponent).Match(")"); - if (!parser.Ok()) - { - parser.RestoreState(initialParserState).Match("rgb(", true) - .Match("r=", true).MInteger(redComponent).Match(",") - .Match("g=", true).MInteger(greenComponent).Match(",") - .Match("b=", true).MInteger(blueComponent).Match(")"); - } - return RGB(redComponent, greenComponent, blueComponent); -} - -// Parses color in `CLRDISPLAY_RGBA` and `CLRDISPLAY_RGBA_TAG` representations. -private final function Color ParseRGBA(Parser parser) -{ - local int redComponent; - local int greenComponent; - local int blueComponent; - local int alphaComponent; - local Parser.ParserState initialParserState; - initialParserState = parser.GetCurrentState(); - parser.Match("rgba(", true) - .MInteger(redComponent).Match(",") - .MInteger(greenComponent).Match(",") - .MInteger(blueComponent).Match(",") - .MInteger(alphaComponent).Match(")"); - if (!parser.Ok()) - { - parser.RestoreState(initialParserState).Match("rgba(", true) - .Match("r=", true).MInteger(redComponent).Match(",") - .Match("g=", true).MInteger(greenComponent).Match(",") - .Match("b=", true).MInteger(blueComponent).Match(",") - .Match("a=", true).MInteger(alphaComponent).Match(")"); - } - return RGBA(redComponent, greenComponent, blueComponent, alphaComponent); -} - -// Parses color in `CLRDISPLAY_HEX` representation. -private final function Color ParseHexColor(Parser parser) -{ - local int redComponent; - local int greenComponent; - local int blueComponent; - parser.Match("#") - .MUnsignedInteger(redComponent, 16, 2) - .MUnsignedInteger(greenComponent, 16, 2) - .MUnsignedInteger(blueComponent, 16, 2); - return RGB(redComponent, greenComponent, blueComponent); -} - -/** - * Uses given parser to try and parse a color in any of the - * `ColorDisplayType` representations. - * - * @param parser Parser that method would use to parse color from - * wherever it left. It's confirmed state will not be changed. - * Do not treat `parser` bein in a non-failed state as a confirmation of - * successful parsing: color parsing might fail regardless. - * Check return value for that. - * @param resultingColor Parsed color will be written here if parsing is - * successful, otherwise value is undefined. - * If parsed color did not specify alpha component - 255 will be used. - * @return `true` if parsing was successful and false otherwise. - */ -public final function bool ParseWith(Parser parser, out Color resultingColor) -{ - local bool successfullyParsed; - local string colorAlias; - local Parser colorParser; - local Parser.ParserState initialParserState; - if (parser == none) return false; - resultingColor.a = 0xff; - colorParser = parser; - initialParserState = parser.GetCurrentState(); - if (parser.Match("$").MUntil(colorAlias,, true).Ok()) - { - colorParser = _.text.ParseString(_.alias.TryColor(colorAlias)); - initialParserState = colorParser.GetCurrentState(); - } - else { - parser.RestoreState(initialParserState); - } - resultingColor = ParseRGB(colorParser); - if (!colorParser.Ok()) - { - colorParser.RestoreState(initialParserState); - resultingColor = ParseRGBA(colorParser); - } - if (!colorParser.Ok()) - { - colorParser.RestoreState(initialParserState); - resultingColor = ParseHexColor(colorParser); - } - successfullyParsed = colorParser.Ok(); - if (colorParser != parser) { - _.memory.Free(colorParser); - } - return successfullyParsed; -} - -/** - * Parses a color in any of the `ColorDisplayType` representations from the - * beginning of a given `string`. - * - * @param stringWithColor String, that contains color definition at - * the beginning. Anything after color definition is not used. - * @param resultingColor Parsed color will be written here if parsing is - * successful, otherwise value is undefined. - * If parsed color did not specify alpha component - 255 will be used. - * @param stringType How to treat given `string`, - * see `StringType` for more details. - * @return `true` if parsing was successful and false otherwise. - */ -public final function bool ParseString( - string stringWithColor, - out Color resultingColor, - optional Text.StringType stringType) -{ - local bool successfullyParsed; - local Parser colorParser; - colorParser = _.text.ParseString(stringWithColor, stringType); - successfullyParsed = ParseWith(colorParser, resultingColor); - _.memory.Free(colorParser); - return successfullyParsed; -} - -/** - * Parses a color in any of the `ColorDisplayType` representations from the - * beginning of a given `Text`. - * - * @param textWithColor `Text`, that contains color definition at - * the beginning. Anything after color definition is not used. - * @param resultingColor Parsed color will be written here if parsing is - * successful, otherwise value is undefined. - * If parsed color did not specify alpha component - 255 will be used. - * @return `true` if parsing was successful and false otherwise. - */ -public final function bool ParseText( - Text textWithColor, - out Color resultingColor) -{ - local bool successfullyParsed; - local Parser colorParser; - colorParser = _.text.Parse(textWithColor); - successfullyParsed = ParseWith(colorParser, resultingColor); - _.memory.Free(colorParser); - return successfullyParsed; -} - -/** - * Parses a color in any of the `ColorDisplayType` representations from the - * beginning of a given raw data. - * - * @param rawDataWithColor Raw data, that contains color definition at - * the beginning. Anything after color definition is not used. - * @param resultingColor Parsed color will be written here if parsing is - * successful, otherwise value is undefined. - * If parsed color did not specify alpha component - 255 will be used. - * @return `true` if parsing was successful and false otherwise. - */ -public final function bool ParseRaw( - array rawDataWithColor, - out Color resultingColor) -{ - local bool successfullyParsed; - local Parser colorParser; - colorParser = _.text.ParseRaw(rawDataWithColor); - successfullyParsed = ParseWith(colorParser, resultingColor); - _.memory.Free(colorParser); - return successfullyParsed; -} - -defaultproperties -{ - Pink=(R=255,G=192,B=203,A=255) - LightPink=(R=255,G=182,B=193,A=255) - HotPink=(R=255,G=105,B=180,A=255) - DeepPink=(R=255,G=20,B=147,A=255) - PaleVioletRed=(R=219,G=112,B=147,A=255) - MediumVioletRed=(R=199,G=21,B=133,A=255) - LightSalmon=(R=255,G=160,B=122,A=255) - Salmon=(R=250,G=128,B=114,A=255) - DarkSalmon=(R=233,G=150,B=122,A=255) - LightCoral=(R=240,G=128,B=128,A=255) - IndianRed=(R=205,G=92,B=92,A=255) - Crimson=(R=220,G=20,B=60,A=255) - Firebrick=(R=178,G=34,B=34,A=255) - DarkRed=(R=139,G=0,B=0,A=255) - Red=(R=255,G=0,B=0,A=255) - OrangeRed=(R=255,G=69,B=0,A=255) - Tomato=(R=255,G=99,B=71,A=255) - Coral=(R=255,G=127,B=80,A=255) - DarkOrange=(R=255,G=140,B=0,A=255) - Orange=(R=255,G=165,B=0,A=255) - Yellow=(R=255,G=255,B=0,A=255) - LightYellow=(R=255,G=255,B=224,A=255) - LemonChiffon=(R=255,G=250,B=205,A=255) - LightGoldenrodYellow=(R=250,G=250,B=210,A=255) - PapayaWhip=(R=255,G=239,B=213,A=255) - Moccasin=(R=255,G=228,B=181,A=255) - PeachPuff=(R=255,G=218,B=185,A=255) - PaleGoldenrod=(R=238,G=232,B=170,A=255) - Khaki=(R=240,G=230,B=140,A=255) - DarkKhaki=(R=189,G=183,B=107,A=255) - Gold=(R=255,G=215,B=0,A=255) - Cornsilk=(R=255,G=248,B=220,A=255) - BlanchedAlmond=(R=255,G=235,B=205,A=255) - Bisque=(R=255,G=228,B=196,A=255) - NavajoWhite=(R=255,G=222,B=173,A=255) - Wheat=(R=245,G=222,B=179,A=255) - Burlywood=(R=222,G=184,B=135,A=255) - TanColor=(R=210,G=180,B=140,A=255) - RosyBrown=(R=188,G=143,B=143,A=255) - SandyBrown=(R=244,G=164,B=96,A=255) - Goldenrod=(R=218,G=165,B=32,A=255) - DarkGoldenrod=(R=184,G=134,B=11,A=255) - Peru=(R=205,G=133,B=63,A=255) - Chocolate=(R=210,G=105,B=30,A=255) - SaddleBrown=(R=139,G=69,B=19,A=255) - Sienna=(R=160,G=82,B=45,A=255) - Brown=(R=165,G=42,B=42,A=255) - Maroon=(R=128,G=0,B=0,A=255) - DarkOliveGreen=(R=85,G=107,B=47,A=255) - Olive=(R=128,G=128,B=0,A=255) - OliveDrab=(R=107,G=142,B=35,A=255) - YellowGreen=(R=154,G=205,B=50,A=255) - LimeGreen=(R=50,G=205,B=50,A=255) - Lime=(R=0,G=255,B=0,A=255) - LawnGreen=(R=124,G=252,B=0,A=255) - Chartreuse=(R=127,G=255,B=0,A=255) - GreenYellow=(R=173,G=255,B=47,A=255) - SpringGreen=(R=0,G=255,B=127,A=255) - MediumSpringGreen=(R=0,G=250,B=154,A=255) - LightGreen=(R=144,G=238,B=144,A=255) - PaleGreen=(R=152,G=251,B=152,A=255) - DarkSeaGreen=(R=143,G=188,B=143,A=255) - MediumAquamarine=(R=102,G=205,B=170,A=255) - MediumSeaGreen=(R=60,G=179,B=113,A=255) - SeaGreen=(R=46,G=139,B=87,A=255) - ForestGreen=(R=34,G=139,B=34,A=255) - Green=(R=0,G=128,B=0,A=255) - DarkGreen=(R=0,G=100,B=0,A=255) - Aqua=(R=0,G=255,B=255,A=255) - Cyan=(R=0,G=255,B=255,A=255) - LightCyan=(R=224,G=255,B=255,A=255) - PaleTurquoise=(R=175,G=238,B=238,A=255) - Aquamarine=(R=127,G=255,B=212,A=255) - Turquoise=(R=64,G=224,B=208,A=255) - MediumTurquoise=(R=72,G=209,B=204,A=255) - DarkTurquoise=(R=0,G=206,B=209,A=255) - LightSeaGreen=(R=32,G=178,B=170,A=255) - CadetBlue=(R=95,G=158,B=160,A=255) - DarkCyan=(R=0,G=139,B=139,A=255) - Teal=(R=0,G=128,B=128,A=255) - LightSteelBlue=(R=176,G=196,B=222,A=255) - PowderBlue=(R=176,G=224,B=230,A=255) - LightBlue=(R=173,G=216,B=230,A=255) - SkyBlue=(R=135,G=206,B=235,A=255) - LightSkyBlue=(R=135,G=206,B=250,A=255) - DeepSkyBlue=(R=0,G=191,B=255,A=255) - DodgerBlue=(R=30,G=144,B=255,A=255) - CornflowerBlue=(R=100,G=149,B=237,A=255) - SteelBlue=(R=70,G=130,B=180,A=255) - RoyalBlue=(R=65,G=105,B=225,A=255) - Blue=(R=0,G=0,B=255,A=255) - MediumBlue=(R=0,G=0,B=205,A=255) - DarkBlue=(R=0,G=0,B=139,A=255) - Navy=(R=0,G=0,B=128,A=255) - MidnightBlue=(R=25,G=25,B=112,A=255) - Lavender=(R=230,G=230,B=250,A=255) - Thistle=(R=216,G=191,B=216,A=255) - Plum=(R=221,G=160,B=221,A=255) - Violet=(R=238,G=130,B=238,A=255) - Orchid=(R=218,G=112,B=214,A=255) - Fuchsia=(R=255,G=0,B=255,A=255) - Magenta=(R=255,G=0,B=255,A=255) - MediumOrchid=(R=186,G=85,B=211,A=255) - MediumPurple=(R=147,G=112,B=219,A=255) - BlueViolet=(R=138,G=43,B=226,A=255) - DarkViolet=(R=148,G=0,B=211,A=255) - DarkOrchid=(R=153,G=50,B=204,A=255) - DarkMagenta=(R=139,G=0,B=139,A=255) - Purple=(R=128,G=0,B=128,A=255) - Indigo=(R=75,G=0,B=130,A=255) - DarkSlateBlue=(R=72,G=61,B=139,A=255) - SlateBlue=(R=106,G=90,B=205,A=255) - MediumSlateBlue=(R=123,G=104,B=238,A=255) - White=(R=255,G=255,B=255,A=255) - Snow=(R=255,G=250,B=250,A=255) - Honeydew=(R=240,G=255,B=240,A=255) - MintCream=(R=245,G=255,B=250,A=255) - Azure=(R=240,G=255,B=255,A=255) - AliceBlue=(R=240,G=248,B=255,A=255) - GhostWhite=(R=248,G=248,B=255,A=255) - WhiteSmoke=(R=245,G=245,B=245,A=255) - Seashell=(R=255,G=245,B=238,A=255) - Beige=(R=245,G=245,B=220,A=255) - OldLace=(R=253,G=245,B=230,A=255) - FloralWhite=(R=255,G=250,B=240,A=255) - Ivory=(R=255,G=255,B=240,A=255) - AntiqueWhite=(R=250,G=235,B=215,A=255) - Linen=(R=250,G=240,B=230,A=255) - LavenderBlush=(R=255,G=240,B=245,A=255) - MistyRose=(R=255,G=228,B=225,A=255) - Gainsboro=(R=220,G=220,B=220,A=255) - LightGray=(R=211,G=211,B=211,A=255) - Silver=(R=192,G=192,B=192,A=255) - Gray=(R=169,G=169,B=169,A=255) - DimGray=(R=128,G=128,B=128,A=255) - DarkGray=(R=105,G=105,B=105,A=255) - LightSlateGray=(R=119,G=136,B=153,A=255) - SlateGray=(R=112,G=128,B=144,A=255) - DarkSlateGray=(R=47,G=79,B=79,A=255) - Eigengrau=(R=22,G=22,B=29,A=255) - Black=(R=0,G=0,B=0,A=255) - CODEPOINT_SMALL_A = 97 - CODEPOINT_ESCAPE = 27 -} \ No newline at end of file diff --git a/sources/Core/Color/Tests/TEST_ColorAPI.uc b/sources/Core/Color/Tests/TEST_ColorAPI.uc deleted file mode 100644 index fc21a3d..0000000 --- a/sources/Core/Color/Tests/TEST_ColorAPI.uc +++ /dev/null @@ -1,507 +0,0 @@ -/** - * Set of tests for Color API. - * 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 TEST_ColorAPI extends TestCase - abstract; - -protected static function TESTS() -{ - Test_ColorCreation(); - Test_EqualityCheck(); - Test_ColorFixing(); - Test_ToString(); - Test_Parse(); - Test_GetTag(); -} - -protected static function Test_ColorCreation() -{ - Context("Testing `ColorAPI`'s functions for creating color structures."); - SubTest_ColorCreationRGB(); - SubTest_ColorCreationRGBA(); -} - -protected static function SubTest_ColorCreationRGB() -{ - local Color createdColor; - Issue("`RGB() function does not set red, green and blue components" - @ "correctly."); - createdColor = _().color.RGB(145, 67, 237); - TEST_ExpectTrue(createdColor.r == 145); - TEST_ExpectTrue(createdColor.g == 67); - TEST_ExpectTrue(createdColor.b == 237); - - Issue("`RGB() function does not set alpha component to 255."); - TEST_ExpectTrue(createdColor.a == 255); - - Issue("`RGB() function does not set special values (border values" - @ "`0`, `255` and value `10`, incorrect for coloring a `string`) for" - @"red, green and blue components correctly."); - createdColor = _().color.RGB(0, 10, 255); - TEST_ExpectTrue(createdColor.r == 0); - TEST_ExpectTrue(createdColor.g == 10); - TEST_ExpectTrue(createdColor.b == 255); - - Issue("`RGB() function does not set alpha value to 255."); - TEST_ExpectTrue(createdColor.a == 255); -} - -protected static function SubTest_ColorCreationRGBA() -{ - local Color createdColor; - Issue("`RGBA() function does not set red, green, blue, alpha" - @ "components correctly."); - createdColor = _().color.RGBA(93, 245, 1, 67); - TEST_ExpectTrue(createdColor.r == 93); - TEST_ExpectTrue(createdColor.g == 245); - TEST_ExpectTrue(createdColor.b == 1); - TEST_ExpectTrue(createdColor.a == 67); - - Issue("`RGBA() function does not set special values (border values" - @ "`0`, `255` and value `10`, incorrect for coloring a `string`) for" - @"red, green, blue components correctly."); - createdColor = _().color.RGBA(0, 10, 10, 255); - TEST_ExpectTrue(createdColor.r == 0); - TEST_ExpectTrue(createdColor.g == 10); - TEST_ExpectTrue(createdColor.b == 10); - TEST_ExpectTrue(createdColor.a == 255); -} - -protected static function Test_EqualityCheck() -{ - Context("Testing `ColorAPI`'s functions for color equality check."); - SubTest_EqualityCheckNotFixed(); - SubTest_EqualityCheckFixed(); -} - -protected static function SubTest_EqualityCheckNotFixed() -{ - local Color color1, color2, color3; - color1 = _().color.RGB(45, 10, 19); - color2 = _().color.RGB(45, 11, 19); - color3 = _().color.RGBA(45, 10, 19, 178); - Issue("`AreEqual()` does not recognized equal colors as such."); - TEST_ExpectTrue(_().color.AreEqual(color1, color1)); - - Issue("`AreEqual()` does not recognized colors that differ only in alpha" - @ "channel as equal."); - TEST_ExpectTrue(_().color.AreEqual(color1, color3)); - - Issue("`AreEqual()` does not recognized different colors as such."); - TEST_ExpectFalse(_().color.AreEqual(color1, color2)); - - Issue("`AreEqualWithAlpha()` does not recognized equal colors as such."); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(color1, color1)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(color3, color3)); - - Issue("`AreEqualWithAlpha()` does not recognized different colors" - @ "as such."); - TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color2)); - TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color3)); -} - -protected static function SubTest_EqualityCheckFixed() -{ - local Color color1, color2, color3; - color1 = _().color.RGB(45, 10, 0); - color2 = _().color.RGB(45, 239, 19); - color3 = _().color.RGBA(45, 11, 1, 178); - Issue("`AreEqual()` does not recognized equal colors as such (with color" - @ "auto-fix)."); - TEST_ExpectTrue(_().color.AreEqual(color1, color1, true)); - - Issue("`AreEqual()` does not recognized colors that differ only in alpha" - @ "channel as equal (with color auto-fix)."); - TEST_ExpectTrue(_().color.AreEqual(color1, color3, true)); - - Issue("`AreEqual()` does not recognized different colors as such" - @ "(with color auto-fix)."); - TEST_ExpectFalse(_().color.AreEqual(color1, color2, true)); - - Issue("`AreEqualWithAlpha()` does not recognized equal colors as such" - @ "(with color auto-fix)."); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(color1, color1, true)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(color3, color3, true)); - - Issue("`AreEqualWithAlpha()` does not recognized different colors as such" - @ "(with color auto-fix)."); - TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color2, true)); - TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color3, true)); -} - -protected static function Test_ColorFixing() -{ - local Color validColor, brokenColor; - validColor = _().color.RGB(23, 179, 244); - brokenColor = _().color.RGB(10, 35, 0); - Context("Testing `ColorAPI`'s functions for fixing color components for" - @ "game's native render functions."); - Issue("`FixColorComponent()` does not \"fix\" values it is expected to," - @ "the way it is expected to."); - TEST_ExpectTrue(_().color.FixColorComponent(0) == 1); - TEST_ExpectTrue(_().color.FixColorComponent(10) == 11); - - Issue("`FixColorComponent()` changes values it should not."); - TEST_ExpectTrue(_().color.FixColorComponent(9) == 9); - TEST_ExpectTrue(_().color.FixColorComponent(255) == 255); - TEST_ExpectTrue(_().color.FixColorComponent(87) == 87); - - Issue("`FixColor()` changes colors it should not."); - TEST_ExpectTrue( - _().color.AreEqualWithAlpha(validColor, - _().color.FixColor(validColor))); - - Issue("`FixColor()` doesn't fix color it should fix in an expected way."); - TEST_ExpectTrue( - _().color.AreEqualWithAlpha(_().color.RGB(11, 35, 1), - _().color.FixColor(brokenColor))); - - Issue("`FixColor()` affects alpha channel."); - TEST_ExpectTrue(_().color.FixColor(validColor).a == 255); - validColor.a = 0; - TEST_ExpectTrue(_().color.FixColor(validColor).a == 0); - validColor.a = 10; - TEST_ExpectTrue(_().color.FixColor(validColor).a == 10); -} - -protected static function Test_ToString() -{ - Context("Testing `ColorAPI`'s `ToString()` function."); - SubTest_ToStringType(); - SubTest_ToString(); -} - -protected static function SubTest_ToStringType() -{ - local Color normalColor, borderValueColor; - normalColor = _().color.RGBA(24, 232, 187, 34); - borderValueColor = _().color.RGBA(0, 255, 255, 0); - Issue("`ToStringType()` improperly works with `CLRDISPLAY_HEX` option."); - TEST_ExpectTrue(_().color.ToStringType(normalColor) ~= "#18e8bb"); - TEST_ExpectTrue(_().color.ToStringType(borderValueColor) ~= "#00ffff"); - - Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGB` option."); - TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGB) - ~= "rgb(24,232,187)"); - TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGB) - ~= "rgb(0,255,255)"); - - Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGBA` option."); - TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGBA) - ~= "rgba(24,232,187,34)"); - TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA) - ~= "rgba(0,255,255,0)"); - - Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGB_TAG`" - @ "option."); - TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGB_TAG) - ~= "rgb(r=24,g=232,b=187)"); - TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGB_TAG) - ~= "rgb(r=0,g=255,b=255)"); - - Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGBA_TAG`" - @ "option."); - TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGBA_TAG) - ~= "rgba(r=24,g=232,b=187,a=34)"); - TEST_ExpectTrue( - _().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA_TAG) - ~= "rgba(r=0,g=255,b=255,a=0)"); -} - -protected static function SubTest_ToString() -{ - local Color opaqueColor, transparentColor; - opaqueColor = _().color.RGBA(143, 211, 43, 255); - transparentColor = _().color.RGBA(234, 32, 145, 13); - Issue("`ToString()` improperly converts color with opaque color."); - TEST_ExpectTrue(_().color.ToString(opaqueColor) ~= "rgb(143,211,43)"); - Issue("`ToString()` improperly converts color with transparent color."); - TEST_ExpectTrue(_().color.ToString(transparentColor) - ~= "rgba(234,32,145,13)"); -} - -protected static function Test_GetTag() -{ - Context("Testing `ColorAPI`'s functionality of creating 4-byte color" - @ "change sequences."); - SubTest_GetTagColor(); - SubTest_GetTagRGB(); -} - -protected static function SubTest_GetTagColor() -{ - local Color normalColor, borderColor; - normalColor = _().color.RGB(143, 211, 43); - borderColor = _().color.RGB(10, 0, 255); - Issue("`GetColorTag()` does not properly convert colors."); - TEST_ExpectTrue(_().color.GetColorTag(normalColor) - == (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43))); - TEST_ExpectTrue(_().color.GetColorTag(borderColor) - == (Chr(27) $ Chr(11) $ Chr(1) $ Chr(255))); - - Issue("`GetColorTag()` does not properly convert colors when asked not to" - @ "fix components."); - TEST_ExpectTrue(_().color.GetColorTag(normalColor, true) - == (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43))); - TEST_ExpectTrue(_().color.GetColorTag(borderColor, true) - == (Chr(27) $ Chr(10) $ Chr(1) $ Chr(255))); -} - -protected static function SubTest_GetTagRGB() -{ - Issue("`GetColorTagRGB()` does not properly convert colors."); - TEST_ExpectTrue(_().color.GetColorTagRGB(143, 211, 43) - == (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43))); - TEST_ExpectTrue(_().color.GetColorTagRGB(10, 0, 255) - == (Chr(27) $ Chr(11) $ Chr(1) $ Chr(255))); - - Issue("`GetColorTagRGB()` does not properly convert colors when asked" - @ "not to fix components."); - TEST_ExpectTrue(_().color.GetColorTagRGB(143, 211, 43, true) - == (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43))); - TEST_ExpectTrue(_().color.GetColorTagRGB(10, 0, 255, true) - == (Chr(27) $ Chr(10) $ Chr(1) $ Chr(255))); -} - -protected static function Test_Parse() -{ - Context("Testing `ColorAPI`'s parsing functionality."); - SubTest_ParseWithParser(); - SubTest_ParseStringPlain(); - SubTest_ParseStringColored(); - SubTest_ParseStringFormatted(); - SubTest_ParseText(); - SubTest_ParseRaw(); -} - -protected static function SubTest_ParseWithParser() -{ - local Color expectedColor, resultColor; - expectedColor = _().color.RGBA(154, 255, 0, 187); - Issue("`ParseWith()` cannot parse hex colors."); - TEST_ExpectTrue(_().color.ParseWith(_().text.ParseString("#9aff00"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseWith()` cannot parse rgb colors."); - TEST_ExpectTrue(_().color.ParseWith(_().text.ParseString("rgb(154,255,0)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseWith()` cannot parse rgba colors."); - TEST_ExpectTrue(_().color.ParseWith( - _().text.ParseString("rgba(154,255,0,187)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseWith()` cannot parse rgb colors with tags."); - TEST_ExpectTrue(_().color.ParseWith( - _().text.ParseString("rgb(r=154,g=255,b=0)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseWith()` cannot parse rgba colors with tags."); - TEST_ExpectTrue(_().color.ParseWith( - _().text.ParseString("rgba(r=154,g=255,b=0,a=187)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseWith()` reports success when parsing invalid color string."); - TEST_ExpectFalse(_().color.ParseWith( _().text.ParseString("#9aff0g"), - resultColor)); -} - -protected static function SubTest_ParseStringPlain() -{ - local Color expectedColor, resultColor; - expectedColor = _().color.RGBA(154, 255, 0, 187); - Issue("`ParseString()` cannot parse hex colors."); - TEST_ExpectTrue(_().color.ParseString("#9aff00", resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString()` cannot parse rgb colors."); - TEST_ExpectTrue(_().color.ParseString("rgb(154,255,0)", resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString()` cannot parse rgba colors."); - TEST_ExpectTrue(_().color.ParseString("rgba(154,255,0,187)", resultColor)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseString()` cannot parse rgb colors with tags."); - TEST_ExpectTrue(_().color.ParseString("rgb(r=154,g=255,b=0)", resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString()` cannot parse rgba colors with tags."); - TEST_ExpectTrue(_().color.ParseString( "rgba(r=154,g=255,b=0,a=187)", - resultColor)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseString()` reports success when parsing invalid color string."); - TEST_ExpectFalse(_().color.ParseString("#9aff0g", resultColor)); -} - -protected static function SubTest_ParseStringColored() -{ - local Color expectedColor, resultColor; - expectedColor = _().color.RGBA(154, 255, 0, 187); - Issue("`ParseString(STRING_Colored)` cannot parse hex colors."); - TEST_ExpectTrue(_().color.ParseString( - "#9af" $ Chr(27) $ Chr(45) $ Chr(234) $ Chr(24) $ "f00", - resultColor, STRING_Colored)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString(STRING_Colored)` cannot parse rgb colors."); - TEST_ExpectTrue(_().color.ParseString( - "rgb(154,2" $ Chr(27) $ Chr(23) $ Chr(32) $ Chr(53) $ "55,0)", - resultColor, STRING_Colored)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString(STRING_Colored)` cannot parse rgba colors."); - TEST_ExpectTrue(_().color.ParseString( - "rgba(154,255,0,187" $ Chr(27) $ Chr(133) $ Chr(234) $ Chr(10) $ ")", - resultColor, STRING_Colored)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseString(STRING_Colored)` cannot parse rgb colors with tags."); - TEST_ExpectTrue(_().color.ParseString( - "rg" $ Chr(27) $ Chr(26) $ Chr(234) $ Chr(125) $ "b(r=154,g=255,b=0)", - resultColor, STRING_Colored)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString(STRING_Colored)` cannot parse rgba colors with tags."); - TEST_ExpectTrue(_().color.ParseString( - "rgba(r=154,g=255,b" $ Chr(27) $ Chr(1) $ Chr(4) $ Chr(7) $ "=0,a=187)", - resultColor, STRING_Colored)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); -} - -protected static function SubTest_ParseStringFormatted() -{ - local Color expectedColor, resultColor; - expectedColor = _().color.RGBA(154, 255, 0, 187); - Issue("`ParseString(STRING_Formatted)` cannot parse hex colors."); - TEST_ExpectTrue(_().color.ParseString( - "#9a{#4753d5 ff0}0", - resultColor, STRING_Formatted)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString(STRING_Formatted)` cannot parse rgb colors."); - TEST_ExpectTrue(_().color.ParseString( - "rg{rgb(45,67,123) b(154,25}5,0)", - resultColor, STRING_Formatted)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString(STRING_Formatted)` cannot parse rgba colors."); - TEST_ExpectTrue(_().color.ParseString( - "rgba(154,2{#34d1a7 }55,0,187)", - resultColor, STRING_Formatted)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseString(STRING_Formatted)` cannot parse rgb colors with tags."); - TEST_ExpectTrue(_().color.ParseString( - "rgb(r{#34d1a7 }=154,g=255,b=0)", - resultColor, STRING_Formatted)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseString(STRING_Formatted)` cannot parse rgba colors with" - @ "tags."); - TEST_ExpectTrue(_().color.ParseString( - "r{rgb(12,12,253) gba(r=154,g=255,b=0,a=187)}", - resultColor, STRING_Formatted)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); -} - -protected static function SubTest_ParseText() -{ - local Color expectedColor, resultColor; - expectedColor = _().color.RGBA(154, 255, 0, 187); - Issue("`ParseText()` cannot parse hex colors."); - TEST_ExpectTrue(_().color.ParseText(_().text.FromString("#9aff00"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseText()` cannot parse rgb colors."); - TEST_ExpectTrue(_().color.ParseText(_().text.FromString("rgb(154,255,0)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseText()` cannot parse rgba colors."); - TEST_ExpectTrue(_().color.ParseText( - _().text.FromString("rgba(154,255,0,187)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseText()` cannot parse rgb colors with tags."); - TEST_ExpectTrue(_().color.ParseText( - _().text.FromString("rgb(r=154,g=255,b=0)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseText()` cannot parse rgba colors with tags."); - TEST_ExpectTrue(_().color.ParseText( - _().text.FromString("rgba(r=154,g=255,b=0,a=187)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseText()` reports success when parsing invalid color string."); - TEST_ExpectFalse(_().color.ParseText( _().text.FromString("#9aff0g"), - resultColor)); -} - -protected static function SubTest_ParseRaw() -{ - local Color expectedColor, resultColor; - expectedColor = _().color.RGBA(154, 255, 0, 187); - Issue("`ParseRaw()` cannot parse hex colors."); - TEST_ExpectTrue(_().color.ParseRaw( _().text.StringToRaw("#9aff00"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseRaw()` cannot parse rgb colors."); - TEST_ExpectTrue(_().color.ParseRaw( _().text.StringToRaw("rgb(154,255,0)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseRaw()` cannot parse rgba colors."); - TEST_ExpectTrue(_().color.ParseRaw( - _().text.StringToRaw("rgba(154,255,0,187)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseRaw()` cannot parse rgb colors with tags."); - TEST_ExpectTrue(_().color.ParseRaw( - _().text.StringToRaw("rgb(r=154,g=255,b=0)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor)); - - Issue("`ParseRaw()` cannot parse rgba colors with tags."); - TEST_ExpectTrue(_().color.ParseRaw( - _().text.StringToRaw("rgba(r=154,g=255,b=0,a=187)"), - resultColor)); - TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor)); - - Issue("`ParseRaw()` reports success when parsing invalid color string."); - TEST_ExpectFalse(_().color.ParseRaw(_().text.StringToRaw("#9aff0g"), - resultColor)); -} - -defaultproperties -{ - caseName = "Colors" -} \ No newline at end of file diff --git a/sources/Core/Console/ConsoleAPI.uc b/sources/Core/Console/ConsoleAPI.uc deleted file mode 100644 index 5365626..0000000 --- a/sources/Core/Console/ConsoleAPI.uc +++ /dev/null @@ -1,280 +0,0 @@ -/** - * API that provides functions for outputting text in - * Killing Floor's console. It takes care of coloring output and breaking up - * long lines (since allowing game to handle line breaking completely - * messes up console output). - * - * Actual output is taken care of by `ConsoleWriter` objects that this - * API generates. - * Copyright 2020 Anton Tarasenko - *------------------------------------------------------------------------------ - * This file is part of Acedia. - * - * Acedia is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License, or - * (at your option) any later version. - * - * Acedia is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Acedia. If not, see . - */ -class ConsoleAPI extends Singleton - config(AcediaSystem); - -/** - * Main issue with console output in Killing Floor is - * automatic line breaking of long enough messages: - * it breaks formatting and can lead to an ugly text overlapping. - * To fix this we will try to break up user's output into lines ourselves, - * before game does it for us. - * - * We are not 100% sure how Killing Floor decides when to break the line, - * but it seems to calculate how much text can actually fit in a certain - * area on screen. - * There are two issues: - * 1. We do not know for sure what this limit value is. - * Even if we knew how to compute it, we cannot do that in server mode, - * since it depends on a screen resolution and font, which - * can vary for different players. - * 2. Even invisible characters, such as color change sequences, - * that do not take any space on the screen, contribute towards - * that limit. So for a heavily colored text we will have to - * break line much sooner than for the plain text. - * Both issues are solved by introducing two limits that users themselves - * are allowed to change: visible character limit and total character limit. - * ~ Total character limit will be a hard limit on a character amount in - * a line (including hidden ones used for color change sequences) that - * will be used to prevent Killing Floor's native line breaks. - * ~ Visible character limit will be a lower limit on amount of actually - * visible character. It introduction basically reserves some space that can be - * used only for color change sequences. Without this limit lines with - * colored lines will appear to be shorter that mono-colored ones. - * Visible limit will help to alleviate this problem. - * - * For example, if we set total limit to `120` and visible limit to `80`: - * 1. Line not formatted with color will all break at - * around length of `80`. - * 2. Since color change sequence consists of 4 characters: - * we can fit up to `(120 - 80) / 4 = 10` color swaps into each line, - * while still breaking them at a around the same length of `80`. - * ~ To differentiate our line breaks from line breaks intended by - * the user, we will also add 2 symbols worth of padding in front of all our - * output: - * 1. Before intended new line they will be just two spaces. - * 2. After our line break we will replace first space with "|" to indicate - * that we had to break a long line. - * - * Described measures are not perfect: - * 1. Since Killing Floor's console doe not use monospaced font, - * the same amount of characters on the line does not mean lines of - * visually the same length; - * 2. Heavily enough colored lines are still going to be shorter; - * 3. Depending on a resolution, default limits may appear to either use - * too little space (for high resolutions) or, on the contrary, - * not prevent native line breaks (low resolutions). - * In these cases user might be required to manually set limits; - * 4. There are probably more. - * But if seems to provide good enough results for the average use case. - */ - -/** - * Configures how text will be rendered in target console(s). - */ -struct ConsoleDisplaySettings -{ - // What color to use for text by default - var Color defaultColor; - // How many visible characters in be displayed in a line? - var int maxVisibleLineWidth; - // How many total characters can be output at once? - var int maxTotalLineWidth; -}; -// We will store data for `ConsoleDisplaySettings` separately for the ease of -// configuration. -var private config Color defaultColor; -var private config int maxVisibleLineWidth; -var private config int maxTotalLineWidth; - -/** - * Return current global visible limit that describes how many (at most) - * visible characters can be output in the console line. - * - * Instances of `ConsoleWriter` are initialized with this value, - * but can later change this value independently. - * Changes to global values do not affect already created `ConsoleWriters`. - * - * @return Current global visible limit. - */ -public final function int GetVisibleLineLength() -{ - return maxVisibleLineWidth; -} - -/** - * Sets current global visible limit that describes how many (at most) visible - * characters can be output in the console line. - * - * Instances of `ConsoleWriter` are initialized with this value, - * but can later change this value independently. - * Changes to global values do not affect already created `ConsoleWriters`. - * - * @param newMaxVisibleLineWidth New global visible character limit. - */ -public final function SetVisibleLineLength(int newMaxVisibleLineWidth) -{ - maxVisibleLineWidth = newMaxVisibleLineWidth; -} - -/** - * Return current global total limit that describes how many (at most) - * characters can be output in the console line. - * - * Instances of `ConsoleWriter` are initialized with this value, - * but can later change this value independently. - * Changes to global values do not affect already created `ConsoleWriters`. - * - * @return Current global total limit. - */ -public final function int GetTotalLineLength() -{ - return maxTotalLineWidth; -} - -/** - * Sets current global total limit that describes how many (at most) - * characters can be output in the console line, counting both visible symbols - * and color change sequences. - * - * Instances of `ConsoleWriter` are initialized with this value, - * but can later change this value independently. - * Changes to global values do not affect already created `ConsoleWriters`. - * - * @param newMaxTotalLineWidth New global total character limit. - */ -public final function SetTotalLineLength(int newMaxTotalLineWidth) -{ - maxTotalLineWidth = newMaxTotalLineWidth; -} - -/** - * Return current global total limit that describes how many (at most) - * characters can be output in the console line. - * - * Instances of `ConsoleWriter` are initialized with this value, - * but can later change this value independently. - * Changes to global values do not affect already created `ConsoleWriters`. - * - * @return Current default output color. - */ -public final function Color GetDefaultColor(int newMaxTotalLineWidth) -{ - return defaultColor; -} - -/** - * Sets current global default color for console output., - * - * Instances of `ConsoleWriter` are initialized with this value, - * but can later change this value independently. - * Changes to global values do not affect already created `ConsoleWriters`. - * - * @param newMaxTotalLineWidth New global default output color. - */ -public final function SetDefaultColor(Color newDefaultColor) -{ - defaultColor = newDefaultColor; -} - -/** - * Returns borrowed `ConsoleWriter` instance that will write into - * consoles of all players. - * - * @return ConsoleWriter Borrowed `ConsoleWriter` instance, configured to - * write into consoles of all players. - * Never `none`. - */ -public final function ConsoleWriter ForAll() -{ - local ConsoleDisplaySettings globalSettings; - globalSettings.defaultColor = defaultColor; - globalSettings.maxTotalLineWidth = maxTotalLineWidth; - globalSettings.maxVisibleLineWidth = maxVisibleLineWidth; - return ConsoleWriter(_.memory.Claim(class'ConsoleWriter')) - .Initialize(globalSettings).ForAll(); -} - -/** - * Returns borrowed `ConsoleWriter` instance that will write into - * console of the player with a given controller. - * - * @param targetController Player, to whom console we want to write. - * If `none` - returned `ConsoleWriter` would be configured to - * throw messages away. - * @return Borrowed `ConsoleWriter` instance, configured to - * write into consoles of all players. - * Never `none`. - */ -public final function ConsoleWriter For(PlayerController targetController) -{ - local ConsoleDisplaySettings globalSettings; - globalSettings.defaultColor = defaultColor; - globalSettings.maxTotalLineWidth = maxTotalLineWidth; - globalSettings.maxVisibleLineWidth = maxVisibleLineWidth; - return ConsoleWriter(_.memory.Claim(class'ConsoleWriter')) - .Initialize(globalSettings).ForController(targetController); -} - -/** - * Returns new `ConsoleWriter` instance that will write into - * consoles of all players. - * Should be freed after use. - * - * @return ConsoleWriter New `ConsoleWriter` instance, configured to - * write into consoles of all players. - * Never `none`. - */ -public final function ConsoleWriter MakeForAll() -{ - local ConsoleDisplaySettings globalSettings; - globalSettings.defaultColor = defaultColor; - globalSettings.maxTotalLineWidth = maxTotalLineWidth; - globalSettings.maxVisibleLineWidth = maxVisibleLineWidth; - return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter')) - .Initialize(globalSettings).ForAll(); -} - -/** - * Returns new `ConsoleWriter` instance that will write into - * console of the player with a given controller. - * Should be freed after use. - * - * @param targetController Player, to whom console we want to write. - * If `none` - returned `ConsoleWriter` would be configured to - * throw messages away. - * @return New `ConsoleWriter` instance, configured to - * write into consoles of all players. - * Never `none`. - */ -public final function ConsoleWriter MakeFor(PlayerController targetController) -{ - local ConsoleDisplaySettings globalSettings; - globalSettings.defaultColor = defaultColor; - globalSettings.maxTotalLineWidth = maxTotalLineWidth; - globalSettings.maxVisibleLineWidth = maxVisibleLineWidth; - return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter')) - .Initialize(globalSettings).ForController(targetController); -} - -defaultproperties -{ - defaultColor = (R=255,G=255,B=255,A=255) - // These should guarantee decent text output even at - // 640x480 shit resolution - maxVisibleLineWidth = 80 - maxTotalLineWidth = 108 -} \ No newline at end of file diff --git a/sources/Core/Console/ConsoleBuffer.uc b/sources/Core/Console/ConsoleBuffer.uc deleted file mode 100644 index a7f477a..0000000 --- a/sources/Core/Console/ConsoleBuffer.uc +++ /dev/null @@ -1,393 +0,0 @@ -/** - * Object that provides a buffer functionality for Killing Floor's (in-game) - * console output: it accepts content that user want to output and breaks it - * into lines that will be well-rendered according to the given - * `ConsoleDisplaySettings`. - * Copyright 2020 Anton Tarasenko - *------------------------------------------------------------------------------ - * This file is part of Acedia. - * - * Acedia is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License, or - * (at your option) any later version. - * - * Acedia is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Acedia. If not, see . - */ -class ConsoleBuffer extends AcediaObject - dependson(Text) - dependson(ConsoleAPI); - -/** - * `ConsoleBuffer` works by breaking it's input into words, counting how much - * space they take up and only then deciding to which line to append them - * (new or the next, new one). - */ - -var private int CODEPOINT_ESCAPE; -var private int CODEPOINT_NEWLINE; -var private int COLOR_SEQUENCE_LENGTH; - -// Display settings according to which to format our output -var private ConsoleAPI.ConsoleDisplaySettings displaySettings; - -/** - * This structure is used to both share results of our work and for tracking - * information about the line we are currently filling. - */ -struct LineRecord -{ - // Contents of the line, in `STRING_Colored` format - var string contents; - // Is this a wrapped line? - // `true` means that this line was supposed to be part part of another, - // singular line of text, that had to be broken into smaller pieces. - // Such lines will start with "|" in front of them in Acedia's - // `ConsoleWriter`. - var bool wrappedLine; - // Information variables that describe how many visible and total symbols - // (visible + color change sequences) are stored int the `line` - var int visibleSymbolsStored; - var int totalSymbolsStored; - // Does `contents` contain a color change sequence? - // Non-empty line can have no such sequence if they consist of whitespaces. - var private bool colorInserted; - // If `colorInserted == true`, stores the last inserted color. - var private Color endColor; -}; -// Lines that are ready to be output to the console -var private array completedLines; - -// Line we are currently building -var private LineRecord currentLine; -// Word we are currently building, colors of it's characters will be -// automatically converted into `STRCOLOR_Struct`, according to the default -// color setting at the time of their addition. -var private array wordBuffer; -// Amount of color swaps inside `wordBuffer` -var private int colorSwapsInWordBuffer; - -/** - * Returns current setting used by this buffer to break up it's input into - * lines fit to be output in console. - * - * @return Currently used `ConsoleDisplaySettings`. - */ -public final function ConsoleAPI.ConsoleDisplaySettings GetSettings() -{ - return displaySettings; -} - -/** - * Sets new setting to be used by this buffer to break up it's input into - * lines fit to be output in console. - * - * It is recommended (although not required) to call `Flush()` before - * changing settings. Not doing so would not lead to any errors or warnings, - * but can lead to some wonky results and is considered an undefined behavior. - * - * @param newSettings New `ConsoleDisplaySettings` to be used. - * @return Returns caller `ConsoleBuffer` to allow for method chaining. - */ -public final function ConsoleBuffer SetSettings( - ConsoleAPI.ConsoleDisplaySettings newSettings) -{ - displaySettings = newSettings; - return self; -} - -/** - * Does caller `ConsoleBuffer` has any completed lines that can be output? - * - * "Completed line" means that nothing else will be added to it. - * So negative (`false`) response does not mean that the buffer is empty, - - * it can still contain an uncompleted and non-empty line that can still be - * expanded with `InsertString()`. If you want to completely empty the buffer - - * call the `Flush()` method. - * Also see `IsEmpty()`. - * - * @return `true` if caller `ConsoleBuffer` has no completed lines and - * `false` otherwise. - */ -public final function bool HasCompletedLines() -{ - return (completedLines.length > 0); -} - -/** - * Does caller `ConsoleBuffer` has any unprocessed input? - * - * Note that `ConsoleBuffer` can be non-empty, but no completed line if it - * currently builds one. - * See `Flush()` and `HasCompletedLines()` methods. - * - * @return `true` if `ConsoleBuffer` is completely empty - * (either did not receive or already returned all processed input) and - * `false` otherwise. - */ -public final function bool IsEmpty() -{ - if (HasCompletedLines()) return false; - if (currentLine.totalSymbolsStored > 0) return false; - if (wordBuffer.length > 0) return false; - return true; -} - -/** - * Clears the buffer of all data, but leaving current settings intact. - * After this calling method `IsEmpty()` should return `true`. - * - * @return Returns caller `ConsoleBuffer` to allow method chaining. - */ -public final function ConsoleBuffer Clear() -{ - local LineRecord newLineRecord; - currentLine = newLineRecord; - completedLines.length = 0; - return self; -} - -/** - * Inserts a string into the buffer. This method does not automatically break - * the line after the `input`, call `Flush()` or add line feed symbol "\n" - * at the end of the `input` if you want that. - * - * @param input `string` to be added to the current line in caller - * `ConsoleBuffer`. - * @param inputType How to treat given `string` regarding coloring. - * @return Returns caller `ConsoleBuffer` to allow method chaining. - */ -public final function ConsoleBuffer InsertString( - string input, - Text.StringType inputType) -{ - local int inputConsumed; - local array rawInput; - rawInput = _().text.StringToRaw(input, inputType); - while (rawInput.length > 0) - { - // Fill word buffer, remove consumed input from `rawInput` - inputConsumed = 0; - while (inputConsumed < rawInput.length) - { - if (_().text.IsWhitespace(rawInput[inputConsumed])) break; - InsertIntoWordBuffer(rawInput[inputConsumed]); - inputConsumed += 1; - } - rawInput.Remove(0, inputConsumed); - // If we didn't encounter any whitespace symbols - bail - if (rawInput.length <= 0) { - return self; - } - FlushWordBuffer(); - // Dump whitespaces into lines - inputConsumed = 0; - while (inputConsumed < rawInput.length) - { - if (!_().text.IsWhitespace(rawInput[inputConsumed])) break; - AppendWhitespaceToCurrentLine(rawInput[inputConsumed]); - inputConsumed += 1; - } - rawInput.Remove(0, inputConsumed); - } - return self; -} - -/** - * Returns (and makes caller `ConsoleBuffer` forget) next completed line that - * can be output to console in `STRING_Colored` format. - * - * If there are no completed line to return - returns an empty one. - * - * @return Next completed line that can be output, in `STRING_Colored` format. - */ -public final function LineRecord PopNextLine() -{ - local LineRecord result; - if (completedLines.length <= 0) return result; - result = completedLines[0]; - completedLines.Remove(0, 1); - return result; -} - -/** - * Forces all buffered data into "completed line" array, making it retrievable - * by `PopNextLine()`. - * - * @return Next completed line that can be output, in `STRING_Colored` format. - */ -public final function ConsoleBuffer Flush() -{ - FlushWordBuffer(); - BreakLine(false); - return self; -} - -// It is assumed that passed characters are not whitespace, - -// responsibility to check is on the one calling this method. -private final function InsertIntoWordBuffer(Text.Character newCharacter) -{ - local int newCharacterIndex; - local Color oldColor, newColor; - newCharacterIndex = wordBuffer.length; - // Fix text color in the buffer to remember default color, if we use it. - newCharacter.color = - _().text.GetCharacterColor(newCharacter, displaySettings.defaultColor); - newCharacter.colorType = STRCOLOR_Struct; - wordBuffer[newCharacterIndex] = newCharacter; - if (newCharacterIndex <= 0) { - return; - } - oldColor = wordBuffer[newCharacterIndex].color; - newColor = wordBuffer[newCharacterIndex - 1].color; - if (!_().color.AreEqual(oldColor, newColor, true)) { - colorSwapsInWordBuffer += 1; - } -} - -// Pushes whole `wordBuffer` into lines -private final function FlushWordBuffer() -{ - local int i; - local Color newColor; - if (!WordCanFitInCurrentLine() && WordCanFitInNewLine()) { - BreakLine(true); - } - for (i = 0; i < wordBuffer.length; i += 1) - { - if (!CanAppendNonWhitespaceIntoLine(wordBuffer[i])) { - BreakLine(true); - } - newColor = wordBuffer[i].color; - if (MustSwapColorsFor(newColor)) - { - currentLine.contents $= _().color.GetColorTag(newColor); - currentLine.totalSymbolsStored += COLOR_SEQUENCE_LENGTH; - currentLine.colorInserted = true; - currentLine.endColor = newColor; - } - currentLine.contents $= Chr(wordBuffer[i].codePoint); - currentLine.totalSymbolsStored += 1; - currentLine.visibleSymbolsStored += 1; - } - wordBuffer.length = 0; - colorSwapsInWordBuffer = 0; -} - -private final function BreakLine(bool makeWrapped) -{ - local LineRecord newLineRecord; - if (currentLine.visibleSymbolsStored > 0) { - completedLines[completedLines.length] = currentLine; - } - currentLine = newLineRecord; - currentLine.wrappedLine = makeWrapped; -} - -private final function bool MustSwapColorsFor(Color newColor) -{ - if (!currentLine.colorInserted) return true; - return !_().color.AreEqual(currentLine.endColor, newColor, true); -} - -private final function bool CanAppendWhitespaceIntoLine() -{ - // We always allow to append at least something into empty line, - // otherwise we can never insert it anywhere - if (currentLine.totalSymbolsStored <= 0) return true; - if (currentLine.totalSymbolsStored >= displaySettings.maxTotalLineWidth) - { - return false; - } - if (currentLine.visibleSymbolsStored >= displaySettings.maxVisibleLineWidth) - { - return false; - } - return true; -} - -private final function bool CanAppendNonWhitespaceIntoLine( - Text.Character nextCharacter) -{ - // We always allow to insert at least something into empty line, - // otherwise we can never insert it anywhere - if (currentLine.totalSymbolsStored <= 0) { - return true; - } - // Check if we can fit a single character by fitting a whitespace symbol. - if (!CanAppendWhitespaceIntoLine()) { - return false; - } - if (!MustSwapColorsFor(nextCharacter.color)) { - return true; - } - // Can we fit character + color swap sequence? - return ( currentLine.totalSymbolsStored + COLOR_SEQUENCE_LENGTH + 1 - <= displaySettings.maxTotalLineWidth); -} - -// For performance reasons assumes that passed character is a whitespace, -// the burden of checking is on the caller. -private final function AppendWhitespaceToCurrentLine(Text.Character whitespace) -{ - if (_().text.IsCodePoint(whitespace, CODEPOINT_NEWLINE)) { - BreakLine(true); - return; - } - if (!CanAppendWhitespaceIntoLine()) { - BreakLine(true); - } - currentLine.contents $= Chr(whitespace.codePoint); - currentLine.totalSymbolsStored += 1; - currentLine.visibleSymbolsStored += 1; -} - -private final function bool WordCanFitInNewLine() -{ - local int totalCharactersInWord; - if (wordBuffer.length <= 0) return true; - if (wordBuffer.length > displaySettings.maxVisibleLineWidth) { - return false; - } - // `(colorSwapsInWordBuffer + 1)` counts how many times we must - // switch color inside a word + 1 for setting initial color - totalCharactersInWord = wordBuffer.length - + (colorSwapsInWordBuffer + 1) * COLOR_SEQUENCE_LENGTH; - return (totalCharactersInWord <= displaySettings.maxTotalLineWidth); -} - -private final function bool WordCanFitInCurrentLine() -{ - local int totalLimit, visibleLimit; - local int totalCharactersInWord; - if (wordBuffer.length <= 0) return true; - totalLimit = - displaySettings.maxTotalLineWidth - currentLine.totalSymbolsStored; - visibleLimit = - displaySettings.maxVisibleLineWidth - currentLine.visibleSymbolsStored; - // Visible symbols check - if (wordBuffer.length > visibleLimit) { - return false; - } - // Total symbols check - totalCharactersInWord = wordBuffer.length - + colorSwapsInWordBuffer * COLOR_SEQUENCE_LENGTH; - if (MustSwapColorsFor(wordBuffer[0].color)) { - totalCharactersInWord += COLOR_SEQUENCE_LENGTH; - } - return (totalCharactersInWord <= totalLimit); -} - -defaultproperties -{ - CODEPOINT_ESCAPE = 27 - CODEPOINT_NEWLINE = 10 - // CODEPOINT_ESCAPE + + + - COLOR_SEQUENCE_LENGTH = 4 -} \ No newline at end of file diff --git a/sources/Core/Console/ConsoleWriter.uc b/sources/Core/Console/ConsoleWriter.uc deleted file mode 100644 index 7408530..0000000 --- a/sources/Core/Console/ConsoleWriter.uc +++ /dev/null @@ -1,373 +0,0 @@ -/** - * Object that provides simple access to console output. - * Can either write to a certain player's console or to all consoles at once. - * Supports "fancy" and "raw" output (for more details @see `ConsoleAPI`). - * Copyright 2020 Anton Tarasenko - *------------------------------------------------------------------------------ - * This file is part of Acedia. - * - * Acedia is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License, or - * (at your option) any later version. - * - * Acedia is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Acedia. If not, see . - */ -class ConsoleWriter extends AcediaObject - dependson(ConsoleAPI) - dependson(ConnectionService); - -// Prefixes we output before every line to signify whether they were broken -// or not -var private string NEWLINE_PREFIX; -var private string BROKENLINE_PREFIX; - -/** - * Describes current output target of the `ConsoleWriter`. - */ -enum ConsoleWriterTarget -{ - // No one. Can happed if our target disconnects. - CWTARGET_None, - // A certain player. - CWTARGET_Player, - // All players. - CWTARGET_All -}; -var private ConsoleWriterTarget targetType; -// Controller of the player that will receive output passed -// to this `ConsoleWriter`. -// Only used when `targetType == CWTARGET_Player` -var private PlayerController outputTarget; -var private ConsoleBuffer outputBuffer; - -var private ConsoleAPI.ConsoleDisplaySettings displaySettings; - -public final function ConsoleWriter Initialize( - ConsoleAPI.ConsoleDisplaySettings newDisplaySettings) -{ - displaySettings = newDisplaySettings; - if (outputBuffer == none) { - outputBuffer = ConsoleBuffer(_().memory.Allocate(class'ConsoleBuffer')); - } - else { - outputBuffer.Clear(); - } - outputBuffer.SetSettings(displaySettings); - return self; -} - -/** - * Return current default color for caller `ConsoleWriter`. - * - * This method returns default color, i.e. color that will be used if no other - * is specified by text you're outputting. - * If color is specified, this value will be ignored. - * - * This value is not synchronized with the global value from `ConsoleAPI` - * (or such value from any other `ConsoleWriter`) and affects only - * output produced by this `ConsoleWriter`. - * - * @return Current default color. - */ -public final function Color GetColor() -{ - return displaySettings.defaultColor; -} - -/** - * Sets default color for caller 'ConsoleWriter`'s output. - * - * This only changes default color, i.e. color that will be used if no other is - * specified by text you're outputting. - * If color is specified, this value will be ignored. - * - * This value is not synchronized with the global value from `ConsoleAPI` - * (or such value from any other `ConsoleWriter`) and affects only - * output produced by this `ConsoleWriter`. - * - * @param newDefaultColor New color to use when none specified by text itself. - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter SetColor(Color newDefaultColor) -{ - displaySettings.defaultColor = newDefaultColor; - if (outputBuffer != none) { - outputBuffer.SetSettings(displaySettings); - } - return self; -} - -/** - * Return current visible limit that describes how many (at most) - * visible characters can be output in the console line. - * - * This value is not synchronized with the global value from `ConsoleAPI` - * (or such value from any other `ConsoleWriter`) and affects only - * output produced by this `ConsoleWriter`. - * - * @return Current global visible limit. - */ -public final function int GetVisibleLineLength() -{ - return displaySettings.maxVisibleLineWidth; -} - -/** - * Sets current visible limit that describes how many (at most) visible - * characters can be output in the console line. - * - * This value is not synchronized with the global value from `ConsoleAPI` - * (or such value from any other `ConsoleWriter`) and affects only - * output produced by this `ConsoleWriter`. - * - * @param newVisibleLimit New global visible limit. - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter SetVisibleLineLength( - int newMaxVisibleLineWidth -) -{ - displaySettings.maxVisibleLineWidth = newMaxVisibleLineWidth; - if (outputBuffer != none) { - outputBuffer.SetSettings(displaySettings); - } - return self; -} - -/** - * Return current total limit that describes how many (at most) - * characters can be output in the console line. - * - * This value is not synchronized with the global value from `ConsoleAPI` - * (or such value from any other `ConsoleWriter`) and affects only - * output produced by this `ConsoleWriter`. - * - * @return Current global total limit. - */ -public final function int GetTotalLineLength() -{ - return displaySettings.maxTotalLineWidth; -} - -/** - * Sets current total limit that describes how many (at most) - * characters can be output in the console line. - * - * This value is not synchronized with the global value from `ConsoleAPI` - * (or such value from any other `ConsoleWriter`) and affects only - * output produced by this `ConsoleWriter`. - * - * @param newTotalLimit New global total limit. - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter SetTotalLineLength(int newMaxTotalLineWidth) -{ - displaySettings.maxTotalLineWidth = newMaxTotalLineWidth; - if (outputBuffer != none) { - outputBuffer.SetSettings(displaySettings); - } - return self; -} - -/** - * Configures caller `ConsoleWriter` to output to all players. - * `Flush()` will be automatically called between target change. - * - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter ForAll() -{ - Flush(); - targetType = CWTARGET_All; - return self; -} - -/** - * Configures caller `ConsoleWriter` to output only to a player, - * given by a passed `PlayerController`. - * `Flush()` will be automatically called between target change. - * - * @param targetController Player, to whom console we want to write. - * If `none` - caller `ConsoleWriter` would be configured to - * throw messages away. - * @return ConsoleWriter Returns caller `ConsoleWriter` to allow for - * method chaining. - */ -public final function ConsoleWriter ForController( - PlayerController targetController -) -{ - Flush(); - if (targetController != none) - { - targetType = CWTARGET_Player; - outputTarget = targetController; - } - else { - targetType = CWTARGET_None; - } - return self; -} - -/** - * Returns type of current target for the caller `ConsoleWriter`. - * - * @return `ConsoleWriterTarget` value, describing current target of - * the caller `ConsoleWriter`. - */ -public final function ConsoleWriterTarget CurrentTarget() -{ - if (targetType == CWTARGET_Player && outputTarget == none) { - targetType = CWTARGET_None; - } - return targetType; -} - -/** - * Returns `PlayerController` of the player to whom console caller - * `ConsoleWriter` is outputting messages. - * - * @return `PlayerController` of the player to whom console caller - * `ConsoleWriter` is outputting messages. - * Returns `none` iff it currently outputs to every player or to no one. - */ -public final function PlayerController GetTargetPlayerController() -{ - if (targetType == CWTARGET_All) return none; - return outputTarget; -} - -/** - * Outputs all buffered input and moves further output onto a new line. - * - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter Flush() -{ - outputBuffer.Flush(); - SendBuffer(); - return self; -} - -/** - * Writes a formatted string into console. - * - * Does not trigger console output, for that use `WriteLine()` or `Flush()`. - * - * To output a different type of string into a console, use `WriteT()`. - * - * @param message Formatted string to output. - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter Write(string message) -{ - outputBuffer.InsertString(message, STRING_Formatted); - return self; -} - -/** - * Writes a formatted string into console. - * Result will be output immediately, starts a new line. - * - * To output a different type of string into a console, use `WriteLineT()`. - * - * @param message Formatted string to output. - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter WriteLine(string message) -{ - outputBuffer.InsertString(message, STRING_Formatted); - Flush(); - return self; -} - -/** - * Writes a `string` of specified type into console. - * - * Does not trigger console output, for that use `WriteLineT()` or `Flush()`. - * - * To output a formatted string you might want to simply use `Write()`. - * - * @param message String of a given type to output. - * @param inputType Type of the string method should output. - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter WriteT( - string message, - Text.StringType inputType) -{ - outputBuffer.InsertString(message, inputType); - return self; -} - -/** - * Writes a `string` of specified type into console. - * Result will be output immediately, starts a new line. - * - * To output a formatted string you might want to simply use `WriteLine()`. - * - * @param message String of a given type to output. - * @param inputType Type of the string method should output. - * @return Returns caller `ConsoleWriter` to allow for method chaining. - */ -public final function ConsoleWriter WriteLineT( - string message, - Text.StringType inputType) -{ - outputBuffer.InsertString(message, inputType); - Flush(); - return self; -} - -// Send all completed lines from an `outputBuffer` -private final function SendBuffer() -{ - local string prefix; - local ConnectionService service; - local ConsoleBuffer.LineRecord nextLineRecord; - while (outputBuffer.HasCompletedLines()) - { - nextLineRecord = outputBuffer.PopNextLine(); - if (nextLineRecord.wrappedLine) { - prefix = NEWLINE_PREFIX; - } - else { - prefix = BROKENLINE_PREFIX; - } - service = ConnectionService(class'ConnectionService'.static.Require()); - SendConsoleMessage(service, prefix $ nextLineRecord.contents); - } -} - -// Assumes `service != none`, caller function must ensure that. -private final function SendConsoleMessage( - ConnectionService service, - string message) -{ - local int i; - local array connections; - if (targetType != CWTARGET_All) - { - if (outputTarget != none) { - outputTarget.ClientMessage(message); - } - return; - } - connections = service.GetActiveConnections(); - for (i = 0; i < connections.length; i += 1) { - connections[i].controllerReference.ClientMessage(message); - } -} - -defaultproperties -{ - NEWLINE_PREFIX = "| " - BROKENLINE_PREFIX = " " -} \ No newline at end of file diff --git a/sources/Core/Data/JSON/JArray.uc b/sources/Core/Data/JSON/JArray.uc deleted file mode 100644 index d160b54..0000000 --- a/sources/Core/Data/JSON/JArray.uc +++ /dev/null @@ -1,351 +0,0 @@ -/** - * This class implements JSON array storage capabilities. - * Array stores ordered JSON values that can be referred by their index. - * It can contain any mix of JSON value types and cannot have any gaps, - * i.e. in array of length N, there must be a valid value for all indices - * from 0 to N-1. - * 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 JArray extends JSON; - -// Data will simply be stored as an array of JSON values -var private array data; - -// Return type of value stored at a given index. -// Returns `JSON_Undefined` if and only if given index is out of bounds. -public final function JType GetTypeOf(int index) -{ - if (index < 0) return JSON_Undefined; - if (index >= data.length) return JSON_Undefined; - - return data[index].type; -} - -// Returns current length of this array. -public final function int GetLength() -{ - return data.length; -} - -// Changes length of this array. -// In case of the increase - fills new indices with `null` values. -public final function SetLength(int newLength) -{ - local int i; - local int oldLength; - oldLength = data.length; - data.length = newLength; - if (oldLength >= newLength) - { - return; - } - i = oldLength; - while (i < newLength) - { - SetNull(i); - i += 1; - } -} - -// Following functions are getters for various types of variables. -// Getter for null value simply checks if it's null -// and returns true/false as a result. -// Getters for simple types (number, string, boolean) can have optional -// default value specified, that will be returned if requested variable -// doesn't exist or has a different type. -// Getters for object and array types don't take default values and -// will simply return `none`. -public final function float GetNumber(int index, optional float defaultValue) -{ - if (index < 0) return defaultValue; - if (index >= data.length) return defaultValue; - if (data[index].type != JSON_Number) return defaultValue; - - return data[index].numberValue; -} - -public final function string GetString(int index, optional string defaultValue) -{ - if (index < 0) return defaultValue; - if (index >= data.length) return defaultValue; - if (data[index].type != JSON_String) return defaultValue; - - return data[index].stringValue; -} - -public final function bool GetBoolean(int index, optional bool defaultValue) -{ - if (index < 0) return defaultValue; - if (index >= data.length) return defaultValue; - if (data[index].type != JSON_Boolean) return defaultValue; - - return data[index].booleanValue; -} - -public final function bool IsNull(int index) -{ - if (index < 0) return false; - if (index >= data.length) return false; - - return (data[index].type == JSON_Null); -} - -public final function JArray GetArray(int index) -{ - if (index < 0) return none; - if (index >= data.length) return none; - if (data[index].type != JSON_Array) return none; - - return JArray(data[index].complexValue); -} - -public final function JObject GetObject(int index) -{ - if (index < 0) return none; - if (index >= data.length) return none; - if (data[index].type != JSON_Object) return none; - - return JObject(data[index].complexValue); -} - -// Following functions provide simple setters for boolean, string, number -// and null values. -// If passed index is negative - does nothing. -// If index lies beyond array length (`>= GetLength()`), - -// these functions will expand array in the same way as `GetLength()` function. -// This can be prevented by setting optional parameter `preventExpansion` to -// `false` (nothing will be done in this case). -// They return object itself, allowing user to chain calls like this: -// `array.SetNumber("num1", 1).SetNumber("num2", 2);`. -public final function JArray SetNumber -( - int index, - float value, - optional bool preventExpansion -) -{ - local JStorageAtom newStorageValue; - if (index < 0) return self; - - if (index >= data.length) - { - if (preventExpansion) - { - return self; - } - else - { - SetLength(index + 1); - } - } - newStorageValue.type = JSON_Number; - newStorageValue.numberValue = value; - data[index] = newStorageValue; - return self; -} - -public final function JArray SetString -( - int index, - string value, - optional bool preventExpansion -) -{ - local JStorageAtom newStorageValue; - if (index < 0) return self; - - if (index >= data.length) - { - if (preventExpansion) - { - return self; - } - else - { - SetLength(index + 1); - } - } - newStorageValue.type = JSON_String; - newStorageValue.stringValue = value; - data[index] = newStorageValue; - return self; -} - -public final function JArray SetBoolean -( - int index, - bool value, - optional bool preventExpansion -) -{ - local JStorageAtom newStorageValue; - if (index < 0) return self; - - if (index >= data.length) - { - if (preventExpansion) - { - return self; - } - else - { - SetLength(index + 1); - } - } - newStorageValue.type = JSON_Boolean; - newStorageValue.booleanValue = value; - data[index] = newStorageValue; - return self; -} - -public final function JArray SetNull -( - int index, - optional bool preventExpansion -) -{ - local JStorageAtom newStorageValue; - if (index < 0) return self; - - if (index >= data.length) - { - if (preventExpansion) - { - return self; - } - else - { - SetLength(index + 1); - } - } - newStorageValue.type = JSON_Null; - data[index] = newStorageValue; - return self; -} - -// JSON array and object types don't have setters, but instead have -// functions to create a new, empty array/object under a certain name. -// If passed index is negative - does nothing. -// If index lies beyond array length (`>= GetLength()`), - -// these functions will expand array in the same way as `GetLength()` function. -// This can be prevented by setting optional parameter `preventExpansion` to -// `false` (nothing will be done in this case). -// They return object itself, allowing user to chain calls like this: -// `array.CreateObject("sub object").CreateArray("sub array");`. -public final function JArray CreateArray -( - int index, - optional bool preventExpansion -) -{ - local JStorageAtom newStorageValue; - if (index < 0) return self; - - if (index >= data.length) - { - if (preventExpansion) - { - return self; - } - else - { - SetLength(index + 1); - } - } - newStorageValue.type = JSON_Array; - newStorageValue.complexValue = _.json.newArray(); - data[index] = newStorageValue; - return self; -} - -public final function JArray CreateObject -( - int index, - optional bool preventExpansion -) -{ - local JStorageAtom newStorageValue; - if (index < 0) return self; - - if (index >= data.length) - { - if (preventExpansion) - { - return self; - } - else - { - SetLength(index + 1); - } - } - newStorageValue.type = JSON_Object; - newStorageValue.complexValue = _.json.newObject(); - data[index] = newStorageValue; - return self; -} - -// Wrappers for setter functions that don't take index or -// `preventExpansion` parameters and add/create value at the end of the array. -public final function JArray AddNumber(float value) -{ - return SetNumber(data.length, value); -} - -public final function JArray AddString(string value) -{ - return SetString(data.length, value); -} - -public final function JArray AddBoolean(bool value) -{ - return SetBoolean(data.length, value); -} - -public final function JArray AddNull() -{ - return SetNull(data.length); -} - -public final function JArray AddArray() -{ - return CreateArray(data.length); -} - -public final function JArray AddObject() -{ - return CreateObject(data.length); -} - -// Removes up to `amount` (minimum of `1`) of values, starting from -// a given index. -// If `index` falls outside array boundaries - nothing will be done. -// Returns `true` if value was actually removed and `false` if it didn't exist. -public final function bool RemoveValue(int index, optional int amount) -{ - if (index < 0) return false; - if (index >= data.length) return false; - - amount = Max(amount, 1); - amount = Min(amount, data.length - index); - data.Remove(index, amount); - return true; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Data/JSON/JObject.uc b/sources/Core/Data/JSON/JObject.uc deleted file mode 100644 index 9d81cf8..0000000 --- a/sources/Core/Data/JSON/JObject.uc +++ /dev/null @@ -1,265 +0,0 @@ -/** - * This class implements JSON object storage capabilities. - * Whenever one wants to store JSON data, they need to define such object. - * It stores name-value pairs, where names are strings and values can be: - * ~ Boolean, string, null or number (float in this implementation) data; - * ~ Other JSON objects; - * ~ JSON Arrays (see `JArray` class). - * - * This implementation provides getters and setters for boolean, string, - * null or number types that allow to freely set and fetch their values - * by name. - * JSON objects and arrays can be fetched by getters, but you cannot - * add existing object or array to another object. Instead one has to create - * a new, empty object with a certain name and then fill it with data. - * This allows to avoid loop situations, where object is contained in itself. - * Functions to remove existing values are also provided and are applicable - * to all variable types. - * Setters can also be used to overwrite any value by a different value, - * even of a different type. - * 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 JObject extends JSON; - -// We will store all our properties as a simple array of name-value pairs. -struct JProperty -{ - var string name; - var JStorageAtom value; -}; -var private array properties; - -// Returns index of name-value pair in `properties` for a given name. -// Returns `-1` if such a pair does not exist. -private final function int GetPropertyIndex(string name) -{ - local int i; - for (i = 0; i < properties.length; i += 1) - { - if (name == properties[i].name) - { - return i; - } - } - return -1; -} - -// Returns `JType` of a variable with a given name in our properties. -// This function can be used to check if certain variable exists -// in this object, since if such variable does not exist - -// function will return `JSON_Undefined`. -public final function JType GetTypeOf(string name) -{ - local int index; - index = GetPropertyIndex(name); - if (index < 0) return JSON_Undefined; - - return properties[index].value.type; -} - -// Following functions are getters for various types of variables. -// Getter for null value simply checks if it's null -// and returns true/false as a result. -// Getters for simple types (number, string, boolean) can have optional -// default value specified, that will be returned if requested variable -// doesn't exist or has a different type. -// Getters for object and array types don't take default values and -// will simply return `none`. -public final function float GetNumber(string name, optional float defaultValue) -{ - local int index; - index = GetPropertyIndex(name); - if (index < 0) return defaultValue; - if (properties[index].value.type != JSON_Number) return defaultValue; - - return properties[index].value.numberValue; -} - -public final function string GetString -( - string name, - optional string defaultValue -) -{ - local int index; - index = GetPropertyIndex(name); - if (index < 0) return defaultValue; - if (properties[index].value.type != JSON_String) return defaultValue; - - return properties[index].value.stringValue; -} - -public final function bool GetBoolean(string name, optional bool defaultValue) -{ - local int index; - index = GetPropertyIndex(name); - if (index < 0) return defaultValue; - if (properties[index].value.type != JSON_Boolean) return defaultValue; - - return properties[index].value.booleanValue; -} - -public final function bool IsNull(string name) -{ - local int index; - index = GetPropertyIndex(name); - if (index < 0) return false; - if (properties[index].value.type != JSON_Null) return false; - - return (properties[index].value.type == JSON_Null); -} - -public final function JArray GetArray(string name) -{ - local int index; - index = GetPropertyIndex(name); - if (index < 0) return none; - if (properties[index].value.type != JSON_Array) return none; - - return JArray(properties[index].value.complexValue); -} - -public final function JObject GetObject(string name) -{ - local int index; - index = GetPropertyIndex(name); - if (index < 0) return none; - if (properties[index].value.type != JSON_Object) return none; - - return JObject(properties[index].value.complexValue); -} - -// Following functions provide simple setters for boolean, string, number -// and null values. -// They return object itself, allowing user to chain calls like this: -// `object.SetNumber("num1", 1).SetNumber("num2", 2);`. -public final function JObject SetNumber(string name, float value) -{ - local int index; - local JProperty newProperty; - index = GetPropertyIndex(name); - if (index < 0) - { - index = properties.length; - } - - newProperty.name = name; - newProperty.value.type = JSON_Number; - newProperty.value.numberValue = value; - properties[index] = newProperty; - return self; -} - -public final function JObject SetString(string name, string value) -{ - local int index; - local JProperty newProperty; - index = GetPropertyIndex(name); - if (index < 0) - { - index = properties.length; - } - newProperty.name = name; - newProperty.value.type = JSON_String; - newProperty.value.stringValue = value; - properties[index] = newProperty; - return self; -} - -public final function JObject SetBoolean(string name, bool value) -{ - local int index; - local JProperty newProperty; - index = GetPropertyIndex(name); - if (index < 0) - { - index = properties.length; - } - newProperty.name = name; - newProperty.value.type = JSON_Boolean; - newProperty.value.booleanValue = value; - properties[index] = newProperty; - return self; -} - -public final function JObject SetNull(string name) -{ - local int index; - local JProperty newProperty; - index = GetPropertyIndex(name); - if (index < 0) - { - index = properties.length; - } - newProperty.name = name; - newProperty.value.type = JSON_Null; - properties[index] = newProperty; - return self; -} - -// JSON array and object types don't have setters, but instead have -// functions to create a new, empty array/object under a certain name. -// They return object itself, allowing user to chain calls like this: -// `object.CreateObject("folded object").CreateArray("names list");`. -public final function JObject CreateArray(string name) -{ - local int index; - local JProperty newProperty; - index = GetPropertyIndex(name); - if (index < 0) - { - index = properties.length; - } - newProperty.name = name; - newProperty.value.type = JSON_Array; - newProperty.value.complexValue = _.json.newArray(); - properties[index] = newProperty; - return self; -} - -public final function JObject CreateObject(string name) -{ - local int index; - local JProperty newProperty; - index = GetPropertyIndex(name); - if (index < 0) - { - index = properties.length; - } - newProperty.name = name; - newProperty.value.type = JSON_Object; - newProperty.value.complexValue = _.json.newObject(); - properties[index] = newProperty; - return self; -} - -// Removes values with a given name. -// Returns `true` if value was actually removed and `false` if it didn't exist. -public final function bool RemoveValue(string name) -{ - local int index; - index = GetPropertyIndex(name); - if (index < 0) return false; - - properties.Remove(index, 1); - return true; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Data/JSON/JSON.uc b/sources/Core/Data/JSON/JSON.uc deleted file mode 100644 index 8a94157..0000000 --- a/sources/Core/Data/JSON/JSON.uc +++ /dev/null @@ -1,84 +0,0 @@ -/** - * JSON is an open standard file format, and data interchange format, - * that uses human-readable text to store and transmit data objects - * consisting of name–value pairs and array data types. - * For more information refer to https://en.wikipedia.org/wiki/JSON - * This is a base class for implementation of JSON data storage for Acedia. - * It does not implement parsing and printing from/into human-readable - * text representation, just provides means to store such information. - * - * JSON data is stored as an object (represented via `JSONObject`) that - * contains a set of name-value pairs, where value can be - * a number, string, boolean value, another object or - * an array (represented by `JSONArray`). - * 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 JSON extends AcediaActor - abstract; - -// Enumeration for possible types of JSON values. -enum JType -{ - // Technical type, used to indicate that requested value is missing. - // Undefined values are not part of JSON format. - JSON_Undefined, - // An empty value, in teste representation defined by a single word "null". - JSON_Null, - // A number, recorded as a float. - // JSON itself doesn't specify whether number is an integer or float. - JSON_Number, - // A string. - JSON_String, - // A bool value. - JSON_Boolean, - // Array of other JSON values, stored without names; - // Single array can contain any mix of value types. - JSON_Array, - // Another JSON object, i.e. associative array of name-value pairs - JSON_Object -}; - -// Stores a single JSON value -struct JStorageAtom -{ - // What type is stored exactly? - // Depending on that, uses one of the other fields as a storage. - var protected JType type; - var protected float numberValue; - var protected string stringValue; - var protected bool booleanValue; - // Used for storing both JSON objects and arrays. - var protected JSON complexValue; -}; - -// TODO: Rewrite JSON object to use more efficient storage data structures -// that will support subtypes: -// ~ Number: byte, int, float -// ~ String: string, class -// (maybe move to auto generated code?). -// TODO: Add cleanup queue to efficiently and without crashes clean up -// removed objects. -// TODO: Add `JValue` - a reference type for number / string / boolean / null -// TODO: Add accessors for last values. -// TODO: Add path-getters. -// TODO: Add iterators. -// TODO: Add parsing/printing. -// TODO: Add functions for deep copy. -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Data/JSON/JSONAPI.uc b/sources/Core/Data/JSON/JSONAPI.uc deleted file mode 100644 index 8e88fa6..0000000 --- a/sources/Core/Data/JSON/JSONAPI.uc +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Provides convenient access to JSON-related functions. - * Copyright 2019 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 JSONAPI extends Singleton; - -public function JObject newObject() -{ - local JObject newObject; - newObject = Spawn(class'JObject'); - return newObject; -} - -public function JArray newArray() -{ - local JArray newArray; - newArray = Spawn(class'JArray'); - return newArray; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Data/JSON/Tests/TEST_JSON.uc b/sources/Core/Data/JSON/Tests/TEST_JSON.uc deleted file mode 100644 index f9801c4..0000000 --- a/sources/Core/Data/JSON/Tests/TEST_JSON.uc +++ /dev/null @@ -1,711 +0,0 @@ -/** - * Set of tests for JSON data storage, implemented via - * `JObject` and `JArray`. - * 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 TEST_JSON extends TestCase - abstract; - -protected static function TESTS() -{ - local JObject jsonData; - jsonData = _().json.newObject(); - Test_ObjectGetSetRemove(); - Test_ArrayGetSetRemove(); -} - -protected static function Test_ObjectGetSetRemove() -{ - SubTest_Undefined(); - SubTest_StringGetSetRemove(); - SubTest_BooleanGetSetRemove(); - SubTest_NumberGetSetRemove(); - SubTest_NullGetSetRemove(); - SubTest_MultipleVariablesGetSet(); - SubTest_Object(); -} - -protected static function Test_ArrayGetSetRemove() -{ - Context("Testing get/set/remove functions for JSON arrays"); - SubTest_ArrayUndefined(); - SubTest_ArrayStringGetSetRemove(); - SubTest_ArrayBooleanGetSetRemove(); - SubTest_ArrayNumberGetSetRemove(); - SubTest_ArrayNullGetSetRemove(); - SubTest_ArrayMultipleVariablesStorage(); - SubTest_ArrayMultipleVariablesRemoval(); - SubTest_ArrayRemovingMultipleVariablesAtOnce(); - SubTest_ArrayExpansions(); -} - -protected static function SubTest_Undefined() -{ - local JObject testJSON; - testJSON = _().json.newObject(); - - Context("Testing how `JObject` handles undefined values"); - Issue("Undefined variable doesn't have proper type."); - TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined); - - Issue("There is a variable in an empty object after `GetTypeOf` call."); - TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined); - - Issue("Getters don't return default values for undefined variables."); - TEST_ExpectTrue(testJSON.GetNumber("some_var", 0) == 0); - TEST_ExpectTrue(testJSON.GetString("some_var", "") == ""); - TEST_ExpectTrue(testJSON.GetBoolean("some_var", false) == false); - TEST_ExpectNone(testJSON.GetObject("some_var")); - TEST_ExpectNone(testJSON.GetArray("some_var")); -} - -protected static function SubTest_BooleanGetSetRemove() -{ - local JObject testJSON; - testJSON = _().json.newObject(); - testJSON.SetBoolean("some_boolean", true); - - Context("Testing `JObject`'s get/set/remove functions for" @ - "boolean variables"); - Issue("Boolean type isn't properly set by `SetBoolean`"); - TEST_ExpectTrue(testJSON.GetTypeOf("some_boolean") == JSON_Boolean); - - Issue("Variable value is incorrectly assigned by `SetBoolean`"); - TEST_ExpectTrue(testJSON.GetBoolean("some_boolean") == true); - - Issue("Variable value isn't correctly reassigned by `SetBoolean`"); - testJSON.SetBoolean("some_boolean", false); - TEST_ExpectTrue(testJSON.GetBoolean("some_boolean") == false); - - Issue( "Getting boolean variable as a wrong type" @ - "doesn't yield default value"); - TEST_ExpectTrue(testJSON.GetNumber("some_boolean", 7) == 7); - - Issue("Boolean variable isn't being properly removed"); - testJSON.RemoveValue("some_boolean"); - TEST_ExpectTrue(testJSON.GetTypeOf("some_boolean") == JSON_Undefined); - - Issue( "Getters don't return default value for missing key that" @ - "previously stored boolean value, that got removed"); - TEST_ExpectTrue(testJSON.GetBoolean("some_boolean", true) == true); -} - -protected static function SubTest_StringGetSetRemove() -{ - local JObject testJSON; - testJSON = _().json.newObject(); - testJSON.SetString("some_string", "first string"); - - Context("Testing `JObject`'s get/set/remove functions for" @ - "string variables"); - Issue("String type isn't properly set by `SetString`"); - TEST_ExpectTrue(testJSON.GetTypeOf("some_string") == JSON_String); - - Issue("Value is incorrectly assigned by `SetString`"); - TEST_ExpectTrue(testJSON.GetString("some_string") == "first string"); - - Issue( "Providing default variable value makes 'GetString'" @ - "return wrong value"); - TEST_ExpectTrue( testJSON.GetString("some_string", "alternative") - == "first string"); - - Issue("Variable value isn't correctly reassigned by `SetString`"); - testJSON.SetString("some_string", "new string!~"); - TEST_ExpectTrue(testJSON.GetString("some_string") == "new string!~"); - - Issue( "Getting string variable as a wrong type" @ - "doesn't yield default value"); - TEST_ExpectTrue(testJSON.GetBoolean("some_string", true) == true); - - Issue("String variable isn't being properly removed"); - testJSON.RemoveValue("some_string"); - TEST_ExpectTrue(testJSON.GetTypeOf("some_string") == JSON_Undefined); - - Issue( "Getters don't return default value for missing key that" @ - "previously stored string value, but got removed"); - TEST_ExpectTrue(testJSON.GetString("some_string", "other") == "other"); -} - -protected static function SubTest_NumberGetSetRemove() -{ - local JObject testJSON; - testJSON = _().json.newObject(); - testJSON.SetNumber("some_number", 3.5); - - Context("Testing `JObject`'s get/set/remove functions for" @ - "number variables"); - Issue("Number type isn't properly set by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Number); - - Issue("Value is incorrectly assigned by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetNumber("some_number") == 3.5); - - Issue( "Providing default variable value makes 'GetNumber'" @ - "return wrong value"); - TEST_ExpectTrue(testJSON.GetNumber("some_number", 5) == 3.5); - - Issue("Variable value isn't correctly reassigned by `SetNumber`"); - testJSON.SetNumber("some_number", 7); - TEST_ExpectTrue(testJSON.GetNumber("some_number") == 7); - - Issue( "Getting number variable as a wrong type" @ - "doesn't yield default value."); - TEST_ExpectTrue(testJSON.GetString("some_number", "default") == "default"); - - Issue("Number type isn't being properly removed"); - testJSON.RemoveValue("some_number"); - TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Undefined); - - Issue( "Getters don't return default value for missing key that" @ - "previously stored number value, that got removed"); - TEST_ExpectTrue(testJSON.GetNumber("some_number", 13) == 13); -} - -protected static function SubTest_NullGetSetRemove() -{ - local JObject testJSON; - testJSON = _().json.newObject(); - - Context("Testing `JObject`'s get/set/remove functions for" @ - "null values"); - Issue("Undefined variable is incorrectly considered `null`"); - TEST_ExpectFalse(testJSON.IsNull("some_var")); - - Issue("Number variable is incorrectly considered `null`"); - testJSON.SetNumber("some_var", 4); - TEST_ExpectFalse(testJSON.IsNull("some_var")); - - Issue("Boolean variable is incorrectly considered `null`"); - testJSON.SetBoolean("some_var", true); - TEST_ExpectFalse(testJSON.IsNull("some_var")); - - Issue("String variable is incorrectly considered `null`"); - testJSON.SetString("some_var", "string"); - TEST_ExpectFalse(testJSON.IsNull("some_var")); - - Issue("Null value is incorrectly assigned"); - testJSON.SetNull("some_var"); - TEST_ExpectTrue(testJSON.IsNull("some_var")); - - Issue("Null type isn't properly set by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Null); - - Issue("Null value isn't being properly removed."); - testJSON.RemoveValue("some_var"); - TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined); -} - -protected static function SubTest_MultipleVariablesGetSet() -{ - local int i; - local bool correctValue, allValuesCorrect; - local JObject testJSON; - testJSON = _().json.newObject(); - Context("Testing how `JObject` handles addition, change and removal" @ - "of relatively large (hundreds) number of variables"); - for (i = 0; i < 2000; i += 1) - { - testJSON.SetNumber("num" $ string(i), 4 * i*i - 2.6 * i + 0.75); - } - for (i = 0; i < 500; i += 1) - { - testJSON.SetString("num" $ string(i), "str" $ string(Sin(i))); - } - for (i = 1500; i < 2000; i += 1) - { - testJSON.RemoveValue("num" $ string(i)); - } - allValuesCorrect = true; - for (i = 0; i < 200; i += 1) - { - if (i < 500) - { - correctValue = ( testJSON.GetString("num" $ string(i)) - == ("str" $ string(Sin(i))) ); - Issue("Variables are incorrectly overwritten"); - } - else if(i < 1500) - { - correctValue = ( testJSON.GetNumber("num" $ string(i)) - == 4 * i*i - 2.6 * i + 0.75); - Issue("Variables are lost"); - } - else - { - correctValue = ( testJSON.GetTypeOf("num" $ string(i)) - == JSON_Undefined); - Issue("Variables aren't removed"); - } - if (!correctValue) - { - allValuesCorrect = false; - break; - } - } - TEST_ExpectTrue(allValuesCorrect); -} - -protected static function SubTest_Object() -{ - local JObject testObject; - Context("Testing setters and getters for folded objects"); - testObject = _().json.newObject(); - testObject.CreateObject("folded"); - testObject.GetObject("folded").CreateObject("folded"); - testObject.SetString("out", "string outside"); - testObject.GetObject("folded").SetNumber("mid", 8); - testObject.GetObject("folded") - .GetObject("folded") - .SetString("in", "string inside"); - - Issue("Addressing variables in root object doesn't work"); - TEST_ExpectTrue(testObject.GetString("out", "default") == "string outside"); - - Issue("Addressing variables in folded object doesn't work"); - TEST_ExpectTrue(testObject.GetObject("folded").GetNumber("mid", 1) == 8); - - Issue("Addressing plain variables in folded (twice) object doesn't work"); - TEST_ExpectTrue(testObject.GetObject("folded").GetObject("folded") - .GetString("in", "default") == "string inside"); -} - -protected static function SubTest_ArrayUndefined() -{ - local JArray testJSON; - testJSON = _().json.newArray(); - Context("Testing how `JArray` handles undefined values"); - Issue("Undefined variable doesn't have `JSON_Undefined` type"); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); - - Issue("There is a variable in an empty object after `GetTypeOf` call"); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); - - Issue("Negative index refers to a defined value"); - TEST_ExpectTrue(testJSON.GetTypeOf(-1) == JSON_Undefined); - - Issue("Getters don't return default values for undefined variables"); - TEST_ExpectTrue(testJSON.GetNumber(0, 0) == 0); - TEST_ExpectTrue(testJSON.GetString(0, "") == ""); - TEST_ExpectTrue(testJSON.GetBoolean(0, false) == false); - TEST_ExpectNone(testJSON.GetObject(0)); - TEST_ExpectNone(testJSON.GetArray(0)); - - Issue( "Getters don't return user-defined default values for" @ - "undefined variables"); - TEST_ExpectTrue(testJSON.GetNumber(0, 10) == 10); - TEST_ExpectTrue(testJSON.GetString(0, "test") == "test"); - TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true); -} - -protected static function SubTest_ArrayBooleanGetSetRemove() -{ - local JArray testJSON; - testJSON = _().json.newArray(); - testJSON.SetBoolean(0, true); - - Context("Testing `JArray`'s get/set/remove functions for" @ - "boolean variables"); - Issue("Boolean type isn't properly set by `SetBoolean`"); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Boolean); - - Issue("Value is incorrectly assigned by `SetBoolean`"); - TEST_ExpectTrue(testJSON.GetBoolean(0) == true); - testJSON.SetBoolean(0, false); - - Issue("Variable value isn't correctly reassigned by `SetBoolean`"); - TEST_ExpectTrue(testJSON.GetBoolean(0) == false); - - Issue( "Getting boolean variable as a wrong type" @ - "doesn't yield default value"); - TEST_ExpectTrue(testJSON.GetNumber(0, 7) == 7); - - Issue("Boolean variable isn't being properly removed"); - testJSON.RemoveValue(0); - TEST_ExpectTrue( testJSON.GetTypeOf(0) == JSON_Undefined); - - Issue( "Getters don't return default value for missing key that" @ - "previously stored boolean value, but got removed"); - TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true); -} - -protected static function SubTest_ArrayStringGetSetRemove() -{ - local JArray testJSON; - testJSON = _().json.newArray(); - testJSON.SetString(0, "first string"); - - Context("Testing `JArray`'s get/set/remove functions for" @ - "string variables"); - Issue("String type isn't properly set by `SetString`"); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_String); - - Issue("Value is incorrectly assigned by `SetString`"); - TEST_ExpectTrue(testJSON.GetString(0) == "first string"); - - Issue( "Providing default variable value makes 'GetString'" @ - "return incorrect value"); - TEST_ExpectTrue(testJSON.GetString(0, "alternative") == "first string"); - - Issue("Variable value isn't correctly reassigned by `SetString`"); - testJSON.SetString(0, "new string!~"); - TEST_ExpectTrue(testJSON.GetString(0) == "new string!~"); - - Issue( "Getting string variable as a wrong type" @ - "doesn't yield default value"); - TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true); - - Issue("Boolean variable isn't being properly removed"); - testJSON.RemoveValue(0); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); - - Issue( "Getters don't return default value for missing key that" @ - "previously stored string value, but got removed"); - TEST_ExpectTrue(testJSON.GetString(0, "other") == "other"); -} - -protected static function SubTest_ArrayNumberGetSetRemove() -{ - local JArray testJSON; - testJSON = _().json.newArray(); - testJSON.SetNumber(0, 3.5); - - Context("Testing `JArray`'s get/set/remove functions for" @ - "number variables"); - Issue("Number type isn't properly set by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Number); - - Issue("Value is incorrectly assigned by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetNumber(0) == 3.5); - - Issue( "Providing default variable value makes 'GetNumber'" @ - "return incorrect value"); - TEST_ExpectTrue(testJSON.GetNumber(0, 5) == 3.5); - - Issue("Variable value isn't correctly reassigned by `SetNumber`"); - testJSON.SetNumber(0, 7); - TEST_ExpectTrue(testJSON.GetNumber(0) == 7); - - Issue( "Getting number variable as a wrong type" @ - "doesn't yield default value"); - TEST_ExpectTrue(testJSON.GetString(0, "default") == "default"); - - Issue("Number type isn't being properly removed"); - testJSON.RemoveValue(0); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); - - Issue( "Getters don't return default value for missing key that" @ - "previously stored number value, but got removed"); - TEST_ExpectTrue(testJSON.GetNumber(0, 13) == 13); -} - -protected static function SubTest_ArrayNullGetSetRemove() -{ - local JArray testJSON; - testJSON = _().json.newArray(); - - Context("Testing `JArray`'s get/set/remove functions for" @ - "null values"); - - Issue("Undefined variable is incorrectly considered `null`"); - TEST_ExpectFalse(testJSON.IsNull(0)); - TEST_ExpectFalse(testJSON.IsNull(2)); - TEST_ExpectFalse(testJSON.IsNull(-1)); - - Issue("Number variable is incorrectly considered `null`"); - testJSON.SetNumber(0, 4); - TEST_ExpectFalse(testJSON.IsNull(0)); - - Issue("Boolean variable is incorrectly considered `null`"); - testJSON.SetBoolean(0, true); - TEST_ExpectFalse(testJSON.IsNull(0)); - - Issue("String variable is incorrectly considered `null`"); - testJSON.SetString(0, "string"); - TEST_ExpectFalse(testJSON.IsNull(0)); - - Issue("Null value is incorrectly assigned"); - testJSON.SetNull(0); - TEST_ExpectTrue(testJSON.IsNull(0)); - - Issue("Null type isn't properly set by `SetNumber`"); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Null); - - Issue("Null value isn't being properly removed"); - testJSON.RemoveValue(0); - TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined); -} - -// Returns following array: -// [10.0, "test string", "another string", true, 0.0, {"var": 7.0}] -protected static function JArray Prepare_Array() -{ - local JArray testArray; - testArray = _().json.newArray(); - testArray.AddNumber(10.0f) - .AddString("test string") - .AddString("another string") - .AddBoolean(true) - .AddNumber(0.0f) - .AddObject(); - testArray.GetObject(5).SetNumber("var", 7); - return testArray; -} - -protected static function SubTest_ArrayMultipleVariablesStorage() -{ - local JArray testArray; - testArray = Prepare_Array(); - - Context("Testing how `JArray` handles adding and" @ - "changing several variables"); - Issue("Stored values are compromised."); - TEST_ExpectTrue(testArray.GetNumber(0) == 10.0f); - TEST_ExpectTrue(testArray.GetString(1) == "test string"); - TEST_ExpectTrue(testArray.GetString(2) == "another string"); - TEST_ExpectTrue(testArray.GetBoolean(3) == true); - TEST_ExpectTrue(testArray.GetNumber(4) == 0.0f); - TEST_ExpectTrue(testArray.GetObject(5).GetNumber("var") == 7); - - Issue("Values incorrectly change their values."); - testArray.SetString(3, "new string"); - TEST_ExpectTrue(testArray.GetString(3) == "new string"); - - Issue( "After overwriting boolean value with a different type," @ - "attempting go get it as a boolean gives old value," @ - "instead of default"); - TEST_ExpectTrue(testArray.GetBoolean(3, false) == false); - - Issue("Type of the variable is incorrectly changed."); - TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_String); -} - -protected static function SubTest_ArrayMultipleVariablesRemoval() -{ - local JArray testArray; - testArray = Prepare_Array(); - // Test removing variables - // After `Prepare_Array`, our array should be: - // [10.0, "test string", "another string", true, 0.0, {"var": 7.0}] - - Context("Testing how `JArray` handles adding and" @ - "removing several variables"); - Issue("Values are incorrectly removed"); - testArray.RemoveValue(2); - // [10.0, "test string", true, 0.0, {"var": 7.0}] - Issue("Values are incorrectly removed"); - TEST_ExpectTrue(testArray.GetNumber(0) == 10.0); - TEST_ExpectTrue(testArray.GetString(1) == "test string"); - TEST_ExpectTrue(testArray.GetBoolean(2) == true); - TEST_ExpectTrue(testArray.GetNumber(3) == 0.0f); - TEST_ExpectTrue(testArray.GetTypeOf(4) == JSON_Object); - - Issue("First element incorrectly removed"); - testArray.RemoveValue(0); - // ["test string", true, 0.0, {"var": 7.0}] - TEST_ExpectTrue(testArray.GetString(0) == "test string"); - TEST_ExpectTrue(testArray.GetBoolean(1) == true); - TEST_ExpectTrue(testArray.GetNumber(2) == 0.0f); - TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Object); - TEST_ExpectTrue(testArray.GetObject(3).GetNumber("var") == 7.0); - - Issue("Last element incorrectly removed"); - testArray.RemoveValue(3); - // ["test string", true, 0.0] - TEST_ExpectTrue(testArray.GetLength() == 3); - TEST_ExpectTrue(testArray.GetString(0) == "test string"); - TEST_ExpectTrue(testArray.GetBoolean(1) == true); - TEST_ExpectTrue(testArray.GetNumber(2) == 0.0f); - - Issue("Removing all elements is handled incorrectly"); - testArray.RemoveValue(0); - testArray.RemoveValue(0); - testArray.RemoveValue(0); - TEST_ExpectTrue(testArray.Getlength() == 0); - TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Undefined); -} - -protected static function SubTest_ArrayRemovingMultipleVariablesAtOnce() -{ - local JArray testArray; - testArray = _().json.newArray(); - testArray.AddNumber(10.0f) - .AddString("test string") - .AddString("another string") - .AddNumber(7.0); - - Context("Testing how `JArray`' handles removing" @ - "multiple elements at once"); - Issue("Multiple values are incorrectly removed"); - testArray.RemoveValue(1, 2); - TEST_ExpectTrue(testArray.GetLength() == 2); - TEST_ExpectTrue(testArray.GetNumber(1) == 7.0); - - testArray.AddNumber(4.0f) - .AddString("test string") - .AddString("another string") - .AddNumber(8.0); - - // Current array: - // [10.0, 7.0, 4.0, "test string", "another string", 8.0] - Issue("Last value is incorrectly removed"); - testArray.RemoveValue(5, 1); - TEST_ExpectTrue(testArray.GetLength() == 5); - TEST_ExpectTrue(testArray.GetString(4) == "another string"); - - // Current array: - // [10.0, 7.0, 4.0, "test string", "another string"] - Issue("Tail elements are incorrectly removed"); - testArray.RemoveValue(3, 4); - TEST_ExpectTrue(testArray.GetLength() == 3); - TEST_ExpectTrue(testArray.GetNumber(0) == 10.0); - TEST_ExpectTrue(testArray.GetNumber(2) == 4.0); - - Issue("Array empties incorrectly"); - testArray.RemoveValue(0, testArray.GetLength()); - TEST_ExpectTrue(testArray.GetLength() == 0); - TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Undefined); - TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Undefined); -} - -protected static function SubTest_ArrayExpansions() -{ - local JArray testArray; - testArray = _().json.newArray(); - - Context("Testing how `JArray`' handles expansions/shrinking " @ - "via `SetLength()`"); - Issue("`SetLength()` doesn't properly expand empty array"); - testArray.SetLength(2); - TEST_ExpectTrue(testArray.GetLength() == 2); - TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); - - Issue("`SetLength()` doesn't properly expand non-empty array"); - testArray.AddNumber(1); - testArray.SetLength(4); - TEST_ExpectTrue(testArray.GetLength() == 4); - TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Number); - TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null); - TEST_ExpectTrue(testArray.GetNumber(2) == 1); - SubSubTest_ArraySetNumberExpansions(); - SubSubTest_ArraySetStringExpansions(); - SubSubTest_ArraySetBooleanExpansions(); -} - -protected static function SubSubTest_ArraySetNumberExpansions() -{ - local JArray testArray; - testArray = _().json.newArray(); - - Context("Testing how `JArray`' handles expansions via" @ - "`SetNumber()` function"); - Issue("Setters don't create correct first element"); - testArray.SetNumber(0, 1); - TEST_ExpectTrue(testArray.GetLength() == 1); - TEST_ExpectTrue(testArray.GetNumber(0) == 1); - - Issue( "`SetNumber()` doesn't properly define array when setting" @ - "value out-of-bounds"); - testArray = _().json.newArray(); - testArray.AddNumber(1); - testArray.SetNumber(4, 2); - TEST_ExpectTrue(testArray.GetLength() == 5); - TEST_ExpectTrue(testArray.GetNumber(0) == 1); - TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null); - TEST_ExpectTrue(testArray.GetNumber(4) == 2); - - Issue("`SetNumber()` expands array even when it told not to"); - testArray.SetNumber(6, 7, true); - TEST_ExpectTrue(testArray.GetLength() == 5); - TEST_ExpectTrue(testArray.GetNumber(6) == 0); - TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined); - TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined); -} - -protected static function SubSubTest_ArraySetStringExpansions() -{ - local JArray testArray; - testArray = _().json.newArray(); - - Context("Testing how `JArray`' handles expansions via" @ - "`SetString()` function"); - Issue("Setters don't create correct first element"); - testArray.SetString(0, "str"); - TEST_ExpectTrue(testArray.GetLength() == 1); - TEST_ExpectTrue(testArray.GetString(0) == "str"); - - Issue( "`SetString()` doesn't properly define array when setting" @ - "value out-of-bounds"); - testArray = _().json.newArray(); - testArray.AddString("str"); - testArray.SetString(4, "str2"); - TEST_ExpectTrue(testArray.GetLength() == 5); - TEST_ExpectTrue(testArray.GetString(0) == "str"); - TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null); - TEST_ExpectTrue(testArray.GetString(4) == "str2"); - - Issue("`SetString()` expands array even when it told not to"); - testArray.SetString(6, "new string", true); - TEST_ExpectTrue(testArray.GetLength() == 5); - TEST_ExpectTrue(testArray.GetString(6) == ""); - TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined); - TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined); -} - -protected static function SubSubTest_ArraySetBooleanExpansions() -{ - local JArray testArray; - testArray = _().json.newArray(); - - Context("Testing how `JArray`' handles expansions via" @ - "`SetBoolean()` function"); - Issue("Setters don't create correct first element"); - testArray.SetBoolean(0, false); - TEST_ExpectTrue(testArray.GetLength() == 1); - TEST_ExpectTrue(testArray.GetBoolean(0) == false); - - Issue( "`SetBoolean()` doesn't properly define array when setting" @ - "value out-of-bounds"); - testArray = _().json.newArray(); - testArray.AddBoolean(true); - testArray.SetBoolean(4, true); - TEST_ExpectTrue(testArray.GetLength() == 5); - TEST_ExpectTrue(testArray.GetBoolean(0) == true); - TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null); - TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null); - TEST_ExpectTrue(testArray.GetBoolean(4) == true); - - Issue("`SetBoolean()` expands array even when it told not to"); - testArray.SetBoolean(6, true, true); - TEST_ExpectTrue(testArray.GetLength() == 5); - TEST_ExpectTrue(testArray.GetBoolean(6) == false); - TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined); - TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined); -} - -defaultproperties -{ - caseName = "JSON" -} \ No newline at end of file diff --git a/sources/Core/Events/Broadcast/BroadcastEvents.uc b/sources/Core/Events/Broadcast/BroadcastEvents.uc deleted file mode 100644 index 61cacfc..0000000 --- a/sources/Core/Events/Broadcast/BroadcastEvents.uc +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Event generator for events, related to broadcasting messages - * through standard Unreal Script means: - * 1. text messages, typed by a player; - * 2. localized messages, identified by a LocalMessage class and id. - * Allows to make decisions whether or not to propagate certain messages. - * 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 BroadcastEvents extends Events - abstract; - -struct LocalizedMessage -{ - // Every localized message is described by a class and id. - // For example, consider 'KFMod.WaitingMessage': - // if passed 'id' is '1', - // then it's supposed to be a message about new wave, - // but if passed 'id' is '2', - // then it's about completing the wave. - var class class; - var int id; - // Localized messages in unreal script can be passed along with - // optional arguments, described by variables below. - var PlayerReplicationInfo relatedPRI1; - var PlayerReplicationInfo relatedPRI2; - var Object relatedObject; -}; - -static function bool CallCanBroadcast(Actor broadcaster, int recentSentTextSize) -{ - local int i; - local bool result; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0;i < listeners.length;i += 1) - { - result = class(listeners[i]) - .static.CanBroadcast(broadcaster, recentSentTextSize); - if (!result) return false; - } - return true; -} - -static function bool CallHandleText -( - Actor sender, - out string message, - name messageType -) -{ - local int i; - local bool result; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0;i < listeners.length;i += 1) - { - result = class(listeners[i]) - .static.HandleText(sender, message, messageType); - if (!result) return false; - } - return true; -} - -static function bool CallHandleTextFor -( - PlayerController receiver, - Actor sender, - out string message, - name messageType -) -{ - local int i; - local bool result; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0;i < listeners.length;i += 1) - { - result = class(listeners[i]) - .static.HandleTextFor(receiver, sender, message, messageType); - if (!result) return false; - } - return true; -} - -static function bool CallHandleLocalized -( - Actor sender, - LocalizedMessage message -) -{ - local int i; - local bool result; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0;i < listeners.length;i += 1) - { - result = class(listeners[i]) - .static.HandleLocalized(sender, message); - if (!result) return false; - } - return true; -} - -static function bool CallHandleLocalizedFor -( - PlayerController receiver, - Actor sender, - LocalizedMessage message -) -{ - local int i; - local bool result; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0;i < listeners.length;i += 1) - { - result = class(listeners[i]) - .static.HandleLocalizedFor(receiver, sender, message); - if (!result) return false; - } - return true; -} - -defaultproperties -{ - relatedListener = class'BroadcastListenerBase' -} \ No newline at end of file diff --git a/sources/Core/Events/Broadcast/BroadcastHandler.uc b/sources/Core/Events/Broadcast/BroadcastHandler.uc deleted file mode 100644 index fd98140..0000000 --- a/sources/Core/Events/Broadcast/BroadcastHandler.uc +++ /dev/null @@ -1,197 +0,0 @@ -/** - * 'BroadcastHandler' class that used by Acedia to catch - * broadcasting events. For Acedia to work properly it needs to be added to - * the very beginning of the broadcast handlers' chain. - * 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 . - */ -// TODO: make it work from any place in the chain. -class BroadcastHandler extends Engine.BroadcastHandler - dependson(BroadcastEvents); - -// The way vanilla 'BroadcastHandler' works - it can check if broadcast is -// possible for any actor, but for actually sending the text messages it will -// try to extract player's data from it -// and will simply pass 'none' if it can't. -// We remember senders in this array in order to pass real ones to our events. -// Array instead of variable is to account for folded calls -// (when handling of broadcast events leads to another message generation). -var private array storedSenders; - -// We want to insert our code in some of the functions between -// 'AllowsBroadcast' check and actual broadcasting, -// so we can't just use a 'super.AllowsBroadcast()' call. -// Instead we first manually do this check, then perform our logic and then -// make a super call, but with 'blockAllowsBroadcast' flag set to 'true', -// which causes overloaded 'AllowsBroadcast()' to omit actual checks. -var private bool blockAllowsBroadcast; - -// Functions below simply reroute vanilla's broadcast events to -// Acedia's 'BroadcastEvents', while keeping original senders -// and blocking 'AllowsBroadcast()' as described in comments for -// 'storedSenders' and 'blockAllowsBroadcast'. - -public function bool HandlerAllowsBroadcast(Actor broadcaster, int sentTextNum) -{ - local bool canBroadcast; - // Check listeners - canBroadcast = class'BroadcastEvents'.static - .CallCanBroadcast(broadcaster, sentTextNum); - // Check other broadcast handlers (if present) - if (canBroadcast && nextBroadcastHandler != none) - { - canBroadcast = nextBroadcastHandler - .HandlerAllowsBroadcast(broadcaster, sentTextNum); - } - return canBroadcast; -} - -function Broadcast(Actor sender, coerce string message, optional name type) -{ - local bool canTryToBroadcast; - if (!AllowsBroadcast(sender, Len(message))) - return; - canTryToBroadcast = class'BroadcastEvents'.static - .CallHandleText(sender, message, type); - if (canTryToBroadcast) - { - storedSenders[storedSenders.length] = sender; - blockAllowsBroadcast = true; - super.Broadcast(sender, message, type); - blockAllowsBroadcast = false; - storedSenders.length = storedSenders.length - 1; - } -} - -function BroadcastTeam -( - Controller sender, - coerce string message, - optional name type -) -{ - local bool canTryToBroadcast; - if (!AllowsBroadcast(sender, Len(message))) - return; - canTryToBroadcast = class'BroadcastEvents'.static - .CallHandleText(sender, message, type); - if (canTryToBroadcast) - { - storedSenders[storedSenders.length] = sender; - blockAllowsBroadcast = true; - super.BroadcastTeam(sender, message, type); - blockAllowsBroadcast = false; - storedSenders.length = storedSenders.length - 1; - } -} - -event AllowBroadcastLocalized -( - Actor sender, - class message, - optional int switch, - optional PlayerReplicationInfo relatedPRI1, - optional PlayerReplicationInfo relatedPRI2, - optional Object optionalObject -) -{ - local bool canTryToBroadcast; - local BroadcastEvents.LocalizedMessage packedMessage; - if (!AllowsBroadcast(sender, Len(message))) - return; - packedMessage.class = message; - packedMessage.id = switch; - packedMessage.relatedPRI1 = relatedPRI1; - packedMessage.relatedPRI2 = relatedPRI2; - packedMessage.relatedObject = optionalObject; - canTryToBroadcast = class'BroadcastEvents'.static - .CallHandleLocalized(sender, packedMessage); - if (canTryToBroadcast) - { - super.AllowBroadcastLocalized( sender, message, switch, - relatedPRI1, relatedPRI2, - optionalObject); - } -} - -function bool AllowsBroadcast(actor broadcaster, int len) -{ - if (blockAllowsBroadcast) - return true; - return super.AllowsBroadcast(broadcaster, len); -} - -function bool AcceptBroadcastText -( - PlayerController receiver, - PlayerReplicationInfo senderPRI, - out string message, - optional name type -) -{ - local bool canBroadcast; - local Actor sender; - if (senderPRI != none) - { - sender = PlayerController(senderPRI.owner); - } - if (sender == none && storedSenders.length > 0) - { - sender = storedSenders[storedSenders.length - 1]; - } - canBroadcast = class'BroadcastEvents'.static - .CallHandleTextFor(receiver, sender, message, type); - if (!canBroadcast) - { - return false; - } - return super.AcceptBroadcastText(receiver, senderPRI, message, type); -} - - -function bool AcceptBroadcastLocalized -( - PlayerController receiver, - Actor sender, - class message, - optional int switch, - optional PlayerReplicationInfo relatedPRI1, - optional PlayerReplicationInfo relatedPRI2, - optional Object obj -) -{ - local bool canBroadcast; - local BroadcastEvents.LocalizedMessage packedMessage; - packedMessage.class = message; - packedMessage.id = switch; - packedMessage.relatedPRI1 = relatedPRI1; - packedMessage.relatedPRI2 = relatedPRI2; - packedMessage.relatedObject = obj; - canBroadcast = class'BroadcastEvents'.static - .CallHandleLocalizedFor(receiver, sender, packedMessage); - if (!canBroadcast) - { - return false; - } - return super.AcceptBroadcastLocalized( receiver, sender, message, switch, - relatedPRI1, relatedPRI2, obj); -} - -defaultproperties -{ - blockAllowsBroadcast = false -} \ No newline at end of file diff --git a/sources/Core/Events/Broadcast/BroadcastListenerBase.uc b/sources/Core/Events/Broadcast/BroadcastListenerBase.uc deleted file mode 100644 index 6cf6b0d..0000000 --- a/sources/Core/Events/Broadcast/BroadcastListenerBase.uc +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Listener for events, related to broadcasting messages - * through standard Unreal Script means: - * 1. text messages, typed by a player; - * 2. localized messages, identified by a LocalMessage class and id. - * Allows to make decisions whether or not to propagate certain messages. - * 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 BroadcastListenerBase extends Listener - abstract; - -static final function PlayerController GetController(Actor sender) -{ - local Pawn senderPawn; - senderPawn = Pawn(sender); - if (senderPawn != none) return PlayerController(senderPawn.controller); - return PlayerController(sender); -} - -// This event is called whenever registered broadcast handlers are asked if -// they'd allow given actor ('broadcaster') to broadcast a text message, -// given that none so far rejected it and he recently already broadcasted -// or tried to broadcast 'recentSentTextSize' symbols of text -// (that value is periodically reset in 'GameInfo', -// by default should be each second). -// NOTE: this function is ONLY called when someone tries to -// broadcast TEXT messages. -// If one of the listeners returns 'false', - -// it will be treated just like one of broadcasters returning 'false' -// in 'AllowsBroadcast' and this method won't be called for remaining -// active listeners. -static function bool CanBroadcast(Actor broadcaster, int recentSentTextSize) -{ - return true; -} - -// This event is called whenever a someone is trying to broadcast -// a text message (typically the typed by a player). -// This function is called once per message and allows you to change it -// (by changing 'message' argument) before any of the players receive it. -// Return 'true' to allow the message through. -// If one of the listeners returns 'false', - -// it will be treated just like one of broadcasters returning 'false' -// in 'AcceptBroadcastText' and this method won't be called for remaining -// active listeners. -static function bool HandleText -( - Actor sender, - out string message, - optional name messageType -) -{ - return true; -} - -// This event is similar to 'HandleText', but is called for every player -// the message is sent to. -// If allows you to alter the message, but the changes are accumulated -// as events go through the players. -static function bool HandleTextFor -( - PlayerController receiver, - Actor sender, - out string message, - optional name messageType -) -{ - return true; -} - -// This event is called whenever a localized message is trying to -// get broadcasted to a certain player ('receiver'). -// Return 'true' to allow the message through. -// If one of the listeners returns 'false', - -// it will be treated just like one of broadcasters returning 'false' -// in 'AcceptBroadcastText' and this method won't be called for remaining -// active listeners. -static function bool HandleLocalized -( - Actor sender, - BroadcastEvents.LocalizedMessage message -) -{ - return true; -} - -// This event is similar to 'HandleLocalized', but is called for -// every player the message is sent to. -static function bool HandleLocalizedFor -( - PlayerController receiver, - Actor sender, - BroadcastEvents.LocalizedMessage message -) -{ - return true; -} - -defaultproperties -{ - relatedEvents = class'BroadcastEvents' -} - - // Text messages can (optionally) have their type specified. - // Examples of it are names 'Say' and 'CriticalEvent'. \ No newline at end of file diff --git a/sources/Core/Events/Events.uc b/sources/Core/Events/Events.uc deleted file mode 100644 index 4f6a0a0..0000000 --- a/sources/Core/Events/Events.uc +++ /dev/null @@ -1,159 +0,0 @@ -/** - * One of the two classes that make up a core of event system in Acedia. - * - * 'Events' (or it's child) class shouldn't be instantiated. - * Usually module would provide '...Events' class that defines - * certain set of static functions that can generate event calls to - * all it's active listeners. - * If you're simply using modules someone made, - - * you don't need to bother yourself with further specifics. - * If you wish to create your own event generator, - * then first create a '...ListenerBase' object - * (more about it in the description of 'Listener' class) - * and set 'relatedListener' variable to point to it's class. - * Then for each event create a caller function in your 'Event' class, - * following this template: - * ____________________________________________________________________________ - * | static function CallEVENT_NAME() - * | { - * | local int i; - * | local array< class > listeners; - * | listeners = GetListeners(); - * | for (i = 0; i < listeners.length; i += 1) - * | { - * | class<...ListenerBase>(listeners[i]) - * | .static.EVENT_NAME(); - * | } - * | } - * |___________________________________________________________________________ - * If each listener must indicate whether it gives it's permission for - * something to happen, then use this template: - * ____________________________________________________________________________ - * | static function CallEVENT_NAME() - * | { - * | local int i; - * | local bool result; - * | local array< class > listeners; - * | listeners = GetListeners(); - * | for (i = 0; i < listeners.length; i += 1) - * | { - * | result = class<...ListenerBase>(listeners[i]) - * | .static.EVENT_NAME(); - * | if (!result) return false; - * | } - * | return true; - * | } - * |___________________________________________________________________________ - * For concrete example look at - * 'MutatorEvents' and 'MutatorListenerBase'. - * 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 Events extends Object - abstract; - -var private array< class > listeners; - -var public const class relatedListener; - -// Even class can also auto-spawn a `Service`, -// in case it's require to generate events -var public const class connectedServiceClass; -// Set this to `true`if you want `connectedServiceClass` service to also -// auto-shutdown whenever no-one listens to the events. -var public const bool shutDownServiceWithoutListeners; - -static public final function array< class > GetListeners() -{ - return default.listeners; -} - -// Make given listener active. -// If listener was already activated also returns 'false'. -static public final function bool ActivateListener(class newListener) -{ - local int i; - if (newListener == none) return false; - if (!ClassIsChildOf(newListener, default.relatedListener)) return false; - - // Spawn service, if absent - if ( default.listeners.length == 0 - && default.connectedServiceClass != none) { - default.connectedServiceClass.static.Require(); - } - // Add listener - for (i = 0;i < default.listeners.length;i += 1) - { - if (default.listeners[i] == newListener) { - return false; - } - } - default.listeners[default.listeners.length] = newListener; - return true; -} - -// Make given listener inactive. -// If listener wasn't active returns 'false'. -static public final function bool DeactivateListener(class listener) -{ - local int i; - local bool removedListener; - local Service service; - if (listener == none) return false; - - // Remove listener - for (i = 0; i < default.listeners.length; i += 1) - { - if (default.listeners[i] == listener) - { - default.listeners.Remove(i, 1); - removedListener = true; - break; - } - } - // Remove unneeded service - if ( default.shutDownServiceWithoutListeners - && default.listeners.length == 0 - && default.connectedServiceClass != none) - { - service = Service(default.connectedServiceClass.static.GetInstance()); - if (service != none) { - service.Destroy(); - } - } - return removedListener; -} - -static public final function bool IsActiveListener(class listener) -{ - local int i; - if (listener == none) return false; - - for (i = 0; i < default.listeners.length; i += 1) - { - if (default.listeners[i] == listener) - { - return true; - } - } - return false; -} - -defaultproperties -{ - relatedListener = class'Listener' -} \ No newline at end of file diff --git a/sources/Core/Events/Listener.uc b/sources/Core/Events/Listener.uc deleted file mode 100644 index b5f0fe6..0000000 --- a/sources/Core/Events/Listener.uc +++ /dev/null @@ -1,59 +0,0 @@ -/** - * One of the two classes that make up a core of event system in Acedia. - * - * 'Listener' (or it's child) class shouldn't be instantiated. - * Usually module would provide '...ListenerBase' class that defines - * certain set of static functions, corresponding to events it can listen to. - * In order to handle those events you must create it's child class and - * override said functions. But they will only be called if - * 'SetActive(true)' is called for that child class. - * To create you own '...ListenerBase' class you need to define - * a static function for each event you wish it to catch and - * set 'relatedEvents' variable to point at the 'Events' class - * that will generate your events. - * For concrete example look at - * 'ConnectionEvents' and 'ConnectionListenerBase'. - * Copyright 2019 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 Listener extends Object - abstract; - -var public const class relatedEvents; - - -static public final function SetActive(bool active) -{ - if (active) - { - default.relatedEvents.static.ActivateListener(default.class); - } - else - { - default.relatedEvents.static.DeactivateListener(default.class); - } -} - -static public final function IsActive(bool active) -{ - default.relatedEvents.static.IsActiveListener(default.class); -} - -defaultproperties -{ - relatedEvents = class'Events' -} \ No newline at end of file diff --git a/sources/Core/Events/Mutator/MutatorEvents.uc b/sources/Core/Events/Mutator/MutatorEvents.uc deleted file mode 100644 index 542c320..0000000 --- a/sources/Core/Events/Mutator/MutatorEvents.uc +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Event generator that repeats events of a mutator. - * Copyright 2019 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 MutatorEvents extends Events - abstract; - -static function bool CallCheckReplacement(Actor other, out byte isSuperRelevant) -{ - local int i; - local bool result; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0; i < listeners.length; i += 1) - { - result = class(listeners[i]) - .static.CheckReplacement(other, isSuperRelevant); - if (!result) return false; - } - return true; -} - -static function bool CallMutate(string command, PlayerController sendingPlayer) -{ - local int i; - local bool result; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0; i < listeners.length;i += 1) - { - result = class(listeners[i]) - .static.Mutate(command, sendingPlayer); - if (!result) return false; - } - return true; -} - -defaultproperties -{ - relatedListener = class'MutatorListenerBase' -} \ No newline at end of file diff --git a/sources/Core/Events/Mutator/MutatorListenerBase.uc b/sources/Core/Events/Mutator/MutatorListenerBase.uc deleted file mode 100644 index 74c4311..0000000 --- a/sources/Core/Events/Mutator/MutatorListenerBase.uc +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Listener for events, normally propagated by mutators. - * Copyright 2019 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 MutatorListenerBase extends Listener - abstract; - -// This event is called whenever 'CheckReplacement' -// check is propagated through mutators. -// If one of the listeners returns 'false', - -// it will be treated just like a mutator returning 'false' -// in 'CheckReplacement' and -// this method won't be called for remaining active listeners. -static function bool CheckReplacement(Actor other, out byte isSuperRelevant) -{ - return true; -} - -// This event is called whenever 'Mutate' is propagated through mutators. -// If one of the listeners returns 'false', - -// this method won't be called for remaining active listeners or mutators. -// If all listeners return 'true', - -// mutate command will be further propagated to the rest of the mutators. -static function bool Mutate(string command, PlayerController sendingPlayer) -{ - return true; -} - -defaultproperties -{ - relatedEvents = class'MutatorEvents' -} \ No newline at end of file diff --git a/sources/Core/Feature.uc b/sources/Core/Feature.uc deleted file mode 100644 index fc29a7b..0000000 --- a/sources/Core/Feature.uc +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Feature represents a certain subset of Acedia's functionality that - * can be enabled or disabled, according to server owner's wishes. - * In the current version of Acedia enabling or disabling a feature requires - * manually editing configuration file and restarting a server. - * Factually feature is just a collection of settings with one universal - * 'isActive' setting that tells Acedia whether or not to load a feature. - * Copyright 2019 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 Feature extends Singleton - abstract - config(Acedia); - -// Setting that tells Acedia whether or not to enable this feature -// during initialization. -// Only it's default value is ever used. -var private config bool autoEnable; - -// Listeners listed here will be automatically activated. -var public const array< class > requiredListeners; - -// Sets whether to enable this feature by default. -public static final function SetAutoEnable(bool doEnable) -{ - default.autoEnable = doEnable; - StaticSaveConfig(); -} - -public static final function bool IsAutoEnabled() -{ - return default.autoEnable; -} - -// Whether feature is enabled is determined by -public static final function bool IsEnabled() -{ - return (GetInstance() != none); -} - -// Enables feature of given class. -public static final function Feature EnableMe() -{ - local Feature newInstance; - if (IsEnabled()) - { - return Feature(GetInstance()); - } - default.blockSpawning = false; - newInstance = class'Acedia'.static.GetInstance().Spawn(default.class); - default.blockSpawning = true; - return newInstance; -} - -public static final function bool DisableMe() -{ - local Feature myself; - myself = Feature(GetInstance()); - if (myself != none) - { - myself.Destroy(); - return true; - } - return false; -} - -// Event functions that are called when -protected function OnEnabled(){} -protected function OnDisabled(){} - -// Set listeners' status -private static function SetListenersActiveSatus(bool newStatus) -{ - local int i; - for (i = 0; i < default.requiredListeners.length; i += 1) - { - if (default.requiredListeners[i] == none) continue; - default.requiredListeners[i].static.SetActive(newStatus); - } -} - -protected function OnCreated() -{ - default.blockSpawning = true; - SetListenersActiveSatus(true); - OnEnabled(); -} - -protected function OnDestroyed() -{ - SetListenersActiveSatus(false); - OnDisabled(); -} - -defaultproperties -{ - autoEnable = false - DrawType = DT_None - // Prevent spawning this feature by any other means than 'EnableMe()'. - blockSpawning = true - // Features are server-only actors - remoteRole = ROLE_None -} \ No newline at end of file diff --git a/sources/Core/Logger/LoggerAPI.uc b/sources/Core/Logger/LoggerAPI.uc deleted file mode 100644 index 8b2f5d8..0000000 --- a/sources/Core/Logger/LoggerAPI.uc +++ /dev/null @@ -1,92 +0,0 @@ -/** - * API that provides functions quick access to Acedia's - * logging functionality. - * 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 LoggerAPI extends Singleton; - -var private LoggerService logService; - -protected function OnCreated() -{ - logService = LoggerService(class'LoggerService'.static.Require()); -} - -public final function Track(string message) -{ - if (logService == none) - { - class'LoggerService'.static.LogMessageToKFLog(LOG_Track, message); - return; - } - logService.LogMessage(LOG_Track, message); -} - -public final function Debug(string message) -{ - if (logService == none) - { - class'LoggerService'.static.LogMessageToKFLog(LOG_Debug, message); - return; - } - logService.LogMessage(LOG_Debug, message); -} - -public final function Info(string message) -{ - if (logService == none) - { - class'LoggerService'.static.LogMessageToKFLog(LOG_Info, message); - return; - } - logService.LogMessage(LOG_Info, message); -} - -public final function Warning(string message) -{ - if (logService == none) - { - class'LoggerService'.static.LogMessageToKFLog(LOG_Warning, message); - return; - } - logService.LogMessage(LOG_Warning, message); -} - -public final function Failure(string message) -{ - if (logService == none) - { - class'LoggerService'.static.LogMessageToKFLog(LOG_Failure, message); - return; - } - logService.LogMessage(LOG_Failure, message); -} - -public final function Fatal(string message) -{ - if (logService == none) - { - class'LoggerService'.static.LogMessageToKFLog(LOG_Fatal, message); - return; - } - logService.LogMessage(LOG_Fatal, message); -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Logger/LoggerService.uc b/sources/Core/Logger/LoggerService.uc deleted file mode 100644 index 3dd8d85..0000000 --- a/sources/Core/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_Trace && 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_Error && 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_Trace: - 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_Error: - 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_Trace: - 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_Error: - 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/Core/Memory/MemoryAPI.uc b/sources/Core/Memory/MemoryAPI.uc deleted file mode 100644 index 810e72a..0000000 --- a/sources/Core/Memory/MemoryAPI.uc +++ /dev/null @@ -1,290 +0,0 @@ -/** - * API that provides functions for managing objects and actors by providing - * easy and general means to create and destroy them, that allow to make use of - * temporary `Object`s in a more efficient way. - * This is a low-level API that most users of Acedia, most likely, - * would not have to use, since creation of most objects would use their own - * wrapper functions around this API. - * 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 MemoryAPI extends Singleton; - -// This variable counts ticks and should be different each new tick. -var private int currentTick; - -// Stores instance of an `Object` that can be borrowed from the pool. -struct BorrowableRecord -{ - // Borrowable instance - var Object instance; - // Was this object borrowed? - // This flag will persist unless object was explicitly freed, - // even if borrowed reference timed out. - var bool borrowed; - // When was this object borrowed? - // Used to automatically free borrowed objects after the tick has passed. - var int borrowTick; -}; - -// Available object pools -var private array borrowPool; - -// Checks if instance in the given `record` is borrowed. -private final function bool IsBorrowed(BorrowableRecord record) -{ - // `record.borrowed` means instance was borrowed, - // but not explicitly freed; - // `record.borrowTick >= currentTick` means that rights to the borrowed - // instance hasn't yet ran out. - return (record.borrowed && record.borrowTick >= currentTick); -} - -// Loads a reference to class instance from it's string representation. -private final function class LoadClass(string classReference) -{ - return class(DynamicLoadObject(classReference, class'Class', true)); -} - -/** - * Creates a new `Object` / `Actor` of a given class. - * - * If uses a proper spawning mechanism for both objects (`new`) - * and actors (`Spawn`). - * - * @param classToAllocate Class of the `Object` / `Actor` that this method - * must create. - * @return Newly created object, might be `none` if creation has failed. - */ -public final function Object Allocate(class classToAllocate) -{ - local class actorClassToSpawn; - if (classToAllocate == none) return none; - - actorClassToSpawn = class(classToAllocate); - if (actorClassToSpawn != none) - { - return Spawn(actorClassToSpawn); - } - return (new classToAllocate); -} - -/** - * Creates a new `Object` / `Actor` of a given class. - * - * If uses a proper spawning mechanism for both objects (`new`) - * and actors (`Spawn`). - * - * @param classToAllocate Text representation (name) of the class of the - * `Object` / `Actor` that this method must create. - * Should contain full package-path. - * @return Newly created object, might be `none` if creation has failed. - */ -public final function Object AllocateByReference(string refToClassToAllocate) -{ - return Allocate(LoadClass(refToClassToAllocate)); -} - -/** - * Borrows an instance of an `Object` / `Actor` of the given class - * from the pool. - * Borrowed instance will be auto-freed during next tick. - * - * @param classToBorrow Class of an `Object` / `Actor` we want to borrow. - * @return Borrowed object, might be `none` if borrow pool is empty and - * creation of a new `Object` / `Actor` has failed. - */ -public final function Object Borrow(class classToBorrow) -{ - local int i; - local BorrowableRecord newRecord; - for (i = 0; i < borrowPool.length; i += 1) - { - if (IsBorrowed(borrowPool[i])) continue; - if (borrowPool[i].instance == none) continue; - if (borrowPool[i].instance.class != classToBorrow) continue; - - borrowPool[i].borrowed = true; - borrowPool[i].borrowTick = currentTick; - return borrowPool[i].instance; - } - // Create a new instance to borrow, if there isn't any available for - // the given class. - newRecord.borrowed = false; - newRecord.instance = Allocate(classToBorrow); - if (newRecord.instance != none) - { - borrowPool[borrowPool.length] = newRecord; - } - return newRecord.instance; -} - -/** - * Borrows an instance of an `Object` / `Actor` of the given class - * from the pool. - * Borrowed instance will be auto-freed during next tick. - * - * @param classToBorrow Text representation (name) of the class of - * an `Object` / `Actor` we want to borrow. - * @return Borrowed object, might be `none` if borrow pool is empty and - * creation of a new `Object` / `Actor` has failed. - */ -public final function Object BorrowByReference(string refToClassToBorrow) -{ - return Borrow(LoadClass(refToClassToBorrow)); -} - -/** - * Claims an instance of an `Object` / `Actor` of the given class - * from the pool. - * Claimed instances are removed from the borrow pool and - * will not be automatically freed. - * - * @param classToClaim Class of an `Object` / `Actor` we wish to borrow. - * @return Borrowed object, might be `none` if borrow pool is empty and - * creation of a new `Object` / `Actor` has failed. - */ -public final function Object Claim(class classToClaim) -{ - local int i; - local Object instance; - for (i = 0; i < borrowPool.length; i += 1) - { - if (IsBorrowed(borrowPool[i])) continue; - if (borrowPool[i].instance == none) continue; - if (borrowPool[i].instance.class != classToClaim) continue; - - instance = borrowPool[i].instance; - borrowPool.Remove(i, 1); - return instance; - } - // Create a new instance to borrow, if there isn't any available for - // the given class. - return Allocate(classToClaim); -} - -/** - * Claims an instance of an `Object` / `Actor` of the given class - * from the pool. - * Claimed instances are removed from the borrow pool and - * will not be automatically freed. - * - * @param classToClaim Text representation (name) of the class of - * an `Object` / `Actor` we wish to claim. - * @return Borrowed object, might be `none` if borrow pool is empty and - * creation of a new `Object` / `Actor` has failed. - */ -public final function Object ClaimByReference(string refToClassToClaim) -{ - return Claim(LoadClass(refToClassToClaim)); -} - -/** - * Frees given `Object` / `Actor` resource. - * - * By default `Actor`s are destroyed. - * Due to limitations of the engine objects cannot be outright destroyed. - * Instead, they are put into a "borrow pool", from where they can later be - * taken for a reuse. - * - * @param objectToDelete `Object` / `Actor` that must be freed. - * @param forceMakeBorrowable Only has an effect if `objectToDelete` - * is an `Actor`, in which case it forces it to be added - * to the borrow pool, instead of being destroyed. - */ -public final function Free -( - Object objectToDelete, - optional bool forceMakeBorrowable -) -{ - local int i; - local Actor actorToDelete; - local BorrowableRecord newRecord; - if (objectToDelete == none) return; - - actorToDelete = Actor(objectToDelete); - if (actorToDelete != none && !forceMakeBorrowable) - { - actorToDelete.Destroy(); - return; - } - // Check if `objectToDelete` is already in our records. - for (i = 0; i < borrowPool.length; i += 1) - { - if (borrowPool[i].instance == objectToDelete) - { - borrowPool[i].borrowed = false; - return; - } - } - // If not - add it - newRecord.instance = objectToDelete; - newRecord.borrowed = false; - borrowPool[borrowPool.length] = newRecord; -} - -/** - * Forces Unreal Engine to do garbage collection. - * By default also cleans up all the objects in the borrow object pool. - * - * Process of garbage collection causes significant lag spike during the game - * and should be used carefully. - * - * NOTE: method does not guarantee that borrow pool will be empty after - * this call (even with `keepBorrowedObjectPool = true`), - * since some of the borrowable objects might be currently in use and, - * therefore, cannot be garbage collected. - * - * @param keepBorrowedObjectPool Set this to `true` to NOT garbage collect - * objects in a borrow pool. Otherwise keep it `false`. - */ -public final function CollectGarbage(optional bool keepBorrowedObjectPool) -{ - local int i; - if (!keepBorrowedObjectPool) - { - // Dereference all non-borrowed objects from borrow pool, - // so that they can be garbage collected. - i = 0; - while (i < borrowPool.length) - { - if ( borrowPool[i].instance == none - || !IsBorrowed(borrowPool[i]) ) - { - borrowPool.Remove(i, 1); - } - else - { - i += 1; - } - } - } - // This makes Unreal Engine do garbage collection - ConsoleCommand("obj garbage"); -} - -event Tick(float delta) -{ - currentTick += 1; -} - -// TODO: add cleaning on cooldown -defaultproperties -{ - currentTick = 0 -} \ No newline at end of file diff --git a/sources/Core/Service.uc b/sources/Core/Service.uc deleted file mode 100644 index ed435e1..0000000 --- a/sources/Core/Service.uc +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Parent class for all services used in Acedia. - * Currently simply makes itself server-only. - * 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 Service extends Singleton - abstract; - -// Listeners listed here will be automatically activated. -var public const array< class > requiredListeners; - -// Enables feature of given class. -public static final function Service Require() -{ - local Service newInstance; - if (IsRunning()) - { - return Service(GetInstance()); - } - default.blockSpawning = false; - newInstance = class'Acedia'.static.GetInstance().Spawn(default.class); - default.blockSpawning = true; - return newInstance; -} - -// Whether service is currently running is determined by -public static final function bool IsRunning() -{ - return (GetInstance() != none); -} - -protected function OnLaunch(){} -protected function OnShutdown(){} - -protected function OnCreated() -{ - default.blockSpawning = true; - SetListenersActiveSatus(true); - OnLaunch(); -} - -protected function OnDestroyed() -{ - SetListenersActiveSatus(false); - OnShutdown(); -} - -// Set listeners' status -private static function SetListenersActiveSatus(bool newStatus) -{ - local int i; - for (i = 0; i < default.requiredListeners.length; i += 1) - { - if (default.requiredListeners[i] == none) continue; - default.requiredListeners[i].static.SetActive(newStatus); - } -} - -defaultproperties -{ - DrawType = DT_None - // Prevent spawning this feature by any other means than 'Launch()'. - blockSpawning = true - // Features are server-only actors - remoteRole = ROLE_None -} \ No newline at end of file diff --git a/sources/Core/Singleton.uc b/sources/Core/Singleton.uc deleted file mode 100644 index bd83fa7..0000000 --- a/sources/Core/Singleton.uc +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Singleton is an auxiliary class, meant to be used as a base for others, - * that allows for only one instance of it to exist. - * To make sure your child class properly works, either don't overload - * 'PreBeginPlay' or make sure to call it's parent's version. - * Copyright 2019 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 Singleton extends AcediaActor - abstract; - -// Default value of this variable will store one and only existing version -// of actor of this class. -var private Singleton activeInstance; - -// Setting default value of this variable to 'true' prevents creation of -// a singleton, even if no instances of it exist. -// Only a default value is ever used. -var protected bool blockSpawning; - -public final static function Singleton GetInstance(optional bool spawnIfMissing) -{ - local bool instanceExists; - instanceExists = default.activeInstance != none - && !default.activeInstance.bPendingDelete; - if (instanceExists) { - return default.activeInstance; - } - if (spawnIfMissing) { - return class'Acedia'.static.GetInstance().Spawn(default.class); - } - return none; -} - -public final static function bool IsSingletonCreationBlocked() -{ - return default.blockSpawning; -} - -protected function OnCreated(){} -protected function OnDestroyed(){} - -// Make sure only one instance of 'Singleton' exists at any point in time. -// Instead of overloading this function we suggest you overload a special -// event function `OnCreated()` that is called whenever a valid `Singleton` -// instance is spawned. -// If you absolutely must overload this function in any child class - -// first call this version of the method and then check if -// you are about to be deleted 'bDeleteMe == true': -// ____________________________________________________________________________ -// | super.PreBeginPlay(); -// | // ^^^ If singleton wasn't already created, - only after that call -// | // will instance, returned by 'GetInstance()', be set. -// | if (bDeleteMe) -// | return; -// |___________________________________________________________________________ -event PreBeginPlay() -{ - super.PreBeginPlay(); - if (default.blockSpawning || GetInstance() != none) - { - Destroy(); - } - else - { - default.activeInstance = self; - OnCreated(); - } -} - -// Make sure only one instance of 'Singleton' exists at any point in time. -// Instead of overloading this function we suggest you overload a special -// event function `OnDestroyed()` that is called whenever a valid `Singleton` -// instance is destroyed. -// If you absolutely must overload this function in any child class - -// first call this version of the method. -event Destroyed() -{ - super.Destroyed(); - if (self == default.activeInstance) - { - OnDestroyed(); - default.activeInstance = none; - } -} - -defaultproperties -{ - blockSpawning = false -} \ No newline at end of file diff --git a/sources/Core/Testing/IssueSummary.uc b/sources/Core/Testing/IssueSummary.uc deleted file mode 100644 index 27e20d7..0000000 --- a/sources/Core/Testing/IssueSummary.uc +++ /dev/null @@ -1,294 +0,0 @@ -/** - * Class for storing and processing the information about how well testing - * against a certain issue went. - * 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 IssueSummary extends AcediaObject; - -// Each issue is uniquely identified by these values. -var private class ownerCase; -var private string context; -var private string description; - -// Records, in chronological order, results of the tests that were -// run to test this issue. -var private array successRecords; - -private final function byte BoolToByte(bool boolToConvert) -{ - if (boolToConvert) return 1; - return 0; -} - -/** - * Sets `TestCase`, context and description for the issue, - * tracked in this summary. - * - * Can only be successfully called once, but will fail if passed a `none` - * class reference to `TestCase`. - * - * @param targetCase `TestCase`, in which issue, - * relevant to this summary, is defined. - * @param targetContext Context, in which this issue, - * relevant to this summary, is defined. - * @param targetDescription Description of the issue relevant to - * this summary. - * @return `true` if `TestCase`, context and description were successfully set, - * `false` otherwise. - */ -public final function bool SetIssue( - class targetCase, - string targetContext, - string targetDescription -) -{ - if (ownerCase != none) return false; - if (initCase == none) return false; - ownerCase = targetCase; - context = targetContext; - description = targetDescription; - return true; -} - -/** - * Returns context for the issue in question. - * - * `TestCase` can be important for both displaying information about testing to - * the user and distinguishing between two different issues with the same - * description and context. - * @see `TestCase` for more information. - * - * @return Test case that tested for relevant issue. - */ -public final function class GetTestCase() -{ - return ownerCase; -} - -/** - * Returns context for the issue in question. - * - * Context can be important for both displaying information about testing to - * the user and distinguishing between two different issues with - * the same description and in the same `TestCase`. - * @see `TestCase` for more information. - * - * @return Context for relevant issue. - */ -public final function string GetContext() -{ - if (ownerCase == none) return ""; - return context; -} - -/** - * Returns description for the issue in question. - * - * Description of an issue is the main way to distinguish between - * different possibly arising problems. - * Two different issues can have the same description if they are defined - * in different `TestCase`s and/or in different context. - * @see `TestCase` for more information. - * - * @return Description for the issue in question. - */ -public final function string GetDescription() -{ - if (ownerCase == none) return ""; - return description; -} - -/** - * Adds result of another test (success or not) to the records of this summary. - * - * @param success `true` if test was successful and had passed, - * `false` otherwise. - */ -public final function AddTestResult(bool success) -{ - successRecords[successRecords.length] = BoolToByte(success); -} - -/** - * Returns total amount of test results recorded in caller summary. - * Never a negative value. - * - * @return Amount of tests that were run. - */ -public final function int GetTotalTestsAmount() -{ - return successRecords.length; -} - -/** - * Returns total amount of recorded successful test results in caller summary. - * Never a negative value. - * - * @return Amount of recorded successfully performed tests for - * the relevant issue. - */ -public final function int GetSuccessfulTestsAmount() -{ - local int i; - local int counter; - counter = 0; - for (i = 0; i < successRecords.length; i += 1) - { - if (successRecords[i] > 0) { - counter += 1; - } - } - return counter; -} - -/** - * Returns total amount of recorded failed test results in caller summary. - * Never a negative value. - * - * @return Amount of recorded failed tests for the relevant issue. - */ -public final function int GetFailedTestsAmount() -{ - return GetTotalTestsAmount() - GetSuccessfulTestsAmount(); -} - -/** - * Returns total success rate ("amount of successes" / "total amount of tests") - * of recorded test results for relevant issue - * (value between 0 and 1, including boundaries). - * - * If there are no test results recorded - returns `-1`. - * - * @return Success rate of recorded test results for the relevant issue - * Returns values outside [0; 1] segment (specifically, negative values) - * iff no test results at all were recorded. - */ -public final function float GetSuccessRate() -{ - local int totalTestsAmount; - totalTestsAmount = GetTotalTestsAmount(); - if (totalTestsAmount <= 0) { - return -1; - } - return GetSuccessfulTestsAmount() / totalTestsAmount; -} - -/** - * Checks whether all tests recorded in this summary have passed. - * - * @return `true` if all tests for relevant issue have passed, - * `false` otherwise. - */ -public final function bool HasPassedAllTests() -{ - return (GetFailedTestsAmount() <= 0); -} - -/** - * Returns boolean array of test results: each element recording whether test - * was a success (`>0`) or a failure (`0`). - * - * All results in the array are in a chronological order of arrival. - * - * @return Returns copy of boolean array of recorded test results. - */ -public final function array GetTestRecords() -{ - return successRecords; -} - -/** - * Returns index numbers (starting from 1, not 0) of tests that ended in - * a success, while performed for the same test case, context and issue. - * So if tests went: [success, success, failure, success, failure], - * method will return: [1, 2, 4]. - * - * All results in the array are in a chronological order of arrival. - * - * @return index numbers of successful tests. - */ -public final function array GetSuccessfulTests() -{ - local int i; - local array result; - for (i = 0; i < successRecords.length; i += 1) - { - if (successRecords[i] > 0) { - result[result.length] = i + 1; - } - } - return result; -} - -/** - * Returns index numbers (starting from 1, not 0) of tests that ended in - * a failure, while performed for the same test case, context and issue. - * So if tests went: [success, success, failure, success, failure], - * method will return: [3, 5]. - * - * All results in the array are in a chronological order of arrival. - * - * @return index numbers of successful tests. - */ -public final function array GetFailedTests() -{ - local int i; - local array result; - for (i = 0; i < successRecords.length; i += 1) - { - if (successRecords[i] == 0) { - result[result.length] = i + 1; - } - } - return result; -} - -/** - * Returns a formatted text representation of the caller `IssueSummary` - * in a following format: - * "{$text_default } {$text_subtle []}" - * - * @return Formatted string with text representation of the - * caller `IssueSummary`. - */ -public final function string ToString() -{ - local int i; - local string result; - local array failedTests; - result = "{$text_default" @ GetDescription() $ "}"; - if (GetFailedTestsAmount() <= 0) { - return result; - } - result @= "{$text_subtle ["; - failedTests = GetFailedTests(); - for (i = 0; i < failedTests.length; i += 1) - { - if (i < failedTests.length - 1) { - result $= string(failedTests[i]) $ ", "; - } - else { - result $= string(failedTests[i]); - } - } - return (result $ "]"); -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Testing/Service/TestingEvents.uc b/sources/Core/Testing/Service/TestingEvents.uc deleted file mode 100644 index 167f14b..0000000 --- a/sources/Core/Testing/Service/TestingEvents.uc +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Event generator for events related to testing. - * 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 TestingEvents extends Events - abstract; - -static function CallTestingBegan(array< class > testQueue) -{ - local int i; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0; i < listeners.length; i += 1) - { - class(listeners[i]) - .static.TestingBegan(testQueue); - } -} - -static function CallCaseTested( - class testedCase, - TestCaseSummary result) -{ - local int i; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0; i < listeners.length; i += 1) - { - class(listeners[i]) - .static.CaseTested(testedCase, result); - } -} - -static function CallTestingEnded( - array< class > testQueue, - array results) -{ - local int i; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0; i < listeners.length; i += 1) - { - class(listeners[i]) - .static.TestingEnded(testQueue, results); - } -} - -defaultproperties -{ - relatedListener = class'TestingListenerBase' -} \ No newline at end of file diff --git a/sources/Core/Testing/Service/TestingService.uc b/sources/Core/Testing/Service/TestingService.uc deleted file mode 100644 index 896a932..0000000 --- a/sources/Core/Testing/Service/TestingService.uc +++ /dev/null @@ -1,253 +0,0 @@ -/** - * This service allows to separate running separate `TestCase`s in separate - * ticks, which helps to avoid hang ups or false infinite loop detection. - * 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 TestingService extends Service - config(AcediaSystem); - -// All test cases, loaded from all available packages. -// Always use `default` copy of this array. -var private array< class > registeredTestCases; - -// Will be `true` if we have yet more tests to run -// (either during current or following ticks) -var private bool runningTests; -// Queue with all test cases for the current/next testing -var private array< class > testCasesToRun; -// Track which test case we need to execute during next tick -var private int nextTestCase; - -// Record test results during the last test run here. -// After testing has finished - copy them into it's default value -// `default.summarizedResults` to be available even after `TestingService` -// shuts down. -var private array summarizedResults; - -// Configuration variables that tell Acedia what tests to run -// (and whether to run any at all) on start up. -var public config const bool runTestsOnStartUp; -var public config const bool filterTestsByName; -var public config const bool filterTestsByGroup; -var public config const string requiredName; -var public config const string requiredGroup; - -// Shortcut to `TestingEvents`, so that we don't have to write -// class'TestingEvents' every time. -var const class events; - -/** - * Registers another `TestCase` class for later testing. - * - * @return `true` if registration was successful. - */ -public final static function bool RegisterTestCase(class newTestCase) -{ - local int i; - if (newTestCase == none) return false; - - for (i = 0; i < default.registeredTestCases.length; i += 1) - { - if (default.registeredTestCases[i] == newTestCase) { - return false; - } - // Warn if there are test cases with the same name and group - if ( !(default.registeredTestCases[i].static.GetGroup() - ~= newTestCase.static.GetGroup())) { - continue; - } - if ( !(default.registeredTestCases[i].static.GetName() - ~= 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."); - } - default.registeredTestCases[default.registeredTestCases.length] = - newTestCase; - return true; -} - -/** - * Checks whether service is still in the process of running tests. - * - * @return `true` if there are still some tests that are scheduled, but - * were not yet ran and `false` otherwise. - */ -public final static function bool IsRunningTests() -{ - local TestingService myInstance; - myInstance = TestingService(class'TestingService'.static.GetInstance()); - if (myInstance == none) return false; - - return myInstance.runningTests; -} - -/** - * Returns the results of the last tests run. - * - * If no tests were run - returns an empty array. - * - * @return Results of the last tests run. - */ -public final static function array GetLastResults() -{ - return default.summarizedResults; -} - -/** - * Adds all tests to the testing queue. - * - * To actually run them use `Run()`. - * To only run certain tests, - filter them by `FilterByName()` - * and `FilterByGroup()` - * - * Will do nothing if service is already in the process of testing - * (`IsRunningTests() == true`). - * - * @return Caller `TestService` to allow for method chaining. - */ -public final function TestingService PrepareTests() -{ - if (runningTests) { - return self; - } - testCasesToRun = default.registeredTestCases; - return self; -} - -/** - * Filters tests in current queue to only those that have a specific name. - * Should be used after `PrepareTests()` call, but before `Run()`. - * - * Will do nothing if service is already in the process of testing - * (`IsRunningTests() == true`). - * - * @return Caller `TestService` to allow for method chaining. - */ -public final function TestingService FilterByName(string caseName) -{ - local int i; - local array< class > preFiltered; - if (runningTests) { - return self; - } - preFiltered = testCasesToRun; - testCasesToRun.length = 0; - for (i = 0; i < preFiltered.length; i += 1) - { - if (preFiltered[i].static.GetName() ~= caseName) { - testCasesToRun[testCasesToRun.length] = preFiltered[i]; - } - } - return self; -} - -/** - * Filters tests in current queue to only those that belong to - * a specific group. Should be used after `PrepareTests()` call, - * but before `Run()`. - * - * Will do nothing if service is already in the process of testing - * (`IsRunningTests() == true`). - * - * @return Caller `TestService` to allow for method chaining. - */ -public final function TestingService FilterByGroup(string caseGroup) -{ - local int i; - local array< class > preFiltered; - if (runningTests) { - return self; - } - preFiltered = testCasesToRun; - testCasesToRun.length = 0; - for (i = 0; i < preFiltered.length; i += 1) - { - if (preFiltered[i].static.GetGroup() ~= caseGroup) { - testCasesToRun[testCasesToRun.length] = preFiltered[i]; - } - } - return self; -} - -/** - * Makes `TestingService` run all tests in a current queue. - * - * Queue musty be build before hand: start with `PrepareTests()` call and - * optionally use `FilterByName()` / `FilterByGroup()` before - * `Run()` method call. - * - * @return `false` if service is already performing the testing - * and `true` otherwise. Note that `TestingService` might be inactive even - * after `Run()` call that returns `true`, if the testing queue was empty. - */ -public final function bool Run() -{ - if (runningTests) { - return false; - } - nextTestCase = 0; - runningTests = true; - summarizedResults.length = 0; - events.static.CallTestingBegan(testCasesToRun); - if (testCasesToRun.length <= 0) { - runningTests = false; - events.static.CallTestingEnded(testCasesToRun, summarizedResults); - } - return true; -} - -private final function DoTestingStep() -{ - local TestCaseSummary newResult; - if (nextTestCase >= testCasesToRun.length) - { - runningTests = false; - default.summarizedResults = summarizedResults; - events.static.CallTestingEnded(testCasesToRun, summarizedResults); - return; - } - testCasesToRun[nextTestCase].static.PerformTests(); - newResult = testCasesToRun[nextTestCase].static.GetSummary(); - events.static.CallCaseTested(testCasesToRun[nextTestCase], newResult); - summarizedResults[summarizedResults.length] = newResult; - nextTestCase += 1; -} - -event Tick(float delta) -{ - // This will destroy us on the next tick after we were - // either created or finished performing tests - if (!runningTests) { - Destroy(); - return; - } - DoTestingStep(); -} - -defaultproperties -{ - runTestsOnStartUp = false - events = class'TestingEvents' -} \ No newline at end of file diff --git a/sources/Core/Testing/TestCase.uc b/sources/Core/Testing/TestCase.uc deleted file mode 100644 index 7f69117..0000000 --- a/sources/Core/Testing/TestCase.uc +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Base class aimed to contain sets of tests for various components of - * Acedia and it's features. - * Neither this class, nor it's children aren't supposed to - * be instantiated. - * 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 TestCase extends AcediaObject - abstract; - -// Name by which this set of unit tests can be referred to. -var protected const string caseName; -// Name of group to which this set of unit tests belong. -var protected const string caseGroup; - -// Were all tests performed? -var private bool finishedTests; -// Context under which we are currently performing our tests. -var private string currentContext; -// Error message that will be generated if some test will fail now. -var private string currentIssue; - -// Summary where we are recording results of all our tests. -var private TestCaseSummary currentSummary; - -/** - * Sets context for any tests that will follow this call (but before the next - * `Context()` call). - * - * Context is supposed to be a short description about what - * exactly you are testing. When reporting failed tests, - failures will be - * grouped up by a context. - * - * Changing current context will also reset current issue, to set it up - * use `Issue()` method. - * - * @param context Context for the following tests. - */ -public final static function Context(string context) -{ - default.currentContext = context; - default.currentIssue = ""; // Reset issue. -} - -// Call this function to define an error message for tests that -// would fail after it. -// Message is reset by another call of `Issue()` or -// by changing the context via `Context()`. -/** - * Changes an issue that any following tests (but before the next `Issue()` or - * `Context()` call) will test for. - * - * Issue is the message that will be displayed to the user if any relevant - * tests have failed. - * - * NOTE: Current issue will be reset by any `Context()` call. - * - * @param issue Issue that following tests will test for. - */ -public final static function Issue(string issue) -{ - default.currentIssue = issue; -} - -// Following functions provide simple test primitives - -/** - * This call will record either one success or one failure for the caller - * `TestCase` class, depending on passed `bool` argument. - * - * @param result Your test's result as a `bool` value: `true` will record a - * success and `false` a failure. - */ -public final static function TEST_ExpectTrue(bool result) -{ - RecordTestResult(result); -} - -/** - * This call will record either one success or one failure for the caller - * `TestCase` class, depending on passed `bool` argument. - * - * @param result Your test's result as a `bool` value: `false` will result in - * recording a success and `true` in a failure. - */ -public final static function TEST_ExpectFalse(bool result) -{ - RecordTestResult(!result); -} - -/** - * This call will record either one success or one failure for the caller - * `TestCase` class, depending on passed `Object` argument. - * - * @param result Your test's result as an `Object` value: `none` will result - * in recording success and any non-`none` value in failure. - */ -public final static function TEST_ExpectNone(Object object) -{ - RecordTestResult(object == none); -} - -/** - * This call will record either one success or one failure for the caller - * `TestCase` class, depending on passed `Object` argument. - * - * @param result Your test's result as an `Object` value: any non-`none` - * value will result in recording success and `none` in failure. - */ -public final static function TEST_ExpectNotNone(Object object) -{ - RecordTestResult(object != none); -} - -// Records (in current context summary) that another test was performed and -// succeeded/failed, along with given error message. -private final static function RecordTestResult(bool isSuccessful) -{ - if (default.finishedTests) return; - if (default.currentSummary == none) return; - default.currentSummary.AddTestResult( default.currentContext, - default.currentIssue, - isSuccessful); -} - -/** - * Once testing has finished returns compiled results as a - * `TestCaseSummary` object. - * - * @return `TestCaseSummary` with compiled results if the testing has finished - * and `none` otherwise. - */ -public final static function TestCaseSummary GetSummary() -{ - if (!default.finishedTests) { - return none; - } - return default.currentSummary; -} - -/** - * Checks whether this `TestCase` has already finished running all it's tests. - * Finished testing means a prepared `TestCaseSummary` is available - * (by `GetSummary()` method). - * - * @return `true` if this test case already did the testing - * and `false` otherwise. - */ -public final static function bool HasFinishedTesting() -{ - return default.finishedTests; -} - -/** - * Returns name of this `TestCase`. - * - * @return Name of this `TestCase`. - */ -public final static function string GetName() -{ - return default.caseName; -} - -/** - * Returns group name of this `TestCase`. - * - * @return Group name of this `TestCase`. - */ -public final static function string GetGroup() -{ - return default.caseGroup; -} - -// Calling this function will perform unit tests defined in `TESTS()` -// function of this test case and will prepare the summary, -// obtainable through `GetSummary()` function. -// Returns `true` if all tests have successfully passed -// and `false` otherwise. -/** - * Performs all tests for this `TestCase`. - * Guaranteed to be done after this finishes. - * - * @return `true` if all tests have finished successfully - * and `false` otherwise. - */ -public final static function bool PerformTests() -{ - default.finishedTests = false; - _().memory.Free(default.currentSummary); - default.currentSummary = new class'TestCaseSummary'; - default.currentSummary.Initialize(default.class); - TESTS(); - default.finishedTests = true; - return default.currentSummary.HasPassedAllTests(); -} - -/** - * Any tests that your `TestCase` class needs to perform should be put in - * this function. - * To separate tests into groups it's recommended (as a style - * consideration) to put them in separate function calls and give these - * functions names starting with "Test_". They can have further folded - * functions with prefix "SubTest_", which can contain "SubSubTest_", etc.. - */ -protected static function TESTS(){} - -defaultproperties -{ - caseName = "" - caseGroup = "" -} \ No newline at end of file diff --git a/sources/Core/Testing/TestCaseSummary.uc b/sources/Core/Testing/TestCaseSummary.uc deleted file mode 100644 index b6b8719..0000000 --- a/sources/Core/Testing/TestCaseSummary.uc +++ /dev/null @@ -1,540 +0,0 @@ -/** - * Class for storing and processing the information about how well testing - * for a certain `TestCase` went. That information is stored as - * a collection of `IssueSummary`s, that can be accessed all at once - * or by their context. - * `TestCaseSummary` must be initialized for some `TestCase` before it can - * be used for anything (unlike `IssueSummary`). - * 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 TestCaseSummary extends AcediaObject; - -// Case for which this summary was initialized. -// `none` if it was not. -var private class ownerCase; - -/** - * - * We will store issue summaries for different contexts separately. - * INVARIANT: any function that adds records to `contextRecords` - * must guarantee that: - * 1. No two distinct records will have the same `context`; - * 2. All the `IssueSummary`s in `issueSummaries` array have different - * issue descriptions. - * Comparisons of `string`s for two above conditions are case-insensitive. - */ -struct ContextRecord -{ - var string context; - var array issueSummaries; -}; -var private array contextRecords; - -// String literals used for displaying array of test case summaries -var private const string indent; -var private const string reportHeader; -var private const string reportSuccessfulEnding; -var private const string reportUnsuccessfulEnding; - -/** - * Initializes caller summary for given `TestCase` class. - * Can only be successfully done once, but will fail if - * passed a `none` reference. - * - * @param targetCase `TestCase` class for which this summary will be - * recording test results. - * @return `true` if initialization was successful and `false otherwise - * (either summary already initialized or passed reference is `none`). - */ -public final function bool Initialize(class targetCase) -{ - if (ownerCase != none) return false; - if (targetCase == none) return false; - ownerCase = targetCase; - return true; -} - -/** - * Returns index of a context record with a given description - * (`context`) in `contextRecords`. - * Creates one if missing. Never fails. - * - * @param context Context that desired record must match. - * @return Index of the context record that matches `context`. - * Returned index is always valid. - */ -private final function int TouchContext(string context) -{ - local int i; - local ContextRecord newRecord; - // Try to find existing record with given context description - for (i = 0; i < contextRecords.length; i += 1) - { - if (context ~= contextRecords[i].context) { - return i; - } - } - // If there is none - make a new one - newRecord.context = context; - contextRecords[contextRecords.length] = newRecord; - return (contextRecords.length - 1); -} - -/** - * Finds indices of a context record and an `IssueSummary` in - * a nested array that have matching `context` - * and `issueDescription`. - * Creates records and/or `IssueSummary` if missing. Never fails. - * - * @param context Context description that - * desired record must match. - * @param issueDescription Issue description that - * desired `IssueSummary`must match. - * @param recordIndex Index of the context record that matches - * `context` description will be recorded here. - * Returned value is always valid. Passed value is discarded. - * @param recordIndex Index of the `IssueSummary` that matches - * `issueDescription` description will be recorded here. - * Returned value is always valid. Passed value is discarded. - */ -private final function TouchIssue( - string context, - string issueDescription, - out int recordIndex, - out int issueIndex -) -{ - local int i; - local array issueSummaries; - recordIndex = TouchContext(context); - issueSummaries = contextRecords[recordIndex].issueSummaries; - // Try to find existing issue summary with a given description - for (i = 0; i < issueSummaries.length; i += 1) - { - if (issueSummaries[i] == none) continue; - if (issueDescription ~= issueSummaries[i].GetDescription()) - { - issueIndex = i; - return; - } - } - // If there is none - add a new one - issueIndex = issueSummaries.length; - issueSummaries[issueIndex] = new class'IssueSummary'; - issueSummaries[issueIndex].SetIssue(ownerCase, context, issueDescription); - contextRecords[recordIndex].issueSummaries = issueSummaries; -} - -/** - * Checks if caller summary was correctly initialized. - * - * @return `true` if summary was correctly initialized and `false` otherwise. - */ -public final function bool IsInitialized() -{ - return (ownerCase != none); -} - -/** - * Adds result of another test (success or not) to the records of this summary. - * - * @param context Context under which test was performed. - * @param issueDescription Description of issue, - * for which test was performed. - * @param success `true` if test was successful and had passed, - * `false` otherwise. - */ -public final function AddTestResult( - string context, - string issueDescription, - bool success -) -{ - local int recordIndex, issueIndex; - TouchIssue(context, issueDescription, recordIndex, issueIndex); - contextRecords[recordIndex] - .issueSummaries[issueIndex] - .AddTestResult(success); -} - -/** - * Returns all contexts, for which caller summary has any records of tests - * being performed. - * - * To check if particular context exists you can use `DoesContextExists()`. - * - * @return Array of `string`s, each representing one of the contexts, - * used in tests. - * Guarantees no duplicates (equality without accounting for case). - */ -public final function array GetContexts() -{ - local int i; - local array result; - for (i = 0; i < contextRecords.length; i += 1) { - result[result.length] = contextRecords[i].context; - } - return result; -} - -/** - * Checks if given context has any records about performing tests - * (whether they ended in success or a failure) under it. - * - * To get an array of all existing contexts use `GetContexts()`. - * - * @param context A context to check for existing in records. - * @return `true` if there was a record about a test being performed under - * a given context and `false` otherwise. - */ -public final function bool DoesContextExists(string context) -{ - local int i; - for (i = 0; i < contextRecords.length; i += 1) - { - if (contextRecords[i].context ~= context) { - return true; - } - } - return false; -} - -/** - * `IssueSummary`s for every issue that was tested and recorded in - * the caller `TestCaseSummary`. - * - * @return Array of `IssueSummary`s for every tested and recorded issue. - */ -public final function array GetIssueSummaries() -{ - local int i, j; - local array recordedSummaries; - local array result; - for (i = 0; i < contextRecords.length; i += 1) - { - recordedSummaries = contextRecords[i].issueSummaries; - for (j = 0; j < recordedSummaries.length; j += 1) { - result[result.length] = recordedSummaries[j]; - } - } - return result; -} - -/** - * Returns `IssueSummary`s for every issue that was tested under - * a given context and recorded in caller `TestCaseSummary`. - * - * @param context Context under which issues of interest were tested. - * @return Array of `IssueSummary`s for every issue that was tested under - * given context. - */ -public final function array GetIssueSummariesForContext( - string context -) -{ - local int i; - local array emptyResult; - for (i = 0; i < contextRecords.length; i += 1) - { - if (contextRecords[i].context ~= context) { - return contextRecords[i].issueSummaries; - } - } - return emptyResult; -} - -// Counts total amount of tests performed under the contexts -// corresponding to `contextRecords[recordIndex]` record. -private final function int GetTotalTestsAmountForRecord(int recordIndex) -{ - local int i; - local int result; - local array issueSummaries; - issueSummaries = contextRecords[recordIndex].issueSummaries; - result = 0; - for (i = 0; i < issueSummaries.length; i += 1) - { - if (issueSummaries[i] == none) continue; - result += issueSummaries[i].GetTotalTestsAmount(); - } - return result; -} - -/** - * Total amount of performed tests, recorded in caller `TestCaseSummary`. - * - * If you are interested in amount of test under a specific context, - - * use `GetTotalTestsAmountForContext()` instead. - * - * @return Total amount of performed tests. - */ -public final function int GetTotalTestsAmount() -{ - local int i; - local int result; - for (i = 0; i < contextRecords.length; i += 1) - { - result += GetTotalTestsAmountForRecord(i); - } - return result; -} - -/** - * Total amount of tests, performed under a context `context` and - * recorded in caller `TestCaseSummary`. - * - * If you are interested in total amount of test under all contexts, - - * use `GetTotalTestsAmount()` instead. - * - * @param context Context for which method must count amount of - * performed tests. - * @return Total amount of tests, performed under given context. - * If given context does not exist in records, - returns `-1`. - */ -public final function int GetTotalTestsAmountForContext(string context) -{ - local int i; - for (i = 0; i < contextRecords.length; i += 1) - { - if (context ~= contextRecords[i].context) { - return GetTotalTestsAmountForRecord(i); - } - } - return -1; -} - -// Counts total amount of successful tests performed under the contexts -// corresponding to `contextRecords[recordIndex]` record. -private final function int GetSuccessfulTestsAmountForRecord(int recordIndex) -{ - local int i; - local int result; - local array issueSummaries; - issueSummaries = contextRecords[recordIndex].issueSummaries; - result = 0; - for (i = 0; i < issueSummaries.length; i += 1) - { - if (issueSummaries[i] == none) continue; - result += issueSummaries[i].GetSuccessfulTestsAmount(); - } - return result; -} - -/** - * Total amount of successfully performed tests, - * recorded in caller `TestCaseSummary`. - * - * If you are interested in amount of successful test under a specific context, - * - use `GetSuccessfulTestsAmountForContext()` instead. - * - * @return Total amount of successfully performed tests. - */ -public final function int GetSuccessfulTestsAmount() -{ - local int i; - local int result; - for (i = 0; i < contextRecords.length; i += 1) - { - result += GetSuccessfulTestsAmountForRecord(i); - } - return result; -} - -/** - * Total amount of tests, performed under a context `context` and - * recorded in caller `TestCaseSummary`. - * - * If you are interested in total amount of successful test under all contexts, - * - use `GetSuccessfulTestsAmount()` instead. - * - * @param context Context for which we method must count amount of - * successful tests. - * @return Total amount of successful tests, performed under given context. - * If given context does not exist in records, - returns `-1`. - */ -public final function int GetSuccessfulTestsAmountForContext(string context) -{ - local int i; - for (i = 0; i < contextRecords.length; i += 1) - { - if (context ~= contextRecords[i].context) { - return GetSuccessfulTestsAmountForRecord(i); - } - } - return -1; -} - -// Counts total amount of tests, failed under the contexts -// corresponding to `contextRecords[recordIndex]` record. -private final function int GetFailedTestsAmountForRecord(int recordIndex) -{ - local int i; - local int result; - local array issueSummaries; - issueSummaries = contextRecords[recordIndex].issueSummaries; - result = 0; - for (i = 0; i < issueSummaries.length; i += 1) - { - if (issueSummaries[i] == none) continue; - result += issueSummaries[i].GetFailedTestsAmount(); - } - return result; -} - -/** - * Total amount of failed tests, recorded in caller `TestCaseSummary`. - * - * If you are interested in amount of failed test under a specific context, - - * use `GetFailedTestsAmountForContext()` instead. - * - * @return Total amount of failed tests. - */ -public final function int GetFailedTestsAmount() -{ - local int i; - local int result; - for (i = 0; i < contextRecords.length; i += 1) - { - result += GetFailedTestsAmountForRecord(i); - } - return result; -} - -/** - * Total amount of failed tests, performed under a context `context` and - * recorded in caller `TestCaseSummary`. - * - * If you are interested in total amount of failed test under all contexts, - - * use `GetFailedTestsAmount()` instead. - * - * @param context Context for which method must count amount of - * failed tests. - * @return Total amount of failed tests, performed under given context. - * If given context does not exist in records, - returns `-1`. - */ -public final function int GetFailedTestsAmountForContext(string context) -{ - local int i; - for (i = 0; i < contextRecords.length; i += 1) - { - if (context ~= contextRecords[i].context) { - return GetFailedTestsAmountForRecord(i); - } - } - return -1; -} - -/** - * Checks whether all tests recorded in this summary have passed. - * - * @return `true` if all tests have passed, `false` otherwise. - */ -public final function bool HasPassedAllTests() -{ - return (GetFailedTestsAmount() <= 0); -} - -/** - * Checks whether all tests, performed under given context and - * recorded in this summary, have passed. - * - * @return `true` if all tests under given context have passed, - * `false` otherwise. - * If given context does not exists - it did not fail any tests. - */ -public final function bool HasPassedAllTestsForContext(string context) -{ - return (GetFailedTestsAmountForContext(context) <= 0); -} - -/** - * Generates a text summary for a set of results, given as array of - * `TestCaseSummary`s (exactly how results are returned by `TestingService`). - * - * @param summaries `TestCase` summaries (obtained as a result of testing) - * that we want to display. - * @return Test representation of `summaries` as an array of - * formatted strings, where each string corresponds to it's own line. - */ -public final static function array GenerateStringSummary( - array summaries) -{ - local int i; - local bool allTestsPassed; - local array result; - allTestsPassed = true; - result[0] = default.reportHeader; - for (i = 0; i < summaries.length; i += 1) - { - if (summaries[i] == none) continue; - summaries[i].AppendCaseSummary(result); - allTestsPassed = allTestsPassed && summaries[i].HasPassedAllTests(); - } - if (allTestsPassed) { - result[result.length] = default.reportSuccessfulEnding; - } - else { - result[result.length] = default.reportUnsuccessfulEnding; - } - return result; -} - -// Add text representation of caller `TestCase` to the existing array `result`. -private final function AppendCaseSummary(out array result) -{ - local int i, j; - local array contexts; - local string testCaseAnnouncement; - local array issues; - if (ownerCase == none) return; - // Announce case - testCaseAnnouncement = "{$text_default Test case {$text_emphasis"; - if (ownerCase.static.GetGroup() != "") { - testCaseAnnouncement @= "[" $ ownerCase.static.GetGroup() $ "]"; - } - testCaseAnnouncement @= ownerCase.static.GetName() $ "}:}"; - if (GetFailedTestsAmount() > 0) { - testCaseAnnouncement @= "{$text_failure failed}!"; - } - else { - testCaseAnnouncement @= "{$text_ok passed}!"; - } - result[result.length] = testCaseAnnouncement; - // Report failed tests - contexts = GetContexts(); - for (i = 0;i < contexts.length; i += 1) - { - if (GetFailedTestsAmountForContext(contexts[i]) <= 0) continue; - result[result.length] = "{$text_warning " $ contexts[i] $ "}"; - issues = GetIssueSummariesForContext(contexts[i]); - for (j = 0; j < issues.length; j += 1) - { - if (issues[j] == none) continue; - if (issues[j].GetFailedTestsAmount() <= 0) continue; - result[result.length] = indent $ issues[j].ToString(); - } - } -} - -defaultproperties -{ - indent = " " - reportHeader = "{$text_default ############################## {$text_emphasis Test summary} ###############################}" - reportSuccessfulEnding = "{$text_default ########################### {$text_ok All tests have passed!} ############################}" - reportUnsuccessfulEnding = "{$text_default ########################## {$text_failure Some tests have failed :(} ###########################}" -} \ No newline at end of file diff --git a/sources/Core/Text/Parser.uc b/sources/Core/Text/Parser.uc deleted file mode 100644 index 0571b7b..0000000 --- a/sources/Core/Text/Parser.uc +++ /dev/null @@ -1,1312 +0,0 @@ -/** - * Implements a simple `Parser` with built-in functions to parse simple - * UnrealScript's types and support for saving / restoring parser states. - * 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 Parser extends AcediaObject - dependson(Text) - dependson(UnicodeData); - -var public int BYTE_MAX; -var public int CODEPOINT_BACKSLASH; -var public int CODEPOINT_USMALL; -var public int CODEPOINT_ULARGE; - -// The sequence of Unicode code points that this `Parser` is supposed to parse. -var private array content; -// Incremented each time `Parser` is reinitialized with new `content`. -// Can be used to make `Parser` object completely independent from -// it's past, necessary since garbage collection is extra expensive in UE2 -// and we want to reuse created objects as much as possible. -var private int version; - -// Describes current state of the `Parser`, instance of this struct -// can be used to revert parser back to this state. -struct ParserState -{ - // Record to which object (and of what version) this state belongs to. - // This information is used to make sure that we apply this state - // only to same `Parser` (of the same version) that it originated from. - var private AcediaObject ownerObject; - var private int ownerVersion; - // Has parser failed at some point? - var private bool failed; - // Points at the next symbol to be used next in parsing. - var private int pointer; -}; -var private ParserState currentState; -// For convenience `Parser` will store one internal state that designates -// a state that's safe to revert to when some parsing attempt goes wrong. -// @see `Confirm()`, `R()` -var private ParserState confirmedState; - -// Describes rules for translating escaped sequences ("\r", "\n", "\t") -// into appropriate code points. -var private const array escapeCharactersMap; - -// Used to store a result of a `ParseSign()` function. -enum ParsedSign -{ - SIGN_Missing, - SIGN_Plus, - SIGN_Minus -}; - -/** - * Initializes `Parser` with new data from a raw data - * (sequence of Unicode code points). Never fails. - * - * Any data from before this call is lost, any checkpoints are invalidated. - * - * @param source Sequence of Unicode code points that represents - * a string `Parser` will need to parse. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser InitializeRaw(array source) -{ - content = source; - version += 1; - currentState.ownerObject = self; - currentState.ownerVersion = version; - currentState.failed = false; - currentState.pointer = 0; - confirmedState = currentState; - return self; -} - -/** - * Initializes `Parser` with new data from a `string`. Never fails. - * - * Any data from before this call is lost, any checkpoints are invalidated. - * - * @param source String `Parser` will need to parse. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser Initialize -( - string source, - optional Text.StringType sourceType -) -{ - InitializeRaw(_().text.StringToRaw(source, sourceType)); - return self; -} - -/** - * Initializes `Parser` with new data from a `Test`. - * - * Can fail if passed `none` as a parameter. - * - * Any data from before this call is lost, any checkpoints are invalidated. - * - * @param source `Text` object `Parser` will need to parse. - * If `none` is passed - parser won't be initialized. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser InitializeT(Text source) -{ - if (source == none) return self; - InitializeRaw(source.ToRaw()); - return self; -} - -/** - * Checks if `Parser` is in a failed state. - * - * Parser enters a failed state whenever any parsing call returns without - * completing it's job. `Parser` in a failed state will automatically fail - * any further parsing attempts until it gets reset via `R()` call. - * - * @return Returns 'false' if `Parser()` is in a failed state and - * `true` otherwise. - */ -public final function bool Ok() -{ - return (!currentState.failed); -} - -/** - * Returns copy of the current state of this parser. - * - * As long as caller `Parser` was not reinitialized, returned `ParserState` - * structure can be used to revert this `Parser` to it's current condition - * by a `RestoreState()` call. - * - * @see `RestoreState()` - * @return Copy of the current state of the caller `Parser`. - */ -public final function ParserState GetCurrentState() -{ - return currentState; -} - -/** - * Returns copy of (currently) last confirmed state of this parser. - * - * As long as caller `Parser` was not reinitialized, returned `ParserState` - * structure can be used to revert this `Parser` to it's current confirmed - * state by a `RestoreState()` call. - * - * @see `RestoreState()`, `Confirm()`, `R()` - * @return Copy of (currently) last confirmed state of this parser. - */ -public final function ParserState GetConfirmedState() -{ - return confirmedState; -} - -/** - * Checks if given `stateToCheck` is valid for the caller `Parser`, i.e.: - * 1. It is a state generated by either `GetCurrentState()` or - * `GetConfirmedState()` calls on the caller `Parser`. - * 2. Caller `Parser` was not reinitialized since a call - * that generated given `stateToCheck`. - * - * @param stateToCheck `ParserState` to check for validity for - * caller `Parser`. - * @return `true` if given `stateToCheck` is valid and `false` otherwise. - */ -public final function bool IsStateValid(ParserState stateToCheck) -{ - if (stateToCheck.ownerObject != self) return false; - if (stateToCheck.ownerVersion != version) return false; - return true; -} - -/** - * Checks if calling `RestoreState()` for passed state will return a `Parser` - * in an "Ok" state (not failed), i.e. state is valid and - * was generated when `Parser` was in a non-failed state. - * - * @param stateToCheck `ParserState` to check for corresponding to - * `Parser` being in a non-failed state. - * By definition must also be valid for the caller `Parser`. - * @return `true` if given `stateToCheck` is valid and `false` otherwise. - */ -public final function bool IsStateOk(ParserState stateToCheck) -{ - if (!IsStateValid(stateToCheck)) return false; - return (!stateToCheck.failed); -} - -/** - * Resets parser to a state, given by `stateToRestore` argument - * (so a state `Parser` was in at the moment given `stateToRestore` - * was obtained). - * - * If given `stateToRestore` is from a different `Parser` or - * the owner `Parser` was reinitialized after passed state was obtained, - - * function will simply put caller `Parser` into a failed state. - * Note that caller `Parser` being put in a failed state after this call - * doesn't mean that described issues are actually present: - * `stateToRestore` can also describe a failed state of the `Parser`. - * - * @param stateToRestore `ParserState` that this method will attempt - * to set for the caller `Parser`. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser RestoreState(ParserState stateToRestore) -{ - if (!IsStateValid(stateToRestore)) - { - currentState.failed = true; - return self; - } - currentState = stateToRestore; - return self; -} - - /** - * Remembers current state of `Parser` in an internal checkpoint variable, - * that can later be restored by an `R()` call. - * - * Can only save non-failed states and will only fail if caller `Parser` is - * in a failed state. - * - * `Confirm()` and `R()` are essentially convenience wrapper functions for - * `GetCurrentState()` and `RestoreState()` calls + - * state storage variable. - * - * @return `true` if current state is recorded in `Parser` as confirmed and - * `false` otherwise. - */ -public final function bool Confirm() -{ - if (!Ok()) return false; - - confirmedState = currentState; - return true; -} - -/** - * Resets `Parser` to a last state recorded as confirmed by a last successful - * `Confirm()` function call. If there weren't any such call - - * reverts `Parser` to it's state right after initialization. - * - * Always resets failed state of a `Parser`. Cannot fail. - * - * `Confirm()` and `R()` are essentially convenience wrapper functions for - * `GetCurrentState()` and `RestoreState()` calls + state storage variable. - * - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser R() -{ - currentState = confirmedState; - return self; -} - -/** - * Shifts parsing pointer forward. - * - * Can only shift forward. To revert to a previous state in case of failure use - * combination of `GetCurrentState()` and `RestoreState()` functions. - * - * @param shift How much to shift parsing pointer? - * Values of zero and below are discarded and `1` is used instead - * (i.e. by default this method shifts pointer by `1` position). - * @return Returns the calling object, to allow for function chaining. - */ -protected final function Parser ShiftPointer(optional int shift) -{ - shift = Max(1, shift); - currentState.pointer = Min(currentState.pointer + shift, content.length); - return self; -} - -/** - * Returns a code point from this `Parser`'s content, relative to next - * code point that caller `Parser` must handle. - * - * @param `shift` If `0` (default value) or negative value is passed - - * simply asks for the code point that caller `Parser` must handle. - * Otherwise shifts that index `shift` code points, i.e. - * `1` to return next code point or `2` to return code point after - * the next one. - * @return Returns code point at a given shift. If `shift` is too small/large - * and does not fit `Parser`'s contents, returns `-1`. - * `GetCodePoint()` with default (`0`) parameter can also return `-1` if - * contents of the caller `Parser` are empty or it has already consumed - * all input. - */ -protected final function Text.Character GetCharacter(optional int shift) -{ - local Text.Character invalidCharacter; - local int absoluteAddress; - absoluteAddress = currentState.pointer + Max(0, shift); - if (absoluteAddress < 0 || absoluteAddress >= content.length) - { - invalidCharacter.codePoint = -1; - return invalidCharacter; - } - return content[absoluteAddress]; -} - -/** - * Forces caller `Parser` to enter a failed state. - * - * @return Returns the calling object, to allow for a quick exit from - * a parsing function by `return Fail();`. - */ -protected final function Parser Fail() -{ - currentState.failed = true; - return self; -} - -/** - * Returns amount of code points that have already been parsed, - * provided that caller `Parser` is in a correct state. - * - * @return Returns how many Unicode code points have already been parsed if - * caller `Parser` is in correct state; - * otherwise return value is undefined. - */ -public final function int GetParsedLength() -{ - return Max(0, currentState.pointer); -} - -/** - * Returns amount of code points that have not yet been parsed, - * provided that caller `Parser` is in a correct state. - * - * @return Returns how many Unicode code points are still unparsed if - * caller `Parser` is in correct state; - * otherwise return value is undefined. - */ -public final function int GetRemainingLength() -{ - return Max(0, content.length - currentState.pointer); -} - -/** - * Checks if caller `Parser` has already parsed all of it's content. - * Uninitialized `Parser` has no content and, therefore, parsed it all. - * - * Should return `true` iff `GetRemainingLength() == 0`. - * - * @return `true` if caller `Parser` has no more data to parse. - */ -public final function bool HasFinished() -{ - return (currentState.pointer >= content.length); -} - -/** - * Returns still unparsed part of caller `Parser`'s source as an array of - * Unicode code points. - * - * @return Unparsed part of caller `Parser`'s source as an array of - * Unicode code points. - */ -public final function array GetRemainderRaw() -{ - local int i; - local array result; - for (i = 0; i < GetRemainingLength(); i += 1) - { - result[result.length] = GetCharacter(i); - } - return result; -} - -/** - * Returns still unparsed part of caller `Parser`'s source as a `string`. - * - * @return Unparsed part of caller `Parser`'s source as a `string`. - */ -public final function string GetRemainder() -{ - local int i; - local array rawResult; - for (i = 0; i < GetRemainingLength(); i += 1) - { - rawResult[rawResult.length] = GetCharacter(i); - } - return _().text.RawToString(rawResult, STRING_Plain); -} - -/** - * Returns still unparsed part of caller `Parser`'s source as `Text`. - * - * @return Unparsed part of caller `Parser`'s source as `Text`. - */ -public final function Text GetRemainderT() -{ - local int i; - local array rawResult; - for (i = 0; i < GetRemainingLength(); i += 1) - { - rawResult[rawResult.length] = GetCharacter(i); - } - return _().text.FromRaw(rawResult); -} - -/** - * Matches any sequence of whitespace symbols, without returning it. - * Starts from where previous parsing function finished. - * - * Can never cause parser to enter failed state. - * - * What symbols exactly are considered whitespace refer to the description of - * `TextAPI.IsWhitespace()` function. - * - * @param whitespacesAmount Returns how many whitespace symbols - * were skipped. Any given value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser Skip(optional out int whitespacesAmount) -{ - local TextAPI api; - if (!Ok()) return self; - - api = _().text; - whitespacesAmount = 0; - // Cycle will end once we either reach a non-whitespace symbol or - // there's not more code points to get - while (api.IsWhitespace(GetCharacter(whitespacesAmount))) - { - whitespacesAmount += 1; - } - ShiftPointer(whitespacesAmount); - return self; -} - -/** - * Function that tries to match given data in `Parser`'s content, - * starting from where previous parsing function finished. - * - * Does nothing if caller `Parser` was in failed state. - * - * @param data Data that must be matched to the `Parser`'s - * contents, starting from where previous parsing function finished. - * @param caseInsensitive If `false` the matching will have to be exact, - * using `true` will make this method to ignore the case, - * where it's applicable. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MatchRaw -( - array data, - optional bool caseInsensitive -) -{ - local int i; - local TextAPI api; - if (!Ok()) return self; - if (data.length > GetRemainingLength()) return Fail(); - - api = _().text; - for (i = 0; i < data.length; i += 1) - { - if (!api.AreEqual(data[i], GetCharacter(i), caseInsensitive)) - { - return Fail(); - } - } - ShiftPointer(data.length); - return self; -} - -/** - * Function that tries to match given `string`, starting from where - * previous parsing function finished. - * - * Does nothing if caller `Parser` was in failed state. - * - * @param word String that must be matched to the `Parser`'s - * contents, starting from where previous parsing function finished. - * @param caseInsensitive If `false` the matching will have to be exact, - * using `true` will make this method to ignore the case, - * where it's applicable. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser Match(string word, optional bool caseInsensitive) -{ - return MatchRaw(_().text.StringToRaw(word), caseInsensitive); -} - -/** - * Function that tries to match given `Text`, starting from where - * previous parsing function finished. - * - * Does nothing if caller `Parser` was in failed state. - * - * @param word Text that must be matched to the `Parser`'s - * contents, starting from where previous parsing function finished. - * @param caseInsensitive If `false` the matching will have to be exact, - * using `true` will make this method to ignore the case, - * where it's applicable. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MatchT(Text word, optional bool caseInsensitive) -{ - if (!Ok()) return self; - if (word == none) return Fail(); - - return MatchRaw(word.ToRaw(), caseInsensitive); -} - -/** - * Internal function for parsing unsigned integers in any base from 2 to 36. - * - * This parsing can fail, putting `Parser` into a failed state. - * - * @param result If parsing is successful, this value will contain - * parsed integer, otherwise value is undefined. - * Any passed value is discarded. - * @param base Base, in which integer in question is recorded. - * @param numberLength If this parameter is less or equal to zero, - * function will stop parsing the moment it can't recognize a character as - * belonging to a number in a given base. - * It will only fail if it couldn't parse a single character; - * If this parameter is set to be positive (`> 0`), function will - * attempt to use exactly `numberLength` character for parsing and will - * fail if they would not constitute a valid number. - * @param consumedCodePoints Amount of code point used (consumed) to parse - * this number; undefined, if parsing is unsuccessful. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MUnsignedInteger -( - out int result, - optional int base, - optional int numberLength, - optional out int consumedCodePoints -) -{ - local bool parsingFixedLength; - local int nextPosition; - numberLength = Max(0, numberLength); - parsingFixedLength = (numberLength != 0); - if (base == 0) - { - base = 10; - } - else if (base < 2 || base > 36) - { - return Fail(); - } - result = 0; - consumedCodePoints = 0; - while (!HasFinished()) - { - if (parsingFixedLength && consumedCodePoints >= numberLength) break; - nextPosition = _().text.CharacterToInt(GetCharacter(), base); - if (nextPosition < 0) break; - - result = result * base + nextPosition; - consumedCodePoints += 1; - ShiftPointer(); - } - if ( parsingFixedLength && consumedCodePoints != numberLength - || consumedCodePoints < 1) - { - return Fail(); - } - return self; -} - -/** - * Parses escaped sequence of the type that is usually used in - * string literals: backslash "\"", followed by any character - * (called escaped character later) or, in special cases, several characters. - * For most characters escaped sequence resolved into - * an escaped character's code point. - * - * Several escaped symbols: - * \n, \r, \t, \b, \f, \v - * are translated into a different code point corresponding to - * a control symbols, normally denoted by these sequences. - * - * A Unicode code point can also be directly entered with either of the two - * commands: - * \U0056 - * \u56 - * The difference is that `\U` allows you to enter two-byte code point, while - * `\u` only allows to define code points that fit into 1 byte, - * but is more compact. - * - * @param denotedCodePoint If parsing is successful, parameter will contain - * appropriate code point, denoted by a parsed escaped sequence; - * If parsing is unsuccessful, value is undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MEscapedSequence -( - out Text.Character denotedCharacter -) -{ - local int i; - if (!Ok()) return self; - // Need at least two characters to parse escaped sequence - if (GetRemainingLength() < 2) return Fail(); - if (GetCharacter().codePoint != CODEPOINT_BACKSLASH) return Fail(); - - denotedCharacter = GetCharacter(1); - ShiftPointer(2); - // Escaped character denotes some special code point - for (i = 0; i < escapeCharactersMap.length; i += 1) - { - if (escapeCharactersMap[i].from == denotedCharacter.codePoint) - { - denotedCharacter.codePoint = escapeCharactersMap[i].to; - return self; - } - } - // Escaped character denotes declaration of arbitrary Unicode code point - if (denotedCharacter.codePoint == CODEPOINT_ULARGE) - { - MUnsignedInteger(denotedCharacter.codePoint, 16, 4); - } - else if (denotedCharacter.codePoint == CODEPOINT_USMALL) - { - MUnsignedInteger(denotedCharacter.codePoint, 16, 2); - } - return self; -} - -/** - * Attempts to parse a string literal: a string enclosed in either of - * the following quotation marks: ", ', `. - * String literals can contain escaped sequences. - * String literals MUST end with closing quotation mark. - * @see `MEscapedSequence()` - * - * @param result If parsing is successful, this array will contain the - * contents of string literal with resolved escaped sequences; - * if parsing has failed, it's value is undefined. - * Any passed contents are simply discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MStringLiteralRaw(out array result) -{ - local TextAPI api; - local Text.Character nextCharacter; - local Text.Character usedQuotationMark; - local Text.Character escapedCharacter; - if (!Ok()) return self; - usedQuotationMark = GetCharacter(); - if (!_().text.IsQuotationMark(usedQuotationMark)) return Fail(); - - ShiftPointer(); // Skip opening quotation mark - api = _().text; - result.length = 0; - while (!HasFinished()) - { - nextCharacter = GetCharacter(); - // Closing quote - if (api.AreEqual(nextCharacter, usedQuotationMark)) - { - ShiftPointer(); - return self; - } - // Escaped characters - if (api.IsCodePoint(nextCharacter, CODEPOINT_BACKSLASH)) - { - if (!MEscapedSequence(escapedCharacter).Ok()) - { - return Fail(); // Backslash MUST mean valid escape sequence - } - result[result.length] = escapedCharacter; - } - // Any other code point - else - { - result[result.length] = nextCharacter; - ShiftPointer(); - } - } - // Content ended without a closing quote. - return Fail(); -} - -/** - * Attempts to parse a string literal: a string enclosed in either of - * the following quotation marks: ", ', `. - * String literals can contain escaped sequences. - * String literals MUST end with closing quotation mark. - * @see `MEscapedSequence()` - * - * @param result If parsing is successful, this `string` will contain the - * contents of string literal with resolved escaped sequences; - * if parsing has failed, it's value is undefined. - * Any passed contents are simply discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MStringLiteral(out string result) -{ - local array rawResult; - if (!Ok()) return self; - - if (MStringLiteralRaw(rawResult).Ok()) - { - result = _().text.RawToString(rawResult, STRING_Plain); - } - return self; -} - -/** - * Attempts to parse a string literal: a string enclosed in either of - * the following quotation marks: ", ', `. - * String literals can contain escaped sequences. - * String literals MUST end with closing quotation mark. - * @see `MEscapedSequence()` - * - * @param result If parsing is successful, this `Text` will contain the - * contents of string literal with resolved escaped sequences; - * if parsing has failed, it's value is undefined. - * Any passed contents are simply discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MStringLiteralT(out Text result) -{ - local array rawResult; - if (!Ok()) return self; - - if (MStringLiteralRaw(rawResult).Ok()) - { - result = _().text.FromRaw(rawResult); - } - return self; -} - -/** - * Matches everything until it finds one of the breaking symbols: - * 1. a specified code point (by default `0`); - * 2. (optionally) whitespace symbol (@see `TextAPI.IsWhitespace()`); - * 3. (optionally) quotation symbol (@see `TextAPI.IsQuotation()`). - * This method cannot fail. - * - * @param result Any content before one of the break symbols - * will be recorded into this array as a sequence of Unicode code points. - * @param codePointBreak Method will stop parsing upon encountering this - * code point (it will not be included in the `result`) - * @param whitespacesBreak `true` if you want to also treat any - * whitespace character as a break symbol - * (@see `TextAPI.IsWhitespace()` for what symbols are - * considered whitespaces) - * @param quotesBreak `true` if you want to also treat any - * quotation mark character as a break symbol - * (@see `TextAPI.IsQuotation()` for what symbols are - * considered quotation marks). - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MUntilRaw -( - out array result, - optional Text.Character characterBreak, - optional bool whitespacesBreak, - optional bool quotesBreak -) -{ - local Text.Character nextCharacter; - local TextAPI api; - if (!Ok()) return self; - - api = _().text; - result.length = 0; - while (!HasFinished()) - { - nextCharacter = GetCharacter(); - if (api.AreEqual(nextCharacter, characterBreak)) break; - if (whitespacesBreak && api.IsWhitespace(nextCharacter)) break; - if (quotesBreak && api.IsQuotationMark(nextCharacter)) break; - - result[result.length] = nextCharacter; - ShiftPointer(); - } - return self; -} - -/** - * Matches everything until it finds one of the breaking symbols: - * 1. a specified code point (by default `0`); - * 2. (optionally) whitespace symbol (@see `TextAPI.IsWhitespace()`); - * 3. (optionally) quotation symbol (@see `TextAPI.IsQuotation()`). - * This method cannot fail. - * - * @param result Any content before one of the break symbols - * will be recorded into this `string`. - * @param codePointBreak Method will stop parsing upon encountering this - * code point (it will not be included in the `result`) - * @param whitespacesBreak `true` if you want to also treat any - * whitespace character as a break symbol - * (@see `TextAPI.IsWhitespace()` for what symbols are - * considered whitespaces) - * @param quotesBreak `true` if you want to also treat any - * quotation mark character as a break symbol - * (@see `TextAPI.IsQuotation()` for what symbols are - * considered quotation marks). - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MUntil -( - out string result, - optional Text.Character characterBreak, - optional bool whitespacesBreak, - optional bool quotesBreak -) -{ - local array rawResult; - if (!Ok()) return self; - - MUntilRaw(rawResult, characterBreak, whitespacesBreak, quotesBreak); - result = _().text.RawToString(rawResult, STRING_Plain); - return self; -} - -/** - * Matches everything until it finds one of the breaking symbols: - * 1. a specified code point (by default `0`); - * 2. (optionally) whitespace symbol (@see `TextAPI.IsWhitespace()`); - * 3. (optionally) quotation symbol (@see `TextAPI.IsQuotation()`). - * This method cannot fail. - * - * @param result Any content before one of the break symbols - * will be recorded into this `Text`. - * @param codePointBreak Method will stop parsing upon encountering this - * code point (it will not be included in the `result`) - * @param whitespacesBreak `true` if you want to also treat any - * whitespace character as a break symbol - * (@see `TextAPI.IsWhitespace()` for what symbols are - * considered whitespaces) - * @param quotesBreak `true` if you want to also treat any - * quotation mark character as a break symbol - * (@see `TextAPI.IsQuotation()` for what symbols are - * considered quotation marks). - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MUntilT -( - out Text result, - optional Text.Character characterBreak, - optional bool whitespacesBreak, - optional bool quotesBreak -) -{ - local array rawResult; - if (!Ok()) return self; - - MUntilRaw(rawResult, characterBreak, whitespacesBreak, quotesBreak); - result = _().text.FromRaw(rawResult); - return self; -} - -/** - * Parses a string as either "simple" or "quoted". - * Not being able to read any symbols is not considered a failure. - * - * Reading empty string (either to lack of further data or - * instantly encountering a break symbol) is not considered a failure. - * - * Quoted string starts with quotation mark and ends either - * at the corresponding closing (un-escaped) mark - * or when `Parser`'s input has been fully consumed. - * If string started with a quotation mark, this method will act exactly - * like `MStringLiteralRaw()`. - * - * @param result If parsing is successful - string's contents will be - * recorded here; if parsing has failed - value is undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MStringRaw(out array result) -{ - if (!Ok()) return self; - - if (_().text.IsQuotationMark(GetCharacter())) - { - MStringLiteralRaw(result); - } - else - { - MUntilRaw(result,, true, true); - } - return self; -} - -/** - * Parses a string as either "simple" or "quoted". - * Not being able to read any symbols is not considered a failure. - * - * Reading empty string (either to lack of further data or - * instantly encountering a break symbol) is not considered a failure. - * - * Quoted string starts with quotation mark and ends either - * at the corresponding closing (un-escaped) mark - * or when `Parser`'s input has been fully consumed. - * If string started with a quotation mark, this method will act exactly - * like `MStringLiteral()`. - * - * @param result If parsing is successful - string's contents will be - * recorded here; if parsing has failed - value is undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MString(out string result) -{ - local array rawResult; - if (!Ok()) return self; - - MStringRaw(rawResult); - result = _().text.RawToString(rawResult, STRING_Plain); - return self; -} - -/** - * Parses a string as either "simple" or "quoted". - * Not being able to read any symbols is not considered a failure. - * - * Reading empty string (either to lack of further data or - * instantly encountering a break symbol) is not considered a failure. - * - * Quoted string starts with quotation mark and ends either - * at the corresponding closing (un-escaped) mark - * or when `Parser`'s input has been fully consumed. - * If string started with a quotation mark, this method will act exactly - * like `MStringLiteralT()`. - * - * @param result If parsing is successful - string's contents will be - * recorded here; if parsing has failed - value is undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MStringT(out Text result) -{ - local array rawResult; - if (!Ok()) return self; - - MStringRaw(rawResult); - result = _().text.FromRaw(rawResult); - return self; -} - -/** - * Matches a non-empty sequence of whitespace symbols. - * - * Cannot fail (not being able to read any input is not considered a failure). - * - * @param result If parsing was successful - whitespaces' Unicode code points - * will be recorded in this array, otherwise - undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MWhitespacesRaw(out array result) -{ - local Text.Character nextCharacter; - local TextAPI api; - if (!Ok()) return self; - - api = _().text; - result.length = 0; - while (!HasFinished()) - { - nextCharacter = GetCharacter(); - if (!api.IsWhitespace(nextCharacter)) break; - result[result.length] = nextCharacter; - ShiftPointer(); - } - return self; -} - -/** - * Matches a non-empty sequence of whitespace symbols. - * - * Cannot fail (not being able to read any input is not considered a failure). - * - * @param result If parsing was successful - whitespaces will be - * recorded here, otherwise - undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MWhitespaces(out string result) -{ - local array rawResult; - if (!Ok()) return self; - - MWhitespacesRaw(rawResult); - result = _().text.RawToString(rawResult, STRING_Plain); - return self; -} - -/** - * Matches a non-empty sequence of whitespace symbols. - * - * Cannot fail (not being able to read any input is not considered a failure). - * - * @param result If parsing was successful - whitespaces will be - * recorded here, otherwise - undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MWhitespacesT(out Text result) -{ - local array rawResult; - if (!Ok()) return self; - - MWhitespacesRaw(rawResult); - result = _().text.FromRaw(rawResult); - return self; -} - -/** - * Parses next code point as itself. - * - * Can only fail if caller `Parser` has already exhausted all available data. - * - * @param result If parsing was successful - next Unicode code point, - * otherwise - value is undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MCharacter(out Text.Character result) -{ - if (!Ok()) return self; - if (HasFinished()) return Fail(); - - result = GetCharacter(); - ShiftPointer(); - return self; -} - -/** - * Parses next code point as as byte. - * Can fail if caller `Parser` has already exhausted all available data or - * next Unicode code point cannot fit into the `byte` value range. - * - * @param result If parsing was successful - next Unicode code point as - * a byte, otherwise - value is undefined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MByte(out byte result) -{ - local Text.Character character; - if (!Ok()) return self; - - if (!MCharacter(character).Ok()) - { - return Fail(); - } - if (character.codePoint < 0 || character.codePoint > BYTE_MAX) - { - return Fail(); - } - result = character.codePoint; - return self; -} - -/** - * Tries to parse a sign: either "+" or "-". - * - * @param result Value of `ParsedSign` will be recorded here, - * depending on what sign was encountered. - * `SIGN_Missing` value is only possible if we allow sign to be missing. - * @param allowMissingSign By default `false` means that parsing will fail - * if next character is neither "+" or "-"; - * `true` means that parsing will not fail even if there is not sign, - - * method will then consume in input and will return `SIGN_Missing` - * as a result. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MSign -( - out ParsedSign result, - optional bool allowMissingSign -) -{ - local ParserState checkpoint; - if (!Ok()) return self; - - // Read sign - checkpoint = GetCurrentState(); - if (Match("-").Ok()) - { - result = SIGN_Minus; - } - else if (RestoreState(checkpoint).Match("+").Ok()) - { - result = SIGN_Plus; - } - else if (allowMissingSign) - { - result = SIGN_Missing; - RestoreState(checkpoint); - } - return self; -} - -/** - * Tries to parse a number prefix that determines a base system for denoting - * integer numbers: - * 1. `0x` means hexadecimal; - * 2. `0b` means binary; - * 3. `0o` means octal; - * 4. otherwise we use decimal system. - * - * This parsing method cannot fail. - * - * Parser consumes appropriate prefix; nothing if decimal system is determined. - * - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MBase(out int base) -{ - local ParserState checkpoint; - if (!Ok()) return self; - - checkpoint = GetCurrentState(); - if (Match("0x").Ok()) - { - base = 16; - } - else if (RestoreState(checkpoint).Match("0b").Ok()) - { - base = 2; - } - else if (RestoreState(checkpoint).Match("0o").Ok()) - { - base = 8; - } - else - { - RestoreState(checkpoint); - base = 10; - } - return self; -} - -/** - * Parses signed integer either in a directly given base (`base`) or in an - * auto-determined one (based on prefix, @see `MBase()`). - * - * Integers are expected in form: (+/-)(0x/0b/0o). - * Examples: 78, 0o34, -2, 0b0101001, -0x78aC. - * - * @param result If parsing is successful - parsed value will be - * recorded here; if parsing fails - value is undetermined. - * Any passed value is discarded. - * @param base base in which function must attempt to parse a number; - * Default value (`0`) means function must auto-determine base, - * based on the prefix, otherwise must be between 2 and 36. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MInteger(out int result, optional int base) -{ - local ParsedSign integerSign; - if (!Ok()) return self; - - MSign(integerSign, true); - if (base == 0) - { - MBase(base); - } - MUnsignedInteger(result, base); - if (integerSign == SIGN_Minus) - { - result *= -1; - } - return self; -} - -// Internal function for parsing fractional part (including the dot ".") -// of the text representation for floating point number (decimal system only). -// Cannot fail, returns `0.0` if it couldn't parse anything. -protected final function Parser MFractionalPart(out float result) -{ - local ParserState checkpoint; - local int fractionalInt; - local int digitsRead; - if (!Ok()) return self; - - result = 0.0; - checkpoint = GetCurrentState(); - if (!Match(".").Ok()) - { - RestoreState(checkpoint); - return self; - } - checkpoint = GetCurrentState(); - if (!MUnsignedInteger(fractionalInt,,, digitsRead).Ok()) - { - fractionalInt = 0.0; - RestoreState(checkpoint); - return self; - } - result = float(fractionalInt) * (0.1 ** digitsRead); - return self; -} - -// Internal function for parsing exponent part (including the symbol "e") -// of the text representation for floating point number (decimal system only). -// Can only fail if symbol "e" / "E" is present, but there is no valid -// integer right after it (whitespace symbols in-between are forbidden). -// Returns `0.0` if there was not exponent to parse. -protected final function Parser MExponentPart(out int result) -{ - local ParserState checkpoint; - local ParsedSign exponendSign; - if (!Ok()) return self; - - // Is there even an exponential part? - checkpoint = GetCurrentState(); - if (!Match("e", true).Ok()) - { - RestoreState(checkpoint); - return self; - } - // If yes - parse it: - result = 0.0; - MSign(exponendSign, true).MUnsignedInteger(result, 10); - if (exponendSign == SIGN_Minus) - { - result *= -1; - } - return self; -} - -// Internal function for parsing optional suffix of the text representation -// for floating point number ("f" or "F"). -// Cannot fail. Can only consume one Unicode code point, -// when it is either "f" or "F". -protected final function Parser MFloatSuffix() -{ - local ParserState checkpoint; - if (!Ok()) return self; - - checkpoint = GetCurrentState(); - if (!Match("f", true).Ok()) - { - RestoreState(checkpoint); - } - return self; -} - -/** - * Parses signed floating point number in JSON form + optional "f" / "F" - * suffix at the end. - * - * @param result If parsing is successful - parsed value will be - * recorded here; if parsing fails - value is undetermined. - * Any passed value is discarded. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Parser MNumber(out float result) -{ - local ParsedSign sign; - local int integerPart, exponentPart; - local float fractionalPart; - if (!Ok()) return self; - - self.MSign(sign, true) - .MUnsignedInteger(integerPart, 10) - .MFractionalPart(fractionalPart) - .MExponentPart(exponentPart) - .MFloatSuffix(); - if (!Ok()) - { - return self; - } - result = float(integerPart) + fractionalPart; - result *= 10.0 ** exponentPart; - if (sign == SIGN_Minus) - { - result *= -1; - } - return self; -} - -defaultproperties -{ - // Start with no initializations done - version = 0 - BYTE_MAX = 255 - CODEPOINT_BACKSLASH = 92 // \ - CODEPOINT_USMALL = 117 // u - CODEPOINT_ULARGE = 85 // U - escapeCharactersMap(0)=(from=110,to=10) // \n - escapeCharactersMap(1)=(from=114,to=13) // \r - escapeCharactersMap(2)=(from=116,to=9) // \t - escapeCharactersMap(3)=(from=98,to=8) // \b - escapeCharactersMap(4)=(from=102,to=12) // \f - escapeCharactersMap(5)=(from=118,to=11) // \v -} \ No newline at end of file diff --git a/sources/Core/Text/Tests/TEST_Parser.uc b/sources/Core/Text/Tests/TEST_Parser.uc deleted file mode 100644 index a6f0cab20faab3fa653dd31db2c181a72bf641df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118744 zcmeHQZF3yQao$v=DrHyYQ~tn-`9LZXApoRa?AVSdh_sOsX+(gcaw3-i5Fm+&1Zf;7 zie~w*C+TMzjqaJ9*`3+l+uMUsSHJ;xyYteoPxti9{_p=)A65TCuYat5S&ceNO+)>Gz!eenOv}(ffY2QytJ>bNcR#{+m^gtLOB|oL*~QTWAljExPkn z^_H$ash(EPT>G2VdWD}^b+0<5PhQgRS+(a{Mq9`9{}1%3jO}(c-It6J<9|kTWNMud zRgP&+{QoOAGo|c3qS|qFMBg45s%cC1duihdQSjKcIVZ|IA*#J`tsYmurheYiXRqn~ zF?|MF;##y1{(2yxEgj~O!>CzO&m4Wu9fd%79mT`yKJ~Fh{c}I}sxN3fU%4@16fC6= z-2ad0+Fg2q(|@G9FjmMH#<@pVY|#w9a{uAeZ|Kt5TBGaO#tFra%^12#uGmFr z5Oi)v^MwsPaP+q}QD^%nN4XhsglYKP(Fpv5jI!>+KAgD!LB9j~c}#b~j)qd(W!yRz zXeTt}DYYwn1x2lW0@b#OUo*PyCCv?*0lmdoe?xOPr`Id`2lr^cTSVz?dVfnAbV#2*r@OiJZY_UH{DHQDM({%5Kz@sCHI8UJdhdqgf9#vh z)#5Mkt8m1^sp6bP6~D-h=%KIQm>t@N_xv5Ts51stt>o%AC9Q0_Hdt2QdF?^Vg?3C6 z(3K@iMh^c!quFTR8>FGYF_2tX{8{y0^;>V;o52!Pal6Ia@7a!u8lyO{kcnTGQao8rre@C)*L5b3)aZuVbu4XOkun} z75^k*mCN}J7!KYc&wTY8I+LB4IKkz6J3fAe_h|L3;^vq>a#FYAb;p? z-W}VnhrDYak(9wh`5fshZ|uoKd8$huEHS9TSm0K5;}Qk%eCu)QP3Keg14-Q{8v{&i zEj}>YjT(h_>8iuaUhD{WiN=g}WBg0sci-IwG+d{6`Y!MYNC4RSsOV1iS6xTTcXt~z z-(rVq(1SxUt}GNNZg_w@JR|^c~-AkDVL-hWB_v?f*pI zKzHDCfml_hfseQa&?BZB;wQc*|M?TO$>?g&jTPguG+XmDQ0Ivmt&#Qq!O67PH}FjS zNXZPrH^vsgkI0k&Ysxq<&!^5F=!}625w+AQTQ<9|-T0elX}#qQ8db>QzO*=^xxkX1 zR>E=24tzNyyMYK&C;^nLG=P+V$G6>Q_zMvXOD9LBoF^JW3gUEn<>K`d;sRn!!7?$) z5UE%+cc6z~6Ahq$Upr2UfYnvzcVzaY1M+`|FSDMmM@ZcMya8*2M*u`a&1Woi^G` z$!^v)rVoX>+FF(wpDxR`ww9wJ_0|ISq0_K!*R_`AHQq}%Z|H0FYWaXNPN}zef1&;& z&YfW_)$nbH-YTR`yvI=17KO^=Q3%H5=N6`l&3XJWk8M5m9{Fy$a#)|ut?rYDo2uWI zdDk&bJB5bw-by9kQz3f?T)Ujer|!Y~)2gc@axJk(fo0J zyT#ILWZjUpvM8$?-LO83crWx)VOChy0&_A9p)V6m*d0L{6s(`}LDY^e}B4 z;Aaw7$NwWY_wPhq&WgPtUlNOX({W|ob0c&2m|j>@=km`t-ftA5p738Lm zeyD6=HfXKci};!!%V9`6{-`V!po2Fk-=KZ>rvxe%P<6zsj9l*m`rV+uKKEA4!~)3s z4Z0I~7_4YweMzW{wPRja!0+)XZ7Ei#RwyMp`>-mHS3CUKU`-wP2k1xVoIl40bT_Z# zHfMA<29c<;NHC_6p^l_!>qpF*2Mg)V^;Jqc&Ycdgm1T2hFVa_oyYyM2?L*LbmGr#} zJ)3#hCcmMyPR}}&(S+?t%r`AIH?gY7qznG;XE*B+$?|Py7b*;6LA&={Vn>If`6}7c z?v(9nQI*Qmx2j#JGrny_)Zo{xtX9m2eFlj35Hlbad+JbbT65dbJJ2}V(Y1JPRrHc^ zI%1TtPMmA7hsW(UE0M1^@wg5v#)xJSS4rIQl%iKq5RsMLui#g~BmUwV1>Uz{TjJOl zbjpeFkonE4WN~AtW;9tB2>woGM~Kv-TK0jSdlKTPn8km7_YAz@!VT(Oj*}Ha17PpKM-|}E+X}2sTE#fMnhC$)W^g)!k>4V zFB}p1{$*NLp$xBE`w>I!Hj`?pQnw&XnG%`3wjr{z1=-ddTNwwfwJG7C@9WCBw=9ce zB~mhjzLn@o$+~Tb*{c|h7aQAdlkZ$r6xIdj*7;Coqf>FfwCr>j$_!_%l}6VVTdYWT zw9|!`UF~+=^LFE9ll{&?>WjmaB^JI5Y)Xci%C?yS8=+|8wokQEdm; zK1CSeE}B>@v`Z17-9yvfvX7Mcg9X` z-Uo#21NMzze*!W;O2shu6{A54kJlw7H|NlwexFW;e%M>hSu0eH2(3!9QDab~Oh5Zb zRSx1n`^e|vN?;1 zEy^0SOY?ZV3uQQmm&i?`rUe;XoYR26=g!`oxg0dCJvfMymg+X5jVCGJvnXzC(79NW zJ6!}$_?qYg{|c#(W1ycOT4<+94rXZfdN}VWN1D+}{q&N$bs7gvv{Obfg_YZORMFn< zRm2wUvleQ8d}#wxwDfIqd&_<4nIpSnppo{Ot~L$N$UgrZkjb+7m>&VACu`yrGti)BZkXZ_Ui2;=PX(_g|I6`#1F=Mnll-z6?U+^Q&1<1AD*ak2qUI$O-?E_Np*YGIIRf;=PIjLC$}&49a_pQ+1*(qx~mjN3VT zIvY*|#p!ldE_38Mo*5e+mqOdaQ}=GscrCBiF0xfk)bP*OV=XEFU9zjSoy$0@f+esV z=dpz><8~OMoH?0;1o)Y`JpQ*Mm6$C1eDkv)_emp>cSVM^YaZlUUWT&{L&}TamVXWr z``S`zvHHdGvP<<~sZ;x!caW~G)Wp2U*mlM4iTcK6mcVV!W9VP@Jk}fw1D}nRADmca zQFyx~$(ht=?p#Hw*e#W>7-fCD&?O7l5J@wS|xyGPZbU(yfGe;4$?m9@Mg zkE%GXAlBWEpdD8KfQI06x||D|i_w4y>*#&TYme5V42{b`jjMskd=Jj+gmtOLd_{t9 zc;}ckQMM}%%B8MvJ2>K-)7s=!LO|F20j|G6t3P%fdTNNH3zyO!Y0-LPJv^dRM1R-? zV?A?@PjBbRYWZ&8%IW&zm{|~;gSqy%a!xpE-&fn?q8{MfRRtgm6`s0~^1D25$ty^P zV;H2$ZDaS$+?sXG2_|i5F)UnEP z62m;>*$#_)_Aj63f;dhy5?2BDOkG0cj3)SW;eJ1i1=Iv2i&N|6sUcHAPg~?s`5Y=< zTg!jG#UDs6=kyE|pmSt_P#ee3iGa+8E1>uUG1BaU1~}&*cVJ&_{5cr0m8c!+Q&!i| z%bfn_XE<^8344c88+_{U4Epp(r)xP9Q|Q?>Xdy?x@Ps+rhdz^z`|_W#AUlTpWb@LL z7Wr&LHdCTooJzc#1vL!z%&_K)$}*(6?c~(w4)7!A^T(s{B4zS_D8B?t6I&bQGYvF9 z3mrM7CoovQ(^S72r*?W@eTv><4g1m^cx=&fJtKL9w#V}MaeAaG-2K(ZQW^~9e-JBi zjX6J)s_uol`h(Ek*DGFNtA8}P97N9&Apv=5o(B>vql{5LTotghLaeG!gM|R)@%pGW;Ail{63&e@SuBljJ%9<{M6H^=KECOS;<|z z<057I@U+=2T1jKu=i7vP7oT#rWs63`JDY&cl)80^?qi3(xaf1JbBr11LYI%dG5Zul zy@fyb)HoI|10uid9fgZ&-p8q(_JaOA|Eq=GG8*M~Ou7~;Ay{97&McmbuqrWa<~3!} zW@I_|=}=sU#GK+-9p0Ir7bTSAZ#81ZQ&sVNV_rdkTO$s@sM;}@h@%4+Rg*?lAI<%0HD4#WZkxvit19hoTj+V%Rj0&Loh zdDf#pmV8SB`>PTZqn>^{Jk9JlQ}QA+j@zbO?%1AkdDKi}E1mFOQcn#_yHEe|Gnk>T zl3z>HdU(q6=oU*;?xTsCi;K=;5h1MUJMYZ@R5T?rm3p_@=|@ee7Te-8#!3>uLaWt6 z_&wOZhl$HD%Q~8~&j+yMirE0Mpu8(h)^Z`w$j$O|Yk}ubr3owunc#Y=e92Cu4BN{I zoeIJ>PtM&zeiDfIi0ol3dXww;JJDJ}Bq9NE>yjE%{mD1H-p%L3#(PTn`Z#ywdA&k; zUw$kCwES8ewR7U*VQ5>=^a!u|Q4ltIsN93*#BHtUU{}3mkR(_Am6hJ7i1j$`WUkpdXf|i;V&6{2Ny$#m91FClr;~ zSym3FB#UtinIkVD|0Snlx$XPU+!`-mVwjdVuMo%$kvG?AXxtpLru8<|k2ht6nn&+z zKsR3`bV_F@F+S{PY?_0}bwA`Lt0>l+%a$TAUvS39)%Y6 z51huwqI%zp?p|^OImwdz9;7dYxa z-9`ePXzv5EFHbIPJ~iKJ^cYeN|0E{`&OHyXNIAD&BK~ZETcneIY-{DK%#!=Psm%EE zg}rA=)K}(0E%JS0-0G>;ruq?#aHr;b%11oxh&CoSqHs?pY(|&8l!}((C;<4jj-A@E zfN-p@M-Qtcb}l2b^mu*Vm$p!Lu1VW?#yO>=Z4_}Xrm?GVuZ~*nJ=OwA%?@YYHY%(K z!JlEB0DBuZYoFW+YH)0yWw}T3+tybA{X9${+c?63<=0bV{knNM-!VkVz@*33Uuce~ zFJjyr!!dSlW83!>6>fR_frSf^PH5%veM;+mUZ*gOk8drK%4@mq8QOe}Px-D{LOZ(+ zl5CD;LywFqOU@3USC)pl=V%+T?Q65BFFmbrE!tbuPaec%SI5Q7zg)Wo9I&;qn;dusl5 z{qy}+8}Ak=**6@9=kcsMe}zthMEE&kw!Ya)Pdh)iNfvbFN!Nrp>bd4Nax1cOG>kTD z-Byl1!%o2Z%Bd?{yTnf&=jviU5pPj)0C_v27t5PS1!}@DCal%PugLMN<=#Z3A4skq zX?O7x>i?{j=$EtYi_z;5ip8lrmoWo&8a2Nxy?lFYThx<*esj4eZ;f)qs651pLCEWa z;=iNbtUsSNF6Oh`;6Lm6zn>dAmUHcV?)AaQu@-^YtbY~R43T&aQjWE+C{0=(2UQ?H z5IZx`r{>rvBIe?Ct&z;7TF!hC0mpbPedNC9p8%ONUr69$=6h$7^Myv}d@VmpVwWaM z*#+~cKUs~~C1z`;flb;@uv>MG3qDzHG;_`WzEO^9d`?WG5fJ0as}tX>t|ToZ;=yW- zoOTXN_Rd2THbU`Z9vaIJRR>1(e*U6l9Crp92czwN zdE}Iacfr~I2}-N5N)T$xR8TfcY3x*_t_XV?d=^i+Tzq}a_5Rwem^lYw!wO#=RK0rg z(+^E+6;nM2e~(t9j_8Dqw=O4NS_vaw+0UQJt*?k*ex4yJFW7Zcr8Cjr7IXQ>eaw;r;frs>&37 zhG&OKJ<1CjGxHNQX8V_1jqk?QaiWeR*ZM9iTC7`*c54_~s8U|nbAYmQRoB!&dpwg# zqB}mtNofm?iZQN74V+mr@ED!tTb$BvCk#gm-AS?>Lnf zL6KDK!cPKvL2Z6cRWZEB4>sJ6)nQ4q7~^Wr|Muk@pC1F6V>@47p&dttc!tPJ+5rfTq2=(Lmo)dlKlmB!3X$38Qge|! z(fN$a73`0}lVnf>dqRJ~V!v>9L8dx92H)yw6 z%RY3OlPPkF^|@JZK&_PSoX{DysKI94Y`1L|At9eJr+Xp8c7Hos!6&ci8*nlQC&hM5 z%-p!p#>}m9r6j}m>fPmJkMa~JE^gxA=oj<5O}3s--eB25q=skz-X_`|IIV|QdO@Gy znu z%4A^qC|bttQ*QZ|Z+QOtID8o#o(dvQ@%$=sU2K$KxRgG_M}?S4=)^jGjqY{V?YQV( z_~6^9tAO!`w{jXE^tEu)i4=z6|Ia50JzitL|On?`7fdx(r?g>|JV&EVAxuiKc_8 z!Bgm)r9wFZ_&H%}WPtCxoZ~swG6CsHPN5&327<*J-}k0w(Pk;*=oyh?a~-wdEUCJf z(|Q~-rq8KAUR9JlXld<^9o?>J6(jYrV0wkYwqa{|hHFnEgXR{?=_trRYE?9!+=q^m z2>^F~a?;h8l0&vAv>c|*uSjOjX`W|pCAh29_^ntO8nQ*1dA-eY$$_3@^_!n<03FOZ z#Wbx$_r3OZgVyRJPPb&WvANYb?exHNGwu-w|40@HHG$ewKTDW5zEGcmCf{YQNQzCI zZY7=N1n9a?w%V>J&W*hdE9<&fYdj>61}uPj095q8sNQqy>Zs)LM`ZCrXzj<3{I&88 zGthPMRD{8iy+0$#?n9RA`vXw-2b#bxqn0sVTjN(O=06wB%H!(LdJH;S+o;c*1HEuU zEn}K?dDm0+xhl=){8VMRu7}O>hQ`hQ{K_O-WKr;3$lVVHGeAOTD~O`WDkBGUslD~ zamBL(mdbuTjd)~@lk>f@(51|Rjpv(Xe3m}TM?-Dvev796;(hjX=svu@oYl~GyXM3_ zzfh@6d&J(W_kMB_rBB8G%e+=g>&8O_lB?*C2}Bl}eZTZ)F@)ufpTV+i4z>2~d!OmW z%i+q&6nq5dv602ZJ`DJ5OGWH!Q_tNTchsY4yIME3czU|LC^^q-2%0b6PfwTDO>!Ue zz|9&E_rK*@AhvP3zIVDDjcLyqK35Q+uGm~>q9rqjePmB4w~$^dYtP+nQ$DGFa?B04 z%QTj+#_m(>Z{(-VUTpu+f3vts}f_?Wl)^5}q`MaFs>-OT4 z5d)>qm_T2sQ{?js<|I#2Re^6X*J0OnY<#7ZjgLSnEfHGYoc3vD7nMqwxnC)=KiZ5^ zBhWdebTp31L{Ir_Dt%Vk6PhEQip@_u4(H@Y zMD}8*F+eua8ot8{Bj}DA9eJLcoHfm71HiYUf&lxpc?>>}x_eeUk)97T1dv3apNo9s*dFks_9YmO?gm7T(#pmmj=u>`hDEcoRv5H(+Y_lBPI=ucU=IyUB zR{{MF!Rz6QVcy!x-_A1rA*2K#gVwol)b<;9&eH%k;|@l zTxi|Lvc|P7+&N-pKs2TFy?*+lU77N$OY$k>Yw>uPURFnie%u70Z>g zPB1sE=H_yAdRgV^@+P)EwMd#T9zX}+^lH$wn5E<+e{c4XAtOSmM zg#A65kYULEyDPa{uRhm((&ejyeZ$#VAF!NUogIN>MY(n{malI1_;ITn&bc{neL&BW zL5wG>bZQIytkRs$KhIe)l+{1vei4zqqA7hoZfpxVLbW3ul`WIkbH?Ri|_}m8H+_= zi=ST;<{D1-ud|g+s9a>M9f!nm`jAr6Y;#1~+jgv5J*?|e`#e>@#z$MWvY-@s34P-4 zmbSk2SiM>TU*;rV&YtCJVmFzimF>_AwCRPFfbM5FN@jct)e4aGHpC&6`?=yknSZ(D z^hbl`{ON-WbMU!T^zmt0_L;}+*n680e7v^3w3;+-zsfnJ_;Glx6kJfpp1OuCp2oK< zDLR#|uCD@>nh>ngDHSc1>&G>g9S_TPx16e%zGj3`wCGQp=Zi}I>`g44Tay+&6e|7Vp-K3#E z=532dlqY?#u4a;_gk-zXT)m~WlqG9uF^rnFv`q*ih39f|Ut`ZcHCDS`*avy4q+X-W zCYg~)sXeneh7?&$(LKx4RTJB3V5M_W`PJsraJ%6@i|FvTrL%8qY(7=+;99li_6nZS zs@h}P6T-VRxlK!Jza`s8e|#8644}q7&8O~-UJ-KTU5jTIJ1y=0b3AX>KNz5|jj_xzeZmuszOX^UU29Ao$C6lt(ttyi1GXG?>dUd_wZndPQ8 z%Ntj6mg{6`Eia+#S5aoyNhAGvo?X#-{wkQ~`4ye#XTdxVujo7<2lIS#Mdx`K%=5=9 zIZt3DYlmX^VMac7-Jz+gXo)|me9e94_`IkX$L`u{##5=Me}D(?JXy4n%Z{o4<2WV# z+u}Pvg0U@HOrLIxzp>~w-D26v7YDRY?n|20IsN!;+VNkr4H;{p&3e3L zd9{p_Y!Jb#9I>?I?}4>)4Qp9q2mPM^n=I*{DLxcCn-keVw}y}I!f|b6jLVO9&!v$`*!SJ+z9mwyeV0WGVc#ET_buq%_FabH z!@fVs?pyq_?Yj(BhkbvV-M64z+jkk#4g3BqyKh0Bw(m0Z8TS3F%)S*3YTsh1Y(}XY zA`C~hk!1@Rd#Wt3B^&~!AwkV@k_8drLQIR!%NIV)rGd7ly`nYqLRRv&QoItWmdj9+Bk+Xi!X)R-ZX=ND+VbV^)3cMJu63-=OPe?hKm8 z)erO+pCvBRj^|JvSFlpp1u5sW&S>Qk&$EEOzoB)Z6Y2?f;M_dbZphd<=@nMV_M@I} z7`-`Y;Wy?W;}~jXuC7Bq6~yf+!*kO5GTlzs>7~44n|8-Ex?Jn>=%3QKt4Q}| z?TS%-QRr2WGjMV_dQ}ZN3t}jXR>6PZK>M=T%g4F4&Hj03#sPMg`!tSxRCVY)FK|0N z0gx>8|Agv87Kc^Wsoq8;L`QBZ7LyR6Zkyk??TDx4%pdrl((b-=d`UFN8pHDusrYha zDsmQOkyOOV^*<36;e`+}zZjXKKaEV$4O$n?M|-L(sp>r-)nlR=tPxIzw5a@${)fl) zwVV+jeruGwS4?0p$Y@fCRJ}>t983Ac@rv*{$IrhZTmioX?je_l7HmcV$QMuZLmY8L-ze6& zRq&`FDmKU(RCXQSQ@~gSWbxX;n9W9 z!m}4&)0IFRoV!&F5?^o3iM~QQi`2JAVPnZWveAe&PD!%bbKL$@U68NFX_1JLVb{7# zHsjd4)yEy>8*;67WC~IYTrOHQuCL#&$@^-1|NEEu`9)@rO7)FpAJ5}_OfS?w%t;fF zEA}(aus%<-bSsXkA=!u2E=NN6JtuvIt$ObK3!k{zUGuv6le8yMZi)0aYhyZ-9ioh( z(7_P?09Ft67&$gG&es^nx_A_&xA>NlxG&Fx4k_iJKH7WY{01^x@I8Kkr_FLB;ZFHa#yOKJfzhjf&g&ch;e!H6D}RH1zO77$m!2IIUCaI;G!(Cm#;6z!@( z7(Y(IIHOU6rasEFbnOxkDfR7IUp`gJCAO=icEzMAJzgWUY;#_1auY_xLyxDARaq>~ zmi2z#e*pP@=hd&+Riz!$F0H+{n&NA=y@##9c$M^RnwVd;#3L764(-GzIeM6?Fd3(E z0q5>)5~miUi;CN=?bQqk_bs-*tGyh9OI%I$(plXxDoWPL&f^@QW$?x3%$LQJnOhC1 zS6=04l+_$t<^P(z=PQRhATeBdgq0LtMNI8yYs&^&%a%qa&@tgp+B1&=jFH!n%#QUPy!%qISdNC;-2|MMkg5xRB?fi?StM%#5O0V2y*SMkf1Q_K zS`^oH4Kr7Ea{V#vLXH<+)XdPoNVCi>ckGNMaE=4)tNxVD$B%-`Q09;pT6sJcDv6yK zaE(b6leDc52};Sac+1T$+$Q~4UVcG)7ftW#xAO7F*u0xce9N?6W`wIF$cb@>%lr+= zaf@$&Hx%704rzdH%a4w4Q?^Uv7ZbI z`5))oNSnY_w`gq|D^ge)Mhp#1?<4hHy5sP&U3=D=9p5fRkG3MMOB;yu{nhm$T7BSX z*JV7rF1N*XqI`DPW7AeYes`m4T>|WC;*QHQBhQaY1K~XW^Jj1GQSOho3d#9)D?P*4 z_36A~GHq#-Uc|dxe%ZqMh%rc@BvKWE~b#F5S)nQlHXji^u7gmAl zGUhR;o?F*91TQ`yjQS~Y1G%5Orp9^|c;b(|jWZe_`EzTo@l!hjU;2bBEk9@K#Q9}8 ziv*bt)PbS)i%*8&wGpms%E@Zz`A&az*8QbJ3*9Z-`UOw%RDgUvOWBfcw5#XF{VFRExOJak-V)i*3~K2&6pO%WvaYlGOY1!(uDN- zpIYfNvU`xGSatJV`fiqK57%f1&JWbeD;E!hm9=bBeas!Fh^|5UV;pk5qrPj*?<++Q z)QHZACOQ70nI8EPG6p>`FR`0#9&P6`qVgrL85JNh7_M#^Yr#0h714EgT4+9H%XKf0 z?`4u&J_=hK?DN2nzv|jQhG*j3!eYJ8sq_9D4zuvzn?XEp^?9g$cRsCI-rEtI#vC^Z zC(Y>Toi_+&;Wts z^CaVr+Y9s=&GL!!*(F>@#ajG&i*w>v_*%w&PeH?k9>V7_{6Q*X49dOpC~R}EJC`V9 z+IidN_$0q(K(&SIQQ;f(LZG=c%qJf}lbWJ(t&HXs7GOL%BZv_s_Uh-Iy%ft9 zgBLk|p*%)r%6&|h>^O_#^;lUXEN;`vM`U$R_OtI za5e)@OgpcBLH8op&n@#_Kg|6X)rYgcquywZWA;nB`UPE${rR)mKhX8O`w}NZ0J&Ig zz|)>^@}c%MCm-gIDfXWoXM2l?nA%>_VtZ?MTH6x-{34jGrJZo)5mD!~xfaf)?9pSi*Ym_D15XHIF1Twd1PA-0fpAEYhe)5*gUtqs-4K4JLaBwgqC;EV&AsvLlT8~0Q_6{ zHv)x)xoeMXL0yv?O6_pgW@7OSaBVA~Nb%pc{ITaoNfw2-$UCCe5mRB=tWv-U!-#rE!M!nU3k8t$?+HIg$2kj)jsllRTkMA~mF^-Dt zoDIxFx1)vLNe_7jxT;r5)Q?Y;+B)=Apxps@qi4`>N$XYc@B*JhT)BdHavo6l=>XzB z?4yGoWBfmFt$|R*=O?&w&5M2KttMP9VKk+lH_cOgCEL)_m~{wa0DNpC*=Y?DB1OWN zY`(sP94GxYo#^t_r;MKNA18K{fq;-`+4g1bjwC_#_eY6ln<@?To2I$BLq3yL`=SkFKZX6RSxivTBqgGw^W1-d#Y&W!-iQdbsu!Yu) zp2GW{y~hZ~OhLaEc#Ptdnew63kuJX_;R|+!FL8qP>vObaloUEyb2DzM^@O}h6;4P) zXyo-Q`v`uCBfKYm*tQy@Yz1~TiV}}+kL4&tIgk@(21dm4$cc0_yZ1n`(`pZNwJULN z-STw=IA_tLK#LwSW5@DBdON6wm<66AUOQOyz8+eCf?Vrk?|`?1XiI8db<{?m>w$(; zQ+jNptRtTIo)RAVq5?*wKsN#KdUia=wIc>@Dysz;%Ep0bXqvODb zunkt_V`LZ^G3p$L&VfF{9gZ@&dckpk`*(YKY~YJv^qHpd2`I|J zamie);0R}p>u@Y*;@Ga>*vsbQ!W+P`l8Ixd!iTQNik9kRbukOaGOVJkb6uTk#&IbV zN4J9Gs-1=4`~z!V`XIC1wQMb2&cb}rT&`e#oQ>ntEF70GV^Nl1zLq}A!f_dE^<^Bo zF50Z6rA+_7 zJETf{5X8yVSryAj75ay2KMp57PopwOQ&Ps1Zn+FeH_yT=#x&e=oD-;NywzHrRHWN4 zqb~QW`%seH4_3f(T>mmXGE&jl`eP8M^s26qoyB-eD}Lrjo?XS)K`(2pOq=xi>vRMX z;#O>N8uV_JqcYAXpzB*ub;e>z)3$7+WtQw1 zl|FV**0-11qTD;Y8lE-Hy`(u&8~3Aia|~lV=bk6$DLv@sG-BAu&Lp_?OUH@7dddA< z)wNmIwrowJzmvx2XTKRK z!~Efoo8E zO*#K$6}7C;I{w0%kNJ}oY4 zdW@~es&*IQh3}H2Q&uYV{jbwMb5?Q}F=xFwoz2s+VDsFx_Her(m+Ts^YOy?{+FY6R zX1h>`y@!lceWF6=Iku-`R*opSFUsw4U_V}!y${^I>h|V^?W$u;f2`b|Q4_}@QrBug zR?b8CoPXRh-$An6S?WLeBToVD;11U%NUPiLCFdVw1l+tYxNgsfSdQ)mr9|tp(Gsw+ z2FUd?uJv=qg!ztnuKp<+jc7X7aW;~|8dm9Hjq7^5oOu>h$b9tDtcp==x!G|Ee3Yf8 zP?rI<%tbC-&i|$0l3Im(vhmO+_~-twx+jCGqn#YDBxKqBE>95oPadT_HD+s}cR3B0 zy@t;n(4XkJ`+j!%5LJrXetO&ZCyZrw){=QqDzZU;|kF)ooW#uLUb?h%pL zAkS{_8h2*NGsJ_azb=kLKK;1{YHngb*lj!C)|ozkR@SfbdwFK5xt`@$Fxn?y2khbR zX!Ii}M`L}A#oS&uuFf#V)$42-J75;=Z-TM9|0teA=kYl9VZ?R2dgk}Ln}Fr-K@L~S zSFzf=hG?}8+0!0n$5*(|i`PTjJzZb?N1)lboKriWWL*0SJCcr{7xFE2WdRgk@GMh( zWpUnqeE}4%crygK6l%L{S+;&ZC#A7iqu9$wneX6?2Y1D-z&5sKQ9;`_$zI!KbJpE| z!<=0JZt8$ZH9hV zpNo5_eV3-=U}oG{&H^04tIKuTm*4p*E z+n5o{PoXnwmY+lCN}{~4g!^w~f7%f^lJ}FazLxi?PGH8D zWJY{Oj_+&6ou?Z$zQenLCfG&EcU2wWO%!|*pXXyd5B^8{6bS8>s}Of*(vrEVm={tm zbhp#N{ri_&D2M#;m9>zVhie2Uq#ZOLy+cdt@;+8qbp8<@& zk7tqE&BN&I$jz+zwlt4##k}@y_N{e9n>*8-L2GkwMLLOZeF->xVw1jn-@aLf@A-0X zHK28$mbLM{6N-HUxcM%#Dt3t^i{>_s@3gqhvvl>XMev)4(P46$cKJGlA(>lp9e4b3 z&Z(MhHB;R~E|hr585?;k4CkEXDM8| zZ~Xz+yHj{t&BbB42%=CwpF+K1p?0TGZvg7GY^Y9l3iYGb?^+*%t~R~}Xgv>F&)}@U z`G>6!BD~kHvP|K^Bf0y46$$GU<{&xil@-%gEkE_ptjFjz3Rn{po*Uy$Qzv diff --git a/sources/Core/Text/Tests/TEST_TextAPI.uc b/sources/Core/Text/Tests/TEST_TextAPI.uc deleted file mode 100644 index 51aece97052af59de1987a5b8f357582f246f133..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67104 zcmeHQZEqaMk)8ni>3{?BCHW8_hxNvZ={TZE>V>jnJEAB{mLpk;L_=rzav~*?5+jOY zxTIuRkbl7KWp+2t}87Ub;COLuj>S9NvI|NdY0arQ@i{yqD*tdCw9pKCh{O_anE!?$*_IB|58O|zv=d$_zD2#;vDaI%m z+spPbivH;Lp&OZDcP)F2tJ}D`+fyTHwkYjX(9>;h}AT&p|Tr|9Q3&Yj@+ z80Uyf-plq$Lt_3Bu9Bu~jd1X;ZE%55_BoEcdl&5(6msW@yK@JvkSm_LHrUb;D9*7e zzsQvv**Dp@IC}$k9Jsr-fQdf%L?qMu#I8G(5V@b2N)LG~xy`O-l%9G&TB=W#BCJ%ozS5IcsC1;9z%39g9` za<=yIA$}99uW{UWcM8tJeoaQK-4wM`v?D(D(1X$8se^!cAZ7$Qq1r3lx$iLl$YJ=z zVR{#s*#|bPZ{dr6_5kv{k$sij0rfV}-Wurm9gYw1?H)e-y@4}-#yPgl{)lJdW5?mv zhiebKafqJwaE(w4r&6{l?Zgl|NY;%WPXS< zUwGL0*iqs!?xHTBHrQ}<-pC%}n{Rcnaf;j#UeSV&0X3!nz+DxsXgI!~Eo2`$th{mm1$VqI zn0@H<`7vaRTuM16E;&*@Cosg%C_VTTp?U^fmfe5Djo<|Msr$s2**_g#sA;HI>OOZL zchf2ouamZM4_J5wy<#oV(>ZW5d*rxA_^NEgH*vq823G6y3lm1g>((huPnN zdvcuF+1rjgo_SP$JC?V}__GfGQJO=(6-p4Z)D#QZV9d!vbNaIMP>MKuGa)UDXeH0< z_kn>&Xgz(TV+{KX)9FLz#NkrXN=>@%jrDf~X;^On&!I3@?{WDUASlCYXCssh@ z_NLQxC%Bi`x`yY8dDZx0la4XkBS24`nb!lp^?2-hCp6R&qG1I-#XfymLQcyz2sDmr zkt?`1fF_{!<5Qt-PSL{b#h0Kubu4?|Ku39X`kvizOT|0B@7pi)ce}y8->e zR{PoK_|T8oa6Zc&cpLOX9s+AS(6L+SZy)XFd8J$UE_Vi>&jock&p2)aW2TiE=D0A{ z9c|i<`PtfUr(v2vRVKl)4oXYZLG}T#m6xYAaML=p?oC{YWs7nc%G5r1akerLj$LJ7 z36i#i&&6zc%KZM^D=fY)Y$Csmiw=;ZnoBwE{h}r=$RSgf*VbWjVH^T5ib zY4hvS|I~)%5uj1L6!YsnK&<{6?Z+$UaY(#oyE^feAWp{LnLh|hTx=+4)K&euV8 zdEKYIKGV#{eE>L_PkIiIl|CG~fd1_ezjyH={bd}>*=o&^tRkK9OdkJD2KdY`%^&{Z^a{qFwg0J^9=aJqADHlgDF- z<>_lE!(krxe{y(|%+5{Bc-;Wr*TD^zU6U-i>Q+6L$s}Xs)hWkbWh3HDC#m24s4hWD z^7`Ai<>m4a^7$KZ&=XiT<6Gfc)#y^Z-KCdQJ-W*YsqJcXzCN0beKsvxmN723#ny)EUGddu zqwQK~%ZhEev{9@%!**yX!+u16rR;K*1rA#-h9K0C%{_qy=gEdp&%*wzy-dyal-vGk z$0o(NPH9TxDf$UzT2lN2ogq5?`Ju9LruF8eVonu?WIk(ws z=yFRL>e3jKU1{lZC_+s=kG`yOb6O|IqYM4mY3t>>`ngzVU4xaM z%?y6kh|@Y|I-@?@nz}nocB8RXx1Y6Wp{|UqA>w95W=vIt$H=D)Q!68^NM;M z=20w;^A}y&`HfL@!fchSpICSEc~)~7+iiM$QhiudBgY&l>wM3R;|Q}JVI^rgXBXxX z6*8?~j%P-gC)`7iGUG^mSzeJ9FIrp8oaJ*jt3lta8{0%!Z=px2-(5goO1C=R;sAB1 zoK@-LFP{->t_KK<)d@F;<=Xtf()(4%u(Y62@6Qn|zD5eU^&2N0=b-C2k4_$7O|R5# zajx#jos-&po@WH{((xoK=~=rkYgJfb&WdKK`PP*T)V0ECjE6htk+Jh2aGSgRRx zXGhdhlQdu&~O4G?UuwMNl)f zw#PX;i;+bk`va#i_F>*2tAjIo(VU4S?z9Gol_en#XOFSdfU5{!^`y^auC2#W z7mo6^bp}td4)_w02-g5p>&N;)6g%l*{6F!hCkV4OVP2 zvfx}K_Y|N-$w&IqLc0;?`>h9ijDzYN!zR|e46$;GdqZ$!oMYI8B;?nsrX+$-(z42$ zwW5~H5{th<+=aDF%rtPd@?OT8XRb+;mGj~50IZ-C7}ME~$>Mx}`x zMf}P6fMsQl@N#~RV3Bo8^FgDFbA*>ma|Fxk9Kj-MOy`4NK8f&B?6VYE0@_PjO!{kT z1Gs9-G`qxY;SLA9lNmZ%k4Y>*b^Oyt@BTi*OIzQ$1vI^1N6@sV66YMaO9p417(3IR zpCtVkeN!3+jgIjdKF#0&7Igns!rz1rg)K+RC%tl zxLS2pd`~shOIXct7NK4{lTcqhlTfdpNvN-#64XVux`__;ccSCYbF?(F>X^Gc_ObiL zeVmp35V<=Iv&^zOMe`XS;4aR{Fw618U4IP?UcJgO)VMNJORa5@{jBuq+L_a*v;bWD zf420gK)rg#pyqx8{G1N1%VnIqY08S&dLEN6?IM3V1CsVwWIs>FQ?gGc`;wW}$BksX z%vWMYkp4th-q8i@Zc~R=)p1^Z|A;ZS^w}<2u*|j9$kVTmcgj267(w>8)EQHY4`RQN zwKd_MeWN#HIA6T>w71XrU-w0js3p~R+;UihPPwCp58*}m_|eF!?J=$|E8aJ8fR(v2>!#XEdr$Mu zwDGb#H+Z=|S9p0jH+cEadBV#mdpQSk)cY)l7hkeN+r~&qbQE)Ev7VC56>AjepY7oe z#$z@Us{Wen&MH-^QfvLn#a_=E=(%Q=h`OPfcG;dAzP&brBxR+_b>5V5)RVp5=7EQ0 z2i3GC@5-z7>&48=sq5jcJ6TYFjP;Z1FKZsU$S=)T`p~m5-$D9M`3Irw8pLt!kngL| zC;bQhWY6ucRy^eW4j-4?m00+AklB7o%*HTp&iYn*XM!c}2;}Q0<^Qgabs#ZcQF~8& z9!;b$jTc4~yU?jxPg-6{HX95zgUN8^Q_ts6;&2dEwUIMfwaN^g%!z7DM4wsTC&Bl2 z(DsOR;<|?ecSrpqnj6LQnoROR%_v^%=QO8!dp&TrsGhdQ zv(}aA+WXnaMqWpzVX>;&*+4~W8s<9Mc}UzLk}Lk@?OW>o?_gC3-&qmvl0j|E{SRN` zT`u&0ifRGAKx>?>ks6o-Jh7j%{7qWY{owAR71`mxYHtb3JuS{&E*s0t4=qm~wnene ztj!Z=t@>E=!hK5Oe#84{pQxpD?_55?kutIhvvFF(umFg3KAmqz;;+x}R}PQf`H_Q) z`#So1_)YLj<&$sM;dlD2M$>er%WqAeFT&LKfcfu;^O*IdeE4+&MOZ6-h8Rr4uW;1E zuih<^kp306q*5ewnx+Ij0LJJ8krJHYlJfDObr%6l7+t(dL{yvnmm94n{ zcQRHiFL})rW3Twx>B^eQT(RC!?q@_GZ%6T^K%cLpcSMSG2d!o}37?%UPVS82nlR6% zHD&^tKcA%-lXxedcNqqHL@}roh~GIQk%;WF)yMkeKS2A&cdbE7$(|iu(WhwFuWPU$ z78jEiv?@~J$M?bIt*DMrdE~uuPLR(D{uZ+}XOk^oqWYKPI&`t9<}32r)R!G;E!3~t ze^|h4AHI-X&Ouw%nsB>W z`}7Wyp)CpFEgskRfa~voj%^mh!T3w~`K!R<7C%vqhf7hVP*rR7@L*aY#6i8*!|D^? zs**FdC!(A|B&`n|mvMeBgs_ZC%Z;cMzSXsA{bGE_OxRx;)>j%~eZNE5m<;PxSXlr*hy&u{lM!B;;N$$M z;bXS})h;xG@*nW6Ngvco$oi zM^V$F+On4=X-2Vk5oLa_hU%>6q!DGY^flGN?_$0(Is4a+`HH#7Rv_1pc+Xy>S3G{- zo|{H|7m2a9#IJ&wG!oO<&~+Cv$$k|w8|u%GOPxo4R#Vni>8xhBo`MyGT%pB^L#{OC zoFHfY;?>98N1An;+y#jvQivo*)2b7{4lKRSME0O29zH-@eDjdi^YFJ^*Lr@!#+^O- zsH|jtr9jAhC0B+J_D=(7bf)tsT)l~_%x$v1fYqaXZvuA!*Ei2|c8V)1*b;Txu4|8d z@hMh$ZsHtkJp57lc^`?wHpg=!iW47-uSR`Jn|_}%E_vJfAs~@@Q_g6o#;Ua*oRMYK zC~Ip8H+f+Tqw{H^{p<0S8_`qTPfSP+3;Blj_rPIQW_k}CE_jb$lObL>IpoOk2=P@9 zH+-+W>VVnd#%3LbONPLZt{);77&dgBNEz?YLJS zIW&ALRNk)LMGIDoqPa!M

VKDr(*#s}cNrOxycfM9+q^d@mr0gpOK8tSI}TR+mq) zElE{OYmH*6U;1IRJyLU~?~eCZHxyNKh5kpmrx1FwVGS3nrf3n!N3tTKKAh{okj(Q@ z7KwWy`N^X;Cd%LQ(y#X3L!5Wks*;VKstz%#v5w+x!>z8c}Fbb*lQaa_2 zll3V5`2BLE(&PSAder<;e_Wjqj*~c_^+<{9Y4qviHBV3RccIdCKOIZyCiYjhsryFfw-cZa5v$h1>l2Y);XqmmsVC+)|J*e z`1&eGXQ|~h+}GigX811WCFKI6h;n3oK4}V%j5*CqhQrDDByYpi2DGc7&{*Z+Xa%(& zkI;AK4)x5%@ZC=Q#r;!K5ro{!7>aRCYR8#)HBFpZ;`-uq8P{WPq9@CwY~l?4?>scA zT`=-*__+t){1u`XYb`x4i)c8)(7sOECVQf86HlYNc4I%u+HS`Wla4gR9`h}sCq^vj zDi5Esc~33>`w;k(RrEduL+bc#H>n7cyyi8u&#Y$HroUUv0PmW#ccJC8p^QqIp$p%I zCQ?Irpob~3Id_TP@mjL6h53XOSEu)O*#KwBTcR{dRS8@i$@2#A?qiOgE9F5)OuVk{MnZE|eo@!k!VeB2OM_Z)VpzqtET8E(Rr-+mMl<0-_sVozqc z(zzq6OEbyV=M1+#^kuF2_brsryG#ZIGd2sLlq7FcH|O7I?(6GV4r~nb&ie+bk43c* z#nNzhthvGww!>Q&QsBd^JVi@oJ8!5IgnUbRi1mz*6<>nVcq^AF+qvs>tXOQXyR&4J z2lEAdtPv;0SMRvlI+?Qz<@2oCy>P6dbabQDg@2pdzL5Tmk0!N$Wn)wg(A;C_Li+1V zaM}juanyw!R4t@O&No!qMe%_{XkuF#(!rNH?MsfuJ^Ih&ZD-iK+5~O6xNl#k$zv&v ztaP$gisl^_##-6G*Pj8Exd6LLuhbt`s8>R-PGqSoyC1^PUiMR%Q~WCi+xx`pf$sM0 zpp9oSm~&3SCAy@pu2*jITM8)?LKWgtbZ7hg&H^i1S{C?s#IOiDQ&hTiRyP zF{ZxH@xG^GG|bCI?5!A-yqErp(&= zujxW9<3n_oqV{;aq2!&e2NUx{8!x76tg2*L#Zu?#+P=1s7q+#6%V03?AB9>N7`P_%j{TeuVg&UqnMbM;U1jI zE9#HLBlS7Or>Sy-KaP+Fao-``z0|weXRQaM>+muA0)Z4jtFOD?Zv>$qvYk+J!i>YjVcI#`l$3zo<)AEA0AKC+O)e*8|`8}I_ACX+C zmDTwes!dC=^rc$Mztyl9vU=_1-$>cEz5MPMZQDDav-{;#Q;TB9$?U$#ms%QaF^tht z6w*88I9dOp9OhxDN7*IlrZzCda$naIOJQYsD$vB9SUudVOa&U&GF5r0Dco>1Q1_II zcP$G&q>#r#?}+orjD>0AK5$l!eBaH?TG}dB%_~>Qz4(x^uftKQpKu&_5Md>}Qh?*6 z&XP)r6t!YMD8yuZ-Pb0{A(MNqKVRC^lWnOQ4r>Y0x-7gWMJ45mdEw4_7+_sjzGC3# zg@KiyJqEa6xd`G z_s+_ymiH4=$#5B8i#6G_=02LlqoEH_7CASGQLDX?(BCYMwyIl_{94`iVopr?O7kAA z%9y)`Qd-SN*3}eHZ=+2t5b~jVo7!?U3xQbDNYLcvM%@8IEv~x{9I^va}lW# z%vS`goIh!`W~_y&sV9wqRbsk@=CmW=m`n4X!6Y_2ZZ*YiK7E;kYVUbf>yFq~huSE< z9&?e|a+Tex*sBRXD1)=h6bKFCQMbWLvHuz4GS6-8b2=dp_zeOJ5~5)(+#0`N6y7F5cmN*8Hyag4`^h`QUqn z=I6ry>IubKrMc!YTiR}(*5=~Zk%iz6DC=IPqKg%omiMsM;@Njbx15V--b8niI>G)k zhof`)&hnHU%Flh|ci{}PFA4>-E6lz4J!>!AQ;YKi&s?U9c^c}}aE>GeNtI^Kb38Le zvPTbVHE!WgaKQKRsZ`iKH*k0AT~}C1*82rAaje2iSK(;C`J-0nH0$ecuCJz6N&Alb zYCHCuGaS40rDON4e>*Nr@%rg-;kRR{Ys`g^keP8IR#CDqrG#=~}3i^XO&799ASnoU^;p zLW;hId@}N-eBQz58ov3FFnPC24l{2eKSqP(KH#{Gy+2||CesvkTAsxi>v8%trO2?a zRP8EKrl%zN+0#>@ys5X&8Cj zc2OetKBkqRC&wMJ2`hi)qgeWYj4By5w{ZOz;F?d!HVfb?<0e*49y%W4&e;+<9KgFQ=NCp1yn6_)VjPj;;k3VU z9Z$->yte*aWoWTdUSisoanM=;K29I9|IR_yl^$DnF&)=o>s!;AiIK{f$+Z|Y9d*iG z9GN*ghQ;+;ZLY%@rErK5-D$v~WxHdQ=MGjI`zmpNUBb7NgU?oV`hHBAZkdeA z+_{Z=lye0a-vMxpk7SYSP&~c&;uW+tm}6ep%Hf*t-BBM$W68AEniaSHn1k><-Yq^~ z5N_lkT*3R#6~gIotny>1d-lMsl24P2TYcMTwDjRs`cGHdFFWjU9!q#nwbJD=4i%0n zKAb)5LV3b|-pP%^?sLHWLNAgf&|dH% zx)x=Wr9J-V%p!e(GL0+V!VWAkduF?$*I6A+OU~T1yg%tI$%2=QSY~3sp z_vxw}I$b$8Sy|3}ZaW*$`?6AmdmpE>mKDDJOZIT(F2#(jLP_Zd83d*H74Xo&Nd`R~ zk(IMyHGY?~xo3ildcQ7#+9I(q-{r?A^iHY&$Z26tNN3r;fi{&)I;%Ok3P!2|`q>(y zD9$+ZS)WtWIce$^Jm33m?_%%M-WRAt$tCvRm;g~tH162s`nefHNamVuzn0!-@~sye*dQT%f#5}@iSUG!I9j5 z8~FWUoR=T9i{H9hiMcuEwDz1up)HZPkvlyw?v?oQC0gTK;%sI`V!}njgTKG@mfT1` c!S_48)!ttnhF37F(_cDV`YVn3?EnA( diff --git a/sources/Core/Text/Text.uc b/sources/Core/Text/Text.uc deleted file mode 100644 index a8d6e27..0000000 --- a/sources/Core/Text/Text.uc +++ /dev/null @@ -1,290 +0,0 @@ -/** - * Text object, meant as Acedia's replacement for a `string` type, - * that is supposed to provide a better (although by no means full) - * Unicode support than what is available from built-in unrealscript functions. - * Main differences with `string` are: - * 1. Text is a reference type, that doesn't copy it's contents with each - * assignment. - * 2. It's functions such as `ToUpper()` work with larger sets of - * symbols than native functions such as `Caps()` that only work with - * ASCII Latin; - * 3. Can store a wider range of characters than `string`, although - * the only way to actually add them to `Text` is via directly - * inputting Unicode code points. - * 4. Since it's functionality implemented in unrealscript, - * Text is slower that a string; - * 5. Once created, Text object won't disappear until garbage collection - * is performed, even if it is not referenced anywhere. - - * API that provides extended text handling with extended Cyrillic (Russian) - * support (native functions like `Caps` only work with Latin letters). - * 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 Text extends AcediaObject; - -// Used to store a result of a `ParseSign()` function. -enum StringType -{ - STRING_Plain, - STRING_Colored, - STRING_Formatted -}; - -enum LetterCase -{ - LCASE_Lower, - LCASE_Upper -}; - -enum StringColorType -{ - STRCOLOR_Default, - STRCOLOR_Struct, - STRCOLOR_Alias -}; - -struct Character -{ - var int codePoint; - // `false` if relevant character has a particular color, - // `true` if it does not (use context-dependent default color). - var StringColorType colorType; - // Color of the relevant character if `isDefaultColor == false`. - var Color color; - var string colorAlias; -}; -// We will store our string data in two different ways at once to make getters -// faster at the cost of doing more work in functions that change the string. -var private array contents; - -/** - * Sets new value of the `Text` object, that has called this method, - * to be equal to the given `Text`. Does not change given `Text`. - * - * @param source After this function caller `Text` will have exactly - * the same contents as given parameter. - * @return Returns the calling `Text` object, to allow for function chaining. - */ -public final function Text Copy(Text otherText) -{ - contents = otherText.contents; - return self; -} - -/** - * Replaces data of caller `Text` object with data given by the array of - * Unicode code points, preserving the order of characters where it matters - * (some modifier code points are allowed arbitrary order in Unicode standard). - * - * `Text` isn't a simple wrapper around array of Unicode code points, so - * this function call should be assumed to be more expensive than - * a simple copy. - * - * @param source New contents of the `Text`. - * @return Returns the calling object, to allow for function chaining. - */ -public final function Text CopyRaw(array rawSource) -{ - contents = rawSource; - return self; -} - -/** - * Copies contents of the given string into caller `Text`. - * - * `Text` isn't a simple wrapper around unrealscript's `string`, so - * this function call should be assumed to be more expensive than simple - * `string` copy. - * - * @param source New contents of the caller `Text`. - * @return Returns the calling `Text` object, to allow for function chaining. - */ -public final function Text CopyString(string source) -{ - CopyRaw(_().text.StringToRaw(source)); - return self; -} - -/** - * Returns data in the caller `Text` object in form of an array of - * Unicode code points, preserving the order of characters where it matters - * (some modifier code points are allowed arbitrary order in Unicode standard). - */ -public final function array ToRaw() -{ - return contents; -} - -/** - * Returns the `string` representation of contents of the caller `Text`. - * - * Unreal Engine doesn't seem to store code points higher than 2^16 in - * `string`, so some data might be lost in the process. - * (To check if it concerns you, refer to the Unicode symbol table, - * but it is not a problem for most people). - */ -public final function string ToString(optional StringType resultType) -{ - return _().text.RawToString(contents, resultType); -} - -/** - * Checks if the caller `Text` and a given `Text` have contain equal text - * content, according to Unicode standard. By default case-sensitive. - */ -public final function bool IsEqual -( - Text otherText, - optional bool caseInsensitive -) -{ - local int i; - local array otherContentsCopy; - local TextAPI api; - if (contents.length != otherText.contents.length) return false; - - api = _().text; - // There's some evidence that UnrealEngine might copy the whole - // `otherText.contents` each time we access any element, - // so just copy it once. - otherContentsCopy = otherText.contents; - for (i = 0; i < contents.length; i += 1) - { - if (!api.AreEqual(contents[i], otherContentsCopy[i], caseInsensitive)) - { - return false; - } - } - return true; -} - -/** - * Checks if the caller `Text` contains the same text content as the given - * `string`. By default case-sensitive. - * - * If text contains Unicode code points that can't be stored in - * a given `string`, equality should be considered impossible. - */ -public final function bool IsEqualToString -( - string source, - optional bool caseInsensitive, - optional StringType sourceType -) -{ - local int i; - local array rawSource; - local TextAPI api; - api = _().text; - rawSource = api.StringToRaw(source, sourceType); - if (contents.length != rawSource.length) return false; - - for (i = 0; i < contents.length; i += 1) - { - if (!api.AreEqual(contents[i], rawSource[i], caseInsensitive)) - { - return false; - } - } - return true; -} - -/** - * Returns `true` if the string has no characters, otherwise returns `false`. - */ -public final function bool IsEmpty() -{ - return (contents.length == 0); -} - -/** - * Attempts to returns Unicode code point, stored in caller `Text` at the - * given `index`. - * - * Doesn't properly work if `Text` contains characters consisting of - * multiple code points. - * - * @return For a valid index (non-negative, not exceeding the length, - * given by `GetLength()` of the `Text`) returns Unicode code point, - * stored in caller `Text` at the given `index`; otherwise - returns `-1`. - */ -public final function Character GetCharacter(optional int index) -{ - if (index < 0) return _().text.GetInvalidCharacter(); - if (index >= contents.length) return _().text.GetInvalidCharacter(); - - return contents[index]; -} - -/* - * Converts caller `Text` to lower case. - * - * Changes every symbol contained in caller `Text` to it's lower case folding - * (according to Unicode standard). Symbols without lower case folding - * (like "&" or "!") are left unchanged. - * - * @return Returns the calling object, to allow for function chaining. - */ -public final function Text ToLower() -{ - local int i; - local TextAPI api; - api = _().text; - for (i = 0; i < contents.length; i += 1) - { - contents[i] = api.ToLower(contents[i]); - } - return self; -} - -/* - * Converts caller `Text` to upper case. - * - * Changes every symbol contained in caller `Text` to it's upper case folding - * (according to Unicode standard). Symbols without upper case folding - * (like "&" or "!") are left unchanged. - * - * @return Returns the calling object, to allow for function chaining. - */ -public final function Text ToUpper() -{ - local int i; - local TextAPI api; - api = _().text; - for (i = 0; i < contents.length; i += 1) - { - contents[i] = api.ToUpper(contents[i]); - } - return self; -} - -public final function int GetHash() { - return _().text.GetHashRaw(contents); -} - -/** - * Returns amount of symbols in the caller `Text`. - */ -public final function int GetLength() -{ - return contents.length; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Core/Text/TextAPI.uc b/sources/Core/Text/TextAPI.uc deleted file mode 100644 index 009a138..0000000 --- a/sources/Core/Text/TextAPI.uc +++ /dev/null @@ -1,1281 +0,0 @@ -/** - * API that provides functions for working with text data, including - * standard `string` and Acedia's `Text` and raw string format - * `array`. - * 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 TextAPI extends Singleton - dependson(Text); - -// Escape code point is used to change output's color and is used in -// Unreal Engine's `string`s. -var private const int CODEPOINT_ESCAPE; -// Opening and closing symbols for colored blocks in formatted strings. -var private const int CODEPOINT_OPEN_FORMAT; -var private const int CODEPOINT_CLOSE_FORMAT; -// Symbol to escape any character in formatted strings, -// including above mentioned opening and closing symbols. -var private const int CODEPOINT_FORMAT_ESCAPE; - -// Every formatted string essentially consists of multiple differently -// formatted (colored) parts. Such strings will be more convenient for us to -// work with if we separate them from each other. -// This structure represents one such block: maximum uninterrupted -// substring, every character of which has identical formatting. -// Do note that a single block does not define text formatting, - -// it is defined by the whole sequence of blocks before it -// (if `isOpening == false` you only know that you should change previous -// formatting, but you do not know to what). -struct FormattedBlock -{ - // Did this block start by opening or closing formatted part? - // Ignored for the very first block without any formatting. - var bool isOpening; - // Full text inside the block, without any formatting - var array contents; - // Formatting tag for this block - // (ignored for `isOpening == false`) - var string tag; - // Whitespace symbol that separates tag from the `contents`; - // For the purposes of reassembling a `string` broken into blocks. - var Text.Character delimiter; -}; - -private final function FormattedBlock CreateFormattedBlock(bool isOpening) -{ - local FormattedBlock newBlock; - newBlock.isOpening = isOpening; - return newBlock; -} - -// Function that breaks formatted string into array of `FormattedBlock`s. -// Returned array is guaranteed to always have at least one block. -// First block in array always corresponds to part of the input string -// (`source`) without any formatting defined, even if it's empty. -// This is to avoid `FormattedBlock` having a third option besides two defined -// by `isOpening` variable. -private final function array DecomposeFormattedString( - string source) -{ - local Parser parser; - local Text.Character nextCharacter; - local FormattedBlock nextBlock; - local array result; - parser = ParseString(source, STRING_Plain); - while (!parser.HasFinished()) { - parser.MCharacter(nextCharacter); - // New formatted block by "{" - if (IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) - { - result[result.length] = nextBlock; - nextBlock = CreateFormattedBlock(true); - parser.MUntil(nextBlock.tag,, true).MCharacter(nextBlock.delimiter); - if (!parser.Ok()) { - break; - } - continue; - } - // New formatted block by "}" - if (IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) - { - result[result.length] = nextBlock; - nextBlock = CreateFormattedBlock(false); - continue; - } - // Escaped sequence - if (IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { - parser.MCharacter(nextCharacter); - } - if (!parser.Ok()) { - break; - } - nextBlock.contents[nextBlock.contents.length] = nextCharacter; - } - // Only put in empty block if there is nothing else. - if (nextBlock.contents.length > 0 || result.length == 0) { - result[result.length] = nextBlock; - } - _.memory.Free(parser); - return result; -} - -/** - * Converts given `string` (`source`) of specified type `sourceType` - * into the "raw data", a sequence of individually colored symbols. - * - * @param source `string` that we want to break into a raw data. - * @param sourceType Type of the `string`, plain by default. - * @return Raw data, corresponding to the given `string` if it's - * treated according to `sourceType`. - */ -public final function array StringToRaw( - string source, - optional Text.StringType sourceType -) -{ - if (sourceType == STRING_Plain) return StoR_Plain(source); - if (sourceType == STRING_Formatted) return StoR_Formatted(source); - return StoR_Colored(source); -} - -// Subroutine for converting plain string into raw data -private final function array StoR_Plain(string source) -{ - local int i; - local int sourceLength; - local Text.Character nextCharacter; - local array result; - - // Decompose `source` into integer codes - sourceLength = Len(source); - for (i = 0; i < sourceLength; i += 1) - { - nextCharacter.codePoint = Asc(Mid(source, i, 1)); - result[result.length] = nextCharacter; - } - return result; -} - -// Subroutine for converting colored string into raw data -private final function array StoR_Colored(string source) -{ - local int i; - local int sourceLength; - local array sourceAsIntegers; - local Text.Character nextCharacter; - local array result; - - // Decompose `source` into integer codes - sourceLength = Len(source); - for (i = 0; i < sourceLength; i += 1) - { - sourceAsIntegers[sourceAsIntegers.length] = Asc(Mid(source, i, 1)); - } - // Record string as array of `Character`s, parsing color tags - i = 0; - while (i < sourceLength) - { - if (sourceAsIntegers[i] == CODEPOINT_ESCAPE) - { - if (i + 3 >= sourceLength) break; - nextCharacter.colorType = STRCOLOR_Struct; - nextCharacter.color = _.color.RGB( sourceAsIntegers[i + 1], - sourceAsIntegers[i + 2], - sourceAsIntegers[i + 3]); - i += 4; - } - else - { - nextCharacter.codePoint = sourceAsIntegers[i]; - result[result.length] = nextCharacter; - i += 1; - } - } - return result; -} - -// Subroutine for converting formatted string into raw data -private final function array StoR_Formatted(string source) -{ - local int i, j; - local Parser parser; - local Text.Character nextCharacter; - local array decomposedSource; - local array blockContentsCopy; - local array colorStack; - local array result; - parser = Parser(_.memory.Borrow(class'Parser')); - nextCharacter.colorType = STRCOLOR_Default; - decomposedSource = DecomposeFormattedString(source); - // First element of `decomposedSource` is special and has - // no color information, see `DecomposeFormattedString()` for details. - result = decomposedSource[0].contents; - for (i = 1; i < decomposedSource.length; i += 1) - { - if (decomposedSource[i].isOpening) - { - parser.Initialize(decomposedSource[i].tag); - nextCharacter = PushIntoColorStack(colorStack, parser); - } - else if (colorStack.length > 0) { - nextCharacter = PopColorStack(colorStack); - } - // This whole method is mostly to decide which formatting each symbol - // should have, so we only copy code points from block's `contents`. - blockContentsCopy = decomposedSource[i].contents; - for (j = 0; j < blockContentsCopy.length; j += 1) - { - nextCharacter.codePoint = blockContentsCopy[j].codePoint; - result[result.length] = nextCharacter; - } - } - _.memory.Free(parser); - return result; -} - -// Following two functions are to maintain a "color stack" that will -// remember unclosed colors (new colors are obtained from a parser) defined in -// formatted string, on order. -// It is necessary to deal with possible folded formatting definitions in -// formatted strings. -// For storing the color information we simply use `Text.Character`, -// ignoring all information that is not related to colors. -private final function Text.Character PushIntoColorStack( - out array stack, - Parser colorDefinitionParser) -{ - local Text.Character coloredCharacter; - if (colorDefinitionParser.Match("$").Ok()) { - coloredCharacter.colorType = STRCOLOR_Alias; - colorDefinitionParser.MUntil(coloredCharacter.colorAlias,, true); - } - else { - coloredCharacter.colorType = STRCOLOR_Struct; - } - colorDefinitionParser.R(); - if (!_.color.ParseWith(colorDefinitionParser, coloredCharacter.color)) { - coloredCharacter.colorType = STRCOLOR_Default; - } - stack[stack.length] = coloredCharacter; - return coloredCharacter; -} - -private final function Text.Character PopColorStack( - out array stack) -{ - local Text.Character coloredCharacter; - stack.length = Max(0, stack.length - 1); - if (stack.length > 0) { - coloredCharacter = stack[stack.length - 1]; - } - else { - coloredCharacter.colorType = STRCOLOR_Default; - } - return coloredCharacter; -} - -/** - * Converts given "raw data" (`source`) into a `string` of a specified type - * `sourceType`. - * - * @param source Raw data that we want to assemble into a `string`. - * @param sourceType Type of the `string` we want to assemble, - * plain by default. - * @return `string`, assembled from given "raw data" in `sourceType` format. - */ -public final function string RawToString( - array source, - optional Text.StringType sourceType, - optional Color defaultColor -) -{ - if (sourceType == STRING_Plain) return RtoS_Plain(source); - if (sourceType == STRING_Formatted) return RtoS_Formatted(source); - return RtoS_Colored(source, defaultColor); -} - -// Subroutine for converting raw data into plain `string` -private final function string RtoS_Plain(array rawData) -{ - local int i; - local string result; - for (i = 0; i < rawData.length; i += 1) - { - result $= Chr(rawData[i].codePoint); - } - return result; -} - -// Subroutine for converting raw data into colored `string` -private final function string RtoS_Colored -( - array rawData, - Color defaultColor -) -{ - local int i; - local Color currentColor; - local Color nextColor; - local string result; - defaultColor = _.color.FixColor(defaultColor); - for (i = 0; i < rawData.length; i += 1) - { - // Skip any escape codepoints to avoid unnecessary colorization - if (IsCodePoint(rawData[i], CODEPOINT_ESCAPE)) continue; - // Find `nextColor` that `rawData[i]` is supposed to have - if (rawData[i].colorType != STRCOLOR_Default) - { - nextColor = _.color.FixColor(rawData[i].color); - } - else - { - nextColor = defaultColor; - } - // Add color tag (either initially or when color changes) - if (i == 0 || !_.color.AreEqual(nextColor, currentColor)) - { - currentColor = nextColor; - result $= Chr(CODEPOINT_ESCAPE); - result $= Chr(currentColor.r); - result $= Chr(currentColor.g); - result $= Chr(currentColor.b); - } - result $= Chr(rawData[i].codePoint); - } - return result; -} - -// Subroutine for converting raw data into formatted `string` -private final function string RtoS_Formatted(array rawData) -{ - local int i; - local bool isColorChange; - local Text.Character previousCharacter; - local string result; - previousCharacter.colorType = STRCOLOR_Default; - for (i = 0; i < rawData.length; i += 1) - { - isColorChange = rawData[i].colorType != previousCharacter.colorType; - if (!isColorChange && rawData[i].colorType != STRCOLOR_Default) - { - isColorChange = !_.color.AreEqual( rawData[i].color, - previousCharacter.color); - } - if (isColorChange) - { - if (previousCharacter.colorType != STRCOLOR_Default) { - result $= "}"; - } - if (rawData[i].colorType == STRCOLOR_Struct) { - result $= "{" $ _.color.ToString(rawData[i].color) $ " "; - } - if (rawData[i].colorType == STRCOLOR_Alias) { - result $= "{" $ "$" $ rawData[i].colorAlias $ " "; - } - } - if ( IsCodePoint(rawData[i], CODEPOINT_OPEN_FORMAT) - || IsCodePoint(rawData[i], CODEPOINT_CLOSE_FORMAT)) { - result $= "&"; - } - result $= Chr(rawData[i].codePoint); - previousCharacter = rawData[i]; - } - if (previousCharacter.colorType != STRCOLOR_Default) { - result $= "}"; - } - return result; -} - -/** - * Converts between three different types of `string`. - * - * @param input `string` to convers - * @param currentType Current type of the given `string`. - * @param newType Type to which given `string` must be converted to. - * @param defaultColor In case `input` is being converted into a - * `STRING_Colored` type, this color will be used for characters - * without one. Otherwise unused. - */ -public final function string ConvertString( - string input, - Text.StringType currentType, - Text.StringType newType, - optional Color defaultColor) -{ - local array rawData; - if (currentType == newType) return input; - rawData = StringToRaw(input, currentType); - return RawToString(rawData, newType, defaultColor); -} - -/** - * Checks if given character is lower case. - * - * Result of this method describes whether character is - * precisely "lower case", instead of just "not being upper of title case". - * That is, this method will return `true` for characters that aren't - * considered either lowercase or uppercase (like "#", "@" or "&"). - * - * @param character Character to test for lower case. - * @return `true` if given character is lower case. - */ -public final function bool IsLower(Text.Character character) -{ - // Small Latin letters - if (character.codePoint >= 97 && character.codePoint <= 122) { - return true; - } - // Small Cyrillic (Russian) letters - if (character.codePoint >= 1072 && character.codePoint <= 1103) { - return true; - } - // `ё` - if (character.codePoint == 1105) { - return true; - } - return false; -} - -/** - * Checks if given `string` is in lower case. - * - * This function returns `true` as long as it's equal to it's own - * `ToLowerString()` folding. - * This means that it can contain symbols that neither lower or upper case, or - * upper case symbols that don't have a lower case folding. - * - * To check whether a symbol is lower cased, use a combination of - * `GetCharacter()` and `IsLower()`. - * - * @param source `string` to check for being in lower case. - * @param sourceType Type of the `string` to check; default is plain string. - * @return `true` if `string` is equal to it's own lower folding, - * (per character given by `ToLower()` method). - */ -public final function bool IsLowerString -( - string source, - optional Text.StringType sourceType -) -{ - local int i; - local array rawData; - rawData = StringToRaw(source, sourceType); - for (i = 0; i < rawData.length; i += 1) - { - if (rawData[i] != ToLower(rawData[i])) { - return false; - } - } - return true; -} - -/** - * Checks if given character is upper case. - * - * Result of this method describes whether character is - * precisely "upper case", instead of just "not being upper of title case". - * That is, this method will return `true` for characters that aren't - * considered either uppercase or uppercase (like "#", "@" or "&"). - * - * @param character Character to test for upper case. - * @return `true` if given character is upper case. - */ -public final function bool IsUpper(Text.Character character) -{ - // Capital Latin letters - if (character.codePoint >= 65 && character.codePoint <= 90) { - return true; - } - // Capital Cyrillic (Russian) letters - if (character.codePoint >= 1040 && character.codePoint <= 1071) { - return true; - } - // `Ё` - if (character.codePoint == 1025) { - return true; - } - return false; -} - -/** - * Checks if given `string` is in upper case. - * - * This function returns `true` as long as it's equal to it's own - * `ToUpperString()` folding. - * This means that it can contain symbols that neither lower or upper case, or - * lower case symbols that don't have an upper case folding. - * - * To check whether a symbol is upper cased, use a combination of - * `GetCharacter()` and `IsUpper()`. - * - * @param source `string` to check for being in upper case. - * @param sourceType Type of the `string` to check; default is plain string. - * @return `true` if `string` is equal to it's own upper folding, - * (per character given by `ToUpper()` method). - */ -public final function bool IsUpperString -( - string source, - optional Text.StringType sourceType -) -{ - local int i; - local array rawData; - rawData = StringToRaw(source, sourceType); - for (i = 0; i < rawData.length; i += 1) - { - if (rawData[i] != ToUpper(rawData[i])) { - return false; - } - } - return true; -} - -/** - * Checks if given character corresponds to a digit. - * - * @param codePoint Unicode code point to check for being a digit. - * @return `true` if given Unicode code point is a digit, `false` otherwise. - */ -public final function bool IsDigit(Text.Character character) -{ - if (character.codePoint >= 48 && character.codePoint <= 57) { - return true; - } - return false; -} - -/** - * Checks if given character is an ASCII character. - * - * @param character Character to check for being a digit. - * @return `true` if given character is a digit, `false` otherwise. - */ -public final function bool IsASCII(Text.Character character) -{ - if (character.codePoint >= 0 && character.codePoint <= 127) { - return true; - } - return false; -} - -/** - * Checks if given `string` consists only from ASCII characters - * (ignoring characters in 4-byte color change sequences in colored strings). - * - * @param source `string` to test for being ASCII-only. - * @param sourceType Type of the passed `string`. - * @return `true` if passed `string` contains only ASCII characters. - */ -public final function bool IsASCIIString -( - string source, - optional Text.StringType sourceType -) -{ - local int i; - local array rawData; - rawData = StringToRaw(source, sourceType); - for (i = 0; i < rawData.length; i += 1) - { - if (!IsASCII(rawData[i])) { - return false; - } - } - return true; -} - -/** - * Checks if given character represents some kind of white space - * symbol (like space ~ 0x0020, tab ~ 0x0009, etc.), - * according to either Unicode or a more classic space symbol definition, - * that includes: - * whitespace, tab, line feed, line tabulation, form feed, carriage return. - * - * @param character Character to check for being a whitespace. - * @return `true` if given character is a whitespace, `false` otherwise. - */ -public final function bool IsWhitespace(Text.Character character) -{ - switch (character.codePoint) - { - // Classic whitespaces - case 0x0020: // Whitespace - case 0x0009: // Tab - case 0x000A: // Line feed - case 0x000B: // Line tabulation - case 0x000C: // Form feed - case 0x000D: // Carriage return - // Unicode Characters in the 'Separator, Space' Category - case 0x00A0: // No-break space - case 0x1680: // Ogham space mark - case 0x2000: // En quad - case 0x2001: // Em quad - case 0x2002: // En space - case 0x2003: // Em space - case 0x2004: // Three-per-em space - case 0x2005: // Four-per-em space - case 0x2006: // Six-per-em space - case 0x2007: // Figure space - case 0x2008: // Punctuation space - case 0x2009: // Thin space - case 0x200A: // Hair space - case 0x202F: // Narrow no-break space - case 0x205F: // Medium mathematical space - case 0x3000: // Ideographic space - return true; - default: - return false; - } - return false; -} - -/** - * Checks if passed character is one of the following quotation mark symbols: - * `"`, `'`, `\``. - * - * @param character Character to check for being a quotation mark. - * @return `true` if given Unicode code point denotes one of the recognized - * quote symbols, `false` otherwise. - */ -public final function bool IsQuotationMark(Text.Character character) -{ - if (character.codePoint == 0x0022) return true; - if (character.codePoint == 0x0027) return true; - if (character.codePoint == 0x0060) return true; - return false; -} - -/** - * Converts given character into a number it represents in some base - * (from 2 to 36), i.e.: - * 1 -> 1 - * 7 -> 7 - * a -> 10 - * e -> 14 - * z -> 35 - * - * @param character Character to convert into integer. - * Case does not matter, i.e. "a" and "A" will be treated the same. - * @param base Base to use for conversion. - * Valid values are from `2` to `36` (inclusive); - * If invalid value was specified (such as default `0`), - * the base of `36` is assumed, since that would allow for all possible - * characters to be converted. - * @return Positive integer value that is denoted by - * given character in given base; - * `-1` if given character does not represent anything in the given base. - */ -public final function int CharacterToInt -( - Text.Character character, - optional int base -) -{ - local int number; - if (base < 2 || base > 36) { - base = 36; - } - character = ToLower(character); - // digits - if (character.codePoint >= 0x0030 && character.codePoint <= 0x0039) { - number = character.codePoint - 0x0030; - } - // a-z - else if (character.codePoint >= 0x0061 && character.codePoint <= 0x007a) { - number = character.codePoint - 0x0061 + 10; - } - else { - return -1; - } - if (number >= base) { - return -1; - } - return number; -} - -/** - * Checks if given `character` can be represented by a given `codePoint` in - * Unicode standard. - * - * @param character Character to check. - * @param codePoint Code point to check. - * @return `true` if given character can be represented by a given code point - * and `false` otherwise. - */ -public final function bool IsCodePoint(Text.Character character, int codePoint) -{ - return (character.codePoint == codePoint); -} - -/** - * Returns a particular character from a given `string`, of a given type, - * with preserved color information. - * - * @param source String, from which to fetch the character. - * @param position Which, in order, character to fetch - * (starting counting from '0'). - * By default returns first (`0`th) character. - * @param sourceType Type of the given `source` `string`. - * @return Character from a `source` at a given position `position`. - * If given position is out-of-bounds for a given `string` - * (it is either negative or at least the same as a total character count), - * - returns invalid character. - */ -public final function Text.Character GetCharacter -( - string source, - optional int position, - optional Text.StringType sourceType -) -{ - local Text.Character resultCharacter; - local array rawData; - if (position < 0) return GetInvalidCharacter(); - - // `STRING_Plain` is the only type where we do not need to do any parsing - // and get just fetch a character, so handle it separately. - if (sourceType == STRING_Plain) - { - if (position >= Len(source)) { - return GetInvalidCharacter(); - } - resultCharacter.codePoint = Asc(Mid(source, position, 1)); - return resultCharacter; - } - rawData = StringToRaw(source, sourceType); - if (position >= rawData.length) { - return GetInvalidCharacter(); - } - return rawData[position]; -} - -/** - * Returns color of a given `Character` with set default color. - * - * `Character`s can have their color set to "default", meaning they would use - * whatever considered default color in the context. - * - * @param character `Character`, which color to return. - * @param defaultColor Color, considered default. - * @return Supposed color of a given `Character`, assuming default color is - * `defaultColor`. - */ -public final function Color GetCharacterColor( - Text.Character character, - Color defaultColor) -{ - if (character.colorType == STRCOLOR_Default) { - return defaultColor; - } - return character.color; -} - -/** - * Returns character that is considered invalid. - * - * It is not unique, there can be different invalid characters. - * - * @return Invalid character instance. - */ -public final function Text.Character GetInvalidCharacter() -{ - local Text.Character result; - result.codePoint = -1; - return result; -} - -/** - * Checks if given character is invalid. - * - * @param character Character to check. - * @return `true` if passed character is valid and `false` otherwise. - */ -public final function bool IsValidCharacter(Text.Character character) -{ - return (character.codePoint >= 0); -} - -/** - * Checks if given characters are equal, with or without accounting - * for their case. - * - * @param codePoint1 Character to compare. - * @param codePoint2 Character to compare. - * @param caseInsensitive Optional parameter, - * if `false` we will require characters to be exactly the same, - * if `true` we will also consider characters equal if they - * only differ by case. - * @return `true` if given characters are considered equal, - * `false` otherwise. - */ -public final function bool AreEqual( - Text.Character character1, - Text.Character character2, - optional bool caseInsensitive -) -{ - if (character1.codePoint == character2.codePoint) return true; - if (character1.codePoint < 0 && character2.codePoint < 0) return true; - - if (caseInsensitive) - { - character1 = ToLower(character1); - character2 = ToLower(character2); - } - return (character1.codePoint == character2.codePoint); -} - -/** - * Checks if given `string`s are equal to each other, with or without - * accounting for their case. - * - * @param string1 `string` to compare. - * @param string2 `string` to compare. - * @param caseInsensitive Optional parameter, - * if `false` we will require `string`s to be exactly the same, - * if `true` we will also consider `string`s equal if their corresponding - * characters only differ by case. - * @return `true` if given `string`s are considered equal, `false` otherwise. - */ -public final function bool AreEqualStrings( - string string1, - string string2, - optional bool caseInsensitive -) -{ - local int i; - local array rawData1, rawData2; - rawData1 = StringToRaw(string1); - rawData2 = StringToRaw(string2); - if (rawData1.length != rawData2.length) return false; - - for (i = 0; i < rawData1.length; i += 1) - { - if (!AreEqual(rawData1[i], rawData2[i], caseInsensitive)) return false; - } - return true; -} - -/** - * Converts Unicode code point into it's lower case folding, - * as defined by Unicode standard. - * - * @param codePoint Code point to convert into lower case. - * @return Lower case folding of the given code point. If Unicode standard does - * not define any lower case folding (like "&" or "!") for given code point, - - * function returns given code point unchanged. - */ -public final function Text.Character ToLower(Text.Character character) -{ - local int newCodePoint; - newCodePoint = - class'UnicodeData'.static.ToLowerCodePoint(character.codePoint); - if (newCodePoint >= 0) { - character.codePoint = newCodePoint; - } - return character; -} - -/** - * Converts Unicode code point into it's upper case version, - * as defined by Unicode standard. - * - * @param codePoint Code point to convert into upper case. - * @return Upper case version of the given code point. If Unicode standard does - * not define any upper case version (like "&" or "!") for given code point, - - * function returns given code point unchanged. - */ -public final function Text.Character ToUpper(Text.Character character) -{ - local int newCodePoint; - newCodePoint = - class'UnicodeData'.static.ToUpperCodePoint(character.codePoint); - if (newCodePoint >= 0) { - character.codePoint = newCodePoint; - } - return character; -} - -/** - * Converts `string` to lower case. - * - * Changes every symbol in the `string` to their lower case folding. - * Characters without lower case folding (like "&" or "!") are left unchanged. - * - * @param source `string` that will be converted into a lower case. - * @return Lower case folding of a given `string`. - */ -public final function string ToLowerString( - string source, - optional Text.StringType sourceType -) -{ - if (sourceType == STRING_Plain) { - return ConvertCaseForString_Plain(source, LCASE_Lower); - } - if (sourceType == STRING_Formatted) { - return ConvertCaseForString_Formatted(source, LCASE_Lower); - } - return ConvertCaseForString_Colored(source, LCASE_Lower); -} - -/** - * Converts `string` to upper case. - * - * Changes every symbol in the `string` to their upper case folding. - * Characters without upper case folding (like "&" or "!") are left unchanged. - * - * @param source `string` that will be converted into an upper case. - * @return Upper case folding of a given `string`. - */ -public final function string ToUpperString( - string source, - optional Text.StringType sourceType -) -{ - if (sourceType == STRING_Plain) { - return ConvertCaseForString_Plain(source, LCASE_Upper); - } - if (sourceType == STRING_Formatted) { - return ConvertCaseForString_Formatted(source, LCASE_Upper); - } - return ConvertCaseForString_Colored(source, LCASE_Upper); -} - -private final function string ConvertCaseForString_Plain -( - string source, - Text.LetterCase targetCase -) -{ - local int i; - local array rawData; - rawData = StringToRaw(source, STRING_Plain); - for (i = 0; i < rawData.length; i += 1) - { - if (targetCase == LCASE_Lower) { - rawData[i] = ToLower(rawData[i]); - } - else { - rawData[i] = ToUpper(rawData[i]); - } - } - return RawToString(rawData, STRING_Plain); -} - -private final function string ConvertCaseForString_Colored -( - string source, - Text.LetterCase targetCase -) -{ - local int i; - local string result; - local array rawData; - rawData = StringToRaw(source, STRING_Colored); - for (i = 0; i < rawData.length; i += 1) - { - if (targetCase == LCASE_Lower) { - rawData[i] = ToLower(rawData[i]); - } - else { - rawData[i] = ToUpper(rawData[i]); - } - } - result = RawToString(rawData, STRING_Colored); - if (rawData.length > 0 && rawData[0].colorType == STRCOLOR_Default) { - result = Mid(result, 4); - } - return result; -} - -private final function string ConvertCaseForString_Formatted -( - string source, - Text.LetterCase targetCase -) -{ - // TODO: finish it later, no one needs it right now, - // no idea wtf I even bothered with these functions - return source; -} - -/** - * Returns hash for a raw `string` data. - * - * Uses djb2 algorithm, somewhat adapted to make use of formatting - * (color) information. Hopefully it did not broke horribly. - * - * @param rawData Data to calculate hash of. - * @return Hash of the given data. - */ -public final function int GetHashRaw(array rawData) { - local int i; - local int colorInt; - local int hash; - hash = 5381; - for (i = 0; i < rawData.length; i += 1) { - // hash * 33 + rawData[i].codePoint - hash = ((hash << 5) + hash) + rawData[i].codePoint; - if (rawData[i].colorType != STRCOLOR_Default) { - colorInt = rawData[i].color.r - + rawData[i].color.g * 0x00ff - + rawData[i].color.b * 0xffff; - hash = ((hash << 5) + hash) + colorInt; - } - } - return hash; -} - -/** - * Returns hash for a `string` data. - * - * Uses djb2 algorithm, somewhat adapted to make use of formatting - * (color) information. Hopefully it did not broke horribly. - * - * @param rawData `string` to calculate hash of. - * @param sourceType Type of the `string`, in case you want has to be more - * formatting-independent. Leaving default value (`STRING_Plain`) should be - * fine for almost any use case. - * @return Hash of the given data. - */ -public final function int GetHash( - string source, - optional Text.StringType sourceType) { - return GetHashRaw(StringToRaw(source, sourceType)); -} - -/** - * Creates a new, empty `Text`. - * - * This is a shortcut, same result cam be achieved by `new class'Text'`. - * - * @return Brand new, empty instance of `Text`. - */ -public final function Text Empty() -{ - local Text newText; - newText = new class'Text'; - return newText; -} - -/** - * Creates a `Text` that will contain a given `string`. Parameter made optional - * to enable easier way of creating empty `Text`. - * - * @param source `string` that will be copied into returned `Text`. - * @return New instance (not taken from the object pool) of `Text` that - * will contain passed `string`. - */ -public final function Text FromString(optional string source) -{ - local Text newText; - newText = new class'Text'; - newText.CopyString(source); - return newText; -} - -/** - * Creates a `Text` that will contain `string` with characters recorded in the - * given array. Parameter made optional to enable easier way of - * creating empty `Text`. - * - * @param rawData Sequence of characters that will be copied into - * returned `Text`. - * @return New instance (not taken from the object pool) of `Text` that - * will contain passed sequence of Unicode code points. - */ -public final function Text FromRaw(array rawData) -{ - local Text newText; - newText = new class'Text'; - newText.CopyRaw(rawData); - return newText; -} - -/** - * Method for creating a new, uninitialized parser object. - * - * Always creates a new parser. This method should be used when you plan to - * store created `Parser` and reuse later. - * To parse something once it's advised to use - * `Parse()`, `ParseString()` or `ParseRaw()` instead. - * - * It is a good practice to free created `Parser` once you don't need it. - * - * @see `Parser` - * @return Guaranteed to be new, uninitialized `Parser`. - */ -public final function Parser NewParser() -{ - return (new class'Parser'); -} - -/** - * Method for creating a new parser, initialized with contents of given `Text`. - * - * Always creates a new parser. This method should be used when you plan to - * store created `Parser` and reuse later. - * To parse something once it's advised to use `Parse()` instead. - * - * It is a good practice to free created `Parser` once you don't need it. - * - * @see `Parser` - * @param source Returned `Parser` will be setup to parse the contents of - * the passed `Text`. - * If `none` value is passed, - parser won't be initialized. - * @return Guaranteed to be new `Parser`, - * initialized with contents of `source`. - */ -public final function Parser NewParserFromText(Text source) -{ - local Parser parser; - parser = new class'Parser'; - parser.InitializeT(source); - return parser; -} - -/** - * Method for creating a new parser, initialized with a given `string`. - * - * Always creates a new parser. This method should be used when you plan to - * store created `Parser` and reuse later. - * To parse something once it's advised to use `ParseString()` instead. - * - * It is a good practice to free created `Parser` once you don't need it. - * - * @see `Parser` - * @param source Returned `Parser` will be setup to parse the `source`. - * @return Guaranteed to be new `Parser`, initialized with given `string`. - */ -public final function Parser NewParserFromString(string source) -{ - local Parser parser; - parser = new class'Parser'; - parser.Initialize(source); - return parser; -} - -/** - * Method for creating a new parser, initialized with a given sequence of - * characters. - * - * Always creates a new parser. This method should be used when you plan to - * store created `Parser` and reuse later. - * To parse something once it's advised to use `ParseRaw()` instead. - * - * It is a good practice to free created `Parser` once you don't need it. - * - * @see `Parser` - * @param source Returned `Parser` will be setup to parse passed - * characters sequence. - * @return Guaranteed to be new `Parser`, initialized with given - * characters sequence. - */ -public final function Parser NewParserFromRaw(array source) -{ - local Parser parser; - parser = new class'Parser'; - parser.InitializeRaw(source); - return parser; -} - -/** - * Returns "temporary" `Parser` that can be used for one-time parsing, - * initialized with a given sequence of characters. - * It will be automatically freed to be reused again after - * current tick ends. - * - * Returned `Parser` does not have to be a new object and - * it is possible that it is still referenced by some buggy or malicious code. - * To ensure that no problem arises: - * 1. Re-initialize returned `Parser` after executing any piece of - * code that you do not trust to misuse `Parser`s; - * 2. Do not use obtained reference after current tick ends or - * calling `FreeParser()` on it. - * For more details @see `Parser`. - * - * @param source Returned `Parser` will be setup to parse passed - * characters sequence. - * @return Temporary `Parser`, initialized with given - * characters sequence. - */ -public final function Parser ParseRaw(array source) -{ - local Parser parser; - parser = Parser(_.memory.Borrow(class'Parser')); - if (parser != none) - { - parser.InitializeRaw(source); - return parser; - } - return none; -} - -/** - * Returns "temporary" `Parser` that can be used for one-time parsing, - * initialized with contents of given `Text`. - * It will be automatically freed to be reused again after - * current tick ends. - * - * Returned `Parser` does not have to be a new object and - * it is possible that it is still referenced by some buggy or malicious code. - * To ensure that no problem arises: - * 1. Re-initialize returned `Parser` after executing any piece of - * code that you do not trust to misuse `Parser`s; - * 2. Do not use obtained reference after current tick ends or - * calling `FreeParser()` on it. - * For more details @see `Parser`. - * - * @param source Returned `Parser` will be setup to parse the contents of - * the passed `Text`. - * @return Temporary `Parser`, initialized with contents of the given `Text`. - */ -public final function Parser Parse(Text source) -{ - local Parser parser; - if (source == none) return NewParser(); - - parser = Parser(_.memory.Borrow(class'Parser')); - if (parser != none) - { - parser.InitializeT(source); - return parser; - } - return none; -} - -/** - * Returns "temporary" `Parser` that can be used for one-time parsing, - * initialized `string`. - * It will be automatically freed to be reused again after - * current tick ends. - * - * Returned `Parser` does not have to be a new object and - * it is possible that it is still referenced by some buggy or malicious code. - * To ensure that no problem arises: - * 1. Re-initialize returned `Parser` after executing any piece of - * code that you do not trust to misuse `Parser`s; - * 2. Do not use obtained reference after current tick ends or - * calling `FreeParser()` on it. - * For more details @see `Parser`. - * - * @param source Returned `Parser` will be setup to parse `source`. - * @return Temporary `Parser`, initialized with the given `string`. - */ -public final function Parser ParseString ( - string source, -optional Text.StringType sourceType) { - local Parser parser; - parser = Parser(_.memory.Borrow(class'Parser')); - if (parser != none) { - parser.Initialize(source, sourceType); - return parser; - } - return none; -} - -defaultproperties -{ - CODEPOINT_ESCAPE = 27 // ANSI escape code - CODEPOINT_OPEN_FORMAT = 123 // '{' - CODEPOINT_CLOSE_FORMAT = 125 // '}' - CODEPOINT_FORMAT_ESCAPE = 38 // '&' -} \ No newline at end of file diff --git a/sources/Core/Text/UnicodeData.uc b/sources/Core/Text/UnicodeData.uc deleted file mode 100644 index f405d5b..0000000 --- a/sources/Core/Text/UnicodeData.uc +++ /dev/null @@ -1,4320 +0,0 @@ -/** - * 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 . - */ - -// !!! THIS FILE IS AUTOGENERATED AND SHOULD NOT BE MODIFIED !!! - -class UnicodeData extends AcediaObject - abstract; - -struct CodePointMapping -{ - var public int from; - var public int to; -}; - -var public const array to_lower; -var public const array to_upper; -var public const array to_title; - -public static function int ToLowerCodePoint(int codePoint) -{ - local int midPoint; - local int lowerIndex, higherIndex; - lowerIndex = 0; - higherIndex = default.to_lower.length - 1; - while (lowerIndex < higherIndex) - { - // Border case: lower and higher indices are right next to each other - if (lowerIndex + 1 >= higherIndex) - { - if (default.to_lower[lowerIndex].from == codePoint) { - return default.to_lower[lowerIndex].to; - } - if (default.to_lower[higherIndex].from == codePoint) { - return default.to_lower[higherIndex].to; - } - return -1; - } - // Choose half - midPoint = (lowerIndex + higherIndex) / 2; - if (default.to_lower[midPoint].from == codePoint) { - return default.to_lower[midPoint].to; - } - if (default.to_lower[midPoint].from < codePoint) { - lowerIndex = midPoint; - } - else {//if (default.to_lower[midPoint].from > codePoint) - higherIndex = midPoint; - } - } - return -1; -} - -public static function int ToUpperCodePoint(int codePoint) -{ - local int midPoint; - local int lowerIndex, higherIndex; - lowerIndex = 0; - higherIndex = default.to_upper.length - 1; - while (lowerIndex < higherIndex) - { - // Border case: lower and higher indices are right next to each other - if (lowerIndex + 1 >= higherIndex) - { - if (default.to_upper[lowerIndex].from == codePoint) { - return default.to_upper[lowerIndex].to; - } - if (default.to_upper[higherIndex].from == codePoint) { - return default.to_upper[higherIndex].to; - } - return -1; - } - // Choose half - midPoint = (lowerIndex + higherIndex) / 2; - if (default.to_upper[midPoint].from == codePoint) { - return default.to_upper[midPoint].to; - } - if (default.to_upper[midPoint].from < codePoint) { - lowerIndex = midPoint; - } - else {//if (default.to_upper[midPoint].from > codePoint) - higherIndex = midPoint; - } - } - return -1; -} - -defaultproperties -{ - to_lower(0)=(from=65,to=97) - to_lower(1)=(from=66,to=98) - to_lower(2)=(from=67,to=99) - to_lower(3)=(from=68,to=100) - to_lower(4)=(from=69,to=101) - to_lower(5)=(from=70,to=102) - to_lower(6)=(from=71,to=103) - to_lower(7)=(from=72,to=104) - to_lower(8)=(from=73,to=105) - to_lower(9)=(from=74,to=106) - to_lower(10)=(from=75,to=107) - to_lower(11)=(from=76,to=108) - to_lower(12)=(from=77,to=109) - to_lower(13)=(from=78,to=110) - to_lower(14)=(from=79,to=111) - to_lower(15)=(from=80,to=112) - to_lower(16)=(from=81,to=113) - to_lower(17)=(from=82,to=114) - to_lower(18)=(from=83,to=115) - to_lower(19)=(from=84,to=116) - to_lower(20)=(from=85,to=117) - to_lower(21)=(from=86,to=118) - to_lower(22)=(from=87,to=119) - to_lower(23)=(from=88,to=120) - to_lower(24)=(from=89,to=121) - to_lower(25)=(from=90,to=122) - to_lower(26)=(from=192,to=224) - to_lower(27)=(from=193,to=225) - to_lower(28)=(from=194,to=226) - to_lower(29)=(from=195,to=227) - to_lower(30)=(from=196,to=228) - to_lower(31)=(from=197,to=229) - to_lower(32)=(from=198,to=230) - to_lower(33)=(from=199,to=231) - to_lower(34)=(from=200,to=232) - to_lower(35)=(from=201,to=233) - to_lower(36)=(from=202,to=234) - to_lower(37)=(from=203,to=235) - to_lower(38)=(from=204,to=236) - to_lower(39)=(from=205,to=237) - to_lower(40)=(from=206,to=238) - to_lower(41)=(from=207,to=239) - to_lower(42)=(from=208,to=240) - to_lower(43)=(from=209,to=241) - to_lower(44)=(from=210,to=242) - to_lower(45)=(from=211,to=243) - to_lower(46)=(from=212,to=244) - to_lower(47)=(from=213,to=245) - to_lower(48)=(from=214,to=246) - to_lower(49)=(from=216,to=248) - to_lower(50)=(from=217,to=249) - to_lower(51)=(from=218,to=250) - to_lower(52)=(from=219,to=251) - to_lower(53)=(from=220,to=252) - to_lower(54)=(from=221,to=253) - to_lower(55)=(from=222,to=254) - to_lower(56)=(from=256,to=257) - to_lower(57)=(from=258,to=259) - to_lower(58)=(from=260,to=261) - to_lower(59)=(from=262,to=263) - to_lower(60)=(from=264,to=265) - to_lower(61)=(from=266,to=267) - to_lower(62)=(from=268,to=269) - to_lower(63)=(from=270,to=271) - to_lower(64)=(from=272,to=273) - to_lower(65)=(from=274,to=275) - to_lower(66)=(from=276,to=277) - to_lower(67)=(from=278,to=279) - to_lower(68)=(from=280,to=281) - to_lower(69)=(from=282,to=283) - to_lower(70)=(from=284,to=285) - to_lower(71)=(from=286,to=287) - to_lower(72)=(from=288,to=289) - to_lower(73)=(from=290,to=291) - to_lower(74)=(from=292,to=293) - to_lower(75)=(from=294,to=295) - to_lower(76)=(from=296,to=297) - to_lower(77)=(from=298,to=299) - to_lower(78)=(from=300,to=301) - to_lower(79)=(from=302,to=303) - to_lower(80)=(from=304,to=105) - to_lower(81)=(from=306,to=307) - to_lower(82)=(from=308,to=309) - to_lower(83)=(from=310,to=311) - to_lower(84)=(from=313,to=314) - to_lower(85)=(from=315,to=316) - to_lower(86)=(from=317,to=318) - to_lower(87)=(from=319,to=320) - to_lower(88)=(from=321,to=322) - to_lower(89)=(from=323,to=324) - to_lower(90)=(from=325,to=326) - to_lower(91)=(from=327,to=328) - to_lower(92)=(from=330,to=331) - to_lower(93)=(from=332,to=333) - to_lower(94)=(from=334,to=335) - to_lower(95)=(from=336,to=337) - to_lower(96)=(from=338,to=339) - to_lower(97)=(from=340,to=341) - to_lower(98)=(from=342,to=343) - to_lower(99)=(from=344,to=345) - to_lower(100)=(from=346,to=347) - to_lower(101)=(from=348,to=349) - to_lower(102)=(from=350,to=351) - to_lower(103)=(from=352,to=353) - to_lower(104)=(from=354,to=355) - to_lower(105)=(from=356,to=357) - to_lower(106)=(from=358,to=359) - to_lower(107)=(from=360,to=361) - to_lower(108)=(from=362,to=363) - to_lower(109)=(from=364,to=365) - to_lower(110)=(from=366,to=367) - to_lower(111)=(from=368,to=369) - to_lower(112)=(from=370,to=371) - to_lower(113)=(from=372,to=373) - to_lower(114)=(from=374,to=375) - to_lower(115)=(from=376,to=255) - to_lower(116)=(from=377,to=378) - to_lower(117)=(from=379,to=380) - to_lower(118)=(from=381,to=382) - to_lower(119)=(from=385,to=595) - to_lower(120)=(from=386,to=387) - to_lower(121)=(from=388,to=389) - to_lower(122)=(from=390,to=596) - to_lower(123)=(from=391,to=392) - to_lower(124)=(from=393,to=598) - to_lower(125)=(from=394,to=599) - to_lower(126)=(from=395,to=396) - to_lower(127)=(from=398,to=477) - to_lower(128)=(from=399,to=601) - to_lower(129)=(from=400,to=603) - to_lower(130)=(from=401,to=402) - to_lower(131)=(from=403,to=608) - to_lower(132)=(from=404,to=611) - to_lower(133)=(from=406,to=617) - to_lower(134)=(from=407,to=616) - to_lower(135)=(from=408,to=409) - to_lower(136)=(from=412,to=623) - to_lower(137)=(from=413,to=626) - to_lower(138)=(from=415,to=629) - to_lower(139)=(from=416,to=417) - to_lower(140)=(from=418,to=419) - to_lower(141)=(from=420,to=421) - to_lower(142)=(from=422,to=640) - to_lower(143)=(from=423,to=424) - to_lower(144)=(from=425,to=643) - to_lower(145)=(from=428,to=429) - to_lower(146)=(from=430,to=648) - to_lower(147)=(from=431,to=432) - to_lower(148)=(from=433,to=650) - to_lower(149)=(from=434,to=651) - to_lower(150)=(from=435,to=436) - to_lower(151)=(from=437,to=438) - to_lower(152)=(from=439,to=658) - to_lower(153)=(from=440,to=441) - to_lower(154)=(from=444,to=445) - to_lower(155)=(from=452,to=454) - to_lower(156)=(from=453,to=454) - to_lower(157)=(from=455,to=457) - to_lower(158)=(from=456,to=457) - to_lower(159)=(from=458,to=460) - to_lower(160)=(from=459,to=460) - to_lower(161)=(from=461,to=462) - to_lower(162)=(from=463,to=464) - to_lower(163)=(from=465,to=466) - to_lower(164)=(from=467,to=468) - to_lower(165)=(from=469,to=470) - to_lower(166)=(from=471,to=472) - to_lower(167)=(from=473,to=474) - to_lower(168)=(from=475,to=476) - to_lower(169)=(from=478,to=479) - to_lower(170)=(from=480,to=481) - to_lower(171)=(from=482,to=483) - to_lower(172)=(from=484,to=485) - to_lower(173)=(from=486,to=487) - to_lower(174)=(from=488,to=489) - to_lower(175)=(from=490,to=491) - to_lower(176)=(from=492,to=493) - to_lower(177)=(from=494,to=495) - to_lower(178)=(from=497,to=499) - to_lower(179)=(from=498,to=499) - to_lower(180)=(from=500,to=501) - to_lower(181)=(from=502,to=405) - to_lower(182)=(from=503,to=447) - to_lower(183)=(from=504,to=505) - to_lower(184)=(from=506,to=507) - to_lower(185)=(from=508,to=509) - to_lower(186)=(from=510,to=511) - to_lower(187)=(from=512,to=513) - to_lower(188)=(from=514,to=515) - to_lower(189)=(from=516,to=517) - to_lower(190)=(from=518,to=519) - to_lower(191)=(from=520,to=521) - to_lower(192)=(from=522,to=523) - to_lower(193)=(from=524,to=525) - to_lower(194)=(from=526,to=527) - to_lower(195)=(from=528,to=529) - to_lower(196)=(from=530,to=531) - to_lower(197)=(from=532,to=533) - to_lower(198)=(from=534,to=535) - to_lower(199)=(from=536,to=537) - to_lower(200)=(from=538,to=539) - to_lower(201)=(from=540,to=541) - to_lower(202)=(from=542,to=543) - to_lower(203)=(from=544,to=414) - to_lower(204)=(from=546,to=547) - to_lower(205)=(from=548,to=549) - to_lower(206)=(from=550,to=551) - to_lower(207)=(from=552,to=553) - to_lower(208)=(from=554,to=555) - to_lower(209)=(from=556,to=557) - to_lower(210)=(from=558,to=559) - to_lower(211)=(from=560,to=561) - to_lower(212)=(from=562,to=563) - to_lower(213)=(from=570,to=11365) - to_lower(214)=(from=571,to=572) - to_lower(215)=(from=573,to=410) - to_lower(216)=(from=574,to=11366) - to_lower(217)=(from=577,to=578) - to_lower(218)=(from=579,to=384) - to_lower(219)=(from=580,to=649) - to_lower(220)=(from=581,to=652) - to_lower(221)=(from=582,to=583) - to_lower(222)=(from=584,to=585) - to_lower(223)=(from=586,to=587) - to_lower(224)=(from=588,to=589) - to_lower(225)=(from=590,to=591) - to_lower(226)=(from=880,to=881) - to_lower(227)=(from=882,to=883) - to_lower(228)=(from=886,to=887) - to_lower(229)=(from=895,to=1011) - to_lower(230)=(from=902,to=940) - to_lower(231)=(from=904,to=941) - to_lower(232)=(from=905,to=942) - to_lower(233)=(from=906,to=943) - to_lower(234)=(from=908,to=972) - to_lower(235)=(from=910,to=973) - to_lower(236)=(from=911,to=974) - to_lower(237)=(from=913,to=945) - to_lower(238)=(from=914,to=946) - to_lower(239)=(from=915,to=947) - to_lower(240)=(from=916,to=948) - to_lower(241)=(from=917,to=949) - to_lower(242)=(from=918,to=950) - to_lower(243)=(from=919,to=951) - to_lower(244)=(from=920,to=952) - to_lower(245)=(from=921,to=953) - to_lower(246)=(from=922,to=954) - to_lower(247)=(from=923,to=955) - to_lower(248)=(from=924,to=956) - to_lower(249)=(from=925,to=957) - to_lower(250)=(from=926,to=958) - to_lower(251)=(from=927,to=959) - to_lower(252)=(from=928,to=960) - to_lower(253)=(from=929,to=961) - to_lower(254)=(from=931,to=963) - to_lower(255)=(from=932,to=964) - to_lower(256)=(from=933,to=965) - to_lower(257)=(from=934,to=966) - to_lower(258)=(from=935,to=967) - to_lower(259)=(from=936,to=968) - to_lower(260)=(from=937,to=969) - to_lower(261)=(from=938,to=970) - to_lower(262)=(from=939,to=971) - to_lower(263)=(from=975,to=983) - to_lower(264)=(from=984,to=985) - to_lower(265)=(from=986,to=987) - to_lower(266)=(from=988,to=989) - to_lower(267)=(from=990,to=991) - to_lower(268)=(from=992,to=993) - to_lower(269)=(from=994,to=995) - to_lower(270)=(from=996,to=997) - to_lower(271)=(from=998,to=999) - to_lower(272)=(from=1000,to=1001) - to_lower(273)=(from=1002,to=1003) - to_lower(274)=(from=1004,to=1005) - to_lower(275)=(from=1006,to=1007) - to_lower(276)=(from=1012,to=952) - to_lower(277)=(from=1015,to=1016) - to_lower(278)=(from=1017,to=1010) - to_lower(279)=(from=1018,to=1019) - to_lower(280)=(from=1021,to=891) - to_lower(281)=(from=1022,to=892) - to_lower(282)=(from=1023,to=893) - to_lower(283)=(from=1024,to=1104) - to_lower(284)=(from=1025,to=1105) - to_lower(285)=(from=1026,to=1106) - to_lower(286)=(from=1027,to=1107) - to_lower(287)=(from=1028,to=1108) - to_lower(288)=(from=1029,to=1109) - to_lower(289)=(from=1030,to=1110) - to_lower(290)=(from=1031,to=1111) - to_lower(291)=(from=1032,to=1112) - to_lower(292)=(from=1033,to=1113) - to_lower(293)=(from=1034,to=1114) - to_lower(294)=(from=1035,to=1115) - to_lower(295)=(from=1036,to=1116) - to_lower(296)=(from=1037,to=1117) - to_lower(297)=(from=1038,to=1118) - to_lower(298)=(from=1039,to=1119) - to_lower(299)=(from=1040,to=1072) - to_lower(300)=(from=1041,to=1073) - to_lower(301)=(from=1042,to=1074) - to_lower(302)=(from=1043,to=1075) - to_lower(303)=(from=1044,to=1076) - to_lower(304)=(from=1045,to=1077) - to_lower(305)=(from=1046,to=1078) - to_lower(306)=(from=1047,to=1079) - to_lower(307)=(from=1048,to=1080) - to_lower(308)=(from=1049,to=1081) - to_lower(309)=(from=1050,to=1082) - to_lower(310)=(from=1051,to=1083) - to_lower(311)=(from=1052,to=1084) - to_lower(312)=(from=1053,to=1085) - to_lower(313)=(from=1054,to=1086) - to_lower(314)=(from=1055,to=1087) - to_lower(315)=(from=1056,to=1088) - to_lower(316)=(from=1057,to=1089) - to_lower(317)=(from=1058,to=1090) - to_lower(318)=(from=1059,to=1091) - to_lower(319)=(from=1060,to=1092) - to_lower(320)=(from=1061,to=1093) - to_lower(321)=(from=1062,to=1094) - to_lower(322)=(from=1063,to=1095) - to_lower(323)=(from=1064,to=1096) - to_lower(324)=(from=1065,to=1097) - to_lower(325)=(from=1066,to=1098) - to_lower(326)=(from=1067,to=1099) - to_lower(327)=(from=1068,to=1100) - to_lower(328)=(from=1069,to=1101) - to_lower(329)=(from=1070,to=1102) - to_lower(330)=(from=1071,to=1103) - to_lower(331)=(from=1120,to=1121) - to_lower(332)=(from=1122,to=1123) - to_lower(333)=(from=1124,to=1125) - to_lower(334)=(from=1126,to=1127) - to_lower(335)=(from=1128,to=1129) - to_lower(336)=(from=1130,to=1131) - to_lower(337)=(from=1132,to=1133) - to_lower(338)=(from=1134,to=1135) - to_lower(339)=(from=1136,to=1137) - to_lower(340)=(from=1138,to=1139) - to_lower(341)=(from=1140,to=1141) - to_lower(342)=(from=1142,to=1143) - to_lower(343)=(from=1144,to=1145) - to_lower(344)=(from=1146,to=1147) - to_lower(345)=(from=1148,to=1149) - to_lower(346)=(from=1150,to=1151) - to_lower(347)=(from=1152,to=1153) - to_lower(348)=(from=1162,to=1163) - to_lower(349)=(from=1164,to=1165) - to_lower(350)=(from=1166,to=1167) - to_lower(351)=(from=1168,to=1169) - to_lower(352)=(from=1170,to=1171) - to_lower(353)=(from=1172,to=1173) - to_lower(354)=(from=1174,to=1175) - to_lower(355)=(from=1176,to=1177) - to_lower(356)=(from=1178,to=1179) - to_lower(357)=(from=1180,to=1181) - to_lower(358)=(from=1182,to=1183) - to_lower(359)=(from=1184,to=1185) - to_lower(360)=(from=1186,to=1187) - to_lower(361)=(from=1188,to=1189) - to_lower(362)=(from=1190,to=1191) - to_lower(363)=(from=1192,to=1193) - to_lower(364)=(from=1194,to=1195) - to_lower(365)=(from=1196,to=1197) - to_lower(366)=(from=1198,to=1199) - to_lower(367)=(from=1200,to=1201) - to_lower(368)=(from=1202,to=1203) - to_lower(369)=(from=1204,to=1205) - to_lower(370)=(from=1206,to=1207) - to_lower(371)=(from=1208,to=1209) - to_lower(372)=(from=1210,to=1211) - to_lower(373)=(from=1212,to=1213) - to_lower(374)=(from=1214,to=1215) - to_lower(375)=(from=1216,to=1231) - to_lower(376)=(from=1217,to=1218) - to_lower(377)=(from=1219,to=1220) - to_lower(378)=(from=1221,to=1222) - to_lower(379)=(from=1223,to=1224) - to_lower(380)=(from=1225,to=1226) - to_lower(381)=(from=1227,to=1228) - to_lower(382)=(from=1229,to=1230) - to_lower(383)=(from=1232,to=1233) - to_lower(384)=(from=1234,to=1235) - to_lower(385)=(from=1236,to=1237) - to_lower(386)=(from=1238,to=1239) - to_lower(387)=(from=1240,to=1241) - to_lower(388)=(from=1242,to=1243) - to_lower(389)=(from=1244,to=1245) - to_lower(390)=(from=1246,to=1247) - to_lower(391)=(from=1248,to=1249) - to_lower(392)=(from=1250,to=1251) - to_lower(393)=(from=1252,to=1253) - to_lower(394)=(from=1254,to=1255) - to_lower(395)=(from=1256,to=1257) - to_lower(396)=(from=1258,to=1259) - to_lower(397)=(from=1260,to=1261) - to_lower(398)=(from=1262,to=1263) - to_lower(399)=(from=1264,to=1265) - to_lower(400)=(from=1266,to=1267) - to_lower(401)=(from=1268,to=1269) - to_lower(402)=(from=1270,to=1271) - to_lower(403)=(from=1272,to=1273) - to_lower(404)=(from=1274,to=1275) - to_lower(405)=(from=1276,to=1277) - to_lower(406)=(from=1278,to=1279) - to_lower(407)=(from=1280,to=1281) - to_lower(408)=(from=1282,to=1283) - to_lower(409)=(from=1284,to=1285) - to_lower(410)=(from=1286,to=1287) - to_lower(411)=(from=1288,to=1289) - to_lower(412)=(from=1290,to=1291) - to_lower(413)=(from=1292,to=1293) - to_lower(414)=(from=1294,to=1295) - to_lower(415)=(from=1296,to=1297) - to_lower(416)=(from=1298,to=1299) - to_lower(417)=(from=1300,to=1301) - to_lower(418)=(from=1302,to=1303) - to_lower(419)=(from=1304,to=1305) - to_lower(420)=(from=1306,to=1307) - to_lower(421)=(from=1308,to=1309) - to_lower(422)=(from=1310,to=1311) - to_lower(423)=(from=1312,to=1313) - to_lower(424)=(from=1314,to=1315) - to_lower(425)=(from=1316,to=1317) - to_lower(426)=(from=1318,to=1319) - to_lower(427)=(from=1320,to=1321) - to_lower(428)=(from=1322,to=1323) - to_lower(429)=(from=1324,to=1325) - to_lower(430)=(from=1326,to=1327) - to_lower(431)=(from=1329,to=1377) - to_lower(432)=(from=1330,to=1378) - to_lower(433)=(from=1331,to=1379) - to_lower(434)=(from=1332,to=1380) - to_lower(435)=(from=1333,to=1381) - to_lower(436)=(from=1334,to=1382) - to_lower(437)=(from=1335,to=1383) - to_lower(438)=(from=1336,to=1384) - to_lower(439)=(from=1337,to=1385) - to_lower(440)=(from=1338,to=1386) - to_lower(441)=(from=1339,to=1387) - to_lower(442)=(from=1340,to=1388) - to_lower(443)=(from=1341,to=1389) - to_lower(444)=(from=1342,to=1390) - to_lower(445)=(from=1343,to=1391) - to_lower(446)=(from=1344,to=1392) - to_lower(447)=(from=1345,to=1393) - to_lower(448)=(from=1346,to=1394) - to_lower(449)=(from=1347,to=1395) - to_lower(450)=(from=1348,to=1396) - to_lower(451)=(from=1349,to=1397) - to_lower(452)=(from=1350,to=1398) - to_lower(453)=(from=1351,to=1399) - to_lower(454)=(from=1352,to=1400) - to_lower(455)=(from=1353,to=1401) - to_lower(456)=(from=1354,to=1402) - to_lower(457)=(from=1355,to=1403) - to_lower(458)=(from=1356,to=1404) - to_lower(459)=(from=1357,to=1405) - to_lower(460)=(from=1358,to=1406) - to_lower(461)=(from=1359,to=1407) - to_lower(462)=(from=1360,to=1408) - to_lower(463)=(from=1361,to=1409) - to_lower(464)=(from=1362,to=1410) - to_lower(465)=(from=1363,to=1411) - to_lower(466)=(from=1364,to=1412) - to_lower(467)=(from=1365,to=1413) - to_lower(468)=(from=1366,to=1414) - to_lower(469)=(from=4256,to=11520) - to_lower(470)=(from=4257,to=11521) - to_lower(471)=(from=4258,to=11522) - to_lower(472)=(from=4259,to=11523) - to_lower(473)=(from=4260,to=11524) - to_lower(474)=(from=4261,to=11525) - to_lower(475)=(from=4262,to=11526) - to_lower(476)=(from=4263,to=11527) - to_lower(477)=(from=4264,to=11528) - to_lower(478)=(from=4265,to=11529) - to_lower(479)=(from=4266,to=11530) - to_lower(480)=(from=4267,to=11531) - to_lower(481)=(from=4268,to=11532) - to_lower(482)=(from=4269,to=11533) - to_lower(483)=(from=4270,to=11534) - to_lower(484)=(from=4271,to=11535) - to_lower(485)=(from=4272,to=11536) - to_lower(486)=(from=4273,to=11537) - to_lower(487)=(from=4274,to=11538) - to_lower(488)=(from=4275,to=11539) - to_lower(489)=(from=4276,to=11540) - to_lower(490)=(from=4277,to=11541) - to_lower(491)=(from=4278,to=11542) - to_lower(492)=(from=4279,to=11543) - to_lower(493)=(from=4280,to=11544) - to_lower(494)=(from=4281,to=11545) - to_lower(495)=(from=4282,to=11546) - to_lower(496)=(from=4283,to=11547) - to_lower(497)=(from=4284,to=11548) - to_lower(498)=(from=4285,to=11549) - to_lower(499)=(from=4286,to=11550) - to_lower(500)=(from=4287,to=11551) - to_lower(501)=(from=4288,to=11552) - to_lower(502)=(from=4289,to=11553) - to_lower(503)=(from=4290,to=11554) - to_lower(504)=(from=4291,to=11555) - to_lower(505)=(from=4292,to=11556) - to_lower(506)=(from=4293,to=11557) - to_lower(507)=(from=4295,to=11559) - to_lower(508)=(from=4301,to=11565) - to_lower(509)=(from=5024,to=43888) - to_lower(510)=(from=5025,to=43889) - to_lower(511)=(from=5026,to=43890) - to_lower(512)=(from=5027,to=43891) - to_lower(513)=(from=5028,to=43892) - to_lower(514)=(from=5029,to=43893) - to_lower(515)=(from=5030,to=43894) - to_lower(516)=(from=5031,to=43895) - to_lower(517)=(from=5032,to=43896) - to_lower(518)=(from=5033,to=43897) - to_lower(519)=(from=5034,to=43898) - to_lower(520)=(from=5035,to=43899) - to_lower(521)=(from=5036,to=43900) - to_lower(522)=(from=5037,to=43901) - to_lower(523)=(from=5038,to=43902) - to_lower(524)=(from=5039,to=43903) - to_lower(525)=(from=5040,to=43904) - to_lower(526)=(from=5041,to=43905) - to_lower(527)=(from=5042,to=43906) - to_lower(528)=(from=5043,to=43907) - to_lower(529)=(from=5044,to=43908) - to_lower(530)=(from=5045,to=43909) - to_lower(531)=(from=5046,to=43910) - to_lower(532)=(from=5047,to=43911) - to_lower(533)=(from=5048,to=43912) - to_lower(534)=(from=5049,to=43913) - to_lower(535)=(from=5050,to=43914) - to_lower(536)=(from=5051,to=43915) - to_lower(537)=(from=5052,to=43916) - to_lower(538)=(from=5053,to=43917) - to_lower(539)=(from=5054,to=43918) - to_lower(540)=(from=5055,to=43919) - to_lower(541)=(from=5056,to=43920) - to_lower(542)=(from=5057,to=43921) - to_lower(543)=(from=5058,to=43922) - to_lower(544)=(from=5059,to=43923) - to_lower(545)=(from=5060,to=43924) - to_lower(546)=(from=5061,to=43925) - to_lower(547)=(from=5062,to=43926) - to_lower(548)=(from=5063,to=43927) - to_lower(549)=(from=5064,to=43928) - to_lower(550)=(from=5065,to=43929) - to_lower(551)=(from=5066,to=43930) - to_lower(552)=(from=5067,to=43931) - to_lower(553)=(from=5068,to=43932) - to_lower(554)=(from=5069,to=43933) - to_lower(555)=(from=5070,to=43934) - to_lower(556)=(from=5071,to=43935) - to_lower(557)=(from=5072,to=43936) - to_lower(558)=(from=5073,to=43937) - to_lower(559)=(from=5074,to=43938) - to_lower(560)=(from=5075,to=43939) - to_lower(561)=(from=5076,to=43940) - to_lower(562)=(from=5077,to=43941) - to_lower(563)=(from=5078,to=43942) - to_lower(564)=(from=5079,to=43943) - to_lower(565)=(from=5080,to=43944) - to_lower(566)=(from=5081,to=43945) - to_lower(567)=(from=5082,to=43946) - to_lower(568)=(from=5083,to=43947) - to_lower(569)=(from=5084,to=43948) - to_lower(570)=(from=5085,to=43949) - to_lower(571)=(from=5086,to=43950) - to_lower(572)=(from=5087,to=43951) - to_lower(573)=(from=5088,to=43952) - to_lower(574)=(from=5089,to=43953) - to_lower(575)=(from=5090,to=43954) - to_lower(576)=(from=5091,to=43955) - to_lower(577)=(from=5092,to=43956) - to_lower(578)=(from=5093,to=43957) - to_lower(579)=(from=5094,to=43958) - to_lower(580)=(from=5095,to=43959) - to_lower(581)=(from=5096,to=43960) - to_lower(582)=(from=5097,to=43961) - to_lower(583)=(from=5098,to=43962) - to_lower(584)=(from=5099,to=43963) - to_lower(585)=(from=5100,to=43964) - to_lower(586)=(from=5101,to=43965) - to_lower(587)=(from=5102,to=43966) - to_lower(588)=(from=5103,to=43967) - to_lower(589)=(from=5104,to=5112) - to_lower(590)=(from=5105,to=5113) - to_lower(591)=(from=5106,to=5114) - to_lower(592)=(from=5107,to=5115) - to_lower(593)=(from=5108,to=5116) - to_lower(594)=(from=5109,to=5117) - to_lower(595)=(from=7312,to=4304) - to_lower(596)=(from=7313,to=4305) - to_lower(597)=(from=7314,to=4306) - to_lower(598)=(from=7315,to=4307) - to_lower(599)=(from=7316,to=4308) - to_lower(600)=(from=7317,to=4309) - to_lower(601)=(from=7318,to=4310) - to_lower(602)=(from=7319,to=4311) - to_lower(603)=(from=7320,to=4312) - to_lower(604)=(from=7321,to=4313) - to_lower(605)=(from=7322,to=4314) - to_lower(606)=(from=7323,to=4315) - to_lower(607)=(from=7324,to=4316) - to_lower(608)=(from=7325,to=4317) - to_lower(609)=(from=7326,to=4318) - to_lower(610)=(from=7327,to=4319) - to_lower(611)=(from=7328,to=4320) - to_lower(612)=(from=7329,to=4321) - to_lower(613)=(from=7330,to=4322) - to_lower(614)=(from=7331,to=4323) - to_lower(615)=(from=7332,to=4324) - to_lower(616)=(from=7333,to=4325) - to_lower(617)=(from=7334,to=4326) - to_lower(618)=(from=7335,to=4327) - to_lower(619)=(from=7336,to=4328) - to_lower(620)=(from=7337,to=4329) - to_lower(621)=(from=7338,to=4330) - to_lower(622)=(from=7339,to=4331) - to_lower(623)=(from=7340,to=4332) - to_lower(624)=(from=7341,to=4333) - to_lower(625)=(from=7342,to=4334) - to_lower(626)=(from=7343,to=4335) - to_lower(627)=(from=7344,to=4336) - to_lower(628)=(from=7345,to=4337) - to_lower(629)=(from=7346,to=4338) - to_lower(630)=(from=7347,to=4339) - to_lower(631)=(from=7348,to=4340) - to_lower(632)=(from=7349,to=4341) - to_lower(633)=(from=7350,to=4342) - to_lower(634)=(from=7351,to=4343) - to_lower(635)=(from=7352,to=4344) - to_lower(636)=(from=7353,to=4345) - to_lower(637)=(from=7354,to=4346) - to_lower(638)=(from=7357,to=4349) - to_lower(639)=(from=7358,to=4350) - to_lower(640)=(from=7359,to=4351) - to_lower(641)=(from=7680,to=7681) - to_lower(642)=(from=7682,to=7683) - to_lower(643)=(from=7684,to=7685) - to_lower(644)=(from=7686,to=7687) - to_lower(645)=(from=7688,to=7689) - to_lower(646)=(from=7690,to=7691) - to_lower(647)=(from=7692,to=7693) - to_lower(648)=(from=7694,to=7695) - to_lower(649)=(from=7696,to=7697) - to_lower(650)=(from=7698,to=7699) - to_lower(651)=(from=7700,to=7701) - to_lower(652)=(from=7702,to=7703) - to_lower(653)=(from=7704,to=7705) - to_lower(654)=(from=7706,to=7707) - to_lower(655)=(from=7708,to=7709) - to_lower(656)=(from=7710,to=7711) - to_lower(657)=(from=7712,to=7713) - to_lower(658)=(from=7714,to=7715) - to_lower(659)=(from=7716,to=7717) - to_lower(660)=(from=7718,to=7719) - to_lower(661)=(from=7720,to=7721) - to_lower(662)=(from=7722,to=7723) - to_lower(663)=(from=7724,to=7725) - to_lower(664)=(from=7726,to=7727) - to_lower(665)=(from=7728,to=7729) - to_lower(666)=(from=7730,to=7731) - to_lower(667)=(from=7732,to=7733) - to_lower(668)=(from=7734,to=7735) - to_lower(669)=(from=7736,to=7737) - to_lower(670)=(from=7738,to=7739) - to_lower(671)=(from=7740,to=7741) - to_lower(672)=(from=7742,to=7743) - to_lower(673)=(from=7744,to=7745) - to_lower(674)=(from=7746,to=7747) - to_lower(675)=(from=7748,to=7749) - to_lower(676)=(from=7750,to=7751) - to_lower(677)=(from=7752,to=7753) - to_lower(678)=(from=7754,to=7755) - to_lower(679)=(from=7756,to=7757) - to_lower(680)=(from=7758,to=7759) - to_lower(681)=(from=7760,to=7761) - to_lower(682)=(from=7762,to=7763) - to_lower(683)=(from=7764,to=7765) - to_lower(684)=(from=7766,to=7767) - to_lower(685)=(from=7768,to=7769) - to_lower(686)=(from=7770,to=7771) - to_lower(687)=(from=7772,to=7773) - to_lower(688)=(from=7774,to=7775) - to_lower(689)=(from=7776,to=7777) - to_lower(690)=(from=7778,to=7779) - to_lower(691)=(from=7780,to=7781) - to_lower(692)=(from=7782,to=7783) - to_lower(693)=(from=7784,to=7785) - to_lower(694)=(from=7786,to=7787) - to_lower(695)=(from=7788,to=7789) - to_lower(696)=(from=7790,to=7791) - to_lower(697)=(from=7792,to=7793) - to_lower(698)=(from=7794,to=7795) - to_lower(699)=(from=7796,to=7797) - to_lower(700)=(from=7798,to=7799) - to_lower(701)=(from=7800,to=7801) - to_lower(702)=(from=7802,to=7803) - to_lower(703)=(from=7804,to=7805) - to_lower(704)=(from=7806,to=7807) - to_lower(705)=(from=7808,to=7809) - to_lower(706)=(from=7810,to=7811) - to_lower(707)=(from=7812,to=7813) - to_lower(708)=(from=7814,to=7815) - to_lower(709)=(from=7816,to=7817) - to_lower(710)=(from=7818,to=7819) - to_lower(711)=(from=7820,to=7821) - to_lower(712)=(from=7822,to=7823) - to_lower(713)=(from=7824,to=7825) - to_lower(714)=(from=7826,to=7827) - to_lower(715)=(from=7828,to=7829) - to_lower(716)=(from=7838,to=223) - to_lower(717)=(from=7840,to=7841) - to_lower(718)=(from=7842,to=7843) - to_lower(719)=(from=7844,to=7845) - to_lower(720)=(from=7846,to=7847) - to_lower(721)=(from=7848,to=7849) - to_lower(722)=(from=7850,to=7851) - to_lower(723)=(from=7852,to=7853) - to_lower(724)=(from=7854,to=7855) - to_lower(725)=(from=7856,to=7857) - to_lower(726)=(from=7858,to=7859) - to_lower(727)=(from=7860,to=7861) - to_lower(728)=(from=7862,to=7863) - to_lower(729)=(from=7864,to=7865) - to_lower(730)=(from=7866,to=7867) - to_lower(731)=(from=7868,to=7869) - to_lower(732)=(from=7870,to=7871) - to_lower(733)=(from=7872,to=7873) - to_lower(734)=(from=7874,to=7875) - to_lower(735)=(from=7876,to=7877) - to_lower(736)=(from=7878,to=7879) - to_lower(737)=(from=7880,to=7881) - to_lower(738)=(from=7882,to=7883) - to_lower(739)=(from=7884,to=7885) - to_lower(740)=(from=7886,to=7887) - to_lower(741)=(from=7888,to=7889) - to_lower(742)=(from=7890,to=7891) - to_lower(743)=(from=7892,to=7893) - to_lower(744)=(from=7894,to=7895) - to_lower(745)=(from=7896,to=7897) - to_lower(746)=(from=7898,to=7899) - to_lower(747)=(from=7900,to=7901) - to_lower(748)=(from=7902,to=7903) - to_lower(749)=(from=7904,to=7905) - to_lower(750)=(from=7906,to=7907) - to_lower(751)=(from=7908,to=7909) - to_lower(752)=(from=7910,to=7911) - to_lower(753)=(from=7912,to=7913) - to_lower(754)=(from=7914,to=7915) - to_lower(755)=(from=7916,to=7917) - to_lower(756)=(from=7918,to=7919) - to_lower(757)=(from=7920,to=7921) - to_lower(758)=(from=7922,to=7923) - to_lower(759)=(from=7924,to=7925) - to_lower(760)=(from=7926,to=7927) - to_lower(761)=(from=7928,to=7929) - to_lower(762)=(from=7930,to=7931) - to_lower(763)=(from=7932,to=7933) - to_lower(764)=(from=7934,to=7935) - to_lower(765)=(from=7944,to=7936) - to_lower(766)=(from=7945,to=7937) - to_lower(767)=(from=7946,to=7938) - to_lower(768)=(from=7947,to=7939) - to_lower(769)=(from=7948,to=7940) - to_lower(770)=(from=7949,to=7941) - to_lower(771)=(from=7950,to=7942) - to_lower(772)=(from=7951,to=7943) - to_lower(773)=(from=7960,to=7952) - to_lower(774)=(from=7961,to=7953) - to_lower(775)=(from=7962,to=7954) - to_lower(776)=(from=7963,to=7955) - to_lower(777)=(from=7964,to=7956) - to_lower(778)=(from=7965,to=7957) - to_lower(779)=(from=7976,to=7968) - to_lower(780)=(from=7977,to=7969) - to_lower(781)=(from=7978,to=7970) - to_lower(782)=(from=7979,to=7971) - to_lower(783)=(from=7980,to=7972) - to_lower(784)=(from=7981,to=7973) - to_lower(785)=(from=7982,to=7974) - to_lower(786)=(from=7983,to=7975) - to_lower(787)=(from=7992,to=7984) - to_lower(788)=(from=7993,to=7985) - to_lower(789)=(from=7994,to=7986) - to_lower(790)=(from=7995,to=7987) - to_lower(791)=(from=7996,to=7988) - to_lower(792)=(from=7997,to=7989) - to_lower(793)=(from=7998,to=7990) - to_lower(794)=(from=7999,to=7991) - to_lower(795)=(from=8008,to=8000) - to_lower(796)=(from=8009,to=8001) - to_lower(797)=(from=8010,to=8002) - to_lower(798)=(from=8011,to=8003) - to_lower(799)=(from=8012,to=8004) - to_lower(800)=(from=8013,to=8005) - to_lower(801)=(from=8025,to=8017) - to_lower(802)=(from=8027,to=8019) - to_lower(803)=(from=8029,to=8021) - to_lower(804)=(from=8031,to=8023) - to_lower(805)=(from=8040,to=8032) - to_lower(806)=(from=8041,to=8033) - to_lower(807)=(from=8042,to=8034) - to_lower(808)=(from=8043,to=8035) - to_lower(809)=(from=8044,to=8036) - to_lower(810)=(from=8045,to=8037) - to_lower(811)=(from=8046,to=8038) - to_lower(812)=(from=8047,to=8039) - to_lower(813)=(from=8072,to=8064) - to_lower(814)=(from=8073,to=8065) - to_lower(815)=(from=8074,to=8066) - to_lower(816)=(from=8075,to=8067) - to_lower(817)=(from=8076,to=8068) - to_lower(818)=(from=8077,to=8069) - to_lower(819)=(from=8078,to=8070) - to_lower(820)=(from=8079,to=8071) - to_lower(821)=(from=8088,to=8080) - to_lower(822)=(from=8089,to=8081) - to_lower(823)=(from=8090,to=8082) - to_lower(824)=(from=8091,to=8083) - to_lower(825)=(from=8092,to=8084) - to_lower(826)=(from=8093,to=8085) - to_lower(827)=(from=8094,to=8086) - to_lower(828)=(from=8095,to=8087) - to_lower(829)=(from=8104,to=8096) - to_lower(830)=(from=8105,to=8097) - to_lower(831)=(from=8106,to=8098) - to_lower(832)=(from=8107,to=8099) - to_lower(833)=(from=8108,to=8100) - to_lower(834)=(from=8109,to=8101) - to_lower(835)=(from=8110,to=8102) - to_lower(836)=(from=8111,to=8103) - to_lower(837)=(from=8120,to=8112) - to_lower(838)=(from=8121,to=8113) - to_lower(839)=(from=8122,to=8048) - to_lower(840)=(from=8123,to=8049) - to_lower(841)=(from=8124,to=8115) - to_lower(842)=(from=8136,to=8050) - to_lower(843)=(from=8137,to=8051) - to_lower(844)=(from=8138,to=8052) - to_lower(845)=(from=8139,to=8053) - to_lower(846)=(from=8140,to=8131) - to_lower(847)=(from=8152,to=8144) - to_lower(848)=(from=8153,to=8145) - to_lower(849)=(from=8154,to=8054) - to_lower(850)=(from=8155,to=8055) - to_lower(851)=(from=8168,to=8160) - to_lower(852)=(from=8169,to=8161) - to_lower(853)=(from=8170,to=8058) - to_lower(854)=(from=8171,to=8059) - to_lower(855)=(from=8172,to=8165) - to_lower(856)=(from=8184,to=8056) - to_lower(857)=(from=8185,to=8057) - to_lower(858)=(from=8186,to=8060) - to_lower(859)=(from=8187,to=8061) - to_lower(860)=(from=8188,to=8179) - to_lower(861)=(from=8486,to=969) - to_lower(862)=(from=8490,to=107) - to_lower(863)=(from=8491,to=229) - to_lower(864)=(from=8498,to=8526) - to_lower(865)=(from=8544,to=8560) - to_lower(866)=(from=8545,to=8561) - to_lower(867)=(from=8546,to=8562) - to_lower(868)=(from=8547,to=8563) - to_lower(869)=(from=8548,to=8564) - to_lower(870)=(from=8549,to=8565) - to_lower(871)=(from=8550,to=8566) - to_lower(872)=(from=8551,to=8567) - to_lower(873)=(from=8552,to=8568) - to_lower(874)=(from=8553,to=8569) - to_lower(875)=(from=8554,to=8570) - to_lower(876)=(from=8555,to=8571) - to_lower(877)=(from=8556,to=8572) - to_lower(878)=(from=8557,to=8573) - to_lower(879)=(from=8558,to=8574) - to_lower(880)=(from=8559,to=8575) - to_lower(881)=(from=8579,to=8580) - to_lower(882)=(from=9398,to=9424) - to_lower(883)=(from=9399,to=9425) - to_lower(884)=(from=9400,to=9426) - to_lower(885)=(from=9401,to=9427) - to_lower(886)=(from=9402,to=9428) - to_lower(887)=(from=9403,to=9429) - to_lower(888)=(from=9404,to=9430) - to_lower(889)=(from=9405,to=9431) - to_lower(890)=(from=9406,to=9432) - to_lower(891)=(from=9407,to=9433) - to_lower(892)=(from=9408,to=9434) - to_lower(893)=(from=9409,to=9435) - to_lower(894)=(from=9410,to=9436) - to_lower(895)=(from=9411,to=9437) - to_lower(896)=(from=9412,to=9438) - to_lower(897)=(from=9413,to=9439) - to_lower(898)=(from=9414,to=9440) - to_lower(899)=(from=9415,to=9441) - to_lower(900)=(from=9416,to=9442) - to_lower(901)=(from=9417,to=9443) - to_lower(902)=(from=9418,to=9444) - to_lower(903)=(from=9419,to=9445) - to_lower(904)=(from=9420,to=9446) - to_lower(905)=(from=9421,to=9447) - to_lower(906)=(from=9422,to=9448) - to_lower(907)=(from=9423,to=9449) - to_lower(908)=(from=11264,to=11312) - to_lower(909)=(from=11265,to=11313) - to_lower(910)=(from=11266,to=11314) - to_lower(911)=(from=11267,to=11315) - to_lower(912)=(from=11268,to=11316) - to_lower(913)=(from=11269,to=11317) - to_lower(914)=(from=11270,to=11318) - to_lower(915)=(from=11271,to=11319) - to_lower(916)=(from=11272,to=11320) - to_lower(917)=(from=11273,to=11321) - to_lower(918)=(from=11274,to=11322) - to_lower(919)=(from=11275,to=11323) - to_lower(920)=(from=11276,to=11324) - to_lower(921)=(from=11277,to=11325) - to_lower(922)=(from=11278,to=11326) - to_lower(923)=(from=11279,to=11327) - to_lower(924)=(from=11280,to=11328) - to_lower(925)=(from=11281,to=11329) - to_lower(926)=(from=11282,to=11330) - to_lower(927)=(from=11283,to=11331) - to_lower(928)=(from=11284,to=11332) - to_lower(929)=(from=11285,to=11333) - to_lower(930)=(from=11286,to=11334) - to_lower(931)=(from=11287,to=11335) - to_lower(932)=(from=11288,to=11336) - to_lower(933)=(from=11289,to=11337) - to_lower(934)=(from=11290,to=11338) - to_lower(935)=(from=11291,to=11339) - to_lower(936)=(from=11292,to=11340) - to_lower(937)=(from=11293,to=11341) - to_lower(938)=(from=11294,to=11342) - to_lower(939)=(from=11295,to=11343) - to_lower(940)=(from=11296,to=11344) - to_lower(941)=(from=11297,to=11345) - to_lower(942)=(from=11298,to=11346) - to_lower(943)=(from=11299,to=11347) - to_lower(944)=(from=11300,to=11348) - to_lower(945)=(from=11301,to=11349) - to_lower(946)=(from=11302,to=11350) - to_lower(947)=(from=11303,to=11351) - to_lower(948)=(from=11304,to=11352) - to_lower(949)=(from=11305,to=11353) - to_lower(950)=(from=11306,to=11354) - to_lower(951)=(from=11307,to=11355) - to_lower(952)=(from=11308,to=11356) - to_lower(953)=(from=11309,to=11357) - to_lower(954)=(from=11310,to=11358) - to_lower(955)=(from=11360,to=11361) - to_lower(956)=(from=11362,to=619) - to_lower(957)=(from=11363,to=7549) - to_lower(958)=(from=11364,to=637) - to_lower(959)=(from=11367,to=11368) - to_lower(960)=(from=11369,to=11370) - to_lower(961)=(from=11371,to=11372) - to_lower(962)=(from=11373,to=593) - to_lower(963)=(from=11374,to=625) - to_lower(964)=(from=11375,to=592) - to_lower(965)=(from=11376,to=594) - to_lower(966)=(from=11378,to=11379) - to_lower(967)=(from=11381,to=11382) - to_lower(968)=(from=11390,to=575) - to_lower(969)=(from=11391,to=576) - to_lower(970)=(from=11392,to=11393) - to_lower(971)=(from=11394,to=11395) - to_lower(972)=(from=11396,to=11397) - to_lower(973)=(from=11398,to=11399) - to_lower(974)=(from=11400,to=11401) - to_lower(975)=(from=11402,to=11403) - to_lower(976)=(from=11404,to=11405) - to_lower(977)=(from=11406,to=11407) - to_lower(978)=(from=11408,to=11409) - to_lower(979)=(from=11410,to=11411) - to_lower(980)=(from=11412,to=11413) - to_lower(981)=(from=11414,to=11415) - to_lower(982)=(from=11416,to=11417) - to_lower(983)=(from=11418,to=11419) - to_lower(984)=(from=11420,to=11421) - to_lower(985)=(from=11422,to=11423) - to_lower(986)=(from=11424,to=11425) - to_lower(987)=(from=11426,to=11427) - to_lower(988)=(from=11428,to=11429) - to_lower(989)=(from=11430,to=11431) - to_lower(990)=(from=11432,to=11433) - to_lower(991)=(from=11434,to=11435) - to_lower(992)=(from=11436,to=11437) - to_lower(993)=(from=11438,to=11439) - to_lower(994)=(from=11440,to=11441) - to_lower(995)=(from=11442,to=11443) - to_lower(996)=(from=11444,to=11445) - to_lower(997)=(from=11446,to=11447) - to_lower(998)=(from=11448,to=11449) - to_lower(999)=(from=11450,to=11451) - to_lower(1000)=(from=11452,to=11453) - to_lower(1001)=(from=11454,to=11455) - to_lower(1002)=(from=11456,to=11457) - to_lower(1003)=(from=11458,to=11459) - to_lower(1004)=(from=11460,to=11461) - to_lower(1005)=(from=11462,to=11463) - to_lower(1006)=(from=11464,to=11465) - to_lower(1007)=(from=11466,to=11467) - to_lower(1008)=(from=11468,to=11469) - to_lower(1009)=(from=11470,to=11471) - to_lower(1010)=(from=11472,to=11473) - to_lower(1011)=(from=11474,to=11475) - to_lower(1012)=(from=11476,to=11477) - to_lower(1013)=(from=11478,to=11479) - to_lower(1014)=(from=11480,to=11481) - to_lower(1015)=(from=11482,to=11483) - to_lower(1016)=(from=11484,to=11485) - to_lower(1017)=(from=11486,to=11487) - to_lower(1018)=(from=11488,to=11489) - to_lower(1019)=(from=11490,to=11491) - to_lower(1020)=(from=11499,to=11500) - to_lower(1021)=(from=11501,to=11502) - to_lower(1022)=(from=11506,to=11507) - to_lower(1023)=(from=42560,to=42561) - to_lower(1024)=(from=42562,to=42563) - to_lower(1025)=(from=42564,to=42565) - to_lower(1026)=(from=42566,to=42567) - to_lower(1027)=(from=42568,to=42569) - to_lower(1028)=(from=42570,to=42571) - to_lower(1029)=(from=42572,to=42573) - to_lower(1030)=(from=42574,to=42575) - to_lower(1031)=(from=42576,to=42577) - to_lower(1032)=(from=42578,to=42579) - to_lower(1033)=(from=42580,to=42581) - to_lower(1034)=(from=42582,to=42583) - to_lower(1035)=(from=42584,to=42585) - to_lower(1036)=(from=42586,to=42587) - to_lower(1037)=(from=42588,to=42589) - to_lower(1038)=(from=42590,to=42591) - to_lower(1039)=(from=42592,to=42593) - to_lower(1040)=(from=42594,to=42595) - to_lower(1041)=(from=42596,to=42597) - to_lower(1042)=(from=42598,to=42599) - to_lower(1043)=(from=42600,to=42601) - to_lower(1044)=(from=42602,to=42603) - to_lower(1045)=(from=42604,to=42605) - to_lower(1046)=(from=42624,to=42625) - to_lower(1047)=(from=42626,to=42627) - to_lower(1048)=(from=42628,to=42629) - to_lower(1049)=(from=42630,to=42631) - to_lower(1050)=(from=42632,to=42633) - to_lower(1051)=(from=42634,to=42635) - to_lower(1052)=(from=42636,to=42637) - to_lower(1053)=(from=42638,to=42639) - to_lower(1054)=(from=42640,to=42641) - to_lower(1055)=(from=42642,to=42643) - to_lower(1056)=(from=42644,to=42645) - to_lower(1057)=(from=42646,to=42647) - to_lower(1058)=(from=42648,to=42649) - to_lower(1059)=(from=42650,to=42651) - to_lower(1060)=(from=42786,to=42787) - to_lower(1061)=(from=42788,to=42789) - to_lower(1062)=(from=42790,to=42791) - to_lower(1063)=(from=42792,to=42793) - to_lower(1064)=(from=42794,to=42795) - to_lower(1065)=(from=42796,to=42797) - to_lower(1066)=(from=42798,to=42799) - to_lower(1067)=(from=42802,to=42803) - to_lower(1068)=(from=42804,to=42805) - to_lower(1069)=(from=42806,to=42807) - to_lower(1070)=(from=42808,to=42809) - to_lower(1071)=(from=42810,to=42811) - to_lower(1072)=(from=42812,to=42813) - to_lower(1073)=(from=42814,to=42815) - to_lower(1074)=(from=42816,to=42817) - to_lower(1075)=(from=42818,to=42819) - to_lower(1076)=(from=42820,to=42821) - to_lower(1077)=(from=42822,to=42823) - to_lower(1078)=(from=42824,to=42825) - to_lower(1079)=(from=42826,to=42827) - to_lower(1080)=(from=42828,to=42829) - to_lower(1081)=(from=42830,to=42831) - to_lower(1082)=(from=42832,to=42833) - to_lower(1083)=(from=42834,to=42835) - to_lower(1084)=(from=42836,to=42837) - to_lower(1085)=(from=42838,to=42839) - to_lower(1086)=(from=42840,to=42841) - to_lower(1087)=(from=42842,to=42843) - to_lower(1088)=(from=42844,to=42845) - to_lower(1089)=(from=42846,to=42847) - to_lower(1090)=(from=42848,to=42849) - to_lower(1091)=(from=42850,to=42851) - to_lower(1092)=(from=42852,to=42853) - to_lower(1093)=(from=42854,to=42855) - to_lower(1094)=(from=42856,to=42857) - to_lower(1095)=(from=42858,to=42859) - to_lower(1096)=(from=42860,to=42861) - to_lower(1097)=(from=42862,to=42863) - to_lower(1098)=(from=42873,to=42874) - to_lower(1099)=(from=42875,to=42876) - to_lower(1100)=(from=42877,to=7545) - to_lower(1101)=(from=42878,to=42879) - to_lower(1102)=(from=42880,to=42881) - to_lower(1103)=(from=42882,to=42883) - to_lower(1104)=(from=42884,to=42885) - to_lower(1105)=(from=42886,to=42887) - to_lower(1106)=(from=42891,to=42892) - to_lower(1107)=(from=42893,to=613) - to_lower(1108)=(from=42896,to=42897) - to_lower(1109)=(from=42898,to=42899) - to_lower(1110)=(from=42902,to=42903) - to_lower(1111)=(from=42904,to=42905) - to_lower(1112)=(from=42906,to=42907) - to_lower(1113)=(from=42908,to=42909) - to_lower(1114)=(from=42910,to=42911) - to_lower(1115)=(from=42912,to=42913) - to_lower(1116)=(from=42914,to=42915) - to_lower(1117)=(from=42916,to=42917) - to_lower(1118)=(from=42918,to=42919) - to_lower(1119)=(from=42920,to=42921) - to_lower(1120)=(from=42922,to=614) - to_lower(1121)=(from=42923,to=604) - to_lower(1122)=(from=42924,to=609) - to_lower(1123)=(from=42925,to=620) - to_lower(1124)=(from=42926,to=618) - to_lower(1125)=(from=42928,to=670) - to_lower(1126)=(from=42929,to=647) - to_lower(1127)=(from=42930,to=669) - to_lower(1128)=(from=42931,to=43859) - to_lower(1129)=(from=42932,to=42933) - to_lower(1130)=(from=42934,to=42935) - to_lower(1131)=(from=42936,to=42937) - to_lower(1132)=(from=42938,to=42939) - to_lower(1133)=(from=42940,to=42941) - to_lower(1134)=(from=42942,to=42943) - to_lower(1135)=(from=42946,to=42947) - to_lower(1136)=(from=42948,to=42900) - to_lower(1137)=(from=42949,to=642) - to_lower(1138)=(from=42950,to=7566) - to_lower(1139)=(from=42951,to=42952) - to_lower(1140)=(from=42953,to=42954) - to_lower(1141)=(from=42997,to=42998) - to_lower(1142)=(from=65313,to=65345) - to_lower(1143)=(from=65314,to=65346) - to_lower(1144)=(from=65315,to=65347) - to_lower(1145)=(from=65316,to=65348) - to_lower(1146)=(from=65317,to=65349) - to_lower(1147)=(from=65318,to=65350) - to_lower(1148)=(from=65319,to=65351) - to_lower(1149)=(from=65320,to=65352) - to_lower(1150)=(from=65321,to=65353) - to_lower(1151)=(from=65322,to=65354) - to_lower(1152)=(from=65323,to=65355) - to_lower(1153)=(from=65324,to=65356) - to_lower(1154)=(from=65325,to=65357) - to_lower(1155)=(from=65326,to=65358) - to_lower(1156)=(from=65327,to=65359) - to_lower(1157)=(from=65328,to=65360) - to_lower(1158)=(from=65329,to=65361) - to_lower(1159)=(from=65330,to=65362) - to_lower(1160)=(from=65331,to=65363) - to_lower(1161)=(from=65332,to=65364) - to_lower(1162)=(from=65333,to=65365) - to_lower(1163)=(from=65334,to=65366) - to_lower(1164)=(from=65335,to=65367) - to_lower(1165)=(from=65336,to=65368) - to_lower(1166)=(from=65337,to=65369) - to_lower(1167)=(from=65338,to=65370) - to_lower(1168)=(from=66560,to=66600) - to_lower(1169)=(from=66561,to=66601) - to_lower(1170)=(from=66562,to=66602) - to_lower(1171)=(from=66563,to=66603) - to_lower(1172)=(from=66564,to=66604) - to_lower(1173)=(from=66565,to=66605) - to_lower(1174)=(from=66566,to=66606) - to_lower(1175)=(from=66567,to=66607) - to_lower(1176)=(from=66568,to=66608) - to_lower(1177)=(from=66569,to=66609) - to_lower(1178)=(from=66570,to=66610) - to_lower(1179)=(from=66571,to=66611) - to_lower(1180)=(from=66572,to=66612) - to_lower(1181)=(from=66573,to=66613) - to_lower(1182)=(from=66574,to=66614) - to_lower(1183)=(from=66575,to=66615) - to_lower(1184)=(from=66576,to=66616) - to_lower(1185)=(from=66577,to=66617) - to_lower(1186)=(from=66578,to=66618) - to_lower(1187)=(from=66579,to=66619) - to_lower(1188)=(from=66580,to=66620) - to_lower(1189)=(from=66581,to=66621) - to_lower(1190)=(from=66582,to=66622) - to_lower(1191)=(from=66583,to=66623) - to_lower(1192)=(from=66584,to=66624) - to_lower(1193)=(from=66585,to=66625) - to_lower(1194)=(from=66586,to=66626) - to_lower(1195)=(from=66587,to=66627) - to_lower(1196)=(from=66588,to=66628) - to_lower(1197)=(from=66589,to=66629) - to_lower(1198)=(from=66590,to=66630) - to_lower(1199)=(from=66591,to=66631) - to_lower(1200)=(from=66592,to=66632) - to_lower(1201)=(from=66593,to=66633) - to_lower(1202)=(from=66594,to=66634) - to_lower(1203)=(from=66595,to=66635) - to_lower(1204)=(from=66596,to=66636) - to_lower(1205)=(from=66597,to=66637) - to_lower(1206)=(from=66598,to=66638) - to_lower(1207)=(from=66599,to=66639) - to_lower(1208)=(from=66736,to=66776) - to_lower(1209)=(from=66737,to=66777) - to_lower(1210)=(from=66738,to=66778) - to_lower(1211)=(from=66739,to=66779) - to_lower(1212)=(from=66740,to=66780) - to_lower(1213)=(from=66741,to=66781) - to_lower(1214)=(from=66742,to=66782) - to_lower(1215)=(from=66743,to=66783) - to_lower(1216)=(from=66744,to=66784) - to_lower(1217)=(from=66745,to=66785) - to_lower(1218)=(from=66746,to=66786) - to_lower(1219)=(from=66747,to=66787) - to_lower(1220)=(from=66748,to=66788) - to_lower(1221)=(from=66749,to=66789) - to_lower(1222)=(from=66750,to=66790) - to_lower(1223)=(from=66751,to=66791) - to_lower(1224)=(from=66752,to=66792) - to_lower(1225)=(from=66753,to=66793) - to_lower(1226)=(from=66754,to=66794) - to_lower(1227)=(from=66755,to=66795) - to_lower(1228)=(from=66756,to=66796) - to_lower(1229)=(from=66757,to=66797) - to_lower(1230)=(from=66758,to=66798) - to_lower(1231)=(from=66759,to=66799) - to_lower(1232)=(from=66760,to=66800) - to_lower(1233)=(from=66761,to=66801) - to_lower(1234)=(from=66762,to=66802) - to_lower(1235)=(from=66763,to=66803) - to_lower(1236)=(from=66764,to=66804) - to_lower(1237)=(from=66765,to=66805) - to_lower(1238)=(from=66766,to=66806) - to_lower(1239)=(from=66767,to=66807) - to_lower(1240)=(from=66768,to=66808) - to_lower(1241)=(from=66769,to=66809) - to_lower(1242)=(from=66770,to=66810) - to_lower(1243)=(from=66771,to=66811) - to_lower(1244)=(from=68736,to=68800) - to_lower(1245)=(from=68737,to=68801) - to_lower(1246)=(from=68738,to=68802) - to_lower(1247)=(from=68739,to=68803) - to_lower(1248)=(from=68740,to=68804) - to_lower(1249)=(from=68741,to=68805) - to_lower(1250)=(from=68742,to=68806) - to_lower(1251)=(from=68743,to=68807) - to_lower(1252)=(from=68744,to=68808) - to_lower(1253)=(from=68745,to=68809) - to_lower(1254)=(from=68746,to=68810) - to_lower(1255)=(from=68747,to=68811) - to_lower(1256)=(from=68748,to=68812) - to_lower(1257)=(from=68749,to=68813) - to_lower(1258)=(from=68750,to=68814) - to_lower(1259)=(from=68751,to=68815) - to_lower(1260)=(from=68752,to=68816) - to_lower(1261)=(from=68753,to=68817) - to_lower(1262)=(from=68754,to=68818) - to_lower(1263)=(from=68755,to=68819) - to_lower(1264)=(from=68756,to=68820) - to_lower(1265)=(from=68757,to=68821) - to_lower(1266)=(from=68758,to=68822) - to_lower(1267)=(from=68759,to=68823) - to_lower(1268)=(from=68760,to=68824) - to_lower(1269)=(from=68761,to=68825) - to_lower(1270)=(from=68762,to=68826) - to_lower(1271)=(from=68763,to=68827) - to_lower(1272)=(from=68764,to=68828) - to_lower(1273)=(from=68765,to=68829) - to_lower(1274)=(from=68766,to=68830) - to_lower(1275)=(from=68767,to=68831) - to_lower(1276)=(from=68768,to=68832) - to_lower(1277)=(from=68769,to=68833) - to_lower(1278)=(from=68770,to=68834) - to_lower(1279)=(from=68771,to=68835) - to_lower(1280)=(from=68772,to=68836) - to_lower(1281)=(from=68773,to=68837) - to_lower(1282)=(from=68774,to=68838) - to_lower(1283)=(from=68775,to=68839) - to_lower(1284)=(from=68776,to=68840) - to_lower(1285)=(from=68777,to=68841) - to_lower(1286)=(from=68778,to=68842) - to_lower(1287)=(from=68779,to=68843) - to_lower(1288)=(from=68780,to=68844) - to_lower(1289)=(from=68781,to=68845) - to_lower(1290)=(from=68782,to=68846) - to_lower(1291)=(from=68783,to=68847) - to_lower(1292)=(from=68784,to=68848) - to_lower(1293)=(from=68785,to=68849) - to_lower(1294)=(from=68786,to=68850) - to_lower(1295)=(from=71840,to=71872) - to_lower(1296)=(from=71841,to=71873) - to_lower(1297)=(from=71842,to=71874) - to_lower(1298)=(from=71843,to=71875) - to_lower(1299)=(from=71844,to=71876) - to_lower(1300)=(from=71845,to=71877) - to_lower(1301)=(from=71846,to=71878) - to_lower(1302)=(from=71847,to=71879) - to_lower(1303)=(from=71848,to=71880) - to_lower(1304)=(from=71849,to=71881) - to_lower(1305)=(from=71850,to=71882) - to_lower(1306)=(from=71851,to=71883) - to_lower(1307)=(from=71852,to=71884) - to_lower(1308)=(from=71853,to=71885) - to_lower(1309)=(from=71854,to=71886) - to_lower(1310)=(from=71855,to=71887) - to_lower(1311)=(from=71856,to=71888) - to_lower(1312)=(from=71857,to=71889) - to_lower(1313)=(from=71858,to=71890) - to_lower(1314)=(from=71859,to=71891) - to_lower(1315)=(from=71860,to=71892) - to_lower(1316)=(from=71861,to=71893) - to_lower(1317)=(from=71862,to=71894) - to_lower(1318)=(from=71863,to=71895) - to_lower(1319)=(from=71864,to=71896) - to_lower(1320)=(from=71865,to=71897) - to_lower(1321)=(from=71866,to=71898) - to_lower(1322)=(from=71867,to=71899) - to_lower(1323)=(from=71868,to=71900) - to_lower(1324)=(from=71869,to=71901) - to_lower(1325)=(from=71870,to=71902) - to_lower(1326)=(from=71871,to=71903) - to_lower(1327)=(from=93760,to=93792) - to_lower(1328)=(from=93761,to=93793) - to_lower(1329)=(from=93762,to=93794) - to_lower(1330)=(from=93763,to=93795) - to_lower(1331)=(from=93764,to=93796) - to_lower(1332)=(from=93765,to=93797) - to_lower(1333)=(from=93766,to=93798) - to_lower(1334)=(from=93767,to=93799) - to_lower(1335)=(from=93768,to=93800) - to_lower(1336)=(from=93769,to=93801) - to_lower(1337)=(from=93770,to=93802) - to_lower(1338)=(from=93771,to=93803) - to_lower(1339)=(from=93772,to=93804) - to_lower(1340)=(from=93773,to=93805) - to_lower(1341)=(from=93774,to=93806) - to_lower(1342)=(from=93775,to=93807) - to_lower(1343)=(from=93776,to=93808) - to_lower(1344)=(from=93777,to=93809) - to_lower(1345)=(from=93778,to=93810) - to_lower(1346)=(from=93779,to=93811) - to_lower(1347)=(from=93780,to=93812) - to_lower(1348)=(from=93781,to=93813) - to_lower(1349)=(from=93782,to=93814) - to_lower(1350)=(from=93783,to=93815) - to_lower(1351)=(from=93784,to=93816) - to_lower(1352)=(from=93785,to=93817) - to_lower(1353)=(from=93786,to=93818) - to_lower(1354)=(from=93787,to=93819) - to_lower(1355)=(from=93788,to=93820) - to_lower(1356)=(from=93789,to=93821) - to_lower(1357)=(from=93790,to=93822) - to_lower(1358)=(from=93791,to=93823) - to_lower(1359)=(from=125184,to=125218) - to_lower(1360)=(from=125185,to=125219) - to_lower(1361)=(from=125186,to=125220) - to_lower(1362)=(from=125187,to=125221) - to_lower(1363)=(from=125188,to=125222) - to_lower(1364)=(from=125189,to=125223) - to_lower(1365)=(from=125190,to=125224) - to_lower(1366)=(from=125191,to=125225) - to_lower(1367)=(from=125192,to=125226) - to_lower(1368)=(from=125193,to=125227) - to_lower(1369)=(from=125194,to=125228) - to_lower(1370)=(from=125195,to=125229) - to_lower(1371)=(from=125196,to=125230) - to_lower(1372)=(from=125197,to=125231) - to_lower(1373)=(from=125198,to=125232) - to_lower(1374)=(from=125199,to=125233) - to_lower(1375)=(from=125200,to=125234) - to_lower(1376)=(from=125201,to=125235) - to_lower(1377)=(from=125202,to=125236) - to_lower(1378)=(from=125203,to=125237) - to_lower(1379)=(from=125204,to=125238) - to_lower(1380)=(from=125205,to=125239) - to_lower(1381)=(from=125206,to=125240) - to_lower(1382)=(from=125207,to=125241) - to_lower(1383)=(from=125208,to=125242) - to_lower(1384)=(from=125209,to=125243) - to_lower(1385)=(from=125210,to=125244) - to_lower(1386)=(from=125211,to=125245) - to_lower(1387)=(from=125212,to=125246) - to_lower(1388)=(from=125213,to=125247) - to_lower(1389)=(from=125214,to=125248) - to_lower(1390)=(from=125215,to=125249) - to_lower(1391)=(from=125216,to=125250) - to_lower(1392)=(from=125217,to=125251) - to_upper(0)=(from=97,to=65) - to_upper(1)=(from=98,to=66) - to_upper(2)=(from=99,to=67) - to_upper(3)=(from=100,to=68) - to_upper(4)=(from=101,to=69) - to_upper(5)=(from=102,to=70) - to_upper(6)=(from=103,to=71) - to_upper(7)=(from=104,to=72) - to_upper(8)=(from=105,to=73) - to_upper(9)=(from=106,to=74) - to_upper(10)=(from=107,to=75) - to_upper(11)=(from=108,to=76) - to_upper(12)=(from=109,to=77) - to_upper(13)=(from=110,to=78) - to_upper(14)=(from=111,to=79) - to_upper(15)=(from=112,to=80) - to_upper(16)=(from=113,to=81) - to_upper(17)=(from=114,to=82) - to_upper(18)=(from=115,to=83) - to_upper(19)=(from=116,to=84) - to_upper(20)=(from=117,to=85) - to_upper(21)=(from=118,to=86) - to_upper(22)=(from=119,to=87) - to_upper(23)=(from=120,to=88) - to_upper(24)=(from=121,to=89) - to_upper(25)=(from=122,to=90) - to_upper(26)=(from=181,to=924) - to_upper(27)=(from=224,to=192) - to_upper(28)=(from=225,to=193) - to_upper(29)=(from=226,to=194) - to_upper(30)=(from=227,to=195) - to_upper(31)=(from=228,to=196) - to_upper(32)=(from=229,to=197) - to_upper(33)=(from=230,to=198) - to_upper(34)=(from=231,to=199) - to_upper(35)=(from=232,to=200) - to_upper(36)=(from=233,to=201) - to_upper(37)=(from=234,to=202) - to_upper(38)=(from=235,to=203) - to_upper(39)=(from=236,to=204) - to_upper(40)=(from=237,to=205) - to_upper(41)=(from=238,to=206) - to_upper(42)=(from=239,to=207) - to_upper(43)=(from=240,to=208) - to_upper(44)=(from=241,to=209) - to_upper(45)=(from=242,to=210) - to_upper(46)=(from=243,to=211) - to_upper(47)=(from=244,to=212) - to_upper(48)=(from=245,to=213) - to_upper(49)=(from=246,to=214) - to_upper(50)=(from=248,to=216) - to_upper(51)=(from=249,to=217) - to_upper(52)=(from=250,to=218) - to_upper(53)=(from=251,to=219) - to_upper(54)=(from=252,to=220) - to_upper(55)=(from=253,to=221) - to_upper(56)=(from=254,to=222) - to_upper(57)=(from=255,to=376) - to_upper(58)=(from=257,to=256) - to_upper(59)=(from=259,to=258) - to_upper(60)=(from=261,to=260) - to_upper(61)=(from=263,to=262) - to_upper(62)=(from=265,to=264) - to_upper(63)=(from=267,to=266) - to_upper(64)=(from=269,to=268) - to_upper(65)=(from=271,to=270) - to_upper(66)=(from=273,to=272) - to_upper(67)=(from=275,to=274) - to_upper(68)=(from=277,to=276) - to_upper(69)=(from=279,to=278) - to_upper(70)=(from=281,to=280) - to_upper(71)=(from=283,to=282) - to_upper(72)=(from=285,to=284) - to_upper(73)=(from=287,to=286) - to_upper(74)=(from=289,to=288) - to_upper(75)=(from=291,to=290) - to_upper(76)=(from=293,to=292) - to_upper(77)=(from=295,to=294) - to_upper(78)=(from=297,to=296) - to_upper(79)=(from=299,to=298) - to_upper(80)=(from=301,to=300) - to_upper(81)=(from=303,to=302) - to_upper(82)=(from=305,to=73) - to_upper(83)=(from=307,to=306) - to_upper(84)=(from=309,to=308) - to_upper(85)=(from=311,to=310) - to_upper(86)=(from=314,to=313) - to_upper(87)=(from=316,to=315) - to_upper(88)=(from=318,to=317) - to_upper(89)=(from=320,to=319) - to_upper(90)=(from=322,to=321) - to_upper(91)=(from=324,to=323) - to_upper(92)=(from=326,to=325) - to_upper(93)=(from=328,to=327) - to_upper(94)=(from=331,to=330) - to_upper(95)=(from=333,to=332) - to_upper(96)=(from=335,to=334) - to_upper(97)=(from=337,to=336) - to_upper(98)=(from=339,to=338) - to_upper(99)=(from=341,to=340) - to_upper(100)=(from=343,to=342) - to_upper(101)=(from=345,to=344) - to_upper(102)=(from=347,to=346) - to_upper(103)=(from=349,to=348) - to_upper(104)=(from=351,to=350) - to_upper(105)=(from=353,to=352) - to_upper(106)=(from=355,to=354) - to_upper(107)=(from=357,to=356) - to_upper(108)=(from=359,to=358) - to_upper(109)=(from=361,to=360) - to_upper(110)=(from=363,to=362) - to_upper(111)=(from=365,to=364) - to_upper(112)=(from=367,to=366) - to_upper(113)=(from=369,to=368) - to_upper(114)=(from=371,to=370) - to_upper(115)=(from=373,to=372) - to_upper(116)=(from=375,to=374) - to_upper(117)=(from=378,to=377) - to_upper(118)=(from=380,to=379) - to_upper(119)=(from=382,to=381) - to_upper(120)=(from=383,to=83) - to_upper(121)=(from=384,to=579) - to_upper(122)=(from=387,to=386) - to_upper(123)=(from=389,to=388) - to_upper(124)=(from=392,to=391) - to_upper(125)=(from=396,to=395) - to_upper(126)=(from=402,to=401) - to_upper(127)=(from=405,to=502) - to_upper(128)=(from=409,to=408) - to_upper(129)=(from=410,to=573) - to_upper(130)=(from=414,to=544) - to_upper(131)=(from=417,to=416) - to_upper(132)=(from=419,to=418) - to_upper(133)=(from=421,to=420) - to_upper(134)=(from=424,to=423) - to_upper(135)=(from=429,to=428) - to_upper(136)=(from=432,to=431) - to_upper(137)=(from=436,to=435) - to_upper(138)=(from=438,to=437) - to_upper(139)=(from=441,to=440) - to_upper(140)=(from=445,to=444) - to_upper(141)=(from=447,to=503) - to_upper(142)=(from=453,to=452) - to_upper(143)=(from=454,to=452) - to_upper(144)=(from=456,to=455) - to_upper(145)=(from=457,to=455) - to_upper(146)=(from=459,to=458) - to_upper(147)=(from=460,to=458) - to_upper(148)=(from=462,to=461) - to_upper(149)=(from=464,to=463) - to_upper(150)=(from=466,to=465) - to_upper(151)=(from=468,to=467) - to_upper(152)=(from=470,to=469) - to_upper(153)=(from=472,to=471) - to_upper(154)=(from=474,to=473) - to_upper(155)=(from=476,to=475) - to_upper(156)=(from=477,to=398) - to_upper(157)=(from=479,to=478) - to_upper(158)=(from=481,to=480) - to_upper(159)=(from=483,to=482) - to_upper(160)=(from=485,to=484) - to_upper(161)=(from=487,to=486) - to_upper(162)=(from=489,to=488) - to_upper(163)=(from=491,to=490) - to_upper(164)=(from=493,to=492) - to_upper(165)=(from=495,to=494) - to_upper(166)=(from=498,to=497) - to_upper(167)=(from=499,to=497) - to_upper(168)=(from=501,to=500) - to_upper(169)=(from=505,to=504) - to_upper(170)=(from=507,to=506) - to_upper(171)=(from=509,to=508) - to_upper(172)=(from=511,to=510) - to_upper(173)=(from=513,to=512) - to_upper(174)=(from=515,to=514) - to_upper(175)=(from=517,to=516) - to_upper(176)=(from=519,to=518) - to_upper(177)=(from=521,to=520) - to_upper(178)=(from=523,to=522) - to_upper(179)=(from=525,to=524) - to_upper(180)=(from=527,to=526) - to_upper(181)=(from=529,to=528) - to_upper(182)=(from=531,to=530) - to_upper(183)=(from=533,to=532) - to_upper(184)=(from=535,to=534) - to_upper(185)=(from=537,to=536) - to_upper(186)=(from=539,to=538) - to_upper(187)=(from=541,to=540) - to_upper(188)=(from=543,to=542) - to_upper(189)=(from=547,to=546) - to_upper(190)=(from=549,to=548) - to_upper(191)=(from=551,to=550) - to_upper(192)=(from=553,to=552) - to_upper(193)=(from=555,to=554) - to_upper(194)=(from=557,to=556) - to_upper(195)=(from=559,to=558) - to_upper(196)=(from=561,to=560) - to_upper(197)=(from=563,to=562) - to_upper(198)=(from=572,to=571) - to_upper(199)=(from=575,to=11390) - to_upper(200)=(from=576,to=11391) - to_upper(201)=(from=578,to=577) - to_upper(202)=(from=583,to=582) - to_upper(203)=(from=585,to=584) - to_upper(204)=(from=587,to=586) - to_upper(205)=(from=589,to=588) - to_upper(206)=(from=591,to=590) - to_upper(207)=(from=592,to=11375) - to_upper(208)=(from=593,to=11373) - to_upper(209)=(from=594,to=11376) - to_upper(210)=(from=595,to=385) - to_upper(211)=(from=596,to=390) - to_upper(212)=(from=598,to=393) - to_upper(213)=(from=599,to=394) - to_upper(214)=(from=601,to=399) - to_upper(215)=(from=603,to=400) - to_upper(216)=(from=604,to=42923) - to_upper(217)=(from=608,to=403) - to_upper(218)=(from=609,to=42924) - to_upper(219)=(from=611,to=404) - to_upper(220)=(from=613,to=42893) - to_upper(221)=(from=614,to=42922) - to_upper(222)=(from=616,to=407) - to_upper(223)=(from=617,to=406) - to_upper(224)=(from=618,to=42926) - to_upper(225)=(from=619,to=11362) - to_upper(226)=(from=620,to=42925) - to_upper(227)=(from=623,to=412) - to_upper(228)=(from=625,to=11374) - to_upper(229)=(from=626,to=413) - to_upper(230)=(from=629,to=415) - to_upper(231)=(from=637,to=11364) - to_upper(232)=(from=640,to=422) - to_upper(233)=(from=642,to=42949) - to_upper(234)=(from=643,to=425) - to_upper(235)=(from=647,to=42929) - to_upper(236)=(from=648,to=430) - to_upper(237)=(from=649,to=580) - to_upper(238)=(from=650,to=433) - to_upper(239)=(from=651,to=434) - to_upper(240)=(from=652,to=581) - to_upper(241)=(from=658,to=439) - to_upper(242)=(from=669,to=42930) - to_upper(243)=(from=670,to=42928) - to_upper(244)=(from=837,to=921) - to_upper(245)=(from=881,to=880) - to_upper(246)=(from=883,to=882) - to_upper(247)=(from=887,to=886) - to_upper(248)=(from=891,to=1021) - to_upper(249)=(from=892,to=1022) - to_upper(250)=(from=893,to=1023) - to_upper(251)=(from=940,to=902) - to_upper(252)=(from=941,to=904) - to_upper(253)=(from=942,to=905) - to_upper(254)=(from=943,to=906) - to_upper(255)=(from=945,to=913) - to_upper(256)=(from=946,to=914) - to_upper(257)=(from=947,to=915) - to_upper(258)=(from=948,to=916) - to_upper(259)=(from=949,to=917) - to_upper(260)=(from=950,to=918) - to_upper(261)=(from=951,to=919) - to_upper(262)=(from=952,to=920) - to_upper(263)=(from=953,to=921) - to_upper(264)=(from=954,to=922) - to_upper(265)=(from=955,to=923) - to_upper(266)=(from=956,to=924) - to_upper(267)=(from=957,to=925) - to_upper(268)=(from=958,to=926) - to_upper(269)=(from=959,to=927) - to_upper(270)=(from=960,to=928) - to_upper(271)=(from=961,to=929) - to_upper(272)=(from=962,to=931) - to_upper(273)=(from=963,to=931) - to_upper(274)=(from=964,to=932) - to_upper(275)=(from=965,to=933) - to_upper(276)=(from=966,to=934) - to_upper(277)=(from=967,to=935) - to_upper(278)=(from=968,to=936) - to_upper(279)=(from=969,to=937) - to_upper(280)=(from=970,to=938) - to_upper(281)=(from=971,to=939) - to_upper(282)=(from=972,to=908) - to_upper(283)=(from=973,to=910) - to_upper(284)=(from=974,to=911) - to_upper(285)=(from=976,to=914) - to_upper(286)=(from=977,to=920) - to_upper(287)=(from=981,to=934) - to_upper(288)=(from=982,to=928) - to_upper(289)=(from=983,to=975) - to_upper(290)=(from=985,to=984) - to_upper(291)=(from=987,to=986) - to_upper(292)=(from=989,to=988) - to_upper(293)=(from=991,to=990) - to_upper(294)=(from=993,to=992) - to_upper(295)=(from=995,to=994) - to_upper(296)=(from=997,to=996) - to_upper(297)=(from=999,to=998) - to_upper(298)=(from=1001,to=1000) - to_upper(299)=(from=1003,to=1002) - to_upper(300)=(from=1005,to=1004) - to_upper(301)=(from=1007,to=1006) - to_upper(302)=(from=1008,to=922) - to_upper(303)=(from=1009,to=929) - to_upper(304)=(from=1010,to=1017) - to_upper(305)=(from=1011,to=895) - to_upper(306)=(from=1013,to=917) - to_upper(307)=(from=1016,to=1015) - to_upper(308)=(from=1019,to=1018) - to_upper(309)=(from=1072,to=1040) - to_upper(310)=(from=1073,to=1041) - to_upper(311)=(from=1074,to=1042) - to_upper(312)=(from=1075,to=1043) - to_upper(313)=(from=1076,to=1044) - to_upper(314)=(from=1077,to=1045) - to_upper(315)=(from=1078,to=1046) - to_upper(316)=(from=1079,to=1047) - to_upper(317)=(from=1080,to=1048) - to_upper(318)=(from=1081,to=1049) - to_upper(319)=(from=1082,to=1050) - to_upper(320)=(from=1083,to=1051) - to_upper(321)=(from=1084,to=1052) - to_upper(322)=(from=1085,to=1053) - to_upper(323)=(from=1086,to=1054) - to_upper(324)=(from=1087,to=1055) - to_upper(325)=(from=1088,to=1056) - to_upper(326)=(from=1089,to=1057) - to_upper(327)=(from=1090,to=1058) - to_upper(328)=(from=1091,to=1059) - to_upper(329)=(from=1092,to=1060) - to_upper(330)=(from=1093,to=1061) - to_upper(331)=(from=1094,to=1062) - to_upper(332)=(from=1095,to=1063) - to_upper(333)=(from=1096,to=1064) - to_upper(334)=(from=1097,to=1065) - to_upper(335)=(from=1098,to=1066) - to_upper(336)=(from=1099,to=1067) - to_upper(337)=(from=1100,to=1068) - to_upper(338)=(from=1101,to=1069) - to_upper(339)=(from=1102,to=1070) - to_upper(340)=(from=1103,to=1071) - to_upper(341)=(from=1104,to=1024) - to_upper(342)=(from=1105,to=1025) - to_upper(343)=(from=1106,to=1026) - to_upper(344)=(from=1107,to=1027) - to_upper(345)=(from=1108,to=1028) - to_upper(346)=(from=1109,to=1029) - to_upper(347)=(from=1110,to=1030) - to_upper(348)=(from=1111,to=1031) - to_upper(349)=(from=1112,to=1032) - to_upper(350)=(from=1113,to=1033) - to_upper(351)=(from=1114,to=1034) - to_upper(352)=(from=1115,to=1035) - to_upper(353)=(from=1116,to=1036) - to_upper(354)=(from=1117,to=1037) - to_upper(355)=(from=1118,to=1038) - to_upper(356)=(from=1119,to=1039) - to_upper(357)=(from=1121,to=1120) - to_upper(358)=(from=1123,to=1122) - to_upper(359)=(from=1125,to=1124) - to_upper(360)=(from=1127,to=1126) - to_upper(361)=(from=1129,to=1128) - to_upper(362)=(from=1131,to=1130) - to_upper(363)=(from=1133,to=1132) - to_upper(364)=(from=1135,to=1134) - to_upper(365)=(from=1137,to=1136) - to_upper(366)=(from=1139,to=1138) - to_upper(367)=(from=1141,to=1140) - to_upper(368)=(from=1143,to=1142) - to_upper(369)=(from=1145,to=1144) - to_upper(370)=(from=1147,to=1146) - to_upper(371)=(from=1149,to=1148) - to_upper(372)=(from=1151,to=1150) - to_upper(373)=(from=1153,to=1152) - to_upper(374)=(from=1163,to=1162) - to_upper(375)=(from=1165,to=1164) - to_upper(376)=(from=1167,to=1166) - to_upper(377)=(from=1169,to=1168) - to_upper(378)=(from=1171,to=1170) - to_upper(379)=(from=1173,to=1172) - to_upper(380)=(from=1175,to=1174) - to_upper(381)=(from=1177,to=1176) - to_upper(382)=(from=1179,to=1178) - to_upper(383)=(from=1181,to=1180) - to_upper(384)=(from=1183,to=1182) - to_upper(385)=(from=1185,to=1184) - to_upper(386)=(from=1187,to=1186) - to_upper(387)=(from=1189,to=1188) - to_upper(388)=(from=1191,to=1190) - to_upper(389)=(from=1193,to=1192) - to_upper(390)=(from=1195,to=1194) - to_upper(391)=(from=1197,to=1196) - to_upper(392)=(from=1199,to=1198) - to_upper(393)=(from=1201,to=1200) - to_upper(394)=(from=1203,to=1202) - to_upper(395)=(from=1205,to=1204) - to_upper(396)=(from=1207,to=1206) - to_upper(397)=(from=1209,to=1208) - to_upper(398)=(from=1211,to=1210) - to_upper(399)=(from=1213,to=1212) - to_upper(400)=(from=1215,to=1214) - to_upper(401)=(from=1218,to=1217) - to_upper(402)=(from=1220,to=1219) - to_upper(403)=(from=1222,to=1221) - to_upper(404)=(from=1224,to=1223) - to_upper(405)=(from=1226,to=1225) - to_upper(406)=(from=1228,to=1227) - to_upper(407)=(from=1230,to=1229) - to_upper(408)=(from=1231,to=1216) - to_upper(409)=(from=1233,to=1232) - to_upper(410)=(from=1235,to=1234) - to_upper(411)=(from=1237,to=1236) - to_upper(412)=(from=1239,to=1238) - to_upper(413)=(from=1241,to=1240) - to_upper(414)=(from=1243,to=1242) - to_upper(415)=(from=1245,to=1244) - to_upper(416)=(from=1247,to=1246) - to_upper(417)=(from=1249,to=1248) - to_upper(418)=(from=1251,to=1250) - to_upper(419)=(from=1253,to=1252) - to_upper(420)=(from=1255,to=1254) - to_upper(421)=(from=1257,to=1256) - to_upper(422)=(from=1259,to=1258) - to_upper(423)=(from=1261,to=1260) - to_upper(424)=(from=1263,to=1262) - to_upper(425)=(from=1265,to=1264) - to_upper(426)=(from=1267,to=1266) - to_upper(427)=(from=1269,to=1268) - to_upper(428)=(from=1271,to=1270) - to_upper(429)=(from=1273,to=1272) - to_upper(430)=(from=1275,to=1274) - to_upper(431)=(from=1277,to=1276) - to_upper(432)=(from=1279,to=1278) - to_upper(433)=(from=1281,to=1280) - to_upper(434)=(from=1283,to=1282) - to_upper(435)=(from=1285,to=1284) - to_upper(436)=(from=1287,to=1286) - to_upper(437)=(from=1289,to=1288) - to_upper(438)=(from=1291,to=1290) - to_upper(439)=(from=1293,to=1292) - to_upper(440)=(from=1295,to=1294) - to_upper(441)=(from=1297,to=1296) - to_upper(442)=(from=1299,to=1298) - to_upper(443)=(from=1301,to=1300) - to_upper(444)=(from=1303,to=1302) - to_upper(445)=(from=1305,to=1304) - to_upper(446)=(from=1307,to=1306) - to_upper(447)=(from=1309,to=1308) - to_upper(448)=(from=1311,to=1310) - to_upper(449)=(from=1313,to=1312) - to_upper(450)=(from=1315,to=1314) - to_upper(451)=(from=1317,to=1316) - to_upper(452)=(from=1319,to=1318) - to_upper(453)=(from=1321,to=1320) - to_upper(454)=(from=1323,to=1322) - to_upper(455)=(from=1325,to=1324) - to_upper(456)=(from=1327,to=1326) - to_upper(457)=(from=1377,to=1329) - to_upper(458)=(from=1378,to=1330) - to_upper(459)=(from=1379,to=1331) - to_upper(460)=(from=1380,to=1332) - to_upper(461)=(from=1381,to=1333) - to_upper(462)=(from=1382,to=1334) - to_upper(463)=(from=1383,to=1335) - to_upper(464)=(from=1384,to=1336) - to_upper(465)=(from=1385,to=1337) - to_upper(466)=(from=1386,to=1338) - to_upper(467)=(from=1387,to=1339) - to_upper(468)=(from=1388,to=1340) - to_upper(469)=(from=1389,to=1341) - to_upper(470)=(from=1390,to=1342) - to_upper(471)=(from=1391,to=1343) - to_upper(472)=(from=1392,to=1344) - to_upper(473)=(from=1393,to=1345) - to_upper(474)=(from=1394,to=1346) - to_upper(475)=(from=1395,to=1347) - to_upper(476)=(from=1396,to=1348) - to_upper(477)=(from=1397,to=1349) - to_upper(478)=(from=1398,to=1350) - to_upper(479)=(from=1399,to=1351) - to_upper(480)=(from=1400,to=1352) - to_upper(481)=(from=1401,to=1353) - to_upper(482)=(from=1402,to=1354) - to_upper(483)=(from=1403,to=1355) - to_upper(484)=(from=1404,to=1356) - to_upper(485)=(from=1405,to=1357) - to_upper(486)=(from=1406,to=1358) - to_upper(487)=(from=1407,to=1359) - to_upper(488)=(from=1408,to=1360) - to_upper(489)=(from=1409,to=1361) - to_upper(490)=(from=1410,to=1362) - to_upper(491)=(from=1411,to=1363) - to_upper(492)=(from=1412,to=1364) - to_upper(493)=(from=1413,to=1365) - to_upper(494)=(from=1414,to=1366) - to_upper(495)=(from=4304,to=7312) - to_upper(496)=(from=4305,to=7313) - to_upper(497)=(from=4306,to=7314) - to_upper(498)=(from=4307,to=7315) - to_upper(499)=(from=4308,to=7316) - to_upper(500)=(from=4309,to=7317) - to_upper(501)=(from=4310,to=7318) - to_upper(502)=(from=4311,to=7319) - to_upper(503)=(from=4312,to=7320) - to_upper(504)=(from=4313,to=7321) - to_upper(505)=(from=4314,to=7322) - to_upper(506)=(from=4315,to=7323) - to_upper(507)=(from=4316,to=7324) - to_upper(508)=(from=4317,to=7325) - to_upper(509)=(from=4318,to=7326) - to_upper(510)=(from=4319,to=7327) - to_upper(511)=(from=4320,to=7328) - to_upper(512)=(from=4321,to=7329) - to_upper(513)=(from=4322,to=7330) - to_upper(514)=(from=4323,to=7331) - to_upper(515)=(from=4324,to=7332) - to_upper(516)=(from=4325,to=7333) - to_upper(517)=(from=4326,to=7334) - to_upper(518)=(from=4327,to=7335) - to_upper(519)=(from=4328,to=7336) - to_upper(520)=(from=4329,to=7337) - to_upper(521)=(from=4330,to=7338) - to_upper(522)=(from=4331,to=7339) - to_upper(523)=(from=4332,to=7340) - to_upper(524)=(from=4333,to=7341) - to_upper(525)=(from=4334,to=7342) - to_upper(526)=(from=4335,to=7343) - to_upper(527)=(from=4336,to=7344) - to_upper(528)=(from=4337,to=7345) - to_upper(529)=(from=4338,to=7346) - to_upper(530)=(from=4339,to=7347) - to_upper(531)=(from=4340,to=7348) - to_upper(532)=(from=4341,to=7349) - to_upper(533)=(from=4342,to=7350) - to_upper(534)=(from=4343,to=7351) - to_upper(535)=(from=4344,to=7352) - to_upper(536)=(from=4345,to=7353) - to_upper(537)=(from=4346,to=7354) - to_upper(538)=(from=4349,to=7357) - to_upper(539)=(from=4350,to=7358) - to_upper(540)=(from=4351,to=7359) - to_upper(541)=(from=5112,to=5104) - to_upper(542)=(from=5113,to=5105) - to_upper(543)=(from=5114,to=5106) - to_upper(544)=(from=5115,to=5107) - to_upper(545)=(from=5116,to=5108) - to_upper(546)=(from=5117,to=5109) - to_upper(547)=(from=7296,to=1042) - to_upper(548)=(from=7297,to=1044) - to_upper(549)=(from=7298,to=1054) - to_upper(550)=(from=7299,to=1057) - to_upper(551)=(from=7300,to=1058) - to_upper(552)=(from=7301,to=1058) - to_upper(553)=(from=7302,to=1066) - to_upper(554)=(from=7303,to=1122) - to_upper(555)=(from=7304,to=42570) - to_upper(556)=(from=7545,to=42877) - to_upper(557)=(from=7549,to=11363) - to_upper(558)=(from=7566,to=42950) - to_upper(559)=(from=7681,to=7680) - to_upper(560)=(from=7683,to=7682) - to_upper(561)=(from=7685,to=7684) - to_upper(562)=(from=7687,to=7686) - to_upper(563)=(from=7689,to=7688) - to_upper(564)=(from=7691,to=7690) - to_upper(565)=(from=7693,to=7692) - to_upper(566)=(from=7695,to=7694) - to_upper(567)=(from=7697,to=7696) - to_upper(568)=(from=7699,to=7698) - to_upper(569)=(from=7701,to=7700) - to_upper(570)=(from=7703,to=7702) - to_upper(571)=(from=7705,to=7704) - to_upper(572)=(from=7707,to=7706) - to_upper(573)=(from=7709,to=7708) - to_upper(574)=(from=7711,to=7710) - to_upper(575)=(from=7713,to=7712) - to_upper(576)=(from=7715,to=7714) - to_upper(577)=(from=7717,to=7716) - to_upper(578)=(from=7719,to=7718) - to_upper(579)=(from=7721,to=7720) - to_upper(580)=(from=7723,to=7722) - to_upper(581)=(from=7725,to=7724) - to_upper(582)=(from=7727,to=7726) - to_upper(583)=(from=7729,to=7728) - to_upper(584)=(from=7731,to=7730) - to_upper(585)=(from=7733,to=7732) - to_upper(586)=(from=7735,to=7734) - to_upper(587)=(from=7737,to=7736) - to_upper(588)=(from=7739,to=7738) - to_upper(589)=(from=7741,to=7740) - to_upper(590)=(from=7743,to=7742) - to_upper(591)=(from=7745,to=7744) - to_upper(592)=(from=7747,to=7746) - to_upper(593)=(from=7749,to=7748) - to_upper(594)=(from=7751,to=7750) - to_upper(595)=(from=7753,to=7752) - to_upper(596)=(from=7755,to=7754) - to_upper(597)=(from=7757,to=7756) - to_upper(598)=(from=7759,to=7758) - to_upper(599)=(from=7761,to=7760) - to_upper(600)=(from=7763,to=7762) - to_upper(601)=(from=7765,to=7764) - to_upper(602)=(from=7767,to=7766) - to_upper(603)=(from=7769,to=7768) - to_upper(604)=(from=7771,to=7770) - to_upper(605)=(from=7773,to=7772) - to_upper(606)=(from=7775,to=7774) - to_upper(607)=(from=7777,to=7776) - to_upper(608)=(from=7779,to=7778) - to_upper(609)=(from=7781,to=7780) - to_upper(610)=(from=7783,to=7782) - to_upper(611)=(from=7785,to=7784) - to_upper(612)=(from=7787,to=7786) - to_upper(613)=(from=7789,to=7788) - to_upper(614)=(from=7791,to=7790) - to_upper(615)=(from=7793,to=7792) - to_upper(616)=(from=7795,to=7794) - to_upper(617)=(from=7797,to=7796) - to_upper(618)=(from=7799,to=7798) - to_upper(619)=(from=7801,to=7800) - to_upper(620)=(from=7803,to=7802) - to_upper(621)=(from=7805,to=7804) - to_upper(622)=(from=7807,to=7806) - to_upper(623)=(from=7809,to=7808) - to_upper(624)=(from=7811,to=7810) - to_upper(625)=(from=7813,to=7812) - to_upper(626)=(from=7815,to=7814) - to_upper(627)=(from=7817,to=7816) - to_upper(628)=(from=7819,to=7818) - to_upper(629)=(from=7821,to=7820) - to_upper(630)=(from=7823,to=7822) - to_upper(631)=(from=7825,to=7824) - to_upper(632)=(from=7827,to=7826) - to_upper(633)=(from=7829,to=7828) - to_upper(634)=(from=7835,to=7776) - to_upper(635)=(from=7841,to=7840) - to_upper(636)=(from=7843,to=7842) - to_upper(637)=(from=7845,to=7844) - to_upper(638)=(from=7847,to=7846) - to_upper(639)=(from=7849,to=7848) - to_upper(640)=(from=7851,to=7850) - to_upper(641)=(from=7853,to=7852) - to_upper(642)=(from=7855,to=7854) - to_upper(643)=(from=7857,to=7856) - to_upper(644)=(from=7859,to=7858) - to_upper(645)=(from=7861,to=7860) - to_upper(646)=(from=7863,to=7862) - to_upper(647)=(from=7865,to=7864) - to_upper(648)=(from=7867,to=7866) - to_upper(649)=(from=7869,to=7868) - to_upper(650)=(from=7871,to=7870) - to_upper(651)=(from=7873,to=7872) - to_upper(652)=(from=7875,to=7874) - to_upper(653)=(from=7877,to=7876) - to_upper(654)=(from=7879,to=7878) - to_upper(655)=(from=7881,to=7880) - to_upper(656)=(from=7883,to=7882) - to_upper(657)=(from=7885,to=7884) - to_upper(658)=(from=7887,to=7886) - to_upper(659)=(from=7889,to=7888) - to_upper(660)=(from=7891,to=7890) - to_upper(661)=(from=7893,to=7892) - to_upper(662)=(from=7895,to=7894) - to_upper(663)=(from=7897,to=7896) - to_upper(664)=(from=7899,to=7898) - to_upper(665)=(from=7901,to=7900) - to_upper(666)=(from=7903,to=7902) - to_upper(667)=(from=7905,to=7904) - to_upper(668)=(from=7907,to=7906) - to_upper(669)=(from=7909,to=7908) - to_upper(670)=(from=7911,to=7910) - to_upper(671)=(from=7913,to=7912) - to_upper(672)=(from=7915,to=7914) - to_upper(673)=(from=7917,to=7916) - to_upper(674)=(from=7919,to=7918) - to_upper(675)=(from=7921,to=7920) - to_upper(676)=(from=7923,to=7922) - to_upper(677)=(from=7925,to=7924) - to_upper(678)=(from=7927,to=7926) - to_upper(679)=(from=7929,to=7928) - to_upper(680)=(from=7931,to=7930) - to_upper(681)=(from=7933,to=7932) - to_upper(682)=(from=7935,to=7934) - to_upper(683)=(from=7936,to=7944) - to_upper(684)=(from=7937,to=7945) - to_upper(685)=(from=7938,to=7946) - to_upper(686)=(from=7939,to=7947) - to_upper(687)=(from=7940,to=7948) - to_upper(688)=(from=7941,to=7949) - to_upper(689)=(from=7942,to=7950) - to_upper(690)=(from=7943,to=7951) - to_upper(691)=(from=7952,to=7960) - to_upper(692)=(from=7953,to=7961) - to_upper(693)=(from=7954,to=7962) - to_upper(694)=(from=7955,to=7963) - to_upper(695)=(from=7956,to=7964) - to_upper(696)=(from=7957,to=7965) - to_upper(697)=(from=7968,to=7976) - to_upper(698)=(from=7969,to=7977) - to_upper(699)=(from=7970,to=7978) - to_upper(700)=(from=7971,to=7979) - to_upper(701)=(from=7972,to=7980) - to_upper(702)=(from=7973,to=7981) - to_upper(703)=(from=7974,to=7982) - to_upper(704)=(from=7975,to=7983) - to_upper(705)=(from=7984,to=7992) - to_upper(706)=(from=7985,to=7993) - to_upper(707)=(from=7986,to=7994) - to_upper(708)=(from=7987,to=7995) - to_upper(709)=(from=7988,to=7996) - to_upper(710)=(from=7989,to=7997) - to_upper(711)=(from=7990,to=7998) - to_upper(712)=(from=7991,to=7999) - to_upper(713)=(from=8000,to=8008) - to_upper(714)=(from=8001,to=8009) - to_upper(715)=(from=8002,to=8010) - to_upper(716)=(from=8003,to=8011) - to_upper(717)=(from=8004,to=8012) - to_upper(718)=(from=8005,to=8013) - to_upper(719)=(from=8017,to=8025) - to_upper(720)=(from=8019,to=8027) - to_upper(721)=(from=8021,to=8029) - to_upper(722)=(from=8023,to=8031) - to_upper(723)=(from=8032,to=8040) - to_upper(724)=(from=8033,to=8041) - to_upper(725)=(from=8034,to=8042) - to_upper(726)=(from=8035,to=8043) - to_upper(727)=(from=8036,to=8044) - to_upper(728)=(from=8037,to=8045) - to_upper(729)=(from=8038,to=8046) - to_upper(730)=(from=8039,to=8047) - to_upper(731)=(from=8048,to=8122) - to_upper(732)=(from=8049,to=8123) - to_upper(733)=(from=8050,to=8136) - to_upper(734)=(from=8051,to=8137) - to_upper(735)=(from=8052,to=8138) - to_upper(736)=(from=8053,to=8139) - to_upper(737)=(from=8054,to=8154) - to_upper(738)=(from=8055,to=8155) - to_upper(739)=(from=8056,to=8184) - to_upper(740)=(from=8057,to=8185) - to_upper(741)=(from=8058,to=8170) - to_upper(742)=(from=8059,to=8171) - to_upper(743)=(from=8060,to=8186) - to_upper(744)=(from=8061,to=8187) - to_upper(745)=(from=8064,to=8072) - to_upper(746)=(from=8065,to=8073) - to_upper(747)=(from=8066,to=8074) - to_upper(748)=(from=8067,to=8075) - to_upper(749)=(from=8068,to=8076) - to_upper(750)=(from=8069,to=8077) - to_upper(751)=(from=8070,to=8078) - to_upper(752)=(from=8071,to=8079) - to_upper(753)=(from=8080,to=8088) - to_upper(754)=(from=8081,to=8089) - to_upper(755)=(from=8082,to=8090) - to_upper(756)=(from=8083,to=8091) - to_upper(757)=(from=8084,to=8092) - to_upper(758)=(from=8085,to=8093) - to_upper(759)=(from=8086,to=8094) - to_upper(760)=(from=8087,to=8095) - to_upper(761)=(from=8096,to=8104) - to_upper(762)=(from=8097,to=8105) - to_upper(763)=(from=8098,to=8106) - to_upper(764)=(from=8099,to=8107) - to_upper(765)=(from=8100,to=8108) - to_upper(766)=(from=8101,to=8109) - to_upper(767)=(from=8102,to=8110) - to_upper(768)=(from=8103,to=8111) - to_upper(769)=(from=8112,to=8120) - to_upper(770)=(from=8113,to=8121) - to_upper(771)=(from=8115,to=8124) - to_upper(772)=(from=8126,to=921) - to_upper(773)=(from=8131,to=8140) - to_upper(774)=(from=8144,to=8152) - to_upper(775)=(from=8145,to=8153) - to_upper(776)=(from=8160,to=8168) - to_upper(777)=(from=8161,to=8169) - to_upper(778)=(from=8165,to=8172) - to_upper(779)=(from=8179,to=8188) - to_upper(780)=(from=8526,to=8498) - to_upper(781)=(from=8560,to=8544) - to_upper(782)=(from=8561,to=8545) - to_upper(783)=(from=8562,to=8546) - to_upper(784)=(from=8563,to=8547) - to_upper(785)=(from=8564,to=8548) - to_upper(786)=(from=8565,to=8549) - to_upper(787)=(from=8566,to=8550) - to_upper(788)=(from=8567,to=8551) - to_upper(789)=(from=8568,to=8552) - to_upper(790)=(from=8569,to=8553) - to_upper(791)=(from=8570,to=8554) - to_upper(792)=(from=8571,to=8555) - to_upper(793)=(from=8572,to=8556) - to_upper(794)=(from=8573,to=8557) - to_upper(795)=(from=8574,to=8558) - to_upper(796)=(from=8575,to=8559) - to_upper(797)=(from=8580,to=8579) - to_upper(798)=(from=9424,to=9398) - to_upper(799)=(from=9425,to=9399) - to_upper(800)=(from=9426,to=9400) - to_upper(801)=(from=9427,to=9401) - to_upper(802)=(from=9428,to=9402) - to_upper(803)=(from=9429,to=9403) - to_upper(804)=(from=9430,to=9404) - to_upper(805)=(from=9431,to=9405) - to_upper(806)=(from=9432,to=9406) - to_upper(807)=(from=9433,to=9407) - to_upper(808)=(from=9434,to=9408) - to_upper(809)=(from=9435,to=9409) - to_upper(810)=(from=9436,to=9410) - to_upper(811)=(from=9437,to=9411) - to_upper(812)=(from=9438,to=9412) - to_upper(813)=(from=9439,to=9413) - to_upper(814)=(from=9440,to=9414) - to_upper(815)=(from=9441,to=9415) - to_upper(816)=(from=9442,to=9416) - to_upper(817)=(from=9443,to=9417) - to_upper(818)=(from=9444,to=9418) - to_upper(819)=(from=9445,to=9419) - to_upper(820)=(from=9446,to=9420) - to_upper(821)=(from=9447,to=9421) - to_upper(822)=(from=9448,to=9422) - to_upper(823)=(from=9449,to=9423) - to_upper(824)=(from=11312,to=11264) - to_upper(825)=(from=11313,to=11265) - to_upper(826)=(from=11314,to=11266) - to_upper(827)=(from=11315,to=11267) - to_upper(828)=(from=11316,to=11268) - to_upper(829)=(from=11317,to=11269) - to_upper(830)=(from=11318,to=11270) - to_upper(831)=(from=11319,to=11271) - to_upper(832)=(from=11320,to=11272) - to_upper(833)=(from=11321,to=11273) - to_upper(834)=(from=11322,to=11274) - to_upper(835)=(from=11323,to=11275) - to_upper(836)=(from=11324,to=11276) - to_upper(837)=(from=11325,to=11277) - to_upper(838)=(from=11326,to=11278) - to_upper(839)=(from=11327,to=11279) - to_upper(840)=(from=11328,to=11280) - to_upper(841)=(from=11329,to=11281) - to_upper(842)=(from=11330,to=11282) - to_upper(843)=(from=11331,to=11283) - to_upper(844)=(from=11332,to=11284) - to_upper(845)=(from=11333,to=11285) - to_upper(846)=(from=11334,to=11286) - to_upper(847)=(from=11335,to=11287) - to_upper(848)=(from=11336,to=11288) - to_upper(849)=(from=11337,to=11289) - to_upper(850)=(from=11338,to=11290) - to_upper(851)=(from=11339,to=11291) - to_upper(852)=(from=11340,to=11292) - to_upper(853)=(from=11341,to=11293) - to_upper(854)=(from=11342,to=11294) - to_upper(855)=(from=11343,to=11295) - to_upper(856)=(from=11344,to=11296) - to_upper(857)=(from=11345,to=11297) - to_upper(858)=(from=11346,to=11298) - to_upper(859)=(from=11347,to=11299) - to_upper(860)=(from=11348,to=11300) - to_upper(861)=(from=11349,to=11301) - to_upper(862)=(from=11350,to=11302) - to_upper(863)=(from=11351,to=11303) - to_upper(864)=(from=11352,to=11304) - to_upper(865)=(from=11353,to=11305) - to_upper(866)=(from=11354,to=11306) - to_upper(867)=(from=11355,to=11307) - to_upper(868)=(from=11356,to=11308) - to_upper(869)=(from=11357,to=11309) - to_upper(870)=(from=11358,to=11310) - to_upper(871)=(from=11361,to=11360) - to_upper(872)=(from=11365,to=570) - to_upper(873)=(from=11366,to=574) - to_upper(874)=(from=11368,to=11367) - to_upper(875)=(from=11370,to=11369) - to_upper(876)=(from=11372,to=11371) - to_upper(877)=(from=11379,to=11378) - to_upper(878)=(from=11382,to=11381) - to_upper(879)=(from=11393,to=11392) - to_upper(880)=(from=11395,to=11394) - to_upper(881)=(from=11397,to=11396) - to_upper(882)=(from=11399,to=11398) - to_upper(883)=(from=11401,to=11400) - to_upper(884)=(from=11403,to=11402) - to_upper(885)=(from=11405,to=11404) - to_upper(886)=(from=11407,to=11406) - to_upper(887)=(from=11409,to=11408) - to_upper(888)=(from=11411,to=11410) - to_upper(889)=(from=11413,to=11412) - to_upper(890)=(from=11415,to=11414) - to_upper(891)=(from=11417,to=11416) - to_upper(892)=(from=11419,to=11418) - to_upper(893)=(from=11421,to=11420) - to_upper(894)=(from=11423,to=11422) - to_upper(895)=(from=11425,to=11424) - to_upper(896)=(from=11427,to=11426) - to_upper(897)=(from=11429,to=11428) - to_upper(898)=(from=11431,to=11430) - to_upper(899)=(from=11433,to=11432) - to_upper(900)=(from=11435,to=11434) - to_upper(901)=(from=11437,to=11436) - to_upper(902)=(from=11439,to=11438) - to_upper(903)=(from=11441,to=11440) - to_upper(904)=(from=11443,to=11442) - to_upper(905)=(from=11445,to=11444) - to_upper(906)=(from=11447,to=11446) - to_upper(907)=(from=11449,to=11448) - to_upper(908)=(from=11451,to=11450) - to_upper(909)=(from=11453,to=11452) - to_upper(910)=(from=11455,to=11454) - to_upper(911)=(from=11457,to=11456) - to_upper(912)=(from=11459,to=11458) - to_upper(913)=(from=11461,to=11460) - to_upper(914)=(from=11463,to=11462) - to_upper(915)=(from=11465,to=11464) - to_upper(916)=(from=11467,to=11466) - to_upper(917)=(from=11469,to=11468) - to_upper(918)=(from=11471,to=11470) - to_upper(919)=(from=11473,to=11472) - to_upper(920)=(from=11475,to=11474) - to_upper(921)=(from=11477,to=11476) - to_upper(922)=(from=11479,to=11478) - to_upper(923)=(from=11481,to=11480) - to_upper(924)=(from=11483,to=11482) - to_upper(925)=(from=11485,to=11484) - to_upper(926)=(from=11487,to=11486) - to_upper(927)=(from=11489,to=11488) - to_upper(928)=(from=11491,to=11490) - to_upper(929)=(from=11500,to=11499) - to_upper(930)=(from=11502,to=11501) - to_upper(931)=(from=11507,to=11506) - to_upper(932)=(from=11520,to=4256) - to_upper(933)=(from=11521,to=4257) - to_upper(934)=(from=11522,to=4258) - to_upper(935)=(from=11523,to=4259) - to_upper(936)=(from=11524,to=4260) - to_upper(937)=(from=11525,to=4261) - to_upper(938)=(from=11526,to=4262) - to_upper(939)=(from=11527,to=4263) - to_upper(940)=(from=11528,to=4264) - to_upper(941)=(from=11529,to=4265) - to_upper(942)=(from=11530,to=4266) - to_upper(943)=(from=11531,to=4267) - to_upper(944)=(from=11532,to=4268) - to_upper(945)=(from=11533,to=4269) - to_upper(946)=(from=11534,to=4270) - to_upper(947)=(from=11535,to=4271) - to_upper(948)=(from=11536,to=4272) - to_upper(949)=(from=11537,to=4273) - to_upper(950)=(from=11538,to=4274) - to_upper(951)=(from=11539,to=4275) - to_upper(952)=(from=11540,to=4276) - to_upper(953)=(from=11541,to=4277) - to_upper(954)=(from=11542,to=4278) - to_upper(955)=(from=11543,to=4279) - to_upper(956)=(from=11544,to=4280) - to_upper(957)=(from=11545,to=4281) - to_upper(958)=(from=11546,to=4282) - to_upper(959)=(from=11547,to=4283) - to_upper(960)=(from=11548,to=4284) - to_upper(961)=(from=11549,to=4285) - to_upper(962)=(from=11550,to=4286) - to_upper(963)=(from=11551,to=4287) - to_upper(964)=(from=11552,to=4288) - to_upper(965)=(from=11553,to=4289) - to_upper(966)=(from=11554,to=4290) - to_upper(967)=(from=11555,to=4291) - to_upper(968)=(from=11556,to=4292) - to_upper(969)=(from=11557,to=4293) - to_upper(970)=(from=11559,to=4295) - to_upper(971)=(from=11565,to=4301) - to_upper(972)=(from=42561,to=42560) - to_upper(973)=(from=42563,to=42562) - to_upper(974)=(from=42565,to=42564) - to_upper(975)=(from=42567,to=42566) - to_upper(976)=(from=42569,to=42568) - to_upper(977)=(from=42571,to=42570) - to_upper(978)=(from=42573,to=42572) - to_upper(979)=(from=42575,to=42574) - to_upper(980)=(from=42577,to=42576) - to_upper(981)=(from=42579,to=42578) - to_upper(982)=(from=42581,to=42580) - to_upper(983)=(from=42583,to=42582) - to_upper(984)=(from=42585,to=42584) - to_upper(985)=(from=42587,to=42586) - to_upper(986)=(from=42589,to=42588) - to_upper(987)=(from=42591,to=42590) - to_upper(988)=(from=42593,to=42592) - to_upper(989)=(from=42595,to=42594) - to_upper(990)=(from=42597,to=42596) - to_upper(991)=(from=42599,to=42598) - to_upper(992)=(from=42601,to=42600) - to_upper(993)=(from=42603,to=42602) - to_upper(994)=(from=42605,to=42604) - to_upper(995)=(from=42625,to=42624) - to_upper(996)=(from=42627,to=42626) - to_upper(997)=(from=42629,to=42628) - to_upper(998)=(from=42631,to=42630) - to_upper(999)=(from=42633,to=42632) - to_upper(1000)=(from=42635,to=42634) - to_upper(1001)=(from=42637,to=42636) - to_upper(1002)=(from=42639,to=42638) - to_upper(1003)=(from=42641,to=42640) - to_upper(1004)=(from=42643,to=42642) - to_upper(1005)=(from=42645,to=42644) - to_upper(1006)=(from=42647,to=42646) - to_upper(1007)=(from=42649,to=42648) - to_upper(1008)=(from=42651,to=42650) - to_upper(1009)=(from=42787,to=42786) - to_upper(1010)=(from=42789,to=42788) - to_upper(1011)=(from=42791,to=42790) - to_upper(1012)=(from=42793,to=42792) - to_upper(1013)=(from=42795,to=42794) - to_upper(1014)=(from=42797,to=42796) - to_upper(1015)=(from=42799,to=42798) - to_upper(1016)=(from=42803,to=42802) - to_upper(1017)=(from=42805,to=42804) - to_upper(1018)=(from=42807,to=42806) - to_upper(1019)=(from=42809,to=42808) - to_upper(1020)=(from=42811,to=42810) - to_upper(1021)=(from=42813,to=42812) - to_upper(1022)=(from=42815,to=42814) - to_upper(1023)=(from=42817,to=42816) - to_upper(1024)=(from=42819,to=42818) - to_upper(1025)=(from=42821,to=42820) - to_upper(1026)=(from=42823,to=42822) - to_upper(1027)=(from=42825,to=42824) - to_upper(1028)=(from=42827,to=42826) - to_upper(1029)=(from=42829,to=42828) - to_upper(1030)=(from=42831,to=42830) - to_upper(1031)=(from=42833,to=42832) - to_upper(1032)=(from=42835,to=42834) - to_upper(1033)=(from=42837,to=42836) - to_upper(1034)=(from=42839,to=42838) - to_upper(1035)=(from=42841,to=42840) - to_upper(1036)=(from=42843,to=42842) - to_upper(1037)=(from=42845,to=42844) - to_upper(1038)=(from=42847,to=42846) - to_upper(1039)=(from=42849,to=42848) - to_upper(1040)=(from=42851,to=42850) - to_upper(1041)=(from=42853,to=42852) - to_upper(1042)=(from=42855,to=42854) - to_upper(1043)=(from=42857,to=42856) - to_upper(1044)=(from=42859,to=42858) - to_upper(1045)=(from=42861,to=42860) - to_upper(1046)=(from=42863,to=42862) - to_upper(1047)=(from=42874,to=42873) - to_upper(1048)=(from=42876,to=42875) - to_upper(1049)=(from=42879,to=42878) - to_upper(1050)=(from=42881,to=42880) - to_upper(1051)=(from=42883,to=42882) - to_upper(1052)=(from=42885,to=42884) - to_upper(1053)=(from=42887,to=42886) - to_upper(1054)=(from=42892,to=42891) - to_upper(1055)=(from=42897,to=42896) - to_upper(1056)=(from=42899,to=42898) - to_upper(1057)=(from=42900,to=42948) - to_upper(1058)=(from=42903,to=42902) - to_upper(1059)=(from=42905,to=42904) - to_upper(1060)=(from=42907,to=42906) - to_upper(1061)=(from=42909,to=42908) - to_upper(1062)=(from=42911,to=42910) - to_upper(1063)=(from=42913,to=42912) - to_upper(1064)=(from=42915,to=42914) - to_upper(1065)=(from=42917,to=42916) - to_upper(1066)=(from=42919,to=42918) - to_upper(1067)=(from=42921,to=42920) - to_upper(1068)=(from=42933,to=42932) - to_upper(1069)=(from=42935,to=42934) - to_upper(1070)=(from=42937,to=42936) - to_upper(1071)=(from=42939,to=42938) - to_upper(1072)=(from=42941,to=42940) - to_upper(1073)=(from=42943,to=42942) - to_upper(1074)=(from=42947,to=42946) - to_upper(1075)=(from=42952,to=42951) - to_upper(1076)=(from=42954,to=42953) - to_upper(1077)=(from=42998,to=42997) - to_upper(1078)=(from=43859,to=42931) - to_upper(1079)=(from=43888,to=5024) - to_upper(1080)=(from=43889,to=5025) - to_upper(1081)=(from=43890,to=5026) - to_upper(1082)=(from=43891,to=5027) - to_upper(1083)=(from=43892,to=5028) - to_upper(1084)=(from=43893,to=5029) - to_upper(1085)=(from=43894,to=5030) - to_upper(1086)=(from=43895,to=5031) - to_upper(1087)=(from=43896,to=5032) - to_upper(1088)=(from=43897,to=5033) - to_upper(1089)=(from=43898,to=5034) - to_upper(1090)=(from=43899,to=5035) - to_upper(1091)=(from=43900,to=5036) - to_upper(1092)=(from=43901,to=5037) - to_upper(1093)=(from=43902,to=5038) - to_upper(1094)=(from=43903,to=5039) - to_upper(1095)=(from=43904,to=5040) - to_upper(1096)=(from=43905,to=5041) - to_upper(1097)=(from=43906,to=5042) - to_upper(1098)=(from=43907,to=5043) - to_upper(1099)=(from=43908,to=5044) - to_upper(1100)=(from=43909,to=5045) - to_upper(1101)=(from=43910,to=5046) - to_upper(1102)=(from=43911,to=5047) - to_upper(1103)=(from=43912,to=5048) - to_upper(1104)=(from=43913,to=5049) - to_upper(1105)=(from=43914,to=5050) - to_upper(1106)=(from=43915,to=5051) - to_upper(1107)=(from=43916,to=5052) - to_upper(1108)=(from=43917,to=5053) - to_upper(1109)=(from=43918,to=5054) - to_upper(1110)=(from=43919,to=5055) - to_upper(1111)=(from=43920,to=5056) - to_upper(1112)=(from=43921,to=5057) - to_upper(1113)=(from=43922,to=5058) - to_upper(1114)=(from=43923,to=5059) - to_upper(1115)=(from=43924,to=5060) - to_upper(1116)=(from=43925,to=5061) - to_upper(1117)=(from=43926,to=5062) - to_upper(1118)=(from=43927,to=5063) - to_upper(1119)=(from=43928,to=5064) - to_upper(1120)=(from=43929,to=5065) - to_upper(1121)=(from=43930,to=5066) - to_upper(1122)=(from=43931,to=5067) - to_upper(1123)=(from=43932,to=5068) - to_upper(1124)=(from=43933,to=5069) - to_upper(1125)=(from=43934,to=5070) - to_upper(1126)=(from=43935,to=5071) - to_upper(1127)=(from=43936,to=5072) - to_upper(1128)=(from=43937,to=5073) - to_upper(1129)=(from=43938,to=5074) - to_upper(1130)=(from=43939,to=5075) - to_upper(1131)=(from=43940,to=5076) - to_upper(1132)=(from=43941,to=5077) - to_upper(1133)=(from=43942,to=5078) - to_upper(1134)=(from=43943,to=5079) - to_upper(1135)=(from=43944,to=5080) - to_upper(1136)=(from=43945,to=5081) - to_upper(1137)=(from=43946,to=5082) - to_upper(1138)=(from=43947,to=5083) - to_upper(1139)=(from=43948,to=5084) - to_upper(1140)=(from=43949,to=5085) - to_upper(1141)=(from=43950,to=5086) - to_upper(1142)=(from=43951,to=5087) - to_upper(1143)=(from=43952,to=5088) - to_upper(1144)=(from=43953,to=5089) - to_upper(1145)=(from=43954,to=5090) - to_upper(1146)=(from=43955,to=5091) - to_upper(1147)=(from=43956,to=5092) - to_upper(1148)=(from=43957,to=5093) - to_upper(1149)=(from=43958,to=5094) - to_upper(1150)=(from=43959,to=5095) - to_upper(1151)=(from=43960,to=5096) - to_upper(1152)=(from=43961,to=5097) - to_upper(1153)=(from=43962,to=5098) - to_upper(1154)=(from=43963,to=5099) - to_upper(1155)=(from=43964,to=5100) - to_upper(1156)=(from=43965,to=5101) - to_upper(1157)=(from=43966,to=5102) - to_upper(1158)=(from=43967,to=5103) - to_upper(1159)=(from=65345,to=65313) - to_upper(1160)=(from=65346,to=65314) - to_upper(1161)=(from=65347,to=65315) - to_upper(1162)=(from=65348,to=65316) - to_upper(1163)=(from=65349,to=65317) - to_upper(1164)=(from=65350,to=65318) - to_upper(1165)=(from=65351,to=65319) - to_upper(1166)=(from=65352,to=65320) - to_upper(1167)=(from=65353,to=65321) - to_upper(1168)=(from=65354,to=65322) - to_upper(1169)=(from=65355,to=65323) - to_upper(1170)=(from=65356,to=65324) - to_upper(1171)=(from=65357,to=65325) - to_upper(1172)=(from=65358,to=65326) - to_upper(1173)=(from=65359,to=65327) - to_upper(1174)=(from=65360,to=65328) - to_upper(1175)=(from=65361,to=65329) - to_upper(1176)=(from=65362,to=65330) - to_upper(1177)=(from=65363,to=65331) - to_upper(1178)=(from=65364,to=65332) - to_upper(1179)=(from=65365,to=65333) - to_upper(1180)=(from=65366,to=65334) - to_upper(1181)=(from=65367,to=65335) - to_upper(1182)=(from=65368,to=65336) - to_upper(1183)=(from=65369,to=65337) - to_upper(1184)=(from=65370,to=65338) - to_upper(1185)=(from=66600,to=66560) - to_upper(1186)=(from=66601,to=66561) - to_upper(1187)=(from=66602,to=66562) - to_upper(1188)=(from=66603,to=66563) - to_upper(1189)=(from=66604,to=66564) - to_upper(1190)=(from=66605,to=66565) - to_upper(1191)=(from=66606,to=66566) - to_upper(1192)=(from=66607,to=66567) - to_upper(1193)=(from=66608,to=66568) - to_upper(1194)=(from=66609,to=66569) - to_upper(1195)=(from=66610,to=66570) - to_upper(1196)=(from=66611,to=66571) - to_upper(1197)=(from=66612,to=66572) - to_upper(1198)=(from=66613,to=66573) - to_upper(1199)=(from=66614,to=66574) - to_upper(1200)=(from=66615,to=66575) - to_upper(1201)=(from=66616,to=66576) - to_upper(1202)=(from=66617,to=66577) - to_upper(1203)=(from=66618,to=66578) - to_upper(1204)=(from=66619,to=66579) - to_upper(1205)=(from=66620,to=66580) - to_upper(1206)=(from=66621,to=66581) - to_upper(1207)=(from=66622,to=66582) - to_upper(1208)=(from=66623,to=66583) - to_upper(1209)=(from=66624,to=66584) - to_upper(1210)=(from=66625,to=66585) - to_upper(1211)=(from=66626,to=66586) - to_upper(1212)=(from=66627,to=66587) - to_upper(1213)=(from=66628,to=66588) - to_upper(1214)=(from=66629,to=66589) - to_upper(1215)=(from=66630,to=66590) - to_upper(1216)=(from=66631,to=66591) - to_upper(1217)=(from=66632,to=66592) - to_upper(1218)=(from=66633,to=66593) - to_upper(1219)=(from=66634,to=66594) - to_upper(1220)=(from=66635,to=66595) - to_upper(1221)=(from=66636,to=66596) - to_upper(1222)=(from=66637,to=66597) - to_upper(1223)=(from=66638,to=66598) - to_upper(1224)=(from=66639,to=66599) - to_upper(1225)=(from=66776,to=66736) - to_upper(1226)=(from=66777,to=66737) - to_upper(1227)=(from=66778,to=66738) - to_upper(1228)=(from=66779,to=66739) - to_upper(1229)=(from=66780,to=66740) - to_upper(1230)=(from=66781,to=66741) - to_upper(1231)=(from=66782,to=66742) - to_upper(1232)=(from=66783,to=66743) - to_upper(1233)=(from=66784,to=66744) - to_upper(1234)=(from=66785,to=66745) - to_upper(1235)=(from=66786,to=66746) - to_upper(1236)=(from=66787,to=66747) - to_upper(1237)=(from=66788,to=66748) - to_upper(1238)=(from=66789,to=66749) - to_upper(1239)=(from=66790,to=66750) - to_upper(1240)=(from=66791,to=66751) - to_upper(1241)=(from=66792,to=66752) - to_upper(1242)=(from=66793,to=66753) - to_upper(1243)=(from=66794,to=66754) - to_upper(1244)=(from=66795,to=66755) - to_upper(1245)=(from=66796,to=66756) - to_upper(1246)=(from=66797,to=66757) - to_upper(1247)=(from=66798,to=66758) - to_upper(1248)=(from=66799,to=66759) - to_upper(1249)=(from=66800,to=66760) - to_upper(1250)=(from=66801,to=66761) - to_upper(1251)=(from=66802,to=66762) - to_upper(1252)=(from=66803,to=66763) - to_upper(1253)=(from=66804,to=66764) - to_upper(1254)=(from=66805,to=66765) - to_upper(1255)=(from=66806,to=66766) - to_upper(1256)=(from=66807,to=66767) - to_upper(1257)=(from=66808,to=66768) - to_upper(1258)=(from=66809,to=66769) - to_upper(1259)=(from=66810,to=66770) - to_upper(1260)=(from=66811,to=66771) - to_upper(1261)=(from=68800,to=68736) - to_upper(1262)=(from=68801,to=68737) - to_upper(1263)=(from=68802,to=68738) - to_upper(1264)=(from=68803,to=68739) - to_upper(1265)=(from=68804,to=68740) - to_upper(1266)=(from=68805,to=68741) - to_upper(1267)=(from=68806,to=68742) - to_upper(1268)=(from=68807,to=68743) - to_upper(1269)=(from=68808,to=68744) - to_upper(1270)=(from=68809,to=68745) - to_upper(1271)=(from=68810,to=68746) - to_upper(1272)=(from=68811,to=68747) - to_upper(1273)=(from=68812,to=68748) - to_upper(1274)=(from=68813,to=68749) - to_upper(1275)=(from=68814,to=68750) - to_upper(1276)=(from=68815,to=68751) - to_upper(1277)=(from=68816,to=68752) - to_upper(1278)=(from=68817,to=68753) - to_upper(1279)=(from=68818,to=68754) - to_upper(1280)=(from=68819,to=68755) - to_upper(1281)=(from=68820,to=68756) - to_upper(1282)=(from=68821,to=68757) - to_upper(1283)=(from=68822,to=68758) - to_upper(1284)=(from=68823,to=68759) - to_upper(1285)=(from=68824,to=68760) - to_upper(1286)=(from=68825,to=68761) - to_upper(1287)=(from=68826,to=68762) - to_upper(1288)=(from=68827,to=68763) - to_upper(1289)=(from=68828,to=68764) - to_upper(1290)=(from=68829,to=68765) - to_upper(1291)=(from=68830,to=68766) - to_upper(1292)=(from=68831,to=68767) - to_upper(1293)=(from=68832,to=68768) - to_upper(1294)=(from=68833,to=68769) - to_upper(1295)=(from=68834,to=68770) - to_upper(1296)=(from=68835,to=68771) - to_upper(1297)=(from=68836,to=68772) - to_upper(1298)=(from=68837,to=68773) - to_upper(1299)=(from=68838,to=68774) - to_upper(1300)=(from=68839,to=68775) - to_upper(1301)=(from=68840,to=68776) - to_upper(1302)=(from=68841,to=68777) - to_upper(1303)=(from=68842,to=68778) - to_upper(1304)=(from=68843,to=68779) - to_upper(1305)=(from=68844,to=68780) - to_upper(1306)=(from=68845,to=68781) - to_upper(1307)=(from=68846,to=68782) - to_upper(1308)=(from=68847,to=68783) - to_upper(1309)=(from=68848,to=68784) - to_upper(1310)=(from=68849,to=68785) - to_upper(1311)=(from=68850,to=68786) - to_upper(1312)=(from=71872,to=71840) - to_upper(1313)=(from=71873,to=71841) - to_upper(1314)=(from=71874,to=71842) - to_upper(1315)=(from=71875,to=71843) - to_upper(1316)=(from=71876,to=71844) - to_upper(1317)=(from=71877,to=71845) - to_upper(1318)=(from=71878,to=71846) - to_upper(1319)=(from=71879,to=71847) - to_upper(1320)=(from=71880,to=71848) - to_upper(1321)=(from=71881,to=71849) - to_upper(1322)=(from=71882,to=71850) - to_upper(1323)=(from=71883,to=71851) - to_upper(1324)=(from=71884,to=71852) - to_upper(1325)=(from=71885,to=71853) - to_upper(1326)=(from=71886,to=71854) - to_upper(1327)=(from=71887,to=71855) - to_upper(1328)=(from=71888,to=71856) - to_upper(1329)=(from=71889,to=71857) - to_upper(1330)=(from=71890,to=71858) - to_upper(1331)=(from=71891,to=71859) - to_upper(1332)=(from=71892,to=71860) - to_upper(1333)=(from=71893,to=71861) - to_upper(1334)=(from=71894,to=71862) - to_upper(1335)=(from=71895,to=71863) - to_upper(1336)=(from=71896,to=71864) - to_upper(1337)=(from=71897,to=71865) - to_upper(1338)=(from=71898,to=71866) - to_upper(1339)=(from=71899,to=71867) - to_upper(1340)=(from=71900,to=71868) - to_upper(1341)=(from=71901,to=71869) - to_upper(1342)=(from=71902,to=71870) - to_upper(1343)=(from=71903,to=71871) - to_upper(1344)=(from=93792,to=93760) - to_upper(1345)=(from=93793,to=93761) - to_upper(1346)=(from=93794,to=93762) - to_upper(1347)=(from=93795,to=93763) - to_upper(1348)=(from=93796,to=93764) - to_upper(1349)=(from=93797,to=93765) - to_upper(1350)=(from=93798,to=93766) - to_upper(1351)=(from=93799,to=93767) - to_upper(1352)=(from=93800,to=93768) - to_upper(1353)=(from=93801,to=93769) - to_upper(1354)=(from=93802,to=93770) - to_upper(1355)=(from=93803,to=93771) - to_upper(1356)=(from=93804,to=93772) - to_upper(1357)=(from=93805,to=93773) - to_upper(1358)=(from=93806,to=93774) - to_upper(1359)=(from=93807,to=93775) - to_upper(1360)=(from=93808,to=93776) - to_upper(1361)=(from=93809,to=93777) - to_upper(1362)=(from=93810,to=93778) - to_upper(1363)=(from=93811,to=93779) - to_upper(1364)=(from=93812,to=93780) - to_upper(1365)=(from=93813,to=93781) - to_upper(1366)=(from=93814,to=93782) - to_upper(1367)=(from=93815,to=93783) - to_upper(1368)=(from=93816,to=93784) - to_upper(1369)=(from=93817,to=93785) - to_upper(1370)=(from=93818,to=93786) - to_upper(1371)=(from=93819,to=93787) - to_upper(1372)=(from=93820,to=93788) - to_upper(1373)=(from=93821,to=93789) - to_upper(1374)=(from=93822,to=93790) - to_upper(1375)=(from=93823,to=93791) - to_upper(1376)=(from=125218,to=125184) - to_upper(1377)=(from=125219,to=125185) - to_upper(1378)=(from=125220,to=125186) - to_upper(1379)=(from=125221,to=125187) - to_upper(1380)=(from=125222,to=125188) - to_upper(1381)=(from=125223,to=125189) - to_upper(1382)=(from=125224,to=125190) - to_upper(1383)=(from=125225,to=125191) - to_upper(1384)=(from=125226,to=125192) - to_upper(1385)=(from=125227,to=125193) - to_upper(1386)=(from=125228,to=125194) - to_upper(1387)=(from=125229,to=125195) - to_upper(1388)=(from=125230,to=125196) - to_upper(1389)=(from=125231,to=125197) - to_upper(1390)=(from=125232,to=125198) - to_upper(1391)=(from=125233,to=125199) - to_upper(1392)=(from=125234,to=125200) - to_upper(1393)=(from=125235,to=125201) - to_upper(1394)=(from=125236,to=125202) - to_upper(1395)=(from=125237,to=125203) - to_upper(1396)=(from=125238,to=125204) - to_upper(1397)=(from=125239,to=125205) - to_upper(1398)=(from=125240,to=125206) - to_upper(1399)=(from=125241,to=125207) - to_upper(1400)=(from=125242,to=125208) - to_upper(1401)=(from=125243,to=125209) - to_upper(1402)=(from=125244,to=125210) - to_upper(1403)=(from=125245,to=125211) - to_upper(1404)=(from=125246,to=125212) - to_upper(1405)=(from=125247,to=125213) - to_upper(1406)=(from=125248,to=125214) - to_upper(1407)=(from=125249,to=125215) - to_upper(1408)=(from=125250,to=125216) - to_upper(1409)=(from=125251,to=125217) - to_title(0)=(from=97,to=65) - to_title(1)=(from=98,to=66) - to_title(2)=(from=99,to=67) - to_title(3)=(from=100,to=68) - to_title(4)=(from=101,to=69) - to_title(5)=(from=102,to=70) - to_title(6)=(from=103,to=71) - to_title(7)=(from=104,to=72) - to_title(8)=(from=105,to=73) - to_title(9)=(from=106,to=74) - to_title(10)=(from=107,to=75) - to_title(11)=(from=108,to=76) - to_title(12)=(from=109,to=77) - to_title(13)=(from=110,to=78) - to_title(14)=(from=111,to=79) - to_title(15)=(from=112,to=80) - to_title(16)=(from=113,to=81) - to_title(17)=(from=114,to=82) - to_title(18)=(from=115,to=83) - to_title(19)=(from=116,to=84) - to_title(20)=(from=117,to=85) - to_title(21)=(from=118,to=86) - to_title(22)=(from=119,to=87) - to_title(23)=(from=120,to=88) - to_title(24)=(from=121,to=89) - to_title(25)=(from=122,to=90) - to_title(26)=(from=181,to=924) - to_title(27)=(from=224,to=192) - to_title(28)=(from=225,to=193) - to_title(29)=(from=226,to=194) - to_title(30)=(from=227,to=195) - to_title(31)=(from=228,to=196) - to_title(32)=(from=229,to=197) - to_title(33)=(from=230,to=198) - to_title(34)=(from=231,to=199) - to_title(35)=(from=232,to=200) - to_title(36)=(from=233,to=201) - to_title(37)=(from=234,to=202) - to_title(38)=(from=235,to=203) - to_title(39)=(from=236,to=204) - to_title(40)=(from=237,to=205) - to_title(41)=(from=238,to=206) - to_title(42)=(from=239,to=207) - to_title(43)=(from=240,to=208) - to_title(44)=(from=241,to=209) - to_title(45)=(from=242,to=210) - to_title(46)=(from=243,to=211) - to_title(47)=(from=244,to=212) - to_title(48)=(from=245,to=213) - to_title(49)=(from=246,to=214) - to_title(50)=(from=248,to=216) - to_title(51)=(from=249,to=217) - to_title(52)=(from=250,to=218) - to_title(53)=(from=251,to=219) - to_title(54)=(from=252,to=220) - to_title(55)=(from=253,to=221) - to_title(56)=(from=254,to=222) - to_title(57)=(from=255,to=376) - to_title(58)=(from=257,to=256) - to_title(59)=(from=259,to=258) - to_title(60)=(from=261,to=260) - to_title(61)=(from=263,to=262) - to_title(62)=(from=265,to=264) - to_title(63)=(from=267,to=266) - to_title(64)=(from=269,to=268) - to_title(65)=(from=271,to=270) - to_title(66)=(from=273,to=272) - to_title(67)=(from=275,to=274) - to_title(68)=(from=277,to=276) - to_title(69)=(from=279,to=278) - to_title(70)=(from=281,to=280) - to_title(71)=(from=283,to=282) - to_title(72)=(from=285,to=284) - to_title(73)=(from=287,to=286) - to_title(74)=(from=289,to=288) - to_title(75)=(from=291,to=290) - to_title(76)=(from=293,to=292) - to_title(77)=(from=295,to=294) - to_title(78)=(from=297,to=296) - to_title(79)=(from=299,to=298) - to_title(80)=(from=301,to=300) - to_title(81)=(from=303,to=302) - to_title(82)=(from=305,to=73) - to_title(83)=(from=307,to=306) - to_title(84)=(from=309,to=308) - to_title(85)=(from=311,to=310) - to_title(86)=(from=314,to=313) - to_title(87)=(from=316,to=315) - to_title(88)=(from=318,to=317) - to_title(89)=(from=320,to=319) - to_title(90)=(from=322,to=321) - to_title(91)=(from=324,to=323) - to_title(92)=(from=326,to=325) - to_title(93)=(from=328,to=327) - to_title(94)=(from=331,to=330) - to_title(95)=(from=333,to=332) - to_title(96)=(from=335,to=334) - to_title(97)=(from=337,to=336) - to_title(98)=(from=339,to=338) - to_title(99)=(from=341,to=340) - to_title(100)=(from=343,to=342) - to_title(101)=(from=345,to=344) - to_title(102)=(from=347,to=346) - to_title(103)=(from=349,to=348) - to_title(104)=(from=351,to=350) - to_title(105)=(from=353,to=352) - to_title(106)=(from=355,to=354) - to_title(107)=(from=357,to=356) - to_title(108)=(from=359,to=358) - to_title(109)=(from=361,to=360) - to_title(110)=(from=363,to=362) - to_title(111)=(from=365,to=364) - to_title(112)=(from=367,to=366) - to_title(113)=(from=369,to=368) - to_title(114)=(from=371,to=370) - to_title(115)=(from=373,to=372) - to_title(116)=(from=375,to=374) - to_title(117)=(from=378,to=377) - to_title(118)=(from=380,to=379) - to_title(119)=(from=382,to=381) - to_title(120)=(from=383,to=83) - to_title(121)=(from=384,to=579) - to_title(122)=(from=387,to=386) - to_title(123)=(from=389,to=388) - to_title(124)=(from=392,to=391) - to_title(125)=(from=396,to=395) - to_title(126)=(from=402,to=401) - to_title(127)=(from=405,to=502) - to_title(128)=(from=409,to=408) - to_title(129)=(from=410,to=573) - to_title(130)=(from=414,to=544) - to_title(131)=(from=417,to=416) - to_title(132)=(from=419,to=418) - to_title(133)=(from=421,to=420) - to_title(134)=(from=424,to=423) - to_title(135)=(from=429,to=428) - to_title(136)=(from=432,to=431) - to_title(137)=(from=436,to=435) - to_title(138)=(from=438,to=437) - to_title(139)=(from=441,to=440) - to_title(140)=(from=445,to=444) - to_title(141)=(from=447,to=503) - to_title(142)=(from=452,to=453) - to_title(143)=(from=453,to=453) - to_title(144)=(from=454,to=453) - to_title(145)=(from=455,to=456) - to_title(146)=(from=456,to=456) - to_title(147)=(from=457,to=456) - to_title(148)=(from=458,to=459) - to_title(149)=(from=459,to=459) - to_title(150)=(from=460,to=459) - to_title(151)=(from=462,to=461) - to_title(152)=(from=464,to=463) - to_title(153)=(from=466,to=465) - to_title(154)=(from=468,to=467) - to_title(155)=(from=470,to=469) - to_title(156)=(from=472,to=471) - to_title(157)=(from=474,to=473) - to_title(158)=(from=476,to=475) - to_title(159)=(from=477,to=398) - to_title(160)=(from=479,to=478) - to_title(161)=(from=481,to=480) - to_title(162)=(from=483,to=482) - to_title(163)=(from=485,to=484) - to_title(164)=(from=487,to=486) - to_title(165)=(from=489,to=488) - to_title(166)=(from=491,to=490) - to_title(167)=(from=493,to=492) - to_title(168)=(from=495,to=494) - to_title(169)=(from=497,to=498) - to_title(170)=(from=498,to=498) - to_title(171)=(from=499,to=498) - to_title(172)=(from=501,to=500) - to_title(173)=(from=505,to=504) - to_title(174)=(from=507,to=506) - to_title(175)=(from=509,to=508) - to_title(176)=(from=511,to=510) - to_title(177)=(from=513,to=512) - to_title(178)=(from=515,to=514) - to_title(179)=(from=517,to=516) - to_title(180)=(from=519,to=518) - to_title(181)=(from=521,to=520) - to_title(182)=(from=523,to=522) - to_title(183)=(from=525,to=524) - to_title(184)=(from=527,to=526) - to_title(185)=(from=529,to=528) - to_title(186)=(from=531,to=530) - to_title(187)=(from=533,to=532) - to_title(188)=(from=535,to=534) - to_title(189)=(from=537,to=536) - to_title(190)=(from=539,to=538) - to_title(191)=(from=541,to=540) - to_title(192)=(from=543,to=542) - to_title(193)=(from=547,to=546) - to_title(194)=(from=549,to=548) - to_title(195)=(from=551,to=550) - to_title(196)=(from=553,to=552) - to_title(197)=(from=555,to=554) - to_title(198)=(from=557,to=556) - to_title(199)=(from=559,to=558) - to_title(200)=(from=561,to=560) - to_title(201)=(from=563,to=562) - to_title(202)=(from=572,to=571) - to_title(203)=(from=575,to=11390) - to_title(204)=(from=576,to=11391) - to_title(205)=(from=578,to=577) - to_title(206)=(from=583,to=582) - to_title(207)=(from=585,to=584) - to_title(208)=(from=587,to=586) - to_title(209)=(from=589,to=588) - to_title(210)=(from=591,to=590) - to_title(211)=(from=592,to=11375) - to_title(212)=(from=593,to=11373) - to_title(213)=(from=594,to=11376) - to_title(214)=(from=595,to=385) - to_title(215)=(from=596,to=390) - to_title(216)=(from=598,to=393) - to_title(217)=(from=599,to=394) - to_title(218)=(from=601,to=399) - to_title(219)=(from=603,to=400) - to_title(220)=(from=604,to=42923) - to_title(221)=(from=608,to=403) - to_title(222)=(from=609,to=42924) - to_title(223)=(from=611,to=404) - to_title(224)=(from=613,to=42893) - to_title(225)=(from=614,to=42922) - to_title(226)=(from=616,to=407) - to_title(227)=(from=617,to=406) - to_title(228)=(from=618,to=42926) - to_title(229)=(from=619,to=11362) - to_title(230)=(from=620,to=42925) - to_title(231)=(from=623,to=412) - to_title(232)=(from=625,to=11374) - to_title(233)=(from=626,to=413) - to_title(234)=(from=629,to=415) - to_title(235)=(from=637,to=11364) - to_title(236)=(from=640,to=422) - to_title(237)=(from=642,to=42949) - to_title(238)=(from=643,to=425) - to_title(239)=(from=647,to=42929) - to_title(240)=(from=648,to=430) - to_title(241)=(from=649,to=580) - to_title(242)=(from=650,to=433) - to_title(243)=(from=651,to=434) - to_title(244)=(from=652,to=581) - to_title(245)=(from=658,to=439) - to_title(246)=(from=669,to=42930) - to_title(247)=(from=670,to=42928) - to_title(248)=(from=837,to=921) - to_title(249)=(from=881,to=880) - to_title(250)=(from=883,to=882) - to_title(251)=(from=887,to=886) - to_title(252)=(from=891,to=1021) - to_title(253)=(from=892,to=1022) - to_title(254)=(from=893,to=1023) - to_title(255)=(from=940,to=902) - to_title(256)=(from=941,to=904) - to_title(257)=(from=942,to=905) - to_title(258)=(from=943,to=906) - to_title(259)=(from=945,to=913) - to_title(260)=(from=946,to=914) - to_title(261)=(from=947,to=915) - to_title(262)=(from=948,to=916) - to_title(263)=(from=949,to=917) - to_title(264)=(from=950,to=918) - to_title(265)=(from=951,to=919) - to_title(266)=(from=952,to=920) - to_title(267)=(from=953,to=921) - to_title(268)=(from=954,to=922) - to_title(269)=(from=955,to=923) - to_title(270)=(from=956,to=924) - to_title(271)=(from=957,to=925) - to_title(272)=(from=958,to=926) - to_title(273)=(from=959,to=927) - to_title(274)=(from=960,to=928) - to_title(275)=(from=961,to=929) - to_title(276)=(from=962,to=931) - to_title(277)=(from=963,to=931) - to_title(278)=(from=964,to=932) - to_title(279)=(from=965,to=933) - to_title(280)=(from=966,to=934) - to_title(281)=(from=967,to=935) - to_title(282)=(from=968,to=936) - to_title(283)=(from=969,to=937) - to_title(284)=(from=970,to=938) - to_title(285)=(from=971,to=939) - to_title(286)=(from=972,to=908) - to_title(287)=(from=973,to=910) - to_title(288)=(from=974,to=911) - to_title(289)=(from=976,to=914) - to_title(290)=(from=977,to=920) - to_title(291)=(from=981,to=934) - to_title(292)=(from=982,to=928) - to_title(293)=(from=983,to=975) - to_title(294)=(from=985,to=984) - to_title(295)=(from=987,to=986) - to_title(296)=(from=989,to=988) - to_title(297)=(from=991,to=990) - to_title(298)=(from=993,to=992) - to_title(299)=(from=995,to=994) - to_title(300)=(from=997,to=996) - to_title(301)=(from=999,to=998) - to_title(302)=(from=1001,to=1000) - to_title(303)=(from=1003,to=1002) - to_title(304)=(from=1005,to=1004) - to_title(305)=(from=1007,to=1006) - to_title(306)=(from=1008,to=922) - to_title(307)=(from=1009,to=929) - to_title(308)=(from=1010,to=1017) - to_title(309)=(from=1011,to=895) - to_title(310)=(from=1013,to=917) - to_title(311)=(from=1016,to=1015) - to_title(312)=(from=1019,to=1018) - to_title(313)=(from=1072,to=1040) - to_title(314)=(from=1073,to=1041) - to_title(315)=(from=1074,to=1042) - to_title(316)=(from=1075,to=1043) - to_title(317)=(from=1076,to=1044) - to_title(318)=(from=1077,to=1045) - to_title(319)=(from=1078,to=1046) - to_title(320)=(from=1079,to=1047) - to_title(321)=(from=1080,to=1048) - to_title(322)=(from=1081,to=1049) - to_title(323)=(from=1082,to=1050) - to_title(324)=(from=1083,to=1051) - to_title(325)=(from=1084,to=1052) - to_title(326)=(from=1085,to=1053) - to_title(327)=(from=1086,to=1054) - to_title(328)=(from=1087,to=1055) - to_title(329)=(from=1088,to=1056) - to_title(330)=(from=1089,to=1057) - to_title(331)=(from=1090,to=1058) - to_title(332)=(from=1091,to=1059) - to_title(333)=(from=1092,to=1060) - to_title(334)=(from=1093,to=1061) - to_title(335)=(from=1094,to=1062) - to_title(336)=(from=1095,to=1063) - to_title(337)=(from=1096,to=1064) - to_title(338)=(from=1097,to=1065) - to_title(339)=(from=1098,to=1066) - to_title(340)=(from=1099,to=1067) - to_title(341)=(from=1100,to=1068) - to_title(342)=(from=1101,to=1069) - to_title(343)=(from=1102,to=1070) - to_title(344)=(from=1103,to=1071) - to_title(345)=(from=1104,to=1024) - to_title(346)=(from=1105,to=1025) - to_title(347)=(from=1106,to=1026) - to_title(348)=(from=1107,to=1027) - to_title(349)=(from=1108,to=1028) - to_title(350)=(from=1109,to=1029) - to_title(351)=(from=1110,to=1030) - to_title(352)=(from=1111,to=1031) - to_title(353)=(from=1112,to=1032) - to_title(354)=(from=1113,to=1033) - to_title(355)=(from=1114,to=1034) - to_title(356)=(from=1115,to=1035) - to_title(357)=(from=1116,to=1036) - to_title(358)=(from=1117,to=1037) - to_title(359)=(from=1118,to=1038) - to_title(360)=(from=1119,to=1039) - to_title(361)=(from=1121,to=1120) - to_title(362)=(from=1123,to=1122) - to_title(363)=(from=1125,to=1124) - to_title(364)=(from=1127,to=1126) - to_title(365)=(from=1129,to=1128) - to_title(366)=(from=1131,to=1130) - to_title(367)=(from=1133,to=1132) - to_title(368)=(from=1135,to=1134) - to_title(369)=(from=1137,to=1136) - to_title(370)=(from=1139,to=1138) - to_title(371)=(from=1141,to=1140) - to_title(372)=(from=1143,to=1142) - to_title(373)=(from=1145,to=1144) - to_title(374)=(from=1147,to=1146) - to_title(375)=(from=1149,to=1148) - to_title(376)=(from=1151,to=1150) - to_title(377)=(from=1153,to=1152) - to_title(378)=(from=1163,to=1162) - to_title(379)=(from=1165,to=1164) - to_title(380)=(from=1167,to=1166) - to_title(381)=(from=1169,to=1168) - to_title(382)=(from=1171,to=1170) - to_title(383)=(from=1173,to=1172) - to_title(384)=(from=1175,to=1174) - to_title(385)=(from=1177,to=1176) - to_title(386)=(from=1179,to=1178) - to_title(387)=(from=1181,to=1180) - to_title(388)=(from=1183,to=1182) - to_title(389)=(from=1185,to=1184) - to_title(390)=(from=1187,to=1186) - to_title(391)=(from=1189,to=1188) - to_title(392)=(from=1191,to=1190) - to_title(393)=(from=1193,to=1192) - to_title(394)=(from=1195,to=1194) - to_title(395)=(from=1197,to=1196) - to_title(396)=(from=1199,to=1198) - to_title(397)=(from=1201,to=1200) - to_title(398)=(from=1203,to=1202) - to_title(399)=(from=1205,to=1204) - to_title(400)=(from=1207,to=1206) - to_title(401)=(from=1209,to=1208) - to_title(402)=(from=1211,to=1210) - to_title(403)=(from=1213,to=1212) - to_title(404)=(from=1215,to=1214) - to_title(405)=(from=1218,to=1217) - to_title(406)=(from=1220,to=1219) - to_title(407)=(from=1222,to=1221) - to_title(408)=(from=1224,to=1223) - to_title(409)=(from=1226,to=1225) - to_title(410)=(from=1228,to=1227) - to_title(411)=(from=1230,to=1229) - to_title(412)=(from=1231,to=1216) - to_title(413)=(from=1233,to=1232) - to_title(414)=(from=1235,to=1234) - to_title(415)=(from=1237,to=1236) - to_title(416)=(from=1239,to=1238) - to_title(417)=(from=1241,to=1240) - to_title(418)=(from=1243,to=1242) - to_title(419)=(from=1245,to=1244) - to_title(420)=(from=1247,to=1246) - to_title(421)=(from=1249,to=1248) - to_title(422)=(from=1251,to=1250) - to_title(423)=(from=1253,to=1252) - to_title(424)=(from=1255,to=1254) - to_title(425)=(from=1257,to=1256) - to_title(426)=(from=1259,to=1258) - to_title(427)=(from=1261,to=1260) - to_title(428)=(from=1263,to=1262) - to_title(429)=(from=1265,to=1264) - to_title(430)=(from=1267,to=1266) - to_title(431)=(from=1269,to=1268) - to_title(432)=(from=1271,to=1270) - to_title(433)=(from=1273,to=1272) - to_title(434)=(from=1275,to=1274) - to_title(435)=(from=1277,to=1276) - to_title(436)=(from=1279,to=1278) - to_title(437)=(from=1281,to=1280) - to_title(438)=(from=1283,to=1282) - to_title(439)=(from=1285,to=1284) - to_title(440)=(from=1287,to=1286) - to_title(441)=(from=1289,to=1288) - to_title(442)=(from=1291,to=1290) - to_title(443)=(from=1293,to=1292) - to_title(444)=(from=1295,to=1294) - to_title(445)=(from=1297,to=1296) - to_title(446)=(from=1299,to=1298) - to_title(447)=(from=1301,to=1300) - to_title(448)=(from=1303,to=1302) - to_title(449)=(from=1305,to=1304) - to_title(450)=(from=1307,to=1306) - to_title(451)=(from=1309,to=1308) - to_title(452)=(from=1311,to=1310) - to_title(453)=(from=1313,to=1312) - to_title(454)=(from=1315,to=1314) - to_title(455)=(from=1317,to=1316) - to_title(456)=(from=1319,to=1318) - to_title(457)=(from=1321,to=1320) - to_title(458)=(from=1323,to=1322) - to_title(459)=(from=1325,to=1324) - to_title(460)=(from=1327,to=1326) - to_title(461)=(from=1377,to=1329) - to_title(462)=(from=1378,to=1330) - to_title(463)=(from=1379,to=1331) - to_title(464)=(from=1380,to=1332) - to_title(465)=(from=1381,to=1333) - to_title(466)=(from=1382,to=1334) - to_title(467)=(from=1383,to=1335) - to_title(468)=(from=1384,to=1336) - to_title(469)=(from=1385,to=1337) - to_title(470)=(from=1386,to=1338) - to_title(471)=(from=1387,to=1339) - to_title(472)=(from=1388,to=1340) - to_title(473)=(from=1389,to=1341) - to_title(474)=(from=1390,to=1342) - to_title(475)=(from=1391,to=1343) - to_title(476)=(from=1392,to=1344) - to_title(477)=(from=1393,to=1345) - to_title(478)=(from=1394,to=1346) - to_title(479)=(from=1395,to=1347) - to_title(480)=(from=1396,to=1348) - to_title(481)=(from=1397,to=1349) - to_title(482)=(from=1398,to=1350) - to_title(483)=(from=1399,to=1351) - to_title(484)=(from=1400,to=1352) - to_title(485)=(from=1401,to=1353) - to_title(486)=(from=1402,to=1354) - to_title(487)=(from=1403,to=1355) - to_title(488)=(from=1404,to=1356) - to_title(489)=(from=1405,to=1357) - to_title(490)=(from=1406,to=1358) - to_title(491)=(from=1407,to=1359) - to_title(492)=(from=1408,to=1360) - to_title(493)=(from=1409,to=1361) - to_title(494)=(from=1410,to=1362) - to_title(495)=(from=1411,to=1363) - to_title(496)=(from=1412,to=1364) - to_title(497)=(from=1413,to=1365) - to_title(498)=(from=1414,to=1366) - to_title(499)=(from=4304,to=4304) - to_title(500)=(from=4305,to=4305) - to_title(501)=(from=4306,to=4306) - to_title(502)=(from=4307,to=4307) - to_title(503)=(from=4308,to=4308) - to_title(504)=(from=4309,to=4309) - to_title(505)=(from=4310,to=4310) - to_title(506)=(from=4311,to=4311) - to_title(507)=(from=4312,to=4312) - to_title(508)=(from=4313,to=4313) - to_title(509)=(from=4314,to=4314) - to_title(510)=(from=4315,to=4315) - to_title(511)=(from=4316,to=4316) - to_title(512)=(from=4317,to=4317) - to_title(513)=(from=4318,to=4318) - to_title(514)=(from=4319,to=4319) - to_title(515)=(from=4320,to=4320) - to_title(516)=(from=4321,to=4321) - to_title(517)=(from=4322,to=4322) - to_title(518)=(from=4323,to=4323) - to_title(519)=(from=4324,to=4324) - to_title(520)=(from=4325,to=4325) - to_title(521)=(from=4326,to=4326) - to_title(522)=(from=4327,to=4327) - to_title(523)=(from=4328,to=4328) - to_title(524)=(from=4329,to=4329) - to_title(525)=(from=4330,to=4330) - to_title(526)=(from=4331,to=4331) - to_title(527)=(from=4332,to=4332) - to_title(528)=(from=4333,to=4333) - to_title(529)=(from=4334,to=4334) - to_title(530)=(from=4335,to=4335) - to_title(531)=(from=4336,to=4336) - to_title(532)=(from=4337,to=4337) - to_title(533)=(from=4338,to=4338) - to_title(534)=(from=4339,to=4339) - to_title(535)=(from=4340,to=4340) - to_title(536)=(from=4341,to=4341) - to_title(537)=(from=4342,to=4342) - to_title(538)=(from=4343,to=4343) - to_title(539)=(from=4344,to=4344) - to_title(540)=(from=4345,to=4345) - to_title(541)=(from=4346,to=4346) - to_title(542)=(from=4349,to=4349) - to_title(543)=(from=4350,to=4350) - to_title(544)=(from=4351,to=4351) - to_title(545)=(from=5112,to=5104) - to_title(546)=(from=5113,to=5105) - to_title(547)=(from=5114,to=5106) - to_title(548)=(from=5115,to=5107) - to_title(549)=(from=5116,to=5108) - to_title(550)=(from=5117,to=5109) - to_title(551)=(from=7296,to=1042) - to_title(552)=(from=7297,to=1044) - to_title(553)=(from=7298,to=1054) - to_title(554)=(from=7299,to=1057) - to_title(555)=(from=7300,to=1058) - to_title(556)=(from=7301,to=1058) - to_title(557)=(from=7302,to=1066) - to_title(558)=(from=7303,to=1122) - to_title(559)=(from=7304,to=42570) - to_title(560)=(from=7545,to=42877) - to_title(561)=(from=7549,to=11363) - to_title(562)=(from=7566,to=42950) - to_title(563)=(from=7681,to=7680) - to_title(564)=(from=7683,to=7682) - to_title(565)=(from=7685,to=7684) - to_title(566)=(from=7687,to=7686) - to_title(567)=(from=7689,to=7688) - to_title(568)=(from=7691,to=7690) - to_title(569)=(from=7693,to=7692) - to_title(570)=(from=7695,to=7694) - to_title(571)=(from=7697,to=7696) - to_title(572)=(from=7699,to=7698) - to_title(573)=(from=7701,to=7700) - to_title(574)=(from=7703,to=7702) - to_title(575)=(from=7705,to=7704) - to_title(576)=(from=7707,to=7706) - to_title(577)=(from=7709,to=7708) - to_title(578)=(from=7711,to=7710) - to_title(579)=(from=7713,to=7712) - to_title(580)=(from=7715,to=7714) - to_title(581)=(from=7717,to=7716) - to_title(582)=(from=7719,to=7718) - to_title(583)=(from=7721,to=7720) - to_title(584)=(from=7723,to=7722) - to_title(585)=(from=7725,to=7724) - to_title(586)=(from=7727,to=7726) - to_title(587)=(from=7729,to=7728) - to_title(588)=(from=7731,to=7730) - to_title(589)=(from=7733,to=7732) - to_title(590)=(from=7735,to=7734) - to_title(591)=(from=7737,to=7736) - to_title(592)=(from=7739,to=7738) - to_title(593)=(from=7741,to=7740) - to_title(594)=(from=7743,to=7742) - to_title(595)=(from=7745,to=7744) - to_title(596)=(from=7747,to=7746) - to_title(597)=(from=7749,to=7748) - to_title(598)=(from=7751,to=7750) - to_title(599)=(from=7753,to=7752) - to_title(600)=(from=7755,to=7754) - to_title(601)=(from=7757,to=7756) - to_title(602)=(from=7759,to=7758) - to_title(603)=(from=7761,to=7760) - to_title(604)=(from=7763,to=7762) - to_title(605)=(from=7765,to=7764) - to_title(606)=(from=7767,to=7766) - to_title(607)=(from=7769,to=7768) - to_title(608)=(from=7771,to=7770) - to_title(609)=(from=7773,to=7772) - to_title(610)=(from=7775,to=7774) - to_title(611)=(from=7777,to=7776) - to_title(612)=(from=7779,to=7778) - to_title(613)=(from=7781,to=7780) - to_title(614)=(from=7783,to=7782) - to_title(615)=(from=7785,to=7784) - to_title(616)=(from=7787,to=7786) - to_title(617)=(from=7789,to=7788) - to_title(618)=(from=7791,to=7790) - to_title(619)=(from=7793,to=7792) - to_title(620)=(from=7795,to=7794) - to_title(621)=(from=7797,to=7796) - to_title(622)=(from=7799,to=7798) - to_title(623)=(from=7801,to=7800) - to_title(624)=(from=7803,to=7802) - to_title(625)=(from=7805,to=7804) - to_title(626)=(from=7807,to=7806) - to_title(627)=(from=7809,to=7808) - to_title(628)=(from=7811,to=7810) - to_title(629)=(from=7813,to=7812) - to_title(630)=(from=7815,to=7814) - to_title(631)=(from=7817,to=7816) - to_title(632)=(from=7819,to=7818) - to_title(633)=(from=7821,to=7820) - to_title(634)=(from=7823,to=7822) - to_title(635)=(from=7825,to=7824) - to_title(636)=(from=7827,to=7826) - to_title(637)=(from=7829,to=7828) - to_title(638)=(from=7835,to=7776) - to_title(639)=(from=7841,to=7840) - to_title(640)=(from=7843,to=7842) - to_title(641)=(from=7845,to=7844) - to_title(642)=(from=7847,to=7846) - to_title(643)=(from=7849,to=7848) - to_title(644)=(from=7851,to=7850) - to_title(645)=(from=7853,to=7852) - to_title(646)=(from=7855,to=7854) - to_title(647)=(from=7857,to=7856) - to_title(648)=(from=7859,to=7858) - to_title(649)=(from=7861,to=7860) - to_title(650)=(from=7863,to=7862) - to_title(651)=(from=7865,to=7864) - to_title(652)=(from=7867,to=7866) - to_title(653)=(from=7869,to=7868) - to_title(654)=(from=7871,to=7870) - to_title(655)=(from=7873,to=7872) - to_title(656)=(from=7875,to=7874) - to_title(657)=(from=7877,to=7876) - to_title(658)=(from=7879,to=7878) - to_title(659)=(from=7881,to=7880) - to_title(660)=(from=7883,to=7882) - to_title(661)=(from=7885,to=7884) - to_title(662)=(from=7887,to=7886) - to_title(663)=(from=7889,to=7888) - to_title(664)=(from=7891,to=7890) - to_title(665)=(from=7893,to=7892) - to_title(666)=(from=7895,to=7894) - to_title(667)=(from=7897,to=7896) - to_title(668)=(from=7899,to=7898) - to_title(669)=(from=7901,to=7900) - to_title(670)=(from=7903,to=7902) - to_title(671)=(from=7905,to=7904) - to_title(672)=(from=7907,to=7906) - to_title(673)=(from=7909,to=7908) - to_title(674)=(from=7911,to=7910) - to_title(675)=(from=7913,to=7912) - to_title(676)=(from=7915,to=7914) - to_title(677)=(from=7917,to=7916) - to_title(678)=(from=7919,to=7918) - to_title(679)=(from=7921,to=7920) - to_title(680)=(from=7923,to=7922) - to_title(681)=(from=7925,to=7924) - to_title(682)=(from=7927,to=7926) - to_title(683)=(from=7929,to=7928) - to_title(684)=(from=7931,to=7930) - to_title(685)=(from=7933,to=7932) - to_title(686)=(from=7935,to=7934) - to_title(687)=(from=7936,to=7944) - to_title(688)=(from=7937,to=7945) - to_title(689)=(from=7938,to=7946) - to_title(690)=(from=7939,to=7947) - to_title(691)=(from=7940,to=7948) - to_title(692)=(from=7941,to=7949) - to_title(693)=(from=7942,to=7950) - to_title(694)=(from=7943,to=7951) - to_title(695)=(from=7952,to=7960) - to_title(696)=(from=7953,to=7961) - to_title(697)=(from=7954,to=7962) - to_title(698)=(from=7955,to=7963) - to_title(699)=(from=7956,to=7964) - to_title(700)=(from=7957,to=7965) - to_title(701)=(from=7968,to=7976) - to_title(702)=(from=7969,to=7977) - to_title(703)=(from=7970,to=7978) - to_title(704)=(from=7971,to=7979) - to_title(705)=(from=7972,to=7980) - to_title(706)=(from=7973,to=7981) - to_title(707)=(from=7974,to=7982) - to_title(708)=(from=7975,to=7983) - to_title(709)=(from=7984,to=7992) - to_title(710)=(from=7985,to=7993) - to_title(711)=(from=7986,to=7994) - to_title(712)=(from=7987,to=7995) - to_title(713)=(from=7988,to=7996) - to_title(714)=(from=7989,to=7997) - to_title(715)=(from=7990,to=7998) - to_title(716)=(from=7991,to=7999) - to_title(717)=(from=8000,to=8008) - to_title(718)=(from=8001,to=8009) - to_title(719)=(from=8002,to=8010) - to_title(720)=(from=8003,to=8011) - to_title(721)=(from=8004,to=8012) - to_title(722)=(from=8005,to=8013) - to_title(723)=(from=8017,to=8025) - to_title(724)=(from=8019,to=8027) - to_title(725)=(from=8021,to=8029) - to_title(726)=(from=8023,to=8031) - to_title(727)=(from=8032,to=8040) - to_title(728)=(from=8033,to=8041) - to_title(729)=(from=8034,to=8042) - to_title(730)=(from=8035,to=8043) - to_title(731)=(from=8036,to=8044) - to_title(732)=(from=8037,to=8045) - to_title(733)=(from=8038,to=8046) - to_title(734)=(from=8039,to=8047) - to_title(735)=(from=8048,to=8122) - to_title(736)=(from=8049,to=8123) - to_title(737)=(from=8050,to=8136) - to_title(738)=(from=8051,to=8137) - to_title(739)=(from=8052,to=8138) - to_title(740)=(from=8053,to=8139) - to_title(741)=(from=8054,to=8154) - to_title(742)=(from=8055,to=8155) - to_title(743)=(from=8056,to=8184) - to_title(744)=(from=8057,to=8185) - to_title(745)=(from=8058,to=8170) - to_title(746)=(from=8059,to=8171) - to_title(747)=(from=8060,to=8186) - to_title(748)=(from=8061,to=8187) - to_title(749)=(from=8064,to=8072) - to_title(750)=(from=8065,to=8073) - to_title(751)=(from=8066,to=8074) - to_title(752)=(from=8067,to=8075) - to_title(753)=(from=8068,to=8076) - to_title(754)=(from=8069,to=8077) - to_title(755)=(from=8070,to=8078) - to_title(756)=(from=8071,to=8079) - to_title(757)=(from=8080,to=8088) - to_title(758)=(from=8081,to=8089) - to_title(759)=(from=8082,to=8090) - to_title(760)=(from=8083,to=8091) - to_title(761)=(from=8084,to=8092) - to_title(762)=(from=8085,to=8093) - to_title(763)=(from=8086,to=8094) - to_title(764)=(from=8087,to=8095) - to_title(765)=(from=8096,to=8104) - to_title(766)=(from=8097,to=8105) - to_title(767)=(from=8098,to=8106) - to_title(768)=(from=8099,to=8107) - to_title(769)=(from=8100,to=8108) - to_title(770)=(from=8101,to=8109) - to_title(771)=(from=8102,to=8110) - to_title(772)=(from=8103,to=8111) - to_title(773)=(from=8112,to=8120) - to_title(774)=(from=8113,to=8121) - to_title(775)=(from=8115,to=8124) - to_title(776)=(from=8126,to=921) - to_title(777)=(from=8131,to=8140) - to_title(778)=(from=8144,to=8152) - to_title(779)=(from=8145,to=8153) - to_title(780)=(from=8160,to=8168) - to_title(781)=(from=8161,to=8169) - to_title(782)=(from=8165,to=8172) - to_title(783)=(from=8179,to=8188) - to_title(784)=(from=8526,to=8498) - to_title(785)=(from=8560,to=8544) - to_title(786)=(from=8561,to=8545) - to_title(787)=(from=8562,to=8546) - to_title(788)=(from=8563,to=8547) - to_title(789)=(from=8564,to=8548) - to_title(790)=(from=8565,to=8549) - to_title(791)=(from=8566,to=8550) - to_title(792)=(from=8567,to=8551) - to_title(793)=(from=8568,to=8552) - to_title(794)=(from=8569,to=8553) - to_title(795)=(from=8570,to=8554) - to_title(796)=(from=8571,to=8555) - to_title(797)=(from=8572,to=8556) - to_title(798)=(from=8573,to=8557) - to_title(799)=(from=8574,to=8558) - to_title(800)=(from=8575,to=8559) - to_title(801)=(from=8580,to=8579) - to_title(802)=(from=9424,to=9398) - to_title(803)=(from=9425,to=9399) - to_title(804)=(from=9426,to=9400) - to_title(805)=(from=9427,to=9401) - to_title(806)=(from=9428,to=9402) - to_title(807)=(from=9429,to=9403) - to_title(808)=(from=9430,to=9404) - to_title(809)=(from=9431,to=9405) - to_title(810)=(from=9432,to=9406) - to_title(811)=(from=9433,to=9407) - to_title(812)=(from=9434,to=9408) - to_title(813)=(from=9435,to=9409) - to_title(814)=(from=9436,to=9410) - to_title(815)=(from=9437,to=9411) - to_title(816)=(from=9438,to=9412) - to_title(817)=(from=9439,to=9413) - to_title(818)=(from=9440,to=9414) - to_title(819)=(from=9441,to=9415) - to_title(820)=(from=9442,to=9416) - to_title(821)=(from=9443,to=9417) - to_title(822)=(from=9444,to=9418) - to_title(823)=(from=9445,to=9419) - to_title(824)=(from=9446,to=9420) - to_title(825)=(from=9447,to=9421) - to_title(826)=(from=9448,to=9422) - to_title(827)=(from=9449,to=9423) - to_title(828)=(from=11312,to=11264) - to_title(829)=(from=11313,to=11265) - to_title(830)=(from=11314,to=11266) - to_title(831)=(from=11315,to=11267) - to_title(832)=(from=11316,to=11268) - to_title(833)=(from=11317,to=11269) - to_title(834)=(from=11318,to=11270) - to_title(835)=(from=11319,to=11271) - to_title(836)=(from=11320,to=11272) - to_title(837)=(from=11321,to=11273) - to_title(838)=(from=11322,to=11274) - to_title(839)=(from=11323,to=11275) - to_title(840)=(from=11324,to=11276) - to_title(841)=(from=11325,to=11277) - to_title(842)=(from=11326,to=11278) - to_title(843)=(from=11327,to=11279) - to_title(844)=(from=11328,to=11280) - to_title(845)=(from=11329,to=11281) - to_title(846)=(from=11330,to=11282) - to_title(847)=(from=11331,to=11283) - to_title(848)=(from=11332,to=11284) - to_title(849)=(from=11333,to=11285) - to_title(850)=(from=11334,to=11286) - to_title(851)=(from=11335,to=11287) - to_title(852)=(from=11336,to=11288) - to_title(853)=(from=11337,to=11289) - to_title(854)=(from=11338,to=11290) - to_title(855)=(from=11339,to=11291) - to_title(856)=(from=11340,to=11292) - to_title(857)=(from=11341,to=11293) - to_title(858)=(from=11342,to=11294) - to_title(859)=(from=11343,to=11295) - to_title(860)=(from=11344,to=11296) - to_title(861)=(from=11345,to=11297) - to_title(862)=(from=11346,to=11298) - to_title(863)=(from=11347,to=11299) - to_title(864)=(from=11348,to=11300) - to_title(865)=(from=11349,to=11301) - to_title(866)=(from=11350,to=11302) - to_title(867)=(from=11351,to=11303) - to_title(868)=(from=11352,to=11304) - to_title(869)=(from=11353,to=11305) - to_title(870)=(from=11354,to=11306) - to_title(871)=(from=11355,to=11307) - to_title(872)=(from=11356,to=11308) - to_title(873)=(from=11357,to=11309) - to_title(874)=(from=11358,to=11310) - to_title(875)=(from=11361,to=11360) - to_title(876)=(from=11365,to=570) - to_title(877)=(from=11366,to=574) - to_title(878)=(from=11368,to=11367) - to_title(879)=(from=11370,to=11369) - to_title(880)=(from=11372,to=11371) - to_title(881)=(from=11379,to=11378) - to_title(882)=(from=11382,to=11381) - to_title(883)=(from=11393,to=11392) - to_title(884)=(from=11395,to=11394) - to_title(885)=(from=11397,to=11396) - to_title(886)=(from=11399,to=11398) - to_title(887)=(from=11401,to=11400) - to_title(888)=(from=11403,to=11402) - to_title(889)=(from=11405,to=11404) - to_title(890)=(from=11407,to=11406) - to_title(891)=(from=11409,to=11408) - to_title(892)=(from=11411,to=11410) - to_title(893)=(from=11413,to=11412) - to_title(894)=(from=11415,to=11414) - to_title(895)=(from=11417,to=11416) - to_title(896)=(from=11419,to=11418) - to_title(897)=(from=11421,to=11420) - to_title(898)=(from=11423,to=11422) - to_title(899)=(from=11425,to=11424) - to_title(900)=(from=11427,to=11426) - to_title(901)=(from=11429,to=11428) - to_title(902)=(from=11431,to=11430) - to_title(903)=(from=11433,to=11432) - to_title(904)=(from=11435,to=11434) - to_title(905)=(from=11437,to=11436) - to_title(906)=(from=11439,to=11438) - to_title(907)=(from=11441,to=11440) - to_title(908)=(from=11443,to=11442) - to_title(909)=(from=11445,to=11444) - to_title(910)=(from=11447,to=11446) - to_title(911)=(from=11449,to=11448) - to_title(912)=(from=11451,to=11450) - to_title(913)=(from=11453,to=11452) - to_title(914)=(from=11455,to=11454) - to_title(915)=(from=11457,to=11456) - to_title(916)=(from=11459,to=11458) - to_title(917)=(from=11461,to=11460) - to_title(918)=(from=11463,to=11462) - to_title(919)=(from=11465,to=11464) - to_title(920)=(from=11467,to=11466) - to_title(921)=(from=11469,to=11468) - to_title(922)=(from=11471,to=11470) - to_title(923)=(from=11473,to=11472) - to_title(924)=(from=11475,to=11474) - to_title(925)=(from=11477,to=11476) - to_title(926)=(from=11479,to=11478) - to_title(927)=(from=11481,to=11480) - to_title(928)=(from=11483,to=11482) - to_title(929)=(from=11485,to=11484) - to_title(930)=(from=11487,to=11486) - to_title(931)=(from=11489,to=11488) - to_title(932)=(from=11491,to=11490) - to_title(933)=(from=11500,to=11499) - to_title(934)=(from=11502,to=11501) - to_title(935)=(from=11507,to=11506) - to_title(936)=(from=11520,to=4256) - to_title(937)=(from=11521,to=4257) - to_title(938)=(from=11522,to=4258) - to_title(939)=(from=11523,to=4259) - to_title(940)=(from=11524,to=4260) - to_title(941)=(from=11525,to=4261) - to_title(942)=(from=11526,to=4262) - to_title(943)=(from=11527,to=4263) - to_title(944)=(from=11528,to=4264) - to_title(945)=(from=11529,to=4265) - to_title(946)=(from=11530,to=4266) - to_title(947)=(from=11531,to=4267) - to_title(948)=(from=11532,to=4268) - to_title(949)=(from=11533,to=4269) - to_title(950)=(from=11534,to=4270) - to_title(951)=(from=11535,to=4271) - to_title(952)=(from=11536,to=4272) - to_title(953)=(from=11537,to=4273) - to_title(954)=(from=11538,to=4274) - to_title(955)=(from=11539,to=4275) - to_title(956)=(from=11540,to=4276) - to_title(957)=(from=11541,to=4277) - to_title(958)=(from=11542,to=4278) - to_title(959)=(from=11543,to=4279) - to_title(960)=(from=11544,to=4280) - to_title(961)=(from=11545,to=4281) - to_title(962)=(from=11546,to=4282) - to_title(963)=(from=11547,to=4283) - to_title(964)=(from=11548,to=4284) - to_title(965)=(from=11549,to=4285) - to_title(966)=(from=11550,to=4286) - to_title(967)=(from=11551,to=4287) - to_title(968)=(from=11552,to=4288) - to_title(969)=(from=11553,to=4289) - to_title(970)=(from=11554,to=4290) - to_title(971)=(from=11555,to=4291) - to_title(972)=(from=11556,to=4292) - to_title(973)=(from=11557,to=4293) - to_title(974)=(from=11559,to=4295) - to_title(975)=(from=11565,to=4301) - to_title(976)=(from=42561,to=42560) - to_title(977)=(from=42563,to=42562) - to_title(978)=(from=42565,to=42564) - to_title(979)=(from=42567,to=42566) - to_title(980)=(from=42569,to=42568) - to_title(981)=(from=42571,to=42570) - to_title(982)=(from=42573,to=42572) - to_title(983)=(from=42575,to=42574) - to_title(984)=(from=42577,to=42576) - to_title(985)=(from=42579,to=42578) - to_title(986)=(from=42581,to=42580) - to_title(987)=(from=42583,to=42582) - to_title(988)=(from=42585,to=42584) - to_title(989)=(from=42587,to=42586) - to_title(990)=(from=42589,to=42588) - to_title(991)=(from=42591,to=42590) - to_title(992)=(from=42593,to=42592) - to_title(993)=(from=42595,to=42594) - to_title(994)=(from=42597,to=42596) - to_title(995)=(from=42599,to=42598) - to_title(996)=(from=42601,to=42600) - to_title(997)=(from=42603,to=42602) - to_title(998)=(from=42605,to=42604) - to_title(999)=(from=42625,to=42624) - to_title(1000)=(from=42627,to=42626) - to_title(1001)=(from=42629,to=42628) - to_title(1002)=(from=42631,to=42630) - to_title(1003)=(from=42633,to=42632) - to_title(1004)=(from=42635,to=42634) - to_title(1005)=(from=42637,to=42636) - to_title(1006)=(from=42639,to=42638) - to_title(1007)=(from=42641,to=42640) - to_title(1008)=(from=42643,to=42642) - to_title(1009)=(from=42645,to=42644) - to_title(1010)=(from=42647,to=42646) - to_title(1011)=(from=42649,to=42648) - to_title(1012)=(from=42651,to=42650) - to_title(1013)=(from=42787,to=42786) - to_title(1014)=(from=42789,to=42788) - to_title(1015)=(from=42791,to=42790) - to_title(1016)=(from=42793,to=42792) - to_title(1017)=(from=42795,to=42794) - to_title(1018)=(from=42797,to=42796) - to_title(1019)=(from=42799,to=42798) - to_title(1020)=(from=42803,to=42802) - to_title(1021)=(from=42805,to=42804) - to_title(1022)=(from=42807,to=42806) - to_title(1023)=(from=42809,to=42808) - to_title(1024)=(from=42811,to=42810) - to_title(1025)=(from=42813,to=42812) - to_title(1026)=(from=42815,to=42814) - to_title(1027)=(from=42817,to=42816) - to_title(1028)=(from=42819,to=42818) - to_title(1029)=(from=42821,to=42820) - to_title(1030)=(from=42823,to=42822) - to_title(1031)=(from=42825,to=42824) - to_title(1032)=(from=42827,to=42826) - to_title(1033)=(from=42829,to=42828) - to_title(1034)=(from=42831,to=42830) - to_title(1035)=(from=42833,to=42832) - to_title(1036)=(from=42835,to=42834) - to_title(1037)=(from=42837,to=42836) - to_title(1038)=(from=42839,to=42838) - to_title(1039)=(from=42841,to=42840) - to_title(1040)=(from=42843,to=42842) - to_title(1041)=(from=42845,to=42844) - to_title(1042)=(from=42847,to=42846) - to_title(1043)=(from=42849,to=42848) - to_title(1044)=(from=42851,to=42850) - to_title(1045)=(from=42853,to=42852) - to_title(1046)=(from=42855,to=42854) - to_title(1047)=(from=42857,to=42856) - to_title(1048)=(from=42859,to=42858) - to_title(1049)=(from=42861,to=42860) - to_title(1050)=(from=42863,to=42862) - to_title(1051)=(from=42874,to=42873) - to_title(1052)=(from=42876,to=42875) - to_title(1053)=(from=42879,to=42878) - to_title(1054)=(from=42881,to=42880) - to_title(1055)=(from=42883,to=42882) - to_title(1056)=(from=42885,to=42884) - to_title(1057)=(from=42887,to=42886) - to_title(1058)=(from=42892,to=42891) - to_title(1059)=(from=42897,to=42896) - to_title(1060)=(from=42899,to=42898) - to_title(1061)=(from=42900,to=42948) - to_title(1062)=(from=42903,to=42902) - to_title(1063)=(from=42905,to=42904) - to_title(1064)=(from=42907,to=42906) - to_title(1065)=(from=42909,to=42908) - to_title(1066)=(from=42911,to=42910) - to_title(1067)=(from=42913,to=42912) - to_title(1068)=(from=42915,to=42914) - to_title(1069)=(from=42917,to=42916) - to_title(1070)=(from=42919,to=42918) - to_title(1071)=(from=42921,to=42920) - to_title(1072)=(from=42933,to=42932) - to_title(1073)=(from=42935,to=42934) - to_title(1074)=(from=42937,to=42936) - to_title(1075)=(from=42939,to=42938) - to_title(1076)=(from=42941,to=42940) - to_title(1077)=(from=42943,to=42942) - to_title(1078)=(from=42947,to=42946) - to_title(1079)=(from=42952,to=42951) - to_title(1080)=(from=42954,to=42953) - to_title(1081)=(from=42998,to=42997) - to_title(1082)=(from=43859,to=42931) - to_title(1083)=(from=43888,to=5024) - to_title(1084)=(from=43889,to=5025) - to_title(1085)=(from=43890,to=5026) - to_title(1086)=(from=43891,to=5027) - to_title(1087)=(from=43892,to=5028) - to_title(1088)=(from=43893,to=5029) - to_title(1089)=(from=43894,to=5030) - to_title(1090)=(from=43895,to=5031) - to_title(1091)=(from=43896,to=5032) - to_title(1092)=(from=43897,to=5033) - to_title(1093)=(from=43898,to=5034) - to_title(1094)=(from=43899,to=5035) - to_title(1095)=(from=43900,to=5036) - to_title(1096)=(from=43901,to=5037) - to_title(1097)=(from=43902,to=5038) - to_title(1098)=(from=43903,to=5039) - to_title(1099)=(from=43904,to=5040) - to_title(1100)=(from=43905,to=5041) - to_title(1101)=(from=43906,to=5042) - to_title(1102)=(from=43907,to=5043) - to_title(1103)=(from=43908,to=5044) - to_title(1104)=(from=43909,to=5045) - to_title(1105)=(from=43910,to=5046) - to_title(1106)=(from=43911,to=5047) - to_title(1107)=(from=43912,to=5048) - to_title(1108)=(from=43913,to=5049) - to_title(1109)=(from=43914,to=5050) - to_title(1110)=(from=43915,to=5051) - to_title(1111)=(from=43916,to=5052) - to_title(1112)=(from=43917,to=5053) - to_title(1113)=(from=43918,to=5054) - to_title(1114)=(from=43919,to=5055) - to_title(1115)=(from=43920,to=5056) - to_title(1116)=(from=43921,to=5057) - to_title(1117)=(from=43922,to=5058) - to_title(1118)=(from=43923,to=5059) - to_title(1119)=(from=43924,to=5060) - to_title(1120)=(from=43925,to=5061) - to_title(1121)=(from=43926,to=5062) - to_title(1122)=(from=43927,to=5063) - to_title(1123)=(from=43928,to=5064) - to_title(1124)=(from=43929,to=5065) - to_title(1125)=(from=43930,to=5066) - to_title(1126)=(from=43931,to=5067) - to_title(1127)=(from=43932,to=5068) - to_title(1128)=(from=43933,to=5069) - to_title(1129)=(from=43934,to=5070) - to_title(1130)=(from=43935,to=5071) - to_title(1131)=(from=43936,to=5072) - to_title(1132)=(from=43937,to=5073) - to_title(1133)=(from=43938,to=5074) - to_title(1134)=(from=43939,to=5075) - to_title(1135)=(from=43940,to=5076) - to_title(1136)=(from=43941,to=5077) - to_title(1137)=(from=43942,to=5078) - to_title(1138)=(from=43943,to=5079) - to_title(1139)=(from=43944,to=5080) - to_title(1140)=(from=43945,to=5081) - to_title(1141)=(from=43946,to=5082) - to_title(1142)=(from=43947,to=5083) - to_title(1143)=(from=43948,to=5084) - to_title(1144)=(from=43949,to=5085) - to_title(1145)=(from=43950,to=5086) - to_title(1146)=(from=43951,to=5087) - to_title(1147)=(from=43952,to=5088) - to_title(1148)=(from=43953,to=5089) - to_title(1149)=(from=43954,to=5090) - to_title(1150)=(from=43955,to=5091) - to_title(1151)=(from=43956,to=5092) - to_title(1152)=(from=43957,to=5093) - to_title(1153)=(from=43958,to=5094) - to_title(1154)=(from=43959,to=5095) - to_title(1155)=(from=43960,to=5096) - to_title(1156)=(from=43961,to=5097) - to_title(1157)=(from=43962,to=5098) - to_title(1158)=(from=43963,to=5099) - to_title(1159)=(from=43964,to=5100) - to_title(1160)=(from=43965,to=5101) - to_title(1161)=(from=43966,to=5102) - to_title(1162)=(from=43967,to=5103) - to_title(1163)=(from=65345,to=65313) - to_title(1164)=(from=65346,to=65314) - to_title(1165)=(from=65347,to=65315) - to_title(1166)=(from=65348,to=65316) - to_title(1167)=(from=65349,to=65317) - to_title(1168)=(from=65350,to=65318) - to_title(1169)=(from=65351,to=65319) - to_title(1170)=(from=65352,to=65320) - to_title(1171)=(from=65353,to=65321) - to_title(1172)=(from=65354,to=65322) - to_title(1173)=(from=65355,to=65323) - to_title(1174)=(from=65356,to=65324) - to_title(1175)=(from=65357,to=65325) - to_title(1176)=(from=65358,to=65326) - to_title(1177)=(from=65359,to=65327) - to_title(1178)=(from=65360,to=65328) - to_title(1179)=(from=65361,to=65329) - to_title(1180)=(from=65362,to=65330) - to_title(1181)=(from=65363,to=65331) - to_title(1182)=(from=65364,to=65332) - to_title(1183)=(from=65365,to=65333) - to_title(1184)=(from=65366,to=65334) - to_title(1185)=(from=65367,to=65335) - to_title(1186)=(from=65368,to=65336) - to_title(1187)=(from=65369,to=65337) - to_title(1188)=(from=65370,to=65338) - to_title(1189)=(from=66600,to=66560) - to_title(1190)=(from=66601,to=66561) - to_title(1191)=(from=66602,to=66562) - to_title(1192)=(from=66603,to=66563) - to_title(1193)=(from=66604,to=66564) - to_title(1194)=(from=66605,to=66565) - to_title(1195)=(from=66606,to=66566) - to_title(1196)=(from=66607,to=66567) - to_title(1197)=(from=66608,to=66568) - to_title(1198)=(from=66609,to=66569) - to_title(1199)=(from=66610,to=66570) - to_title(1200)=(from=66611,to=66571) - to_title(1201)=(from=66612,to=66572) - to_title(1202)=(from=66613,to=66573) - to_title(1203)=(from=66614,to=66574) - to_title(1204)=(from=66615,to=66575) - to_title(1205)=(from=66616,to=66576) - to_title(1206)=(from=66617,to=66577) - to_title(1207)=(from=66618,to=66578) - to_title(1208)=(from=66619,to=66579) - to_title(1209)=(from=66620,to=66580) - to_title(1210)=(from=66621,to=66581) - to_title(1211)=(from=66622,to=66582) - to_title(1212)=(from=66623,to=66583) - to_title(1213)=(from=66624,to=66584) - to_title(1214)=(from=66625,to=66585) - to_title(1215)=(from=66626,to=66586) - to_title(1216)=(from=66627,to=66587) - to_title(1217)=(from=66628,to=66588) - to_title(1218)=(from=66629,to=66589) - to_title(1219)=(from=66630,to=66590) - to_title(1220)=(from=66631,to=66591) - to_title(1221)=(from=66632,to=66592) - to_title(1222)=(from=66633,to=66593) - to_title(1223)=(from=66634,to=66594) - to_title(1224)=(from=66635,to=66595) - to_title(1225)=(from=66636,to=66596) - to_title(1226)=(from=66637,to=66597) - to_title(1227)=(from=66638,to=66598) - to_title(1228)=(from=66639,to=66599) - to_title(1229)=(from=66776,to=66736) - to_title(1230)=(from=66777,to=66737) - to_title(1231)=(from=66778,to=66738) - to_title(1232)=(from=66779,to=66739) - to_title(1233)=(from=66780,to=66740) - to_title(1234)=(from=66781,to=66741) - to_title(1235)=(from=66782,to=66742) - to_title(1236)=(from=66783,to=66743) - to_title(1237)=(from=66784,to=66744) - to_title(1238)=(from=66785,to=66745) - to_title(1239)=(from=66786,to=66746) - to_title(1240)=(from=66787,to=66747) - to_title(1241)=(from=66788,to=66748) - to_title(1242)=(from=66789,to=66749) - to_title(1243)=(from=66790,to=66750) - to_title(1244)=(from=66791,to=66751) - to_title(1245)=(from=66792,to=66752) - to_title(1246)=(from=66793,to=66753) - to_title(1247)=(from=66794,to=66754) - to_title(1248)=(from=66795,to=66755) - to_title(1249)=(from=66796,to=66756) - to_title(1250)=(from=66797,to=66757) - to_title(1251)=(from=66798,to=66758) - to_title(1252)=(from=66799,to=66759) - to_title(1253)=(from=66800,to=66760) - to_title(1254)=(from=66801,to=66761) - to_title(1255)=(from=66802,to=66762) - to_title(1256)=(from=66803,to=66763) - to_title(1257)=(from=66804,to=66764) - to_title(1258)=(from=66805,to=66765) - to_title(1259)=(from=66806,to=66766) - to_title(1260)=(from=66807,to=66767) - to_title(1261)=(from=66808,to=66768) - to_title(1262)=(from=66809,to=66769) - to_title(1263)=(from=66810,to=66770) - to_title(1264)=(from=66811,to=66771) - to_title(1265)=(from=68800,to=68736) - to_title(1266)=(from=68801,to=68737) - to_title(1267)=(from=68802,to=68738) - to_title(1268)=(from=68803,to=68739) - to_title(1269)=(from=68804,to=68740) - to_title(1270)=(from=68805,to=68741) - to_title(1271)=(from=68806,to=68742) - to_title(1272)=(from=68807,to=68743) - to_title(1273)=(from=68808,to=68744) - to_title(1274)=(from=68809,to=68745) - to_title(1275)=(from=68810,to=68746) - to_title(1276)=(from=68811,to=68747) - to_title(1277)=(from=68812,to=68748) - to_title(1278)=(from=68813,to=68749) - to_title(1279)=(from=68814,to=68750) - to_title(1280)=(from=68815,to=68751) - to_title(1281)=(from=68816,to=68752) - to_title(1282)=(from=68817,to=68753) - to_title(1283)=(from=68818,to=68754) - to_title(1284)=(from=68819,to=68755) - to_title(1285)=(from=68820,to=68756) - to_title(1286)=(from=68821,to=68757) - to_title(1287)=(from=68822,to=68758) - to_title(1288)=(from=68823,to=68759) - to_title(1289)=(from=68824,to=68760) - to_title(1290)=(from=68825,to=68761) - to_title(1291)=(from=68826,to=68762) - to_title(1292)=(from=68827,to=68763) - to_title(1293)=(from=68828,to=68764) - to_title(1294)=(from=68829,to=68765) - to_title(1295)=(from=68830,to=68766) - to_title(1296)=(from=68831,to=68767) - to_title(1297)=(from=68832,to=68768) - to_title(1298)=(from=68833,to=68769) - to_title(1299)=(from=68834,to=68770) - to_title(1300)=(from=68835,to=68771) - to_title(1301)=(from=68836,to=68772) - to_title(1302)=(from=68837,to=68773) - to_title(1303)=(from=68838,to=68774) - to_title(1304)=(from=68839,to=68775) - to_title(1305)=(from=68840,to=68776) - to_title(1306)=(from=68841,to=68777) - to_title(1307)=(from=68842,to=68778) - to_title(1308)=(from=68843,to=68779) - to_title(1309)=(from=68844,to=68780) - to_title(1310)=(from=68845,to=68781) - to_title(1311)=(from=68846,to=68782) - to_title(1312)=(from=68847,to=68783) - to_title(1313)=(from=68848,to=68784) - to_title(1314)=(from=68849,to=68785) - to_title(1315)=(from=68850,to=68786) - to_title(1316)=(from=71872,to=71840) - to_title(1317)=(from=71873,to=71841) - to_title(1318)=(from=71874,to=71842) - to_title(1319)=(from=71875,to=71843) - to_title(1320)=(from=71876,to=71844) - to_title(1321)=(from=71877,to=71845) - to_title(1322)=(from=71878,to=71846) - to_title(1323)=(from=71879,to=71847) - to_title(1324)=(from=71880,to=71848) - to_title(1325)=(from=71881,to=71849) - to_title(1326)=(from=71882,to=71850) - to_title(1327)=(from=71883,to=71851) - to_title(1328)=(from=71884,to=71852) - to_title(1329)=(from=71885,to=71853) - to_title(1330)=(from=71886,to=71854) - to_title(1331)=(from=71887,to=71855) - to_title(1332)=(from=71888,to=71856) - to_title(1333)=(from=71889,to=71857) - to_title(1334)=(from=71890,to=71858) - to_title(1335)=(from=71891,to=71859) - to_title(1336)=(from=71892,to=71860) - to_title(1337)=(from=71893,to=71861) - to_title(1338)=(from=71894,to=71862) - to_title(1339)=(from=71895,to=71863) - to_title(1340)=(from=71896,to=71864) - to_title(1341)=(from=71897,to=71865) - to_title(1342)=(from=71898,to=71866) - to_title(1343)=(from=71899,to=71867) - to_title(1344)=(from=71900,to=71868) - to_title(1345)=(from=71901,to=71869) - to_title(1346)=(from=71902,to=71870) - to_title(1347)=(from=71903,to=71871) - to_title(1348)=(from=93792,to=93760) - to_title(1349)=(from=93793,to=93761) - to_title(1350)=(from=93794,to=93762) - to_title(1351)=(from=93795,to=93763) - to_title(1352)=(from=93796,to=93764) - to_title(1353)=(from=93797,to=93765) - to_title(1354)=(from=93798,to=93766) - to_title(1355)=(from=93799,to=93767) - to_title(1356)=(from=93800,to=93768) - to_title(1357)=(from=93801,to=93769) - to_title(1358)=(from=93802,to=93770) - to_title(1359)=(from=93803,to=93771) - to_title(1360)=(from=93804,to=93772) - to_title(1361)=(from=93805,to=93773) - to_title(1362)=(from=93806,to=93774) - to_title(1363)=(from=93807,to=93775) - to_title(1364)=(from=93808,to=93776) - to_title(1365)=(from=93809,to=93777) - to_title(1366)=(from=93810,to=93778) - to_title(1367)=(from=93811,to=93779) - to_title(1368)=(from=93812,to=93780) - to_title(1369)=(from=93813,to=93781) - to_title(1370)=(from=93814,to=93782) - to_title(1371)=(from=93815,to=93783) - to_title(1372)=(from=93816,to=93784) - to_title(1373)=(from=93817,to=93785) - to_title(1374)=(from=93818,to=93786) - to_title(1375)=(from=93819,to=93787) - to_title(1376)=(from=93820,to=93788) - to_title(1377)=(from=93821,to=93789) - to_title(1378)=(from=93822,to=93790) - to_title(1379)=(from=93823,to=93791) - to_title(1380)=(from=125218,to=125184) - to_title(1381)=(from=125219,to=125185) - to_title(1382)=(from=125220,to=125186) - to_title(1383)=(from=125221,to=125187) - to_title(1384)=(from=125222,to=125188) - to_title(1385)=(from=125223,to=125189) - to_title(1386)=(from=125224,to=125190) - to_title(1387)=(from=125225,to=125191) - to_title(1388)=(from=125226,to=125192) - to_title(1389)=(from=125227,to=125193) - to_title(1390)=(from=125228,to=125194) - to_title(1391)=(from=125229,to=125195) - to_title(1392)=(from=125230,to=125196) - to_title(1393)=(from=125231,to=125197) - to_title(1394)=(from=125232,to=125198) - to_title(1395)=(from=125233,to=125199) - to_title(1396)=(from=125234,to=125200) - to_title(1397)=(from=125235,to=125201) - to_title(1398)=(from=125236,to=125202) - to_title(1399)=(from=125237,to=125203) - to_title(1400)=(from=125238,to=125204) - to_title(1401)=(from=125239,to=125205) - to_title(1402)=(from=125240,to=125206) - to_title(1403)=(from=125241,to=125207) - to_title(1404)=(from=125242,to=125208) - to_title(1405)=(from=125243,to=125209) - to_title(1406)=(from=125244,to=125210) - to_title(1407)=(from=125245,to=125211) - to_title(1408)=(from=125246,to=125212) - to_title(1409)=(from=125247,to=125213) - to_title(1410)=(from=125248,to=125214) - to_title(1411)=(from=125249,to=125215) - to_title(1412)=(from=125250,to=125216) - to_title(1413)=(from=125251,to=125217) -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/AmmoPickupStalker.uc b/sources/Features/FixAmmoSelling/AmmoPickupStalker.uc deleted file mode 100644 index 603f2f9..0000000 --- a/sources/Features/FixAmmoSelling/AmmoPickupStalker.uc +++ /dev/null @@ -1,85 +0,0 @@ -/** - * This actor attaches itself to the ammo boxes - * and imitates their collision to let us detect when they're picked up. - * 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 AmmoPickupStalker extends Actor; - -// Ammo box this stalker is attached to. -// If it is destroyed (not just picked up) - stalker must die too. -var private KFAmmoPickup target; - -// This variable is used to record if our 'target' ammo box was in -// active state ('Pickup') last time we've checked. -// We need this because ammo box's 'Touch' event can fire off first and -// force the box to sleep before stalker could catch same event. -// Without this variable we would have no way to know if player -// simply walked near the place of a sleeping box or actually grabbed it. -var private bool wasActive; - -// Static function that spawns a new stalker for the given box. -// Careful, as there's no checks for whether a stalker is -// already attached to it. -// Ensuring that is on the user of the function. -public final static function StalkAmmoPickup(KFAmmoPickup newTarget) -{ - local AmmoPickupStalker newStalker; - if (newTarget == none) return; - - newStalker = newTarget.Spawn(class'AmmoPickupStalker'); - newStalker.target = newTarget; - newStalker.SetBase(newTarget); - newStalker.SetCollision(true); - newStalker.SetCollisionSize(newTarget.collisionRadius, - newTarget.collisionHeight); -} - -event Touch(Actor other) -{ - local FixAmmoSelling ammoSellingFix; - if (target == none) return; - // If our box was sleeping for while (more than a tick), - - // player couldn't have gotten any ammo. - if (!wasActive && !target.IsInState('Pickup')) return; - - ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance()); - if (ammoSellingFix != none) - { - ammoSellingFix.RecordAmmoPickup(Pawn(other), target); - } -} - -event Tick(float delta) -{ - if (target != none) - { - wasActive = target.IsInState('Pickup'); - } - else - { - Destroy(); - } -} - -defaultproperties -{ - // Server-only, hidden - remoteRole = ROLE_None - bAlwaysRelevant = true - drawType = DT_None -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixAmmoSelling.uc b/sources/Features/FixAmmoSelling/FixAmmoSelling.uc deleted file mode 100644 index 88f4541..0000000 --- a/sources/Features/FixAmmoSelling/FixAmmoSelling.uc +++ /dev/null @@ -1,395 +0,0 @@ -/** - * This feature addressed an oversight in vanilla code that - * allows clients to sell weapon's ammunition. - * Moreover, when being sold, ammunition cost is always multiplied by 0.75, - * without taking into an account possible discount a player might have. - * This allows cheaters to "print money" by buying and selling ammo over and - * over again ammunition for some weapons, - * notably pipe bombs (74% discount for lvl6 demolition) - * and crossbow (42% discount for lvl6 sharpshooter). - * - * This feature fixes this problem by setting 'pickupClass' variable in - * potentially abusable weapons to our own value that won't receive a discount. - * Luckily for us, it seems that pickup spawn and discount checks are the only - * two place where variable is directly checked in a vanilla game's code - * ('default.pickupClass' is used everywhere else), - * so we can easily deal with the side effects of such change. - * 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 FixAmmoSelling extends Feature; - -/** - * We will replace 'pickupClass' variable for all instances of potentially - * abusable weapons. That is weapons, that have a discount for their ammunition - * (via 'GetAmmoCostScaling' function in a corresponding perk class). - * They are defined (along with our pickup replacements) in 'rules' array. - * That array isn't configurable, since the abusable status is hardcoded into - * perk classes and the main mod that allows to change those (ServerPerks), - * also solves ammo selling by a more direct method - * (only available for the mods that replace player pawn class). - * This change already completely fixes ammo printing. - * Possible concern with changing the value of 'pickupClass' is that - * it might affect gameplay in too many ways. - * But, luckily for us, that value is only used when spawning a new pickup and - * in 'ServerBuyAmmo' function of 'KFPawn' - * (all the other places use it's default value instead). - * This means that the only two side-effects of our change are: - * 1. That wrong pickup class will be spawned. This problem is easily - * solved by replacing spawned actor in 'CheckReplacement'. - * 2. That ammo will be sold at a different (lower for us) price, - * while trader would still display and require the original price. - * This problem is solved by manually taking from player the difference - * between what he should have had to pay and what he actually paid. - * This brings us to the second issue - - * detecting when player bought the ammo. - * Unfortunately, it doesn't seem possible to detect with 100% certainty - * without replacing pawn or shop classes, - * so we have to eliminate other possibilities. - * There are seem to be three ways for players to get more ammo: - * 1. For some mod to give it; - * 2. Found it an ammo box; - * 3. To buy ammo (can only happen in trader). - * We don't want to provide mods with low-level API for bug fixes, - * so to ensure the compatibility, mods that want to increase ammo values - * will have to solve compatibility issue by themselves: - * either by reimplementing this fix (possibly the best option) - * or by giving players appropriate money along with the ammo. - * The only other case we have to eliminate is ammo boxes. - * First, all cases of ammo boxes outside the trader are easy to detect, - * since in this case we can be sure that player didn't buy ammo - * (and mods that can allow it can just get rid of - * 'ServerSellAmmo' function directly, similarly to how ServerPerks does it). - * We'll detect all the other boxes by attaching an auxiliary actor - * ('AmmoPickupStalker') to them, that will fire off 'Touch' event - * at the same time as ammo boxes. - * The only possible problem is that part of the ammo cost is - * taken with a slight delay, which leaves cheaters a window of opportunity - * to buy more than they can afford. - * This issue is addressed by each ammo type costing as little as possible - * (its' cost for corresponding perk at lvl6) - * and a flag that does allow players to go into negative dosh values - * (the cost is potential bugs in this fix itself, that - * can somewhat affect regular players). - */ - -// Due to how this fix works, players with level below 6 get charged less -// than necessary by the shop and this fix must take the rest of -// the cost by itself. -// The problem is, due to how ammo purchase is coded, low-level (<6 lvl) -// players can actually buy more ammo for "fixed" weapons than they can afford -// by filling ammo for one or all weapons. -// Setting this flag to 'true' will allow us to still take full cost -// from them, putting them in "debt" (having negative dosh amount). -// If you don't want to have players with negative dosh values on your server -// as a side-effect of this fix, then leave this flag as 'false', -// letting low level players buy ammo cheaper -// (but not cheaper than lvl6 could). -// NOTE: this issue doesn't affect level 6 players. -// NOTE #2: this fix does give players below level 6 some -// technical advantage compared to vanilla game, but this advantage -// cannot exceed benefits of having level 6. -var private config const bool allowNegativeDosh; - -// This structure records what classes of weapons can be abused -// and what pickup class we should use to fix the exploit. -struct ReplacementRule -{ - var class abusableWeapon; - var class pickupReplacement; -}; - -// Actual list of abusable weapons. -var private const array rules; - -// We create one such record for any -// abusable weapon instance in the game to store: -struct WeaponRecord -{ - // The instance itself. - var KFWeapon weapon; - // Corresponding ammo instance - // (all abusable weapons only have one ammo type). - var KFAmmunition ammo; - // Last ammo amount we've seen, used to detect players gaining ammo - // (from either ammo boxes or buying it). - var int lastAmmoAmount; -}; - -// All weapons we've detected so far. -var private array registeredWeapons; - -protected function OnEnabled() -{ - local KFWeapon nextWeapon; - local KFAmmoPickup nextPickup; - // Find all abusable weapons - foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon) - { - FixWeapon(nextWeapon); - } - // Start tracking all ammo boxes - foreach level.DynamicActors(class'KFMod.KFAmmoPickup', nextPickup) - { - class'AmmoPickupStalker'.static.StalkAmmoPickup(nextPickup); - } -} - -protected function OnDisabled() -{ - local int i; - local AmmoPickupStalker nextStalker; - local array stalkers; - // Restore all the 'pickupClass' variables we've changed. - for (i = 0; i < registeredWeapons.length; i += 1) - { - if (registeredWeapons[i].weapon != none) - { - registeredWeapons[i].weapon.pickupClass = - registeredWeapons[i].weapon.default.pickupClass; - } - } - registeredWeapons.length = 0; - // Kill all the stalkers; - // to be safe, avoid destroying them directly in the iterator. - foreach level.DynamicActors(class'AmmoPickupStalker', nextStalker) - { - stalkers[stalkers.length] = nextStalker; - } - for (i = 0; i < stalkers.length; i += 1) - { - if (stalkers[i] != none) - { - stalkers[i].Destroy(); - } - } -} - -// Checks if given class is a one of our pickup replacer classes. -public static final function bool IsReplacer(class pickupClass) -{ - local int i; - if (pickupClass == none) return false; - for (i = 0; i < default.rules.length; i += 1) - { - if (pickupClass == default.rules[i].pickupReplacement) - { - return true; - } - } - return false; -} - -// 1. Checks if weapon can be abused and if it can, - fixes the problem. -// 2. Starts tracking abusable weapon to detect when player buys ammo for it. -public final function FixWeapon(KFWeapon potentialAbuser) -{ - local int i; - local WeaponRecord newRecord; - if (potentialAbuser == none) return; - - for (i = 0; i < registeredWeapons.length; i += 1) - { - if (registeredWeapons[i].weapon == potentialAbuser) - { - return; - } - } - for (i = 0; i < rules.length; i += 1) - { - if (potentialAbuser.class == rules[i].abusableWeapon) - { - potentialAbuser.pickupClass = rules[i].pickupReplacement; - newRecord.weapon = potentialAbuser; - registeredWeapons[registeredWeapons.length] = newRecord; - return; - } - } -} - -// Finds ammo instance for recorded weapon in it's owner's inventory. -private final function WeaponRecord FindAmmoInstance(WeaponRecord record) -{ - local Inventory invIter; - local KFAmmunition ammo; - if (record.weapon == none) return record; - if (record.weapon.instigator == none) return record; - - // Find instances anew - invIter = record.weapon.instigator.inventory; - while (invIter != none) - { - if (record.weapon.ammoClass[0] == invIter.class) - { - ammo = KFAmmunition(invIter); - } - invIter = invIter.inventory; - } - // Add missing instances - if (ammo != none) - { - record.ammo = ammo; - record.lastAmmoAmount = ammo.ammoAmount; - } - return record; -} - -// Calculates how much more player should have paid for 'ammoAmount' -// amount of ammo, compared to how much trader took after our fix. -private final function float GetPriceCorrection -( - KFWeapon kfWeapon, - int ammoAmount -) -{ - local float boughtMagFraction; - // 'vanillaPrice' - price that would be calculated - // without our interference - // 'fixPrice' - price that will be calculated after - // we've replaced pickup class - local float vanillaPrice, fixPrice; - local KFPlayerReplicationInfo kfRI; - local class vanillaPickupClass, fixPickupClass; - if (kfWeapon == none || kfWeapon.instigator == none) return 0.0; - fixPickupClass = class(kfWeapon.pickupClass); - vanillaPickupClass = class(kfWeapon.default.pickupClass); - if (fixPickupClass == none || vanillaPickupClass == none) return 0.0; - - // Calculate base prices - boughtMagFraction = (float(ammoAmount) / kfWeapon.default.magCapacity); - fixPrice = boughtMagFraction * fixPickupClass.default.AmmoCost; - vanillaPrice = boughtMagFraction * vanillaPickupClass.default.AmmoCost; - // Apply perk discount for vanilla price - // (we don't need to consider secondary ammo or husk gun special cases, - // since such weapons can't be abused via ammo dosh-printing) - kfRI = KFPlayerReplicationInfo(kfWeapon.instigator.playerReplicationInfo); - if (kfRI != none && kfRI.clientVeteranSkill != none) - { - vanillaPrice *= kfRI.clientVeteranSkill.static. - GetAmmoCostScaling(kfRI, vanillaPickupClass); - } - // TWI's code rounds up ammo cost - // to the integer value whenever ammo is bought, - // so to calculate exactly how much we need to correct the cost, - // we must find difference between the final, rounded cost values. - return float(Max(0, int(vanillaPrice) - int(fixPrice))); -} - -// Takes current ammo and last recorded in 'record' value to calculate -// how much money to take from the player -// (calculations are done via 'GetPriceCorrection'). -private final function WeaponRecord TaxAmmoChange(WeaponRecord record) -{ - local int ammoDiff; - local KFPawn taxPayer; - local PlayerReplicationInfo replicationInfo; - taxPayer = KFPawn(record.weapon.instigator); - if (record.weapon == none || taxPayer == none) return record; - // No need to charge money if player couldn't have - // possibly bought the ammo. - if (!taxPayer.CanBuyNow()) return record; - // Find ammo difference with recorded value. - if (record.ammo != none) - { - ammoDiff = Max(0, record.ammo.ammoAmount - record.lastAmmoAmount); - record.lastAmmoAmount = record.ammo.ammoAmount; - } - // Make player pay dosh - replicationInfo = taxPayer.playerReplicationInfo; - if (replicationInfo != none) - { - replicationInfo.score -= GetPriceCorrection(record.weapon, ammoDiff); - // This shouldn't happen, since shop is supposed to make sure - // player has enough dosh to buy ammo at full price - // (actual price + our correction). - // But if user is extra concerned about it, - - // we can additionally for force the score above 0. - if (!allowNegativeDosh) - { - replicationInfo.score = FMax(0, replicationInfo.score); - } - } - return record; -} - -// Changes our records to account for player picking up the ammo box, -// to avoid charging his for it. -public final function RecordAmmoPickup(Pawn pawnWithAmmo, KFAmmoPickup pickup) -{ - local int i; - local int newAmount; - // Check conditions from 'KFAmmoPickup' code ('Touch' function) - if (pickup == none) return; - if (pawnWithAmmo == none) return; - if (pawnWithAmmo.controller == none) return; - if (!pawnWithAmmo.bCanPickupInventory) return; - if (!FastTrace(pawnWithAmmo.location, pickup.location)) return; - - // Add relevant amount of ammo to our records - for (i = 0; i < registeredWeapons.length; i += 1) - { - if (registeredWeapons[i].weapon == none) continue; - if (registeredWeapons[i].weapon.instigator == pawnWithAmmo) - { - newAmount = registeredWeapons[i].lastAmmoAmount - + registeredWeapons[i].ammo.ammoPickupAmount; - newAmount = Min(registeredWeapons[i].ammo.maxAmmo, newAmount); - registeredWeapons[i].lastAmmoAmount = newAmount; - } - } -} - -event Tick(float delta) -{ - local int i; - // For all the weapon records... - i = 0; - while (i < registeredWeapons.length) - { - // ...remove dead records - if (registeredWeapons[i].weapon == none) - { - registeredWeapons.Remove(i, 1); - continue; - } - // ...find ammo if it's missing - if (registeredWeapons[i].ammo == none) - { - registeredWeapons[i] = FindAmmoInstance(registeredWeapons[i]); - } - // ...tax for ammo, if we can - registeredWeapons[i] = TaxAmmoChange(registeredWeapons[i]); - i += 1; - } -} - -defaultproperties -{ - allowNegativeDosh = false - rules(0)=(abusableWeapon=class'KFMod.Crossbow',pickupReplacement=class'FixAmmoSellingClass_CrossbowPickup') - rules(1)=(abusableWeapon=class'KFMod.PipeBombExplosive',pickupReplacement=class'FixAmmoSellingClass_PipeBombPickup') - rules(2)=(abusableWeapon=class'KFMod.M79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M79Pickup') - rules(3)=(abusableWeapon=class'KFMod.GoldenM79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_GoldenM79Pickup') - rules(4)=(abusableWeapon=class'KFMod.M32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M32Pickup') - rules(5)=(abusableWeapon=class'KFMod.CamoM32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_CamoM32Pickup') - rules(6)=(abusableWeapon=class'KFMod.LAW',pickupReplacement=class'FixAmmoSellingClass_LAWPickup') - rules(7)=(abusableWeapon=class'KFMod.SPGrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_SPGrenadePickup') - rules(8)=(abusableWeapon=class'KFMod.SealSquealHarpoonBomber',pickupReplacement=class'FixAmmoSellingClass_SealSquealPickup') - rules(9)=(abusableWeapon=class'KFMod.SeekerSixRocketLauncher',pickupReplacement=class'FixAmmoSellingClass_SeekerSixPickup') - // Listeners - requiredListeners(0) = class'MutatorListener_FixAmmoSelling' -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc deleted file mode 100644 index 9c55c9e..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that - * of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_CamoM32Pickup extends CamoM32Pickup; - -defaultproperties -{ - AmmoCost = 42 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc deleted file mode 100644 index 6fd07c1..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for xbow to that - * of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_CrossbowPickup extends CrossbowPickup; - -defaultproperties -{ - AmmoCost = 11.6 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc deleted file mode 100644 index ae1e1a3..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for m79 to that - * of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_GoldenM79Pickup extends GoldenM79Pickup; - -defaultproperties -{ - AmmoCost = 7 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc deleted file mode 100644 index d1cc802..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for LAW to that - * of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_LAWPickup extends LAWPickup; - -defaultproperties -{ - AmmoCost = 21 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc deleted file mode 100644 index 04f0321..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that - * of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_M32Pickup extends M32Pickup; - -defaultproperties -{ - AmmoCost = 42 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc deleted file mode 100644 index 21a93f4..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for M79 to that - * of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_M79Pickup extends M79Pickup; - -defaultproperties -{ - AmmoCost = 7 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc deleted file mode 100644 index ff2041c..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for pipes - * to that of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_PipeBombPickup extends PipeBombPickup; - -defaultproperties -{ - AmmoCost = 195 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc deleted file mode 100644 index 5a1a29c..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc +++ /dev/null @@ -1,27 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for - * orca grnade launcher to that of a level 6 player - * and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_SPGrenadePickup extends SPGrenadePickup; - -defaultproperties -{ - AmmoCost = 7 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc deleted file mode 100644 index c80c56f..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for harpoon - * to that of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_SealSquealPickup extends SealSquealPickup; - -defaultproperties -{ - AmmoCost = 21 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc deleted file mode 100644 index 31d3c26..0000000 --- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * A helper class for 'FixAmmoSelling' that sets ammo cost for seeker - * to that of a level 6 player and doesn't allow for a perk discount. - * Copyright 2019 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 FixAmmoSellingClass_SeekerSixPickup extends SeekerSixPickup; - -defaultproperties -{ - AmmoCost = 10.5 -} \ No newline at end of file diff --git a/sources/Features/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc b/sources/Features/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc deleted file mode 100644 index 78178de..0000000 --- a/sources/Features/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Overloaded mutator events listener to register every new - * spawned weapon and ammo pickup. - * 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 MutatorListener_FixAmmoSelling extends MutatorListenerBase - abstract; - -static function bool CheckReplacement(Actor other, out byte isSuperRelevant) -{ - if (other == none) return true; - - // We need to replace pickup classes back, - // as they might not even exist on clients. - if (class'FixAmmoSelling'.static.IsReplacer(other.class)) - { - ReplacePickupWith(Pickup(other)); - return false; - } - CheckAbusableWeapon(KFWeapon(other)); - // If it's ammo pickup - we need to stalk it - class'AmmoPickupStalker'.static.StalkAmmoPickup(KFAmmoPickup(other)); - return true; -} - -private static function CheckAbusableWeapon(KFWeapon newWeapon) -{ - local FixAmmoSelling ammoSellingFix; - if (newWeapon == none) return; - ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance()); - if (ammoSellingFix == none) return; - ammoSellingFix.FixWeapon(newWeapon); -} - -// This function recreates the logic of 'KFWeapon.DropFrom()', -// since standard 'ReplaceWith' function produces bad results. -private static function ReplacePickupWith(Pickup oldPickup) -{ - local Pawn instigator; - local Pickup newPickup; - local KFWeapon relevantWeapon; - if (oldPickup == none) return; - instigator = oldPickup.instigator; - if (instigator == none) return; - relevantWeapon = GetWeaponOfClass(instigator, oldPickup.inventoryType); - if (relevantWeapon == none) return; - - newPickup = relevantWeapon.Spawn( relevantWeapon.default.pickupClass,,, - relevantWeapon.location); - newPickup.InitDroppedPickupFor(relevantWeapon); - newPickup.velocity = relevantWeapon.velocity + - Vector(instigator.rotation) * 100; - if (instigator.health > 0) - KFWeaponPickup(newPickup).bThrown = true; -} - -// TODO: this is code duplication, some sort of solution is needed -static final function KFWeapon GetWeaponOfClass -( - Pawn playerPawn, - class weaponClass -) -{ - local Inventory invIter; - if (playerPawn == none) return none; - - invIter = playerPawn.inventory; - while (invIter != none) - { - if (invIter.class == weaponClass) - { - return KFWeapon(invIter); - } - invIter = invIter.inventory; - } - return none; -} - -defaultproperties -{ - relatedEvents = class'MutatorEvents' -} \ No newline at end of file diff --git a/sources/Features/FixDoshSpam/FixDoshSpam.uc b/sources/Features/FixDoshSpam/FixDoshSpam.uc deleted file mode 100644 index ea298a0..0000000 --- a/sources/Features/FixDoshSpam/FixDoshSpam.uc +++ /dev/null @@ -1,252 +0,0 @@ -/** - * This feature addressed two dosh-related issues: - * 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash'; - * 2. Breaking collision detection logic by stacking large amount of - * 'CashPickup' actors in one place, which allows one to either - * reach unintended locations or even instantly kill zeds. - * - * It fixes them by limiting speed, with which dosh can spawn, and - * allowing this limit to decrease when there's already too much dosh - * present on the map. - * Copyright 2019 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 FixDoshSpam extends Feature; - -/** - * First, we limit amount of dosh that can be spawned simultaneously. - * The simplest method is to place a cooldown on spawning 'CashPickup' actors, - * i.e. after spawning one 'CashPickup' we'd completely prevent spawning - * any other instances of it for a fixed amount of time. - * However, that might allow a malicious spammer to block others from - * throwing dosh, - all he needs to do is to spam dosh at right time intervals. - * We'll resolve this issue by recording how many 'CashPickup' actors - * each player has spawned as their "contribution" and decay - * that value with time, only allowing to spawn new dosh after - * contribution decayed to zero. Speed of decay is derived from current dosh - * spawning speed limit and decreases with amount of players - * with non-zero contributions (since it means that they're throwing dosh). - * Second issue is player amassing a large amount of dosh in one point - * that leads to skipping collision checks, which then allows players to pass - * through level geometry or enter zeds' collisions, instantly killing them. - * Since dosh disappears on it's own, the easiest method to prevent that is to - * severely limit how much dosh players can throw per second, - * so that there's never enough dosh laying around to affect collision logic. - * The downside to such severe limitations is that game behaves less - * vanilla-like, where you could throw away streams of dosh. - * To solve that we'll first use a more generous limit on dosh players can - * throw per second, but will track how much dosh is currently present - * in a level and linearly decelerate speed, according to that amount. - */ - -// Highest and lowest speed with which players can throw dosh wads. -// It'll be evenly spread between all players. -// For example, if speed is set to 6 and only one player will be spamming dosh, -// - he'll be able to throw 6 wads of dosh per second; -// but if all 6 players are spamming it, - each will throw only 1 per second. -// NOTE: these speed values can be exceeded, since a player is guaranteed -// to be able to throw at least one wad of dosh, if he didn't do so in awhile. -// NOTE #2: if maximum value is less than minimum one, -// the lowest (maximum one) will be used. -var private config const float doshPerSecondLimitMax; -var private config const float doshPerSecondLimitMin; -// Amount of dosh pickups on the map at which we must set dosh per second -// to 'doshPerSecondLimitMin'. -// We use 'doshPerSecondLimitMax' when there's no dosh on the map and -// scale linearly between them as it's amount grows. -var private config const int criticalDoshAmount; - -// To limit dosh spawning speed we need some measure of -// time passage between ticks. -// This variable stores last value seen by us as a good approximation. -// It's a real (not in-game) time. -var private float lastTickDuration; - -// This structure records how much a certain player has -// contributed to an overall dosh creation. -struct DoshStreamPerPlayer -{ - var PlayerController player; - // Amount of dosh we remember this player creating, decays with time. - var float contribution; -}; -var private array currentContributors; - -// Wads of cash that are lying around on the map. -var private array wads; - -protected function OnEnabled() -{ - local CashPickup nextCash; - // Find all wads of cash laying around on the map, - // so that we could accordingly limit the cash spam. - foreach level.DynamicActors(class'KFMod.CashPickup', nextCash) - { - wads[wads.length] = nextCash; - } -} - -protected function OnDisabled() -{ - wads.length = 0; - currentContributors.length = 0; -} - -// Did player with this controller contribute to the latest dosh generation? -public final function bool IsContributor(PlayerController player) -{ - return (GetContributorIndex(player) >= 0); -} - -// Did we already reach allowed limit of dosh per second? -public final function bool IsDoshStreamOverLimit() -{ - local int i; - local float overallContribution; - overallContribution = 0.0; - for (i = 0; i < currentContributors.length; i += 1) - { - overallContribution += currentContributors[i].contribution; - } - return (overallContribution > lastTickDuration * GetCurrentDPSLimit()); -} - -// What is our current dosh per second limit? -private final function float GetCurrentDPSLimit() -{ - local float speedScale; - if (doshPerSecondLimitMax < doshPerSecondLimitMin) - { - return doshPerSecondLimitMax; - } - speedScale = Float(wads.length) / Float(criticalDoshAmount); - speedScale = FClamp(speedScale, 0.0, 1.0); - // At 0.0 scale (no dosh on the map) - use max speed - // At 1.0 scale (critical dosh on the map) - use min speed - return Lerp(speedScale, doshPerSecondLimitMax, doshPerSecondLimitMin); -} - -// Returns index of the contributor corresponding to the given controller. -// Returns '-1' if no connection correspond to the given controller. -// Returns '-1' if given controller is equal to 'none'. -private final function int GetContributorIndex(PlayerController player) -{ - local int i; - if (player == none) return -1; - - for (i = 0; i < currentContributors.length; i += 1) - { - if (currentContributors[i].player == player) - { - return i; - } - } - return -1; -} - -// Adds given cash to given player contribution record and -// registers that cash in our wads array. -// Does nothing if given cash was already registered. -public final function AddContribution(PlayerController player, CashPickup cash) -{ - local int i; - local int playerIndex; - local DoshStreamPerPlayer newStreamRecord; - // Check if given dosh was already accounted for. - for (i = 0; i < wads.length; i += 1) - { - if (cash == wads[i]) - { - return; - } - } - wads[wads.length] = cash; - // Add contribution to player - playerIndex = GetContributorIndex(player); - if (playerIndex >= 0) - { - currentContributors[playerIndex].contribution += 1.0; - return; - } - newStreamRecord.player = player; - newStreamRecord.contribution = 1.0; - currentContributors[currentContributors.length] = newStreamRecord; -} - -private final function ReducePlayerContributions(float trueTimePassed) -{ - local int i; - local float streamReduction; - streamReduction = trueTimePassed * - (GetCurrentDPSLimit() / currentContributors.length); - for (i = 0; i < currentContributors.length; i += 1) - { - currentContributors[i].contribution -= streamReduction; - } -} - -// Clean out wads that disappeared or were picked up by players. -private final function CleanWadsArray() -{ - local int i; - i = 0; - while (i < wads.length) - { - if (wads[i] == none) - { - wads.Remove(i, 1); - } - else - { - i += 1; - } - } -} - -// Don't track players that no longer contribute to dosh generation. -private final function RemoveNonContributors() -{ - local int i; - local array updContributors; - for (i = 0; i < currentContributors.length; i += 1) - { - // We want to keep on record even players that quit, - // since their contribution still must be accounted for. - if (currentContributors[i].contribution <= 0.0) continue; - updContributors[updContributors.length] = currentContributors[i]; - } - currentContributors = updContributors; -} - -event Tick(float delta) -{ - local float trueTimePassed; - trueTimePassed = delta * (1.1 / level.timeDilation); - CleanWadsArray(); - ReducePlayerContributions(trueTimePassed); - RemoveNonContributors(); - lastTickDuration = trueTimePassed; -} - -defaultproperties -{ - doshPerSecondLimitMax = 50 - doshPerSecondLimitMin = 5 - criticalDoshAmount = 25 - // Listeners - requiredListeners(0) = class'MutatorListener_FixDoshSpam' -} \ No newline at end of file diff --git a/sources/Features/FixDoshSpam/MutatorListener_FixDoshSpam.uc b/sources/Features/FixDoshSpam/MutatorListener_FixDoshSpam.uc deleted file mode 100644 index 5b85353..0000000 --- a/sources/Features/FixDoshSpam/MutatorListener_FixDoshSpam.uc +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Overloaded mutator events listener to catch and, possibly, - * prevent spawning dosh actors. - * Copyright 2019 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 MutatorListener_FixDoshSpam extends MutatorListenerBase - abstract; - -static function bool CheckReplacement(Actor other, out byte isSuperRelevant) -{ - local FixDoshSpam doshFix; - local PlayerController player; - if (other.class != class'CashPickup') return true; - // This means this dosh wasn't spawned in 'TossCash' of 'KFPawn', - // so it isn't related to the exploit we're trying to fix. - if (other.instigator == none) return true; - doshFix = FixDoshSpam(class'FixDoshSpam'.static.GetInstance()); - if (doshFix == none) return true; - - // We only want to prevent spawning cash if we're already over - // the limit and the one trying to throw this cash contributed to it. - // We allow other players to throw at least one wad of cash. - player = PlayerController(other.instigator.controller); - if (doshFix.IsDoshStreamOverLimit() && doshFix.IsContributor(player)) - { - return false; - } - // If we do spawn cash - record this contribution. - doshFix.AddContribution(player, CashPickup(other)); - return true; -} - -defaultproperties -{ - relatedEvents = class'MutatorEvents' -} \ No newline at end of file diff --git a/sources/Features/FixDualiesCost/DualiesCostRule.uc b/sources/Features/FixDualiesCost/DualiesCostRule.uc deleted file mode 100644 index aa03156..0000000 --- a/sources/Features/FixDualiesCost/DualiesCostRule.uc +++ /dev/null @@ -1,45 +0,0 @@ -/** - * This rule detects any pickup events to allow us to - * properly record and/or fix pistols' prices. - * Copyright 2019 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 DualiesCostRule extends GameRules; - -function bool OverridePickupQuery -( - Pawn other, - Pickup item, - out byte allowPickup -) -{ - local KFWeaponPickup weaponPickup; - local FixDualiesCost dualiesCostFix; - weaponPickup = KFWeaponPickup(item); - dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance()); - if (weaponPickup != none && dualiesCostFix != none) - { - dualiesCostFix.ApplyPendingValues(); - dualiesCostFix.StoreSinglePistolValues(); - dualiesCostFix.SetNextSellValue(weaponPickup.sellValue); - } - return super.OverridePickupQuery(other, item, allowPickup); -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Features/FixDualiesCost/FixDualiesCost.uc b/sources/Features/FixDualiesCost/FixDualiesCost.uc deleted file mode 100644 index 639a689..0000000 --- a/sources/Features/FixDualiesCost/FixDualiesCost.uc +++ /dev/null @@ -1,454 +0,0 @@ -/** - * This feature fixes several issues related to the selling price of both - * single and dual pistols, all originating from the existence of dual weapons. - * Most notable issue is the ability to "print" money by buying and - * selling pistols in a certain way. - * - * It fixes all of the issues by manually setting pistols' - * 'SellValue' variables to proper values. - * Fix only works with vanilla pistols, as it's unpredictable what - * custom ones can do and they can handle these issues on their own - * in a better way. - * 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 FixDualiesCost extends Feature; - -/** - * Issues with pistols' cost may look varied and surface in - * a plethora of ways, but all of them originate from the two main errors - * in vanilla's code: - * 1. If you have a pistol in your inventory at the time when you - * buy/pickup another one - the sell value of resulting dualies is - * incorrectly set to the sell value of the second pistol; - * 2. When player has dual pistols and drops one on the floor, - - * the sell value for the one left with the player isn't set. - * All weapons in Killing Floor get sell value assigned to them - * (appropriately, in a 'SellValue' variable). This is to ensure that the sell - * price is set the moment players buys the gun. Otherwise, due to ridiculous - * perked discounts, you'd be able to buy a pistol at 30% price - * as sharpshooter, but sell at 75% of a price as any other perk, - * resulting in 45% of pure profit. - * Unfortunately, that's exactly what happens when 'SellValue' isn't set - * (left as it's default value of '-1'): sell value of such weapons is - * determined only at the moment of sale and depends on the perk of the seller, - * allowing for possible exploits. - * - * These issues are fixed by directly assigning - * proper values to 'SellValue'. To do that we need to detect when player - * buys/sells/drops/picks up weapons, which we accomplish by catching - * 'CheckReplacement' event for weapon instances. This approach has two issues. - * One is that, if vanilla's code sets an incorrect sell value, - - * it's doing it after weapon is spawned and, therefore, - * after 'CheckReplacement' call, so we have, instead, to remember to do - * it later, as early as possible - * (either the next tick or before another operation with weapons). - * Another issue is that when you have a pistol and pick up a pistol of - * the same type, - at the moment dualies instance is spawned, - * the original pistol in player's inventory is gone and we can't use - * it's sell value to calculate new value of dual pistols. - * This problem is solved by separately recording the value for every - * single pistol every tick. - * However, if pistol pickups are placed close enough together on the map, - * player can start touching them (which triggers a pickup) at the same time, - * picking them both in a single tick. This leaves us no room to record - * the value of a single pistol players picks up first. - * To get it we use game rules to catch 'OverridePickupQuery' event that's - * called before the first one gets destroyed, - * but after it's sell value was already set. - * Last issue is that when player picks up a second pistol - we don't know - * it's sell value and, therefore, can't calculate value of dual pistols. - * This is resolved by recording that value directly from a pickup, - * in abovementioned function 'OverridePickupQuery'. - * NOTE: 9mm is an exception due to the fact that you always have at least - * one and the last one can't be sold. We'll deal with it by setting - * the following rule: sell value of the un-droppable pistol is always 0 - * and the value of a pair of 9mms is the value of the single droppable pistol. - */ - -// Some issues involve possible decrease in pistols' price and -// don't lead to exploit, but are still bugs and require fixing. -// If you have a Deagle in your inventory and then get another one -// (by either buying or picking it off the ground) - the price of resulting -// dual pistols will be set to the price of the last deagle, -// like the first one wasn't worth anything at all. -// In particular this means that (prices are off-perk for more clarity): -// 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of -// the cost (+750 do$h), you lose 250 do$h; -// 2. If you first buy a deagle (-500 do$h), then buy -// the second one (-500 do$h) and then sell them, you'll only get -// 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h; -// 3. So if you already have bought a deagle (-500 do$h), -// you can get a more expensive weapon by doing a stupid thing -// and first selling your Deagle (+375 do$h), -// then buying dual deagles (-1000 do$h). -// If you sell them after that, you'll gain 75% of the cost of -// dual deagles (+750 do$h), leaving you with losing only 375 do$h. -// Of course, situations described above are only relevant if you're planning -// to sell your weapons at some point and most people won't even notice it. -// But such an oversight still shouldn't exist in a game and we fix it by -// setting sell value of dualies as a sum of values of each pistol. -// Yet, fixing this issue leads to players having more expensive -// (while fairly priced) weapons than on vanilla, technically making -// the game easier. And some people might object to having that in -// a whitelisted bug-fixing feature. -// These people are, without a question, complete degenerates. -// But making mods for only non-mentally challenged isn't inclusive. -// So we add this option. -// Set it to 'false' if you only want to fix ammo printing -// and leave the rest of the bullshit as-is. -var private config const bool allowSellValueIncrease; - -// Describe all the possible pairs of dual pistols in a vanilla game. -struct DualiesPair -{ - var class single; - var class dual; -}; -var private const array dualiesClasses; - -// Describe sell values that need to be applied at earliest later point. -struct WeaponValuePair -{ - var KFWeapon weapon; - var float value; -}; -var private const array pendingValues; - -// Describe sell values of all currently existing single pistols. -struct WeaponDataRecord -{ - var KFWeapon reference; - var class class; - var float value; - // The whole point of this structure is to remember value of a weapon - // after it's destroyed. Since 'reference' will become 'none' by then, - // we will use the 'owner' reference to identify the weapon. - var Pawn owner; -}; -var private const array storedValues; - -// Sell value of the last seen pickup in 'OverridePickupQuery' -var private int nextSellValue; - -protected function OnEnabled() -{ - local KFWeapon nextWeapon; - // Find all frags, that spawned when this fix wasn't running. - foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon) - { - RegisterSinglePistol(nextWeapon, false); - } - level.game.AddGameModifier(Spawn(class'DualiesCostRule')); -} - -protected function OnDisabled() -{ - local GameRules rulesIter; - local DualiesCostRule ruleToDestroy; - // Check first rule - if (level.game.gameRulesModifiers == none) return; - - ruleToDestroy = DualiesCostRule(level.game.gameRulesModifiers); - if (ruleToDestroy != none) - { - level.game.gameRulesModifiers = ruleToDestroy.nextGameRules; - ruleToDestroy.Destroy(); - return; - } - // Check rest of the rules - rulesIter = level.game.gameRulesModifiers; - while (rulesIter != none) - { - ruleToDestroy = DualiesCostRule(rulesIter.nextGameRules); - if (ruleToDestroy != none) - { - rulesIter.nextGameRules = ruleToDestroy.nextGameRules; - ruleToDestroy.Destroy(); - } - rulesIter = rulesIter.nextGameRules; - } -} - -public final function SetNextSellValue(int newValue) -{ - nextSellValue = newValue; -} - -// Finds a weapon of a given class in given 'Pawn' 's inventory. -// Returns 'none' if weapon isn't there. -private final function KFWeapon GetWeaponOfClass -( - Pawn playerPawn, - class weaponClass -) -{ - local Inventory invIter; - if (playerPawn == none) return none; - - invIter = playerPawn.inventory; - while (invIter != none) - { - if (invIter.class == weaponClass) - { - return KFWeapon(invIter); - } - invIter = invIter.inventory; - } - return none; -} - -// Gets weapon index in our record of dual pistol classes. -// Second variable determines whether we're searching for single -// or dual variant: -// ~ 'true' - searching for single -// ~ 'false' - for dual -// Returns '-1' if weapon isn't found -// (dual MK23 won't be found as a single weapon). -private final function int GetIndexAs(KFWeapon weapon, bool asSingle) -{ - local int i; - if (weapon == none) return -1; - - for (i = 0; i < dualiesClasses.length; i += 1) - { - if (asSingle && dualiesClasses[i].single == weapon.class) - { - return i; - } - if (!asSingle && dualiesClasses[i].dual == weapon.class) - { - return i; - } - } - return -1; -} - -// Calculates full cost of a weapon with a discount, -// dependent on it's instigator's perk. -private final function float GetFullCost(KFWeapon weapon) -{ - local float cost; - local class pickupClass; - local KFPlayerReplicationInfo instigatorRI; - if (weapon == none) return 0.0; - pickupClass = class(weapon.default.pickupClass); - if (pickupClass == none) return 0.0; - - cost = pickupClass.default.cost; - if (weapon.instigator != none) - { - instigatorRI = - KFPlayerReplicationInfo(weapon.instigator.playerReplicationInfo); - } - if (instigatorRI != none && instigatorRI.clientVeteranSkill != none) - { - cost *= instigatorRI.clientVeteranSkill.static - .GetCostScaling(instigatorRI, pickupClass); - } - return cost; -} - -// If passed weapon is a pistol - we start tracking it's value; -// Otherwise - do nothing. -public final function RegisterSinglePistol -( - KFWeapon singlePistol, - bool justSpawned -) -{ - local WeaponDataRecord newRecord; - if (singlePistol == none) return; - if (GetIndexAs(singlePistol, true) < 0) return; - - newRecord.reference = singlePistol; - newRecord.class = singlePistol.class; - newRecord.owner = singlePistol.instigator; - if (justSpawned) - { - newRecord.value = nextSellValue; - } - else - { - newRecord.value = singlePistol.sellValue; - } - storedValues[storedValues.length] = newRecord; -} - -// Fixes sell value after player throws one pistol out of a pair. -public final function FixCostAfterThrow(KFWeapon singlePistol) -{ - local int index; - local KFWeapon dualPistols; - if (singlePistol == none) return; - index = GetIndexAs(singlePistol, true); - if (index < 0) return; - dualPistols = GetWeaponOfClass( singlePistol.instigator, - dualiesClasses[index].dual); - if (dualPistols == none) return; - - // Sell value recorded into 'dualPistols' will end up as a value of - // a dropped pickup. - // Sell value of 'singlePistol' will be the value for the pistol, - // left in player's hands. - if (dualPistols.class == class'KFMod.Single') - { - // 9mm is an exception. - // Remaining weapon costs nothing. - singlePistol.sellValue = 0; - // We don't change the sell value of the dropped weapon, - // as it's default behavior to transfer full value of a pair to it. - return; - } - // For other pistols - divide the value. - singlePistol.sellValue = dualPistols.sellValue / 2; - dualPistols.sellValue = singlePistol.sellValue; -} - -// Fixes sell value after buying a pair of dual pistols, -// if player already had a single version. -public final function FixCostAfterBuying(KFWeapon dualPistols) -{ - local int index; - local KFWeapon singlePistol; - local WeaponValuePair newPendingValue; - if (dualPistols == none) return; - index = GetIndexAs(dualPistols, false); - if (index < 0) return; - singlePistol = GetWeaponOfClass(dualPistols.instigator, - dualiesClasses[index].single); - if (singlePistol == none) return; - - // 'singlePistol' will get destroyed, so it's sell value is irrelevant. - // 'dualPistols' will be the new pair of pistols, but it's value will - // get overwritten by vanilla's code after this function. - // So we must add it to pending values to be changed later. - newPendingValue.weapon = dualPistols; - if (dualPistols.class == class'KFMod.Dualies') - { - // 9mm is an exception. - // The value of pair of 9mms is the price of additional pistol, - // that defined as a price of a pair in game. - newPendingValue.value = GetFullCost(dualPistols) * 0.75; - } - else - { - // Otherwise price of a pair is the price of two pistols: - // 'singlePistol.sellValue' - the one we had - // '(FullCost / 2) * 0.75' - and the one we bought - newPendingValue.value = singlePistol.sellValue - + (GetFullCost(dualPistols) / 2) * 0.75; - } - pendingValues[pendingValues.length] = newPendingValue; -} - -// Fixes sell value after player picks up a single pistol, -// while already having one of the same time in his inventory. -public final function FixCostAfterPickUp(KFWeapon dualPistols) -{ - local int i; - local int index; - local KFWeapon singlePistol; - local WeaponValuePair newPendingValue; - if (dualPistols == none) return; - // In both cases of: - // 1. buying dualies, without having a single pistol of - // corresponding type; - // 2. picking up a second pistol, while having another one; - // by the time of 'CheckReplacement' (and, therefore, this function) - // is called, there's no longer any single pistol in player's inventory - // (in first case it never was there, in second - it got destroyed). - // To distinguish between those possibilities we can check the owner of - // the spawned weapon, since it's only set to instigator at the time of - // 'CheckReplacement' when player picks up a weapon. - // So we require that owner exists. - if (dualPistols.owner == none) return; - index = GetIndexAs(dualPistols, false); - if (index < 0) return; - singlePistol = GetWeaponOfClass(dualPistols.instigator, - dualiesClasses[index].single); - if (singlePistol != none) return; - - if (nextSellValue == -1) - { - nextSellValue = GetFullCost(dualPistols) * 0.75; - } - for (i = 0; i < storedValues.length; i += 1) - { - if (storedValues[i].reference != none) continue; - if (storedValues[i].class != dualiesClasses[index].single) continue; - if (storedValues[i].owner != dualPistols.instigator) continue; - newPendingValue.weapon = dualPistols; - newPendingValue.value = storedValues[i].value + nextSellValue; - pendingValues[pendingValues.length] = newPendingValue; - break; - } -} - -public final function ApplyPendingValues() -{ - local int i; - for (i = 0; i < pendingValues.length; i += 1) - { - if (pendingValues[i].weapon == none) continue; - // Our fixes can only increase the correct ('!= -1') - // sell value of weapons, so if we only need to change sell value - // if we're allowed to increase it or it's incorrect. - if (allowSellValueIncrease || pendingValues[i].weapon.sellValue == -1) - { - pendingValues[i].weapon.sellValue = pendingValues[i].value; - } - } - pendingValues.length = 0; -} - -public final function StoreSinglePistolValues() -{ - local int i; - i = 0; - while (i < storedValues.length) - { - if (storedValues[i].reference == none) - { - storedValues.Remove(i, 1); - continue; - } - storedValues[i].owner = storedValues[i].reference.instigator; - storedValues[i].value = storedValues[i].reference.sellValue; - i += 1; - } -} - -event Tick(float delta) -{ - ApplyPendingValues(); - StoreSinglePistolValues(); -} - -defaultproperties -{ - allowSellValueIncrease = true - // Inner variables - dualiesClasses(0)=(single=class'KFMod.Single',dual=class'KFMod.Dualies') - dualiesClasses(1)=(single=class'KFMod.Magnum44Pistol',dual=class'KFMod.Dual44Magnum') - dualiesClasses(2)=(single=class'KFMod.MK23Pistol',dual=class'KFMod.DualMK23Pistol') - dualiesClasses(3)=(single=class'KFMod.Deagle',dual=class'KFMod.DualDeagle') - dualiesClasses(4)=(single=class'KFMod.GoldenDeagle',dual=class'KFMod.GoldenDualDeagle') - dualiesClasses(5)=(single=class'KFMod.FlareRevolver',dual=class'KFMod.DualFlareRevolver') - // Listeners - requiredListeners(0) = class'MutatorListener_FixDualiesCost' -} \ No newline at end of file diff --git a/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc b/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc deleted file mode 100644 index 0f209ae..0000000 --- a/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Overloaded mutator events listener to catch when pistol-type weapons - * (single or dual) are spawned and to correct their price. - * 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 MutatorListener_FixDualiesCost extends MutatorListenerBase - abstract; - -static function bool CheckReplacement(Actor other, out byte isSuperRelevant) -{ - local KFWeapon weapon; - local FixDualiesCost dualiesCostFix; - weapon = KFWeapon(other); - if (weapon == none) return true; - dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance()); - if (dualiesCostFix == none) return true; - - dualiesCostFix.RegisterSinglePistol(weapon, true); - dualiesCostFix.FixCostAfterThrow(weapon); - dualiesCostFix.FixCostAfterBuying(weapon); - dualiesCostFix.FixCostAfterPickUp(weapon); - return true; -} - -defaultproperties -{ - relatedEvents = class'MutatorEvents' -} \ No newline at end of file diff --git a/sources/Features/FixFFHack/FFHackRule.uc b/sources/Features/FixFFHack/FFHackRule.uc deleted file mode 100644 index efe0556..0000000 --- a/sources/Features/FixFFHack/FFHackRule.uc +++ /dev/null @@ -1,74 +0,0 @@ -/** - * This rule detects suspicious attempts to deal damage and - * applies friendly fire scaling according to 'FixFFHack's rules. - * Copyright 2019 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 FFHackRule extends GameRules; - -function int NetDamage -( - int originalDamage, - int damage, - Pawn injured, - Pawn instigator, - Vector hitLocation, - out Vector momentum, - class damageType -) -{ - local KFGameType gameType; - local FixFFHack ffHackFix; - gameType = KFGameType(level.game); - // Something is very wrong and we can just bail on this damage - if (damageType == none || gameType == none) return 0; - - // We only check when suspicious instigators that aren't a world - if (!damageType.default.bCausedByWorld && IsSuspicious(instigator)) - { - ffHackFix = FixFFHack(class'FixFFHack'.static.GetInstance()); - if (ffHackFix != none && ffHackFix.ShouldScaleDamage(damageType)) - { - // Remove pushback to avoid environmental kills - momentum = Vect(0.0, 0.0, 0.0); - damage *= gameType.friendlyFireScale; - } - } - return super.NetDamage( originalDamage, damage, injured, instigator, - hitLocation, momentum, damageType); -} - -private function bool IsSuspicious(Pawn instigator) -{ - // Instigator vanished - if (instigator == none) return true; - - // Instigator already became spectator - if (KFPawn(instigator) != none) - { - if (instigator.playerReplicationInfo != none) - { - return instigator.playerReplicationInfo.bOnlySpectator; - } - return true; // Replication info is gone => suspicious - } - return false; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Features/FixFFHack/FixFFHack.uc b/sources/Features/FixFFHack/FixFFHack.uc deleted file mode 100644 index ed38c9f..0000000 --- a/sources/Features/FixFFHack/FixFFHack.uc +++ /dev/null @@ -1,152 +0,0 @@ -/** - * This feature fixes a bug that can allow players to bypass server's - * friendly fire limitations and teamkill. - * Usual fixes apply friendly fire scale to suspicious damage themselves, which - * also disables some of the environmental damage. - * In order to avoid that, this fix allows server owner to define precisely - * to what damage types to apply the friendly fire scaling. - * It should be all damage types related to projectiles. - * Copyright 2019 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 FixFFHack extends Feature; - -/** - * It's possible to bypass friendly fire damage scaling and always deal - * full damage to other players, if one were to either leave the server or - * spectate right after shooting a projectile. We use game rules to catch - * such occurrences and apply friendly fire scaling to weapons, - * specified by server admins. - * To specify required subset of weapons, one must first - * chose a general rule (scale by default / don't scale by default) and then, - * optionally, add exceptions to it. - * Choosing 'scaleByDefault == true' as a general rule will make this fix - * behave in the similar way to 'KFExplosiveFix' by mutant and will disable - * some environmental sources of damage on some maps. One can then add relevant - * damage classes as exceptions to fix that downside, but making an extensive - * list of such sources might prove problematic. - * On the other hand, setting 'scaleByDefault == false' will allow to get - * rid of team-killing exploits by simply adding damage types of all - * projectile weapons, used on a server. This fix comes with such filled-in - * list of all vanilla projectile classes. - */ - -// Defines a general rule for choosing whether or not to apply -// friendly fire scaling. -// This can be overwritten by exceptions ('alwaysScale' or 'neverScale'). -// Enabling scaling by default without any exceptions in 'neverScale' will -// make this fix behave almost identically to Mutant's 'Explosives Fix Mutator'. -var private config const bool scaleByDefault; -// Damage types, for which we should always reapply friendly fire scaling. -var private config const array< class > alwaysScale; -// Damage types, for which we should never reapply friendly fire scaling. -var private config const array< class > neverScale; - -protected function OnEnabled() -{ - level.game.AddGameModifier(Spawn(class'FFHackRule')); -} - -protected function OnDisabled() -{ - local GameRules rulesIter; - local FFHackRule ruleToDestroy; - // Check first rule - if (level.game.gameRulesModifiers == none) return; - - ruleToDestroy = FFHackRule(level.game.gameRulesModifiers); - if (ruleToDestroy != none) - { - level.game.gameRulesModifiers = ruleToDestroy.nextGameRules; - ruleToDestroy.Destroy(); - return; - } - // Check rest of the rules - rulesIter = level.game.gameRulesModifiers; - while (rulesIter != none) - { - ruleToDestroy = FFHackRule(rulesIter.nextGameRules); - if (ruleToDestroy != none) - { - rulesIter.nextGameRules = ruleToDestroy.nextGameRules; - ruleToDestroy.Destroy(); - } - rulesIter = rulesIter.nextGameRules; - } -} - -// Checks general rule and exception list -public final function bool ShouldScaleDamage(class damageType) -{ - local int i; - local array< class > exceptions; - if (damageType == none) return false; - - if (scaleByDefault) - exceptions = neverScale; - else - exceptions = alwaysScale; - for (i = 0; i < exceptions.length; i += 1) - { - if (exceptions[i] == damageType) - { - return (!scaleByDefault); - } - } - return scaleByDefault; -} - -defaultproperties -{ - scaleByDefault = false - // Vanilla damage types for projectiles - alwaysScale(0) = class'KFMod.DamTypeCrossbuzzsawHeadShot' - alwaysScale(1) = class'KFMod.DamTypeCrossbuzzsaw' - alwaysScale(2) = class'KFMod.DamTypeFrag' - alwaysScale(3) = class'KFMod.DamTypePipeBomb' - alwaysScale(4) = class'KFMod.DamTypeM203Grenade' - alwaysScale(5) = class'KFMod.DamTypeM79Grenade' - alwaysScale(6) = class'KFMod.DamTypeM79GrenadeImpact' - alwaysScale(7) = class'KFMod.DamTypeM32Grenade' - alwaysScale(8) = class'KFMod.DamTypeLAW' - alwaysScale(9) = class'KFMod.DamTypeLawRocketImpact' - alwaysScale(10) = class'KFMod.DamTypeFlameNade' - alwaysScale(11) = class'KFMod.DamTypeFlareRevolver' - alwaysScale(12) = class'KFMod.DamTypeFlareProjectileImpact' - alwaysScale(13) = class'KFMod.DamTypeBurned' - alwaysScale(14) = class'KFMod.DamTypeTrenchgun' - alwaysScale(15) = class'KFMod.DamTypeHuskGun' - alwaysScale(16) = class'KFMod.DamTypeCrossbow' - alwaysScale(17) = class'KFMod.DamTypeCrossbowHeadShot' - alwaysScale(18) = class'KFMod.DamTypeM99SniperRifle' - alwaysScale(19) = class'KFMod.DamTypeM99HeadShot' - alwaysScale(20) = class'KFMod.DamTypeShotgun' - alwaysScale(21) = class'KFMod.DamTypeNailGun' - alwaysScale(22) = class'KFMod.DamTypeDBShotgun' - alwaysScale(23) = class'KFMod.DamTypeKSGShotgun' - alwaysScale(24) = class'KFMod.DamTypeBenelli' - alwaysScale(25) = class'KFMod.DamTypeSPGrenade' - alwaysScale(26) = class'KFMod.DamTypeSPGrenadeImpact' - alwaysScale(27) = class'KFMod.DamTypeSeekerSixRocket' - alwaysScale(28) = class'KFMod.DamTypeSeekerRocketImpact' - alwaysScale(29) = class'KFMod.DamTypeSealSquealExplosion' - alwaysScale(30) = class'KFMod.DamTypeRocketImpact' - alwaysScale(31) = class'KFMod.DamTypeBlowerThrower' - alwaysScale(32) = class'KFMod.DamTypeSPShotgun' - alwaysScale(33) = class'KFMod.DamTypeZEDGun' - alwaysScale(34) = class'KFMod.DamTypeZEDGunMKII' -} \ No newline at end of file diff --git a/sources/Features/FixInfiniteNades/FixInfiniteNades.uc b/sources/Features/FixInfiniteNades/FixInfiniteNades.uc deleted file mode 100644 index 53e0e8a..0000000 --- a/sources/Features/FixInfiniteNades/FixInfiniteNades.uc +++ /dev/null @@ -1,233 +0,0 @@ - /** - * This feature fixes a vulnerability in a code of 'Frag' that can allow - * player to throw grenades even when he no longer has any. - * There's also no cooldowns on the throw, which can lead to a server crash. - * Copyright 2019 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 FixInfiniteNades extends Feature; - -/** - * It is possible to call 'ServerThrow' function from client, - * forcing it to get executed on a server. This function consumes the grenade - * ammo and spawns a nade, but it doesn't check if player had any grenade ammo - * in the first place, allowing you him to throw however many grenades - * he wants. Moreover, unlike a regular throwing method, calling this function - * allows to spawn many grenades without any delay, - * which can lead to a server crash. - * - * This fix tracks every instance of 'Frag' weapon that's responsible for - * throwing grenades and records how much ammo they have have. - * This is necessary, because whatever means we use, when we get a say in - * preventing grenade from spawning the ammo was already reduced. - * This means that we can't distinguished between a player abusing a bug by - * throwing grenade when he doesn't have necessary ammo and player throwing - * his last nade, as in both cases current ammo visible to us will be 0. - * Then, before every nade throw, it checks if player has enough ammo and - * blocks grenade from spawning if he doesn't. - * We change a 'FireModeClass[0]' from 'FragFire' to 'FixedFragFire' and - * only call 'super.DoFireEffect()' if we decide spawning grenade - * should be allowed. The side effect is a change in server's 'FireModeClass'. - */ - -// Setting this flag to 'true' will allow to throw grenades by calling -// 'ServerThrow' directly, as long as player has necessary ammo. -// This can allow some players to throw grenades much quicker than intended, -// therefore it's suggested to keep this flag set to 'false'. -var private config const bool ignoreTossFlags; - -// Records how much ammo given frag grenade ('Frag') has. -struct FragAmmoRecord -{ - var public Frag fragReference; - var public int amount; -}; -var private array ammoRecords; - -protected function OnEnabled() -{ - local Frag nextFrag; - // Find all frags, that spawned when this fix wasn't running. - foreach level.DynamicActors(class'KFMod.Frag', nextFrag) - { - RegisterFrag(nextFrag); - } - RecreateFrags(); -} - -protected function OnDisabled() -{ - RecreateFrags(); - ammoRecords.length = 0; -} - -// Returns index of the connection corresponding to the given controller. -// Returns '-1' if no connection correspond to the given controller. -// Returns '-1' if given controller is equal to 'none'. -private final function int GetAmmoIndex(Frag fragToCheck) -{ - local int i; - if (fragToCheck == none) return -1; - - for (i = 0; i < ammoRecords.length; i += 1) - { - if (ammoRecords[i].fragReference == fragToCheck) - { - return i; - } - } - return -1; -} - -// Recreates all the 'Frag' actors, to change their fire mode mid-game. -private final function RecreateFrags() -{ - local int i; - local float maxAmmo, currentAmmo; - local Frag newFrag; - local Pawn fragOwner; - local array oldRecords; - oldRecords = ammoRecords; - for (i = 0; i < oldRecords.length; i += 1) - { - // Check if we even need to recreate that instance of 'Frag' - if (oldRecords[i].fragReference == none) continue; - fragOwner = oldRecords[i].fragReference.instigator; - if (fragOwner == none) continue; - // Recreate - oldRecords[i].fragReference.Destroy(); - fragOwner.CreateInventory("KFMod.Frag"); - newFrag = GetPawnFrag(fragOwner); - // Restore ammo amount - if (newFrag != none) - { - newFrag.GetAmmoCount(maxAmmo, currentAmmo); - newFrag.AddAmmo(oldRecords[i].amount - Int(currentAmmo), 0); - } - } -} - -// Utility function to help find a 'Frag' instance in a given pawn's inventory. -static private final function Frag GetPawnFrag(Pawn pawnWithFrag) -{ - local Frag foundFrag; - local Inventory invIter; - if (pawnWithFrag == none) return none; - invIter = pawnWithFrag.inventory; - while (invIter != none) - { - foundFrag = Frag(invIter); - if (foundFrag != none) - { - return foundFrag; - } - invIter = invIter.inventory; - } - return none; -} - -// Utility function for extracting current ammo amount from a frag class. -private final function int GetFragAmmo(Frag fragReference) -{ - local float maxAmmo; - local float currentAmmo; - if (fragReference == none) return 0; - - fragReference.GetAmmoCount(maxAmmo, currentAmmo); - return Int(currentAmmo); -} - -// Attempts to add new 'Frag' instance to our records. -public final function RegisterFrag(Frag newFrag) -{ - local int index; - local FragAmmoRecord newRecord; - index = GetAmmoIndex(newFrag); - if (index >= 0) return; - - newRecord.fragReference = newFrag; - newRecord.amount = GetFragAmmo(newFrag); - ammoRecords[ammoRecords.length] = newRecord; -} - -// This function tells our fix that there was a nade throw and we should -// reduce current 'Frag' ammo in our records. -// Returns 'true' if we had ammo for that, and 'false' if we didn't. -public final function bool RegisterNadeThrow(Frag relevantFrag) -{ - if (CanThrowGrenade(relevantFrag)) - { - ReduceGrenades(relevantFrag); - return true; - } - return false; -} - -// Can we throw grenade according to our rules? -// A throw can be prevented if: -// - we think that player doesn't have necessary ammo; -// - Player isn't currently 'tossing' a nade, -// meaning it was a direct call of 'ServerThrow'. -private final function bool CanThrowGrenade(Frag fragToCheck) -{ - local int index; - // Nothing to check - if (fragToCheck == none) return false; - // No ammo - index = GetAmmoIndex(fragToCheck); - if (index < 0) return false; - if (ammoRecords[index].amount <= 0) return false; - // Not tossing - if (ignoreTossFlags) return true; - if (!fragToCheck.bTossActive || fragToCheck.bTossSpawned) return false; - return true; -} - -// Reduces recorded amount of ammo in our records for the given nade. -private final function ReduceGrenades(Frag relevantFrag) -{ - local int index; - index = GetAmmoIndex(relevantFrag); - if (index < 0) return; - ammoRecords[index].amount -= 1; -} - -event Tick(float delta) -{ - local int i; - // Update our ammo records with current, correct data. - i = 0; - while (i < ammoRecords.length) - { - if (ammoRecords[i].fragReference != none) - { - ammoRecords[i].amount = GetFragAmmo(ammoRecords[i].fragReference); - i += 1; - } - else - { - ammoRecords.Remove(i, 1); - } - } -} - -defaultproperties -{ - ignoreTossFlags = false - // Listeners - requiredListeners(0) = class'MutatorListener_FixInfiniteNades' -} \ No newline at end of file diff --git a/sources/Features/FixInfiniteNades/FixedFragFire.uc b/sources/Features/FixInfiniteNades/FixedFragFire.uc deleted file mode 100644 index e0e356c..0000000 --- a/sources/Features/FixInfiniteNades/FixedFragFire.uc +++ /dev/null @@ -1,36 +0,0 @@ - /** - * A replacement for vanilla 'FragFire' fire class for 'Frag' weapon that - * adds additional ammo check in accordance to ammo records - * of 'FixInfiniteNades'. - * Copyright 2019 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 FixedFragFire extends KFMod.FragFire; - -function DoFireEffect() -{ - local FixInfiniteNades nadeFix; - nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance()); - if (nadeFix == none || nadeFix.RegisterNadeThrow(Frag(weapon))) - { - super.DoFireEffect(); - } -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Features/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc b/sources/Features/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc deleted file mode 100644 index 29e6e3e..0000000 --- a/sources/Features/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Overloaded mutator events listener to catch - * new 'Frag' weapons and 'Nade' projectiles. - * Copyright 2019 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 MutatorListener_FixInfiniteNades extends MutatorListenerBase - abstract; - -static function bool CheckReplacement(Actor other, out byte isSuperRelevant) -{ - local Frag relevantFrag; - local FixInfiniteNades nadeFix; - nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance()); - if (nadeFix == none) return true; - - // Handle detecting new frag (weapons that allows to throw nades) - relevantFrag = Frag(other); - if (relevantFrag != none) - { - nadeFix.RegisterFrag(relevantFrag); - relevantFrag.FireModeClass[0] = class'FixedFragFire'; - return true; - } - return true; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc b/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc deleted file mode 100644 index e7146fd..0000000 --- a/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc +++ /dev/null @@ -1,225 +0,0 @@ -/** - * This feature addressed two inventory issues: - * 1. Players carrying amount of weapons that shouldn't be allowed by the - * weight limit. - * 2. Players carrying two variants of the same gun. - * For example carrying both M32 and camo M32. - * Single and dual version of the same weapon are also considered - * the same gun, so you can't carry both MK23 and dual MK23 or - * dual handcannons and golden handcannon. - * - * It fixes them by doing repeated checks to find violations of those rules - * and destroys all droppable weapons of people that use this exploit. - * 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 FixInventoryAbuse extends Feature; - -// How often (in seconds) should we do our inventory validations? -// We shouldn't really worry about performance, but there's also no need to -// do this check too often. -var private config const float checkInterval; - -struct DualiesPair -{ - var class single; - var class dual; -}; -// For this fix to properly work, this array must contain an entry for -// every dual weapon in the game (like pistols, with single and dual versions). -// It's made configurable in case of custom dual weapons. -var private config const array dualiesClasses; - -protected function OnEnabled() -{ - local float actualInterval; - actualInterval = checkInterval; - if (actualInterval <= 0) - { - actualInterval = 0.25; - } - SetTimer(actualInterval, true); -} - -protected function OnDisabled() -{ - SetTimer(0.0f, false); -} - -// Did player with this controller contribute to the latest dosh generation? -private final function bool IsWeightLimitViolated(KFHumanPawn playerPawn) -{ - if (playerPawn == none) return false; - return (playerPawn.currentWeight > playerPawn.maxCarryWeight); -} - -// Returns a root pickup class. -// For non-dual weapons, root class is defined as either: -// 1. the first variant (reskin), if there are variants for that weapon; -// 2. and as the class itself, if there are no variants. -// For dual weapons (all dual pistols) root class is defined as -// a root of their single version. -// This definition is useful because: -// ~ Vanilla game rules are such that player can only have two weapons -// in the inventory if they have different roots; -// ~ Root is easy to find. -private final function class GetRootPickupClass(KFWeapon weapon) -{ - local int i; - local class root; - if (weapon == none) return none; - // Start with a pickup of the given weapons - root = class(weapon.default.pickupClass); - if (root == none) return none; - - // In case it's a dual version - find corresponding single pickup class - // (it's root would be the same). - for (i = 0; i < dualiesClasses.length; i += 1) - { - if (dualiesClasses[i].dual == root) - { - root = dualiesClasses[i].single; - break; - } - } - // Take either first variant class or the class itself, - - // it's going to be root by definition. - if (root.default.variantClasses.length > 0) - { - root = class(root.default.variantClasses[0]); - } - return root; -} - -// Returns 'true' if passed pawn has two weapons that are just variants of -// each other (they have the same root, see 'GetRootPickupClass'). -private final function bool HasDuplicateGuns(KFHumanPawn playerPawn) -{ - local int i, j; - local Inventory inv; - local KFWeapon nextWeapon; - local class rootClass; - local array< class > rootList; - if (playerPawn == none) return false; - - // First find a root for every weapon in the pawn's inventory. - for (inv = playerPawn.inventory; inv != none; inv = inv.inventory) - { - nextWeapon = KFWeapon(inv); - if (nextWeapon == none) continue; - if (nextWeapon.bKFNeverThrow) continue; - rootClass = GetRootPickupClass(nextWeapon); - if (rootClass != none) - { - rootList[rootList.length] = rootClass; - } - } - // Then just check obtained roots for duplicates. - for (i = 0; i < rootList.length; i += 1) - { - for (j = i + 1; j < rootList.length; j += 1) - { - if (rootList[i] == rootList[j]) - { - return true; - } - } - } - return false; -} - -private final function Vector DropWeapon(KFWeapon weaponToDrop) -{ - local Vector x, y, z; - local Vector weaponVelocity; - local Vector dropLocation; - local KFHumanPawn playerPawn; - if (weaponToDrop == none) return Vect(0, 0, 0); - playerPawn = KFHumanPawn(weaponToDrop.instigator); - if (playerPawn == none) return Vect(0, 0, 0); - - // Calculations from 'PlayerController.ServerThrowWeapon' - weaponVelocity = Vector(playerPawn.GetViewRotation()); - weaponVelocity *= (playerPawn.velocity dot weaponVelocity) + 150; - weaponVelocity += Vect(0, 0, 100); - // Calculations from 'Pawn.TossWeapon' - GetAxes(playerPawn.rotation, x, y, z); - dropLocation = playerPawn.location + 0.8 * playerPawn.collisionRadius * x - - 0.5 * playerPawn.collisionRadius * y; - // Do the drop - weaponToDrop.velocity = weaponVelocity; - weaponToDrop.DropFrom(dropLocation); -} - -// Kill the gun devil! -private final function DropEverything(KFHumanPawn playerPawn) -{ - local int i; - local Inventory inv; - local KFWeapon nextWeapon; - local array weaponList; - if (playerPawn == none) return; - // Going through the linked list while removing items can be tricky, - // so just find all weapons first. - for (inv = playerPawn.inventory; inv != none; inv = inv.inventory) - { - nextWeapon = KFWeapon(inv); - if (nextWeapon == none) continue; - if (nextWeapon.bKFNeverThrow) continue; - weaponList[weaponList.length] = nextWeapon; - } - // And destroy them later. - for(i = 0; i < weaponList.length; i += 1) - { - DropWeapon(weaponList[i]); - } -} - -event Timer() -{ - local int i; - local KFHumanPawn nextPawn; - local ConnectionService service; - local array connections; - service = ConnectionService(class'ConnectionService'.static.GetInstance()); - if (service == none) return; - - connections = service.GetActiveConnections(); - for (i = 0; i < connections.length; i += 1) - { - nextPawn = none; - if (connections[i].controllerReference != none) - { - nextPawn = KFHumanPawn(connections[i].controllerReference.pawn); - } - if (IsWeightLimitViolated(nextPawn) || HasDuplicateGuns(nextPawn)) - { - DropEverything(nextPawn); - } - } -} - -defaultproperties -{ - checkInterval = 0.25 - dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup') - dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup') - dualiesClasses(2)=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup') - dualiesClasses(3)=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup') - dualiesClasses(4)=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup') - dualiesClasses(5)=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup') -} \ No newline at end of file diff --git a/sources/Features/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc b/sources/Features/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc deleted file mode 100644 index 297dc5b..0000000 --- a/sources/Features/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Overloaded broadcast events listener to catch the moment - * someone becomes alive player / spectator. - * 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 BroadcastListener_FixSpectatorCrash extends BroadcastListenerBase - abstract; - -var private const int becomeAlivePlayerID; -var private const int becomeSpectatorID; - -static function bool HandleLocalized -( - Actor sender, - BroadcastEvents.LocalizedMessage message -) -{ - local FixSpectatorCrash specFix; - local PlayerController senderController; - if (sender == none) return true; - if (sender.level == none || sender.level.game == none) return true; - if (message.class != sender.level.game.gameMessageClass) return true; - if ( message.id != default.becomeAlivePlayerID - && message.id != default.becomeSpectatorID) return true; - - specFix = FixSpectatorCrash(class'FixSpectatorCrash'.static.GetInstance()); - senderController = GetController(sender); - specFix.NotifyStatusChange(senderController); - return (!specFix.IsViolator(senderController)); -} - -defaultproperties -{ - becomeAlivePlayerID = 1 - becomeSpectatorID = 14 -} \ No newline at end of file diff --git a/sources/Features/FixSpectatorCrash/FixSpectatorCrash.uc b/sources/Features/FixSpectatorCrash/FixSpectatorCrash.uc deleted file mode 100644 index f843056..0000000 --- a/sources/Features/FixSpectatorCrash/FixSpectatorCrash.uc +++ /dev/null @@ -1,291 +0,0 @@ -/** - * This feature attempts to prevent server crashes caused by someone - * quickly switching between being spectator and an active player. - * - * We do so by disconnecting players who start switching way too fast - * (more than twice in a short amount of time) and temporarily faking a large - * amount of players on the server, to prevent such spam from affecting the server. - * 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 FixSpectatorCrash extends Feature - dependson(ConnectionService); - -/** - * We use broadcast events to track when someone is switching - * to active player or spectator and remember such people - * for a short time (cooldown), defined by ('spectatorChangeTimeout'). - * If one of the player we've remembered tries to switch again, - * before the defined cooldown ran out, - we kick him - * by destroying his controller. - * One possible problem arises from the fact that controllers aren't - * immediately destroyed and instead initiate player disconnection, - - * exploiter might have enough time to cause a lag or even crash the server. - * We address this issue by temporarily blocking anyone from - * becoming active player (we do this by setting 'numPlayers' variable in - * killing floor's game info to a large value). - * After all malicious players have successfully disconnected, - - * we remove the block. - */ - -// This fix will try to kick any player that switches between active player -// and cooldown faster than time (in seconds) in this value. -// NOTE: raising this value past default value of '0.25' -// won't actually improve crash prevention. -var private config const float spectatorChangeTimeout; - -// [ADVANCED] Don't change this setting unless you know what you're doing. -// Allows you to turn off server blocking. -// Players that don't respect timeout will still be kicked. -// This might be needed if this fix conflicts with another mutator -// that also changes 'numPlayers'. -// However, it is necessary to block aggressive enough server crash attempts, -// but can cause compatibility issues with some mutators. -// It's highly preferred to rewrite such a mutator to be compatible. -// NOTE: it should be compatible with most faked players-type mutators, -// since this fix remembers the difference between amount of -// real players and 'numPlayers'. -// After unblocking, it sets 'numPlayers' to -// the current amount of real players + that difference. -// So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes -// 3 players + 3 (=6 numPlayers). -var private config const bool allowServerBlock; - -// Stores remaining cooldown value before the next allowed -// spectator change per player. -struct CooldownRecord -{ - var PlayerController player; - var float cooldown; -}; - -// Currently active cooldowns -var private array currentCooldowns; - -// Players who were decided to be violators and -// were marked for disconnecting. -// We'll be maintaining server block as long as even one -// of them hasn't yet disconnected. -var private array violators; - -// Is server currently blocked? -var private bool becomingActiveBlocked; -// This value introduced to accommodate mods such as faked player that can -// change 'numPlayers' to a value that isn't directly tied to the -// current number of active players. -// We remember the difference between active players and 'numPlayers' -/// variable in game type before server block and add it after block is over. -// If some mod introduces a more complicated relation between amount of -// active players and 'numPlayers', then it must take care of -// compatibility on it's own. -var private int recordedNumPlayersMod; - -// If given 'PlayerController' is registered in our cooldown records, - -// returns it's index. -// If it doesn't exists (or 'none' value was passes), - returns '-1'. -private final function int GetCooldownIndex(PlayerController player) -{ - local int i; - if (player == none) return -1; - - for (i = 0; i < currentCooldowns.length; i += 1) - { - if (currentCooldowns[i].player == player) - { - return i; - } - } - return -1; -} - -// Checks if given 'PlayerController' is registered as a violator. -// 'none' value isn't a violator. -public final function bool IsViolator(PlayerController player) -{ - local int i; - if (player == none) return false; - - for (i = 0; i < violators.length; i += 1) - { - if (violators[i] == player) - { - return true; - } - } - return false; -} - -// This function is to notify our fix that some player just changed status -// of active player / spectator. -// If passes value isn't 'none', it puts given player on cooldown or kicks him. -public final function NotifyStatusChange(PlayerController player) -{ - local int index; - local CooldownRecord newRecord; - if (player == none) return; - - index = GetCooldownIndex(player); - // Players already on cool down must be kicked and marked as violators - if (index >= 0) - { - player.Destroy(); - currentCooldowns.Remove(index, 1); - violators[violators.length] = player; - if (allowServerBlock) - { - SetBlock(true); - } - } - // Players that aren't on cooldown are - // either violators (do nothing, just wait for their disconnect) - // or didn't recently change their status (put them on cooldown). - else if (!IsViolator(player)) - { - newRecord.player = player; - newRecord.cooldown = spectatorChangeTimeout; - currentCooldowns[currentCooldowns.length] = newRecord; - } -} - -// Pass 'true' to block server, 'false' to unblock. -// Only works if 'allowServerBlock' is set to 'true'. -private final function SetBlock(bool activateBlock) -{ - local KFGameType kfGameType; - // Do we even need to do anything? - if (!allowServerBlock) return; - if (activateBlock == becomingActiveBlocked) return; - // Only works with 'KFGameType' and it's children. - if (level != none) kfGameType = KFGameType(level.game); - if (kfGameType == none) return; - - // Actually block/unblock - becomingActiveBlocked = activateBlock; - if (activateBlock) - { - recordedNumPlayersMod = GetNumPlayersMod(); - // This value both can't realistically fall below - // 'kfGameType.maxPlayer' and won't overflow from random increase - // in vanilla code. - kfGameType.numPlayers = maxInt / 2; - } - else - { - // Adding 'recordedNumPlayersMod' should prevent - // faked players from breaking. - kfGameType.numPlayers = GetRealPlayers() + recordedNumPlayersMod; - } -} - -// Performs server blocking if violators have disconnected. -private final function TryUnblocking() -{ - local int i; - if (!allowServerBlock) return; - if (!becomingActiveBlocked) return; - - for (i = 0; i < violators.length; i += 1) - { - if (violators[i] != none) - { - return; - } - } - SetBlock(false); -} - -// Counts current amount of "real" active players -// (connected to the server and not spectators). -// Need 'ConnectionService' to be running, otherwise return '-1'. -private final function int GetRealPlayers() -{ - // Auxiliary variables - local int i; - local int realPlayersAmount; - local PlayerController player; - // Information extraction - local ConnectionService service; - local array connections; - service = ConnectionService(class'ConnectionService'.static.GetInstance()); - if (service == none) return -1; - - // Count non-spectators - connections = service.GetActiveConnections(); - realPlayersAmount = 0; - for (i = 0; i < connections.length; i += 1) - { - player = connections[i].controllerReference; - if (player == none) continue; - if (player.playerReplicationInfo == none) continue; - if (!player.playerReplicationInfo.bOnlySpectator) - { - realPlayersAmount += 1; - } - } - return realPlayersAmount; -} - -// Calculates difference between current amount of "real" active players -// and 'numPlayers' from 'KFGameType'. -// Most typically this difference will be non-zero when using -// faked players-type mutators -// (difference will be equal to the amount of faked players). -private final function int GetNumPlayersMod() -{ - local KFGameType kfGameType; - if (level != none) kfGameType = KFGameType(level.game); - if (kfGameType == none) return 0; - return kfGameType.numPlayers - GetRealPlayers(); -} - -private final function ReduceCooldowns(float timePassed) -{ - local int i; - i = 0; - while (i < currentCooldowns.length) - { - currentCooldowns[i].cooldown -= timePassed; - if ( currentCooldowns[i].player != none - && currentCooldowns[i].cooldown > 0.0) - { - i += 1; - } - else - { - currentCooldowns.Remove(i, 1); - } - } -} - -event Tick(float delta) -{ - local float trueTimePassed; - trueTimePassed = delta * (1.1 / level.timeDilation); - TryUnblocking(); - ReduceCooldowns(trueTimePassed); -} - -defaultproperties -{ - // Configurable variables - spectatorChangeTimeout = 0.25 - allowServerBlock = true - // Inner variables - becomingActiveBlocked = false - // Listeners - requiredListeners(0) = class'BroadcastListener_FixSpectatorCrash' -} \ No newline at end of file diff --git a/sources/Features/FixZedTimeLags/FixZedTimeLags.uc b/sources/Features/FixZedTimeLags/FixZedTimeLags.uc deleted file mode 100644 index 5ec9fcd..0000000 --- a/sources/Features/FixZedTimeLags/FixZedTimeLags.uc +++ /dev/null @@ -1,188 +0,0 @@ -/** - * This feature fixes lags caused by a zed time that can occur - * on some maps when a lot of zeds are present at once. - * As a side effect it also fixes an issue where during zed time speed up - * 'zedTimeSlomoScale' was assumed to be default value of '0.2'. - * Now zed time will behave correctly with mods that - * change 'zedTimeSlomoScale'. - * 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 FixZedTimeLags extends Feature - dependson(ConnectionService); - -/** - * When zed time activates, game speed is immediately set to - * 'zedTimeSlomoScale' (0.2 by default), defined, like all other variables, - * in 'KFGameType'. Zed time lasts 'zedTimeDuration' seconds (3.0 by default), - * but during last 'zedTimeDuration * 0.166' seconds (by default 0.498) - * it starts to speed back up, causing game speed to update every tick. - * This makes animations look more smooth when exiting zed-time; - * however, updating speed every tick for that purpose seems like - * an overkill and, combined with things like - * increased tick rate, certain maps and raised zed limit, - * it can lead to noticeable lags at the end of zed time. - * To fix this issue we disable 'Tick' event in - * 'KFGameType' and then repeat that functionality in our own 'Tick' event, - * but only perform game speed updates occasionally, - * to make sure that overall amount of updates won't go over a limit, - * that can be configured via 'maxGameSpeedUpdatesAmount' - * Author's test (looking really hard on clots' animations) - * seem to suggest that there shouldn't be much visible difference if - * we limit game speed updates to about 2 or 3. - */ - -// Max amount of game speed updates during speed up phase -// (actual amount of updates can't be larger than amount of ticks). -// On servers with default 30 tick rate there's usually -// about 13 updates total on vanilla game. -// Values lower than 1 are treated like 1. -var private config const int maxGameSpeedUpdatesAmount; -// [ADVANCED] Don't change this setting unless you know what you're doing. -// Compatibility setting that allows to keep 'GameInfo' 's 'Tick' event -// from being disabled. -// Useful when running Acedia along with custom 'GameInfo' -// (that isn't 'KFGameType') that relies on 'Tick' event. -// Note, however, that in order to keep this fix working properly, -// it's on you to make sure 'KFGameType.Tick()' logic isn't executed. -var private config const bool disableTick; -// Counts how much time is left until next update -var private float updateCooldown; -// Recorded game type, to avoid constant conversions every tick -var private KFGameType gameType; - -protected function OnEnabled() -{ - gameType = KFGameType(level.game); - if (gameType == none) - { - Destroy(); - } - else if (disableTick) - { - gameType.Disable('Tick'); - } -} - -protected function OnDisabled() -{ - gameType = KFGameType(level.game); - if (gameType != none && disableTick) - { - gameType.Enable('Tick'); - } -} - -event Tick(float delta) -{ - local float trueTimePassed; - if (gameType == none) return; - if (!gameType.bZEDTimeActive) return; - // Unfortunately we need to keep disabling 'Tick' probe function, - // because it constantly gets enabled back and I don't know where - // (maybe native code?); only really matters during zed time. - if (disableTick) - { - gameType.Disable('Tick'); - } - // How much real (not in-game) time has passed - trueTimePassed = delta * (1.1 / level.timeDilation); - gameType.currentZEDTimeDuration -= trueTimePassed; - - // Handle speeding up phase - if (gameType.bSpeedingBackUp) - { - DoSpeedBackUp(trueTimePassed); - } - else if (gameType.currentZEDTimeDuration < GetSpeedupDuration()) - { - gameType.bSpeedingBackUp = true; - updateCooldown = GetFullUpdateCooldown(); - TellClientsZedTimeEnds(); - DoSpeedBackUp(trueTimePassed); - } - // End zed time once it's duration has passed - if (gameType.currentZEDTimeDuration <= 0) - { - gameType.bZEDTimeActive = false; - gameType.bSpeedingBackUp = false; - gameType.zedTimeExtensionsUsed = 0; - gameType.SetGameSpeed(1.0); - } -} - -private final function TellClientsZedTimeEnds() -{ - local int i; - local KFPlayerController player; - local ConnectionService service; - local array connections; - service = ConnectionService(class'ConnectionService'.static.GetInstance()); - if (service == none) return; - connections = service.GetActiveConnections(); - for (i = 0; i < connections.length; i += 1) - { - player = KFPlayerController(connections[i].controllerReference); - if (player != none) - { - // Play sound of leaving zed time - player.ClientExitZedTime(); - } - } -} - -// This function is called every tick during speed up phase and manages -// gradual game speed increase. -private final function DoSpeedBackUp(float trueTimePassed) -{ - // Game speed will always be updated in our 'Tick' event - // at the very end of the zed time. - // The rest of the updates will be uniformly distributed - // over the speed up duration. - - local float newGameSpeed; - local float slowdownScale; - if (maxGameSpeedUpdatesAmount <= 1) return; - if (updateCooldown > 0.0) - { - updateCooldown -= trueTimePassed; - return; - } - else - { - updateCooldown = GetFullUpdateCooldown(); - } - slowdownScale = gameType.currentZEDTimeDuration / GetSpeedupDuration(); - newGameSpeed = Lerp(slowdownScale, 1.0, gameType.zedTimeSlomoScale); - gameType.SetGameSpeed(newGameSpeed); -} - -private final function float GetSpeedupDuration() -{ - return gameType.zedTimeDuration * 0.166; -} - -private final function float GetFullUpdateCooldown() -{ - return GetSpeedupDuration() / maxGameSpeedUpdatesAmount; -} - -defaultproperties -{ - maxGameSpeedUpdatesAmount = 3 - disableTick = true -} \ No newline at end of file diff --git a/sources/Global.uc b/sources/Global.uc deleted file mode 100644 index 5cbb741..0000000 --- a/sources/Global.uc +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Class for an object that will provide an access to a Acedia's functionality - * by giving a reference to this actor to all Acedia's objects and actors, - * emulating a global API namespace. - * 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 Global extends Singleton; - -var public Acedia acedia; -var public LoggerAPI logger; -var public JSONAPI json; -var public AliasesAPI alias; -var public TextAPI text; -var public MemoryAPI memory; -var public ConsoleAPI console; -var public ColorAPI color; - -// TODO: APIs must be `remoteRole = ROLE_None` -protected function OnCreated() -{ - acedia = class'Acedia'.static.GetInstance(); - Spawn(class'LoggerAPI'); - logger = LoggerAPI(class'LoggerAPI'.static.GetInstance()); - Spawn(class'JSONAPI'); - json = JSONAPI(class'JSONAPI'.static.GetInstance()); - Spawn(class'AliasesAPI'); - alias = AliasesAPI(class'AliasesAPI'.static.GetInstance()); - Spawn(class'TextAPI'); - text = TextAPI(class'TextAPI'.static.GetInstance()); - Spawn(class'MemoryAPI'); - memory = MemoryAPI(class'MemoryAPI'.static.GetInstance()); - Spawn(class'ConsoleAPI'); - console = ConsoleAPI(class'ConsoleAPI'.static.GetInstance()); - Spawn(class'ColorAPI'); - color = ColorAPI(class'ColorAPI'.static.GetInstance()); -} \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc deleted file mode 100644 index 0652b72..0000000 --- a/sources/Manifest.uc +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Manifest is meant to describe contents of the package (mutator file) - * as well as what actors/objects should be automatically created when package - * is loaded and what event listeners should be activated. - * Currently only implements automatic listener activation. - * Copyright 2019 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 Manifest extends Object - abstract; - -// List of features in this manifest's package. -var public const array< class > aliasSources; - -// List of features in this manifest's package. -var public const array< class > features; - -// List of features in this manifest's package. -var public const array< class > testCases; - -defaultproperties -{ - aliasSources(0) = class'AliasSource' - aliasSources(1) = class'WeaponAliasSource' - aliasSources(2) = class'ColorAliasSource' - features(0) = class'FixZedTimeLags' - features(1) = class'FixDoshSpam' - features(2) = class'FixFFHack' - features(3) = class'FixInfiniteNades' - features(4) = class'FixAmmoSelling' - features(5) = class'FixSpectatorCrash' - features(6) = class'FixDualiesCost' - features(7) = class'FixInventoryAbuse' - // Unit tests - testCases(0) = class'TEST_Aliases' - testCases(1) = class'TEST_ColorAPI' - testCases(2) = class'TEST_JSON' - testCases(3) = class'TEST_Text' - testCases(4) = class'TEST_TextAPI' - testCases(5) = class'TEST_Parser' -} \ No newline at end of file diff --git a/sources/Core/Acedia.uc b/sources/Packages.uc similarity index 64% rename from sources/Core/Acedia.uc rename to sources/Packages.uc index 96707ce..b2c40ef 100644 --- a/sources/Core/Acedia.uc +++ b/sources/Packages.uc @@ -1,6 +1,7 @@ /** - * Main and only Acedia mutator used for loading Acedia packages + * Main and only Acedia mutator used for loading Acedia packages * and providing access to mutator events' calls. + * Name is chosen to make config files more readable. * Copyright 2020 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -18,7 +19,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class Acedia extends Mutator +class Packages extends Mutator config(Acedia); // Default value of this variable will be used to store @@ -26,15 +27,22 @@ class Acedia extends Mutator // as well as to ensure there's only one copy of it. // We can't use 'Singleton' class for that, // as we have to derive from 'Mutator'. -var private Acedia selfReference; +var private Packages selfReference; + +// Acedia's reference to a `Global` object. +var private Global _; + +// Package's manifest is supposed to always have a name of +// ".Manifest", this variable stores the ".Manifest" part +var private const string manifestSuffix; // Array of predefined services that must be started along with Acedia mutator. -var private config array< class > registeredManifests; +var private config array package; -// Array of predefined services that must be started along with Acedia mutator. -var private array< class > systemServices; +// AcediaCore package that this launcher is build for +var private config const string corePackage; -static public final function Acedia GetInstance() +static public final function Packages GetInstance() { return default.selfReference; } @@ -56,11 +64,33 @@ event PreBeginPlay() private final function BootUp() { - local int i; - Spawn(class'Global'); - for (i = 0; i < registeredManifests.length; i += 1) { - LoadManifest(registeredManifests[i]); + local int i; + local class<_manifest> nextManifest; + _ = Spawn(class'Global'); + // Load core + nextManifest = LoadManifestClass(corePackage); + if (nextManifest == none) + { + _.logger.Fatal("Cannot load required AcediaCore package \"" + $ corePackage $ "\". Acedia will shut down."); + Destroy(); + return; } + LoadManifest(nextManifest); + // Load packages + for (i = 0; i < package.length; i += 1) + { + nextManifest = LoadManifestClass(package[i]); + if (nextManifest == none) + { + _.logger.Failure("Cannot load `Manifest` for package \"" + $ package[i] $ "\". Check if it's missing or" + @ "if it's name is spelled incorrectly."); + continue; + } + LoadManifest(nextManifest); + } + // Inject broadcast handler InjectBroadcastHandler(); // TODO: move this to 'SideEffect' mechanic } @@ -75,10 +105,24 @@ private final function RunStartUpTests() if (testService.filterTestsByGroup) { testService.FilterByName(testService.requiredGroup); } - testService.Run(); + if (testService.Run()) + { + // This listener will output test results into server's console + class'TestingListener_AcediaLauncher'.static.SetActive(true); + } + else + { + _.logger.Failure("Could not launch Acedia's start up testing process."); + } } -private final function LoadManifest(class manifestClass) +private final function class<_manifest> LoadManifestClass(string packageName) +{ + return class<_manifest>(DynamicLoadObject( packageName $ manifestSuffix, + class'Class', true)); +} + +private final function LoadManifest(class<_manifest> manifestClass) { local int i; // Load alias sources @@ -140,13 +184,13 @@ function Mutate(string command, PlayerController sendingController) defaultproperties { - // Add Acedia's own manifest - registeredManifests(0) = class'Manifest' + corePackage = "AcediaCore_0_2" + manifestSuffix = ".Manifest" // This is a server-only mutator remoteRole = ROLE_None bAlwaysRelevant = true // Mutator description - GroupName = "Core mutator" - FriendlyName = "Acedia" - Description = "Launcher for Acedia modules" + GroupName = "Package loader" + FriendlyName = "Acedia loader" + Description = "Launcher for Acedia packages" } \ No newline at end of file diff --git a/sources/Services/Connection/ConnectionEvents.uc b/sources/Services/Connection/ConnectionEvents.uc deleted file mode 100644 index aad9d42..0000000 --- a/sources/Services/Connection/ConnectionEvents.uc +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Event generator for 'ConnectionService'. - * Copyright 2019 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 ConnectionEvents extends Events - dependson(ConnectionService) - abstract; - -static function CallPlayerConnected(ConnectionService.Connection connection) -{ - local int i; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0; i < listeners.length; i += 1) - { - class(listeners[i]) - .static.PlayerConnected(connection); - } -} - -static function CallPlayerDisconnected(ConnectionService.Connection connection) -{ - local int i; - local array< class > listeners; - listeners = GetListeners(); - for (i = 0; i < listeners.length; i += 1) - { - class(listeners[i]) - .static.PlayerDisconnected(connection); - } -} - -defaultproperties -{ - relatedListener = class'ConnectionListenerBase' - connectedServiceClass = class'ConnectionService' -} \ No newline at end of file diff --git a/sources/Services/Connection/ConnectionListenerBase.uc b/sources/Services/Connection/ConnectionListenerBase.uc deleted file mode 100644 index b224b3c..0000000 --- a/sources/Services/Connection/ConnectionListenerBase.uc +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Listener for events generated by 'ConnectionService'. - * Copyright 2019 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 ConnectionListenerBase extends Listener - dependson(ConnectionService) - abstract; - -// 'PlayerConnected' is called the moment we detect a new player on a server. -static function PlayerConnected(ConnectionService.Connection connection); - -// 'PlayerDisconnected' is called the moment we -// detect a player leaving the server. -static function PlayerDisconnected(ConnectionService.Connection connection); - -defaultproperties -{ - relatedEvents = class'ConnectionEvents' -} \ No newline at end of file diff --git a/sources/Services/Connection/ConnectionService.uc b/sources/Services/Connection/ConnectionService.uc deleted file mode 100644 index bc128cb..0000000 --- a/sources/Services/Connection/ConnectionService.uc +++ /dev/null @@ -1,143 +0,0 @@ -/** - * This service tracks current connections to the server - * as well as their basic information, - * like IP or steam ID of connecting player. - * Copyright 2019 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 ConnectionService extends Service; - -// Stores basic information about a connection -struct Connection -{ - var public string networkAddress; - var public string steamID; - var public PlayerController controllerReference; - // Reference to 'AcediaReplicationInfo' for this client, - // in case it was created. - var private AcediaReplicationInfo acediaRI; -}; - -var private array activeConnections; - -// Shortcut to 'ConnectionEvents', so that we don't have to write -// class'ConnectionEvents' every time. -var const class events; - -// Returning 'true' guarantees that 'controllerToCheck != none' -// and either 'controllerToCheck.playerReplicationInfo != none' -// or 'auxiliaryRepInfo != none'. -private function bool IsHumanController(PlayerController controllerToCheck) -{ - local PlayerReplicationInfo replicationInfo; - if (controllerToCheck == none) return false; - if (!controllerToCheck.bIsPlayer) return false; - // Is this a WebAdmin that didn't yet set 'bIsPlayer = false' - if (MessagingSpectator(controllerToCheck) != none) return false; - // Check replication info - replicationInfo = controllerToCheck.playerReplicationInfo; - if (replicationInfo == none) return false; - if (replicationInfo.bBot) return false; - return true; -} - -// Returns index of the connection corresponding to the given controller. -// Returns '-1' if no connection correspond to the given controller. -// Returns '-1' if given controller is equal to 'none'. -private function int GetConnectionIndex(PlayerController controllerToCheck) -{ - local int i; - if (controllerToCheck == none) return -1; - for (i = 0; i < activeConnections.length; i += 1) - { - if (activeConnections[i].controllerReference == controllerToCheck) - { - return i; - } - } - return -1; -} - -// Remove connections with now invalid ('none') player controller reference. -private function RemoveBrokenConnections() -{ - local int i; - i = 0; - while (i < activeConnections.length) - { - if (activeConnections[i].controllerReference == none) - { - if (activeConnections[i].acediaRI != none) - { - activeConnections[i].acediaRI.Destroy(); - } - events.static.CallPlayerDisconnected(activeConnections[i]); - activeConnections.Remove(i, 1); - } - else - { - i += 1; - } - } -} - -// Return connection, corresponding to a given player controller. -public final function Connection GetConnection(PlayerController player) -{ - local int connectionIndex; - local Connection emptyConnection; - connectionIndex = GetConnectionIndex(player); - if (connectionIndex < 0) return emptyConnection; - return activeConnections[connectionIndex]; -} - -// Attempts to register a connection for this player controller. -// Shouldn't be used outside of 'ConnectionService' module. -// Returns 'true' if connection is registered (even if it was already added). -public final function bool RegisterConnection(PlayerController player) -{ - local Connection newConnection; - if (!IsHumanController(player)) return false; - if (GetConnectionIndex(player) >= 0) return true; - newConnection.controllerReference = player; - if (!class'Acedia'.static.GetInstance().IsServerOnly()) - { - newConnection.acediaRI = Spawn(class'AcediaReplicationInfo', player); - newConnection.acediaRI.linkOwner = player; - } - newConnection.networkAddress = player.GetPlayerNetworkAddress(); - newConnection.steamID = player.GetPlayerIDHash(); - activeConnections[activeConnections.length] = newConnection; - events.static.CallPlayerConnected(newConnection); - return true; -} - -public final function array GetActiveConnections() -{ - return activeConnections; -} - -event Tick(float delta) -{ - RemoveBrokenConnections(); -} - -defaultproperties -{ - events = class'ConnectionEvents' - requiredListeners(0) = class'MutatorListener_Connection' -} \ No newline at end of file diff --git a/sources/Services/Connection/MutatorListener_Connection.uc b/sources/Services/Connection/MutatorListener_Connection.uc deleted file mode 100644 index acbf404..0000000 --- a/sources/Services/Connection/MutatorListener_Connection.uc +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Overloaded mutator events listener to catch connecting players. - * Copyright 2019 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 MutatorListener_Connection extends MutatorListenerBase - abstract; - -static function bool CheckReplacement(Actor other, out byte isSuperRelevant) -{ - local KFSteamStatsAndAchievements playerSteamStatsAndAchievements; - local PlayerController player; - local ConnectionService service; - // We are looking for 'KFSteamStatsAndAchievements' instead of - // 'PlayerController' because, by the time they it's created, - // controller should have a valid reference to 'PlayerReplicationInfo', - // as well as valid network address and IDHash (steam id). - // However, neither of those are properly initialized at the point when - // 'CheckReplacement' is called for 'PlayerController'. - // - // Since 'KFSteamStatsAndAchievements' - // is created soon after (at the same tick) - // for each new `PlayerController`, - // we'll be detecting new users right after server - // detected and properly initialized them. - playerSteamStatsAndAchievements = KFSteamStatsAndAchievements(other); - if (playerSteamStatsAndAchievements == none) return true; - service = ConnectionService(class'ConnectionService'.static.GetInstance()); - if (service == none) return true; - - player = PlayerController(playerSteamStatsAndAchievements.owner); - service.RegisterConnection(player); - return true; -} - -defaultproperties -{ - relatedEvents = class'MutatorEvents' -} \ No newline at end of file diff --git a/sources/Core/StartUp.uc b/sources/StartUp.uc similarity index 95% rename from sources/Core/StartUp.uc rename to sources/StartUp.uc index 180eaf6..7d4ac49 100644 --- a/sources/Core/StartUp.uc +++ b/sources/StartUp.uc @@ -25,7 +25,7 @@ function PreBeginPlay() super.PreBeginPlay(); if (level != none && level.game != none) { - level.game.AddMutator(string(class'Acedia')); + level.game.AddMutator(string(class'Packages')); } Destroy(); } diff --git a/sources/Core/Testing/Service/TestingListenerBase.uc b/sources/TestingListener_AcediaLauncher.uc similarity index 55% rename from sources/Core/Testing/Service/TestingListenerBase.uc rename to sources/TestingListener_AcediaLauncher.uc index 92272bb..c888930 100644 --- a/sources/Core/Testing/Service/TestingListenerBase.uc +++ b/sources/TestingListener_AcediaLauncher.uc @@ -1,5 +1,6 @@ /** - * Listener for events related to testing. + * Overloaded testing events listener to catch when tests that we run during + * server loading finish. * Copyright 2020 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -17,16 +18,26 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class TestingListenerBase extends Listener +class TestingListener_AcediaLauncher extends TestingListenerBase abstract; -static function TestingBegan(array< class > testQueue) {} - -static function CaseTested(class testQueue, TestCaseSummary result) {} - static function TestingEnded( - array< class > testedCase, - array results) {} + array< class > testQueue, + array results) +{ + local int i; + local string nextLine; + local array textSummary; + textSummary = class'TestCaseSummary'.static.GenerateStringSummary(results); + for (i = 0; i < textSummary.length; i += 1) + { + nextLine = _().text.ConvertString( textSummary[i], + STRING_Formatted, STRING_Plain); + Log(nextLine); + } + // No longer need to listen to testing events + SetActive(false); +} defaultproperties { From 53f16e794c20de87fa02832dfe9fd160c45f2421 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Fri, 26 Feb 2021 22:06:01 +0700 Subject: [PATCH 36/59] Add loading capabilities for acedia v0.1.dev2 In new Acedia version we must create `CoreService` and register commands. --- sources/Packages.uc | 81 ++++++++++++++++++----- sources/TestingListener_AcediaLauncher.uc | 9 +-- 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index b2c40ef..709c153 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -66,8 +66,9 @@ private final function BootUp() { local int i; local class<_manifest> nextManifest; - _ = Spawn(class'Global'); // Load core + Spawn(class'CoreService'); + _ = class'Global'.static.GetInstance(); nextManifest = LoadManifestClass(corePackage); if (nextManifest == none) { @@ -91,7 +92,7 @@ private final function BootUp() LoadManifest(nextManifest); } // Inject broadcast handler - InjectBroadcastHandler(); // TODO: move this to 'SideEffect' mechanic + InjectBroadcastHandler(); } private final function RunStartUpTests() @@ -103,7 +104,7 @@ private final function RunStartUpTests() testService.FilterByName(testService.requiredName); } if (testService.filterTestsByGroup) { - testService.FilterByName(testService.requiredGroup); + testService.FilterByGroup(testService.requiredGroup); } if (testService.Run()) { @@ -125,21 +126,16 @@ private final function class<_manifest> LoadManifestClass(string packageName) private final function LoadManifest(class<_manifest> manifestClass) { local int i; - // Load alias sources for (i = 0; i < manifestClass.default.aliasSources.length; i += 1) { if (manifestClass.default.aliasSources[i] == none) continue; - Spawn(manifestClass.default.aliasSources[i]); + //Spawn(manifestClass.default.aliasSources[i]); + _.memory.Allocate(manifestClass.default.aliasSources[i]); } - // Enable features - for (i = 0; i < manifestClass.default.features.length; i += 1) - { - if (manifestClass.default.features[i] == none) continue; - if (manifestClass.default.features[i].static.IsAutoEnabled()) { - manifestClass.default.features[i].static.EnableMe(); - } + LaunchServicesAndFeatures(manifestClass); + if (class'Commands'.static.IsEnabled()) { + RegisterCommands(manifestClass); } - // Load tests cases for (i = 0; i < manifestClass.default.testCases.length; i += 1) { class'TestingService'.static @@ -147,18 +143,69 @@ private final function LoadManifest(class<_manifest> manifestClass) } } +private final function RegisterCommands(class<_manifest> manifestClass) +{ + local int i; + local Commands commandsFeature; + commandsFeature = Commands(class'Commands'.static.GetInstance()); + for (i = 0; i < manifestClass.default.commands.length; i += 1) + { + if (manifestClass.default.commands[i] == none) continue; + commandsFeature.RegisterCommand(manifestClass.default.commands[i]); + } +} + +private final function LaunchServicesAndFeatures(class<_manifest> manifestClass) +{ + local int i; + local bool packageFeaturesAreUsed; + for (i = 0; i < manifestClass.default.features.length; i += 1) + { + if (manifestClass.default.features[i] == none) continue; + if (manifestClass.default.features[i].static.IsAutoEnabled()) + { + packageFeaturesAreUsed = true; + break; + } + } + // Services + for (i = 0; i < manifestClass.default.services.length; i += 1) + { + if (manifestClass.default.services[i] == none) continue; + manifestClass.default.services[i].static.Require(); + } + // Features + for (i = 0; i < manifestClass.default.features.length; i += 1) + { + if (manifestClass.default.features[i] == none) continue; + if (manifestClass.default.features[i].static.IsAutoEnabled()) { + manifestClass.default.features[i].static.EnableMe(); + } + } +} + private final function InjectBroadcastHandler() { - local BroadcastHandler ourBroadcastHandler; - if (level == none || level.game == none) return; + local BroadcastEventsObserver ourBroadcastHandler; + local BroadcastEventsObserver.InjectionLevel injectionLevel; + injectionLevel = class'BroadcastEventsObserver'.default.usedInjectionLevel; + if (level == none || level.game == none) return; + if (injectionLevel == BHIJ_None) return; - ourBroadcastHandler = Spawn(class'BroadcastHandler'); + ourBroadcastHandler = Spawn(class'BroadcastEventsObserver'); + if (injectionLevel == BHIJ_Registered) + { + level.game.broadcastHandler + .RegisterBroadcastHandler(ourBroadcastHandler); + return; + } + // Here `injectionLevel == BHIJ_Root` holds. // Swap out level's first handler with ours // (needs to be done for both actor reference and it's class) ourBroadcastHandler.nextBroadcastHandler = level.game.broadcastHandler; ourBroadcastHandler.nextBroadcastHandlerClass = level.game.broadcastClass; level.game.broadcastHandler = ourBroadcastHandler; - level.game.broadcastClass = class'BroadcastHandler'; + level.game.broadcastClass = class'BroadcastEventsObserver'; } // Acedia is only able to run in a server mode right now, diff --git a/sources/TestingListener_AcediaLauncher.uc b/sources/TestingListener_AcediaLauncher.uc index c888930..ac2283c 100644 --- a/sources/TestingListener_AcediaLauncher.uc +++ b/sources/TestingListener_AcediaLauncher.uc @@ -26,14 +26,15 @@ static function TestingEnded( array results) { local int i; - local string nextLine; + local MutableText nextLine; local array textSummary; + nextLine = __().text.Empty(); textSummary = class'TestCaseSummary'.static.GenerateStringSummary(results); for (i = 0; i < textSummary.length; i += 1) { - nextLine = _().text.ConvertString( textSummary[i], - STRING_Formatted, STRING_Plain); - Log(nextLine); + nextLine.Clear(); + nextLine.AppendFormattedString(textSummary[i]); + Log(nextLine.ToPlainString()); } // No longer need to listen to testing events SetActive(false); From 280cf5af57e011304e513f27e8799f9e36903038 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 28 Jul 2021 02:28:38 +0700 Subject: [PATCH 37/59] Remove outdated log messages --- sources/Packages.uc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index 709c153..6e6bff9 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -72,8 +72,8 @@ private final function BootUp() nextManifest = LoadManifestClass(corePackage); if (nextManifest == none) { - _.logger.Fatal("Cannot load required AcediaCore package \"" - $ corePackage $ "\". Acedia will shut down."); + /*_.logger.Fatal("Cannot load required AcediaCore package \"" + $ corePackage $ "\". Acedia will shut down.");*/ Destroy(); return; } @@ -84,9 +84,9 @@ private final function BootUp() nextManifest = LoadManifestClass(package[i]); if (nextManifest == none) { - _.logger.Failure("Cannot load `Manifest` for package \"" + /*_.logger.Failure("Cannot load `Manifest` for package \"" $ package[i] $ "\". Check if it's missing or" - @ "if it's name is spelled incorrectly."); + @ "if it's name is spelled incorrectly.");*/ continue; } LoadManifest(nextManifest); @@ -113,7 +113,7 @@ private final function RunStartUpTests() } else { - _.logger.Failure("Could not launch Acedia's start up testing process."); + //_.logger.Failure("Could not launch Acedia's start up testing process."); } } From e2eedc1d28e5ba92b35e6952bda32a29ea569dcf Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 28 Jul 2021 02:29:11 +0700 Subject: [PATCH 38/59] Change `Feature` loading to support new AcediaCore --- sources/Packages.uc | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index 6e6bff9..113ee17 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -133,7 +133,7 @@ private final function LoadManifest(class<_manifest> manifestClass) _.memory.Allocate(manifestClass.default.aliasSources[i]); } LaunchServicesAndFeatures(manifestClass); - if (class'Commands'.static.IsEnabled()) { + if (class'Commands_Feature'.static.IsEnabled()) { RegisterCommands(manifestClass); } for (i = 0; i < manifestClass.default.testCases.length; i += 1) @@ -145,9 +145,10 @@ private final function LoadManifest(class<_manifest> manifestClass) private final function RegisterCommands(class<_manifest> manifestClass) { - local int i; - local Commands commandsFeature; - commandsFeature = Commands(class'Commands'.static.GetInstance()); + local int i; + local Commands_Feature commandsFeature; + commandsFeature = + Commands_Feature(class'Commands_Feature'.static.GetInstance()); for (i = 0; i < manifestClass.default.commands.length; i += 1) { if (manifestClass.default.commands[i] == none) continue; @@ -158,16 +159,7 @@ private final function RegisterCommands(class<_manifest> manifestClass) private final function LaunchServicesAndFeatures(class<_manifest> manifestClass) { local int i; - local bool packageFeaturesAreUsed; - for (i = 0; i < manifestClass.default.features.length; i += 1) - { - if (manifestClass.default.features[i] == none) continue; - if (manifestClass.default.features[i].static.IsAutoEnabled()) - { - packageFeaturesAreUsed = true; - break; - } - } + local Text autoConfigName; // Services for (i = 0; i < manifestClass.default.services.length; i += 1) { @@ -178,9 +170,13 @@ private final function LaunchServicesAndFeatures(class<_manifest> manifestClass) for (i = 0; i < manifestClass.default.features.length; i += 1) { if (manifestClass.default.features[i] == none) continue; - if (manifestClass.default.features[i].static.IsAutoEnabled()) { - manifestClass.default.features[i].static.EnableMe(); + manifestClass.default.features[i].static.StaticConstructor(); + autoConfigName = + manifestClass.default.features[i].static.GetAutoEnabledConfig(); + if (autoConfigName != none) { + manifestClass.default.features[i].static.EnableMe(autoConfigName); } + _.memory.Free(autoConfigName); } } From f94103f38261c13e88576e517942040d9343cbfc Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 3 Aug 2021 15:42:21 +0700 Subject: [PATCH 39/59] Change feature loading to work with new AcediaCore --- sources/Packages.uc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index 113ee17..fb0a796 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -170,7 +170,7 @@ private final function LaunchServicesAndFeatures(class<_manifest> manifestClass) for (i = 0; i < manifestClass.default.features.length; i += 1) { if (manifestClass.default.features[i] == none) continue; - manifestClass.default.features[i].static.StaticConstructor(); + manifestClass.default.features[i].static.LoadConfigs(); autoConfigName = manifestClass.default.features[i].static.GetAutoEnabledConfig(); if (autoConfigName != none) { From 3681fcabf76811921d9140cb4ae81005a3d2c802 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Fri, 5 Nov 2021 03:43:36 +0700 Subject: [PATCH 40/59] Add game modes support to Acedia --- config/Acedia.ini | 1 + sources/GameModes/BaseGameMode.uc | 386 ++++++++++++++++++++++ sources/GameModes/GameMode.uc | 201 +++++++++++ sources/Packages.uc | 244 ++++++-------- sources/StartUp.uc | 3 +- sources/TestingListener_AcediaLauncher.uc | 46 --- sources/VotingHandlerAdapter.uc | 345 +++++++++++++++++++ 7 files changed, 1032 insertions(+), 194 deletions(-) create mode 100644 sources/GameModes/BaseGameMode.uc create mode 100644 sources/GameModes/GameMode.uc delete mode 100644 sources/TestingListener_AcediaLauncher.uc create mode 100644 sources/VotingHandlerAdapter.uc diff --git a/config/Acedia.ini b/config/Acedia.ini index fecbb06..8e1b9d2 100644 --- a/config/Acedia.ini +++ b/config/Acedia.ini @@ -1,3 +1,4 @@ [Acedia.Packages] +useGameModes=false corePackage="AcediaCore_0_2" package="AcediaFixes" \ No newline at end of file diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc new file mode 100644 index 0000000..b7141c0 --- /dev/null +++ b/sources/GameModes/BaseGameMode.uc @@ -0,0 +1,386 @@ +/** + * Base class for a game mode config, contains all the information Acedia's + * game modes must have, including settings + * (`includeFeature`, `includeFeatureAs` and `excludeFeature`) + * for picking used `Feature`s. + * + * Contains three types of methods: + * 1. Getters for its values; + * 2. `UpdateFeatureArray()` method for updating list of `Feature`s to + * be used based on game info's settings; + * 3. `Report...()` methods that perform various validation checks + * (and log them) on config data. + * 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 BaseGameMode extends AcediaConfig + dependson(CoreService) + abstract; + +// Name of the game mode players will see in voting (formatted string) +var protected config string title; +// Preferable difficulty level (plain string) +var protected config string difficulty; +// `Mutator`s to add with this game mode +var protected config array includeMutator; +// `Feature`s to include (with "default" config) +var protected config array includeFeature; +// `Feature`s to exclude from game mode, regardless of other settings +// (this one has highest priority) +var protected config array excludeFeature; + +struct FeatureConfigPair +{ + var public string feature; + var public string config; +}; +// `Feature`s to include (with specified config). +// Higher priority than `includeFeature`, but lower than `excludeFeature`. +var protected config array includeFeatureAs; + +var private LoggerAPI.Definition warnBadMutatorName, warnBadFeatureName; + +protected function AssociativeArray ToData() +{ + local int i; + local AssociativeArray result; + local AssociativeArray nextPair; + local DynamicArray nextArray; + result = _.collections.EmptyAssociativeArray(); + result.SetItem(P("title"), _.text.FromFormattedString(title)); + result.SetItem(P("difficulty"), _.text.FromString(difficulty)); + nextArray = _.collections.EmptyDynamicArray(); + for (i = 0; i < includeFeature.length; i += 1) { + nextArray.AddItem(_.text.FromString(includeFeature[i])); + } + result.SetItem(P("includeFeature"), nextArray); + nextArray = _.collections.EmptyDynamicArray(); + for (i = 0; i < excludeFeature.length; i += 1) { + nextArray.AddItem(_.text.FromString(excludeFeature[i])); + } + result.SetItem(P("excludeFeature"), nextArray); + nextArray = _.collections.EmptyDynamicArray(); + for (i = 0; i < includeMutator.length; i += 1) { + nextArray.AddItem(_.text.FromString(includeFeature[i])); + } + result.SetItem(P("includeMutator"), nextArray); + nextArray = _.collections.EmptyDynamicArray(); + for (i = 0; i < includeFeatureAs.length; i += 1) + { + nextPair = _.collections.EmptyAssociativeArray(); + nextPair.SetItem(P("feature"), + _.text.FromString(includeFeatureAs[i].feature)); + nextPair.SetItem(P("config"), + _.text.FromString(includeFeatureAs[i].config)); + nextArray.AddItem(nextPair); + } + result.SetItem(P("includeFeatureAs"), nextArray); + return result; +} + +protected function FromData(AssociativeArray source) +{ + local int i; + local Text nextText; + local DynamicArray includeFeatureAsSource; + if (source == none) { + return; + } + nextText = source.GetText(P("title")); + if (nextText != none) { + title = nextText.ToFormattedString(); + } + nextText = source.GetText(P("difficulty")); + if (nextText != none) { + difficulty = nextText.ToPlainString(); + } + includeFeature = + DynamicIntoStringArray(source.GetDynamicArray(P("includeFeature"))); + excludeFeature = + DynamicIntoStringArray(source.GetDynamicArray(P("excludeFeature"))); + includeMutator = + DynamicIntoStringArray(source.GetDynamicArray(P("includeMutator"))); + includeFeatureAsSource = source.GetDynamicArray(P("includeFeatureAs")); + if (includeFeatureAsSource == none) { + return; + } + includeFeatureAs.length = 0; + for (i = 0; i < includeFeatureAsSource.GetLength(); i += 1) + { + includeFeatureAs[i] = AssociativeArrayIntoPair( + includeFeatureAsSource.GetAssociativeArray(i)); + } +} + +private final function FeatureConfigPair AssociativeArrayIntoPair( + AssociativeArray source) +{ + local Text nextText; + local FeatureConfigPair result; + if (source == none) { + return result; + } + nextText = source.GetText(P("feature")); + if (nextText != none) { + result.feature = nextText.ToPlainString(); + } + nextText = source.GetText(P("config")); + if (nextText != none) { + result.config = nextText.ToPlainString(); + } + return result; +} + +private final function array DynamicIntoStringArray(DynamicArray source) +{ + local int i; + local Text nextText; + local array result; + if (source == none) { + return result; + } + for (i = 0; i < source.GetLength(); i += 1) + { + nextText = source.GetText(i); + if (nextText != none) { + includeFeature[i] = nextText.ToPlainString(); + } + } +} + +protected function array StringToTextArray(array input) +{ + local int i; + local array result; + for (i = 0; i < input.length; i += 1) { + result[i] = _.text.FromString(input[i]); + } + return result; +} + +/** + * @return Name of the `GameInfo` class to be used with the caller game mode. + */ +public function Text GetGameTypeClass() +{ + return none; +} + +/** + * @return Human-readable name of the caller game mode. + * Players will see it as the name of the mode in the voting options. + */ +public function Text GetTitle() +{ + return _.text.FromFormattedString(title); +} + +/** + * @return Specified difficulty for the game mode. + * Interpretation of this value can depend on each particular game mode. + */ +public function Text GetDifficulty() +{ + return _.text.FromString(difficulty); +} + +/** + * Checks `Feature`-related settings (`includeFeature`, `includeFeatureAs` and + * `excludeFeature`) for correctness and reports any issues. + * Currently correctness check simply ensures that all listed `Feature`s + * actually exist. + */ +public function ReportIncorrectSettings( + array featuresToEnable) +{ + local int i; + local array featureNames, featuresToReplace; + for (i = 0; i < featuresToEnable.length; i += 1) { + featureNames[i] = string(featuresToEnable[i].featureClass); + } + ValidateFeatureArray(includeFeature, featureNames, "includeFeatures"); + ValidateFeatureArray(excludeFeature, featureNames, "excludeFeatures"); + for (i = 0; i < includeFeatureAs.length; i += 1) { + featuresToReplace[i] = includeFeatureAs[i].feature; + } + ValidateFeatureArray(featuresToReplace, featureNames, "includeFeatureAs"); +} + +/** + * Checks `Mutator`-related settings (`includeMutator`) for correctness and + * reports any issues. + * Currently correctness check performs a simple validity check for mutator, + * to make sure it would not define a new option in server's URL. + * + * See `ValidateServerURLName()` for more information. + */ +public function ReportBadMutatorNames() +{ + local int i; + for (i = 0; i < includeMutator.length; i += 1) + { + if (!ValidateServerURLName(includeMutator[i])) + { + _.logger.Auto(warnBadMutatorName) + .Arg(_.text.FromString(includeMutator[i])) + .Arg(_.text.FromString(string(name))); + } + } +} + +/** + * Makes sure that a word to be used in server URL as a part of an option + * does not contain "," / "?" / "=" or whitespace. + * This is useful to make sure that user-specified mutator entries only add + * one mutator or option's key / values will not specify only one pair, + * avoiding "?opt1=value1?opt2=value2" entries. + */ +protected function bool ValidateServerURLName(string entry) +{ + if (InStr(entry, "=") >= 0) return false; + if (InStr(entry, "?") >= 0) return false; + if (InStr(entry, ",") >= 0) return false; + if (InStr(entry, " ") >= 0) return false; + return true; +} + +// Is every element `subset` present inside `whole`? +private function ValidateFeatureArray( + array subset, + array whole, + string arrayName) +{ + local int i, j; + local bool foundItem; + for (i = 0; i < subset.length; i += 1) + { + foundItem = false; + for (j = 0; j < whole.length; j += 1) + { + if (subset[i] ~= whole[j]) + { + foundItem = true; + break; + } + } + if (!foundItem) + { + _.logger.Auto(warnBadMutatorName) + .Arg(_.text.FromString(includeMutator[i])) + .Arg(_.text.FromString(string(name))) + .Arg(_.text.FromString(arrayName)); + } + } +} + +/** + * Updates passed `Feature` settings according to this game mode's settings. + * + * @param featuresToEnable Settings to update. + * `FeatureConfigPair` is a pair of `Feature` (`featureClass`) and its + * config's name (`configName`). + * If `configName` is set to `none`, then corresponding `Feature` + * should not be enabled. + * Otherwise it should be enabled with a specified config. + */ +public function UpdateFeatureArray( + out array featuresToEnable) +{ + local int i; + local Text newConfigName; + local string nextFeatureClassName; + for (i = 0; i < featuresToEnable.length; i += 1) + { + nextFeatureClassName = string(featuresToEnable[i].featureClass); + // `excludeFeature` + if (FeatureExcluded(nextFeatureClassName)) + { + _.memory.Free(featuresToEnable[i].configName); + featuresToEnable[i].configName = none; + continue; + } + // `includeFeatureAs` + newConfigName = TryReplacingFeatureConfig(nextFeatureClassName); + if (newConfigName != none) + { + _.memory.Free(featuresToEnable[i].configName); + featuresToEnable[i].configName = newConfigName; + } + // `includeFeature` + if ( featuresToEnable[i].configName == none + && FeatureInIncludedArray(nextFeatureClassName)) + { + featuresToEnable[i].configName = P("default").Copy(); + } + } +} + +private function bool FeatureExcluded(string featureClassName) +{ + local int i; + for (i = 0; i < excludeFeature.length; i += 1) + { + if (excludeFeature[i] ~= featureClassName) { + return true; + } + } + return false; +} + +private function Text TryReplacingFeatureConfig(string featureClassName) +{ + local int i; + for (i = 0; i < includeFeatureAs.length; i += 1) + { + if (includeFeatureAs[i].feature ~= featureClassName) { + return _.text.FromString(includeFeatureAs[i].config); + } + } + return none; +} + +private function bool FeatureInIncludedArray(string featureClassName) +{ + local int i; + for (i = 0; i < includeFeature.length; i += 1) + { + if (includeFeature[i] ~= featureClassName) { + return true; + } + } + return false; +} + +public function array GetIncludedMutators() +{ + local int i; + local array validatedMutators; + for (i = 0; i < includeMutator.length; i += 1) + { + if (ValidateServerURLName(includeMutator[i])) { + validatedMutators[validatedMutators.length] = includeMutator[i]; + } + } + return StringToTextArray(validatedMutators); +} + +defaultproperties +{ + configName = "AcediaGameModes" + warnBadMutatorName = (l=LOG_Warning,m="Mutator \"%1\" specified for game mode \"%2\" contains invalid characters and will be ignored. This is a configuration error, you should fix it.") + warnBadFeatureName = (l=LOG_Warning,m="Feature \"%1\" specified for game mode \"%2\" in array `%3` does not exist in enabled packages and will be ignored. This is a configuration error, you should fix it.") +} \ No newline at end of file diff --git a/sources/GameModes/GameMode.uc b/sources/GameModes/GameMode.uc new file mode 100644 index 0000000..4c1960f --- /dev/null +++ b/sources/GameModes/GameMode.uc @@ -0,0 +1,201 @@ +/** + * The only implementation for `BaseGameMode` suitable for standard + * killing floor game types. + * 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 GameMode extends BaseGameMode + perobjectconfig + config(AcediaGameModes); + +struct GameOption +{ + var public string key; + var public string value; +}; +// Allow to specify additional server options for this game mode +var protected config array option; +// Specify `GameInfo`'s class to use, default is "KFMod.KFGameType" +// (plain string) +var protected config string gameTypeClass; +// Short version of the name of the game mode players will see in +// voting handler messages sometimes (plain string) +var protected config string acronym; +// Map prefix - only maps that start with specified prefix will be voteable for +// this game mode (plain string) +var protected config string mapPrefix; + +var private LoggerAPI.Definition warnBadOption; + +protected function DefaultIt() +{ + title = "Acedia game mode"; + difficulty = "Hell On Earth"; + gameTypeClass = "KFMod.KFGameType"; + acronym = ""; + mapPrefix = "KF"; + includeFeature.length = 0; + excludeFeature.length = 0; + includeMutator.length = 0; + option.length = 0; +} + +protected function AssociativeArray ToData() +{ + local int i; + local AssociativeArray result; + local AssociativeArray nextPair; + local DynamicArray nextArray; + result = super.ToData(); + if (result == none) { + return none; + } + result.SetItem(P("gameTypeClass"), _.text.FromString(gameTypeClass)); + result.SetItem(P("acronym"), _.text.FromString(acronym)); + result.SetItem(P("mapPrefix"), _.text.FromString(mapPrefix)); + nextArray = _.collections.EmptyDynamicArray(); + for (i = 0; i < option.length; i += 1) + { + nextPair = _.collections.EmptyAssociativeArray(); + nextPair.SetItem(P("key"), _.text.FromString(option[i].key)); + nextPair.SetItem(P("value"), _.text.FromString(option[i].value)); + nextArray.AddItem(nextPair); + } + result.SetItem(P("option"), nextArray); + return result; +} + +protected function FromData(AssociativeArray source) +{ + local int i; + local Text nextText; + local GameOption nextPair; + local DynamicArray nextArray; + super.FromData(source); + if (source == none) { + return; + } + nextText = source.GetText(P("gameTypeClass")); + if (nextText != none) { + gameTypeClass = nextText.ToPlainString(); + } + nextText = source.GetText(P("acronym")); + if (nextText != none) { + acronym = nextText.ToPlainString(); + } + nextText = source.GetText(P("mapPrefix")); + if (nextText != none) { + mapPrefix = nextText.ToPlainString(); + } + nextArray = source.GetDynamicArray(P("option")); + if (nextArray == none) { + return; + } + option.length = 0; + for (i = 0; i < nextArray.GetLength(); i += 1) + { + nextPair.key = ""; + nextPair.value = ""; + nextText = source.GetText(P("key")); + if (nextText != none) { + nextPair.key = nextText.ToPlainString(); + } + nextText = source.GetText(P("value")); + if (nextText != none) { + nextPair.value = nextText.ToPlainString(); + } + option[option.length] = nextPair; + } +} + +public function Text GetGameTypeClass() +{ + if (gameTypeClass == "") { + return P("KFMod.KFGameType").Copy(); + } + else { + return _.text.FromString(gameTypeClass); + } +} + +public function Text GetAcronym() +{ + if (acronym == "") { + return _.text.FromString(string(name)); + } + else { + return _.text.FromString(acronym); + } +} + +public function Text GetMapPrefix() +{ + if (acronym == "") { + return _.text.FromString("KF-"); + } + else { + return _.text.FromString(mapPrefix); + } +} + +/** + * Checks option-related settings (`option`) for correctness and reports + * any issues. + * Currently correctness check performs a simple validity check for mutator, + * to make sure it would not define a new option in server's URL. + * + * See `ValidateServerURLName()` in `BaseGameMode` for more information. + */ +public function ReportBadOptions() +{ + local int i; + for (i = 0; i < option.length; i += 1) + { + if ( !ValidateServerURLName(option[i].key) + || !ValidateServerURLName(option[i].value)) + { + _.logger.Auto(warnBadOption) + .Arg(_.text.FromString(option[i].key)) + .Arg(_.text.FromString(option[i].value)) + .Arg(_.text.FromString(string(name))); + } + } +} + +/** + * @return Server options as key-value pairs in an `AssociativeArray`. + */ +public function AssociativeArray GetOptions() +{ + local int i; + local AssociativeArray result; + result = _.collections.EmptyAssociativeArray(); + for (i = 0; i < option.length; i += 1) + { + if (!ValidateServerURLName(option[i].key)) continue; + if (!ValidateServerURLName(option[i].value)) continue; + result.SetItem( _.text.FromString(option[i].key), + _.text.FromString(option[i].value)); + } + return result; +} + +defaultproperties +{ + configName = "AcediaGameModes" + warnBadOption = (l=LOG_Warning,m="Option with key \"%1\" and value \"%2\" specified for game mode \"%3\" contains invalid characters and will be ignored. This is a configuration error, you should fix it.") +} \ No newline at end of file diff --git a/sources/Packages.uc b/sources/Packages.uc index fb0a796..05a2cb6 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -2,7 +2,7 @@ * Main and only Acedia mutator used for loading Acedia packages * and providing access to mutator events' calls. * Name is chosen to make config files more readable. - * Copyright 2020 Anton Tarasenko + * Copyright 2020-2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -20,6 +20,7 @@ * along with Acedia. If not, see . */ class Packages extends Mutator + dependson(CoreService) config(Acedia); // Default value of this variable will be used to store @@ -32,23 +33,29 @@ var private Packages selfReference; // Acedia's reference to a `Global` object. var private Global _; -// Package's manifest is supposed to always have a name of -// ".Manifest", this variable stores the ".Manifest" part -var private const string manifestSuffix; - // Array of predefined services that must be started along with Acedia mutator. var private config array package; +// Set to `true` to activate Acedia's game modes system +var private config bool useGameModes; +// Responsible for setting up Acedia's game modes in current voting system +var VotingHandlerAdapter votingAdapter; -// AcediaCore package that this launcher is build for -var private config const string corePackage; +var Mutator_OnMutate_Signal onMutateSignal; +var Mutator_OnCheckReplacement_Signal onCheckReplacementSignal; + +var private LoggerAPI.Definition infoFeatureEnabled; static public final function Packages GetInstance() { return default.selfReference; } +// "Constructor" event PreBeginPlay() { + local GameMode currentGameMode; + local array availableFeatures; + CheckForGarbage(); // Enforce one copy rule and remember a reference to that copy if (default.selfReference != none) { @@ -56,179 +63,123 @@ event PreBeginPlay() return; } default.selfReference = self; - BootUp(); - if (class'TestingService'.default.runTestsOnStartUp) { - RunStartUpTests(); - } -} - -private final function BootUp() -{ - local int i; - local class<_manifest> nextManifest; - // Load core - Spawn(class'CoreService'); + // Launch and setup core Acedia + class'CoreService'.static.LaunchAcedia(self, package); _ = class'Global'.static.GetInstance(); - nextManifest = LoadManifestClass(corePackage); - if (nextManifest == none) + SetupMutatorSignals(); + // Determine required features and launch them + availableFeatures = CoreService(class'CoreService'.static.GetInstance()) + .GetAutoConfigurationInfo(); + if (useGameModes) { - /*_.logger.Fatal("Cannot load required AcediaCore package \"" - $ corePackage $ "\". Acedia will shut down.");*/ - Destroy(); - return; - } - LoadManifest(nextManifest); - // Load packages - for (i = 0; i < package.length; i += 1) - { - nextManifest = LoadManifestClass(package[i]); - if (nextManifest == none) - { - /*_.logger.Failure("Cannot load `Manifest` for package \"" - $ package[i] $ "\". Check if it's missing or" - @ "if it's name is spelled incorrectly.");*/ - continue; + votingAdapter = VotingHandlerAdapter( + _.memory.Allocate(class'VotingHandlerAdapter')); + votingAdapter.InjectIntoVotingHandler(); + currentGameMode = votingAdapter.SetupGameModeAfterTravel(); + if (currentGameMode != none) { + currentGameMode.UpdateFeatureArray(availableFeatures); } - LoadManifest(nextManifest); } - // Inject broadcast handler - InjectBroadcastHandler(); + EnableFeatures(availableFeatures); } -private final function RunStartUpTests() +// "Finalizer" +function ServerTraveling(string URL, bool bItems) { - local TestingService testService; - testService = TestingService(class'TestingService'.static.Require()); - testService.PrepareTests(); - if (testService.filterTestsByName) { - testService.FilterByName(testService.requiredName); - } - if (testService.filterTestsByGroup) { - testService.FilterByGroup(testService.requiredGroup); - } - if (testService.Run()) + if (votingAdapter != none) { - // This listener will output test results into server's console - class'TestingListener_AcediaLauncher'.static.SetActive(true); + votingAdapter.PrepareForServerTravel(); + votingAdapter.RestoreVotingHandlerConfigBackup(); + _.memory.Free(votingAdapter); + votingAdapter = none; + } + default.selfReference = none; + CoreService(class'CoreService'.static.GetInstance()).ShutdownAcedia(); + if (nextMutator != none) { + nextMutator.ServerTraveling(URL, bItems); + } + Destroy(); +} + +// Checks whether Acedia has left garbage after the previous map. +// This can lead to serious problems, so such diagnostic check is warranted. +private function CheckForGarbage() +{ + local int leftoverObjectAmount, leftoverActorAmount, leftoverDBRAmount; + local AcediaObject nextObject; + local AcediaActor nextActor; + local DBRecord nextRecord; + foreach AllObjects(class'AcediaObject', nextObject) { + leftoverObjectAmount += 1; + } + foreach AllActors(class'AcediaActor', nextActor) { + leftoverActorAmount += 1; + } + foreach AllObjects(class'DBRecord', nextRecord) { + leftoverDBRAmount += 1; + } + if ( leftoverObjectAmount == 0 && leftoverActorAmount == 0 + && leftoverDBRAmount == 0) + { + Log("Acedia garbage check: nothing was found."); } else { - //_.logger.Failure("Could not launch Acedia's start up testing process."); + Log("Acedia garbage check: garbage was found." @ + "This can cause problems, report it."); + Log("Leftover object:" @ leftoverObjectAmount); + Log("Leftover actors:" @ leftoverActorAmount); + Log("Leftover database records:" @ leftoverDBRAmount); } } -private final function class<_manifest> LoadManifestClass(string packageName) -{ - return class<_manifest>(DynamicLoadObject( packageName $ manifestSuffix, - class'Class', true)); -} - -private final function LoadManifest(class<_manifest> manifestClass) +private function EnableFeatures(array features) { local int i; - for (i = 0; i < manifestClass.default.aliasSources.length; i += 1) + for (i = 0; i < features.length; i += 1) { - if (manifestClass.default.aliasSources[i] == none) continue; - //Spawn(manifestClass.default.aliasSources[i]); - _.memory.Allocate(manifestClass.default.aliasSources[i]); - } - LaunchServicesAndFeatures(manifestClass); - if (class'Commands_Feature'.static.IsEnabled()) { - RegisterCommands(manifestClass); - } - for (i = 0; i < manifestClass.default.testCases.length; i += 1) - { - class'TestingService'.static - .RegisterTestCase(manifestClass.default.testCases[i]); + if (features[i].featureClass == none) continue; + if (features[i].configName == none) continue; + features[i].featureClass.static.EnableMe(features[i].configName); + _.logger.Auto(infoFeatureEnabled) + .Arg(_.text.FromString(string(features[i].featureClass))) + .Arg(features[i].configName); // consumes `configName` } } -private final function RegisterCommands(class<_manifest> manifestClass) +// Fetches and sets up signals that `Mutator` needs to provide +private function SetupMutatorSignals() { - local int i; - local Commands_Feature commandsFeature; - commandsFeature = - Commands_Feature(class'Commands_Feature'.static.GetInstance()); - for (i = 0; i < manifestClass.default.commands.length; i += 1) - { - if (manifestClass.default.commands[i] == none) continue; - commandsFeature.RegisterCommand(manifestClass.default.commands[i]); - } + local UnrealService service; + service = UnrealService(class'UnrealService'.static.Require()); + onMutateSignal = Mutator_OnMutate_Signal( + service.GetSignal(class'Mutator_OnMutate_Signal')); + onCheckReplacementSignal = Mutator_OnCheckReplacement_Signal( + service.GetSignal(class'Mutator_OnCheckReplacement_Signal')); } -private final function LaunchServicesAndFeatures(class<_manifest> manifestClass) -{ - local int i; - local Text autoConfigName; - // Services - for (i = 0; i < manifestClass.default.services.length; i += 1) - { - if (manifestClass.default.services[i] == none) continue; - manifestClass.default.services[i].static.Require(); - } - // Features - for (i = 0; i < manifestClass.default.features.length; i += 1) - { - if (manifestClass.default.features[i] == none) continue; - manifestClass.default.features[i].static.LoadConfigs(); - autoConfigName = - manifestClass.default.features[i].static.GetAutoEnabledConfig(); - if (autoConfigName != none) { - manifestClass.default.features[i].static.EnableMe(autoConfigName); - } - _.memory.Free(autoConfigName); - } -} - -private final function InjectBroadcastHandler() -{ - local BroadcastEventsObserver ourBroadcastHandler; - local BroadcastEventsObserver.InjectionLevel injectionLevel; - injectionLevel = class'BroadcastEventsObserver'.default.usedInjectionLevel; - if (level == none || level.game == none) return; - if (injectionLevel == BHIJ_None) return; - - ourBroadcastHandler = Spawn(class'BroadcastEventsObserver'); - if (injectionLevel == BHIJ_Registered) - { - level.game.broadcastHandler - .RegisterBroadcastHandler(ourBroadcastHandler); - return; - } - // Here `injectionLevel == BHIJ_Root` holds. - // Swap out level's first handler with ours - // (needs to be done for both actor reference and it's class) - ourBroadcastHandler.nextBroadcastHandler = level.game.broadcastHandler; - ourBroadcastHandler.nextBroadcastHandlerClass = level.game.broadcastClass; - level.game.broadcastHandler = ourBroadcastHandler; - level.game.broadcastClass = class'BroadcastEventsObserver'; -} - -// Acedia is only able to run in a server mode right now, -// so this function is just a stub. -public final function bool IsServerOnly() -{ - return true; -} - -// Provide a way to handle CheckReplacement event +/** + * Below `Mutator` events are redirected into appropriate signals. + */ function bool CheckReplacement(Actor other, out byte isSuperRelevant) { - return class'MutatorEvents'.static. - CallCheckReplacement(other, isSuperRelevant); + if (onCheckReplacementSignal != none) { + return onCheckReplacementSignal.Emit(other, isSuperRelevant); + } + return true; } function Mutate(string command, PlayerController sendingController) { - if (class'MutatorEvents'.static.CallMutate(command, sendingController)) { - super.Mutate(command, sendingController); + if (onMutateSignal != none) { + onMutateSignal.Emit(command, sendingController); } + super.Mutate(command, sendingController); } defaultproperties { - corePackage = "AcediaCore_0_2" - manifestSuffix = ".Manifest" + useGameModes = false // This is a server-only mutator remoteRole = ROLE_None bAlwaysRelevant = true @@ -236,4 +187,5 @@ defaultproperties GroupName = "Package loader" FriendlyName = "Acedia loader" Description = "Launcher for Acedia packages" + infoFeatureEnabled = (l=LOG_Info,m="Feature `%1` enabled with config \"%2\".") } \ No newline at end of file diff --git a/sources/StartUp.uc b/sources/StartUp.uc index 7d4ac49..ac40796 100644 --- a/sources/StartUp.uc +++ b/sources/StartUp.uc @@ -23,8 +23,7 @@ class StartUp extends Actor; function PreBeginPlay() { super.PreBeginPlay(); - if (level != none && level.game != none) - { + if (level != none && level.game != none) { level.game.AddMutator(string(class'Packages')); } Destroy(); diff --git a/sources/TestingListener_AcediaLauncher.uc b/sources/TestingListener_AcediaLauncher.uc deleted file mode 100644 index ac2283c..0000000 --- a/sources/TestingListener_AcediaLauncher.uc +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Overloaded testing events listener to catch when tests that we run during - * server loading finish. - * 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 TestingListener_AcediaLauncher extends TestingListenerBase - abstract; - -static function TestingEnded( - array< class > testQueue, - array results) -{ - local int i; - local MutableText nextLine; - local array textSummary; - nextLine = __().text.Empty(); - textSummary = class'TestCaseSummary'.static.GenerateStringSummary(results); - for (i = 0; i < textSummary.length; i += 1) - { - nextLine.Clear(); - nextLine.AppendFormattedString(textSummary[i]); - Log(nextLine.ToPlainString()); - } - // No longer need to listen to testing events - SetActive(false); -} - -defaultproperties -{ - relatedEvents = class'TestingEvents' -} \ No newline at end of file diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc new file mode 100644 index 0000000..18d76ba --- /dev/null +++ b/sources/VotingHandlerAdapter.uc @@ -0,0 +1,345 @@ +/** + * Acedia currently lacks its own means to provide a map/mode voting + * (and new voting mod with proper GUI would not be whitelisted anyway). + * This is why this class was made - to inject existing voting handlers with + * data from Acedia's game modes. + * Requires `GameInfo`'s voting handler to be derived from + * `XVotingHandler`, which is satisfied by pretty much every used handler. + * 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 VotingHandlerAdapter extends AcediaObject + dependson(VotingHandler); + +/** + * All usage of this object should start with `InjectIntoVotingHandler()` + * method that will read all the `GameMode` configs and fill voting handler's + * config with their data, while making a backup of all values. + * Backup can be restored with `RestoreVotingHandlerConfigBackup()` method. + * How that affects the clients depends on whether restoration was done before, + * during or after the replication. It is intended to be done after + * server travel has started. + * the process of injection is to create an ordered list of game modes + * (`availableGameModes`) and generate appropriate voting handler's configs + * with `BuildVotingHandlerConfig()`, saving them in the same order inside + * the voting handler. Picked game mode is then determined by index of + * the picked voting handler's option. + * + * Additionally this class has a static internal state that allows it to + * transfer data along the server travel - it is used mainly to remember picked + * game mode and enforce game's difficulty by altering and restoring + * `GameInfo`'s variable. + * To make such transfer happen one must call `PrepareForServerTravel()` before + * server travel to set the internal static state and + * then `SetupGameModeAfterTravel()` after travel (when the new map is loading) + * to read (and forget) from internal state. + */ + +// Aliases are an unnecessary overkill for difficulty names, so just define +// them in special `string` arrays. +// We accept detect not just these exact words, but any of their prefixes. +var private const array beginnerSynonyms; +var private const array normalSynonyms; +var private const array hardSynonyms; +var private const array suicidalSynonyms; +var private const array hoeSynonyms; + +// All available game modes for Acedia, loaded during initialization. +// This array is directly produces replacement for `XVotingHandler`'s +// `gameConfig` array and records of `availableGameModes` relate to those of +// `gameConfig` with the same index. +// So if we know that a voting option with a certain index was chosen - +// it means that user picked game mode from `availableGameModes` with +// the same index. +var private array availableGameModes; + +// Finding voting handler is not cheap, so only do it once and then store it. +var private NativeActorRef votingHandlerReference; +// Save `VotingHandler`'s config to restore it before server travel - +// otherwise Acedia will alter its config +var private array backupVotingHandlerConfig; + +// Setting default value of this flag to `true` indicates that map switching +// just occurred and we need to recover some information from the previous map. +var private bool isServerTraveling; +// We should not rely on "VotingHandler" to inform us from which game mode its +// selected config option originated after server travel, so we need to +// remember it in this default variable before switching maps. +var private string targetGameMode; +// Acedia's game modes intend on supporting difficulty switching, but +// `KFGameType` does not support appropriate flags, so we enforce default +// difficulty by overwriting default value of its `gameDifficulty` variable. +// But to not affect game's configs we must restore old value after new map is +// loaded. Store it in default variable for that. +var private float storedGameDifficulty; + +var private LoggerAPI.Definition fatNoXVotingHandler, fatBadGameConfigIndexVH; +var private LoggerAPI.Definition fatBadGameConfigIndexAdapter; + +protected function Finalizer() +{ + _.memory.Free(votingHandlerReference); + _.memory.FreeMany(availableGameModes); + votingHandlerReference = none; + availableGameModes.length = 0; +} + +/** + * Replaces `XVotingHandler`'s configs with Acedia's game modes. + * Backup of replaced configs is made internally, so that they can be restored + * on map change. + */ +public final function InjectIntoVotingHandler() +{ + local int i; + local GameMode nextGameMode; + local XVotingHandler votingHandler; + local array newVotingHandlerConfig; + if (votingHandlerReference != none) { + return; + } + votingHandler = XVotingHandler(_.unreal.FindActorInstance( + _.unreal.GetGameType().VotingHandlerClass)); + if (votingHandler == none) + { + _.logger.Auto(fatNoXVotingHandler); + return; + } + votingHandlerReference = _.unreal.ActorRef(votingHandler); + class'GameMode'.static.Initialize(); + availableGameModes = class'GameMode'.static.AvailableConfigs(); + for (i = 0; i < availableGameModes.length; i += 1) + { + nextGameMode = GameMode(class'GameMode'.static + .GetConfigInstance(availableGameModes[i])); + newVotingHandlerConfig[i] = BuildVotingHandlerConfig(nextGameMode); + // Report omitted mutators / server options + nextGameMode.ReportBadMutatorNames(); + nextGameMode.ReportBadOptions(); + } + backupVotingHandlerConfig = votingHandler.gameConfig; + votingHandler.gameConfig = newVotingHandlerConfig; +} + +private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig( + GameMode gameMode) +{ + local VotingHandler.MapVoteGameConfig result; + result.gameClass = _.text.ToString(gameMode.GetGameTypeClass()); + result.gameName = _.text.ToColoredString(gameMode.GetTitle()); + result.prefix = _.text.ToString(gameMode.GetMapPrefix()); + result.acronym = _.text.ToString(gameMode.GetAcronym()); + result.mutators = BuildMutatorString(gameMode); + result.options = BuildOptionsString(gameMode); + return result; +} + +private function string BuildMutatorString(GameMode gameMode) +{ + local int i; + local string result; + local array usedMutators; + usedMutators = gameMode.GetIncludedMutators(); + for (i = 0; i < usedMutators.length; i += 1) + { + if (i > 0) { + result $= ","; + } + result $= _.text.ToString(usedMutators[i]); + } + return result; +} + +private function string BuildOptionsString(GameMode gameMode) +{ + local bool optionWasAdded; + local string result; + local string nextKey, nextValue; + local Iter iter; + local AssociativeArray options; + options = gameMode.GetOptions(); + for (iter = options.Iterate(); !iter.HasFinished(); iter.Next()) + { + nextKey = Text(iter.GetKey()).ToPlainString(); + nextValue = Text(iter.Get()).ToPlainString(); + if (optionWasAdded) { + result $= "?"; + } + result $= (nextKey $ "=" $ nextValue); + optionWasAdded = true; + } + options.Empty(true); + options.FreeSelf(); + iter.FreeSelf(); + return result; +} + +/** + * Makes necessary preparations for the server travel. + */ +public final function PrepareForServerTravel() +{ + local int pickedVHConfig; + local GameMode nextGameMode; + local string nextGameClassName; + local class nextGameClass; + local XVotingHandler votingHandler; + if (votingHandlerReference == none) return; + votingHandler = XVotingHandler(votingHandlerReference.Get()); + if (votingHandler == none) return; + // Server travel caused by something else than `XVotingHandler` + if (!votingHandler.bLevelSwitchPending) return; + + pickedVHConfig = votingHandler.currentGameConfig; + if (pickedVHConfig < 0 || pickedVHConfig >= votingHandler.gameConfig.length) + { + _.logger.Auto(fatBadGameConfigIndexVH) + .ArgInt(pickedVHConfig) + .ArgInt(votingHandler.gameConfig.length); + return; + } + if (pickedVHConfig >= availableGameModes.length) + { + _.logger.Auto(fatBadGameConfigIndexAdapter) + .ArgInt(pickedVHConfig) + .ArgInt(availableGameModes.length); + return; + } + nextGameClassName = votingHandler.gameConfig[pickedVHConfig].gameClass; + if (string(_.unreal.GetGameType().class) ~= nextGameClassName) { + nextGameClass = _.unreal.GetGameType().class; + } + else { + nextGameClass = class(_.memory.LoadClassS(nextGameClassName)); + } + default.isServerTraveling = true; + default.targetGameMode = availableGameModes[pickedVHConfig].ToPlainString(); + nextGameMode = GetConfigFromString(default.targetGameMode); + default.storedGameDifficulty = nextGameClass.default.gameDifficulty; + nextGameClass.default.gameDifficulty = GetNumericDifficulty(nextGameMode); +} + +/** + * Restore `GameInfo`'s settings after the server travel and + * apply selected `GameMode`. + * + * @return `GameMode` picked before server travel + * (the one that must be running now). + */ +public final function GameMode SetupGameModeAfterTravel() +{ + if (!default.isServerTraveling) { + return none; + } + _.unreal.GetGameType().default.gameDifficulty = default.storedGameDifficulty; + default.isServerTraveling = false; + return GetConfigFromString(targetGameMode); +} + +/** + * Restores `XVotingHandler`'s config to the values that were overridden by + * `VHAdapter`'s `InjectIntoVotingHandler()` method. + */ +public final function RestoreVotingHandlerConfigBackup() +{ + local XVotingHandler votingHandler; + if (votingHandlerReference == none) return; + votingHandler = XVotingHandler(votingHandlerReference.Get()); + if (votingHandler == none) return; + + votingHandler.gameConfig = backupVotingHandlerConfig; + votingHandler.default.gameConfig = backupVotingHandlerConfig; + votingHandler.SaveConfig(); +} + +// `GameMode`'s name as a `string` -> `GameMode` instance +private function GameMode GetConfigFromString(string configName) +{ + local GameMode result; + local Text nextConfigName; + nextConfigName = _.text.FromString(configName); + result = GameMode(class'GameMode'.static.GetConfigInstance(nextConfigName)); + _.memory.Free(nextConfigName); + return result; +} + +// Convert `GameMode`'s difficulty's textual representation into +// KF's numeric one. +private final function int GetNumericDifficulty(GameMode gameMode) +{ + local int i; + local string difficulty; + difficulty = Locs(_.text.ToString(gameMode.GetDifficulty())); + for (i = 0; i < default.beginnerSynonyms.length; i += 1) + { + if (IsPrefixOf(difficulty, default.beginnerSynonyms[i])) { + return 1; + } + } + for (i = 0; i < default.normalSynonyms.length; i += 1) + { + if (IsPrefixOf(difficulty, default.normalSynonyms[i])) { + return 2; + } + } + for (i = 0; i < default.hardSynonyms.length; i += 1) + { + if (IsPrefixOf(difficulty, default.hardSynonyms[i])) { + return 4; + } + } + for (i = 0; i < default.suicidalSynonyms.length; i += 1) + { + if (IsPrefixOf(difficulty, default.suicidalSynonyms[i])) { + return 5; + } + } + for (i = 0; i < default.hoeSynonyms.length; i += 1) + { + if (IsPrefixOf(difficulty, default.hoeSynonyms[i])) { + return 7; + } + } + return int(difficulty); +} + +protected final static function bool IsPrefixOf(string prefix, string value) +{ + return (InStr(value, prefix) == 0); +} + +defaultproperties +{ + beginnerSynonyms(0) = "easy" + beginnerSynonyms(1) = "beginer" + beginnerSynonyms(2) = "beginner" + beginnerSynonyms(3) = "begginer" + beginnerSynonyms(4) = "begginner" + normalSynonyms(0) = "regular" + normalSynonyms(1) = "default" + normalSynonyms(2) = "normal" + hardSynonyms(0) = "harder" // "hard" is prefix of this, so it will count + hardSynonyms(1) = "difficult" + suicidalSynonyms(0) = "suicidal" + hoeSynonyms(0) = "hellonearth" + hoeSynonyms(1) = "hellon earth" + hoeSynonyms(2) = "hell onearth" + hoeSynonyms(3) = "hoe" + fatNoXVotingHandler = (l=LOG_Fatal,m="`XVotingHandler` class is missing. Make sure your server setup supports Acedia's game modes (by used voting handler derived from `XVotingHandler`).") + fatBadGameConfigIndexVH = (l=LOG_Fatal,m="`XVotingHandler`'s `currentGameConfig` variable value of %1 is out-of-bounds for `XVotingHandler.gameConfig` of length %2. Report this issue.") + fatBadGameConfigIndexAdapter = (l=LOG_Fatal,m="`XVotingHandler`'s `currentGameConfig` variable value of %1 is out-of-bounds for `VHAdapter` of length %2. Report this issue.") +} \ No newline at end of file From 636e614ba484a31e7f333e732d4292308e9beb89 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 6 Nov 2021 02:34:35 +0700 Subject: [PATCH 41/59] Fix using old method name `ToPlainString()` --- sources/GameModes/BaseGameMode.uc | 8 ++++---- sources/GameModes/GameMode.uc | 10 +++++----- sources/VotingHandlerAdapter.uc | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc index b7141c0..266958d 100644 --- a/sources/GameModes/BaseGameMode.uc +++ b/sources/GameModes/BaseGameMode.uc @@ -106,7 +106,7 @@ protected function FromData(AssociativeArray source) } nextText = source.GetText(P("difficulty")); if (nextText != none) { - difficulty = nextText.ToPlainString(); + difficulty = nextText.ToString(); } includeFeature = DynamicIntoStringArray(source.GetDynamicArray(P("includeFeature"))); @@ -136,11 +136,11 @@ private final function FeatureConfigPair AssociativeArrayIntoPair( } nextText = source.GetText(P("feature")); if (nextText != none) { - result.feature = nextText.ToPlainString(); + result.feature = nextText.ToString(); } nextText = source.GetText(P("config")); if (nextText != none) { - result.config = nextText.ToPlainString(); + result.config = nextText.ToString(); } return result; } @@ -157,7 +157,7 @@ private final function array DynamicIntoStringArray(DynamicArray source) { nextText = source.GetText(i); if (nextText != none) { - includeFeature[i] = nextText.ToPlainString(); + includeFeature[i] = nextText.ToString(); } } } diff --git a/sources/GameModes/GameMode.uc b/sources/GameModes/GameMode.uc index 4c1960f..bafe5bc 100644 --- a/sources/GameModes/GameMode.uc +++ b/sources/GameModes/GameMode.uc @@ -91,15 +91,15 @@ protected function FromData(AssociativeArray source) } nextText = source.GetText(P("gameTypeClass")); if (nextText != none) { - gameTypeClass = nextText.ToPlainString(); + gameTypeClass = nextText.ToString(); } nextText = source.GetText(P("acronym")); if (nextText != none) { - acronym = nextText.ToPlainString(); + acronym = nextText.ToString(); } nextText = source.GetText(P("mapPrefix")); if (nextText != none) { - mapPrefix = nextText.ToPlainString(); + mapPrefix = nextText.ToString(); } nextArray = source.GetDynamicArray(P("option")); if (nextArray == none) { @@ -112,11 +112,11 @@ protected function FromData(AssociativeArray source) nextPair.value = ""; nextText = source.GetText(P("key")); if (nextText != none) { - nextPair.key = nextText.ToPlainString(); + nextPair.key = nextText.ToString(); } nextText = source.GetText(P("value")); if (nextText != none) { - nextPair.value = nextText.ToPlainString(); + nextPair.value = nextText.ToString(); } option[option.length] = nextPair; } diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc index 18d76ba..5b921aa 100644 --- a/sources/VotingHandlerAdapter.uc +++ b/sources/VotingHandlerAdapter.uc @@ -174,8 +174,8 @@ private function string BuildOptionsString(GameMode gameMode) options = gameMode.GetOptions(); for (iter = options.Iterate(); !iter.HasFinished(); iter.Next()) { - nextKey = Text(iter.GetKey()).ToPlainString(); - nextValue = Text(iter.Get()).ToPlainString(); + nextKey = Text(iter.GetKey()).ToString(); + nextValue = Text(iter.Get()).ToString(); if (optionWasAdded) { result $= "?"; } @@ -227,7 +227,7 @@ public final function PrepareForServerTravel() nextGameClass = class(_.memory.LoadClassS(nextGameClassName)); } default.isServerTraveling = true; - default.targetGameMode = availableGameModes[pickedVHConfig].ToPlainString(); + default.targetGameMode = availableGameModes[pickedVHConfig].ToString(); nextGameMode = GetConfigFromString(default.targetGameMode); default.storedGameDifficulty = nextGameClass.default.gameDifficulty; nextGameClass.default.gameDifficulty = GetNumericDifficulty(nextGameMode); From 195c671df4cfc84592a3bd4f5965251ceacedc2e Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 7 Nov 2021 16:04:29 +0700 Subject: [PATCH 42/59] Fix `Feature` validator using wrong log warning --- sources/GameModes/BaseGameMode.uc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc index 266958d..3ab61aa 100644 --- a/sources/GameModes/BaseGameMode.uc +++ b/sources/GameModes/BaseGameMode.uc @@ -279,7 +279,7 @@ private function ValidateFeatureArray( } if (!foundItem) { - _.logger.Auto(warnBadMutatorName) + _.logger.Auto(warnBadFeatureName) .Arg(_.text.FromString(includeMutator[i])) .Arg(_.text.FromString(string(name))) .Arg(_.text.FromString(arrayName)); From d3afb611d81aef1f616524ceddf8fa478e42b9a1 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 30 Nov 2021 01:18:05 +0700 Subject: [PATCH 43/59] Remove unused config entries --- config/Acedia.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/Acedia.ini b/config/Acedia.ini index 8e1b9d2..51be774 100644 --- a/config/Acedia.ini +++ b/config/Acedia.ini @@ -1,4 +1,2 @@ [Acedia.Packages] -useGameModes=false -corePackage="AcediaCore_0_2" -package="AcediaFixes" \ No newline at end of file +useGameModes=false \ No newline at end of file From 845930f8f1a7e51c5862ad37c245979342840547 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 30 Nov 2021 02:04:50 +0700 Subject: [PATCH 44/59] Add game modes default config --- config/AcediaGameModes.ini | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 config/AcediaGameModes.ini diff --git a/config/AcediaGameModes.ini b/config/AcediaGameModes.ini new file mode 100644 index 0000000..484b88a --- /dev/null +++ b/config/AcediaGameModes.ini @@ -0,0 +1,7 @@ +[hard GameMode] +title={$green Hard difficulty} +difficulty=normal + +[hell GameMode] +title={$crimson Hell On Earth} +difficulty=hoe From 0274c0c31f38ece6280c0a26945a9a2309ac66d1 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 12 Jan 2022 02:16:34 +0700 Subject: [PATCH 45/59] Add `OnModifyLogin()` signal support --- sources/Packages.uc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sources/Packages.uc b/sources/Packages.uc index 05a2cb6..91cd66a 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -41,6 +41,7 @@ var private config bool useGameModes; var VotingHandlerAdapter votingAdapter; var Mutator_OnMutate_Signal onMutateSignal; +var Mutator_OnModifyLogin_Signal onModifyLoginSignal; var Mutator_OnCheckReplacement_Signal onCheckReplacementSignal; var private LoggerAPI.Definition infoFeatureEnabled; @@ -154,6 +155,8 @@ private function SetupMutatorSignals() service = UnrealService(class'UnrealService'.static.Require()); onMutateSignal = Mutator_OnMutate_Signal( service.GetSignal(class'Mutator_OnMutate_Signal')); + onModifyLoginSignal = Mutator_OnModifyLogin_Signal( + service.GetSignal(class'Mutator_OnModifyLogin_Signal')); onCheckReplacementSignal = Mutator_OnCheckReplacement_Signal( service.GetSignal(class'Mutator_OnCheckReplacement_Signal')); } @@ -177,6 +180,14 @@ function Mutate(string command, PlayerController sendingController) super.Mutate(command, sendingController); } +function ModifyLogin(out string portal, out string options) +{ + if (onModifyLoginSignal != none) { + onModifyLoginSignal.Emit(portal, options); + } + super.ModifyLogin(portal, options); +} + defaultproperties { useGameModes = false From 57f11ad64472d30189692f66a2ccd195b697c68e Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 19 Jun 2022 00:23:42 +0700 Subject: [PATCH 46/59] Adapt to AcediaCore's `Text` changes --- sources/GameModes/BaseGameMode.uc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc index 3ab61aa..80c7027 100644 --- a/sources/GameModes/BaseGameMode.uc +++ b/sources/GameModes/BaseGameMode.uc @@ -94,8 +94,8 @@ protected function AssociativeArray ToData() protected function FromData(AssociativeArray source) { - local int i; - local Text nextText; + local int i; + local Text nextText; local DynamicArray includeFeatureAsSource; if (source == none) { return; From 824a6e270a4b45f57e04e973e058c184f88da9c8 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Thu, 23 Jun 2022 02:52:05 +0700 Subject: [PATCH 47/59] Change to account for AcediaCore's refactor --- sources/GameModes/BaseGameMode.uc | 6 +-- sources/Packages.uc | 77 +++++++++++++++++++++++++++---- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc index 80c7027..7f7b309 100644 --- a/sources/GameModes/BaseGameMode.uc +++ b/sources/GameModes/BaseGameMode.uc @@ -28,7 +28,7 @@ * along with Acedia. If not, see . */ class BaseGameMode extends AcediaConfig - dependson(CoreService) + dependson(Packages) abstract; // Name of the game mode players will see in voting (formatted string) @@ -205,7 +205,7 @@ public function Text GetDifficulty() * actually exist. */ public function ReportIncorrectSettings( - array featuresToEnable) + array featuresToEnable) { local int i; local array featureNames, featuresToReplace; @@ -298,7 +298,7 @@ private function ValidateFeatureArray( * Otherwise it should be enabled with a specified config. */ public function UpdateFeatureArray( - out array featuresToEnable) + out array featuresToEnable) { local int i; local Text newConfigName; diff --git a/sources/Packages.uc b/sources/Packages.uc index 91cd66a..efe5251 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -20,7 +20,6 @@ * along with Acedia. If not, see . */ class Packages extends Mutator - dependson(CoreService) config(Acedia); // Default value of this variable will be used to store @@ -45,6 +44,13 @@ var Mutator_OnModifyLogin_Signal onModifyLoginSignal; var Mutator_OnCheckReplacement_Signal onCheckReplacementSignal; var private LoggerAPI.Definition infoFeatureEnabled; +var private LoggerAPI.Definition errNoServerLevelCore, errorCannotRunTests; + +struct FeatureConfigPair +{ + var public class featureClass; + var public Text configName; +}; static public final function Packages GetInstance() { @@ -54,8 +60,10 @@ static public final function Packages GetInstance() // "Constructor" event PreBeginPlay() { - local GameMode currentGameMode; - local array availableFeatures; + local int i; + local LevelCore serverCore; + local GameMode currentGameMode; + local array availableFeatures; CheckForGarbage(); // Enforce one copy rule and remember a reference to that copy if (default.selfReference != none) @@ -65,12 +73,25 @@ event PreBeginPlay() } default.selfReference = self; // Launch and setup core Acedia - class'CoreService'.static.LaunchAcedia(self, package); + serverCore = class'ServerLevelCore'.static.CreateLevelCore(self); _ = class'Global'.static.GetInstance(); + for (i = 0; i < package.length; i += 1) { + _.environment.RegisterPackage_S(package[i]); + } + if (serverCore != none) { + _.ConnectServerLevelCore(); + } + else + { + _.logger.Auto(errNoServerLevelCore); + return; + } + if (class'TestingService'.default.runTestsOnStartUp) { + RunStartUpTests(); + } SetupMutatorSignals(); // Determine required features and launch them - availableFeatures = CoreService(class'CoreService'.static.GetInstance()) - .GetAutoConfigurationInfo(); + availableFeatures = GetAutoConfigurationInfo(); if (useGameModes) { votingAdapter = VotingHandlerAdapter( @@ -95,7 +116,8 @@ function ServerTraveling(string URL, bool bItems) votingAdapter = none; } default.selfReference = none; - CoreService(class'CoreService'.static.GetInstance()).ShutdownAcedia(); + _.environment.DisableAllFeatures(); + class'UnrealService'.static.Require().Destroy(); if (nextMutator != none) { nextMutator.ServerTraveling(URL, bItems); } @@ -134,7 +156,25 @@ private function CheckForGarbage() } } -private function EnableFeatures(array features) +public final function array GetAutoConfigurationInfo() +{ + local int i; + local array< class > availableFeatures; + local FeatureConfigPair nextPair; + local array result; + + availableFeatures = _.environment.GetAvailableFeatures(); + for (i = 0; i < availableFeatures.length; i += 1) + { + nextPair.featureClass = availableFeatures[i]; + nextPair.configName = availableFeatures[i].static + .GetAutoEnabledConfig(); + result[result.length] = nextPair; + } + return result; +} + +private function EnableFeatures(array features) { local int i; for (i = 0; i < features.length; i += 1) @@ -161,6 +201,23 @@ private function SetupMutatorSignals() service.GetSignal(class'Mutator_OnCheckReplacement_Signal')); } +private final function RunStartUpTests() +{ + local TestingService testService; + + testService = TestingService(class'TestingService'.static.Require()); + testService.PrepareTests(); + if (testService.filterTestsByName) { + testService.FilterByName(testService.requiredName); + } + if (testService.filterTestsByGroup) { + testService.FilterByGroup(testService.requiredGroup); + } + if (!testService.Run()) { + _.logger.Auto(errorCannotRunTests); + } +} + /** * Below `Mutator` events are redirected into appropriate signals. */ @@ -198,5 +255,7 @@ defaultproperties GroupName = "Package loader" FriendlyName = "Acedia loader" Description = "Launcher for Acedia packages" - infoFeatureEnabled = (l=LOG_Info,m="Feature `%1` enabled with config \"%2\".") + infoFeatureEnabled = (l=LOG_Info,m="Feature `%1` enabled with config \"%2\".") + errNoServerLevelCore = (l=LOG_Error,m="Cannot create `ServerLevelCore`!") + errorCannotRunTests = (l=LOG_Error,m="Could not perform Acedia's tests.") } \ No newline at end of file From 1e9e146a9fc9dc4faecf397e791901e9567ed253 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 2 Jul 2022 05:27:56 +0700 Subject: [PATCH 48/59] Change to supprot new AcediaCore's events --- sources/Packages.uc | 1 + 1 file changed, 1 insertion(+) diff --git a/sources/Packages.uc b/sources/Packages.uc index efe5251..2bd5d4a 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -118,6 +118,7 @@ function ServerTraveling(string URL, bool bItems) default.selfReference = none; _.environment.DisableAllFeatures(); class'UnrealService'.static.Require().Destroy(); + class'ServerLevelCore'.static.GetInstance().Destroy(); if (nextMutator != none) { nextMutator.ServerTraveling(URL, bItems); } From a026b731ee6d2c12f6e396309e79de9d9ef12f0e Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 5 Jul 2022 01:47:52 +0700 Subject: [PATCH 49/59] Change to adapt to AcediaCore's changes --- sources/GameModes/BaseGameMode.uc | 90 ++++++++++++++++--------------- sources/GameModes/GameMode.uc | 68 ++++++++++------------- 2 files changed, 75 insertions(+), 83 deletions(-) diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc index 7f7b309..04b29f6 100644 --- a/sources/GameModes/BaseGameMode.uc +++ b/sources/GameModes/BaseGameMode.uc @@ -54,80 +54,82 @@ var protected config array includeFeatureAs; var private LoggerAPI.Definition warnBadMutatorName, warnBadFeatureName; -protected function AssociativeArray ToData() +protected function HashTable ToData() { - local int i; - local AssociativeArray result; - local AssociativeArray nextPair; - local DynamicArray nextArray; - result = _.collections.EmptyAssociativeArray(); - result.SetItem(P("title"), _.text.FromFormattedString(title)); - result.SetItem(P("difficulty"), _.text.FromString(difficulty)); - nextArray = _.collections.EmptyDynamicArray(); + local int i; + local HashTable result; + local HashTable nextPair; + local ArrayList nextArray; + + result = _.collections.EmptyHashTable(); + result.SetFormattedString(P("title"), title); + result.SetString(P("difficulty"), difficulty); + nextArray = _.collections.EmptyArrayList(); for (i = 0; i < includeFeature.length; i += 1) { - nextArray.AddItem(_.text.FromString(includeFeature[i])); + nextArray.AddString(includeFeature[i]); } result.SetItem(P("includeFeature"), nextArray); - nextArray = _.collections.EmptyDynamicArray(); + _.memory.Free(nextArray); + nextArray = _.collections.EmptyArrayList(); for (i = 0; i < excludeFeature.length; i += 1) { nextArray.AddItem(_.text.FromString(excludeFeature[i])); } result.SetItem(P("excludeFeature"), nextArray); - nextArray = _.collections.EmptyDynamicArray(); + _.memory.Free(nextArray); + nextArray = _.collections.EmptyArrayList(); for (i = 0; i < includeMutator.length; i += 1) { nextArray.AddItem(_.text.FromString(includeFeature[i])); } result.SetItem(P("includeMutator"), nextArray); - nextArray = _.collections.EmptyDynamicArray(); + _.memory.Free(nextArray); + nextArray = _.collections.EmptyArrayList(); for (i = 0; i < includeFeatureAs.length; i += 1) { - nextPair = _.collections.EmptyAssociativeArray(); - nextPair.SetItem(P("feature"), - _.text.FromString(includeFeatureAs[i].feature)); - nextPair.SetItem(P("config"), - _.text.FromString(includeFeatureAs[i].config)); + nextPair = _.collections.EmptyHashTable(); + nextPair.SetString(P("feature"), includeFeatureAs[i].feature); + nextPair.SetString(P("config"), includeFeatureAs[i].config); nextArray.AddItem(nextPair); + _.memory.Free(nextPair); } result.SetItem(P("includeFeatureAs"), nextArray); + _.memory.Free(nextArray); return result; } -protected function FromData(AssociativeArray source) +protected function FromData(HashTable source) { - local int i; - local Text nextText; - local DynamicArray includeFeatureAsSource; + local int i; + local ArrayList nextArray; + local HashTable nextPair; if (source == none) { return; } - nextText = source.GetText(P("title")); - if (nextText != none) { - title = nextText.ToFormattedString(); - } - nextText = source.GetText(P("difficulty")); - if (nextText != none) { - difficulty = nextText.ToString(); - } - includeFeature = - DynamicIntoStringArray(source.GetDynamicArray(P("includeFeature"))); - excludeFeature = - DynamicIntoStringArray(source.GetDynamicArray(P("excludeFeature"))); - includeMutator = - DynamicIntoStringArray(source.GetDynamicArray(P("includeMutator"))); - includeFeatureAsSource = source.GetDynamicArray(P("includeFeatureAs")); - if (includeFeatureAsSource == none) { + title = source.GetFormattedString(P("title")); + title = source.GetString(P("title")); + nextArray = source.GetArrayList(P("includeFeature")); + includeFeature = DynamicIntoStringArray(nextArray); + _.memory.Free(nextArray); + nextArray = source.GetArrayList(P("excludeFeature")); + excludeFeature = DynamicIntoStringArray(nextArray); + _.memory.Free(nextArray); + nextArray = source.GetArrayList(P("includeMutator")); + includeMutator = DynamicIntoStringArray(nextArray); + _.memory.Free(nextArray); + nextArray = source.GetArrayList(P("includeFeatureAs")); + if (nextArray == none) { return; } includeFeatureAs.length = 0; - for (i = 0; i < includeFeatureAsSource.GetLength(); i += 1) + for (i = 0; i < nextArray.GetLength(); i += 1) { - includeFeatureAs[i] = AssociativeArrayIntoPair( - includeFeatureAsSource.GetAssociativeArray(i)); + nextPair = nextArray.GetHashTable(i); + includeFeatureAs[i] = HashTableIntoPair(nextPair); + _.memory.Free(nextPair); } + _.memory.Free(nextArray); } -private final function FeatureConfigPair AssociativeArrayIntoPair( - AssociativeArray source) +private final function FeatureConfigPair HashTableIntoPair(HashTable source) { local Text nextText; local FeatureConfigPair result; @@ -145,7 +147,7 @@ private final function FeatureConfigPair AssociativeArrayIntoPair( return result; } -private final function array DynamicIntoStringArray(DynamicArray source) +private final function array DynamicIntoStringArray(ArrayList source) { local int i; local Text nextText; diff --git a/sources/GameModes/GameMode.uc b/sources/GameModes/GameMode.uc index bafe5bc..6e37490 100644 --- a/sources/GameModes/GameMode.uc +++ b/sources/GameModes/GameMode.uc @@ -54,72 +54,62 @@ protected function DefaultIt() option.length = 0; } -protected function AssociativeArray ToData() +protected function HashTable ToData() { - local int i; - local AssociativeArray result; - local AssociativeArray nextPair; - local DynamicArray nextArray; + local int i; + local ArrayList nextArray; + local HashTable result, nextPair; result = super.ToData(); if (result == none) { return none; } - result.SetItem(P("gameTypeClass"), _.text.FromString(gameTypeClass)); - result.SetItem(P("acronym"), _.text.FromString(acronym)); - result.SetItem(P("mapPrefix"), _.text.FromString(mapPrefix)); - nextArray = _.collections.EmptyDynamicArray(); + result.SetString(P("gameTypeClass"), gameTypeClass); + result.SetString(P("acronym"), acronym); + result.SetString(P("mapPrefix"), mapPrefix); + nextArray = _.collections.EmptyArrayList(); for (i = 0; i < option.length; i += 1) { - nextPair = _.collections.EmptyAssociativeArray(); - nextPair.SetItem(P("key"), _.text.FromString(option[i].key)); - nextPair.SetItem(P("value"), _.text.FromString(option[i].value)); + nextPair = _.collections.EmptyHashTable(); + nextPair.SetString(P("key"), option[i].key); + nextPair.SetString(P("value"), option[i].value); nextArray.AddItem(nextPair); + _.memory.Free(nextPair); } result.SetItem(P("option"), nextArray); + _.memory.Free(nextArray); return result; } -protected function FromData(AssociativeArray source) +protected function FromData(HashTable source) { local int i; - local Text nextText; - local GameOption nextPair; - local DynamicArray nextArray; + local GameOption nextGameOption; + local ArrayList nextArray; + local HashTable nextPair; super.FromData(source); if (source == none) { return; } - nextText = source.GetText(P("gameTypeClass")); - if (nextText != none) { - gameTypeClass = nextText.ToString(); - } - nextText = source.GetText(P("acronym")); - if (nextText != none) { - acronym = nextText.ToString(); - } - nextText = source.GetText(P("mapPrefix")); - if (nextText != none) { - mapPrefix = nextText.ToString(); - } - nextArray = source.GetDynamicArray(P("option")); + gameTypeClass = source.GetString(P("gameTypeClass")); + acronym = source.GetString(P("acronym")); + mapPrefix = source.GetString(P("mapPrefix")); + nextArray = source.GetArrayList(P("option")); if (nextArray == none) { return; } option.length = 0; for (i = 0; i < nextArray.GetLength(); i += 1) { - nextPair.key = ""; - nextPair.value = ""; - nextText = source.GetText(P("key")); - if (nextText != none) { - nextPair.key = nextText.ToString(); + nextPair = HashTable(nextArray.GetItem(i)); + if (nextPair == none) { + continue; } - nextText = source.GetText(P("value")); - if (nextText != none) { - nextPair.value = nextText.ToString(); - } - option[option.length] = nextPair; + nextGameOption.key = nextPair.GetString(P("key")); + nextGameOption.value = nextPair.GetString(P("value")); + option[option.length] = nextGameOption; + _.memory.Free(nextPair); } + _.memory.Free(nextArray); } public function Text GetGameTypeClass() From 7721b520f55985ed8d008f77a65a455466736d2d Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Fri, 8 Jul 2022 04:16:27 +0700 Subject: [PATCH 50/59] Change code to adapt to iterator's refactor --- sources/VotingHandlerAdapter.uc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc index 5b921aa..7e02f9c 100644 --- a/sources/VotingHandlerAdapter.uc +++ b/sources/VotingHandlerAdapter.uc @@ -166,11 +166,11 @@ private function string BuildMutatorString(GameMode gameMode) private function string BuildOptionsString(GameMode gameMode) { - local bool optionWasAdded; - local string result; - local string nextKey, nextValue; - local Iter iter; - local AssociativeArray options; + local bool optionWasAdded; + local string result; + local string nextKey, nextValue; + local CollectionIterator iter; + local AssociativeArray options; options = gameMode.GetOptions(); for (iter = options.Iterate(); !iter.HasFinished(); iter.Next()) { From 0bb7f8904036369d26e590f104a1f59bcd26f0b7 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 12 Jul 2022 04:49:37 +0700 Subject: [PATCH 51/59] Change to support new AcediaCore changes --- sources/Packages.uc | 45 ++++++++++++++++++++++++++++----- sources/VotingHandlerAdapter.uc | 6 +++-- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index 2bd5d4a..cfe9ce4 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -2,7 +2,7 @@ * Main and only Acedia mutator used for loading Acedia packages * and providing access to mutator events' calls. * Name is chosen to make config files more readable. - * Copyright 2020-2021 Anton Tarasenko + * Copyright 2020-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -30,8 +30,12 @@ class Packages extends Mutator var private Packages selfReference; // Acedia's reference to a `Global` object. -var private Global _; +var private Global _; +var private ServerGlobal _server; +var private ClientGlobal _client; +// Load Acedia on the client as well? +var private config bool clientside; // Array of predefined services that must be started along with Acedia mutator. var private config array package; // Set to `true` to activate Acedia's game modes system @@ -58,12 +62,38 @@ static public final function Packages GetInstance() } // "Constructor" -event PreBeginPlay() +simulated function PreBeginPlay() +{ + if (level.netMode == NM_DedicatedServer) { + InitializeServer(); + } + else { + InitializeClient(); + } +} + +private simulated function InitializeClient() +{ + // Enforce one copy rule and remember a reference to that copy + if (default.selfReference != none) + { + Destroy(); + return; + } + default.selfReference = self; + class'ClientLevelCore'.static.CreateLevelCore(self); + _ = class'Global'.static.GetInstance(); +} + +private function InitializeServer() { local int i; local LevelCore serverCore; local GameMode currentGameMode; local array availableFeatures; + if (clientside) { + AddToPackageMap("Acedia"); + } CheckForGarbage(); // Enforce one copy rule and remember a reference to that copy if (default.selfReference != none) @@ -74,12 +104,14 @@ event PreBeginPlay() default.selfReference = self; // Launch and setup core Acedia serverCore = class'ServerLevelCore'.static.CreateLevelCore(self); - _ = class'Global'.static.GetInstance(); + _ = class'Global'.static.GetInstance(); + _server = class'ServerGlobal'.static.GetInstance(); + _client = class'ClientGlobal'.static.GetInstance(); for (i = 0; i < package.length; i += 1) { _.environment.RegisterPackage_S(package[i]); } if (serverCore != none) { - _.ConnectServerLevelCore(); + _server.ConnectServerLevelCore(); } else { @@ -248,9 +280,10 @@ function ModifyLogin(out string portal, out string options) defaultproperties { + clientside = false useGameModes = false // This is a server-only mutator - remoteRole = ROLE_None + remoteRole = ROLE_SimulatedProxy bAlwaysRelevant = true // Mutator description GroupName = "Package loader" diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc index 7e02f9c..89c89ee 100644 --- a/sources/VotingHandlerAdapter.uc +++ b/sources/VotingHandlerAdapter.uc @@ -223,8 +223,10 @@ public final function PrepareForServerTravel() if (string(_.unreal.GetGameType().class) ~= nextGameClassName) { nextGameClass = _.unreal.GetGameType().class; } - else { - nextGameClass = class(_.memory.LoadClassS(nextGameClassName)); + else + { + nextGameClass = + class(_.memory.LoadClass_S(nextGameClassName)); } default.isServerTraveling = true; default.targetGameMode = availableGameModes[pickedVHConfig].ToString(); From d3d6ae46277a691146007ef75d75e018bd47508c Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Fri, 15 Jul 2022 04:35:29 +0700 Subject: [PATCH 52/59] Change to use `UnrealAPI` from `_server` --- sources/VotingHandlerAdapter.uc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc index 89c89ee..04be56f 100644 --- a/sources/VotingHandlerAdapter.uc +++ b/sources/VotingHandlerAdapter.uc @@ -112,14 +112,14 @@ public final function InjectIntoVotingHandler() if (votingHandlerReference != none) { return; } - votingHandler = XVotingHandler(_.unreal.FindActorInstance( - _.unreal.GetGameType().VotingHandlerClass)); + votingHandler = XVotingHandler(_server.unreal.FindActorInstance( + _server.unreal.GetGameType().VotingHandlerClass)); if (votingHandler == none) { _.logger.Auto(fatNoXVotingHandler); return; } - votingHandlerReference = _.unreal.ActorRef(votingHandler); + votingHandlerReference = _server.unreal.ActorRef(votingHandler); class'GameMode'.static.Initialize(); availableGameModes = class'GameMode'.static.AvailableConfigs(); for (i = 0; i < availableGameModes.length; i += 1) @@ -220,8 +220,8 @@ public final function PrepareForServerTravel() return; } nextGameClassName = votingHandler.gameConfig[pickedVHConfig].gameClass; - if (string(_.unreal.GetGameType().class) ~= nextGameClassName) { - nextGameClass = _.unreal.GetGameType().class; + if (string(_server.unreal.GetGameType().class) ~= nextGameClassName) { + nextGameClass = _server.unreal.GetGameType().class; } else { @@ -247,7 +247,8 @@ public final function GameMode SetupGameModeAfterTravel() if (!default.isServerTraveling) { return none; } - _.unreal.GetGameType().default.gameDifficulty = default.storedGameDifficulty; + _server.unreal.GetGameType().default.gameDifficulty = + default.storedGameDifficulty; default.isServerTraveling = false; return GetConfigFromString(targetGameMode); } From 83f137c0635306c6b6f09d987d17d4b5bcc9e9e6 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Fri, 15 Jul 2022 20:37:04 +0700 Subject: [PATCH 53/59] Change to new AcediaCore collections --- sources/GameModes/GameMode.uc | 10 +++++----- sources/VotingHandlerAdapter.uc | 7 +++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sources/GameModes/GameMode.uc b/sources/GameModes/GameMode.uc index 6e37490..7c869be 100644 --- a/sources/GameModes/GameMode.uc +++ b/sources/GameModes/GameMode.uc @@ -167,13 +167,13 @@ public function ReportBadOptions() } /** - * @return Server options as key-value pairs in an `AssociativeArray`. + * @return Server options as key-value pairs in an `HashTable`. */ -public function AssociativeArray GetOptions() +public function HashTable GetOptions() { - local int i; - local AssociativeArray result; - result = _.collections.EmptyAssociativeArray(); + local int i; + local HashTable result; + result = _.collections.EmptyHashTable(); for (i = 0; i < option.length; i += 1) { if (!ValidateServerURLName(option[i].key)) continue; diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc index 04be56f..bb73010 100644 --- a/sources/VotingHandlerAdapter.uc +++ b/sources/VotingHandlerAdapter.uc @@ -170,19 +170,18 @@ private function string BuildOptionsString(GameMode gameMode) local string result; local string nextKey, nextValue; local CollectionIterator iter; - local AssociativeArray options; + local HashTable options; options = gameMode.GetOptions(); for (iter = options.Iterate(); !iter.HasFinished(); iter.Next()) { - nextKey = Text(iter.GetKey()).ToString(); - nextValue = Text(iter.Get()).ToString(); + nextKey = _.text.ToString(Text(iter.GetKey())); + nextValue = _.text.ToString(Text(iter.Get())); if (optionWasAdded) { result $= "?"; } result $= (nextKey $ "=" $ nextValue); optionWasAdded = true; } - options.Empty(true); options.FreeSelf(); iter.FreeSelf(); return result; From 51beae9849cf01ece356839722866790717906be Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 17 Jul 2022 02:30:35 +0700 Subject: [PATCH 54/59] Change to support Acedia's new shutdown proces --- sources/Packages.uc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index cfe9ce4..9f984df 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -81,6 +81,7 @@ private simulated function InitializeClient() return; } default.selfReference = self; + // TODO: Swap these around after dealing with aliases class'ClientLevelCore'.static.CreateLevelCore(self); _ = class'Global'.static.GetInstance(); } @@ -103,6 +104,7 @@ private function InitializeServer() } default.selfReference = self; // Launch and setup core Acedia + // TODO: Swap these around after dealing with aliases serverCore = class'ServerLevelCore'.static.CreateLevelCore(self); _ = class'Global'.static.GetInstance(); _server = class'ServerGlobal'.static.GetInstance(); @@ -148,9 +150,7 @@ function ServerTraveling(string URL, bool bItems) votingAdapter = none; } default.selfReference = none; - _.environment.DisableAllFeatures(); - class'UnrealService'.static.Require().Destroy(); - class'ServerLevelCore'.static.GetInstance().Destroy(); + _.environment.ShutDown(); if (nextMutator != none) { nextMutator.ServerTraveling(URL, bItems); } From 7f7221e1b75a3f4d3dc14fbca7461ae1d10a19dc Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 18 Jul 2022 02:03:08 +0700 Subject: [PATCH 55/59] Change to use `ServerUnrealAPI` instead --- sources/Packages.uc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index 9f984df..e87d703 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -224,8 +224,8 @@ private function EnableFeatures(array features) // Fetches and sets up signals that `Mutator` needs to provide private function SetupMutatorSignals() { - local UnrealService service; - service = UnrealService(class'UnrealService'.static.Require()); + local ServerUnrealService service; + service = ServerUnrealService(class'ServerUnrealService'.static.Require()); onMutateSignal = Mutator_OnMutate_Signal( service.GetSignal(class'Mutator_OnMutate_Signal')); onModifyLoginSignal = Mutator_OnModifyLogin_Signal( From e1d61ed7e897984091e9e7c9638300beec843362 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 25 Jul 2022 02:52:03 +0700 Subject: [PATCH 56/59] Change to use new `TextAPI.IntoString()` name --- sources/VotingHandlerAdapter.uc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc index bb73010..c9c3363 100644 --- a/sources/VotingHandlerAdapter.uc +++ b/sources/VotingHandlerAdapter.uc @@ -139,10 +139,10 @@ private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig( GameMode gameMode) { local VotingHandler.MapVoteGameConfig result; - result.gameClass = _.text.ToString(gameMode.GetGameTypeClass()); + result.gameClass = _.text.IntoString(gameMode.GetGameTypeClass()); result.gameName = _.text.ToColoredString(gameMode.GetTitle()); - result.prefix = _.text.ToString(gameMode.GetMapPrefix()); - result.acronym = _.text.ToString(gameMode.GetAcronym()); + result.prefix = _.text.IntoString(gameMode.GetMapPrefix()); + result.acronym = _.text.IntoString(gameMode.GetAcronym()); result.mutators = BuildMutatorString(gameMode); result.options = BuildOptionsString(gameMode); return result; @@ -159,7 +159,7 @@ private function string BuildMutatorString(GameMode gameMode) if (i > 0) { result $= ","; } - result $= _.text.ToString(usedMutators[i]); + result $= _.text.IntoString(usedMutators[i]); } return result; } @@ -174,8 +174,8 @@ private function string BuildOptionsString(GameMode gameMode) options = gameMode.GetOptions(); for (iter = options.Iterate(); !iter.HasFinished(); iter.Next()) { - nextKey = _.text.ToString(Text(iter.GetKey())); - nextValue = _.text.ToString(Text(iter.Get())); + nextKey = _.text.IntoString(Text(iter.GetKey())); + nextValue = _.text.IntoString(Text(iter.Get())); if (optionWasAdded) { result $= "?"; } @@ -285,7 +285,7 @@ private final function int GetNumericDifficulty(GameMode gameMode) { local int i; local string difficulty; - difficulty = Locs(_.text.ToString(gameMode.GetDifficulty())); + difficulty = Locs(_.text.IntoString(gameMode.GetDifficulty())); for (i = 0; i < default.beginnerSynonyms.length; i += 1) { if (IsPrefixOf(difficulty, default.beginnerSynonyms[i])) { From 925f9a100d41f1b3eda1963173f276a1cde401e4 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 3 Aug 2022 10:29:54 +0700 Subject: [PATCH 57/59] Change `LevelCore`s to be loaded after base API --- sources/Packages.uc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index e87d703..90a8052 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -81,9 +81,8 @@ private simulated function InitializeClient() return; } default.selfReference = self; - // TODO: Swap these around after dealing with aliases - class'ClientLevelCore'.static.CreateLevelCore(self); _ = class'Global'.static.GetInstance(); + class'ClientLevelCore'.static.CreateLevelCore(self); } private function InitializeServer() @@ -104,11 +103,10 @@ private function InitializeServer() } default.selfReference = self; // Launch and setup core Acedia - // TODO: Swap these around after dealing with aliases - serverCore = class'ServerLevelCore'.static.CreateLevelCore(self); _ = class'Global'.static.GetInstance(); _server = class'ServerGlobal'.static.GetInstance(); _client = class'ClientGlobal'.static.GetInstance(); + serverCore = class'ServerLevelCore'.static.CreateLevelCore(self); for (i = 0; i < package.length; i += 1) { _.environment.RegisterPackage_S(package[i]); } From 6a13c509a20a78f62599f8696e94851e2e3478d3 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 8 Aug 2022 04:54:52 +0700 Subject: [PATCH 58/59] Remove `selfReference` from `Packages` mutator This variable didn't serve any really useful purpose, but has led to game crashes. While adding proper cleanup could also solve these crashes, there is no real point to keeping it at all. NOTE: It's only purpose was to make sure only one instance of corresponding mutator exists, but duplicates shouldn't happen in the first place. --- sources/Packages.uc | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/sources/Packages.uc b/sources/Packages.uc index 90a8052..58c11eb 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -22,13 +22,6 @@ class Packages extends Mutator config(Acedia); -// Default value of this variable will be used to store -// reference to the active Acedia mutator, -// as well as to ensure there's only one copy of it. -// We can't use 'Singleton' class for that, -// as we have to derive from 'Mutator'. -var private Packages selfReference; - // Acedia's reference to a `Global` object. var private Global _; var private ServerGlobal _server; @@ -56,11 +49,6 @@ struct FeatureConfigPair var public Text configName; }; -static public final function Packages GetInstance() -{ - return default.selfReference; -} - // "Constructor" simulated function PreBeginPlay() { @@ -74,13 +62,6 @@ simulated function PreBeginPlay() private simulated function InitializeClient() { - // Enforce one copy rule and remember a reference to that copy - if (default.selfReference != none) - { - Destroy(); - return; - } - default.selfReference = self; _ = class'Global'.static.GetInstance(); class'ClientLevelCore'.static.CreateLevelCore(self); } @@ -95,13 +76,6 @@ private function InitializeServer() AddToPackageMap("Acedia"); } CheckForGarbage(); - // Enforce one copy rule and remember a reference to that copy - if (default.selfReference != none) - { - Destroy(); - return; - } - default.selfReference = self; // Launch and setup core Acedia _ = class'Global'.static.GetInstance(); _server = class'ServerGlobal'.static.GetInstance(); @@ -147,7 +121,6 @@ function ServerTraveling(string URL, bool bItems) _.memory.Free(votingAdapter); votingAdapter = none; } - default.selfReference = none; _.environment.ShutDown(); if (nextMutator != none) { nextMutator.ServerTraveling(URL, bItems); From 8583bef23d2513cd7ff02acb67e81ddd72e72186 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 8 Aug 2022 13:20:02 +0700 Subject: [PATCH 59/59] Fix formatting --- sources/GameModes/BaseGameMode.uc | 26 +++++++++++++++++++------- sources/GameModes/GameMode.uc | 6 +++++- sources/Packages.uc | 8 +++++++- sources/StartUp.uc | 2 +- sources/VotingHandlerAdapter.uc | 13 ++++++++++--- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc index 04b29f6..cb7d8fe 100644 --- a/sources/GameModes/BaseGameMode.uc +++ b/sources/GameModes/BaseGameMode.uc @@ -10,7 +10,7 @@ * be used based on game info's settings; * 3. `Report...()` methods that perform various validation checks * (and log them) on config data. - * Copyright 2021 Anton Tarasenko + * Copyright 2021-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -101,6 +101,7 @@ protected function FromData(HashTable source) local int i; local ArrayList nextArray; local HashTable nextPair; + if (source == none) { return; } @@ -133,6 +134,7 @@ private final function FeatureConfigPair HashTableIntoPair(HashTable source) { local Text nextText; local FeatureConfigPair result; + if (source == none) { return result; } @@ -152,6 +154,7 @@ private final function array DynamicIntoStringArray(ArrayList source) local int i; local Text nextText; local array result; + if (source == none) { return result; } @@ -168,6 +171,7 @@ protected function array StringToTextArray(array input) { local int i; local array result; + for (i = 0; i < input.length; i += 1) { result[i] = _.text.FromString(input[i]); } @@ -209,8 +213,9 @@ public function Text GetDifficulty() public function ReportIncorrectSettings( array featuresToEnable) { - local int i; + local int i; local array featureNames, featuresToReplace; + for (i = 0; i < featuresToEnable.length; i += 1) { featureNames[i] = string(featuresToEnable[i].featureClass); } @@ -233,6 +238,7 @@ public function ReportIncorrectSettings( public function ReportBadMutatorNames() { local int i; + for (i = 0; i < includeMutator.length; i += 1) { if (!ValidateServerURLName(includeMutator[i])) @@ -266,8 +272,9 @@ private function ValidateFeatureArray( array whole, string arrayName) { - local int i, j; - local bool foundItem; + local int i, j; + local bool foundItem; + for (i = 0; i < subset.length; i += 1) { foundItem = false; @@ -302,9 +309,10 @@ private function ValidateFeatureArray( public function UpdateFeatureArray( out array featuresToEnable) { - local int i; - local Text newConfigName; - local string nextFeatureClassName; + local int i; + local Text newConfigName; + local string nextFeatureClassName; + for (i = 0; i < featuresToEnable.length; i += 1) { nextFeatureClassName = string(featuresToEnable[i].featureClass); @@ -334,6 +342,7 @@ public function UpdateFeatureArray( private function bool FeatureExcluded(string featureClassName) { local int i; + for (i = 0; i < excludeFeature.length; i += 1) { if (excludeFeature[i] ~= featureClassName) { @@ -346,6 +355,7 @@ private function bool FeatureExcluded(string featureClassName) private function Text TryReplacingFeatureConfig(string featureClassName) { local int i; + for (i = 0; i < includeFeatureAs.length; i += 1) { if (includeFeatureAs[i].feature ~= featureClassName) { @@ -358,6 +368,7 @@ private function Text TryReplacingFeatureConfig(string featureClassName) private function bool FeatureInIncludedArray(string featureClassName) { local int i; + for (i = 0; i < includeFeature.length; i += 1) { if (includeFeature[i] ~= featureClassName) { @@ -371,6 +382,7 @@ public function array GetIncludedMutators() { local int i; local array validatedMutators; + for (i = 0; i < includeMutator.length; i += 1) { if (ValidateServerURLName(includeMutator[i])) { diff --git a/sources/GameModes/GameMode.uc b/sources/GameModes/GameMode.uc index 7c869be..e314ec3 100644 --- a/sources/GameModes/GameMode.uc +++ b/sources/GameModes/GameMode.uc @@ -1,7 +1,7 @@ /** * The only implementation for `BaseGameMode` suitable for standard * killing floor game types. - * Copyright 2021 Anton Tarasenko + * Copyright 2021-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -59,6 +59,7 @@ protected function HashTable ToData() local int i; local ArrayList nextArray; local HashTable result, nextPair; + result = super.ToData(); if (result == none) { return none; @@ -86,6 +87,7 @@ protected function FromData(HashTable source) local GameOption nextGameOption; local ArrayList nextArray; local HashTable nextPair; + super.FromData(source); if (source == none) { return; @@ -153,6 +155,7 @@ public function Text GetMapPrefix() public function ReportBadOptions() { local int i; + for (i = 0; i < option.length; i += 1) { if ( !ValidateServerURLName(option[i].key) @@ -173,6 +176,7 @@ public function HashTable GetOptions() { local int i; local HashTable result; + result = _.collections.EmptyHashTable(); for (i = 0; i < option.length; i += 1) { diff --git a/sources/Packages.uc b/sources/Packages.uc index 58c11eb..d35e0e5 100644 --- a/sources/Packages.uc +++ b/sources/Packages.uc @@ -72,6 +72,7 @@ private function InitializeServer() local LevelCore serverCore; local GameMode currentGameMode; local array availableFeatures; + if (clientside) { AddToPackageMap("Acedia"); } @@ -132,10 +133,13 @@ function ServerTraveling(string URL, bool bItems) // This can lead to serious problems, so such diagnostic check is warranted. private function CheckForGarbage() { - local int leftoverObjectAmount, leftoverActorAmount, leftoverDBRAmount; + local int leftoverObjectAmount; + local int leftoverActorAmount; + local int leftoverDBRAmount; local AcediaObject nextObject; local AcediaActor nextActor; local DBRecord nextRecord; + foreach AllObjects(class'AcediaObject', nextObject) { leftoverObjectAmount += 1; } @@ -181,6 +185,7 @@ public final function array GetAutoConfigurationInfo() private function EnableFeatures(array features) { local int i; + for (i = 0; i < features.length; i += 1) { if (features[i].featureClass == none) continue; @@ -196,6 +201,7 @@ private function EnableFeatures(array features) private function SetupMutatorSignals() { local ServerUnrealService service; + service = ServerUnrealService(class'ServerUnrealService'.static.Require()); onMutateSignal = Mutator_OnMutate_Signal( service.GetSignal(class'Mutator_OnMutate_Signal')); diff --git a/sources/StartUp.uc b/sources/StartUp.uc index ac40796..0755d39 100644 --- a/sources/StartUp.uc +++ b/sources/StartUp.uc @@ -1,6 +1,6 @@ /** * This actor's role is to add Acedia mutator on listen and dedicated servers. - * Copyright 2019 Anton Tarasenko + * Copyright 2019-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc index c9c3363..69b72ec 100644 --- a/sources/VotingHandlerAdapter.uc +++ b/sources/VotingHandlerAdapter.uc @@ -5,7 +5,7 @@ * data from Acedia's game modes. * Requires `GameInfo`'s voting handler to be derived from * `XVotingHandler`, which is satisfied by pretty much every used handler. - * Copyright 2021 Anton Tarasenko + * Copyright 2021-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -109,6 +109,7 @@ public final function InjectIntoVotingHandler() local GameMode nextGameMode; local XVotingHandler votingHandler; local array newVotingHandlerConfig; + if (votingHandlerReference != none) { return; } @@ -139,6 +140,7 @@ private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig( GameMode gameMode) { local VotingHandler.MapVoteGameConfig result; + result.gameClass = _.text.IntoString(gameMode.GetGameTypeClass()); result.gameName = _.text.ToColoredString(gameMode.GetTitle()); result.prefix = _.text.IntoString(gameMode.GetMapPrefix()); @@ -153,6 +155,7 @@ private function string BuildMutatorString(GameMode gameMode) local int i; local string result; local array usedMutators; + usedMutators = gameMode.GetIncludedMutators(); for (i = 0; i < usedMutators.length; i += 1) { @@ -171,6 +174,7 @@ private function string BuildOptionsString(GameMode gameMode) local string nextKey, nextValue; local CollectionIterator iter; local HashTable options; + options = gameMode.GetOptions(); for (iter = options.Iterate(); !iter.HasFinished(); iter.Next()) { @@ -197,6 +201,7 @@ public final function PrepareForServerTravel() local string nextGameClassName; local class nextGameClass; local XVotingHandler votingHandler; + if (votingHandlerReference == none) return; votingHandler = XVotingHandler(votingHandlerReference.Get()); if (votingHandler == none) return; @@ -259,6 +264,7 @@ public final function GameMode SetupGameModeAfterTravel() public final function RestoreVotingHandlerConfigBackup() { local XVotingHandler votingHandler; + if (votingHandlerReference == none) return; votingHandler = XVotingHandler(votingHandlerReference.Get()); if (votingHandler == none) return; @@ -283,8 +289,9 @@ private function GameMode GetConfigFromString(string configName) // KF's numeric one. private final function int GetNumericDifficulty(GameMode gameMode) { - local int i; - local string difficulty; + local int i; + local string difficulty; + difficulty = Locs(_.text.IntoString(gameMode.GetDifficulty())); for (i = 0; i < default.beginnerSynonyms.length; i += 1) {