namespace Supercell.Laser.Bot.Utils
{
using System;
/// Что не так (или так) с тэгом, который ввёл пользователь.
public enum TagStatus
{
/// Тэг корректный, AccountId посчитан.
Valid,
/// Пустой / слишком длинный тэг или недопустимые символы.
Invalid,
///
/// В тэге есть буква "O" — самая частая опечатка: в тэгах Brawl Stars
/// используется только цифра "0", буквы "O" в алфавите тэгов нет вообще.
///
LetterOInsteadOfZero
}
/// Результат : статус + нормализованный тэг + id аккаунта.
public readonly struct TagCheck
{
public readonly TagStatus Status;
/// Нормализованный тэг (заглавные буквы, без '#' и пробелов). Может быть null.
public readonly string Tag;
/// Id аккаунта в базе. Заполнен только при .
public readonly long AccountId;
public TagCheck(TagStatus status, string tag, long accountId)
{
Status = status;
Tag = tag;
AccountId = accountId;
}
public bool IsValid => Status == TagStatus.Valid;
/// Готовое сообщение об ошибке для пользователя (null, если тэг верный).
public string ErrorMessage => LogicTag.ErrorFor(Status);
}
///
/// Всё, что связано с тэгами игроков: проверка введённого пользователем тэга,
/// перевод тэга в id аккаунта и обратно, и тексты ошибок для бота.
///
/// Алгоритм повторяет Supercell.Laser.Logic.Util.LogicLongCodeGenerator (то есть
/// то же самое, что делает клиент игры), но живёт здесь отдельно: Supercell.Laser.Bot
/// не ссылается на Supercell.Laser.Logic (это сервер ссылается на бота, обратная
/// ссылка дала бы цикл).
///
/// Используется не только выдачей ресурсов — сюда же пойдёт привязка аккаунтов
/// по тэгу и любые будущие команды, которым нужен тэг.
///
public static class LogicTag
{
/// Алфавит тэгов. Обрати внимание: цифра "0" есть, буквы "O" — нет.
private const string ConversionChars = "0289PYLQGRJCUV";
///
/// Максимум символов в тэге без '#'. Клиент отбрасывает код длиной 14 символов
/// и больше вместе с решёткой, поэтому у самого тэга остаётся 12 символов.
///
private const int MaxTagLength = 12;
public const string EnterTagMessage = "❔Введите тэг игрока:";
public const string InvalidTagMessage = "❌ Неправильный тэг, введите еще раз:\n\nПример: #2PP";
public const string LetterOMessage =
"❌ Вы ввели неверный тэг (Нужно использовать цифру \"0\", а не букву \"O\"), попробуйте еще раз:";
public const string PlayerNotFoundMessage = "❌ Такого игрока нету в базе данных, повторите еще раз:";
///
/// Проверяет тэг, введённый пользователем, и сразу считает id аккаунта.
/// Наличие аккаунта в базе здесь НЕ проверяется — это дело вызывающего
/// (см. ).
///
public static TagCheck Check(string input)
{
string tag = Normalize(input);
if (string.IsNullOrEmpty(tag))
return new TagCheck(TagStatus.Invalid, null, 0);
// Проверяем "O" раньше остальных ошибок: пользователю полезнее узнать
// про конкретную опечатку, а не общее "неправильный тэг".
if (tag.IndexOf('O') >= 0)
return new TagCheck(TagStatus.LetterOInsteadOfZero, tag, 0);
if (tag.Length > MaxTagLength)
return new TagCheck(TagStatus.Invalid, tag, 0);
long accountId = ToId(tag);
if (accountId <= 0)
return new TagCheck(TagStatus.Invalid, tag, 0);
return new TagCheck(TagStatus.Valid, tag, accountId);
}
///
/// Приводит ввод к виду, с которым можно работать: убирает пробелы и '#',
/// поднимает регистр. Терпит и "#2PP", и " 2pp ", и "2 PP".
///
public static string Normalize(string input)
{
if (string.IsNullOrWhiteSpace(input)) return null;
var buffer = new char[input.Length];
int length = 0;
foreach (char c in input)
{
if (char.IsWhiteSpace(c) || c == '#') continue;
buffer[length++] = char.ToUpperInvariant(c);
}
return length == 0 ? null : new string(buffer, 0, length);
}
/// Тэг (без '#') -> id аккаунта в базе. Возвращает -1, если тэг не разбирается.
public static long ToId(string tag)
{
if (string.IsNullOrEmpty(tag) || tag.Length > MaxTagLength) return -1;
long value = 0;
foreach (char c in tag)
{
int index = ConversionChars.IndexOf(c);
if (index < 0) return -1;
value = value * ConversionChars.Length + index;
}
// Раскладка LogicLong как у клиента: младший байт кода -> старшая половина,
// остальные биты -> младшая. Дальше две половины склеиваются в long,
// ровно как implicit operator long(LogicLong).
int high = (int)(value % 256);
int low = (int)((value >> 8) & 0x7FFFFFFF);
return ((long)high << 32) | (uint)low;
}
/// Id аккаунта -> тэг без '#'. null, если id вне диапазона тэгов.
public static string ToTag(long accountId)
{
int high = (int)(accountId >> 32);
int low = (int)accountId;
// Клиент умеет кодировать только high < 256 — на больших id тэга просто нет.
if (high < 0 || high >= 256) return null;
long value = ((long)low << 8) | (uint)high;
if (value < 0) return null;
char[] code = new char[MaxTagLength];
for (int i = MaxTagLength - 1; i >= 0; i--)
{
code[i] = ConversionChars[(int)(value % ConversionChars.Length)];
value /= ConversionChars.Length;
if (value == 0)
return new string(code, i, MaxTagLength - i);
}
return new string(code);
}
/// Тэг с решёткой для вывода в сообщениях: "2PP" -> "#2PP".
public static string WithHash(string tag)
{
if (string.IsNullOrEmpty(tag)) return "#";
return tag[0] == '#' ? tag : "#" + tag;
}
/// Текст ошибки под конкретный статус (null для корректного тэга).
public static string ErrorFor(TagStatus status)
{
return status switch
{
TagStatus.LetterOInsteadOfZero => LetterOMessage,
TagStatus.Invalid => InvalidTagMessage,
_ => null
};
}
}
}