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 }; } } }