mirror of
https://github.com/claunia/SabreTools.git
synced 2025-12-16 19:14:27 +00:00
Create Filtering object, add helpers
This commit is contained in:
256
SabreTools.Filter/FilterObject.cs
Normal file
256
SabreTools.Filter/FilterObject.cs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
using System;
|
||||||
|
using SabreTools.Models.Internal;
|
||||||
|
|
||||||
|
namespace SabreTools.Filter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single filtering object
|
||||||
|
/// </summary>
|
||||||
|
public class FilterObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Key name for the filter
|
||||||
|
/// </summary>
|
||||||
|
public string Key { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value to match in the filter
|
||||||
|
/// </summary>
|
||||||
|
public string? Value { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Operation on how to match the filter
|
||||||
|
/// </summary>
|
||||||
|
public Operation Operation { get; init; }
|
||||||
|
|
||||||
|
public FilterObject(string keyValue, string? operation)
|
||||||
|
{
|
||||||
|
(string? itemName, string? fieldName) = FilterParser.ParseFilterId(keyValue);
|
||||||
|
if (itemName == null)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(keyValue));
|
||||||
|
|
||||||
|
this.Key = itemName;
|
||||||
|
this.Value = fieldName;
|
||||||
|
this.Operation = GetOperation(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FilterObject(string keyValue, Operation operation)
|
||||||
|
{
|
||||||
|
(string? itemName, string? fieldName) = FilterParser.ParseFilterId(keyValue);
|
||||||
|
if (itemName == null)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(keyValue));
|
||||||
|
|
||||||
|
this.Key = itemName;
|
||||||
|
this.Value = fieldName;
|
||||||
|
this.Operation = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FilterObject(string key, string? value, string? operation)
|
||||||
|
{
|
||||||
|
(string? itemName, string? fieldName) = FilterParser.ParseFilterId(key, value);
|
||||||
|
if (itemName == null)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(fieldName));
|
||||||
|
|
||||||
|
this.Key = itemName;
|
||||||
|
this.Value = fieldName;
|
||||||
|
this.Operation = GetOperation(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FilterObject(string key, string? value, Operation operation)
|
||||||
|
{
|
||||||
|
(string? itemName, string? fieldName) = FilterParser.ParseFilterId(key, value);
|
||||||
|
if (itemName == null)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(fieldName));
|
||||||
|
|
||||||
|
this.Key = itemName;
|
||||||
|
this.Value = fieldName;
|
||||||
|
this.Operation = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Derive an operation from the input string, if possible
|
||||||
|
/// </summary>
|
||||||
|
private static Operation GetOperation(string? operation)
|
||||||
|
{
|
||||||
|
return operation?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"=" => Operation.Equals,
|
||||||
|
"==" => Operation.Equals,
|
||||||
|
|
||||||
|
"!" => Operation.NotEquals,
|
||||||
|
"!=" => Operation.NotEquals,
|
||||||
|
|
||||||
|
">" => Operation.GreaterThan,
|
||||||
|
">=" => Operation.GreaterThanOrEqual,
|
||||||
|
|
||||||
|
"<" => Operation.LessThan,
|
||||||
|
"<=" => Operation.LessThanOrEqual,
|
||||||
|
|
||||||
|
_ => Operation.NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Matching
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine if a DictionaryBase object matches the key and value
|
||||||
|
/// </summary>
|
||||||
|
public bool Matches(DictionaryBase dictionaryBase)
|
||||||
|
{
|
||||||
|
return this.Operation switch
|
||||||
|
{
|
||||||
|
Operation.Equals => MatchesEqual(dictionaryBase),
|
||||||
|
Operation.NotEquals => MatchesNotEqual(dictionaryBase),
|
||||||
|
Operation.GreaterThan => MatchesGreaterThan(dictionaryBase),
|
||||||
|
Operation.GreaterThanOrEqual => MatchesGreaterThanOrEqual(dictionaryBase),
|
||||||
|
Operation.LessThan => MatchesLessThan(dictionaryBase),
|
||||||
|
Operation.LessThanOrEqual => MatchesLessThanOrEqual(dictionaryBase),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a value matches exactly
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>TODO: Add regex matching to this method</remarks>
|
||||||
|
private bool MatchesEqual(DictionaryBase dictionaryBase)
|
||||||
|
{
|
||||||
|
if (!dictionaryBase.ContainsKey(this.Key))
|
||||||
|
return this.Value == null;
|
||||||
|
|
||||||
|
string? checkValue = dictionaryBase.ReadString(this.Key);
|
||||||
|
return checkValue == this.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a value does not match exactly
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>TODO: Add regex matching to this method</remarks>
|
||||||
|
private bool MatchesNotEqual(DictionaryBase dictionaryBase)
|
||||||
|
{
|
||||||
|
if (!dictionaryBase.ContainsKey(this.Key))
|
||||||
|
return this.Value != null;
|
||||||
|
|
||||||
|
string? checkValue = dictionaryBase.ReadString(this.Key);
|
||||||
|
return checkValue != this.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a value is strictly greater than
|
||||||
|
/// </summary>
|
||||||
|
private bool MatchesGreaterThan(DictionaryBase dictionaryBase)
|
||||||
|
{
|
||||||
|
if (!dictionaryBase.ContainsKey(this.Key))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
long? checkLongValue = dictionaryBase.ReadLong(this.Key);
|
||||||
|
if (checkLongValue != null)
|
||||||
|
{
|
||||||
|
if (!long.TryParse(this.Value, out long matchValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return checkLongValue > matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? checkDoubleValue = dictionaryBase.ReadDouble(this.Key);
|
||||||
|
if (checkDoubleValue != null)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(this.Value, out double matchValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return checkDoubleValue > matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a value is greater than or equal
|
||||||
|
/// </summary>
|
||||||
|
private bool MatchesGreaterThanOrEqual(DictionaryBase dictionaryBase)
|
||||||
|
{
|
||||||
|
if (!dictionaryBase.ContainsKey(this.Key))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
long? checkLongValue = dictionaryBase.ReadLong(this.Key);
|
||||||
|
if (checkLongValue != null)
|
||||||
|
{
|
||||||
|
if (!long.TryParse(this.Value, out long matchValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return checkLongValue >= matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? checkDoubleValue = dictionaryBase.ReadDouble(this.Key);
|
||||||
|
if (checkDoubleValue != null)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(this.Value, out double matchValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return checkDoubleValue >= matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a value is strictly less than
|
||||||
|
/// </summary>
|
||||||
|
private bool MatchesLessThan(DictionaryBase dictionaryBase)
|
||||||
|
{
|
||||||
|
if (!dictionaryBase.ContainsKey(this.Key))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
long? checkLongValue = dictionaryBase.ReadLong(this.Key);
|
||||||
|
if (checkLongValue != null)
|
||||||
|
{
|
||||||
|
if (!long.TryParse(this.Value, out long matchValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return checkLongValue < matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? checkDoubleValue = dictionaryBase.ReadDouble(this.Key);
|
||||||
|
if (checkDoubleValue != null)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(this.Value, out double matchValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return checkDoubleValue < matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a value is less than or equal
|
||||||
|
/// </summary>
|
||||||
|
private bool MatchesLessThanOrEqual(DictionaryBase dictionaryBase)
|
||||||
|
{
|
||||||
|
if (!dictionaryBase.ContainsKey(this.Key))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
long? checkLongValue = dictionaryBase.ReadLong(this.Key);
|
||||||
|
if (checkLongValue != null)
|
||||||
|
{
|
||||||
|
if (!long.TryParse(this.Value, out long matchValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return checkLongValue <= matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? checkDoubleValue = dictionaryBase.ReadDouble(this.Key);
|
||||||
|
if (checkDoubleValue != null)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(this.Value, out double matchValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return checkDoubleValue <= matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,38 +12,50 @@ namespace SabreTools.Filter
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a filter ID string into the item name and field name, if possible
|
/// Parse a filter ID string into the item name and field name, if possible
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static (string?, string?) ParseFilterId(string filterId)
|
public static (string?, string?) ParseFilterId(string itemFieldString)
|
||||||
{
|
{
|
||||||
// If we don't have a filter ID, we can't do anything
|
// If we don't have a filter ID, we can't do anything
|
||||||
if (string.IsNullOrWhiteSpace(filterId))
|
if (string.IsNullOrWhiteSpace(itemFieldString))
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
// If we only have one part, we can't do anything
|
// If we only have one part, we can't do anything
|
||||||
string[] splitFilter = filterId.Split('.');
|
string[] splitFilter = itemFieldString.Split('.');
|
||||||
if (splitFilter.Length != 2)
|
if (splitFilter.Length != 2)
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
|
return ParseFilterId(splitFilter[0], splitFilter[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a filter ID string into the item name and field name, if possible
|
||||||
|
/// </summary>
|
||||||
|
public static (string?, string?) ParseFilterId(string itemName, string? fieldName)
|
||||||
|
{
|
||||||
|
// If we don't have a filter ID, we can't do anything
|
||||||
|
if (string.IsNullOrWhiteSpace(itemName) || string.IsNullOrWhiteSpace(fieldName))
|
||||||
|
return (null, null);
|
||||||
|
|
||||||
// Return santized values based on the split ID
|
// Return santized values based on the split ID
|
||||||
return splitFilter[0].ToLowerInvariant() switch
|
return itemName.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
// Header
|
// Header
|
||||||
"header" => ParseHeaderFilterId(splitFilter),
|
"header" => ParseHeaderFilterId(fieldName),
|
||||||
|
|
||||||
// Machine
|
// Machine
|
||||||
"game" => ParseMachineFilterId(splitFilter),
|
"game" => ParseMachineFilterId(fieldName),
|
||||||
"machine" => ParseMachineFilterId(splitFilter),
|
"machine" => ParseMachineFilterId(fieldName),
|
||||||
"resource" => ParseMachineFilterId(splitFilter),
|
"resource" => ParseMachineFilterId(fieldName),
|
||||||
"set" => ParseMachineFilterId(splitFilter),
|
"set" => ParseMachineFilterId(fieldName),
|
||||||
|
|
||||||
// DatItem
|
// DatItem
|
||||||
_ => ParseDatItemFilterId(splitFilter),
|
_ => ParseDatItemFilterId(itemName, fieldName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse and validate header fields
|
/// Parse and validate header fields
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static (string?, string?) ParseHeaderFilterId(string[] filterId)
|
private static (string?, string?) ParseHeaderFilterId(string fieldName)
|
||||||
{
|
{
|
||||||
// Get the set of constants
|
// Get the set of constants
|
||||||
var constants = GetConstants(typeof(Header));
|
var constants = GetConstants(typeof(Header));
|
||||||
@@ -51,7 +63,7 @@ namespace SabreTools.Filter
|
|||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
// Get if there's a match to the constant
|
// Get if there's a match to the constant
|
||||||
string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, filterId[1], StringComparison.OrdinalIgnoreCase));
|
string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, fieldName, StringComparison.OrdinalIgnoreCase));
|
||||||
if (constantMatch == null)
|
if (constantMatch == null)
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
@@ -62,7 +74,7 @@ namespace SabreTools.Filter
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse and validate machine/game fields
|
/// Parse and validate machine/game fields
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static (string?, string?) ParseMachineFilterId(string[] filterId)
|
private static (string?, string?) ParseMachineFilterId(string fieldName)
|
||||||
{
|
{
|
||||||
// Get the set of constants
|
// Get the set of constants
|
||||||
var constants = GetConstants(typeof(Machine));
|
var constants = GetConstants(typeof(Machine));
|
||||||
@@ -70,7 +82,7 @@ namespace SabreTools.Filter
|
|||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
// Get if there's a match to the constant
|
// Get if there's a match to the constant
|
||||||
string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, filterId[1], StringComparison.OrdinalIgnoreCase));
|
string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, fieldName, StringComparison.OrdinalIgnoreCase));
|
||||||
if (constantMatch == null)
|
if (constantMatch == null)
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
@@ -81,10 +93,10 @@ namespace SabreTools.Filter
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse and validate item fields
|
/// Parse and validate item fields
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static (string?, string?) ParseDatItemFilterId(string[] filterId)
|
private static (string?, string?) ParseDatItemFilterId(string itemName, string fieldName)
|
||||||
{
|
{
|
||||||
// Get the correct item type
|
// Get the correct item type
|
||||||
var itemType = GetDatItemType(filterId[0].ToLowerInvariant());
|
var itemType = GetDatItemType(itemName.ToLowerInvariant());
|
||||||
if (itemType == null)
|
if (itemType == null)
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
@@ -94,12 +106,12 @@ namespace SabreTools.Filter
|
|||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
// Get if there's a match to the constant
|
// Get if there's a match to the constant
|
||||||
string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, filterId[1], StringComparison.OrdinalIgnoreCase));
|
string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, fieldName, StringComparison.OrdinalIgnoreCase));
|
||||||
if (constantMatch == null)
|
if (constantMatch == null)
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
// Return the sanitized ID
|
// Return the sanitized ID
|
||||||
return (filterId[0].ToLowerInvariant(), constantMatch);
|
return (itemName.ToLowerInvariant(), constantMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
20
SabreTools.Filter/Operation.cs
Normal file
20
SabreTools.Filter/Operation.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace SabreTools.Filter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines what operation is being done
|
||||||
|
/// </summary>
|
||||||
|
public enum Operation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default value, does nothing
|
||||||
|
/// </summary>
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
Equals,
|
||||||
|
NotEquals,
|
||||||
|
GreaterThan,
|
||||||
|
GreaterThanOrEqual,
|
||||||
|
LessThan,
|
||||||
|
LessThanOrEqual,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,7 @@ namespace SabreTools.Models.Internal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T? Read<T>(string key)
|
public T? Read<T>(string key)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(key))
|
if (!ValidateKey(key))
|
||||||
return default;
|
|
||||||
if (!ContainsKey(key))
|
|
||||||
return default;
|
return default;
|
||||||
return (T?)this[key];
|
return (T?)this[key];
|
||||||
}
|
}
|
||||||
@@ -24,6 +22,9 @@ namespace SabreTools.Models.Internal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? ReadBool(string key)
|
public bool? ReadBool(string key)
|
||||||
{
|
{
|
||||||
|
if (!ValidateKey(key))
|
||||||
|
return null;
|
||||||
|
|
||||||
bool? asBool = Read<bool>(key);
|
bool? asBool = Read<bool>(key);
|
||||||
if (asBool != null)
|
if (asBool != null)
|
||||||
return asBool;
|
return asBool;
|
||||||
@@ -44,6 +45,9 @@ namespace SabreTools.Models.Internal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double? ReadDouble(string key)
|
public double? ReadDouble(string key)
|
||||||
{
|
{
|
||||||
|
if (!ValidateKey(key))
|
||||||
|
return null;
|
||||||
|
|
||||||
double? asDouble = Read<double>(key);
|
double? asDouble = Read<double>(key);
|
||||||
if (asDouble != null)
|
if (asDouble != null)
|
||||||
return asDouble;
|
return asDouble;
|
||||||
@@ -60,6 +64,9 @@ namespace SabreTools.Models.Internal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public long? ReadLong(string key)
|
public long? ReadLong(string key)
|
||||||
{
|
{
|
||||||
|
if (!ValidateKey(key))
|
||||||
|
return null;
|
||||||
|
|
||||||
long? asLong = Read<long>(key);
|
long? asLong = Read<long>(key);
|
||||||
if (asLong != null)
|
if (asLong != null)
|
||||||
return asLong;
|
return asLong;
|
||||||
@@ -76,6 +83,9 @@ namespace SabreTools.Models.Internal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ReadString(string key)
|
public string? ReadString(string key)
|
||||||
{
|
{
|
||||||
|
if (!ValidateKey(key))
|
||||||
|
return null;
|
||||||
|
|
||||||
string? asString = Read<string>(key);
|
string? asString = Read<string>(key);
|
||||||
if (asString != null)
|
if (asString != null)
|
||||||
return asString;
|
return asString;
|
||||||
@@ -84,7 +94,7 @@ namespace SabreTools.Models.Internal
|
|||||||
if (asArray != null)
|
if (asArray != null)
|
||||||
return string.Join(',', asArray);
|
return string.Join(',', asArray);
|
||||||
|
|
||||||
return null;
|
return this[key]!.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -92,6 +102,9 @@ namespace SabreTools.Models.Internal
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string[]? ReadStringArray(string key)
|
public string[]? ReadStringArray(string key)
|
||||||
{
|
{
|
||||||
|
if (!ValidateKey(key))
|
||||||
|
return null;
|
||||||
|
|
||||||
string[]? asArray = Read<string[]>(key);
|
string[]? asArray = Read<string[]>(key);
|
||||||
if (asArray != null)
|
if (asArray != null)
|
||||||
return asArray;
|
return asArray;
|
||||||
@@ -100,7 +113,26 @@ namespace SabreTools.Models.Internal
|
|||||||
if (asString != null)
|
if (asString != null)
|
||||||
return new string[] { asString };
|
return new string[] { asString };
|
||||||
|
|
||||||
|
asString = this[key]!.ToString();
|
||||||
|
if (asString != null)
|
||||||
|
return new string[] { asString };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a key is valid
|
||||||
|
/// </summary>
|
||||||
|
private bool ValidateKey(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
return false;
|
||||||
|
else if (!ContainsKey(key))
|
||||||
|
return false;
|
||||||
|
else if (this[key] == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user