using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums.Store; namespace RadicadeServer.Utils { public static class LevelUpRewardPool { public const string ConfigFileName = "levelup-rewards.json"; public sealed class Pool { public List AvatarItems { get; set; } = new(); public List Consumables { get; set; } = new(); } private static readonly string[] DeniedPatterns = [ "exclusive", "early access", "alpha", "art festival", "rec con", "reccon", "rrcc", "raw data", "sfvrcc", "contest", "trophy", "competition", "moderator", "volunteer", "video partner", "creator program", "partnered creator", "partner", "bug bounty", "security", "ethical hackers", "gold notes", "seller swag", "sponsored league", "rro-", "rro specific", "promo", "promotional", "sponsor", "collab", "licensed", "prom", "mrbeast", "mr beast", "miraculous", "nba", "class of", "graduation", "class)", "creator conference", "for earning", "available until", "unlocked by visiting", "unlocked during", "xbox", "launch reward", "launch exclusive", "reward for", "given to", "space marine", "space visor", "(gold)", "limited", "never return", "founder", "founders", "halloween", "holiday", "thanksgiving", "invasion", "elseware", "new year", "carnivale", "reclympics", "rec rocks", "anniversary", "2020 glasses", "developer", "dev only", "staff only", "admin only", "test item", "debug", ]; private static readonly string[] DeniedTags = [ "halloween", "holidays", "thanksgiving", "invasion", "elseware", "reccon", "reccon2021", ]; private static Pool? _cached; private static string? _cachedFrom; public static void ResetCache() { _cached = null; _cachedFrom = null; } public static Pool Load(string? json = null) { if (json != null) return Parse(json); if (_cached != null) return _cached; foreach (var directory in DBConfig.AvatarCatalogDirectories()) { var path = Path.Join(directory, ConfigFileName); if (!File.Exists(path)) continue; try { _cached = Parse(File.ReadAllText(path)); _cachedFrom = path; return _cached; } catch (Exception) { } } _cached = new Pool(); return _cached; } public static string? LoadedFrom => _cachedFrom; private static Pool Parse(string json) { var pool = json.FromJson() ?? new Pool(); pool.AvatarItems = Clean(pool.AvatarItems); pool.Consumables = Clean(pool.Consumables); return pool; } private static List Clean(IEnumerable? values) => (values ?? Enumerable.Empty()) .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); public static bool IsAlwaysDenied(string? name, string? tooltip, string? tag = null) { return Matches(name) || Matches(tooltip) || MatchesTag(tag); static bool Matches(string? text) { if (string.IsNullOrWhiteSpace(text)) return false; return DeniedPatterns.Any(pattern => text.Contains(pattern, StringComparison.OrdinalIgnoreCase)); } static bool MatchesTag(string? text) { if (string.IsNullOrWhiteSpace(text)) return false; var tags = text.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); return tags.Any(tag => DeniedTags.Contains(tag, StringComparer.OrdinalIgnoreCase)); } } public static bool IsAlwaysDenied(AvatarItem item) => item != null && IsAlwaysDenied(item.FriendlyName, item.Tooltip, item.TagList); public static bool IsAlwaysDenied(ConsumableItem item) => item != null && IsAlwaysDenied(item.FriendlyName, item.Tooltip); public static string OwnershipKey(string? description) => AvatarItemDescription.Normalize(description).Trim().ToLowerInvariant(); public static AvatarItem? PickAvatarItem( IEnumerable catalogue, Rarity rarity, ISet owned, Random random, Pool? pool = null) { var allowed = (pool ?? Load()).AvatarItems.ToHashSet(StringComparer.OrdinalIgnoreCase); if (allowed.Count == 0) return null; var candidates = catalogue .Where(item => item != null && !string.IsNullOrWhiteSpace(item.AvatarItemDesc) && !item.IsBaseAvatarItem && item.Rarity == (int)rarity && allowed.Contains(item.AvatarItemDesc) && !IsAlwaysDenied(item) && !owned.Contains(OwnershipKey(item.AvatarItemDesc))) .OrderBy(item => item.AvatarItemDesc, StringComparer.Ordinal) .ToList(); return candidates.Count == 0 ? null : candidates[random.Next(candidates.Count)]; } public static ConsumableItem? PickConsumable( IEnumerable catalogue, ISet owned, Random random, Pool? pool = null) { var configured = (pool ?? Load()).Consumables.ToHashSet(StringComparer.OrdinalIgnoreCase); var candidates = catalogue .Where(item => item != null && !string.IsNullOrWhiteSpace(item.ConsumableItemDesc) && (configured.Count == 0 || configured.Contains(item.ConsumableItemDesc)) && !IsAlwaysDenied(item) && !owned.Contains(OwnershipKey(ConsumableCatalog.ToClientDesc(item.ConsumableItemDesc)))) .OrderBy(item => item.ConsumableItemDesc, StringComparer.Ordinal) .ToList(); return candidates.Count == 0 ? null : candidates[random.Next(candidates.Count)]; } } }